Skip to content

Feat/library#28

Draft
aman-wednesdaysol wants to merge 11 commits intowednesday-solutions:masterfrom
aman-wednesdaysol:feat/library
Draft

Feat/library#28
aman-wednesdaysol wants to merge 11 commits intowednesday-solutions:masterfrom
aman-wednesdaysol:feat/library

Conversation

@aman-wednesdaysol
Copy link

@aman-wednesdaysol aman-wednesdaysol commented Feb 20, 2026

fixed some ux related bugs in light/dark mode

Summary by CodeRabbit

  • New Features

    • Added music player with playback controls, progress slider, and volume adjustment.
    • Added like/unlike functionality for saving favorite songs.
    • Added music search with debounced input.
    • Added dark/light theme toggle.
    • Added authentication with login and signup pages.
    • Added library page to view and manage liked songs.
    • Added animated music visualization effects.
  • Documentation

    • Added code quality guidelines and style rules.
  • Tests

    • Added comprehensive test coverage for all new features.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Authentication System
app/containers/Auth/*, pages/login.js, pages/signup.js
Introduces login/signup forms, Redux reducer/saga for auth flow, selectors for loading/error/data state, API endpoints for user authentication. Wires auth state to login/signup pages with saga-based side effects and token persistence.
Music Discovery
app/containers/Music/*, app/services/musicApi.js
Implements music search container with debounced input, Redux reducer for search results, saga handling, and song list rendering. Includes hooks for playback navigation (next/prev) and like toggling.
Library Management
app/containers/Library/*, app/services/libraryApi.js
Adds liked songs library container with fetch/like/unlike operations, Redux reducer managing liked songs array and trackId map, saga orchestration for API calls, and selectors for library state.
Audio Playback
app/components/AudioPlayer/*
Introduces AudioPlayer component with media controls (play/pause, next/prev), progress and volume sliders, and useAudioPlayer hook managing HTMLAudioElement lifecycle with event listeners.
UI Components
app/components/HeartButton/*, app/components/LogoutButton/*, app/components/NavLink/*, app/components/SearchBar/*, app/components/SongList/*, app/components/MusicVisual/*, app/components/ThemeToggle/*
Adds presentational components: HeartButton with like state, LogoutButton with token clearing, NavLink for navigation, SearchBar with onChange handler, SongList with song cards and optional like buttons, MusicVisual with animated vinyl/equalizer/floating notes, and ThemeToggle for light/dark switching.
Styled Components
app/components/styled/*
Exports emotion-styled components for layout, form inputs, buttons, player bar, music visual effects, song cards, and theme toggles. Uses shared color tokens (C) and keyframe animations.
Theme System
app/contexts/ThemeContext.js, app/utils/themeStorage.js
Introduces ThemeContext provider with light/dark theme toggling, localStorage persistence, CSS variable injection via Emotion Global, and useTheme hook for theme consumption.
API & Storage Utilities
app/services/authApi.js, app/utils/apiUtils.js, app/utils/authStorage.js, app/utils/withAuth.js
Adds API clients for auth/music endpoints, token storage/retrieval utilities with browser safety checks, auth header management, and withAuth HOC enforcing authentication on protected routes.
Redux Infrastructure
app/reducers.js, CLAUDE.md
Integrates auth, music, and library reducers into root reducer. Adds frontend aesthetics prompt and code quality guidelines to documentation.
Pages & App Setup
pages/index.js, pages/library.js, pages/_app.js, pages/_document.js
Replaces ReposPage with authenticated Music page, adds Library page, initializes auth token and theme on app startup, injects Google Fonts (Syne, Outfit).
Configuration & Environment
environments/.env.*, app/global-styles.js, package.json
Adds PostHog keys to environment files, updates global styles with Outfit font and CSS variables, enables snapshot updates in pre-commit hooks.
Test Suites
app/components/*/tests/*, app/containers/*/tests/*, app/services/tests/*, app/utils/tests/*
Comprehensive test coverage for all new components, containers, hooks, reducers, sagas, selectors, and utilities using React Testing Library and Redux test utilities.

Sequence Diagrams

sequenceDiagram
    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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 In MUSICA's new garden, we hop with glee,
Auth guards the gates, where users are free,
Hearts beat with songs, libraries take flight,
Vinyl spins proudly—dark mode or bright! ✨🎵

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/library' is vague and doesn't clearly describe the main changes in the pull request, which includes authentication, music search/playback, theming, library management, and multiple UI components beyond just library functionality. Consider a more descriptive title that captures the primary feature, such as 'Add music library, authentication, and theme management' or 'Implement library, auth, and UI components for music platform'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • Authentication System: Implemented a complete user authentication system including login and signup forms, Redux reducers, sagas, and selectors for state management, and API services for user authentication.
  • Music Playback and Search: Introduced a functional audio player with playback controls (next/previous song, play/pause, volume, progress bar) and a music search feature with debouncing for efficient API calls.
  • User Library for Liked Songs: Developed a personal music library where users can like and unlike songs, with state persistence and dedicated API interactions.
  • Theme Toggling and Global Styling: Added a theme toggle functionality for light/dark mode, utilizing a new ThemeContext and CSS variables for consistent styling across the application. Global styles were updated to reflect this change.
  • New UI Components: Created several new reusable UI components such as AudioPlayer, HeartButton, LogoutButton, MusicVisual, NavLink, SearchBar, and SongList, all built with Emotion styled-components.
  • Code Rules and Aesthetics Guidelines: A new CLAUDE.md file was added, outlining code rules and detailed frontend aesthetic guidelines to ensure consistent and high-quality UI/UX design.

🧠 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
  • CLAUDE.md
    • Added a new file outlining code rules and frontend aesthetic guidelines.
  • app/components/AudioPlayer/constants.js
    • Added constants for the AudioPlayer component.
  • app/components/AudioPlayer/index.js
    • Added the main AudioPlayer component.
  • app/components/AudioPlayer/tests/index.test.js
    • Added tests for the AudioPlayer component.
  • app/components/AudioPlayer/tests/useAudioPlayer.test.js
    • Added tests for the useAudioPlayer hook.
  • app/components/AudioPlayer/useAudioPlayer.js
    • Added a custom hook for audio player logic.
  • app/components/HeartButton/index.js
    • Added the HeartButton component for liking/unliking songs.
  • app/components/HeartButton/tests/index.test.js
    • Added tests for the HeartButton component.
  • app/components/LogoutButton/index.js
    • Added the LogoutButton component.
  • app/components/LogoutButton/tests/index.test.js
    • Added tests for the LogoutButton component.
  • app/components/MusicVisual/constants.js
    • Added constants for the MusicVisual component.
  • app/components/MusicVisual/index.js
    • Added the MusicVisual component for animated backgrounds.
  • app/components/MusicVisual/tests/snapshots/index.test.js.snap
    • Added snapshot tests for MusicVisual.
  • app/components/MusicVisual/tests/index.test.js
    • Added tests for the MusicVisual component.
  • app/components/NavLink/index.js
    • Added the NavLink component for navigation.
  • app/components/NavLink/tests/index.test.js
    • Added tests for the NavLink component.
  • app/components/SearchBar/index.js
    • Added the SearchBar component.
  • app/components/SearchBar/tests/snapshots/index.test.js.snap
    • Added snapshot tests for SearchBar.
  • app/components/SearchBar/tests/index.test.js
    • Added tests for the SearchBar component.
  • app/components/SongList/index.js
    • Added the SongList component to display a list of songs.
  • app/components/SongList/tests/snapshots/index.test.js.snap
    • Added snapshot tests for SongList.
  • app/components/SongList/tests/index.test.js
    • Added tests for the SongList component.
  • app/components/ThemeToggle/index.js
    • Added the ThemeToggle component for switching themes.
  • app/components/ThemeToggle/tests/index.test.js
    • Added tests for the ThemeToggle component.
  • app/components/styled/authForm.js
    • Added styled components for authentication forms.
  • app/components/styled/authLayout.js
    • Added styled components for authentication page layout.
  • app/components/styled/colors.js
    • Added color constants for styled components.
  • app/components/styled/heartButton.js
    • Added styled components for the HeartButton.
  • app/components/styled/logoutButton.js
    • Added styled components for the LogoutButton.
  • app/components/styled/musicPage.js
    • Added styled components for general music page layout.
  • app/components/styled/musicSearch.js
    • Added styled components for the music search bar.
  • app/components/styled/musicVisual.js
    • Added styled components for the music visualizer.
  • app/components/styled/navLink.js
    • Added styled components for navigation links.
  • app/components/styled/playerBar.js
    • Added styled components for the audio player bar.
  • app/components/styled/songList.js
    • Added styled components for the song list.
  • app/components/styled/themeToggle.js
    • Added styled components for the theme toggle button.
  • app/containers/Auth/LoginForm.js
    • Added the LoginForm container component.
  • app/containers/Auth/SignupForm.js
    • Added the SignupForm container component.
  • app/containers/Auth/reducer.js
    • Added Redux reducer for authentication state.
  • app/containers/Auth/saga.js
    • Added Redux saga for authentication logic.
  • app/containers/Auth/selectors.js
    • Added Redux selectors for authentication state.
  • app/containers/Auth/tests/LoginForm.test.js
    • Added tests for the LoginForm component.
  • app/containers/Auth/tests/SignupForm.test.js
    • Added tests for the SignupForm component.
  • app/containers/Auth/tests/snapshots/LoginForm.test.js.snap
    • Added snapshot tests for LoginForm.
  • app/containers/Auth/tests/snapshots/SignupForm.test.js.snap
    • Added snapshot tests for SignupForm.
  • app/containers/Auth/tests/reducer.test.js
    • Added tests for the auth reducer.
  • app/containers/Auth/tests/saga.test.js
    • Added tests for the auth saga.
  • app/containers/Auth/tests/selectors.test.js
    • Added tests for the auth selectors.
  • app/containers/Library/constants.js
    • Added constants for the Library container.
  • app/containers/Library/index.js
    • Added the main Library container component.
  • app/containers/Library/reducer.js
    • Added Redux reducer for the library state.
  • app/containers/Library/saga.js
    • Added Redux saga for library logic.
  • app/containers/Library/selectors.js
    • Added Redux selectors for the library state.
  • app/containers/Library/tests/index.test.js
    • Added tests for the Library container.
  • app/containers/Library/tests/reducer.test.js
    • Added tests for the library reducer.
  • app/containers/Library/tests/saga.test.js
    • Added tests for the library saga.
  • app/containers/Library/tests/selectors.test.js
    • Added tests for the library selectors.
  • app/containers/Music/constants.js
    • Added constants for the Music container.
  • app/containers/Music/index.js
    • Added the main Music container component.
  • app/containers/Music/reducer.js
    • Added Redux reducer for music search and playback state.
  • app/containers/Music/saga.js
    • Added Redux saga for music search logic.
  • app/containers/Music/selectors.js
    • Added Redux selectors for music state.
  • app/containers/Music/tests/snapshots/index.test.js.snap
    • Added snapshot tests for Music container.
  • app/containers/Music/tests/index.test.js
    • Added tests for the Music container.
  • app/containers/Music/tests/reducer.test.js
    • Added tests for the music reducer.
  • app/containers/Music/tests/saga.test.js
    • Added tests for the music saga.
  • app/containers/Music/tests/selectors.test.js
    • Added tests for the music selectors.
  • app/containers/Music/tests/usePlaybackNav.test.js
    • Added tests for the usePlaybackNav hook.
  • app/containers/Music/tests/useToggleLike.test.js
    • Added tests for the useToggleLike hook.
  • app/containers/Music/usePlaybackNav.js
    • Added a custom hook for music playback navigation.
  • app/containers/Music/useToggleLike.js
    • Added a custom hook for toggling liked songs.
  • app/containers/Repos/tests/snapshots/index.test.js.snap
    • Updated snapshot for the Repos container tests.
  • app/contexts/ThemeContext.js
    • Added a new React context for managing theme state.
  • app/contexts/tests/ThemeContext.test.js
    • Added tests for the ThemeContext.
  • app/global-styles.js
    • Modified global styles to use CSS variables for theming and updated font families.
  • app/reducers.js
    • Integrated new reducers for auth, music, and library into the root reducer.
  • app/services/authApi.js
    • Added API service for authentication endpoints.
  • app/services/libraryApi.js
    • Added API service for library management endpoints.
  • app/services/musicApi.js
    • Added API service for music search endpoints.
  • app/services/tests/libraryApi.test.js
    • Added tests for the library API service.
  • app/themes/palettes.js
    • Added new theme palettes (dark/light) and a utility to convert them to CSS variables.
  • app/utils/apiUtils.js
    • Modified API utility to support multiple API clients and set auth headers.
  • app/utils/authStorage.js
    • Added utility functions for storing and retrieving authentication tokens.
  • app/utils/tests/authStorage.test.js
    • Added tests for auth storage utilities.
  • app/utils/tests/themeStorage.test.js
    • Added tests for theme storage utilities.
  • app/utils/tests/withAuth.test.js
    • Added tests for the withAuth HOC.
  • app/utils/themeStorage.js
    • Added utility functions for storing and retrieving theme preferences.
  • app/utils/withAuth.js
    • Added a Higher-Order Component (HOC) for authentication protection.
  • environments/.env.development
    • Added PostHog environment variables.
  • environments/.env.production
    • Added PostHog environment variables.
  • environments/.env.qa
    • Added PostHog environment variables.
  • package.json
    • Updated lint-staged configuration for Jest to include the -u flag for snapshot updates.
  • pages/_app.js
    • Modified the root application component to include ThemeContext.Provider and initialize auth token from storage.
  • pages/_document.js
    • Added Google Fonts preconnect and font links for new typography.
  • pages/index.js
    • Changed the default home page to the Music container, protected by withAuth.
  • pages/library.js
    • Added a new page for the user's music library, protected by withAuth.
  • pages/login.js
    • Added a new page for user login.
  • pages/signup.js
    • Added a new page for user signup.
Activity
  • The author, aman-wednesdaysol, has introduced a significant feature set, including authentication, music search, and playback.
  • The commit message indicates that some UX-related bugs in light/dark mode were fixed, suggesting attention to detail in the new theme implementation.
  • Extensive new files have been added across components, containers, services, and utilities, indicating a major development effort.
  • New test files accompany most new components and logic, demonstrating a commitment to code quality and maintainability.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@aman-wednesdaysol aman-wednesdaysol marked this pull request as draft February 20, 2026 13:11
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to 24
case 'auth':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
return apiClients[type];
case 'music':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });
return apiClients[type];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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];

Comment on lines +38 to +42
if (song?.previewUrl && audioRef.current) {
audioRef.current.src = song.previewUrl;
audioRef.current.play().catch(() => {});
setIsPlaying(true);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
inputBg: '#d2d2db',
inputBg: '#1a1a2e',

@@ -0,0 +1,130 @@
import React, { useState, useCallback, useEffect } from 'react';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
"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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
border-color: #3d3d54;
border-color: ${C.surface};
References
  1. Keep constants in a separate constants file (link)

Comment on lines +40 to +42
useEffect(() => {
dispatchFetchLibrary();
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The debounced search function should be cancelled when the component unmounts to prevent potential memory leaks or state updates on an unmounted component.

Suggested change
useEffect(() => {
dispatchFetchLibrary();
}, []);
useEffect(() => {
dispatchFetchLibrary();
return () => debouncedSearch.cancel();
}, []);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_HOST should be ordered before NEXT_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_HOST should be ordered before NEXT_PUBLIC_POSTHOG_KEY, and a trailing newline is missing.

Same dotenv-linter findings 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_HOST should be ordered before NEXT_PUBLIC_POSTHOG_KEY, and a trailing newline is missing.

Same dotenv-linter findings 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 | 🟡 Minor

Hard-coded #3d3d54 bypasses the theme system and will break in light mode.

All other color usages in this file correctly reference C tokens, 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) coerces null to the string "null", making getStoredToken() return a truthy "null" string afterwards. If any call site invokes setStoredToken(null) to "clear" the token, auth checks comparing the result to null/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

handleNext dispatches songs[0] when currentSong is absent or not matched.

When currentSong is null or its trackId is not in songs, findIdx() returns -1. The guard idx < songs.length - 1 evaluates to true for any non-empty list, so dispatchSetSong(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-testid is derived from label.toLowerCase() and is unaffected by isActive, so getByTestId('nav-search') would pass regardless of whether isActive is 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 | 🟡 Minor

Add a test for the currentSong not-found edge case.

There's no test covering when currentSong is null or its trackId doesn't match any entry in songs (i.e. findIdx() returns -1). As shown in the hook analysis, this currently causes handleNext to silently dispatch songs[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 | 🟡 Minor

Missing error handling for localStorage failures.

localStorage operations can throw (e.g., QuotaExceededError when storage is full, or SecurityError when access is denied by browser policy in some private-mode or iframe configurations). Wrapping each operation in a try/catch prevents 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 | 🟡 Minor

Range slider styling is WebKit-only; Firefox users will see unstyled controls.

Both ProgressSlider and VolumeSlider only declare ::-webkit-slider-thumb. Firefox renders <input type="range"> through ::-moz-range-track and ::-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 VolumeSlider substituting C.pink and the 10px thumb 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 | 🟡 Minor

File exceeds the 100-line limit defined in CLAUDE.md.

At 122 lines, playerBar.js violates the project's own guideline ("Max file length: 100 lines — break into smaller files if exceeded"). Consider splitting slider utilities (ProgressSlider, VolumeSlider) into a separate sliders.js styled 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

value should be required or have a default to avoid uncontrolled/controlled switching.

With value optional and no defaultProps, a caller omitting the prop passes undefined to <input value={undefined}>, making it uncontrolled. If value is 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 | 🟡 Minor

Wrap DISTILLED_AESTHETICS_PROMPT in a fenced code block or remove the Python-style assignment.

The variable assignment and """ triple-quotes are Python syntax, but this is a .md file. 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 | 🟡 Minor

Hardcoded 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.accent and 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 of C.accent
  • Line 56: #3d3d54 is 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-mix browser support is a concern, expose an accentGlow token in colors.js mapping 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: hidden on AuthPageWrapper may clip scrollable form content.

If the viewport is short (e.g., mobile landscape) or the form is long, overflow: hidden will silently cut off content with no scroll affordance. Consider overflow-y: auto or at minimum confining the overflow restriction to overflow-x: hidden so 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" if term is 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 term at 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 | 🟡 Minor

CSS 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-text and --musica-bg are undefined, so #app has 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 | 🟡 Minor

Spy leaks on test failure + weak assertion.

Two issues:

  1. Spy cleanupmockRestore() 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 calls jest.spyOn on an already-spied function, stacking spies, and the final mockRestore() 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.

  2. Redundant assertiongetByTestId already throws if the element is absent, making .toBeTruthy() on line 25 redundant. Prefer toBeInTheDocument() from @testing-library/jest-dom for 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_AUTH test 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_SONGS test 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 | 🟡 Minor

Hardcoded 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.accent or 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 | 🟡 Minor

Progress slider value may exceed max briefly.

When player.duration is undefined or 0, max resolves to 0, but player.currentTime could be non-zero transiently. This would produce an invalid range input state. Consider also defaulting value to 0:

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 | 🟡 Minor

Debounced function leaks on unmount — cancel it in a cleanup effect.

The debounced function returned by lodash/debounce may fire after the component unmounts, triggering an unnecessary API call. Also, dispatchFetchLibrary is missing from the useEffect deps (and dispatchSearch from the useCallback deps), 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 | 🟡 Minor

Custom props (duration, delay, size, left, bottom) forwarded to DOM will trigger React warnings.

Emotion's styled forwards all props to the underlying DOM element by default. Props like duration, delay, size, and bottom aren't valid HTML attributes on a div and will produce "React does not recognize the X prop on a DOM element" console warnings.

Fix this by filtering props with shouldForwardProp. Since @emotion/is-prop-valid is 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: filter duration, delay) and FloatingNote (lines 101–110: filter size, duration, delay, left, bottom).

Alternatively, add @emotion/is-prop-valid as a dependency and use its idiomatic shouldForwardProp: isPropValid check.

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).

Comment on lines +11 to +35
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();
};
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +37 to +54
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +7 to +10
const handleLogout = () => {
clearStoredToken();
Router.push('/login');
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. Preferred – make the component receive onLogout via props (or connect it) so the Redux action is dispatched by the caller, or
  2. Move the token-clear + redirect side-effects into the auth saga triggered by a LOGOUT action, 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.

Comment on lines +10 to +37
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));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +19 to 24
case 'auth':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
return apiClients[type];
case 'music':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });
return apiClients[type];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +1 to +24
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);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +5 to +17
export const getStoredTheme = () => {
if (!isBrowser()) {
return null;
}
return localStorage.getItem(THEME_KEY);
};

export const setStoredTheme = (theme) => {
if (!isBrowser()) {
return;
}
localStorage.setItem(THEME_KEY, theme);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 (the NEXT_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"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. 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.
  2. Snapshot history becomes unreliable. Reviewers and CI lose the ability to audit what changed in .snap files 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.

Suggested change
"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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant