Skip to content

Feat/details(tract details)#30

Open
aman-wednesdaysol wants to merge 10 commits into
wednesday-solutions:masterfrom
aman-wednesdaysol:feat/details
Open

Feat/details(tract details)#30
aman-wednesdaysol wants to merge 10 commits into
wednesday-solutions:masterfrom
aman-wednesdaysol:feat/details

Conversation

@aman-wednesdaysol

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

Copy link
Copy Markdown

Summary by CodeRabbit

Release Notes

  • New Features
    • Added music search functionality with debounced input handling
    • Implemented audio player with playback controls, progress tracking, and volume adjustment
    • Added ability to save and manage favorite songs
    • Created track detail page with song metadata and store link
    • Introduced user authentication with login and signup pages
    • Added light/dark theme toggle
    • Enhanced music library interface with like/dislike functionality

@coderabbitai

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive music streaming application featuring user authentication (login/signup), music search and playback, library management for liked songs, and track detail viewing. It includes a full Redux-based state management system with sagas, custom React hooks for playback control, a styled component library with theme support, API service layer, and corresponding pages with authentication protection.

Changes

Cohort / File(s) Summary
Documentation & Constants
CLAUDE.md
Added code rules for linting, complexity, and file-length constraints, plus a frontend aesthetics prompt constant.
Authentication System
app/containers/Auth/*, pages/login.js, pages/signup.js, app/services/authApi.js, app/utils/authStorage.js, app/utils/withAuth.js
Complete auth flow with login/signup forms, Redux reducer/saga/selectors, token storage, and HOC-based route protection.
Music Container
app/containers/Music/*, app/services/musicApi.js
Music search, playback control, and state management with Redux patterns, custom hooks for playback navigation and like toggling.
Library Container
app/containers/Library/*, app/services/libraryApi.js
Liked songs management with fetch, like, and unlike operations; Redux state handling and selectors.
Track Detail Container
app/containers/TrackDetail/*
Track detail view with fetch and display logic, Redux reducer, saga, and selectors.
Audio Playback Components
app/components/AudioPlayer/*, app/components/MusicVisual/*
AudioPlayer component with playback controls, progress/volume sliders; MusicVisual with animations and decorative elements.
UI Components (Buttons & Navigation)
app/components/BackButton/*, app/components/HeartButton/*, app/components/LogoutButton/*, app/components/ThemeToggle/*, app/components/NavLink/*
Reusable button and navigation components with PropTypes and test coverage.
Content Display Components
app/components/SearchBar/*, app/components/SongList/*, app/components/TrackInfo/*, app/components/ArtworkPlayButton/*
Components for search input, song lists with play/like controls, track details, and artwork overlays.
Styled Components
app/components/styled/*
Emotion-based styling system with color tokens, typography, animations (spin, pulse, float, glow), and theme-aware components.
Theme & Storage
app/contexts/ThemeContext.js, app/themes/palettes.js, app/utils/themeStorage.js
Dark/light theme context with CSS variable generation, palette definitions, and localStorage persistence.
Utilities & Services
app/utils/apiUtils.js, app/utils/formatDuration.js, app/reducers.js
API client factory with auth and music endpoints, duration formatting utility, root reducer composition.
Pages & Routing
pages/index.js, pages/library.js, pages/track/[trackId].js, pages/_app.js, pages/_document.js
Home, library, and track detail pages with auth HOC; app initialization with token setup and theme providers; font preloading.
Environment & Config
environments/.env.*, package.json
PostHog analytics environment variables for dev/qa/prod; Jest snapshot update flag in pre-commit hook.
Comprehensive Test Suites
app/**/*.test.js
Unit tests for all containers (reducers, sagas, selectors, components), components, hooks, and utilities covering happy paths, error states, and edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🎵 A rabbit hops through code so bright,
With auth and songs in darkest night,
Play buttons hop, themes change their hue,
Music streams through Redux true,
The library grows with hearts so keen,
A symphony of features seen! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Feat/details(tract details)' is vague and contains a typo ('tract' instead of 'track'). While it references 'details', it fails to clearly summarize the extensive changes including authentication, music player, library, theme system, and multiple UI components added in this changeset. Revise the title to clearly describe the main feature(s) added. Consider: 'Add music player, library, authentication, and theme system' or a more specific title focusing on the primary objective of this PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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
Copy Markdown

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 introduces significant new features to the application, transforming it into a music streaming-like platform. It includes core functionalities such as user authentication (login and signup), a music search page, a user's liked songs library, and a detailed track view. A global audio player is implemented for seamless playback, along with UI components for interacting with songs (play/pause, like/unlike). The application now supports theme toggling and uses Redux for state management across these new features.

Highlights

  • New Core Features: Introduced comprehensive music functionality including search, playback, a user library for liked songs, and detailed track views.
  • Authentication System: Implemented full user authentication with login and signup forms, Redux state management, and route protection using a withAuth Higher-Order Component.
  • UI/UX Enhancements: Added a global audio player, interactive song list items with play/pause and like/unlike buttons, a dynamic music visualizer, and a theme toggling feature for light/dark modes.
  • Modular Architecture: Structured new features into dedicated components, containers, Redux modules (reducers, sagas, selectors), and API services for better organization and maintainability.
  • Theming and Styling: Integrated a new theme context with light and dark palettes, utilizing CSS variables for consistent styling across the application.

