Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive music streaming application feature set. It adds user authentication (login/signup), music search and library management, audio playback, liked songs tracking, theme switching, and Redux-Saga state management orchestration across 50+ new files. Changes
Sequence DiagramssequenceDiagram
participant User
participant UI as LoginForm/SignupForm
participant Redux
participant Saga
participant API as authApi
participant Storage
participant Router
User->>UI: Enter credentials & submit
UI->>Redux: dispatch(requestLogin/requestSignup)
Redux->>Saga: Saga watches REQUEST_LOGIN/SIGNUP
Saga->>API: call loginUser/signupUser(credentials)
API-->>Saga: response with accessToken
Saga->>Storage: setStoredToken(accessToken)
Saga->>Redux: setAuthHeader(token)
Saga->>Redux: dispatch(successAuth(data))
Redux-->>UI: Update loading=false
UI->>Router: navigate to /music
sequenceDiagram
participant User
participant SearchBar
participant Redux
participant Saga
participant API as musicApi
participant SongList
User->>SearchBar: Type search term (debounced)
SearchBar->>Redux: dispatch(requestSearchSongs(term))
Redux->>Saga: Saga watches REQUEST_SEARCH_SONGS
Saga->>API: call searchSongs(term)
API-->>Saga: response with songs array
Saga->>Redux: dispatch(successSearchSongs(songs))
Redux->>SongList: Update songs in state
SongList-->>User: Render matching songs
sequenceDiagram
participant User
participant SongList
participant AudioPlayer
participant useAudioPlayer
participant HTMLAudio as HTMLAudioElement
User->>SongList: Click song / use next/prev
SongList->>Redux: dispatch(setCurrentSong)
Redux->>AudioPlayer: currentSong prop updated
AudioPlayer->>useAudioPlayer: song changed (trackId)
useAudioPlayer->>HTMLAudio: Create/load audio src
useAudioPlayer->>HTMLAudio: Call play()
HTMLAudio-->>useAudioPlayer: timeupdate/metadata events
useAudioPlayer-->>AudioPlayer: Update progress/duration state
AudioPlayer-->>User: Display playback UI
User->>AudioPlayer: Click play/pause or heart
AudioPlayer->>useAudioPlayer: togglePlay() or seek()
useAudioPlayer->>HTMLAudio: pause() or set currentTime
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @aman-wednesdaysol, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly expands the application's core functionality by introducing a robust music streaming experience. It includes user authentication, music search, a personal song library, and an interactive audio player. Additionally, it enhances the user interface with a theme toggling feature and a suite of new, styled components, all while adhering to new code and aesthetic guidelines. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request introduces a comprehensive music library feature, including authentication, song search, and a playback system. The implementation follows a modular structure with dedicated containers, components, and services. However, there are several critical issues to address: hardcoded API URLs that will break in production, a potential audio leak where music continues playing after a song is cleared, and significant accessibility concerns in the dark theme due to low color contrast. Additionally, several files exceed the 100-line limit established in the project's code rules, and the test configuration automatically updates snapshots, which can lead to undetected regressions.
| case 'auth': | ||
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000'); | ||
| return apiClients[type]; | ||
| case 'music': | ||
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true }); | ||
| return apiClients[type]; |
There was a problem hiding this comment.
The API base URL is hardcoded as http://localhost:9000. This will cause the application to fail in production and QA environments. It should be retrieved from environment variables, similar to how the GitHub URL is handled.
| case 'auth': | |
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000'); | |
| return apiClients[type]; | |
| case 'music': | |
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true }); | |
| return apiClients[type]; | |
| case 'auth': | |
| apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:9000'); | |
| return apiClients[type]; | |
| case 'music': | |
| apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_MUSIC_URL || 'http://localhost:9000', { skipRequestTransform: true }); | |
| return apiClients[type]; |
| if (song?.previewUrl && audioRef.current) { | ||
| audioRef.current.src = song.previewUrl; | ||
| audioRef.current.play().catch(() => {}); | ||
| setIsPlaying(true); | ||
| } |
There was a problem hiding this comment.
When the song prop becomes null (e.g., when a user clears the current selection), the audio continues to play in the background because there is no logic to pause it when the condition fails. This results in a poor user experience and a resource leak.
if (song?.previewUrl && audioRef.current) {
audioRef.current.src = song.previewUrl;
audioRef.current.play().catch(() => {});
setIsPlaying(true);
} else if (audioRef.current) {
audioRef.current.pause();
audioRef.current.src = '';
setIsPlaying(false);
}| ...shared, | ||
| bg: '#0d0d0d', | ||
| cardBg: '#161622', | ||
| inputBg: '#d2d2db', |
There was a problem hiding this comment.
In the darkPalette, inputBg is set to a light color (#d2d2db), while the text color is white. This creates a significant accessibility issue with extremely low contrast, making the input text nearly unreadable. The input background should be a dark color in the dark theme.
| inputBg: '#d2d2db', | |
| inputBg: '#1a1a2e', |
| @@ -0,0 +1,130 @@ | |||
| import React, { useState, useCallback, useEffect } from 'react'; | |||
There was a problem hiding this comment.
This file is 130 lines long, which violates the 'Max file length: 100 lines' rule defined in CLAUDE.md. Consider extracting the header or the search logic into separate components to improve maintainability.
References
- Max file length: 100 lines — break into smaller files if exceeded (link)
| "npm run lint:eslint:fix", | ||
| "git add --force", | ||
| "jest --findRelatedTests --passWithNoTests $STAGED_FILES" | ||
| "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES" |
There was a problem hiding this comment.
The -u flag in the pre-commit hook automatically updates Jest snapshots. This is dangerous as it can lead to committing broken snapshots without developer review, defeating the purpose of snapshot testing.
| "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES" | |
| "jest --findRelatedTests --passWithNoTests $STAGED_FILES" |
| box-shadow: 0 0 20px rgba(255, 107, 53, 0.15); | ||
| } | ||
| &:hover:not(:focus) { | ||
| border-color: #3d3d54; |
There was a problem hiding this comment.
The border-color is hardcoded as #3d3d54. According to the code rules in CLAUDE.md, constants and shared logic should be extracted. This color should be part of the theme palette or a shared constant.
| border-color: #3d3d54; | |
| border-color: ${C.surface}; |
References
- Keep constants in a separate constants file (link)
| useEffect(() => { | ||
| dispatchFetchLibrary(); | ||
| }, []); |
There was a problem hiding this comment.
The debounced search function should be cancelled when the component unmounts to prevent potential memory leaks or state updates on an unmounted component.
| useEffect(() => { | |
| dispatchFetchLibrary(); | |
| }, []); | |
| useEffect(() => { | |
| dispatchFetchLibrary(); | |
| return () => debouncedSearch.cancel(); | |
| }, []); |
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (24)
environments/.env.qa-2-3 (1)
2-3:⚠️ Potential issue | 🟡 Minor
NEXT_PUBLIC_POSTHOG_HOSTshould be ordered beforeNEXT_PUBLIC_POSTHOG_KEY, and a trailing newline is missing.Both issues are flagged by
dotenv-linter.🔧 Proposed fix
NEXT_PUBLIC_GITHUB_URL=https://api.github.com/ -NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@environments/.env.qa` around lines 2 - 3, Reorder the two environment variables so NEXT_PUBLIC_POSTHOG_HOST appears before NEXT_PUBLIC_POSTHOG_KEY and add a trailing newline at the end of the file to satisfy dotenv-linter; specifically move the line with NEXT_PUBLIC_POSTHOG_HOST above the line with NEXT_PUBLIC_POSTHOG_KEY and ensure the file ends with a newline character.environments/.env.development-2-3 (1)
2-3:⚠️ Potential issue | 🟡 Minor
NEXT_PUBLIC_POSTHOG_HOSTshould be ordered beforeNEXT_PUBLIC_POSTHOG_KEY, and a trailing newline is missing.Same
dotenv-linterfindings as in.env.qa.🔧 Proposed fix
NEXT_PUBLIC_GITHUB_URL=https://api.github.com/ -NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@environments/.env.development` around lines 2 - 3, Reorder the environment entries so NEXT_PUBLIC_POSTHOG_HOST appears before NEXT_PUBLIC_POSTHOG_KEY and add a trailing newline at the end of the file; specifically, update the .env entries involving NEXT_PUBLIC_POSTHOG_HOST and NEXT_PUBLIC_POSTHOG_KEY to swap their order and ensure the file ends with a newline character so dotenv-linter passes.environments/.env.production-2-3 (1)
2-3:⚠️ Potential issue | 🟡 Minor
NEXT_PUBLIC_POSTHOG_HOSTshould be ordered beforeNEXT_PUBLIC_POSTHOG_KEY, and a trailing newline is missing.Same
dotenv-linterfindings as in the other env files.🔧 Proposed fix
NEXT_PUBLIC_GITHUB_URL=https://api.github.com/ -NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@environments/.env.production` around lines 2 - 3, Reorder the two environment variables so NEXT_PUBLIC_POSTHOG_HOST appears before NEXT_PUBLIC_POSTHOG_KEY and add a trailing newline at the end of the file; specifically swap the positions of NEXT_PUBLIC_POSTHOG_HOST and NEXT_PUBLIC_POSTHOG_KEY in environments/.env.production so the HOST key comes first and ensure the file ends with a newline character.app/components/styled/musicSearch.js-29-31 (1)
29-31:⚠️ Potential issue | 🟡 MinorHard-coded
#3d3d54bypasses the theme system and will break in light mode.All other color usages in this file correctly reference
Ctokens, but the hover border state uses a fixed dark-mode-only hex value. In light mode, this will render an out-of-place dark border on hover.🐛 Proposed fix
&:hover:not(:focus) { - border-color: `#3d3d54`; + border-color: ${C.border}; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/musicSearch.js` around lines 29 - 31, The hover rule &:hover:not(:focus) in the styled component uses a hard-coded hex `#3d3d54` which bypasses the theme and breaks light mode; replace that hex with the appropriate theme color token from the file’s C tokens (use the same C token used for other borders in this component) so the hover border color respects light/dark themes.app/utils/authStorage.js-12-17 (1)
12-17:⚠️ Potential issue | 🟡 Minor
setStoredToken(null)silently stores the string"null"instead of clearing the token.
localStorage.setItem(key, null)coercesnullto the string"null", makinggetStoredToken()return a truthy"null"string afterwards. If any call site invokessetStoredToken(null)to "clear" the token, auth checks comparing the result tonull/falsy will break. Add a guard or document the expected call contract explicitly.🛡️ Proposed fix
export const setStoredToken = (token) => { if (!isBrowser()) { return; } + if (token == null) { + localStorage.removeItem(TOKEN_KEY); + return; + } localStorage.setItem(TOKEN_KEY, token); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/utils/authStorage.js` around lines 12 - 17, The function setStoredToken currently writes the literal "null" when passed null; change setStoredToken to guard for null/undefined/empty values and call localStorage.removeItem(TOKEN_KEY) in that case (while retaining the existing isBrowser() early return), otherwise call localStorage.setItem(TOKEN_KEY, token); update any related behavior expectations in getStoredToken if needed so callers can rely on null/absence when token is cleared.app/containers/Music/usePlaybackNav.js-6-11 (1)
6-11:⚠️ Potential issue | 🟡 Minor
handleNextdispatchessongs[0]whencurrentSongis absent or not matched.When
currentSongisnullor itstrackIdis not insongs,findIdx()returns-1. The guardidx < songs.length - 1evaluates totruefor any non-empty list, sodispatchSetSong(songs[-1 + 1])— i.e.songs[0]— is called. This is likely unintended; a "Next" press with no active song silently jumps to track 1. Add a guard:🐛 Proposed fix
const handleNext = useCallback(() => { const idx = findIdx(); - if (idx < songs.length - 1) { + if (idx !== -1 && idx < songs.length - 1) { dispatchSetSong(songs[idx + 1]); } }, [songs, findIdx, dispatchSetSong]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/Music/usePlaybackNav.js` around lines 6 - 11, The handleNext function can call dispatchSetSong(songs[0]) when findIdx() returns -1; update handleNext to early-return when idx is -1 or when songs is empty so it only advances when the current song is found and not already at the last index. Specifically, inside handleNext (which uses findIdx, songs, and dispatchSetSong) add a guard like "if (idx === -1 || songs.length === 0) return;" before the existing idx < songs.length - 1 check so you never advance from a missing/unmatched currentSong.app/components/NavLink/tests/index.test.js-18-23 (1)
18-23:⚠️ Potential issue | 🟡 Minor"should render with active styling prop" doesn't actually assert any styling.
data-testidis derived fromlabel.toLowerCase()and is unaffected byisActive, sogetByTestId('nav-search')would pass regardless of whetherisActiveis wired up at all. Consider asserting on the computed style or an attribute that reflects the active state (e.g.,aria-current, inline style, or a class name).🛡️ Example: asserting the active border style
it('should render with active styling prop', () => { - const { getByTestId } = renderProvider( - <NavLink href='/' label='Search' isActive /> - ) - expect(getByTestId('nav-search')).toBeTruthy() + const { getByTestId } = renderProvider( + <NavLink href='/' label='Search' isActive /> + ) + const el = getByTestId('nav-search') + expect(el).toBeInTheDocument() + // Verify the active border-bottom style is applied + expect(el).toHaveStyle('font-weight: 700') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/NavLink/tests/index.test.js` around lines 18 - 23, The test "should render with active styling prop" currently only checks existence via getByTestId and doesn't validate that NavLink's isActive prop affects output; update the test for the NavLink component to assert an active indicator instead of just presence — e.g., render <NavLink href='/' label='Search' isActive /> then getByTestId('nav-search') and assert either element.hasAttribute('aria-current') with the expected value (e.g., 'page'), or that element.classList contains the active class name your component uses (e.g., 'active'), or that window.getComputedStyle(element) matches the expected active border/style; pick the attribute/class/style that NavLink implements and replace the existing expect with that assertion.app/containers/Music/tests/usePlaybackNav.test.js-10-61 (1)
10-61:⚠️ Potential issue | 🟡 MinorAdd a test for the
currentSongnot-found edge case.There's no test covering when
currentSongisnullor itstrackIddoesn't match any entry insongs(i.e.findIdx()returns-1). As shown in the hook analysis, this currently causeshandleNextto silently dispatchsongs[0]. A test documenting the intended behavior (no-op) will both catch the current bug and guard against regressions after the fix.➕ Suggested additional tests
+ it('should not dispatch when currentSong is null (handleNext)', () => { + const { result } = renderHook(() => + usePlaybackNav({ + songs, + currentSong: null, + dispatchSetSong: mockSetSong + }) + ) + act(() => result.current.handleNext()) + expect(mockSetSong).not.toHaveBeenCalled() + }) + + it('should not dispatch when currentSong is null (handlePrev)', () => { + const { result } = renderHook(() => + usePlaybackNav({ + songs, + currentSong: null, + dispatchSetSong: mockSetSong + }) + ) + act(() => result.current.handlePrev()) + expect(mockSetSong).not.toHaveBeenCalled() + })Do you want me to open a new issue to track adding these edge-case tests?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/Music/tests/usePlaybackNav.test.js` around lines 10 - 61, Add a test to cover the currentSong-not-found edge case for usePlaybackNav: simulate currentSong being null and another case where currentSong.trackId doesn't exist in songs (so findIdx returns -1), call result.current.handleNext() and handlePrev() and assert dispatchSetSong (mockSetSong) is not called in both cases; this documents intended no-op behavior and will catch the existing bug where handleNext can incorrectly dispatch songs[0].app/utils/authStorage.js-5-24 (1)
5-24:⚠️ Potential issue | 🟡 MinorMissing error handling for
localStoragefailures.
localStorageoperations can throw (e.g.,QuotaExceededErrorwhen storage is full, orSecurityErrorwhen access is denied by browser policy in some private-mode or iframe configurations). Wrapping each operation in atry/catchprevents uncaught exceptions from crashing the app.🛡️ Proposed fix
export const getStoredToken = () => { if (!isBrowser()) return null; - return localStorage.getItem(TOKEN_KEY); + try { + return localStorage.getItem(TOKEN_KEY); + } catch { + return null; + } }; export const setStoredToken = (token) => { if (!isBrowser()) return; - localStorage.setItem(TOKEN_KEY, token); + try { + localStorage.setItem(TOKEN_KEY, token); + } catch { + // Storage quota exceeded or access denied — fail silently + } }; export const clearStoredToken = () => { if (!isBrowser()) return; - localStorage.removeItem(TOKEN_KEY); + try { + localStorage.removeItem(TOKEN_KEY); + } catch { + // Ignore — token will expire naturally + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/utils/authStorage.js` around lines 5 - 24, The localStorage calls in getStoredToken, setStoredToken, and clearStoredToken can throw (QuotaExceededError, SecurityError); wrap each localStorage access in a try/catch: in getStoredToken catch and return null on error, in setStoredToken and clearStoredToken catch and no-op (optionally log the error via console.warn or a provided logger) so the app won’t crash; update the functions getStoredToken, setStoredToken, and clearStoredToken to use these try/catch guards around localStorage.getItem, setItem, and removeItem respectively.app/components/styled/playerBar.js-81-97 (1)
81-97:⚠️ Potential issue | 🟡 MinorRange slider styling is WebKit-only; Firefox users will see unstyled controls.
Both
ProgressSliderandVolumeSlideronly declare::-webkit-slider-thumb. Firefox renders<input type="range">through::-moz-range-trackand::-moz-range-thumb; without these, the thumb color/size and track appearance fall back to the browser default, clashing with the app's theme.♻️ Proposed fix (apply to both `ProgressSlider` and `VolumeSlider`)
&::-webkit-slider-thumb { appearance: none; width: 12px; height: 12px; border-radius: 50%; background: ${C.accent}; cursor: pointer; } + &::-moz-range-track { + background: ${C.border}; + height: 4px; + border-radius: 2px; + } + &::-moz-range-thumb { + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: ${C.accent}; + cursor: pointer; + border: none; + }Apply the same pattern to
VolumeSlidersubstitutingC.pinkand the10pxthumb dimensions.Also applies to: 106-122
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/playerBar.js` around lines 81 - 97, ProgressSlider and VolumeSlider only style WebKit thumbs, leaving Firefox to render default controls; add matching Firefox pseudo-elements to both components by defining ::-moz-range-thumb (use same dimensions, border-radius and background as the existing ::-webkit-slider-thumb: for ProgressSlider use C.accent, for VolumeSlider use C.pink and 10px thumb size) and ::-moz-range-track (apply the same track background, height and border-radius as the base input styles) so Firefox matches the app theme; keep the existing ::-webkit-* rules and ensure both -webkit and -moz rules use consistent sizes and cursor settings.app/components/styled/playerBar.js-1-122 (1)
1-122:⚠️ Potential issue | 🟡 MinorFile exceeds the 100-line limit defined in
CLAUDE.md.At 122 lines,
playerBar.jsviolates the project's own guideline ("Max file length: 100 lines — break into smaller files if exceeded"). Consider splitting slider utilities (ProgressSlider,VolumeSlider) into a separatesliders.jsstyled module.As per coding guidelines: "Max file length: 100 lines — break into smaller files if exceeded"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/playerBar.js` around lines 1 - 122, This file exceeds the 100-line limit; extract the slider styled-components into a new module and import them back to reduce playerBar.js length. Specifically, move ProgressSlider and VolumeSlider (and any shared slider styles like ::-webkit-slider-thumb rules) into a new styled module (e.g., sliders.js) that exports ProgressSlider and VolumeSlider, update playerBar.js to import { ProgressSlider, VolumeSlider } instead of defining them inline, and ensure exports/names (PlayerContainer, NowPlayingArt, PlayerTrackInfo, TrackTitle, TrackArtist, PlayerControls, ControlButton, VolumeGroup) remain unchanged so consumers are unaffected. Ensure relative imports and C color references are preserved and run lint/tests to confirm no breakages.app/components/SearchBar/index.js-18-22 (1)
18-22:⚠️ Potential issue | 🟡 Minor
valueshould be required or have a default to avoid uncontrolled/controlled switching.With
valueoptional and nodefaultProps, a caller omitting the prop passesundefinedto<input value={undefined}>, making it uncontrolled. Ifvalueis later provided, React warns about switching modes.🛡️ Proposed fix (add `isRequired`)
SearchBar.propTypes = { - value: PropTypes.string, + value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, loading: PropTypes.bool };Alternatively, add
SearchBar.defaultProps = { value: '' }if an optional prop is preferred.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/SearchBar/index.js` around lines 18 - 22, The SearchBar component's value prop is optional in SearchBar.propTypes which can cause React to switch between uncontrolled and controlled inputs; fix by either making value required in SearchBar.propTypes (add isRequired to value) or by defining SearchBar.defaultProps = { value: '' } so the input always receives a string; update the SearchBar.propTypes/value or add the SearchBar.defaultProps entry accordingly.CLAUDE.md-13-33 (1)
13-33:⚠️ Potential issue | 🟡 MinorWrap
DISTILLED_AESTHETICS_PROMPTin a fenced code block or remove the Python-style assignment.The variable assignment and
"""triple-quotes are Python syntax, but this is a.mdfile. GitHub renders these as raw prose, making the labeling confusing. If it's meant as a named prompt, use a Markdown heading and fence block instead.♻️ Proposed fix
-DISTILLED_AESTHETICS_PROMPT = """ -<frontend_aesthetics> -... -</frontend_aesthetics> -""" +### Aesthetics Prompt (`DISTILLED_AESTHETICS_PROMPT`) + +```xml +<frontend_aesthetics> +... +</frontend_aesthetics> +```🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` around lines 13 - 33, The MD contains a Python-style variable assignment DISTILLED_AESTHETICS_PROMPT and triple-quoted string which renders poorly in Markdown; remove the Python assignment and surrounding triple quotes and instead present the content as a proper Markdown block by adding a descriptive heading (e.g., "DISTILLED_AESTHETICS_PROMPT" or "Frontend Aesthetics") and placing the XML-like <frontend_aesthetics>…</frontend_aesthetics> content inside a fenced code block (```xml or ```) so it renders correctly as a named prompt/example; ensure the symbol DISTILLED_AESTHETICS_PROMPT is no longer used as code in the .md file.app/components/styled/authForm.js-53-53 (1)
53-53:⚠️ Potential issue | 🟡 MinorHardcoded color values bypass the theming system.
Lines 53, 56, and 80 use raw hex/rgba values instead of the
C.*token system. In light/dark mode,C.accentand other tokens update via CSS custom properties, but these hardcoded values stay fixed — directly relevant to the UX-in-light/dark-mode bugs mentioned in this PR.
- Line 53:
rgba(255, 107, 53, 0.15)is the RGB decomposition ofC.accent- Line 56:
#3d3d54is a dark-mode-specific color with no light-mode equivalent in the palette- Line 80:
rgba(255, 107, 53, 0.3)same issue as line 53🎨 Proposed fix — use tokens throughout
&:focus { border-color: ${C.accent}; - box-shadow: 0 0 20px rgba(255, 107, 53, 0.15); + box-shadow: 0 0 20px color-mix(in srgb, ${C.accent} 15%, transparent); } &:hover:not(:focus) { - border-color: `#3d3d54`; + border-color: ${C.border}; }&:hover { transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(255, 107, 53, 0.3); + box-shadow: 0 8px 25px color-mix(in srgb, ${C.accent} 30%, transparent); }Alternatively, if
color-mixbrowser support is a concern, expose anaccentGlowtoken incolors.jsmapping to a CSS custom property that varies per theme.Also applies to: 56-56, 80-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/authForm.js` at line 53, The styled component in app/components/styled/authForm.js uses hardcoded colors for box-shadow and background (`#rgba` and `#3d3d54`) which bypass the theme token system; replace those raw values with the existing design tokens (e.g., C.accent with an appropriate alpha or a new C.accentGlow token) for the box-shadow usages and swap the `#3d3d54` background with the matching token (e.g., C.backgroundElevated or C.panel) so colors respond to light/dark mode; if you need semi-transparent accent glows use CSS color-mix or add an accentGlow token in colors.js that maps to a theme-aware CSS custom property and reference that in the StyledAuthForm (box-shadow and the other two occurrences).app/components/styled/authLayout.js-4-10 (1)
4-10:⚠️ Potential issue | 🟡 Minor
overflow: hiddenonAuthPageWrappermay clip scrollable form content.If the viewport is short (e.g., mobile landscape) or the form is long,
overflow: hiddenwill silently cut off content with no scroll affordance. Consideroverflow-y: autoor at minimum confining the overflow restriction tooverflow-x: hiddenso the form remains accessible.🛠️ Suggested fix
export const AuthPageWrapper = styled.div` display: flex; min-height: 100vh; width: 100%; background: ${C.bg}; - overflow: hidden; + overflow-x: hidden; `;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/authLayout.js` around lines 4 - 10, The AuthPageWrapper styled component currently uses overflow: hidden which can clip vertically scrollable form content on short viewports; update AuthPageWrapper to allow vertical scrolling by replacing overflow: hidden with either overflow-y: auto (preferred) or at minimum overflow-x: hidden so horizontal overflow is suppressed but vertical content can scroll; modify the styled component definition for AuthPageWrapper accordingly.app/services/musicApi.js-5-5 (1)
5-5:⚠️ Potential issue | 🟡 Minor
encodeURIComponent(term)will produce the literal string"undefined"or"null"iftermis not a string.If the saga passes an uninitialized search term, the request becomes
/music/resources/songs?term=undefined, which is a valid URL but almost certainly an unintended query.🛡️ Proposed guard
-export const searchSongs = (term) => musicApi.get(`/music/resources/songs?term=${encodeURIComponent(term)}`); +export const searchSongs = (term) => { + if (!term) throw new Error('searchSongs: term is required'); + return musicApi.get(`/music/resources/songs?term=${encodeURIComponent(term)}`); +};Alternatively, validate
termat the saga level before dispatching the API call.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/musicApi.js` at line 5, The searchSongs export currently interpolates term directly which yields the literal "undefined"/"null" when term is not a string; update the function (searchSongs) to guard and normalize term before building the URL: if term is null/undefined use an empty string or omit the query param, otherwise convert to String(term) and pass encodeURIComponent(String(term)); ensure the final call to musicApi.get uses the safe term (e.g., ?term=${safeTerm} or no ?term= when safeTerm is empty).app/global-styles.js-16-16 (1)
16-16:⚠️ Potential issue | 🟡 MinorCSS custom properties lack fallback values — FOUC risk in SSR
In a Next.js app, the server renders HTML before ThemeContext hydrates on the client. At that point
--musica-textand--musica-bgare undefined, so#apphas no background colour and body text may render without intended colouring until hydration completes.Add fallback values matching your default (light) palette:
🛡️ Proposed fix
p, label { font-family: 'Outfit', sans-serif; line-height: 1.5; - color: var(--musica-text); + color: var(--musica-text, `#333333`); } body { p, label, span, div, h1 { line-height: 1.5; font-family: 'Outfit', sans-serif; - color: var(--musica-text); + color: var(--musica-text, `#333333`); } } `#app` { - background-color: var(--musica-bg); + background-color: var(--musica-bg, `#ffffff`); min-height: 100%; min-width: 100%; }Replace the fallback hex values with the actual light-theme defaults from your palette.
Also applies to: 27-27, 35-35
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/global-styles.js` at line 16, The CSS uses CSS variables without fallbacks causing FOUC on SSR; update usages of var(--musica-text) and var(--musica-bg) (e.g., in the `#app` selector and the body/background declarations) to include explicit fallback hex values that match your light-theme defaults (replace the placeholder hexes with the actual palette values) so the server-rendered styles show the correct colours until ThemeContext hydrates.app/utils/tests/withAuth.test.js-18-35 (1)
18-35:⚠️ Potential issue | 🟡 MinorSpy leaks on test failure + weak assertion.
Two issues:
Spy cleanup —
mockRestore()is called inline at the end of each test body. If the assertion on line 25 or line 33 throws first,mockRestore()is never reached. The next test then callsjest.spyOnon an already-spied function, stacking spies, and the finalmockRestore()unwinds only one layer — leaving the original function un-restored for subsequent suites.Move teardown to
afterEach:- beforeEach(() => { - mockReplace.mockClear() - }) + beforeEach(() => { + mockReplace.mockClear() + }) + + afterEach(() => { + jest.restoreAllMocks() + })Then drop the inline
authStorage.getStoredToken.mockRestore()calls from each test.Redundant assertion —
getByTestIdalready throws if the element is absent, making.toBeTruthy()on line 25 redundant. PrefertoBeInTheDocument()from@testing-library/jest-domfor a more expressive failure message:- expect(getByTestId('protected')).toBeTruthy() + expect(getByTestId('protected')).toBeInTheDocument()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/utils/tests/withAuth.test.js` around lines 18 - 35, The tests leak spies and use a weak assertion: stop restoring authStorage.getStoredToken inside each test and instead add an afterEach that calls authStorage.getStoredToken.mockRestore() (or jest.restoreAllMocks()) so the spy is always cleaned up even if an assertion fails; remove the inline authStorage.getStoredToken.mockRestore() calls from the two tests. Also replace the redundant expect(getByTestId('protected')).toBeTruthy() with expect(getByTestId('protected')).toBeInTheDocument() (ensure `@testing-library/jest-dom` matchers are enabled) and keep the existing queryByTestId assertion and mockReplace check as-is.app/containers/Auth/tests/reducer.test.js-45-54 (1)
45-54:⚠️ Potential issue | 🟡 Minor
FAILURE_AUTHtest only verifies the fallback default, not actual error propagation.The action is dispatched with no error field, and the test hard-codes
'something_went_wrong'as the expected error. This only exercises the reducer's fallback branch and doesn't confirm that an actual error value passed via the action is correctly stored. Add a sibling test (or extend this one) that dispatches with a real error payload:💡 Suggested additional test case
it('should set error when FAILURE_AUTH is dispatched', () => { const expectedResult = { ...state, [PAYLOAD.ERROR]: 'something_went_wrong', loading: false } expect(authReducer(state, { type: authTypes.FAILURE_AUTH })).toEqual( expectedResult ) }) + + it('should store the provided error when FAILURE_AUTH is dispatched with an error', () => { + const error = 'Invalid credentials' + const expectedResult = { ...state, [PAYLOAD.ERROR]: error, loading: false } + expect( + authReducer(state, { type: authTypes.FAILURE_AUTH, [PAYLOAD.ERROR]: error }) + ).toEqual(expectedResult) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/Auth/tests/reducer.test.js` around lines 45 - 54, The existing test for authReducer handling authTypes.FAILURE_AUTH only exercises the fallback when no error is provided; update the tests to also assert that an actual error passed in the action is stored. Add a new test (or extend the current one) that dispatches authReducer(state, { type: authTypes.FAILURE_AUTH, [PAYLOAD.ERROR]: 'actual_error_message' }) and expect the resulting state to include PAYLOAD.ERROR: 'actual_error_message' and loading: false; reference authReducer, authTypes.FAILURE_AUTH, PAYLOAD.ERROR and the test state to locate where to add this assertion.app/containers/Music/tests/reducer.test.js-41-50 (1)
41-50:⚠️ Potential issue | 🟡 Minor
FAILURE_SEARCH_SONGStest only validates the fallback default, not actual error propagation — same gap as the auth reducer tests.Dispatching without an error field and asserting
'something_went_wrong'only tests the reducer's default fallback. Add a complementary case that passes a real error and asserts it is stored:💡 Suggested additional test case
it('should set error on FAILURE_SEARCH_SONGS', () => { const expected = { ...state, [PAYLOAD.ERROR]: 'something_went_wrong', loading: false } expect( musicReducer(state, { type: musicTypes.FAILURE_SEARCH_SONGS }) ).toEqual(expected) }) + + it('should store the provided error on FAILURE_SEARCH_SONGS', () => { + const error = 'Network unavailable' + const expected = { ...state, [PAYLOAD.ERROR]: error, loading: false } + expect( + musicReducer(state, { type: musicTypes.FAILURE_SEARCH_SONGS, [PAYLOAD.ERROR]: error }) + ).toEqual(expected) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/Music/tests/reducer.test.js` around lines 41 - 50, Current test for FAILURE_SEARCH_SONGS only asserts the reducer's fallback error value; update tests to also assert real error propagation by dispatching musicReducer(state, { type: musicTypes.FAILURE_SEARCH_SONGS, payload: { [PAYLOAD.ERROR]: 'real_error_message' } }) and expecting the resulting state to include [PAYLOAD.ERROR]: 'real_error_message' (and loading: false) so musicReducer actually stores the provided error from the action payload.app/components/AudioPlayer/index.js-58-58 (1)
58-58:⚠️ Potential issue | 🟡 MinorHardcoded color undermines the light/dark theme fix.
'#e84393'is a hardcoded pink that won't adapt to the active theme. Given this PR explicitly targets light/dark mode UX, this should reference a theme token (e.g.,C.accentor similar) consistent with how the rest of the styled components are themed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/AudioPlayer/index.js` at line 58, The icon color is hardcoded in the AudioPlayer component; replace the inline style color '#e84393' on the SoundFilled element with a theme token so it respects light/dark mode (e.g., use the project's color constant like C.accent or pull color from the theme and pass it as the style/className/prop). Update the SoundFilled usage in AudioPlayer/index.js to reference the theme token instead of the literal string so the icon inherits the correct themed color.app/components/AudioPlayer/index.js-49-56 (1)
49-56:⚠️ Potential issue | 🟡 MinorProgress slider
valuemay exceedmaxbriefly.When
player.durationisundefinedor0,maxresolves to0, butplayer.currentTimecould be non-zero transiently. This would produce an invalid range input state. Consider also defaultingvalueto0:Proposed fix
<ProgressSlider data-testid="progress-slider" type="range" min={0} max={player.duration || 0} - value={player.currentTime} + value={player.currentTime || 0} onChange={(e) => player.seek(Number(e.target.value))} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/AudioPlayer/index.js` around lines 49 - 56, The ProgressSlider can receive a value greater than its max when player.duration is falsy; change the value calculation to clamp/default it: compute a local max (e.g. const sliderMax = player.duration || 0) and set value to Math.min(player.currentTime || 0, sliderMax) so the range input never has value > max; keep onChange using player.seek(Number(e.target.value)) and leave max set to sliderMax.app/containers/Music/index.js-40-47 (1)
40-47:⚠️ Potential issue | 🟡 MinorDebounced function leaks on unmount — cancel it in a cleanup effect.
The debounced function returned by
lodash/debouncemay fire after the component unmounts, triggering an unnecessary API call. Also,dispatchFetchLibraryis missing from theuseEffectdeps (anddispatchSearchfrom theuseCallbackdeps), though both are stable Redux dispatch wrappers.🛡️ Suggested cleanup
+ const debouncedSearch = useCallback( + debounce((term) => dispatchSearch(term), SEARCH_DEBOUNCE_MS), + [dispatchSearch] + ); + useEffect(() => { dispatchFetchLibrary(); - }, []); + return () => debouncedSearch.cancel(); + }, [dispatchFetchLibrary, debouncedSearch]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/Music/index.js` around lines 40 - 47, The debounced function created by debounce can fire after unmount and your effects are missing stable deps: change the debouncedSearch setup so it's created with the correct dependencies (include dispatchSearch and SEARCH_DEBOUNCE_MS) and ensure you cancel it on unmount (call debouncedSearch.cancel in a cleanup). Also add dispatchFetchLibrary to the useEffect dependency array (or explicitly document it's stable) so the initial fetch effect is correct; reference the useEffect that calls dispatchFetchLibrary and the debouncedSearch creation that uses debounce and dispatchSearch.app/components/styled/musicVisual.js-67-75 (1)
67-75:⚠️ Potential issue | 🟡 MinorCustom props (
duration,delay,size,left,bottom) forwarded to DOM will trigger React warnings.Emotion's
styledforwards all props to the underlying DOM element by default. Props likeduration,delay,size, andbottomaren't valid HTML attributes on adivand will produce "React does not recognize theXprop on a DOM element" console warnings.Fix this by filtering props with
shouldForwardProp. Since@emotion/is-prop-validis not in your dependencies, you can manually list the props to exclude:-export const GlowRing = styled.div` +const GlowRing = styled('div', { + shouldForwardProp: (prop) => !['size', 'duration', 'delay'].includes(prop) +})` position: absolute; width: ${(props) => props.size}px; height: ${(props) => props.size}px; border-radius: 50%; border: 1px solid rgba(255, 107, 53, 0.08); animation: ${pulse} ${(props) => props.duration}s ease-in-out infinite; animation-delay: ${(props) => props.delay}s; `; + +export { GlowRing };Apply the same pattern to
EqualizerBar(lines 87–99: filterduration,delay) andFloatingNote(lines 101–110: filtersize,duration,delay,left,bottom).Alternatively, add
@emotion/is-prop-validas a dependency and use its idiomaticshouldForwardProp: isPropValidcheck.Also applies to: 87–99, 101–110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/styled/musicVisual.js` around lines 67 - 75, The styled components are forwarding custom props (duration, delay, size, left, bottom) to DOM causing React warnings; update GlowRing, EqualizerBar, and FloatingNote to filter these props using Emotion's shouldForwardProp (either import and use isPropValid from `@emotion/is-prop-valid` or implement a manual shouldForwardProp that returns false for the listed prop names) so the custom props are consumed only by the styled logic and not emitted as HTML attributes; specifically, add shouldForwardProp to the styled(...) call for GlowRing (filter size, duration, delay), EqualizerBar (filter duration, delay) and FloatingNote (filter size, duration, delay, left, bottom).
| useEffect(() => { | ||
| const audio = new Audio(); | ||
| audio.volume = DEFAULT_VOLUME; | ||
| audioRef.current = audio; | ||
|
|
||
| const onTime = () => setCurrentTime(audio.currentTime); | ||
| const onLoaded = () => setDuration(audio.duration); | ||
| const onEnded = () => { | ||
| setIsPlaying(false); | ||
| if (onSongEnd) { | ||
| onSongEnd(); | ||
| } | ||
| }; | ||
|
|
||
| audio.addEventListener('timeupdate', onTime); | ||
| audio.addEventListener('loadedmetadata', onLoaded); | ||
| audio.addEventListener('ended', onEnded); | ||
|
|
||
| return () => { | ||
| audio.removeEventListener('timeupdate', onTime); | ||
| audio.removeEventListener('loadedmetadata', onLoaded); | ||
| audio.removeEventListener('ended', onEnded); | ||
| audio.pause(); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
Stale onSongEnd closure — navigation will operate on mount-time state.
onEnded is created once in the mount-only effect and permanently captures the onSongEnd prop from that first render. onSongEnd maps to handleNext (from usePlaybackNav), which closes over songs and currentSong. After those values change (song played, list searched, etc.), handleNext gets a new reference, but the stale closure registered on the 'ended' event still calls the mount-time version — making end-of-track navigation unreliable.
Use a ref to always dispatch to the latest callback without recreating the Audio element:
🐛 Proposed fix
+ const onSongEndRef = useRef(onSongEnd);
+ useEffect(() => {
+ onSongEndRef.current = onSongEnd;
+ }); // no deps — keeps the ref current on every render
useEffect(() => {
const audio = new Audio();
audio.volume = DEFAULT_VOLUME;
audioRef.current = audio;
const onTime = () => setCurrentTime(audio.currentTime);
const onLoaded = () => setDuration(audio.duration);
const onEnded = () => {
setIsPlaying(false);
- if (onSongEnd) {
- onSongEnd();
- }
+ onSongEndRef.current?.();
};
// ...
}, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/AudioPlayer/useAudioPlayer.js` around lines 11 - 35, The
mounted effect registers an onEnded listener that closes over the initial
onSongEnd prop, causing stale handleNext behavior; fix by adding a mutable ref
(e.g., latestOnSongEndRef) that you update whenever onSongEnd changes and change
the mounted onEnded handler (the one added/removed in the useEffect where
audioRef is created) to call latestOnSongEndRef.current() if defined — this
preserves the single Audio instance while ensuring onEnded always delegates to
the latest onSongEnd/handleNext from usePlaybackNav.
| useEffect(() => { | ||
| if (song?.previewUrl && audioRef.current) { | ||
| audioRef.current.src = song.previewUrl; | ||
| audioRef.current.play().catch(() => {}); | ||
| setIsPlaying(true); | ||
| } | ||
| }, [song?.trackId]); | ||
|
|
||
| const togglePlay = useCallback(() => { | ||
| if (!audioRef.current?.src) { | ||
| return; | ||
| } | ||
| if (isPlaying) { | ||
| audioRef.current.pause(); | ||
| } else { | ||
| audioRef.current.play().catch(() => {}); | ||
| } | ||
| setIsPlaying(!isPlaying); |
There was a problem hiding this comment.
Optimistic setIsPlaying(true) ignores play() rejection — UI desynchronises from audio state.
Both the song-change effect (line 40-41) and togglePlay (lines 52-54) call setIsPlaying(true) unconditionally after play(), while silently swallowing the rejection. When the browser blocks autoplay, the player UI shows a "playing" indicator with no audio output.
Move setIsPlaying(true) inside the .then() handler, and revert state in .catch():
🐛 Proposed fix
useEffect(() => {
if (song?.previewUrl && audioRef.current) {
audioRef.current.src = song.previewUrl;
- audioRef.current.play().catch(() => {});
- setIsPlaying(true);
+ audioRef.current.play()
+ .then(() => setIsPlaying(true))
+ .catch(() => setIsPlaying(false));
}
}, [song?.trackId]);
const togglePlay = useCallback(() => {
if (!audioRef.current?.src) {
return;
}
if (isPlaying) {
audioRef.current.pause();
setIsPlaying(false);
} else {
- audioRef.current.play().catch(() => {});
- setIsPlaying(!isPlaying);
+ audioRef.current.play()
+ .then(() => setIsPlaying(true))
+ .catch(() => {});
}
}, [isPlaying]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (song?.previewUrl && audioRef.current) { | |
| audioRef.current.src = song.previewUrl; | |
| audioRef.current.play().catch(() => {}); | |
| setIsPlaying(true); | |
| } | |
| }, [song?.trackId]); | |
| const togglePlay = useCallback(() => { | |
| if (!audioRef.current?.src) { | |
| return; | |
| } | |
| if (isPlaying) { | |
| audioRef.current.pause(); | |
| } else { | |
| audioRef.current.play().catch(() => {}); | |
| } | |
| setIsPlaying(!isPlaying); | |
| useEffect(() => { | |
| if (song?.previewUrl && audioRef.current) { | |
| audioRef.current.src = song.previewUrl; | |
| audioRef.current.play() | |
| .then(() => setIsPlaying(true)) | |
| .catch(() => setIsPlaying(false)); | |
| } | |
| }, [song?.trackId]); | |
| const togglePlay = useCallback(() => { | |
| if (!audioRef.current?.src) { | |
| return; | |
| } | |
| if (isPlaying) { | |
| audioRef.current.pause(); | |
| setIsPlaying(false); | |
| } else { | |
| audioRef.current.play() | |
| .then(() => setIsPlaying(true)) | |
| .catch(() => {}); | |
| } | |
| setIsPlaying(!isPlaying); | |
| }, [isPlaying]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/AudioPlayer/useAudioPlayer.js` around lines 37 - 54, The
effect in useEffect and the togglePlay callback optimistically call
setIsPlaying(true) regardless of whether audioRef.current.play() succeeds,
causing UI desync when play() is rejected; update both the song-change effect
(useEffect) and togglePlay to call audioRef.current.play().then(() =>
setIsPlaying(true)).catch(() => setIsPlaying(false) or leave false) so state
only flips when play succeeds and is reverted on rejection, and keep the pause
branch calling audioRef.current.pause() followed by setIsPlaying(false) as
before.
| const handleLogout = () => { | ||
| clearStoredToken(); | ||
| Router.push('/login'); | ||
| }; |
There was a problem hiding this comment.
Logout skips Redux state reset — stale auth state survives client-side navigation.
handleLogout clears localStorage and redirects, but never dispatches a logout/reset action to the Redux store. On a client-side route change (Router.push), the Redux store is preserved. Any component or selector that reads state.auth will still see the authenticated user until a hard reload, potentially rendering protected UI or leaking user data in-memory.
Because handleLogout is defined at module scope it cannot call useDispatch; either:
- Preferred – make the component receive
onLogoutvia props (or connect it) so the Redux action is dispatched by the caller, or - Move the token-clear + redirect side-effects into the auth saga triggered by a
LOGOUTaction, and have the component simply dispatch that action.
-const handleLogout = () => {
- clearStoredToken();
- Router.push('/login');
-};
-
-const LogoutButton = () => (
- <LogoutBtn data-testid="logout-button" onClick={handleLogout} aria-label="Log out">
+const LogoutButton = ({ onLogout }) => (
+ <LogoutBtn data-testid="logout-button" onClick={onLogout} aria-label="Log out">
<LogoutOutlined />
</LogoutBtn>
);The caller (e.g., header/layout) then dispatches the logout action which the saga handles (clear token, reset state, redirect):
// in parent container
const dispatch = useDispatch();
<LogoutButton onLogout={() => dispatch(authCreators.logout())} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/LogoutButton/index.js` around lines 7 - 10, handleLogout
currently only calls clearStoredToken() and Router.push('/login') so Redux auth
state remains populated; update the component to dispatch a logout/reset action
instead of performing side-effects at module scope. Either (preferred) make
LogoutButton accept an onLogout prop and remove the module-scoped handleLogout
so the parent calls dispatch(authCreators.logout()) (refer to LogoutButton and
handleLogout), or connect the component/use useDispatch inside it to dispatch
LOGOUT; then move token-clearing, state reset and Router.push into the auth saga
that handles LOGOUT (refer to LOGOUT action and the auth saga) so client-side
navigation clears both storage and Redux state atomically.
| export function* handleFetchLibrary() { | ||
| const response = yield call(fetchLibrary); | ||
| if (response.ok) { | ||
| yield put(successFetchLibrary(response.data)); | ||
| } else { | ||
| yield put(failureFetchLibrary(response.data)); | ||
| } | ||
| } | ||
|
|
||
| export function* handleLikeSong(action) { | ||
| const songData = action[LIBRARY_PAYLOAD.SONG_DATA]; | ||
| const response = yield call(likeSong, songData); | ||
| if (response.ok) { | ||
| yield put(successLikeSong(songData)); | ||
| } else { | ||
| yield put(failureLikeSong(response.data)); | ||
| } | ||
| } | ||
|
|
||
| export function* handleUnlikeSong(action) { | ||
| const trackId = action[LIBRARY_PAYLOAD.TRACK_ID]; | ||
| const response = yield call(unlikeSong, trackId); | ||
| if (response.ok) { | ||
| yield put(successUnlikeSong(trackId)); | ||
| } else { | ||
| yield put(failureUnlikeSong(response.data)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing try-catch — a thrown API call will kill the watcher.
If fetchLibrary, likeSong, or unlikeSong throw (e.g., network error, JSON parse failure), the unhandled exception propagates out of the generator and the corresponding takeLatest watcher dies silently. Subsequent dispatches of that action type will never be handled again.
Wrap each handler body in try-catch, dispatching the failure action in the catch block.
🛡️ Example fix for handleFetchLibrary (apply similarly to the other two)
export function* handleFetchLibrary() {
+ try {
const response = yield call(fetchLibrary);
if (response.ok) {
yield put(successFetchLibrary(response.data));
} else {
yield put(failureFetchLibrary(response.data));
}
+ } catch (err) {
+ yield put(failureFetchLibrary(err.message));
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/containers/Library/saga.js` around lines 10 - 37, Each handler
(handleFetchLibrary, handleLikeSong, handleUnlikeSong) must wrap its body in a
try-catch so thrown errors from the API calls (fetchLibrary, likeSong,
unlikeSong) don't kill the saga watcher; move the existing call/response logic
into try, and in catch dispatch the corresponding failure action
(failureFetchLibrary, failureLikeSong, failureUnlikeSong) with the caught error
(or error.message) as payload (and optionally log it), leaving the success
branches unchanged.
| @@ -0,0 +1,7 @@ | |||
| import { generateApiClient } from '@utils/apiUtils'; | |||
|
|
|||
| const authApi = generateApiClient('auth'); | |||
There was a problem hiding this comment.
generateApiClient('auth') resolves to a hardcoded http://localhost:9000 — breaks staging/production
From the apiUtils.js snippet (lines 19-20), both 'auth' and 'music' clients use a hardcoded base URL rather than an environment variable. The default (GitHub) client correctly reads from process.env.NEXT_PUBLIC_GITHUB_URL. This means loginUser and signupUser will hard-fail in any environment other than local development.
Fix in apiUtils.js:
🐛 Proposed fix
case 'auth':
- apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
+ apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_AUTH_API_URL);
return apiClients[type];
case 'music':
- apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });
+ apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_MUSIC_API_URL, { skipRequestTransform: true });
return apiClients[type];Add the corresponding variables to .env.local (and CI/staging secrets):
NEXT_PUBLIC_AUTH_API_URL=http://localhost:9000
NEXT_PUBLIC_MUSIC_API_URL=http://localhost:9000
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/services/authApi.js` at line 3, The auth and music API clients created
via generateApiClient('auth') and generateApiClient('music') are using a
hardcoded http://localhost:9000 base URL in apiUtils.js; change
generateApiClient so that when serviceName === 'auth' it reads
process.env.NEXT_PUBLIC_AUTH_API_URL and when serviceName === 'music' it reads
process.env.NEXT_PUBLIC_MUSIC_API_URL (falling back to the current localhost
value only if the env var is undefined), and ensure the default/github client
continues to use process.env.NEXT_PUBLIC_GITHUB_URL; also add
NEXT_PUBLIC_AUTH_API_URL and NEXT_PUBLIC_MUSIC_API_URL to .env.local and
CI/staging secrets.
| case 'auth': | ||
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000'); | ||
| return apiClients[type]; | ||
| case 'music': | ||
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true }); | ||
| return apiClients[type]; |
There was a problem hiding this comment.
Hardcoded http://localhost:9000 will not work outside local development.
Both auth and music clients use a hardcoded localhost URL. In production or staging, these API calls will fail. Use an environment variable (similar to how the default case uses process.env.NEXT_PUBLIC_GITHUB_URL).
🐛 Proposed fix
case 'auth':
- apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
+ apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_API_URL);
return apiClients[type];
case 'music':
- apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });
+ apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_API_URL, { skipRequestTransform: true });
return apiClients[type];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case 'auth': | |
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000'); | |
| return apiClients[type]; | |
| case 'music': | |
| apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true }); | |
| return apiClients[type]; | |
| case 'auth': | |
| apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_API_URL); | |
| return apiClients[type]; | |
| case 'music': | |
| apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_API_URL, { skipRequestTransform: true }); | |
| return apiClients[type]; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/utils/apiUtils.js` around lines 19 - 24, Replace the hardcoded
'http://localhost:9000' used when creating the 'auth' and 'music' clients with a
configurable environment variable (use the same pattern as the default case,
e.g. process.env.NEXT_PUBLIC_GITHUB_URL or a new NEXT_PUBLIC_API_URL) so the
createApiClientWithTransForm calls inside the 'auth' and 'music' cases use
process.env values (with an optional dev fallback to 'http://localhost:9000' if
desired); update the references in the switch that assigns apiClients[type] for
the 'auth' and 'music' cases so they call createApiClientWithTransForm(<envVar>,
{...}) instead of the hardcoded URL.
| const TOKEN_KEY = 'musica_access_token'; | ||
|
|
||
| const isBrowser = () => typeof window !== 'undefined'; | ||
|
|
||
| export const getStoredToken = () => { | ||
| if (!isBrowser()) { | ||
| return null; | ||
| } | ||
| return localStorage.getItem(TOKEN_KEY); | ||
| }; | ||
|
|
||
| export const setStoredToken = (token) => { | ||
| if (!isBrowser()) { | ||
| return; | ||
| } | ||
| localStorage.setItem(TOKEN_KEY, token); | ||
| }; | ||
|
|
||
| export const clearStoredToken = () => { | ||
| if (!isBrowser()) { | ||
| return; | ||
| } | ||
| localStorage.removeItem(TOKEN_KEY); | ||
| }; |
There was a problem hiding this comment.
Storing auth tokens in localStorage exposes them to XSS.
localStorage is readable by any JavaScript running on the page. A single XSS vulnerability anywhere in the app (including third-party scripts) would allow an attacker to exfiltrate the access token. The recommended approach for sensitive tokens is HttpOnly cookies, which are inaccessible to JavaScript. If cookies aren't feasible, consider memory-only storage (a module-level variable or React context) that survives the session tab but is never persisted to localStorage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/utils/authStorage.js` around lines 1 - 24, The current helpers
(TOKEN_KEY, isBrowser, getStoredToken, setStoredToken, clearStoredToken) persist
the access token to localStorage which is vulnerable to XSS; change the storage
strategy to use server-set HttpOnly, Secure, SameSite cookies for sensitive
tokens (i.e. remove storing the raw access token from client JS and have the
server set the cookie via Set-Cookie on auth responses), and update these
functions to stop reading/writing localStorage — either make them no-op/return
null and rely on cookie-based auth, or implement an in-memory fallback
(module-level variable) for client-only sessions; also keep the isBrowser guards
and adjust any callers to use the cookie-backed auth flow (or the new in-memory
token API) instead of expecting localStorage.
| export const getStoredTheme = () => { | ||
| if (!isBrowser()) { | ||
| return null; | ||
| } | ||
| return localStorage.getItem(THEME_KEY); | ||
| }; | ||
|
|
||
| export const setStoredTheme = (theme) => { | ||
| if (!isBrowser()) { | ||
| return; | ||
| } | ||
| localStorage.setItem(THEME_KEY, theme); | ||
| }; |
There was a problem hiding this comment.
localStorage calls are unguarded against exceptions.
localStorage.getItem and localStorage.setItem can throw SecurityError (e.g., storage access blocked by browser privacy settings or cross-origin restrictions) and QuotaExceededError. Since these utilities are likely called during app initialization, an unhandled throw would crash the entire app.
🛡️ Proposed fix
export const getStoredTheme = () => {
if (!isBrowser()) {
return null;
}
- return localStorage.getItem(THEME_KEY);
+ try {
+ return localStorage.getItem(THEME_KEY);
+ } catch {
+ return null;
+ }
};
export const setStoredTheme = (theme) => {
if (!isBrowser()) {
return;
}
- localStorage.setItem(THEME_KEY, theme);
+ try {
+ localStorage.setItem(THEME_KEY, theme);
+ } catch {
+ // Storage unavailable; proceed with in-memory theme only
+ }
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/utils/themeStorage.js` around lines 5 - 17, Wrap the localStorage access
in try/catch in both getStoredTheme and setStoredTheme so Storage exceptions
(SecurityError, QuotaExceededError) don’t bubble up; in getStoredTheme return
null on error, and in setStoredTheme silently no-op (or log a warning) if
localStorage.setItem throws. Keep the existing isBrowser() guard but move/retain
the try/catch around localStorage.getItem(THEME_KEY) and
localStorage.setItem(THEME_KEY, theme) to safely handle failures.
| @@ -1 +1,3 @@ | |||
| NEXT_PUBLIC_GITHUB_URL=https://api.github.com/ | |||
| NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 | |||
There was a problem hiding this comment.
Identical NEXT_PUBLIC_POSTHOG_KEY across all three environments will mix analytics data.
The same key phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 is committed verbatim to .env.development, .env.qa, and .env.production. This means events from developers, QA testers, and real production users all land in the same PostHog project with no way to segment or filter by environment. QA automation runs and developer experiments will pollute production analytics.
Each environment should use a separate PostHog project (and therefore a separate key), or a PostHog property/distinct ID scheme must be used to tag events by environment.
On the Gitleaks alert: PostHog
phc_*project API keys are intentionally public, write-only keys designed to be embedded in client-side code (theNEXT_PUBLIC_prefix confirms this). The Gitleaks "Generic API Key" detection is a false positive here. That said, anyone with this key can send arbitrary synthetic events to your PostHog project, which is another reason to use separate projects per environment.
🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 2-2: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@environments/.env.development` at line 2, The NEXT_PUBLIC_POSTHOG_KEY value
is identical across development/QA/production env files, which causes analytics
from all environments to mix; fix by provisioning separate PostHog projects (or
distinct write keys) for each environment and replacing the shared
NEXT_PUBLIC_POSTHOG_KEY in each env file with the environment-specific key, or
alternatively ensure every client event is tagged with a clear environment
property (e.g., env or projectId) at emission to allow reliable segmentation;
also acknowledge that the phc_* key is a public client-side write key (Gitleaks
false positive) but still rotate/use separate keys per environment to avoid
cross-environment pollution.
| "npm run lint:eslint:fix", | ||
| "git add --force", | ||
| "jest --findRelatedTests --passWithNoTests $STAGED_FILES" | ||
| "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES" |
There was a problem hiding this comment.
Remove -u / --updateSnapshot from the pre-commit hook — it silently destroys snapshot regression coverage.
-u (--updateSnapshot) tells Jest to overwrite every snapshot whose output differs from the stored baseline, without any human confirmation. Wiring this into a pre-commit hook means:
- Regressions are never caught at commit time. If a code change accidentally alters a component's rendered output, the snapshot is silently overwritten and committed alongside the change. The test suite continues to "pass" — but it now asserts the broken output.
- Snapshot history becomes unreliable. Reviewers and CI lose the ability to audit what changed in
.snapfiles versus what was intentional.
The -u flag is meant to be run deliberately in a developer's terminal after visually confirming the diff (jest -u → review snapshot diff → git add *.snap). It should never be in an automated hook.
🔧 Proposed fix
- "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES"
+ "jest --findRelatedTests --passWithNoTests $STAGED_FILES"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES" | |
| "jest --findRelatedTests --passWithNoTests $STAGED_FILES" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 34, The pre-commit hook command currently includes the
Jest snapshot update flag (`-u`/`--updateSnapshot`) which causes silent snapshot
overwrites; remove the `-u` (or `--updateSnapshot`) token from the hook command
string that contains "jest --findRelatedTests -u --passWithNoTests
$STAGED_FILES" so the hook runs tests without auto-updating snapshots and fails
if snapshots diverge; optionally add a separate npm script (e.g.,
"test:updateSnapshots") for deliberate manual snapshot updates rather than
performing updates in the pre-commit hook.
fixed some ux related bugs in light/dark mode
Summary by CodeRabbit
New Features
Documentation
Tests