Skip to content

Feature/theme toggle#26

Open
aman-wednesdaysol wants to merge 1 commit intowednesday-solutions:masterfrom
aman-wednesdaysol:feature/theme-toggle
Open

Feature/theme toggle#26
aman-wednesdaysol wants to merge 1 commit intowednesday-solutions:masterfrom
aman-wednesdaysol:feature/theme-toggle

Conversation

@aman-wednesdaysol
Copy link

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

Summary by CodeRabbit

  • New Features
    • Added a theme toggle control allowing users to switch between light and dark display modes.
    • Theme preference is automatically saved and persists across browser sessions.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

The PR introduces a complete theme management system featuring a Redux reducer that manages light/dark theme state with localStorage persistence, memoized selectors for accessing theme data, and a ThemeToggle component that dispatches theme-change actions when clicked.

Changes

Cohort / File(s) Summary
Theme Management System
app/containers/Theme/reducer.js, app/containers/Theme/selectors.js, app/components/ThemeToggle/index.js
Adds Redux reducer with SET_THEME and TOGGLE_THEME actions that persist theme to localStorage and update DOM data-theme attribute. Includes memoized selectors (selectTheme, selectIsDarkMode) for accessing theme state. React component provides a styled toggle button connected to Redux that dispatches toggleTheme action.

Sequence Diagram

sequenceDiagram
    actor User
    participant ThemeToggle as ThemeToggle<br/>Component
    participant Redux as Redux Store<br/>(Reducer)
    participant LocalStorage
    participant DOM

    User->>ThemeToggle: Click toggle button
    ThemeToggle->>Redux: dispatch toggleTheme()
    Redux->>Redux: Update state<br/>(light ↔ dark)
    Redux->>LocalStorage: persistTheme()<br/>(save to localStorage)
    Redux->>DOM: Set data-theme<br/>attribute
    Redux-->>ThemeToggle: Notify state change
    ThemeToggle->>ThemeToggle: Re-render with<br/>new theme
    ThemeToggle-->>User: Display updated theme
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A toggle I've crafted with Redux so neat,
Where themes dance between dark and light's sweet retreat,
localStorage whispers what users adore,
And selectors sing softly forevermore! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/theme toggle' directly reflects the main objective of the pull request, which adds a complete theme toggle feature including a ThemeToggle component, Redux reducer, and selectors.

✏️ 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

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 delivers a foundational set of features for a music streaming application, focusing on user interaction and core functionality. It establishes a robust authentication system, a dynamic and themeable user interface, and a fully functional music playback experience. The changes also include significant backend integration for music data and crucial security updates to ensure a stable and secure platform.

Highlights

  • Authentication System: Implemented full user authentication flow including login, signup, and email verification pages, integrated with Redux for state management and new API services.
  • Dynamic Layout & Navigation: Introduced a new main application layout with a persistent sidebar, a global search bar, and a theme toggle, enhancing user experience and navigation.
  • Music Playback Functionality: Developed a comprehensive music player with play/pause, next/previous, volume control, and progress tracking, along with components for displaying song cards and grids.
  • Theme Toggling & Global Styles: Added light and dark theme support using CSS variables, along with new fonts, atmospheric backgrounds, and subtle animations across the application.
  • Search & Trending Tracks: Integrated search functionality for songs with pagination and a home page displaying trending tracks based on user's geolocation.
  • Security Vulnerability Mitigation: Addressed several critical security vulnerabilities (RCE, Prototype Pollution, Unsafe Random Function) by updating dependencies and adding Yarn resolutions.

🧠 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
  • .cursor/frontend.md
    • Added a new file outlining frontend aesthetic guidelines.
  • app/components/Auth/LoginForm.js
    • Added a new React component for user login, integrated with Redux for state management and routing.
  • app/components/Auth/SignupForm.js
    • Added a new React component for user signup, integrated with Redux for state management and routing.
  • app/components/Layout/index.js
    • Added a new React component for the main application layout, including sidebar, search bar, and theme toggle, with theme initialization logic.
  • app/components/MusicPlayer/index.js
    • Added a new React component for the music player, integrating audio playback logic, progress, volume control, and playlist navigation with Redux.
  • app/components/ProtectedRoute/index.js
    • Added a new React component to protect routes based on user authentication status.
  • app/components/Recommended/tests/snapshots/index.test.js.snap
    • Updated a snapshot test for the Recommended component, changing a border color.
  • app/components/SearchBar/index.js
    • Added a new React component for a search bar with debouncing and Next.js router integration.
  • app/components/Sidebar/index.js
    • Added a new React component for the application sidebar navigation.
  • app/components/SongCard/index.js
    • Added a new React component to display individual song information and playback controls.
  • app/components/SongGrid/index.js
    • Added a new React component to display a grid of songs, including loading and empty states, and song normalization.
  • app/components/ThemeToggle/index.js
    • Added a new React component for toggling between light and dark themes.
  • app/components/controls/PlayControls.js
    • Added a new React component for music player control buttons (previous, play/pause, next).
  • app/components/css/controlscss.js
    • Added a new file defining styled-components for music player controls and search-related UI elements.
  • app/containers/Auth/reducer.js
    • Added a new Redux reducer for authentication state management, including login, signup, logout, and email verification.
  • app/containers/Auth/saga.js
    • Added a new Redux Saga for handling authentication requests (login, signup) and API interactions.
  • app/containers/Auth/selectors.js
    • Added new Redux selectors for accessing authentication state.
  • app/containers/Home/reducer.js
    • Added a new Redux reducer for managing home page state, specifically trending tracks.
  • app/containers/Home/saga.js
    • Added a new Redux Saga for fetching trending tracks.
  • app/containers/Home/selectors.js
    • Added new Redux selectors for accessing home page state.
  • app/containers/MusicPlayer/reducer.js
    • Added a new Redux reducer for managing music player state, including current song, playlist, playback status, and volume.
  • app/containers/MusicPlayer/selectors.js
    • Added new Redux selectors for accessing music player state.
  • app/containers/Repos/tests/snapshots/index.test.js.snap
    • Updated a snapshot test for the Repos component, changing a text color and button class.
  • app/containers/Search/reducer.js
    • Added a new Redux reducer for managing search state, including search term, results, pagination, and loading status.
  • app/containers/Search/saga.js
    • Added a new Redux Saga for handling song search requests and pagination.
  • app/containers/Search/selectors.js
    • Added new Redux selectors for accessing search state.
  • app/containers/Theme/reducer.js
    • Added a new Redux reducer for managing theme state (light/dark) and persisting it to local storage.
  • app/containers/Theme/selectors.js
    • Added new Redux selectors for accessing theme state.
  • app/global-styles.js
    • Modified global styles to implement CSS variables for theme support, add new font imports, and include new animations and scrollbar styling.
  • app/reducers.js
    • Modified the root Redux reducer to include new reducers for auth, theme, search, musicPlayer, and home.
  • app/services/authApi.js
    • Added a new API service for handling user authentication (signup, login).
  • app/services/musicApi.js
    • Added a new API service for searching songs and fetching trending tracks.
  • app/themes/colors.js
    • Modified the color theme definition to use CSS variables for dynamic theme switching.
  • app/utils/apiUtils.js
    • Modified API utility to support multiple API clients (github, music) and include authorization headers.
  • app/utils/authUtils.js
    • Added new utility functions for JWT token management, including decoding, storage, and session handling.
  • app/utils/geolocation.js
    • Added new utility functions for determining and storing user's country code.
  • app/utils/useAudio.js
    • Added a new React hook for managing audio playback logic.
  • app/utils/useMusicNavigation.js
    • Added a new React hook for handling music player navigation (previous/next song).
  • app/utils/useProgress.js
    • Added a new React hook for managing music playback progress bar interactions.
  • docs/adr/2026-02-17-fix-critical-vulnerabilities.md
    • Added a new Architecture Decision Record documenting critical vulnerability mitigations.
  • package.json
    • Updated several dependencies, including apisauce, next, eslint-config-next, lint-staged, and added axios and coveralls. Also added resolutions for critical vulnerability fixes.
  • pages/_app.js
    • Modified the main App component to include Redux dispatch/selector hooks, implement theme and authentication bootstrapping, and conditionally render the MusicPlayer.
  • pages/_document.js
    • Modified the Document component to import Google Fonts (Sora).
  • pages/auth.js
    • Added a new page for authentication, dynamically rendering LoginForm or SignupForm based on URL query.
  • pages/index.js
    • Modified the Home page to fetch and display trending tracks using SongGrid, integrate with the music player, and use geolocation for country detection.
  • pages/library.js
    • Added a new protected page for displaying the user's music library.
  • pages/login.js
    • Added a new page for user login with animated background and LoginForm component.
  • pages/search.js
    • Added a new page for song search functionality, including search bar, results grid, and "load more" pagination.
  • pages/signup.js
    • Added a new page for user signup with animated background and SignupForm component.
  • pages/verify-email.js
    • Added a new page for email verification, guiding users to confirm their account.
Activity
  • The pull request introduces a significant amount of new code, indicating a major feature implementation.
  • Many new files were added across components, containers, services, and utility folders, suggesting a broad expansion of application capabilities.
  • Existing core files like app/global-styles.js, app/reducers.js, app/themes/colors.js, app/utils/apiUtils.js, pages/_app.js, and pages/_document.js were modified to integrate the new features and styling.
  • Dependency updates were performed in package.json, including upgrades to apisauce, next, eslint-config-next, and lint-staged, and the addition of axios and coveralls.
  • Yarn resolutions were added in package.json to mitigate several critical security vulnerabilities in simple-git, property-expr, minimist, and form-data.
  • Snapshot tests were updated for app/components/Recommended/tests/__snapshots__/index.test.js.snap and app/containers/Repos/tests/__snapshots__/index.test.js.snap, reflecting UI changes.
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.