🧠 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 aesthetics guidelines.
  • app/components/ArtworkPlayButton/index.js
    • Added a new React component for displaying artwork with a play/pause overlay.
  • app/components/ArtworkPlayButton/tests/index.test.js
    • Added tests for the ArtworkPlayButton component.
  • app/components/AudioPlayer/constants.js
    • Added a new file defining constants for the audio player.
  • app/components/AudioPlayer/index.js
    • Added a new React component for a global audio player with controls.
  • 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 new React hook for managing audio playback state and controls.
  • app/components/BackButton/index.js
    • Added a new React component for a back navigation button.
  • app/components/BackButton/tests/index.test.js
    • Added tests for the BackButton component.
  • app/components/HeartButton/index.js
    • Added a new React component for a like/unlike button with a heart icon.
  • app/components/HeartButton/tests/index.test.js
    • Added tests for the HeartButton component.
  • app/components/LogoutButton/index.js
    • Added a new React component for logging out a user.
  • app/components/LogoutButton/tests/index.test.js
    • Added tests for the LogoutButton component.
  • app/components/MusicVisual/constants.js
    • Added a new file defining constants for the music visualizer.
  • app/components/MusicVisual/index.js
    • Added a new React component for a dynamic music visualizer.
  • 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 a new React component for navigation links.
  • app/components/NavLink/tests/index.test.js
    • Added tests for the NavLink component.
  • app/components/SearchBar/index.js
    • Added a new React component for a music search input.
  • 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 a new React component for displaying a list of songs.
  • app/components/SongList/tests/index.test.js
    • Added tests for the SongList component.
  • app/components/ThemeToggle/index.js
    • Added a new React component for toggling between light and dark themes.
  • app/components/ThemeToggle/tests/index.test.js
    • Added tests for the ThemeToggle component.
  • app/components/TrackInfo/index.js
    • Added a new React component for displaying detailed track information.
  • app/components/TrackInfo/tests/index.test.js
    • Added tests for the TrackInfo component.
  • app/components/styled/artworkPlayButton.js
    • Added styled components for ArtworkPlayButton.
  • 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/backButton.js
    • Added styled components for BackButton.
  • app/components/styled/colors.js
    • Added a new file defining color constants for themes.
  • app/components/styled/heartButton.js
    • Added styled components for HeartButton.
  • app/components/styled/logoutButton.js
    • Added styled components for LogoutButton.
  • app/components/styled/musicPage.js
    • Added styled components for general music page layout.
  • app/components/styled/musicSearch.js
    • Added styled components for SearchBar.
  • app/components/styled/musicVisual.js
    • Added styled components for MusicVisual.
  • app/components/styled/navLink.js
    • Added styled components for NavLink.
  • app/components/styled/playerBar.js
    • Added styled components for AudioPlayer.
  • app/components/styled/songList.js
    • Added styled components for SongList.
  • app/components/styled/themeToggle.js
    • Added styled components for ThemeToggle.
  • app/components/styled/trackInfo.js
    • Added styled components for TrackInfo.
  • app/containers/Auth/LoginForm.js
    • Added a new container component for user login.
  • app/containers/Auth/SignupForm.js
    • Added a new container component for user signup.
  • app/containers/Auth/reducer.js
    • Added Redux reducer for authentication state management.
  • app/containers/Auth/saga.js
    • Added Redux saga for handling authentication API calls.
  • app/containers/Auth/selectors.js
    • Added Redux selectors for authentication state.
  • app/containers/Auth/tests/LoginForm.test.js
    • Added tests for LoginForm.
  • app/containers/Auth/tests/SignupForm.test.js
    • Added tests for SignupForm.
  • 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 Auth reducer.
  • app/containers/Auth/tests/saga.test.js
    • Added tests for Auth saga.
  • app/containers/Auth/tests/selectors.test.js
    • Added tests for Auth selectors.
  • app/containers/Library/constants.js
    • Added constants for the Library container.
  • app/containers/Library/index.js
    • Added a new container component for the user's liked songs library.
  • app/containers/Library/reducer.js
    • Added Redux reducer for library state management.
  • app/containers/Library/saga.js
    • Added Redux saga for handling library API calls.
  • app/containers/Library/selectors.js
    • Added Redux selectors for 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 Library reducer.
  • app/containers/Library/tests/saga.test.js
    • Added tests for Library saga.
  • app/containers/Library/tests/selectors.test.js
    • Added tests for Library selectors.
  • app/containers/Music/constants.js
    • Added constants for the Music container.
  • app/containers/Music/index.js
    • Added a new container component for music search and playback.
  • app/containers/Music/reducer.js
    • Added Redux reducer for music search and playback state.
  • app/containers/Music/saga.js
    • Added Redux saga for handling music search API calls.
  • app/containers/Music/selectors.js
    • Added Redux selectors for music search and playback state.
  • app/containers/Music/tests/index.test.js
    • Added tests for the Music container.
  • app/containers/Music/tests/reducer.test.js
    • Added tests for Music reducer.
  • app/containers/Music/tests/saga.test.js
    • Added tests for Music saga.
  • app/containers/Music/tests/selectors.test.js
    • Added tests for Music selectors.
  • app/containers/Music/tests/usePlayToggle.test.js
    • Added tests for usePlayToggle hook.
  • app/containers/Music/tests/usePlaybackNav.test.js
    • Added tests for usePlaybackNav hook.
  • app/containers/Music/tests/useToggleLike.test.js
    • Added tests for useToggleLike hook.
  • app/containers/Music/usePlayToggle.js
    • Added a new React hook for managing play/pause state.
  • app/containers/Music/usePlaybackNav.js
    • Added a new React hook for navigating between songs in a list.
  • app/containers/Music/useToggleLike.js
    • Added a new React hook for toggling song likes.
  • app/containers/Repos/tests/snapshots/index.test.js.snap
    • Updated snapshot for Repos component.
  • app/containers/TrackDetail/constants.js
    • Added constants for the TrackDetail container.
  • app/containers/TrackDetail/index.js
    • Added a new container component for displaying detailed track information.
  • app/containers/TrackDetail/reducer.js
    • Added Redux reducer for track detail state.
  • app/containers/TrackDetail/saga.js
    • Added Redux saga for handling track detail API calls.
  • app/containers/TrackDetail/selectors.js
    • Added Redux selectors for track detail state.
  • app/containers/TrackDetail/tests/index.test.js
    • Added tests for the TrackDetail container.
  • app/containers/TrackDetail/tests/reducer.test.js
    • Added tests for TrackDetail reducer.
  • app/containers/TrackDetail/tests/saga.test.js
    • Added tests for TrackDetail saga.
  • app/containers/TrackDetail/tests/selectors.test.js
    • Added tests for TrackDetail selectors.
  • app/contexts/ThemeContext.js
    • Added a new React context for managing application theme.
  • app/contexts/tests/ThemeContext.test.js
    • Added tests for ThemeContext.
  • app/global-styles.js
    • Updated global styles to use CSS variables for theming and new fonts.
  • app/reducers.js
    • Added new reducers for Auth, Music, Library, and TrackDetail.
  • app/services/authApi.js
    • Added new API service for authentication endpoints.
  • app/services/libraryApi.js
    • Added new API service for library (liked songs) endpoints.
  • app/services/musicApi.js
    • Added new API service for music search and track details.
  • app/services/tests/libraryApi.test.js
    • Added tests for libraryApi.
  • app/themes/palettes.js
    • Added new file defining light and dark color palettes and a utility to convert them to CSS variables.
  • app/utils/apiUtils.js
    • Modified API utility to support multiple API clients and auth headers.
  • app/utils/authStorage.js
    • Added new utility functions for storing and retrieving authentication tokens.
  • app/utils/formatDuration.js
    • Added a new utility function for formatting track durations.
  • app/utils/tests/authStorage.test.js
    • Added tests for authStorage.
  • app/utils/tests/formatDuration.test.js
    • Added tests for formatDuration.
  • app/utils/tests/themeStorage.test.js
    • Added tests for themeStorage.
  • app/utils/tests/withAuth.test.js
    • Added tests for withAuth HOC.
  • app/utils/themeStorage.js
    • Added new utility functions for storing and retrieving theme preferences.
  • app/utils/withAuth.js
    • Added a new Higher-Order Component (HOC) for protecting routes with authentication.
  • 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 tests.
  • pages/_app.js
    • Modified _app.js to integrate ThemeContext and set auth headers on app load.
  • pages/_document.js
    • Modified _document.js to include new font imports.
  • pages/index.js
    • Modified the home page to use the new Music container and withAuth HOC.
  • pages/library.js
    • Added a new page for the user's music library, protected by authentication.
  • pages/login.js
    • Added a new page for user login.
  • pages/signup.js
    • Added a new page for user signup.
  • pages/track/[trackId].js
    • Added a new dynamic route page for track details, protected by authentication.
Activity
  • The pull request introduces a substantial amount of new code, indicating a major feature implementation.
  • Numerous new components, containers, Redux modules (reducers, sagas, selectors), and utility files have been added.
  • Comprehensive test files have been added for most new components, hooks, reducers, sagas, and utilities, suggesting a focus on code quality and maintainability.
  • Existing core files like app/reducers.js, app/global-styles.js, app/utils/apiUtils.js, pages/_app.js, pages/_document.js, and pages/index.js have been modified to integrate the new features.
  • New environment variables for PostHog have been added across development, production, and QA environments.
  • The package.json lint-staged configuration was updated to include snapshot updates for Jest tests.
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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive music player feature, including search functionality, a favorites library, and detailed track views, leveraging modern React patterns and unit tests. However, a security audit revealed critical vulnerabilities, including hardcoded API URLs that could leak into production, a potential XSS vulnerability through unsanitized external URLs, and insecure storage of authentication tokens in localStorage. Additionally, the authentication header initialization logic has a bug preventing token application, several files exceed the 100-line limit and require refactoring, and playback state synchronization in the audio hook needs refinement.

