Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .claude/agents/frontend-engineer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: frontend-engineer
description: Use this agent when working on any frontend-related tasks including UI development, component creation, styling, user experience improvements, frontend testing, or visual debugging. Examples: <example>Context: User is building a React component and wants to ensure it follows best practices. user: 'I need to create a responsive navigation component for my website' assistant: 'I'll use the frontend-engineer agent to help you create a responsive navigation component following UI/UX best practices' <commentary>Since this is a frontend development task involving UI components, use the frontend-engineer agent to provide expert guidance on implementation and design.</commentary></example> <example>Context: User has implemented a form and wants to test its visual appearance and functionality. user: 'I just finished implementing a contact form, can you help me test it?' assistant: 'Let me use the frontend-engineer agent to review and test your contact form implementation' <commentary>Since this involves frontend testing and validation, use the frontend-engineer agent who can utilize Playwright for visual testing and provide UX feedback.</commentary></example>
model: sonnet
color: yellow
---

You are an expert senior frontend engineer with deep expertise in modern web development, UI/UX design principles, and frontend testing methodologies. You have extensive experience with React, Vue, Angular, vanilla JavaScript, CSS, HTML, and modern build tools. You are proficient with testing frameworks, particularly Playwright for end-to-end and visual testing.

When working on frontend tasks, you will:

1. **Analyze Requirements**: Carefully assess the frontend requirements, considering user experience, accessibility, performance, and responsive design needs.

2. **Apply Best Practices**: Implement solutions following industry best practices including:
- Semantic HTML structure
- Modern CSS techniques (Flexbox, Grid, CSS custom properties)
- Component-based architecture
- Accessibility standards (WCAG guidelines)
- Performance optimization
- Mobile-first responsive design

3. **Use Playwright for Testing**: Leverage Playwright to:
- Take screenshots of implementations
- Test user interactions and workflows
- Verify responsive behavior across different viewports
- Validate visual consistency
- Check for accessibility issues
- Test cross-browser compatibility

4. **UI/UX Design Principles**: Apply fundamental design principles including:
- Visual hierarchy and typography
- Color theory and contrast
- Spacing and layout consistency
- User-centered design approach
- Intuitive navigation patterns
- Loading states and error handling

5. **Code Quality**: Ensure code is:
- Clean, readable, and maintainable
- Properly structured and organized
- Following established coding standards
- Optimized for performance
- Well-documented when necessary

6. **Problem-Solving Approach**: When encountering issues:
- Use Playwright to inspect and debug visual problems
- Test across different devices and browsers
- Validate against design specifications
- Consider edge cases and error states
- Provide alternative solutions when needed

Always prioritize user experience, accessibility, and performance. When testing with Playwright, provide clear explanations of what you're testing and why. Offer specific, actionable recommendations for improvements based on modern frontend development standards.
10 changes: 10 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"mcp__sequential-thinking__sequentialthinking",
"mcp__browser-tools__takeScreenshot"
],
"deny": [],
"ask": []
}
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.15.0
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

- `npm run dev` - Start development server on localhost:3000
- `npm run build` - Build the application for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint with auto-fix

## Architecture Overview

Moodify is a Next.js application that extracts color palettes from Spotify album artwork and creates dynamic backgrounds. The app uses a microservices architecture with a separate backend for data processing.

### Core Data Flow
1. User searches for music via Spotify API (`lib/spotify.ts`)
2. Selected track's album artwork is sent to external palette service (`lib/palette-fetcher.ts`)
3. Color palette is generated and stored in MongoDB via backend API
4. Redux state manages both Spotify data and color palettes
5. Background dynamically updates based on extracted colors

### Key Architecture Components

**State Management**
- Redux Toolkit with two main slices: `spotifySlice` and `colourPaletteSlice`
- Store configuration in `lib/store.ts` with serialization disabled for complex color data
- Provider setup in `app/StoreProvider.tsx`

**External Dependencies**
- Backend API: `NEXT_PUBLIC_MOODIFY_BACKEND_URL` (palette generation and data storage)
- Spotify API: Client credentials flow implemented in `lib/spotify.ts`
- MongoDB: Accessed through backend, not directly from frontend

**API Routes Structure**
- `/api/spotify/search` - Proxies Spotify search requests
- `/api/data/collection/bulk` - Handles bulk track data operations
- `/api/data/collection/single` - Handles single track operations
- `/api/data/palette-picker` - Manages color palette operations

**Component Architecture**
- Theme system with dark/light mode support
- Reusable UI components in `app/components/ui/`
- Search functionality with debounced input
- Dynamic background rendering based on color palettes

### Environment Variables Required
```
NEXT_PUBLIC_MOODIFY_BACKEND_URL - Backend service URL
SPOTIFY_CLIENT_ID - Spotify API credentials
SPOTIFY_CLIENT_SECRET - Spotify API credentials
SPOTIFY_REFRESH_TOKEN - For authenticated requests
```

### Database Integration
- Track data is stored via backend API calls in `lib/database-handler.ts`
- Backend handles MongoDB operations and palette generation
- Frontend never directly accesses database
- Bulk operations supported for efficient data storage