Copy link

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces new features including authentication, a theming system with a toggle, and a music player UI. However, it contains a critical authentication bypass vulnerability in the magic link login logic, allowing for full account takeover, and a privacy issue regarding unauthorized geolocation tracking, both of which require immediate attention. Furthermore, a critical syntax error in package.json needs to be fixed. Other recommendations include improving code reuse in authentication forms, ensuring proper client-side navigation, consistently applying the new theming system by removing hardcoded colors, and refining component logic to adhere to React best practices.

pages/_app.js Outdated
Comment on lines +31 to +54
function handleMagicLinkLogin(dispatch, parsed) {
if (!parsed?.accessToken) return false;

const payload = decodeJwtPayload(parsed.accessToken);
const email = payload?.email || null;

const verifiedUser = {
email,
email_confirmed_at: new Date().toISOString(),
confirmed_at: new Date().toISOString()
};

dispatch(
authActionCreators.loginSuccess(verifiedUser, parsed.accessToken)
);

window.history.replaceState(
null,
document.title,
window.location.pathname + window.location.search
);

return true;
}

Choose a reason for hiding this comment

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

security-critical critical

CRITICAL: Authentication Bypass / Account Takeover. The handleMagicLinkLogin function decodes a JWT from the URL hash and logs the user in based on the payload's email address WITHOUT verifying the cryptographic signature. An attacker can craft a malicious JWT with any email address (e.g., an administrator's) and append it to the URL hash to gain unauthorized access to any account.

Remediation: Never trust JWT payloads on the client side without verification. The token should be sent to the server for verification, or a secure authentication library should be used to handle the login flow.

Comment on lines +20 to +135
const FormContainer = styled.form`
width: 100%;
max-width: 420px;
padding: 48px;
background: transparent;
transition: all var(--transition-base);

@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
animation: fadeInUp 0.6s ease 0.2s forwards;
opacity: 0;
`;

const Title = styled.h1`
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
color: var(--color-text);
text-align: center;
`;

const Subtitle = styled.p`
font-size: 15px;
color: var(--color-text-secondary);
text-align: center;
margin-bottom: 32px;
`;

const FormGroup = styled.div`
margin-bottom: 20px;
`;

const Label = styled.label`
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: var(--color-text);
`;

const Input = styled.input`
width: 100%;
padding: 12px 16px;
font-size: 15px;
border: 1px solid var(--color-border);
border-radius: 8px;
background-color: var(--color-background);
color: var(--color-text);
font-family: inherit;
transition: all var(--transition-fast);

&:focus {
outline: none;
border-color: var(--color-accent);
}

&::placeholder {
color: var(--color-text-secondary);
}
`;

const ErrorMessage = styled.div`
color: #dc3545;
font-size: 13px;
margin-top: 8px;
padding: 8px;
background-color: rgba(220, 53, 69, 0.1);
border-radius: 6px;
`;

const SubmitButton = styled.button`
width: 100%;
padding: 12px;
font-size: 15px;
font-weight: 500;
color: #ffffff;
background-color: var(--color-accent);
border: none;
border-radius: 8px;
cursor: pointer;
transition: all var(--transition-fast);
margin-top: 8px;

&:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}

&:active:not(:disabled) {
transform: translateY(0);
}

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;

const LinkText = styled.p`
text-align: center;
margin-top: 24px;
font-size: 13px;
color: var(--color-text-secondary);

a {
color: var(--color-accent);
text-decoration: none;
font-weight: 500;

&:hover {
text-decoration: underline;
}
}
`;

Choose a reason for hiding this comment

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

high

This component and LoginForm.js have several areas for improvement:

  1. Code Duplication: Almost all styled-components are identical in both files. To improve maintainability, you should extract them into a shared file, like app/components/Auth/Form.styles.js.
  2. Navigation: The <a> tag used for linking to the other auth page should be replaced with Next.js's <Link> component to enable client-side routing and avoid unnecessary page reloads. You can use the passHref prop to ensure your styles are applied correctly.
  3. Theming: Several colors are hardcoded (e.g., in ErrorMessage, SubmitButton). These should use the new CSS variables from global-styles.js to ensure they work correctly with both light and dark themes.

}

// Use ipapi.co for geolocation (free, no API key needed)
const response = await fetch('https://ipapi.co/json/');

Choose a reason for hiding this comment

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

security-medium medium

MEDIUM: Privacy Violation / Unauthorized Tracking. The application automatically sends the user's IP address to a third-party service (https://ipapi.co/json/) to determine their country without obtaining prior consent. This violates privacy regulations such as GDPR and CCPA.

Remediation: Implement a consent mechanism before tracking user location or using third-party geolocation services. Alternatively, perform geolocation on the server side using the request's IP address to avoid exposing user data to third parties from the client.

Comment on lines +61 to +70
useEffect(() => {
// Initialize theme from localStorage on mount
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
if (savedTheme !== theme) {
initializeTheme(savedTheme);
}
}
}, []);

Choose a reason for hiding this comment

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

medium

The useEffect hook has missing dependencies: initializeTheme and theme. While this might work as intended because initializeTheme is stable, it's a best practice to include all values from the component scope that are used in the effect in the dependency array. This prevents potential stale closures and aligns with React's linting rules.

  }, [initializeTheme, theme]);

Comment on lines +14 to +32
export function ProtectedRoute({ children, isAuthenticated }) {
const router = useRouter();

useEffect(() => {
// Check authentication status
const token = getStoredToken();
if (!token || !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, router]);

// Check if authenticated before rendering
const token = getStoredToken();
if (!token || !isAuthenticated) {
return null; // Don't render children while redirecting
}

return <>{children}</>;
}

Choose a reason for hiding this comment

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

medium

This ProtectedRoute component seems to duplicate the route protection logic that is already implemented globally in pages/_app.js. The handleRouteProtection function in _app.js already redirects unauthenticated users from protected pages. Relying on a single, centralized protection mechanism is generally more maintainable. Consider removing this component and relying solely on the logic in _app.js.

Comment on lines +67 to +143
background: linear-gradient(135deg, var(--color-accent) 0%, #0056CC 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
`;

export const SongDetails = styled.div`
flex: 1;
min-width: 0;
`;

export const SongTitle = styled.div`
font-size: 14px;
font-weight: 600;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 4px;
`;

export const SongArtist = styled.div`
font-size: 12px;
color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;

export const Controls = styled.div`
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
`;

export const ControlButtons = styled.div`
display: flex;
align-items: center;
gap: 16px;
`;

export const ControlButton = styled.button`
background: transparent;
border: none;
color: var(--color-text);
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all var(--transition-fast);
font-size: 20px;

&:hover {
background-color: var(--color-hover);
transform: scale(1.1);
}

&:active {
transform: scale(0.95);
}