Comment thread app/utils/apiUtils.js
Comment on lines +32 to +35
const client = apiClients[type];
if (client) {
client.setHeader('Authorization', `Bearer ${token}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The setAuthHeader function directly accesses apiClients[type]. If this function is called before the client has been initialized (which occurs in _app.js on mount), the header will not be set because the client is still null. Using generateApiClient(type) ensures the client instance is created before the header is applied.

Suggested change
const client = apiClients[type];
if (client) {
client.setHeader('Authorization', `Bearer ${token}`);
}
const client = generateApiClient(type);
if (client) {
client.setHeader('Authorization', `Bearer ${token}`);
}

Comment thread app/utils/apiUtils.js
case 'github':
apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_GITHUB_URL);
case 'auth':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The API base URL for the 'auth' client is hardcoded to http://localhost:9000. This poses a security risk by potentially leaking internal network details and making secure configuration across environments difficult. If deployed, it could lead to exploitation. Please move this to an environment variable (e.g., process.env.NEXT_PUBLIC_AUTH_URL) for secure and correct functionality across all environments.

Comment thread app/utils/apiUtils.js
apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
return apiClients[type];
case 'music':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The API base URL for the 'music' service is hardcoded to http://localhost:9000. Similar to the 'auth' service, this should be moved to an environment variable to ensure secure and flexible configuration across different deployment environments.

)}
</TagRow>
{track.trackUrl && (
<StoreLink href={track.trackUrl} target="_blank" rel="noopener noreferrer" data-testid="store-link">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The track.trackUrl property, which originates from an external API response, is directly assigned to the href attribute of the StoreLink component. If the API returns a malicious URL using the javascript: protocol, clicking the link will execute arbitrary JavaScript in the context of the user's session. It is recommended to validate that the URL uses an allowed protocol (e.g., http: or https:).

Comment thread app/utils/authStorage.js
if (!isBrowser()) {
return;
}
localStorage.setItem(TOKEN_KEY, token);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The application stores the authentication accessToken in localStorage. Data in localStorage is accessible to any JavaScript running on the same origin, making it vulnerable to theft via Cross-Site Scripting (XSS). For better security, consider storing sensitive session tokens in secure, HttpOnly cookies, which are inaccessible to client-side scripts.

Comment on lines +43 to +47
audioRef.current.play().catch(() => {});
setIsPlaying(true);
if (onPlayStateChange) {
onPlayStateChange(true);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The setIsPlaying(true) state update and the onPlayStateChange callback are executed immediately after calling play(), without waiting for the promise to resolve. If the browser blocks playback (e.g., due to autoplay policies), the UI will incorrectly show that the song is playing. These updates should be moved inside the .then() block of the play() call, and a corresponding reset should be added to the .catch() block. This pattern should also be applied to the togglePlay function.

      audioRef.current.play()
        .then(() => {
          setIsPlaying(true);
          if (onPlayStateChange) {
            onPlayStateChange(true);
          }
        })
        .catch(() => {
          setIsPlaying(false);
          if (onPlayStateChange) {
            onPlayStateChange(false);
          }
        });

import useToggleLike from './useToggleLike';
import usePlayToggle from './usePlayToggle';

export function Music(props) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This file is 139 lines long, which violates the repository style guide rule (Max file length: 100 lines). Consider breaking this container into smaller sub-components or moving the Redux connect logic and prop types to a separate file to improve readability and maintainability.

References
  1. Max file length: 100 lines — break into smaller files if exceeded (link)

100% { transform: translateY(-280px) rotate(40deg); opacity: 0; }
`;

export const VinylRecord = styled.div`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This styled-components file is 131 lines long, exceeding the 100-line limit specified in the style guide. It is recommended to split these styles into smaller, logically grouped files (e.g., equalizerStyles.js, vinylStyles.js) to adhere to the project's coding standards.

References
  1. Max file length: 100 lines — break into smaller files if exceeded (link)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (2)
environments/.env.development (1)

2-3: Same issue as environments/.env.qa.

The identical hard-coded NEXT_PUBLIC_POSTHOG_KEY value flags the same Gitleaks warning and shares the same PostHog project across environments. See the comment on environments/.env.qa for the full analysis and remediation steps.

🤖 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, The file contains a
hard-coded public PostHog API key (NEXT_PUBLIC_POSTHOG_KEY) and host
(NEXT_PUBLIC_POSTHOG_HOST) which duplicates the QA env and triggers Gitleaks;
replace the literal key with an environment-driven value and avoid committing
secrets by: remove the hard-coded NEXT_PUBLIC_POSTHOG_KEY from
environments/.env.development, reference a secure deploy-time secret or a
different per-environment key (e.g., NEXT_PUBLIC_POSTHOG_KEY set via CI/hosting
secrets), keep NEXT_PUBLIC_POSTHOG_HOST configurable if needed, and update any
docs or CI to populate NEXT_PUBLIC_POSTHOG_KEY for development so each
environment uses its own key rather than the duplicated value.
environments/.env.production (1)

2-3: Same issue as environments/.env.qa; production environment is highest-impact.

The hard-coded production key is now permanently embedded in git history. Refer to the environments/.env.qa comment for remediation. The production key in particular should be rotated after removal.

🤖 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, Replace the hard-coded
PostHog key and host by removing the literal NEXT_PUBLIC_POSTHOG_KEY value and
switching to a runtime secret (e.g., read NEXT_PUBLIC_POSTHOG_KEY from your
secret manager or CI/CD environment) and ensure NEXT_PUBLIC_POSTHOG_HOST is also
sourced from config rather than committed plaintext; remove the sensitive value
from the repository, add your environment files or patterns (e.g., .env.*) to
.gitignore, purge the leaked key from git history using a history-cleaning tool
(git filter-repo or BFG), and rotate the exposed PostHog API key immediately
after removal.
🟡 Minor comments (24)
CLAUDE.md-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor

Self-contradicting convention: constant defined in this same file.

Line 8 states "Keep constants in a separate constants file," yet DISTILLED_AESTHETICS_PROMPT is defined as a constant directly in this file (lines 13–33). Either move the prompt to a dedicated constants file and reference it here, or relax/remove this specific rule if CLAUDE.md is intentionally the home for AI-context constants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 8, The CLAUDE.md rule "Keep constants in a separate
constants file" conflicts with the in-file constant DISTILLED_AESTHETICS_PROMPT;
fix by either moving DISTILLED_AESTHETICS_PROMPT into the shared constants
module and replacing the in-file definition with an import/reference, or
remove/adjust the rule text so it no longer mandates externalizing AI-context
constants; update any references to DISTILLED_AESTHETICS_PROMPT (the symbol
defined in this file) to use the new exported constant name/location if you
choose the move approach.
CLAUDE.md-13-33 (1)

13-33: ⚠️ Potential issue | 🟡 Minor

Python triple-quote syntax in a Markdown file is unformatted and structurally ambiguous.

DISTILLED_AESTHETICS_PROMPT = """...""" is raw Python-style assignment syntax with no Markdown structure. In a .md file it renders as plain paragraph text with literal """ delimiters, which looks broken in any Markdown viewer. Since CLAUDE.md is read by both humans and the Claude AI toolchain, the intent should be explicit. Consider one of:

  1. If it's documentation only — add a heading and use a fenced code block:
📝 Proposed fix: fenced code block with section heading
+## Design Aesthetics Prompt
+
+```python
 DISTILLED_AESTHETICS_PROMPT = """
 <frontend_aesthetics>
 ...
 </frontend_aesthetics>
 """
+```
  1. If it's a live instruction to the Claude CLI — place it as plain prose under a heading (no variable assignment needed; Claude reads the whole file as context):
📝 Proposed fix: plain prose section
-DISTILLED_AESTHETICS_PROMPT = """
-<frontend_aesthetics>
+## Frontend Aesthetics Guidelines
+
+<frontend_aesthetics>
 ...
-</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 block using DISTILLED_AESTHETICS_PROMPT
= """...""" is Python syntax embedded in a Markdown doc and should be converted
to a proper Markdown structure: either (A) treat it as documentation—replace the
assignment with a heading and a fenced code block (use ```xml or ```python)
containing the <frontend_aesthetics> ... </frontend_aesthetics> content so the
triple-quotes disappear and it renders correctly, or (B) treat it as live
instructions for Claude—remove the variable assignment and triple-quotes and
place the inner content as plain prose under a clear heading so the tool reads
it as context; locate the block by the DISTILLED_AESTHETICS_PROMPT identifier
and the <frontend_aesthetics> tags when making the change.
app/utils/tests/withAuth.test.js-26-26 (1)

26-26: ⚠️ Potential issue | 🟡 Minor

Fragile spy cleanup — move mockRestore() to afterEach.

mockRestore() is called inline after assertions. If any expect(...) call throws before it, the spy is never restored, leaving stale mock state for the remaining tests.

🔧 Proposed fix
   beforeEach(() => {
     mockReplace.mockClear()
+    jest.restoreAllMocks()
   })

   it('should render the wrapped component when token exists', () => {
     jest.spyOn(authStorage, 'getStoredToken').mockReturnValue('valid-token')
     const { getByTestId } = render(<WrappedComponent />)
     expect(getByTestId('protected')).toBeTruthy()
-    authStorage.getStoredToken.mockRestore()
   })

   it('should redirect to /login when no token exists', () => {
     jest.spyOn(authStorage, 'getStoredToken').mockReturnValue(null)
     const { queryByTestId } = render(<WrappedComponent />)
     expect(queryByTestId('protected')).toBeNull()
     expect(mockReplace).toHaveBeenCalledWith('/login')
-    authStorage.getStoredToken.mockRestore()
   })

Also applies to: 34-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/tests/withAuth.test.js` at line 26, The test calls
authStorage.getStoredToken.mockRestore() inline after assertions which can leave
the spy restored only when assertions pass; move the mockRestore() calls into a
common afterEach cleanup so mocks are always restored even if a test fails —
update the test suite in withAuth.test.js to call
authStorage.getStoredToken.mockRestore() (and any other inline mockRestore at
the other occurrence) from an afterEach block that runs after each test.
app/containers/Music/tests/usePlaybackNav.test.js-39-61 (1)

39-61: ⚠️ Potential issue | 🟡 Minor

Missing test: currentSong not found in songs (idx === −1).

The four existing boundary tests all use a currentSong that is in the songs array. There is no case where findIdx() returns -1, which is the exact scenario that triggers the dispatch-of-songs[0] bug described above. Adding this test would both document the expected no-op behaviour and catch the regression.

it('should not dispatch when currentSong is not in songs', () => {
  const { result } = renderHook(() =>
    usePlaybackNav({
      songs,
      currentSong: { trackId: 99, trackName: 'Unknown' },
      dispatchSetSong: mockSetSong
    })
  )
  act(() => result.current.handleNext())
  act(() => result.current.handlePrev())
  expect(mockSetSong).not.toHaveBeenCalled()
})
🤖 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 39 - 61, Add
a test covering the case where currentSong is not present in songs (findIdx ===
-1) to ensure usePlaybackNav does not call dispatchSetSong; specifically,
renderHook calling usePlaybackNav with a currentSong object not in the songs
array and then call result.current.handleNext() and result.current.handlePrev(),
asserting mockSetSong was not called. This targets the functions usePlaybackNav,
findIdx, handleNext, handlePrev and the callback dispatchSetSong so the
regression that dispatches songs[0] when idx === -1 is caught.
app/global-styles.js-16-16 (1)

16-16: ⚠️ Potential issue | 🟡 Minor

CSS custom properties lack fallback values — FOUC risk on SSR/hydration.

var(--musica-text) and var(--musica-bg) have no fallback. If ThemeContext hasn't injected the CSS variables yet (e.g., initial SSR render before client hydration), text renders with an undefined color and the background may flash. Providing a safe default (matching the default theme) prevents any visible flicker.

🔧 Proposed fix (using dark-theme defaults as an example)
-    color: var(--musica-text);
+    color: var(--musica-text, `#e8e8e8`);
 
 ...
 
-      color: var(--musica-text);
+      color: var(--musica-text, `#e8e8e8`);
 
 ...
 
-    background-color: var(--musica-bg);
+    background-color: var(--musica-bg, `#0e0e0e`);

Replace the fallback values with whatever the actual default palette tokens evaluate to for the initial theme.

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 custom properties used in
global-styles.js (color: var(--musica-text) and background: var(--musica-bg))
need safe fallbacks to avoid FOUC on SSR/hydration; update every occurrence of
var(--musica-text) and var(--musica-bg) to include explicit fallback values that
match the default theme palette (the initial theme tokens you use on first
render) so the text and background have deterministic colors before ThemeContext
injects variables.
app/components/ArtworkPlayButton/tests/index.test.js-37-46 (1)

37-46: ⚠️ Potential issue | 🟡 Minor

"Stop propagation" is never actually asserted.

The test sets up parentClick and fires a click, but only checks that defaultProps.onClick was called. The parentClick mock is never asserted, so the test does not verify that e.stopPropagation() worked. The test name is misleading without the missing assertion.

🐛 Proposed fix
   fireEvent.click(wrapper)
   expect(defaultProps.onClick).toHaveBeenCalledTimes(1)
+  expect(parentClick).not.toHaveBeenCalled()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ArtworkPlayButton/tests/index.test.js` around lines 37 - 46,
The test named "should call onClick and stop propagation" currently never
asserts propagation was stopped: after setting up parentClick on
wrapper.parentElement and firing fireEvent.click(wrapper), add an assertion that
parentClick was not called (e.g. expect(parentClick).not.toHaveBeenCalled()) to
verify e.stopPropagation() worked; keep the existing assertion that
defaultProps.onClick was called so both behaviors (ArtworkPlayButton onClick
handler and propagation stopping) are validated.
app/components/BackButton/index.js-6-10 (1)

6-10: ⚠️ Potential issue | 🟡 Minor

Add aria-hidden="true" to the decorative icon.

The button has an explicit aria-label, so ArrowLeftOutlined is purely decorative. Without aria-hidden, some screen reader / SVG combinations may announce the icon's internal title or description in addition to the button label. Marking it hidden is defensive and idiomatic for icon-only buttons.

♿ Proposed fix
 <BackBtn onClick={onClick} data-testid="back-button" aria-label="Go back">
-  <ArrowLeftOutlined />
+  <ArrowLeftOutlined aria-hidden="true" />
 </BackBtn>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/BackButton/index.js` around lines 6 - 10, The
ArrowLeftOutlined icon inside the BackButton is decorative and should be hidden
from assistive tech; update the BackButton render to add aria-hidden="true" (or
role="presentation") to the ArrowLeftOutlined element so screen readers only
announce the BackBtn's aria-label; locate the BackButton component and the
ArrowLeftOutlined usage and add the aria-hidden attribute to the icon element.
app/components/SearchBar/index.js-18-22 (1)

18-22: ⚠️ Potential issue | 🟡 Minor

value should be required or have a default to avoid the uncontrolled→controlled warning.

value is not marked .isRequired and has no defaultProps. When value={undefined}, React treats the input as uncontrolled; if the parent later provides a string, React will warn about the mode switch. Since onChange is required, the component is intended to be fully controlled.

🛡️ Proposed fix
 SearchBar.propTypes = {
-  value: PropTypes.string,
+  value: PropTypes.string.isRequired,
   onChange: PropTypes.func.isRequired,
   loading: PropTypes.bool
 };
🤖 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 prop "value" on
SearchBar is currently optional which can cause an uncontrolled→controlled React
warning; update SearchBar to either mark value as required in
SearchBar.propTypes (make value: PropTypes.string.isRequired) or add a default
prop (e.g., SearchBar.defaultProps = { value: '' }) so the input is always
controlled; change the propTypes/defaultProps near the SearchBar.propTypes
declaration to implement the chosen fix.
app/containers/Music/usePlayToggle.js-10-21 (1)

10-21: ⚠️ Potential issue | 🟡 Minor

Guard against null/undefined song in handlePlayToggle.

currentSong?.trackId uses optional chaining while song.trackId does not. If handlePlayToggle is ever called with a falsy song, the access on line 12 will throw a TypeError.

🛡️ Proposed fix
  const handlePlayToggle = useCallback(
    (song) => {
+     if (!song) return;
      if (currentSong?.trackId === song.trackId) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Music/usePlayToggle.js` around lines 10 - 21, The handler
handlePlayToggle can throw when called with a falsy song because it accesses
song.trackId without a guard; update handlePlayToggle to first check that song
is truthy (e.g., if (!song) return) before comparing song.trackId to
currentSong?.trackId, and keep using togglePlayRef.current() and
dispatchSetSong(song) as before so existing behavior is preserved.
app/utils/withAuth.js-19-21 (1)

19-21: ⚠️ Potential issue | 🟡 Minor

Returning null during the auth check causes a blank flash on every protected route.

Since isReady starts as false, every page wrapped with withAuth renders a blank screen until the useEffect fires and the token check completes. A loading skeleton or spinner is a straightforward improvement.

♻️ Proposed fix
  if (!isReady) {
-   return null;
+   return <YourLoadingSpinner />;  // or a page-level skeleton
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/withAuth.js` around lines 19 - 21, withAuth currently returns null
while isReady is false which causes a blank flash on protected routes; update
the withAuth HOC (the withAuth function and its internal isReady check) to
render a loading fallback instead of null — either use an existing
LoadingSpinner/LoadingSkeleton component or accept a loadingFallback prop and
render that while isReady is false, then proceed with the normal auth rendering
once token check completes.
app/utils/authStorage.js-5-24 (1)

5-24: ⚠️ Potential issue | 🟡 Minor

localStorage access is not wrapped in try/catch.

localStorage.setItem throws a QuotaExceededError when storage is full. In some restricted browser contexts getItem/setItem/removeItem can also throw a SecurityError. An unhandled exception here would propagate up to the auth flow.

🛡️ 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 unavailable or quota exceeded — fail silently
+  }
 };

 export const clearStoredToken = () => {
   if (!isBrowser()) return;
-  localStorage.removeItem(TOKEN_KEY);
+  try {
+    localStorage.removeItem(TOKEN_KEY);
+  } catch {
+    // Ignore
+  }
 };
🤖 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, Wrap all direct localStorage
accesses in try/catch inside getStoredToken, setStoredToken, and
clearStoredToken so any SecurityError/QuotaExceededError is caught; in
getStoredToken return null on error, in setStoredToken/clearStoredToken swallow
the error (or log it via a logger) and avoid throwing, and still guard with
isBrowser() first; reference TOKEN_KEY and isBrowser() when adding the try/catch
blocks so the functions consistently handle storage exceptions without leaking
them into the auth flow.
app/containers/Music/useToggleLike.js-4-13 (1)

4-13: ⚠️ Potential issue | 🟡 Minor

No null guard on song before accessing song.trackId.

If handleToggleLike is ever invoked without an argument (e.g., a stale event handler during unmount, or an incorrect call site), it will throw a TypeError. A minimal guard prevents an unhandled runtime error:

🛡️ Proposed defensive fix
     (song) => {
+      if (!song?.trackId) return;
       if (likedTrackIds[song.trackId]) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Music/useToggleLike.js` around lines 4 - 13, handleToggleLike
accesses song.trackId without guarding song; add a defensive null/undefined
check at the top of the handleToggleLike callback (e.g., return early if song is
falsy) so it never attempts to read song.trackId, then proceed to check
likedTrackIds and call dispatchLike/dispatchUnlike as before; reference the
handleToggleLike function and the likedTrackIds, dispatchLike, dispatchUnlike
symbols when making the change.
app/themes/palettes.js-10-21 (1)

10-21: ⚠️ Potential issue | 🟡 Minor

darkPalette has potentially inconsistent inputBg, placeholder, and border values.

Three values look suspect for a dark theme:

Token Dark value Light value Concern
inputBg #d2d2db (light-ish grey) #eeeef4 (very light) Dark-theme inputs typically have a dark background; #d2d2db is brighter than expected
placeholder #070708 (near-black) #b0b0c0 (muted grey) Near-black placeholder on a #d2d2db input is barely-legible and inverts the usual contrast expectation; may have been swapped with #0d0d0d bg
border #000000 (pure black) #d8d8e4 A pure-black border against a #0d0d0d background is indistinguishable

If the inputBg/placeholder values were accidentally swapped compared to intent, verify against design specs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/themes/palettes.js` around lines 10 - 21, darkPalette has likely
swapped/mischosen contrast tokens: inputBg ('#d2d2db') and placeholder
('#070708') are inverted for a dark theme and border ('#000000') is too
indistinguishable against bg; update the darkPalette object so inputBg is a
dark/low‑light color (near bg or surface), placeholder is a muted lighter grey
for readability, and border uses a slightly lighter dark grey (not pure black)
to create subtle separation—change the values in the darkPalette definition for
inputBg, placeholder, and border to match the light/dark contrast expectations.
app/components/styled/musicSearch.js-30-30 (1)

30-30: ⚠️ Potential issue | 🟡 Minor

Hardcoded #3d3d54 breaks the theming system.

Every other color in this file uses the C.* CSS variable tokens to respond to dark/light theme switching. The hover border color at line 30 is hardcoded, so it will render incorrectly in the light theme (a dark-purple border on a light input).

♻️ Proposed fix using a theme token
   &:hover:not(:focus) {
-    border-color: `#3d3d54`;
+    border-color: ${C.surface};
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/styled/musicSearch.js` at line 30, Replace the hardcoded
border-color "#3d3d54" in the styled component's hover rule with the appropriate
theme CSS variable (a C.* token used elsewhere in this file) so the input
responds to dark/light themes; locate the border-color in the musicSearch styled
component's hover rule and swap the literal value for the matching token (e.g.,
C.inputBorder or the token the file uses for input borders) so theming works
consistently.
app/components/ArtworkPlayButton/index.js-13-19 (1)

13-19: ⚠️ Potential issue | 🟡 Minor

ArtworkWrapper (div) has no role or aria-label, breaking keyboard/screen-reader accessibility.

A div with an onClick is not focusable or announced as interactive by default. Without role="button" and a descriptive aria-label, keyboard users can't reach it and screen readers won't announce the play/pause intent.

♿ Proposed fix
-    <ArtworkWrapper onClick={handleClick} data-testid="artwork-play-btn">
+    <ArtworkWrapper
+      onClick={handleClick}
+      role="button"
+      tabIndex={0}
+      aria-label={isActive && isPlaying ? `Pause ${alt}` : `Play ${alt}`}
+      onKeyDown={(e) => e.key === 'Enter' && handleClick(e)}
+      data-testid="artwork-play-btn"
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ArtworkPlayButton/index.js` around lines 13 - 19, The
ArtworkWrapper div is not accessible: make it an interactive button by adding
role="button", tabIndex={0}, and a descriptive aria-label that reflects the
current action (e.g., aria-label={`Play ${alt}`} or aria-label={isPlaying ?
'Pause' : 'Play'}) and consider adding aria-pressed={isPlaying} for toggle
state; also wire keyboard activation by handling keyDown (Enter/Space) to call
handleClick. Update the ArtworkWrapper element and related handlers (handleClick
and any parent component state using isPlaying) so screen readers announce the
control and keyboard users can activate it.
app/components/styled/authForm.js-55-57 (1)

55-57: ⚠️ Potential issue | 🟡 Minor

Hardcoded #3d3d54 breaks the theme abstraction.

Every other color in this file uses a C.* token, making this raw hex value a maintenance hazard — it won't respond to theme changes. Either add a dedicated token (e.g., C.inputHoverBorder) to colors.js, or alias it to the nearest existing token.

🐛 Proposed fix

In app/components/styled/colors.js:

 export const C = {
   ...
   border: 'var(--musica-border)',
+  inputHoverBorder: 'var(--musica-inputHoverBorder)',
   ...
 };

In authForm.js:

   &:hover:not(:focus) {
-    border-color: `#3d3d54`;
+    border-color: ${C.inputHoverBorder};
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/styled/authForm.js` around lines 55 - 57, The hover border
color in the styled auth form is a hardcoded hex (`#3d3d54`) which breaks theme
tokens; add a new color token (for example C.inputHoverBorder) to colors.js or
map it to the closest existing token (e.g., C.borderMuted) and then replace the
raw hex in the &:hover:not(:focus) rule inside authForm.js with that token (use
C.inputHoverBorder or the chosen existing C.* token) so the component respects
theme changes.
app/containers/Auth/saga.js-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor

failureAuth(response.data) dispatches null on network/timeout errors.

When the request fails at the network layer (timeout, no connection), apisauce sets response.ok = false and response.data is null or undefined. Dispatching failureAuth(null) means the reducer receives no error message, which may silently swallow the failure in the UI. Prefer a normalised fallback:

🛡️ Proposed fix
-    yield put(failureAuth(response.data));
+    yield put(failureAuth(response.data ?? { message: 'Something went wrong. Please try again.' }));

Apply this pattern in both handleLogin and handleSignup.

Also applies to: 37-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Auth/saga.js` around lines 25 - 26, The failureAuth dispatch
currently passes response.data which can be null for network/timeouts; update
both handleLogin and handleSignup to normalize the error before dispatching:
derive an errorMessage from response.data (if present) else fallback to
response.problem/response.statusText/or a static string like "Network error" (or
response.originalError if available), then call yield
put(failureAuth(errorMessage)); replace the direct failureAuth(response.data)
usages with this normalized error variable so reducers always receive a
meaningful error string/object.
app/utils/formatDuration.js-1-6 (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Add a guard for undefined/NaN input.

If ms is undefined (e.g., track.durationMs absent from an API response), Math.floor(undefined / 1000) evaluates to NaN and the function returns the visible string "NaN:NaN".

🛡️ Proposed fix
 export const formatDuration = (ms) => {
+  if (ms == null || isNaN(ms) || ms < 0) return '0:00';
   const totalSeconds = Math.floor(ms / 1000);
   const minutes = Math.floor(totalSeconds / 60);
   const seconds = totalSeconds % 60;
   return `${minutes}:${seconds.toString().padStart(2, '0')}`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/formatDuration.js` around lines 1 - 6, The formatDuration function
should guard against undefined/NaN inputs: inside formatDuration check that the
incoming ms is a finite number (e.g., via Number.isFinite(ms) or Number.isNaN)
and if not, set ms to 0 (or return a safe fallback like "0:00"); then proceed to
compute totalSeconds/minutes/seconds as before so the function never returns
"NaN:NaN". Ensure you update the ms validation in the formatDuration function.
app/components/TrackInfo/index.js-25-25 (1)

25-25: ⚠️ Potential issue | 🟡 Minor

Genre tag always renders — inconsistent with other optional tags

track.genre has no guard, so an empty <Tag> is emitted when genre is absent. All other metadata tags (durationMs, releaseDate, trackPrice) are conditionally rendered. Apply the same guard for consistency:

🐛 Proposed fix
-      {<Tag data-testid="tag-genre">{track.genre}</Tag>}
+      {track.genre && <Tag data-testid="tag-genre">{track.genre}</Tag>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/TrackInfo/index.js` at line 25, The Genre Tag always renders
an empty Tag when track.genre is falsy; update the TrackInfo component to
conditionally render the Tag only when track.genre exists. Locate the JSX using
Tag and track.genre (the Genre Tag line) and wrap it with the same guard pattern
used for durationMs/releaseDate/trackPrice so the Tag is only emitted when
track.genre is truthy. Ensure you keep the existing data-testid ("tag-genre") on
the Tag when it is rendered.
app/contexts/tests/ThemeContext.test.js-59-63 (1)

59-63: ⚠️ Potential issue | 🟡 Minor

Hardcoded storage key creates silent coupling

'musica_theme' is the private THEME_KEY from themeStorage.js. If it ever changes, these tests will continue to pass (localStorage will simply be empty) while the actual persistence feature is broken. Exporting THEME_KEY from themeStorage.js and importing it here removes the coupling.

🛠 Suggested fix

In app/utils/themeStorage.js:

-const THEME_KEY = 'musica_theme';
+export const THEME_KEY = 'musica_theme';

In app/contexts/tests/ThemeContext.test.js:

+import { THEME_KEY } from '../../utils/themeStorage'
 ...
-    expect(window.localStorage.getItem('musica_theme')).toBe('light')
+    expect(window.localStorage.getItem(THEME_KEY)).toBe('light')
 ...
-    window.localStorage.setItem('musica_theme', 'light')
+    window.localStorage.setItem(THEME_KEY, 'light')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/contexts/tests/ThemeContext.test.js` around lines 59 - 63, Export the
private THEME_KEY constant from app/utils/themeStorage.js and update the tests
in ThemeContext.test.js to import THEME_KEY (e.g., import { THEME_KEY } from
'app/utils/themeStorage') and use that constant in calls to
window.localStorage.setItem/getItem instead of the hardcoded 'musica_theme'
string; this ensures the test references the same key as the production code
(look for THEME_KEY in themeStorage.js and replace string usages in
ThemeContext.test.js).
app/containers/Library/reducer.js-63-72 (1)

63-72: ⚠️ Potential issue | 🟡 Minor

No handlers for REQUEST_LIKE_SONG / REQUEST_UNLIKE_SONG — like/unlike failures call stopLoading without a prior startLoading.

The handleFetchFailure handler (reused for like/unlike failures) calls stopLoading, but since no request handler sets loading = true for like/unlike, the stopLoading is a harmless no-op. However, the error will be set on failure without any mechanism to clear it on the next like/unlike attempt (no request handler resets error to null). This could leave a stale error message in the UI after a failed like/unlike, even if a subsequent attempt succeeds.

♻️ Proposed fix — add lightweight request handlers for like/unlike to clear errors
+const handleLikeRequest = (draft) => {
+  draft[PAYLOAD.ERROR] = null;
+};
+
 const handlers = {
   [libraryTypes.REQUEST_FETCH_LIBRARY]: handleFetchRequest,
   [libraryTypes.SUCCESS_FETCH_LIBRARY]: handleFetchSuccess,
   [libraryTypes.FAILURE_FETCH_LIBRARY]: handleFetchFailure,
+  [libraryTypes.REQUEST_LIKE_SONG]: handleLikeRequest,
   [libraryTypes.SUCCESS_LIKE_SONG]: handleLikeSuccess,
   [libraryTypes.FAILURE_LIKE_SONG]: handleFetchFailure,
+  [libraryTypes.REQUEST_UNLIKE_SONG]: handleLikeRequest,
   [libraryTypes.SUCCESS_UNLIKE_SONG]: handleUnlikeSuccess,
   [libraryTypes.FAILURE_UNLIKE_SONG]: handleFetchFailure,
   [libraryTypes.CLEAR_LIBRARY]: () => initialState
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Library/reducer.js` around lines 63 - 72, Add request handlers
for libraryTypes.REQUEST_LIKE_SONG and libraryTypes.REQUEST_UNLIKE_SONG in the
handlers map so like/unlike requests reset error and set loading state; either
create small functions (e.g., handleLikeRequest / handleUnlikeRequest) or use
inline handlers that return { ...state, loading: true, error: null } so
subsequent failures/successes correctly clear stale errors and pair with
existing handleLikeSuccess/handleUnlikeSuccess and handleFetchFailure behavior.
app/containers/TrackDetail/index.js-54-62 (1)

54-62: ⚠️ Potential issue | 🟡 Minor

Conditional blocks are not mutually exclusive — overlapping states can render simultaneously.

Since each <If> block is independent, if both loading and error are truthy at the same time (e.g., a fetch failure that doesn't clear loading), both the spinner and the error message will render. Consider using else-if semantics or a single conditional branch.

♻️ Proposed fix — use early returns or ternary for exclusive rendering
-        <If condition={loading}>
-          <LoadingSpinner data-testid="loading-spinner" />
-        </If>
-        <If condition={!!error}>
-          <EmptyState data-testid="error-state">Failed to load track details.</EmptyState>
-        </If>
-        <If condition={!loading && !error && !!trackData}>
-          <TrackInfo track={trackData || {}} onPlay={handlePlay} />
-        </If>
+        <If condition={loading} otherwise={
+          <If condition={!!error} otherwise={
+            <If condition={!!trackData}>
+              <TrackInfo track={trackData} onPlay={handlePlay} />
+            </If>
+          }>
+            <EmptyState data-testid="error-state">Failed to load track details.</EmptyState>
+          </If>
+        }>
+          <LoadingSpinner data-testid="loading-spinner" />
+        </If>

Alternatively, a simpler approach with plain ternaries may be more readable:

{loading ? (
  <LoadingSpinner data-testid="loading-spinner" />
) : error ? (
  <EmptyState data-testid="error-state">Failed to load track details.</EmptyState>
) : trackData ? (
  <TrackInfo track={trackData} onPlay={handlePlay} />
) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/TrackDetail/index.js` around lines 54 - 62, The three
independent <If> blocks in the TrackDetail render allow overlapping states
(e.g., loading and error) to display simultaneously; change the rendering to an
exclusive branch (early return or chained conditional) so only one state
renders: use loading first, then error, then trackData — replace the three <If>
blocks with a single conditional expression or early-return in the TrackDetail
component that renders LoadingSpinner when loading is true, EmptyState when
error is truthy, TrackInfo (passing trackData and handlePlay) when trackData
exists, otherwise null.
app/components/AudioPlayer/useAudioPlayer.js-67-71 (1)

67-71: ⚠️ Potential issue | 🟡 Minor

seek does not update the currentTime state.

Setting audioRef.current.currentTime will trigger the timeupdate event asynchronously, so the UI slider value won't immediately reflect the seek position, causing a brief visual snap-back on the progress slider. Consider also calling setCurrentTime(time) for immediate feedback.

♻️ Proposed fix
   const seek = useCallback((time) => {
     if (audioRef.current) {
       audioRef.current.currentTime = time;
+      setCurrentTime(time);
     }
   }, []);
🤖 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 67 - 71, The seek
function only sets audioRef.current.currentTime which updates asynchronously;
update UI immediately by also calling setCurrentTime(time) inside useCallback so
the slider reflects the new position instantly. Modify the seek callback
(function seek) to call setCurrentTime(time) after verifying audioRef.current
exists, keeping the existing audioRef.current.currentTime = time assignment so
the actual media position still changes. Ensure you import/use the same
setCurrentTime state updater that the component uses for tracking currentTime so
the UI state and media position stay in sync.
app/components/AudioPlayer/index.js-65-65 (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Use CSS variable for accent color to maintain theme consistency.

The hardcoded color #ff6b35 bypasses the theme system. All theme colors should use var(--musica-accent) which is dynamically set based on the active palette.

♻️ Proposed fix
-        <SoundFilled data-testid="volume-icon" style={{ color: '#ff6b35', fontSize: '1rem' }} />
+        <SoundFilled data-testid="volume-icon" style={{ color: 'var(--musica-accent)', fontSize: '1rem' }} />
🤖 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 65, The volume icon uses a
hardcoded color '#ff6b35' in the SoundFilled element
(data-testid="volume-icon"), which bypasses theme variables; update the inline
style in the SoundFilled component to use the CSS variable var(--musica-accent)
for color (e.g., style={{ color: 'var(--musica-accent)', fontSize: '1rem' }}) so
the icon follows the active palette and theme system.

Comment on lines +11 to +38
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 (onPlayStateChange) {
onPlayStateChange(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
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stale closure: onSongEnd and onPlayStateChange are captured once at mount and never updated.

The first useEffect has an empty dependency array, so the onEnded handler permanently captures the initial onSongEnd and onPlayStateChange references from the first render. If these callbacks change (e.g., parent re-renders with new props), the audio ended event will invoke the stale versions.

Store callbacks in refs to keep them current:

🐛 Proposed fix
+  const onSongEndRef = useRef(onSongEnd);
+  const onPlayStateChangeRef = useRef(onPlayStateChange);
+
+  useEffect(() => { onSongEndRef.current = onSongEnd; }, [onSongEnd]);
+  useEffect(() => { onPlayStateChangeRef.current = onPlayStateChange; }, [onPlayStateChange]);
+
   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 (onPlayStateChange) {
-        onPlayStateChange(false);
+      if (onPlayStateChangeRef.current) {
+        onPlayStateChangeRef.current(false);
       }
-      if (onSongEnd) {
-        onSongEnd();
+      if (onSongEndRef.current) {
+        onSongEndRef.current();
       }
     };

Apply the same pattern for onPlayStateChange used inside the song-change effect on line 46.

🤖 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 - 38, The audio
ended handler inside the useEffect mounts with stale references to onSongEnd and
onPlayStateChange; fix it by storing those callbacks in refs (e.g., onSongEndRef
and onPlayStateChangeRef), update those refs whenever the props change, and have
the onEnded closure call the current ref values (onSongEndRef.current,
onPlayStateChangeRef.current) instead of the captured variables; update the
effect that sets up listeners (the useEffect that creates audio and adds
'ended') to read from the refs so the latest callbacks run while keeping the
same mount/unmount listener logic.

Comment on lines +58 to +68
it('should navigate to track detail when card clicked', () => {
const { getByTestId } = renderProvider(<SongList {...defaultProps} />)
fireEvent.click(getByTestId('song-1'))
expect(mockPush).toHaveBeenCalledWith('/track/1')
})

it('should call onPlayToggle when artwork clicked', () => {
const { getAllByTestId } = renderProvider(<SongList {...defaultProps} />)
fireEvent.click(getAllByTestId('artwork-play-btn')[0])
expect(mockPlayToggle).toHaveBeenCalledWith(mockSongs[0])
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Event bubbling: clicking play button will also trigger card navigation.

Looking at the SongList component (app/components/SongList/index.js), ArtworkPlayButton's onClick is nested inside SongCard's onClick. Without e.stopPropagation(), clicking the play button will also fire handleCardClick, navigating the user away. The same applies to the HeartButton. These tests pass in isolation because the mock doesn't simulate bubbling fully, but the real behavior would trigger both handlers.

Consider adding e.stopPropagation() in the ArtworkPlayButton and HeartButton onClick wrappers inside SongList.

#!/bin/bash
# Check SongList component for stopPropagation usage
ast-grep --pattern $'onClick={($_) => {
  $$$
  stopPropagation
  $$$
}}'

rg -n 'stopPropagation' --type=js -g '**/SongList/**'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/SongList/tests/index.test.js` around lines 58 - 68, The
SongList component currently nests ArtworkPlayButton and HeartButton click
handlers inside SongCard's onClick (handleCardClick), causing clicks on those
buttons to bubble and trigger navigation; update the onClick wrappers for
ArtworkPlayButton and HeartButton in SongList (the arrow functions passed as
onClick for ArtworkPlayButton and HeartButton) to accept the event param and
call e.stopPropagation() before invoking onPlayToggle/onHeartToggle so the
button clicks do not propagate to handleCardClick and trigger navigation.

required
/>
</InputWrapper>
{error && <ErrorMessage data-testid="login-error">{error}</ErrorMessage>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for the login page file
fd -type f -name "*.js" -path "*/pages/*" | grep -i login | head -20

Repository: wednesday-solutions/nextjs-template

Length of output: 249


🏁 Script executed:

# Also search for LoginForm usage in pages
rg "LoginForm" --type=js -A 3 -B 1 pages/ 2>/dev/null | head -50

Repository: wednesday-solutions/nextjs-template

Length of output: 699


🏁 Script executed:

# Search for selectAuthError usage
rg "selectAuthError" --type=js -A 2 -B 2 2>/dev/null | head -80

Repository: wednesday-solutions/nextjs-template

Length of output: 2662


🏁 Script executed:

# Look at the LoginForm component to see PropTypes and error handling
rg "PropTypes\|error" app/containers/Auth/LoginForm.js -A 1 -B 1 | head -40

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Find and examine the auth saga
rg "failureAuth\|PAYLOAD" app/containers/Auth/saga.js -A 2 -B 1 | head -80

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Check the auth reducer for PAYLOAD constant and how error is stored
rg "PAYLOAD\|ERROR" app/containers/Auth/reducer.js -A 2 -B 1 | head -100

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Look at LoginForm.js PropTypes declaration (lines 70-74 approximately)
sed -n '65,80p' app/containers/Auth/LoginForm.js

Repository: wednesday-solutions/nextjs-template

Length of output: 269


🏁 Script executed:

# Also get the full picture of what happens in saga with error handling
rg "failureAuth|catch|response" app/containers/Auth/saga.js -A 3 -B 1 | head -100

Repository: wednesday-solutions/nextjs-template

Length of output: 779


🏁 Script executed:

# Search for authCreators definition and failureAuth action
rg "authCreators\|failureAuth" app/containers/Auth/ --type-list | head -5

Repository: wednesday-solutions/nextjs-template

Length of output: 468


🏁 Script executed:

# Find the reducer file and see how it handles failureAuth
fd -name "reducer.js" app/containers/Auth/

Repository: wednesday-solutions/nextjs-template

Length of output: 309


🏁 Script executed:

# Check the Auth container structure
ls -la app/containers/Auth/

Repository: wednesday-solutions/nextjs-template

Length of output: 549


🏁 Script executed:

# Search for how authCreators is defined
rg -n "const authCreators\|export.*failureAuth\|failureAuth.*=" app/containers/Auth/ -A 3 -B 1 | head -100

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Read the full reducer file
cat app/containers/Auth/reducer.js

Repository: wednesday-solutions/nextjs-template

Length of output: 1352


🏁 Script executed:

# Read the full saga file
cat app/containers/Auth/saga.js

Repository: wednesday-solutions/nextjs-template

Length of output: 1410


🏁 Script executed:

# Search for where action creators are defined
rg "successAuth\|failureAuth" app/containers/Auth/reducer.js -A 2 -B 2

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Check the utils/reducer file to understand setError
rg "setError\|PAYLOAD" app/utils/reducer.js -A 3 -B 1

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

# Also check the full utils/reducer.js file if short enough
wc -l app/utils/reducer.js

Repository: wednesday-solutions/nextjs-template

Length of output: 102


🏁 Script executed:

# Read the full utils/reducer.js
cat app/utils/reducer.js

Repository: wednesday-solutions/nextjs-template

Length of output: 597


Verify that error is always a string before being passed to LoginForm.

The component renders {error} directly and declares PropTypes.string. However, the auth saga dispatches failureAuth(response.data) where response.data is the raw API error payload — typically an object (e.g., { message: '...' }). This object gets stored in the state and passed directly to LoginForm without extraction. If the API returns an error object instead of a string, React will throw "Objects are not valid as a React child" at runtime.

The chain is: sagafailureAuth(response.data)state.error = response.dataselectAuthError()error prop → rendered as {error}.

Either extract the error message string in the saga (e.g., failureAuth(response.data?.message || 'something_went_wrong')) before dispatching, or ensure the API response shape is normalized before reaching this component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Auth/LoginForm.js` at line 55, The LoginForm is rendering the
error prop directly (LoginForm -> {error}) but the saga dispatches
failureAuth(response.data) where response.data can be an object, causing
"Objects are not valid as a React child"; update the saga that calls failureAuth
to pass a string instead (e.g., extract response.data.message or fallback to a
default like 'Something went wrong') before dispatching, or normalize
state.error in the reducer/selector (selectAuthError) to return a string, so
LoginForm always receives a string error prop.

Comment on lines +45 to +54
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
)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FAILURE_AUTH test will fail — error is never supplied in the dispatched action.

The action { type: authTypes.FAILURE_AUTH } carries no error field, so setError(draft, action) sets draft[PAYLOAD.ERROR] to undefined. The expected result asserts [PAYLOAD.ERROR]: 'something_went_wrong', which will never match.

🐛 Proposed fix — supply the error in the action
  it('should set error when FAILURE_AUTH is dispatched', () => {
+   const errorMessage = 'something_went_wrong'
    const expectedResult = {
      ...state,
-     [PAYLOAD.ERROR]: 'something_went_wrong',
+     [PAYLOAD.ERROR]: errorMessage,
      loading: false
    }
-   expect(authReducer(state, { type: authTypes.FAILURE_AUTH })).toEqual(
+   expect(authReducer(state, { type: authTypes.FAILURE_AUTH, [PAYLOAD.ERROR]: errorMessage })).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 test for
FAILURE_AUTH dispatches an action without an error so authReducer (and its
helper setError) sets PAYLOAD.ERROR to undefined; update the test to dispatch
the error payload (include an error property equal to 'something_went_wrong' on
the action passed to authReducer for authTypes.FAILURE_AUTH) so the resulting
state matches expectedResult that checks [PAYLOAD.ERROR] and loading: false.

Comment on lines +59 to +76
<If condition={loading}>
<LoadingSpinner data-testid="loading-spinner" />
</If>
<If condition={!loading && likedSongs.length > 0}>
<SongList
songs={likedSongs}
currentSong={currentSong}
isPlaying={isPlaying}
onPlayToggle={handlePlayToggle}
likedTrackIds={likedTrackIds}
onToggleLike={handleToggleLike}
/>
</If>
<If condition={!loading && likedSongs.length === 0}>
<EmptyState data-testid="empty-library">
No liked songs yet. Search and like songs to build your library.
</EmptyState>
</If>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for selectLibraryError in the codebase
rg -n 'selectLibraryError' --type=js

Repository: wednesday-solutions/nextjs-template

Length of output: 508


🏁 Script executed:

#!/bin/bash
# Read the Library component file to see the complete mapStateToProps and imports
wc -l app/containers/Library/index.js

Repository: wednesday-solutions/nextjs-template

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Check if the file exists and get its structure
head -100 app/containers/Library/index.js

Repository: wednesday-solutions/nextjs-template

Length of output: 3731


🏁 Script executed:

#!/bin/bash
# Read the remaining part of the file to see the mapStateToProps
tail -30 app/containers/Library/index.js

Repository: wednesday-solutions/nextjs-template

Length of output: 1109


Add error state handling to prevent misleading empty state on fetch failure.

selectLibraryError exists in selectors.js but is not imported or used in mapStateToProps. If fetchLibrary fails, users see the "No liked songs yet" empty state instead of an error message, which is misleading. Import the selector and render an error state when a fetch fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Library/index.js` around lines 59 - 76, Import
selectLibraryError and include it in mapStateToProps (e.g., mapStateToProps ->
error: selectLibraryError(state)), pass the error prop into the Library
component, and add an early render branch in the JSX to display an error state
when error is truthy (e.g., render an ErrorState/ErrorMessage component or
element) before the empty-state branch so a fetch failure shows an error instead
of "No liked songs yet"; update any tests or data-testid usage as needed.

Comment on lines +6 to +11
const handleNext = useCallback(() => {
const idx = findIdx();
if (idx < songs.length - 1) {
dispatchSetSong(songs[idx + 1]);
}
}, [songs, findIdx, dispatchSetSong]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

handleNext dispatches the wrong song when currentSong is absent from songs.

When findIdx() returns -1 (e.g., when the Library container passes likedSongs but currentSong is from a global search result), the guard idx < songs.length - 1 evaluates to true for any non-empty list, causing songs[0] to be dispatched silently.

🐛 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]);
📝 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
const handleNext = useCallback(() => {
const idx = findIdx();
if (idx < songs.length - 1) {
dispatchSetSong(songs[idx + 1]);
}
}, [songs, findIdx, dispatchSetSong]);
const handleNext = useCallback(() => {
const idx = findIdx();
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
logic dispatches an incorrect song when findIdx() returns -1; change the guard
in handleNext (function handleNext) to ensure idx is a valid index before
advancing (i.e., require idx >= 0 && idx < songs.length - 1) and do nothing (or
return early) when idx === -1 so dispatchSetSong(songs[idx + 1]) is never called
for an absent currentSong; update any related callers that assume a next song
only when findIdx() yields a valid index.

Comment thread app/utils/apiUtils.js
Comment on lines +19 to +23
case 'auth':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
return apiClients[type];
case 'music':
apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded localhost:9000 will break in any non-local deployment.

Both 'auth' and 'music' clients use 'http://localhost:9000' as baseURL. This needs to be driven by environment variables so staging and production can be configured correctly.

🔧 Proposed fix
-    case 'auth':
-      apiClients[type] = createApiClientWithTransForm('http://localhost:9000');
+    case 'auth':
+      apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_AUTH_URL);
       return apiClients[type];
-    case 'music':
-      apiClients[type] = createApiClientWithTransForm('http://localhost:9000', { skipRequestTransform: true });
+    case 'music':
+      apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_MUSIC_URL, { skipRequestTransform: true });
       return apiClients[type];

Add NEXT_PUBLIC_AUTH_URL and NEXT_PUBLIC_MUSIC_URL to .env.local, .env.staging, and .env.production.

📝 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 });
case 'auth':
apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_AUTH_URL);
return apiClients[type];
case 'music':
apiClients[type] = createApiClientWithTransForm(process.env.NEXT_PUBLIC_MUSIC_URL, { skipRequestTransform: true });
🤖 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 - 23, Replace the hardcoded
'http://localhost:9000' in the switch cases for 'auth' and 'music' by reading
their base URLs from environment variables; update the 'auth' case and the
'music' case where apiClients[type] is assigned via createApiClientWithTransForm
to use process.env.NEXT_PUBLIC_AUTH_URL and process.env.NEXT_PUBLIC_MUSIC_URL
respectively (optionally with a local fallback), and ensure those env vars are
documented/added to .env.local/.env.staging/.env.production so
staging/production use the correct endpoints.

Comment thread app/utils/authStorage.js
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
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Storing the auth token in localStorage exposes it to XSS.

Any script running on the page (including injected third-party or XSS payloads) can read localStorage and exfiltrate the token. HttpOnly cookies are immune to this class of attack because they are never accessible to JavaScript.

If migrating to HttpOnly cookies is out of scope for this PR, the risk should at minimum be documented, and an issue should be created to revisit this before the app handles real user credentials.

🤖 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 helpers TOKEN_KEY,
isBrowser, getStoredToken, setStoredToken, and clearStoredToken currently
persist the auth token to localStorage which exposes it to XSS; replace this by
moving token storage to HttpOnly, Secure cookies set by the server (use
Set-Cookie on login/refresh and rely on cookie sending rather than reading via
JS) and remove client-side reads/writes to localStorage, or if server-side
cookie migration is out of scope: update these functions to no longer store
sensitive tokens, add an explicit code comment documenting the XSS risk, and
open a ticket to implement HttpOnly cookie-based auth (include steps: issue
should require server endpoints to set/clear Secure, HttpOnly cookies on auth
and refresh and update client to stop reading localStorage).

Comment thread environments/.env.qa
Comment on lines +2 to +3
NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same PostHog key across all three environments will mix production and non-production analytics data.

The PostHog project API key (phc_…) is a write-only key designed to be safe for public client-side use. However, two separate problems exist here:

  1. Environment data pollution: The identical key phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8 appears verbatim in .env.qa, .env.development, and .env.production. All three environments send events to the same PostHog project, making it impossible to isolate production user behaviour from dev/QA noise. PostHog recommends creating multiple projects for sandbox and production environments — the sandbox API key should point to a separate PostHog project used for testing.

  2. Gitleaks CI failure: Gitleaks (8.30.0) flags line 2 as a high-severity Generic API Key, which will fail any CI pipeline that runs secret scanning. Even though the key is write-only, it should be injected at build time via CI/CD secret variables rather than hard-coded in source files.

🛠️ Suggested remediation

Remove the hard-coded values from all three env files and inject them via your CI/CD platform (e.g., GitHub Actions secrets, AWS SSM):

-NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8
-NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
+NEXT_PUBLIC_POSTHOG_KEY=
+NEXT_PUBLIC_POSTHOG_HOST=

Use distinct PostHog projects (and therefore distinct keys) for development, QA, and production.

📝 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
NEXT_PUBLIC_POSTHOG_KEY=phc_s5dpMW7DvBDzwFpo0dWrj59N2iFi6aj59yn4bW8gmP8
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 3-3: [UnorderedKey] The NEXT_PUBLIC_POSTHOG_HOST key should go before the NEXT_PUBLIC_POSTHOG_KEY key

(UnorderedKey)

🪛 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.qa` around lines 2 - 3, Remove the hard-coded PostHog API
key from environments/.env.qa (and the other .env files) and replace with a
reference to an injected secret; specifically stop committing
NEXT_PUBLIC_POSTHOG_KEY and ensure NEXT_PUBLIC_POSTHOG_HOST remains configurable
if needed. Provision separate PostHog projects/keys for production vs
QA/development and configure your CI/CD (e.g., GitHub Actions secrets or AWS
SSM) to inject the appropriate NEXT_PUBLIC_POSTHOG_KEY per environment at
build/deploy time to avoid mixing telemetry and to satisfy secret-scanning
(gitleaks) requirements.

Comment thread package.json
"npm run lint:eslint:fix",
"git add --force",
"jest --findRelatedTests --passWithNoTests $STAGED_FILES"
"jest --findRelatedTests -u --passWithNoTests $STAGED_FILES"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

-u in pre-commit silently auto-updates snapshots, masking regressions; $STAGED_FILES is a no-op.

Two problems on this line:

  1. -u / --updateSnapshot in the pre-commit hook — any component whose rendered output has changed (intentionally or not) will have its snapshot automatically overwritten and committed without any developer warning. This defeats the purpose of snapshot tests as a regression guard. Snapshot updates should only happen when a developer runs jest -u manually and reviews the diff. Remove -u from the pre-commit command.

  2. $STAGED_FILES — lint-staged does not set this environment variable. lint-staged takes care of passing the file paths of the staged files itself; your linter-command should just be written to run for a given file path. The variable expands to an empty string in the shell; staged files are still correctly appended by lint-staged as positional arguments, so the test run is functionally unaffected, but the token is misleading and should be removed.

🛠️ Proposed fix
-      "jest --findRelatedTests -u --passWithNoTests $STAGED_FILES"
+      "jest --findRelatedTests --passWithNoTests"
📝 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"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 34, Remove the snapshot auto-update flag and the
misleading env var from the pre-commit jest command: in package.json update the
lint-staged / pre-commit entry that currently runs "jest --findRelatedTests -u
--passWithNoTests $STAGED_FILES" by deleting the "-u" / "--updateSnapshot" token
and removing "$STAGED_FILES" so the command becomes "jest --findRelatedTests
--passWithNoTests" (lint-staged will supply file paths).

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