### Color Palette Processing
- Album artwork URLs are sent to external palette service
- Service returns RGB color arrays (format: `[[r,g,b], [r,g,b], ...]`)
- Default emerald palette used as fallback on errors
- Palettes are stored with track metadata for future retrieval
Empty file added GEMINI.md
Empty file.
9 changes: 5 additions & 4 deletions app/api/data/palette-picker/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
);
} catch (error) {
console.error('Error in Spotify palette-picker API:', error);
// Return a default palette on error
// Return a default palette on error with proper error status
const defaultPalette = [
[52, 211, 153], // Emerald
[16, 185, 129],
Expand All @@ -35,10 +35,11 @@ export async function POST(request: NextRequest) {

return NextResponse.json(
{
error: 'Failed to process request',
palette: defaultPalette
error: error instanceof Error ? error.message : 'Failed to process request',
palette: defaultPalette,
fallback: true
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the palette-picker API fails, it returns a 200 status code, which is misleading. It should return a 500 status code to properly indicate a server error. This will help in debugging and handling errors on the client-side.

Suggested change
},
{ status: 500 }

{ status: 200 }
{ status: 500 }
);
}
}
28 changes: 28 additions & 0 deletions app/api/spotify/album/[id]/tracks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAlbumTracks } from '@/lib/spotify';

export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const params = await context.params;
const albumId = params.id;

if (!albumId) {
return NextResponse.json(
{ error: 'Album ID is required' },
{ status: 400 }
);
}

try {
const tracks = await getAlbumTracks(albumId);
return NextResponse.json({ tracks }, { status: 200 });
} catch (error) {
console.error('Error fetching album tracks:', error);
return NextResponse.json(
{ error: 'Failed to fetch album tracks' },
{ status: 500 }
);
}
}
7 changes: 5 additions & 2 deletions app/api/spotify/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { searchSpotify } from '@/lib/spotify';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const searchQuery = searchParams.get('q');
const offset = parseInt(searchParams.get('offset') ?? '0', 10);
const limit = parseInt(searchParams.get('limit') ?? '12', 10);
const type = searchParams.get('type') ?? 'track';

if (!searchQuery || !searchQuery.trim()) {
return NextResponse.json(
Expand All @@ -13,8 +16,8 @@ export async function GET(request: NextRequest) {
}

try {
const data = await searchSpotify(searchQuery);
return NextResponse.json({ tracks: data }, { status: 200 });
const data = await searchSpotify(searchQuery, offset, limit, type);
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error('Error searching Spotify:', error);
return NextResponse.json(
Expand Down
123 changes: 123 additions & 0 deletions app/components/album-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useState, useRef, useEffect, useCallback, memo } from 'react'
import Image from 'next/image'
import { Calendar, Disc, Music } from 'lucide-react'
import { SpotifyAlbum } from '../utils/interfaces'
import { useDispatch } from 'react-redux'
import { openAlbumTracksModal } from '@/lib/features/spotifySlice'

interface LazyImageProps {
src: string;
alt: string;
className?: string;
children?: React.ReactNode;
}

const LazyImage = memo(({ src, alt, className, children }: LazyImageProps) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);

if (imgRef.current) {
observer.observe(imgRef.current);
}

return () => observer.disconnect();
}, []);

return (
<div ref={imgRef} className="relative aspect-square">
{!isInView ? (
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 animate-pulse" />
) : (
<>
{!isLoaded && (
<div className="absolute inset-0 bg-gray-200 dark:bg-gray-700 animate-pulse" />
)}
<Image
src={src}
alt={alt}
fill
className={`object-cover transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'} ${className || ''}`}
onLoad={() => setIsLoaded(true)}
/>
{children}
</>
)}
</div>
);
});

LazyImage.displayName = 'LazyImage';

export const AlbumCard = memo(({ album }: { album: SpotifyAlbum }) => {
const dispatch = useDispatch()

const handleAlbumClick = useCallback(() => {
dispatch(openAlbumTracksModal(album))
}, [album, dispatch])

const formatDate = (dateString: string) => {
try {
return new Date(dateString).getFullYear()
} catch {
return dateString
}
}

return (
<div
className='rounded-2xl overflow-hidden transition-all group shadow-lg cursor-pointer hover:shadow-xl hover:scale-105'
onClick={handleAlbumClick}
>
<LazyImage
src={album.albumCover || '/placeholder.svg?height=300&width=300'}
alt={album.name}
className="group-hover:scale-110 transition-transform duration-300"
/>

<div className='p-4 backdrop-blur-lg bg-white/70 dark:bg-black/70'>
<h3 className='font-medium text-gray-900 dark:text-white truncate'>{album.name}</h3>
<p className='text-gray-700 dark:text-gray-300 text-sm truncate'>{album.artists.join(', ')}</p>

<div className='flex items-center gap-4 mt-2 text-xs text-gray-600 dark:text-gray-400'>
<div className='flex items-center gap-1'>
<Calendar className='h-3 w-3' />
<span>{formatDate(album.releaseDate)}</span>
</div>
<div className='flex items-center gap-1'>
<Music className='h-3 w-3' />
<span>{album.totalTracks} tracks</span>
</div>
</div>

<div className='flex gap-1 mt-2'>
{album.colourPalette.map((c: number[], i: number) => (
<div
key={i}
className="palette-circle"
style={{
backgroundColor: `rgb(${c[0]}, ${c[1]}, ${c[2]})`,
animationDelay: `${i * 0.05}s`,
animation: 'pop-in 0.3s forwards'
}}
title={`RGB(${c[0]}, ${c[1]}, ${c[2]})`}
/>
))}
</div>
</div>
</div>
)
})

AlbumCard.displayName = 'AlbumCard'
Loading