${(props) =>
props.$primary &&
`
width: 40px;
height: 40px;
background-color: var(--color-accent);
color: white;
font-size: 16px;

&:hover {
background-color: #0056CC;

Choose a reason for hiding this comment

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

medium

Several components here use hardcoded colors (e.g., #0056CC) instead of the new theme variables. This will cause them to look out of place in dark mode. Specifically, PlaceholderArt and the hover state for the primary ControlButton should be updated to use variables like var(--color-accent-dark) or var(--color-accent-hover), which you can define in global-styles.js.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

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

🟡 Minor comments (24)
.cursor/frontend.md-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

Minor brand-name capitalization: "itunes" → "iTunes", "spotify" → "Spotify".

✏️ Proposed fix
-Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from itunes or spotify themes and cultural aesthetics for inspiration.
+Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from iTunes or Spotify themes and cultural aesthetics for inspiration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/frontend.md at line 7, The document contains incorrect brand-name
capitalization in the "Color & Theme" line: change "itunes" to "iTunes" and
"spotify" to "Spotify" in the sentence that reads "Draw from itunes or spotify
themes and cultural aesthetics for inspiration" (locate the "Color & Theme"
paragraph or that exact phrase) so both brand names use proper capitalization.
docs/adr/2026-02-17-fix-critical-vulnerabilities.md-71-79 (1)

71-79: ⚠️ Potential issue | 🟡 Minor

File appears truncated — missing closing code fence.

The JSON code block starting at line 71 is never closed with a matching ```. This will cause the Markdown to render incorrectly. Add the closing fence after line 79.

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

In `@docs/adr/2026-02-17-fix-critical-vulnerabilities.md` around lines 71 - 79,
The Markdown JSON code block that begins with the "resolutions" object is
missing its closing triple-backtick, causing rendering issues; add a closing ```
fence immediately after the JSON block (after the final "}" line) to properly
terminate the code fence and restore correct Markdown rendering.
app/utils/useMusicNavigation.js-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

onPause is accepted but never used.

Remove it from the destructured parameter list or document why it's passed through (it is wired in the caller at app/components/MusicPlayer/index.js line 55).

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

In `@app/utils/useMusicNavigation.js` at line 1, The useMusicNavigation function
currently accepts an unused onPause parameter; either remove onPause from the
destructured parameter list of useMusicNavigation or implement its intended
behavior. If removing, also remove the onPause prop passed from the MusicPlayer
caller so there's no unused prop; if keeping, wire the onPause callback into
useMusicNavigation where playback pause handling occurs (e.g., call onPause when
pausing audio via audioRef or pause handlers) and update the function signature
accordingly.
app/components/controls/PlayControls.js-16-24 (1)

16-24: ⚠️ Potential issue | 🟡 Minor

Play/Pause button is not gated by disabled.

The disabled prop is forwarded to the Prev and Next ControlButtons (lines 12 and 26) but not to the Play/Pause button (lines 17 and 21). If the intent is to keep pausing always active this should be documented; otherwise apply disabled here too.

🐛 Proposed fix (if play/pause should also be disabled)
-       <ControlButton $primary onClick={onPause} aria-label="Pause">
+       <ControlButton $primary onClick={onPause} disabled={disabled} aria-label="Pause">
          ⏸
        </ControlButton>
      ) : (
-       <ControlButton $primary onClick={onResume} aria-label="Play">
+       <ControlButton $primary onClick={onResume} disabled={disabled} aria-label="Play">
          ▶
        </ControlButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/controls/PlayControls.js` around lines 16 - 24, The Play/Pause
ControlButton in PlayControls.js isn't receiving the disabled prop like the
Prev/Next buttons, so Play/Pause remains clickable when it should be disabled;
update both branches that render ControlButton (the isPlaying branch that calls
onPause and the else branch that calls onResume) to pass the disabled prop
through (e.g., ControlButton $primary disabled={disabled} ...) so that the
component's disabled state is consistently applied across ControlButton
instances (refer to ControlButton, isPlaying, onPause, onResume, and the
existing disabled prop usage).
app/components/css/controlscss.js-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

Prettier formatting errors will fail CI.

Three distinct violations:

  • Line 15: transition shorthand needs to split to multiple lines.
  • Line 67: #0056CC#0056cc (Prettier enforces lowercase hex).
  • Line 262: Missing trailing newline at end of file.

Also applies to: 67-67, 262-262

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

In `@app/components/css/controlscss.js` at line 15, Prettier failures due to
formatting: split the transition shorthand into multiple lines for the
"transition" declaration (the line containing "transition: background
var(--transition-base), border-color var(--transition-base);") so each
transition is on its own line, change the hex color constant "#0056CC" to
lowercase "#0056cc", and add a trailing newline at the end of the file
(app/components/css/controlscss.js) so the file ends with a newline.
app/global-styles.js-4-30 (1)

4-30: ⚠️ Potential issue | 🟡 Minor

Multiple Prettier errors will fail CI.

The following violations are flagged by ESLint:

  • Lines 6–29: All hex color values use uppercase (e.g., #FAFAFC, #F0F0F5, #0A84FF) — Prettier requires lowercase hex.
  • Line 43: The transition property value must be split across lines.
  • Lines 119–123: The .animate-on-load-delay-* rules pack multiple declarations on one line; each declaration needs its own line.
  • Line 129: input, textarea { needs a preceding newline.

Run prettier --write app/global-styles.js to resolve all formatting issues in one pass.

Also applies to: 43-43, 119-123, 129-129

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

In `@app/global-styles.js` around lines 4 - 30, The CSS in :root and
[data-theme='dark'] uses uppercase hex colors and multiline/whitespace issues
that fail Prettier; run prettier or manually convert all hex values (e.g.,
--color-background, --color-text, --color-accent, --color-border, --color-hover,
--color-accent-muted) to lowercase, split the
--transition-base/--transition-fast/--transition-slow value onto separate lines
as Prettier expects, expand each declaration in the .animate-on-load-delay-*
rules so each property is on its own line, and ensure there is a blank line
before the input, textarea selector; running prettier --write
app/global-styles.js will apply all fixes automatically.
app/components/css/controlscss.js-148-151 (1)

148-151: ⚠️ Potential issue | 🟡 Minor

Disabled ControlButton still triggers the hover transform animation.

The &:disabled block sets opacity and cursor but does not suppress the &:hover { transform: scale(1.1); } rule above it. On mouse-over, a disabled button still scales up, which is visually misleading.

🐛 Proposed fix
   &:disabled {
     opacity: 0.5;
     cursor: not-allowed;
+    pointer-events: none;
   }

Or, alternatively, add &:disabled:hover { transform: none; } if pointer events need to remain active for tooltip support.

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

In `@app/components/css/controlscss.js` around lines 148 - 151, The ControlButton
styles apply a hover transform (scale(1.1)) but the existing &:disabled block
only sets opacity and cursor, so a disabled ControlButton still triggers the
&:hover transform; update the styles for ControlButton by either adding a rule
to suppress hover when disabled (e.g., add an &:disabled:hover rule that
neutralizes transform) or make disabled fully non-interactive by adding
pointer-events: none to the &:disabled block (keep pointer-events if you rely on
hover tooltips). Target the ControlButton &:hover transform rule and the
existing &:disabled block so the disabled state no longer scales on hover.
app/containers/MusicPlayer/selectors.js-16-18 (1)

16-18: ⚠️ Potential issue | 🟡 Minor

selectQueue is missing the || [] null-guard present on selectPlaylist.

If a reducer action ever explicitly sets queue: null, selectQueue returns null while selectPlaylist would return []. Consumers of selectQueue that call array methods on the result (e.g. .map, .length) would throw.

🐛 Proposed fix
-export const selectQueue = () => createSelector(selectPlayerDomain, (substate) => substate.queue);
+export const selectQueue = () => createSelector(selectPlayerDomain, (substate) => substate.queue || []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/MusicPlayer/selectors.js` around lines 16 - 18, selectQueue
currently returns substate.queue directly which can be null; add the same
null-guard used in selectPlaylist so it always returns an array. Update the
selector created in selectQueue (the createSelector call that uses
selectPlayerDomain) to return substate.queue || [] (mirroring selectPlaylist) so
consumers can safely call array methods without null checks.
app/containers/Search/selectors.js-8-14 (1)

8-14: ⚠️ Potential issue | 🟡 Minor

Prettier formatting errors will fail CI.

Lines 8, 10, 12, and 14 each need a line break after the opening parenthesis of createSelector. The inconsistency between these single-line declarations and the already-split forms on lines 16–20 suggests the formatter was not run. The two-line format used for selectSearchOffset and selectSearchHasMore is the expected canonical form.

🔧 Proposed fix
-export const selectSearchResults = () => createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.DATA, []));
+export const selectSearchResults = () =>
+  createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.DATA, []));

-export const selectSearchTerm = () => createSelector(selectSearchDomain, (substate) => get(substate, SEARCH_PAYLOAD.SEARCH_TERM, ''));
+export const selectSearchTerm = () =>
+  createSelector(selectSearchDomain, (substate) => get(substate, SEARCH_PAYLOAD.SEARCH_TERM, ''));

-export const selectSearchLoading = () => createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.LOADING, false));
+export const selectSearchLoading = () =>
+  createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.LOADING, false));

-export const selectSearchError = () => createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.ERROR, null));
+export const selectSearchError = () =>
+  createSelector(selectSearchDomain, (substate) => get(substate, PAYLOAD.ERROR, null));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Search/selectors.js` around lines 8 - 14, The four selector
exports (selectSearchResults, selectSearchTerm, selectSearchLoading,
selectSearchError) are formatted as single-line createSelector calls but should
match the two-line style used by selectSearchOffset/selectSearchHasMore; update
each createSelector invocation (for selectSearchResults, selectSearchTerm,
selectSearchLoading, selectSearchError) to place a line break immediately after
the opening parenthesis and before the selector callback, preserving the same
arguments and return values, then run Prettier/formatter to ensure CI passes.
app/global-styles.js-15-17 (1)

15-17: ⚠️ Potential issue | 🟡 Minor

--transition-slow (0.25 s) is shorter than --transition-base (0.3 s) — the naming is inverted.

The current variable hierarchy is:

Variable Duration
--transition-fast 0.2 s
--transition-slow 0.25 s
--transition-base 0.3 s

"Slow" is faster than "base", which is counter-intuitive and will cause subtle animation timing bugs whenever a developer reaches for --transition-slow expecting a longer-than-normal animation.

🐛 Proposed fix
-    --transition-base: 0.3s ease;
-    --transition-fast: 0.2s ease;
-    --transition-slow: 0.25s ease;
+    --transition-fast: 0.15s ease;
+    --transition-base: 0.25s ease;
+    --transition-slow: 0.4s ease;

Or simply rename the existing variable to match its actual duration semantics (e.g., rename --transition-slow--transition-medium).

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

In `@app/global-styles.js` around lines 15 - 17, The CSS custom property names are
inconsistent: --transition-slow is 0.25s which is shorter than --transition-base
(0.3s), so either swap the durations or rename the variable to reflect its
actual speed; update the declarations for --transition-base, --transition-fast,
and --transition-slow (or rename --transition-slow → --transition-medium) so the
ordering reads fast (0.2s) < medium/slow (0.25s) < base (0.3s), and adjust any
usages of --transition-slow throughout the codebase to the new name if you
rename it.
app/components/css/controlscss.js-197-222 (1)

197-222: ⚠️ Potential issue | 🟡 Minor

VolumeSlider is missing appearance: none on the input element itself.

Without appearance: none (and -webkit-appearance: none) on the <input> itself, browsers retain native range-input rendering, which ignores the custom thumb pseudo-element styles entirely in some environments (notably Firefox ignores -webkit-slider-thumb). The border: none on the moz-range-thumb suggests cross-browser support was intended.

🐛 Proposed fix
 export const VolumeSlider = styled.input`
   flex: 1;
   height: 4px;
   background-color: var(--color-border);
   border-radius: 2px;
   outline: none;
   cursor: pointer;
+  appearance: none;
+  -webkit-appearance: none;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/css/controlscss.js` around lines 197 - 222, The VolumeSlider
styled input lacks appearance overrides on the input itself, so add appearance:
none and vendor-prefixed -webkit-appearance: none (and optionally
-moz-appearance: none) to the VolumeSlider root rule so browsers don't preserve
native range styling and your custom ::-webkit-slider-thumb and
::-moz-range-thumb rules are applied; update the VolumeSlider styled.input block
to include those appearance declarations near the top of the rule.
app/components/ThemeToggle/index.js-13-40 (1)

13-40: ⚠️ Potential issue | 🟡 Minor

ToggleButton is missing type="button".

Without an explicit type, a <button> defaults to type="submit" in HTML. If ThemeToggle is ever rendered inside a <form> (e.g., on the login or signup pages present in this PR), clicking the toggle would submit the form instead of toggling the theme.

🐛 Proposed fix
 const ToggleButton = styled.button`
   background: var(--color-accent-muted);

And in the JSX:

     <ToggleButton
+      type="button"
       onClick={onToggle}
       aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ThemeToggle/index.js` around lines 13 - 40, The ToggleButton
styled component currently creates a plain <button> which defaults to
type="submit" in forms; update the ToggleButton declaration to set an explicit
type attribute (e.g., use styled.button.attrs({ type: 'button' }) or otherwise
add type: 'button' via attrs/default props) so ThemeToggle's button will not
submit enclosing forms, and ensure any JSX using ToggleButton does not override
this type.
app/containers/Search/selectors.js-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Add [PAYLOAD.LOADING]: false to initialState in the Search reducer.

The Search reducer's initialState is missing [PAYLOAD.LOADING] even though startLoading() and stopLoading() explicitly manage this key in action handlers (searchRequest, searchMoreRequest, searchSuccess, searchFailure). While the selector's default fallback (get(substate, PAYLOAD.LOADING, false)) masks functional issues, the state should explicitly initialize this key for consistency and clarity—especially since other related state keys (PAYLOAD.DATA, PAYLOAD.ERROR) are already initialized.

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

In `@app/containers/Search/selectors.js` at line 12, The Search reducer's
initialState lacks the [PAYLOAD.LOADING] key used by selectors and the
startLoading/stopLoading handlers; update the reducer's initialState (the
variable named initialState in the Search reducer) to include [PAYLOAD.LOADING]:
false alongside existing [PAYLOAD.DATA] and [PAYLOAD.ERROR] so searchRequest,
searchMoreRequest, searchSuccess and searchFailure have a defined loading flag
and selectSearchLoading (which uses selectSearchDomain and PAYLOAD.LOADING) no
longer relies on a fallback.
app/containers/Theme/selectors.js-7-9 (1)

7-9: ⚠️ Potential issue | 🟡 Minor

Remove the unused selectIsDarkMode selector.

selectThemeDomain correctly references state.theme, matching the key registered in app/reducers.js. The || initialState fallback is safe given initialState is always a truthy object.

However, selectIsDarkMode is exported but unused throughout the codebase. Remove this dead export:

Suggested removal
export const selectTheme = () => createSelector(selectThemeDomain, (substate) => substate.theme);

-export const selectIsDarkMode = () => createSelector(selectThemeDomain, (substate) => substate.theme === 'dark');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Theme/selectors.js` around lines 7 - 9, Remove the dead
exported selector selectIsDarkMode: delete the selectIsDarkMode declaration and
its export so only selectTheme (which uses selectThemeDomain) remains; also
remove any imports or tests referencing selectIsDarkMode to avoid unresolved
import errors and run the test suite/linter to confirm nothing else depends on
it.
app/utils/apiUtils.js-24-24 (1)

24-24: ⚠️ Potential issue | 🟡 Minor

getApiClient bypasses resolveClientKey, inconsistent with generateApiClient.

getApiClient indexes apiClients directly by type, while generateApiClient resolves the key first via resolveClientKey. If a caller passes an unrecognized type to getApiClient, they get undefined instead of falling back to 'default'. Consider routing through resolveClientKey for consistency.

Proposed fix
-export const getApiClient = (type = 'github') => apiClients[type];
+export const getApiClient = (type = 'github') => apiClients[resolveClientKey(type)];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/apiUtils.js` at line 24, getApiClient currently indexes apiClients
directly by the provided type and can return undefined for unknown types; change
it to mirror generateApiClient by first calling resolveClientKey(type) and then
returning apiClients[resolvedKey] (which will fall back to the 'default' key per
resolveClientKey behavior), using the same resolveClientKey, getApiClient,
generateApiClient, and apiClients symbols to locate the change.
app/utils/useProgress.js-2-9 (1)

2-9: ⚠️ Potential issue | 🟡 Minor

percentage is not clamped, allowing out-of-bounds seek.

If the user clicks near the very edge of the progress bar (or drags outside it), percentage can be negative or exceed 1, setting currentTime to a negative value or beyond duration. Clamp to [0, 1].

Proposed fix
   const handleProgressClick = (e) => {
     if (!progressBarRef.current || !duration || !audioRef.current) return;
     const rect = progressBarRef.current.getBoundingClientRect();
-    const percentage = (e.clientX - rect.left) / rect.width;
+    const percentage = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
     const newTime = percentage * duration;
     audioRef.current.currentTime = newTime;
     onSetCurrentTime(newTime);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/useProgress.js` around lines 2 - 9, The click handler
handleProgressClick can compute a percentage <0 or >1 and set an out-of-bounds
time; clamp percentage to the [0,1] range and compute newTime from the clamped
value, then set audioRef.current.currentTime and call onSetCurrentTime with a
value clamped to [0, duration] (use progressBarRef, audioRef, duration,
handleProgressClick, and onSetCurrentTime to locate and update the logic).
app/components/Layout/index.js-61-70 (1)

61-70: ⚠️ Potential issue | 🟡 Minor

initializeTheme can be undefined, causing a runtime crash.

initializeTheme is declared as an optional prop (line 90, no .isRequired). If this component is rendered via the named export (e.g., in tests) without the Redux-connected wrapper, initializeTheme(savedTheme) on line 67 will throw a TypeError. Add a guard.

Also, note that pages/_app.js (line 74) already reads localStorage.getItem('theme') and applies it. Having duplicate initialization in both _app.js and Layout can lead to ordering issues or redundant dispatches. Consider centralizing theme initialization in one place.

Proposed fix for the guard
     if (typeof window !== 'undefined') {
       const savedTheme = localStorage.getItem('theme') || 'light';
       document.documentElement.setAttribute('data-theme', savedTheme);
       if (savedTheme !== theme) {
-        initializeTheme(savedTheme);
+        initializeTheme?.(savedTheme);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Layout/index.js` around lines 61 - 70, The useEffect in Layout
calls initializeTheme(savedTheme) but initializeTheme is an optional prop and
can be undefined; update the effect to check that initializeTheme is a function
before invoking it (e.g., typeof initializeTheme === 'function') and only call
it when savedTheme !== theme and the function exists; keep the existing
document.documentElement.setAttribute('data-theme', savedTheme) behavior. Also
remove or consolidate duplicate localStorage/theme initialization between
Layout's useEffect and pages/_app.js by centralizing theme bootstrapping in one
place (preferably _app.js) to avoid redundant dispatches and ordering issues.
app/components/SearchBar/index.js-44-51 (1)

44-51: ⚠️ Potential issue | 🟡 Minor

Debounced function is never cancelled on unmount or dependency change.

When onSearch changes, useCallback creates a new debounced function, but the previous one's pending timer still fires. On unmount, any pending invocation will attempt to call onSearch on a stale closure. Use useEffect cleanup or useRef to cancel the debounced function.

Proposed fix using useRef + useEffect cleanup
-import React, { useState, useCallback } from 'react';
+import React, { useState, useCallback, useEffect, useRef } from 'react';
...

 export function SearchBar({ onSearch, placeholder = 'Search for songs, artists...' }) {
   const router = useRouter();
   const [searchTerm, setSearchTerm] = useState('');

-  const debouncedSearch = useCallback(
-    debounce((term) => {
-      if (onSearch) {
-        onSearch(term);
-      }
-    }, 300),
-    [onSearch]
-  );
+  const debouncedSearchRef = useRef(null);
+
+  useEffect(() => {
+    debouncedSearchRef.current = debounce((term) => {
+      if (onSearch) {
+        onSearch(term);
+      }
+    }, 300);
+    return () => debouncedSearchRef.current?.cancel();
+  }, [onSearch]);
+
+  const debouncedSearch = useCallback(
+    (...args) => debouncedSearchRef.current?.(...args),
+    []
+  );
🤖 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 44 - 51, The debouncedSearch
created with useCallback + debounce can leave pending timers active when
onSearch changes or the component unmounts; update the implementation to store
the debounced function in a ref (e.g., debouncedSearchRef) and call
debouncedSearchRef.current.cancel() in a useEffect cleanup (and before replacing
it when onSearch changes) so pending invocations are cleared and you never call
a stale onSearch; ensure you reference the debounce-returned function's cancel
method when cleaning up.
app/containers/MusicPlayer/reducer.js-40-49 (1)

40-49: ⚠️ Potential issue | 🟡 Minor

isSameSong fallback comparison may produce false positives.

Line 49 falls back to comparing trackName and artistName when neither trackId nor id is present. Two different tracks by the same artist with the same name (e.g., remixes, live versions) would be treated as identical. This is a minor edge case given the primary ID checks, but worth noting.

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

In `@app/containers/MusicPlayer/reducer.js` around lines 40 - 49, The fallback in
isSameSong uses only trackName and artistName which can yield false positives;
update isSameSong to strengthen the fallback by also comparing an additional
disambiguating field(s) (e.g., durationMillis, collectionName/albumName, or
releaseDate) when hasSameId(a,b) is false and both trackName and artistName
match — only treat as same song if trackName && artistName match AND at least
one of those extra fields also match (or isMissing on both), keeping the
existing hasSameId and null checks in hasSameId/isSameSong to locate the change.
app/components/MusicPlayer/index.js-109-109 (1)

109-109: ⚠️ Potential issue | 🟡 Minor

onPlay is declared as a required prop but never used in the component.

onPlay is wired in mapDispatchToProps (Line 130) and listed as PropTypes.func.isRequired, but it is never referenced in the component body or passed to any child. This is dead code and the isRequired constraint will trigger prop-type warnings if it's ever missing.

Either remove it from props/propTypes/mapDispatchToProps, or wire it to the appropriate handler if it was intended to be used (e.g., passed to PlayerControls).

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

In `@app/components/MusicPlayer/index.js` at line 109, The prop onPlay is declared
in propTypes and wired in mapDispatchToProps but never used; remove the dead
prop or wire it into the component: either delete onPlay from propTypes and from
mapDispatchToProps, or pass the onPlay prop into the playback handler or child
component (e.g., pass onPlay into PlayerControls or call it inside the
component's play handler). Locate usages by searching for onPlay,
mapDispatchToProps, and PlayerControls in this file and update accordingly so no
unused prop remains and propTypes reflect the actual API.
app/components/Auth/SignupForm.js-191-193 (1)

191-193: ⚠️ Potential issue | 🟡 Minor

Use Next.js Link instead of a plain <a> tag for client-side navigation.

<a href="/login"> triggers a full page reload. Use next/link to preserve SPA behavior, client-side state, and faster transitions.

Proposed fix

Add the import:

import Link from 'next/link';

Then update the JSX:

      <LinkText>
-       Already have an account? <a href="/login">Sign in</a>
+       Already have an account? <Link href="/login">Sign in</Link>
      </LinkText>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Auth/SignupForm.js` around lines 191 - 193, In SignupForm.js
replace the plain anchor used inside LinkText with Next.js client-side
navigation: add "import Link from 'next/link';" at the top of the file and
update the JSX that currently renders <a href="/login">Sign in</a> to use the
Next Link component (use Link with href="/login" to wrap the sign-in text so
navigation is client-side and preserves SPA state). Target the JSX inside the
LinkText element in the SignupForm component when making this change.
app/components/Auth/LoginForm.js-191-193 (1)

191-193: ⚠️ Potential issue | 🟡 Minor

Use Next.js Link for internal navigation instead of <a href="/signup">.

Same issue as in SignupForm.js — a plain <a> tag causes a full page reload, discarding client-side state.

Proposed fix

Add the import:

import Link from 'next/link';

Then update the JSX:

      <LinkText>
-      {" Don't have an account?"} <a href="/signup">Sign up</a>
+      {" Don't have an account?"} <Link href="/signup">Sign up</Link>
      </LinkText>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Auth/LoginForm.js` around lines 191 - 193, Replace the plain
anchor in the LoginForm component so client-side routing is used: import Next.js
Link (add "import Link from 'next/link'") at the top of the file and change the
JSX inside the LinkText block to use the <Link> component linking to "/signup"
instead of the raw <a href="/signup">, keeping the same visible text ("Sign
up"); update any styling or nested elements accordingly so LoginForm and
LinkText render identically but without a full page reload.
app/components/SongCard/index.js-128-134 (1)

128-134: ⚠️ Potential issue | 🟡 Minor

getSongData should validate the song parameter to handle null/undefined inputs defensively.

The function accesses properties on song without checking if it's null or undefined, which will throw a TypeError. While the current caller in SongCard has song marked as .isRequired in PropTypes, the function is exported as a public API. The MusicPlayer component already demonstrates the guard pattern (currentSong || {}) that should be used. For defensive programming, exported utility functions should validate their inputs, particularly when PropTypes may be disabled in production builds.

Proposed fix
 export function getSongData(song) {
+  if (!song) song = {};
   return {
     artworkUrl: firstNonNull(song.artworkUrl, song.cover, song.coverImage, song.albumCover),
     trackName: firstNonNull(song.trackName, song.title, song.name, 'Unknown Title'),
     artistName: firstNonNull(song.artistName, song.artist, 'Unknown Artist'),
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/SongCard/index.js` around lines 128 - 134, getSongData
currently dereferences song without checking for null/undefined; update it to
defensively handle missing input by treating song as an empty object when falsy
(e.g., const s = song || {}) before using firstNonNull, so
getSongData(trackName/artistName/artworkUrl) always returns the fallback values
instead of throwing; reference getSongData and firstNonNull to locate and change
the logic.
pages/search.js-55-58 (1)

55-58: ⚠️ Potential issue | 🟡 Minor

URL-sync effect can double-dispatch searches and has stale closure over searchTerm.

When the user searches via SearchBar, doSearch dispatches searchRequest and pushes the URL. The URL change re-triggers this useEffect, which compares against a potentially stale searchTerm (it's omitted from the dependency array) and dispatches a second searchRequest. takeLatest in the saga masks the duplicate, but it's still a wasted dispatch and a correctness hazard if the saga implementation changes.

Consider guarding against programmatic navigation (e.g., with a ref flag) or including searchTerm and onSearch in the dependency array with appropriate logic:

♻️ Option: skip effect on programmatic pushes
+ const programmaticNav = useRef(false);
+
+ function doSearchLocal(term) {
+   if (!term?.trim()) return;
+   const trimmed = term.trim();
+   onSearch(trimmed);
+   programmaticNav.current = true;
+   router.push(`/search?q=${encodeURIComponent(trimmed)}`, undefined, { shallow: true });
+ }

  useEffect(() => {
+   if (programmaticNav.current) {
+     programmaticNav.current = false;
+     return;
+   }
    const query = router.query.q;
    if (query && query !== searchTerm) onSearch(query);
- }, [router.query.q]);
+ }, [router.query.q, searchTerm, onSearch]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/search.js` around lines 55 - 58, The current useEffect watching
router.query.q double-dispatches searches and captures a stale searchTerm
because searchTerm and onSearch are not in the dependency array; update the
effect to avoid duplicate programmatic triggers by either (A) adding searchTerm
and onSearch to the dependency array and only calling onSearch when
router.query.q !== searchTerm, or (B) add a ref flag (e.g., isProgrammaticRef)
that doSearch/doSearch-triggering code sets before pushing the URL and have the
useEffect skip when that ref is set; target the useEffect block, the
router.query.q check, and the onSearch/doSearch/searchRequest flow (and ensure
any ref is cleared after the push) so you don't double-dispatch even if
takeLatest behavior changes.

Comment on lines +1 to +22
DISTILLED_AESTHETICS_PROMPT = """
<frontend_aesthetics>
You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight. Focus on:

Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.

Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from itunes or spotify themes and cultural aesthetics for inspiration.

Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.

Backgrounds: Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.

Avoid generic AI-generated aesthetics:
- Overused font families (Inter, Roboto, Arial, system fonts)
- Clichéd color schemes (particularly purple gradients on white backgrounds)
- Predictable layouts and component patterns
- Cookie-cutter design that lacks context-specific character

Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics. You still tend to converge on common choices (Space Grotesk, for example) across generations. Avoid this: it is critical that you think outside the box!
</frontend_aesthetics>
"""

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify file location and structure
echo "=== Checking .cursor/frontend.md location ==="
find .cursor -name "frontend.md" -o -name "frontend.mdc" 2>/dev/null

echo ""
echo "=== Contents of .cursor directory ==="
fd . .cursor --type f

echo ""
echo "=== Contents of .cursor/rules directory (if exists) ==="
fd . .cursor/rules --type f 2>/dev/null || echo "No .cursor/rules directory found"

echo ""
echo "=== First 30 lines of .cursor/frontend.md ==="
if [ -f ".cursor/frontend.md" ]; then
  head -30 .cursor/frontend.md
else
  echo "File not found at .cursor/frontend.md"
fi

Repository: wednesday-solutions/nextjs-template

Length of output: 2184


🌐 Web query:

Cursor AI .cursor/rules directory file format requirements mdc

💡 Result:

Cursor “Project Rules” live in a .cursor/rules/ directory, and each rule is a separate file tracked in version control. (docs.cursor.com)

Required file format (MDC / .mdc)

  • Cursor’s docs state that each rule file is written in “MDC” and uses the .mdc extension (Markdown content + metadata). (docs.cursor.com)
  • In practice, users report that rules in the rules folder aren’t recognized unless the file ends in .mdc. (github.com)

Frontmatter (metadata) fields Cursor recognizes

A rule file starts with a frontmatter block (then your Markdown instructions). The docs/community-confirmed fields are: (forum.cursor.com)

  • description: text description used by the agent for relevance
  • globs: file pattern(s) used to scope/auto-attach the rule
  • alwaysApply: true / false

How the “rule type” is determined

Cursor infers the rule’s behavior from the field combination (not a separate type field): (forum.cursor.com)

  • Always Apply: alwaysApply: true
  • Apply Intelligently (agent-decided): alwaysApply: false, description required
  • Apply to Specific Files (auto-attached): alwaysApply: false, globs required
  • Apply Manually: alwaysApply: false, no globs, empty/omitted description

Minimal .mdc template

---
description: Briefly explain when/why this rule should be used
globs: ["src/**/*.ts", "src/**/*.tsx"]   # only if you want file-scoped auto-attach
alwaysApply: false
---

# Rule instructions
Write the guidance in normal Markdown here.

Citations:


Move to .cursor/rules/frontend.mdc and reformat as a proper Cursor rule — file will not be recognized in its current location or format.

This file cannot be processed by Cursor in its current state:

  • Wrong location: Cursor reads project rules only from .cursor/rules/ directory, not .cursor/. The file at .cursor/frontend.md will be silently ignored.
  • Wrong format: Rule files must be .mdc (not .md) with YAML frontmatter (description, globs, alwaysApply) followed by clean Markdown content. The Python variable wrapper (DISTILLED_AESTHETICS_PROMPT = """...) is unnecessary syntactic noise.
  • Missing metadata: Without YAML frontmatter, there is no way to control when the rule activates (e.g., scope to specific file types).
Refactor to `.cursor/rules/frontend.mdc`
---
description: "Frontend aesthetics guidelines for UI/component design"
globs: ["**/*.tsx", "**/*.css", "**/*.scss"]
alwaysApply: false
---

# Frontend Aesthetics

You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight. Focus on:

**Typography:** Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.

**Color & Theme:** Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from iTunes or Spotify themes and cultural aesthetics for inspiration.

**Motion:** Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.

**Backgrounds:** Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.

**Avoid generic AI-generated aesthetics:**
- Overused font families (Inter, Roboto, Arial, system fonts)
- Clichéd color schemes (particularly purple gradients on white backgrounds)
- Predictable layouts and component patterns
- Cookie-cutter design that lacks context-specific character

Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics.

Also: fix "itunes" → "iTunes" (line 7 of original).

📝 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
DISTILLED_AESTHETICS_PROMPT = """
<frontend_aesthetics>
You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight. Focus on:
Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.
Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from itunes or spotify themes and cultural aesthetics for inspiration.
Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.
Backgrounds: Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.
Avoid generic AI-generated aesthetics:
- Overused font families (Inter, Roboto, Arial, system fonts)
- Clichéd color schemes (particularly purple gradients on white backgrounds)
- Predictable layouts and component patterns
- Cookie-cutter design that lacks context-specific character
Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics. You still tend to converge on common choices (Space Grotesk, for example) across generations. Avoid this: it is critical that you think outside the box!
</frontend_aesthetics>
"""
---
description: "Frontend aesthetics guidelines for UI/component design"
globs: ["**/*.tsx", "**/*.css", "**/*.scss"]
alwaysApply: false
---
# Frontend Aesthetics
You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight. Focus on:
**Typography:** Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.
**Color & Theme:** Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from iTunes or Spotify themes and cultural aesthetics for inspiration.
**Motion:** Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.
**Backgrounds:** Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.
**Avoid generic AI-generated aesthetics:**
- Overused font families (Inter, Roboto, Arial, system fonts)
- Clichéd color schemes (particularly purple gradients on white backgrounds)
- Predictable layouts and component patterns
- Cookie-cutter design that lacks context-specific character
Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics.
🧰 Tools
🪛 LanguageTool

[style] ~19-~19: “think outside the box” can be a clichéd phrase in professional communication. Consider an alternative expression to make your writing more engaging and original.
Context: ...ns. Avoid this: it is critical that you think outside the box! </frontend_aesthetics> """

(THINK_OUTSIDE_THE_BOX)

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

In @.cursor/frontend.md around lines 1 - 22, The current file embeds the prompt
in a Python variable (DISTILLED_AESTHETICS_PROMPT) and is in the wrong
location/format for Cursor; extract the raw prompt text, remove the Python
wrapper, and create a proper Cursor rule file named frontend.mdc in the
project’s Cursor rules directory; add YAML frontmatter with description, globs
(e.g., target frontend files or patterns), and alwaysApply, then place the
reflowed Markdown content (the frontend_aesthetics guidance) after the
frontmatter, correcting "itunes" → "iTunes" and removing the angle-tag wrapper
or converting it to a normal heading/section so Cursor can parse the rule.

Comment on lines +17 to +29
useEffect(() => {
// Check authentication status
const token = getStoredToken();
if (!token || !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, router]);

// Check if authenticated before rendering
const token = getStoredToken();
if (!token || !isAuthenticated) {
return null; // Don't render children while redirecting
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition between Redux state hydration and localStorage check.

On initial page load, isAuthenticated in Redux defaults to false before the store is hydrated, while getStoredToken() may return a valid token. The !token || !isAuthenticated condition means the component will redirect to /login if either check fails. During the brief window before Redux hydration completes, a legitimately authenticated user (with a valid token in localStorage) will be redirected because isAuthenticated is still false.

Consider using && (both must fail) instead of ||, or rely on a single source of truth. If the intent is that both must be present, introduce a loading/hydrating state so you don't redirect before Redux has settled.

Proposed fix (use AND logic so redirect only fires when both are absent)
  useEffect(() => {
    const token = getStoredToken();
-   if (!token || !isAuthenticated) {
+   if (!token && !isAuthenticated) {
      router.push('/login');
    }
  }, [isAuthenticated, router]);

  const token = getStoredToken();
- if (!token || !isAuthenticated) {
+ if (!token && !isAuthenticated) {
    return null;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
// Check authentication status
const token = getStoredToken();
if (!token || !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, router]);
// Check if authenticated before rendering
const token = getStoredToken();
if (!token || !isAuthenticated) {
return null; // Don't render children while redirecting
}
useEffect(() => {
// Check authentication status
const token = getStoredToken();
if (!token && !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, router]);
// Check if authenticated before rendering
const token = getStoredToken();
if (!token && !isAuthenticated) {
return null; // Don't render children while redirecting
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ProtectedRoute/index.js` around lines 17 - 29, The current
logic in useEffect and the immediate render check uses `!token ||
!isAuthenticated`, which causes a redirect if either localStorage
`getStoredToken()` or Redux `isAuthenticated` is false and triggers a race on
initial hydration; update the checks to only redirect when both sources indicate
unauthenticated (use `&&` logic) or, better, wait for a Redux hydration/loading
flag before deciding: change the useEffect and the early return in the component
that call `getStoredToken()` and `router.push('/login')` to require both `!token
&& !isAuthenticated` (or gate them behind a `isHydrated`/`isLoading` state from
the store) so you do not redirect while `isAuthenticated` is still initializing.

Comment on lines +5 to +9
function extractTracksFromResponse(response) {
if (!response) return null;
if (Array.isArray(response.data)) return response.data;
return response.data.tracks || response.data.results || [];
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential TypeError on Line 8 if response.data is nullish.

Line 6 guards against a falsy response, and Line 7 checks if response.data is an array. But if response is truthy while response.data is null or undefined, Line 8 will throw TypeError: Cannot read properties of null (reading 'tracks').

Proposed fix
 function extractTracksFromResponse(response) {
   if (!response) return null;
+  if (!response.data) return null;
   if (Array.isArray(response.data)) return response.data;
   return response.data.tracks || response.data.results || [];
 }
📝 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
function extractTracksFromResponse(response) {
if (!response) return null;
if (Array.isArray(response.data)) return response.data;
return response.data.tracks || response.data.results || [];
}
function extractTracksFromResponse(response) {
if (!response) return null;
if (!response.data) return null;
if (Array.isArray(response.data)) return response.data;
return response.data.tracks || response.data.results || [];
}
🧰 Tools
🪛 ESLint

[error] 6-6: Expected { after 'if' condition.

(curly)


[error] 7-7: Expected { after 'if' condition.

(curly)

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

In `@app/containers/Home/saga.js` around lines 5 - 9, The
extractTracksFromResponse function can throw a TypeError when response.data is
nullish; update the logic in extractTracksFromResponse to safely handle
null/undefined response.data (e.g., check if response.data is truthy before
accessing .tracks/.results or use optional chaining) so the function returns []
(or null per existing behavior) instead of throwing; ensure the branches that
currently test Array.isArray(response.data) and return response.data.tracks ||
response.data.results || [] are guarded by a preceding check like if (!response
|| !response.data) return [] (or null) or use response.data?.tracks ??
response.data?.results ?? [].

Comment on lines +22 to +28
export const initialState = {
[SEARCH_PAYLOAD.SEARCH_TERM]: '',
[PAYLOAD.DATA]: [],
[PAYLOAD.ERROR]: null,
[SEARCH_PAYLOAD.OFFSET]: 0,
[SEARCH_PAYLOAD.HAS_MORE]: false
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "reducer.js" app/containers/Search

Repository: wednesday-solutions/nextjs-template

Length of output: 249


🏁 Script executed:

cat -n app/containers/Search/reducer.js | head -100

Repository: wednesday-solutions/nextjs-template

Length of output: 3326


🏁 Script executed:

fd -type f -name "reducer.js" app/utils

Repository: wednesday-solutions/nextjs-template

Length of output: 249


🏁 Script executed:

rg -n "PAYLOAD\s*=" app/utils --type js -B 2 -A 10

Repository: wednesday-solutions/nextjs-template

Length of output: 647


🏁 Script executed:

rg -n "startLoading\|stopLoading" app/utils/reducer.js --type js -B 3 -A 5

Repository: wednesday-solutions/nextjs-template

Length of output: 61


🏁 Script executed:

find app/containers -name "reducer.js" -type f | head -5

Repository: wednesday-solutions/nextjs-template

Length of output: 241


🏁 Script executed:

rg -l "initialState" app/containers --glob="*reducer.js" | head -3

Repository: wednesday-solutions/nextjs-template

Length of output: 175


🏁 Script executed:

cat -n app/containers/Auth/reducer.js | head -40

Repository: wednesday-solutions/nextjs-template

Length of output: 1472


🏁 Script executed:

cat -n app/containers/Home/reducer.js | head -40

Repository: wednesday-solutions/nextjs-template

Length of output: 1494


initialState is missing [PAYLOAD.LOADING] and handleClearSearch doesn't reset the loading flag.

Two confirmed issues:

  1. initialState (lines 22-28) does not include [PAYLOAD.LOADING]: false. The startLoading function imported from @app/utils/reducer sets draft[PAYLOAD.LOADING] = true, but if this key is missing from initialState, the initial value before the first search request will be undefined rather than false. The Auth reducer correctly includes [PAYLOAD.LOADING]: false in its initialState.

  2. handleClearSearch (lines 87-93) resets SEARCH_TERM, DATA, ERROR, OFFSET, and HAS_MORE — but not PAYLOAD.LOADING. If a search request is in-flight when clearSearch is triggered, the loading flag remains true and the spinner becomes stuck permanently.

🐛 Proposed fix
 export const initialState = {
   [SEARCH_PAYLOAD.SEARCH_TERM]: '',
   [PAYLOAD.DATA]: [],
   [PAYLOAD.ERROR]: null,
   [SEARCH_PAYLOAD.OFFSET]: 0,
-  [SEARCH_PAYLOAD.HAS_MORE]: false
+  [SEARCH_PAYLOAD.HAS_MORE]: false,
+  [PAYLOAD.LOADING]: false
 };
 const handleClearSearch = (draft) => {
   draft[SEARCH_PAYLOAD.SEARCH_TERM] = '';
   draft[PAYLOAD.DATA] = [];
   draft[PAYLOAD.ERROR] = null;
   draft[SEARCH_PAYLOAD.OFFSET] = 0;
   draft[SEARCH_PAYLOAD.HAS_MORE] = false;
+  draft[PAYLOAD.LOADING] = false;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Search/reducer.js` around lines 22 - 28, The initialState is
missing the [PAYLOAD.LOADING] key and handleClearSearch doesn't reset it; add
[PAYLOAD.LOADING]: false to initialState so loading defaults to false, and
update handleClearSearch to set draft[PAYLOAD.LOADING] = false (alongside
resetting SEARCH_PAYLOAD.SEARCH_TERM, PAYLOAD.DATA, PAYLOAD.ERROR,
SEARCH_PAYLOAD.OFFSET, and SEARCH_PAYLOAD.HAS_MORE) to avoid leaving the spinner
stuck when clearSearch is called; this aligns with how startLoading (from
`@app/utils/reducer`) expects the loading flag to exist.

Comment on lines +12 to +13
export const initialState = {
theme: 'light' // 'light' or 'dark'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

initialState hardcodes 'light' — causes a flash of wrong theme on page load.

When a user with a saved 'dark' preference revisits, the Redux store initializes to 'light'. Until an action is dispatched to restore the saved theme (if that even happens), CSS variables will render in the wrong mode, causing a visible flash. A common fix is to seed initialState from localStorage in the browser, or apply the data-theme attribute in a blocking <script> in _document.js before React hydrates.

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

In `@app/containers/Theme/reducer.js` around lines 12 - 13, initialState currently
hardcodes theme: 'light' causing a flash; change initialState in reducer.js to
read the persisted value when available (use
window.localStorage.getItem('theme') or similar) and fall back to 'light' when
missing, i.e. create initialState.theme = (typeof window !== 'undefined' &&
localStorage.getItem('theme')) || 'light'; ensure the reducer/exported
initialState and any function names referencing initialState or theme still
work; alternatively, if you prefer a non-Redux fix, add a blocking script in
_document.js that reads localStorage and sets
document.documentElement.dataset.theme before React hydrates.

Comment on lines +67 to +79
## Summary of Actions

- Added **Yarn resolutions** for all critical packages:

```json
{
"resolutions": {
"simple-git": ">=3.16.0",
"property-expr": ">=2.0.3",
"minimist": ">=1.2.6",
"form-data": ">=2.5.4"
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

ADR does not document all resolutions added in package.json.

The summary section lists resolutions for 4 packages (simple-git, property-expr, minimist, form-data), but package.json adds 5 additional resolutions (next, json, braces, cross-spawn, qs) that are not documented here. For the ADR to serve its purpose as a decision record, it should cover all enforced resolutions with their rationale and associated CVEs.

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

In `@docs/adr/2026-02-17-fix-critical-vulnerabilities.md` around lines 67 - 79,
The ADR's "resolutions" JSON block is incomplete: update the resolutions section
in the ADR (the "resolutions" JSON object) to include all packages actually
added to package.json—specifically add entries for "next", "json", "braces",
"cross-spawn", and "qs"—and for each added package include a short rationale and
associated CVE(s) like the existing entries for "simple-git", "property-expr",
"minimist", and "form-data" so the ADR fully documents every enforced
resolution.

package.json Outdated
Comment on lines +50 to +51
"apisauce": "^3.2.2",
"axios": "^1.13.5",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if axios is used directly anywhere in the codebase (not just via apisauce)
echo "=== Direct axios imports ==="
rg -n "from ['\"]axios['\"]|require\(['\"]axios['\"]\)" --type=js --type=ts -g '!node_modules' -g '!package*.json'

echo ""
echo "=== apisauce imports ==="
rg -n "from ['\"]apisauce['\"]|require\(['\"]apisauce['\"]\)" --type=js --type=ts -g '!node_modules'

echo ""
# Verify apisauce 3.x exists
echo "=== Checking apisauce@3.2.2 ==="
curl -s "https://registry.npmjs.org/apisauce/3.2.2" | jq '.version // "NOT FOUND"'

echo ""
# Verify axios version
echo "=== Checking axios@1.13.5 ==="
curl -s "https://registry.npmjs.org/axios/1.13.5" | jq '.version // "NOT FOUND"'

Repository: wednesday-solutions/nextjs-template

Length of output: 273


Remove redundant axios dependency — not used directly in codebase.

The codebase imports only from apisauce (specifically the create function in app/utils/apiUtils.js) with no direct axios imports anywhere. Since apisauce internally handles axios, the explicit axios dependency is redundant. Remove it from package.json unless there's a specific reason for direct use that doesn't appear in the current code.

The apisauce@^3.2.2 major version bump is confirmed — review the changelog for breaking API changes that might affect apiUtils.js.

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

In `@package.json` around lines 50 - 51, package.json currently lists both
"apisauce" and "axios" though the code imports only from apisauce (see
app/utils/apiUtils.js using create), so remove the redundant "axios" entry from
package.json, run your package manager to update the lockfile (npm/yarn
install), and run the test suite; while doing this verify app/utils/apiUtils.js
still works with apisauce@^3.2.2 by checking usages of the create function
against the apisauce v3 changelog and update any call sites if required.

package.json Outdated
"invariant": "^2.2.4",
"lodash": "^4.17.21",
"next": "14.2.8",
"next": "14.2.35",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify npm package versions and indentation issues

echo "=== Checking if next@14.2.35 exists ==="
curl -s "https://registry.npmjs.org/next/14.2.35" | jq '.version // "NOT FOUND"' 2>/dev/null || echo "Request failed"

echo ""
echo "=== Checking latest next 14.x versions ==="
curl -s "https://registry.npmjs.org/next" | jq '[.versions | keys[] | select(startswith("14.2."))] | sort | .[-5:]' 2>/dev/null || echo "Request failed"

echo ""
echo "=== Checking if next@15.0.8 exists ==="
curl -s "https://registry.npmjs.org/next/15.0.8" | jq '.version // "NOT FOUND"' 2>/dev/null || echo "Request failed"

Repository: wednesday-solutions/nextjs-template

Length of output: 284


🏁 Script executed:

# Read the actual package.json content around the disputed lines
echo "=== Reading package.json lines 50-135 ==="
cat package.json | head -135 | tail -86 | cat -n

Repository: wednesday-solutions/nextjs-template

Length of output: 3533


Critical: next direct dependency (14.2.35) conflicts with its resolution override (>=15.0.8).

Line 57 pins next to 14.2.35, but the resolutions block (lines 80–84) forces "next": ">=15.0.8". Yarn/npm resolutions override all instances of a package in the dependency tree, including the root. This will force Next.js to resolve to 15.x, which is a major version with breaking changes (React 19 requirement, changed routing defaults, removed legacy APIs, etc.). The app will not build or run under this conflict.

Either remove the next resolution if you intend to stay on 14.x, or upgrade the direct dependency and entire app to Next.js 15+ if that's the goal.

Additionally, the resolutions block has inconsistent indentation. Lines 76–79 use 4-space indentation while lines 80–84 use 2-space indentation; standardize to 4 spaces for consistency.

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

In `@package.json` at line 57, The package.json currently pins the direct
dependency "next": "14.2.35" while the "resolutions" block forces "next":
">=15.0.8", creating a conflicting override that will force Next 15.x; either
remove the "next" entry from the resolutions block if you intend to remain on
Next 14.x, or upgrade the direct "next" dependency (and update app code to be
compatible) to >=15.0.8; additionally, normalize the indentation in the
"resolutions" block to 4 spaces for consistency.

package.json Outdated
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^9.9.1",
"eslint-config-next": "14.2.8",
"eslint-config-next": "^16.1.6",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n package.json | sed -n '50,100p'

Repository: wednesday-solutions/nextjs-template

Length of output: 2211


🌐 Web query:

eslint-config-next 16.1.6 Next.js 14 compatibility version requirements

💡 Result:

  • eslint-config-next@16.1.6 is meant to be used with next@16.1.6 (same release line). In real installs you’ll see projects pairing next: "16.1.6" with eslint-config-next: "16.1.6". [1]
  • It is not a compatible/expected pairing for Next.js 14. Next’s ESLint tooling changed in the v16 line (e.g., next lint and the eslint option in next.config.js were removed), so mixing eslint-config-next@16.x into a Next 14 app is likely to break or behave unexpectedly. [2], [3]
  • Version requirements for eslint-config-next@16.1.6:
    • Peer dependency: eslint >= 9.0.0 (so ESLint 8 will not satisfy it). [1]
    • Peer dependency: typescript >= 3.3.1 (optional). [1]

What to do for Next.js 14: pin eslint-config-next to the 14.x line (typically matching your next version, e.g. next@14.2.xeslint-config-next@14.2.x) rather than using 16.1.6. [2]

Sources:
[1] Package-lock excerpt showing eslint-config-next@16.1.6 peer deps (eslint >=9.0.0) and being used alongside next@16.1.6. (gitea.gahusb.synology.me)
[2] Next.js ESLint docs noting v16.0.0 removed next lint / related integration. (nextjs.org)
[3] Next.js GitHub issue documenting removal of the eslint field in next.config.js in Next 16 betas. (github.com)

Citations:


Update eslint-config-next to version 14.x to match Next.js 14.2.35.

eslint-config-next@^16.1.6 is incompatible with Next.js 14. The v16 release introduced breaking changes including removal of next lint and the eslint option in next.config.js. Pin eslint-config-next to the 14.x line (e.g., ^14.2.6) to align with the project's Next.js 14.2.35 version.

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

In `@package.json` at line 92, Update the package.json dependency
"eslint-config-next" from ^16.1.6 to the 14.x line to match Next.js 14.2.35
(e.g., change "eslint-config-next" to "^14.2.6"); after editing package.json,
run your package manager (npm/yarn/pnpm) to reinstall and update lockfile so the
dependency tree aligns with Next.js 14.

pages/_app.js Outdated
isAuthenticated,
needsEmailVerification
}) {
const publicPaths = ['/login', '/signup', '/verify-email'];
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

publicPaths is duplicated — extract to a shared constant.

The same list ['/login', '/signup', '/verify-email'] is defined in handleRouteProtection (Line 86) and again inside AppContent (Line 141). If one is updated without the other, route protection and player visibility will diverge silently.

♻️ Proposed fix
+const PUBLIC_PATHS = ['/login', '/signup', '/verify-email'];
+
 function handleRouteProtection({ router, isAuthenticated, needsEmailVerification }) {
-  const publicPaths = ['/login', '/signup', '/verify-email'];
   const path = router.pathname;
   ...
-  if (!isAuthenticated && !publicPaths.includes(path)) {
+  if (!isAuthenticated && !PUBLIC_PATHS.includes(path)) {
     router.replace('/login');
   }
 }

And in AppContent:

-  const publicPaths = ['/login', '/signup', '/verify-email'];
-  const showPlayer = canUseApp && !publicPaths.includes(router.pathname);
+  const showPlayer = canUseApp && !PUBLIC_PATHS.includes(router.pathname);

Also applies to: 141-141

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

In `@pages/_app.js` at line 86, The array literal for public paths is duplicated;
extract it into a single shared constant (e.g., const PUBLIC_PATHS =
['/login','/signup','/verify-email']) and replace both occurrences with that
constant so handleRouteProtection and AppContent reference the same value;
update any imports/usages to reference PUBLIC_PATHS and remove the duplicated
literal to ensure route protection and player visibility stay in sync.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/containers/Theme/selectors.js (1)

6-8: Unnecessary factory pattern defeats memoization — export selectors directly.

Both selectTheme and selectIsDarkMode are zero-argument factories that return a new createSelector instance on every call. Because neither selector closes over component-specific props, the factory wrapping provides no benefit. If consumers call them inline (e.g., useSelector(selectTheme())), a brand-new selector instance is allocated on every render, fully defeating reselect's memoization.

♻️ Simplify to direct exports
-export const selectTheme = () => createSelector(selectThemeDomain, (substate) => substate.theme);
+export const selectTheme = createSelector(selectThemeDomain, (substate) => substate.theme);

-export const selectIsDarkMode = () => createSelector(selectThemeDomain, (substate) => substate.theme === 'dark');
+export const selectIsDarkMode = createSelector(selectThemeDomain, (substate) => substate.theme === 'dark');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Theme/selectors.js` around lines 6 - 8, Change the zero-arg
selector factories to direct exported selectors: instead of exporting functions
selectTheme() and selectIsDarkMode() that return createSelector(...) each call,
export the selectors themselves (selectTheme and selectIsDarkMode) created once
via createSelector(selectThemeDomain, ...). Keep the same selector logic using
selectThemeDomain and the same projection functions, so consumers can call
useSelector(selectTheme) and benefit from reselect memoization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/containers/Theme/reducer.js`:
- Around line 32-34: The SET_THEME reducer currently assigns draft.theme =
action.theme and calls persistTheme(action.theme) without validation, which can
persist "undefined"; update the case handling in the reducer to validate
action.theme (e.g., check typeof action.theme === 'string' and/or membership in
the allowed theme set) before assigning draft.theme and calling persistTheme; if
invalid, skip persisting and fall back to a safe default or retain the existing
draft.theme (or call persistTheme with the fallback), and optionally extract the
validation into a helper like isValidTheme to keep the SET_THEME case concise.

---

Duplicate comments:
In `@app/containers/Theme/reducer.js`:
- Around line 13-15: initialState currently hardcodes theme: 'light' causing a
flash; change initialState in Theme reducer to derive theme from the persisted
source instead of a hardcoded value (e.g., read the saved value from
localStorage/sessionStorage or a synchronous persisted store) or set theme to
null/undefined so rehydration logic picks up the persisted value immediately;
update the initialState object (symbol: initialState, key: theme in
app/containers/Theme/reducer.js) and ensure any consumer logic handles
null/undefined until the persisted theme is applied.
- Around line 22-45: The reducer currently performs side effects via
persistTheme (writing to localStorage and mutating document.documentElement)
inside themeReducer when handling themeActionTypes.SET_THEME and TOGGLE_THEME;
remove the persistTheme calls from themeReducer so it remains a pure function,
and instead invoke persistence from an action creator or a middleware (e.g., a
thunk or a custom middleware that listens for SET_THEME/TOGGLE_THEME) which
calls persistTheme(theme) after the action is dispatched, or use store.subscribe
to persist state changes—ensure persistTheme remains a pure utility used only
outside reducers.

---

Nitpick comments:
In `@app/containers/Theme/selectors.js`:
- Around line 6-8: Change the zero-arg selector factories to direct exported
selectors: instead of exporting functions selectTheme() and selectIsDarkMode()
that return createSelector(...) each call, export the selectors themselves
(selectTheme and selectIsDarkMode) created once via
createSelector(selectThemeDomain, ...). Keep the same selector logic using
selectThemeDomain and the same projection functions, so consumers can call
useSelector(selectTheme) and benefit from reselect memoization.

Comment on lines +32 to +34
case themeActionTypes.SET_THEME:
draft.theme = action.theme;
persistTheme(action.theme);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SET_THEME applies action.theme without validation — can corrupt localStorage.

If setTheme() is called with undefined or an invalid string, draft.theme is set to that value and persistTheme(undefined) writes the literal string "undefined" to localStorage. On the next page load, the persisted theme is unrecognizable and the store may behave unexpectedly.

🛡️ Proposed fix — guard against invalid values
 case themeActionTypes.SET_THEME:
+  if (action.theme !== 'light' && action.theme !== 'dark') break;
   draft.theme = action.theme;
   persistTheme(action.theme);
   break;
📝 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 themeActionTypes.SET_THEME:
draft.theme = action.theme;
persistTheme(action.theme);
case themeActionTypes.SET_THEME:
if (action.theme !== 'light' && action.theme !== 'dark') break;
draft.theme = action.theme;
persistTheme(action.theme);
break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Theme/reducer.js` around lines 32 - 34, The SET_THEME reducer
currently assigns draft.theme = action.theme and calls
persistTheme(action.theme) without validation, which can persist "undefined";
update the case handling in the reducer to validate action.theme (e.g., check
typeof action.theme === 'string' and/or membership in the allowed theme set)
before assigning draft.theme and calling persistTheme; if invalid, skip
persisting and fall back to a safe default or retain the existing draft.theme
(or call persistTheme with the fallback), and optionally extract the validation
into a helper like isValidTheme to keep the SET_THEME case concise.

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