mirror of
https://github.com/Chevron7Locked/kima-hub.git
synced 2026-06-19 07:37:17 +00:00
refactor: arch audit Phase 3 -- frontend TypeScript strict mode fixes
- Enable strict mode in frontend tsconfig.json - Prefix unused parameters with _ across hooks and feature components - Fix noUnusedLocals violations in useQueries, useTVNavigation, and various feature components (AlbumHero, ArtistHero, ArtistActionBar, etc.) Addresses audit finding: TYPE-001 (frontend)
This commit is contained in:
@@ -0,0 +1,983 @@
|
||||
# Component Catalog for SvelteKit Migration
|
||||
|
||||
**Generated:** 2026-03-20
|
||||
**Total Components:** ~142 files across global and feature directories
|
||||
|
||||
---
|
||||
|
||||
## Global Components
|
||||
|
||||
### Layout Components
|
||||
|
||||
#### Sidebar
|
||||
**Path:** `/components/layout/Sidebar.tsx` (512 lines)
|
||||
|
||||
**Props:** None (self-contained)
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `isMobileMenuOpen`, `isSyncing`, `showCreatePlaylist`, `newPlaylistName`, `isCreating`
|
||||
- `refs`: `syncTimeoutRef`, `createPopoverRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Cleanup sync timeout on unmount
|
||||
- `useEffect`: Close mobile menu on route change (depends: `pathname`)
|
||||
- `useEffect`: Handle escape key for mobile menu (depends: `isMobileMenuOpen`)
|
||||
- `useEffect`: Listen for `toggle-mobile-menu` custom event
|
||||
- `useEffect`: Close create playlist popover on click outside (depends: `showCreatePlaylist`)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePathname`, `useRouter`: Next.js navigation
|
||||
- `useAuth`: `isAuthenticated`
|
||||
- `useToast`: `toast.error()`
|
||||
- `useAudioState`: `currentTrack`, `currentAudiobook`, `currentPodcast`, `playbackType`
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
- `useQuery`: Fetch playlists from API
|
||||
|
||||
**API Calls:**
|
||||
- `api.getPlaylists()`: Fetch user playlists
|
||||
- `api.scanLibrary()`: Trigger library sync
|
||||
- `api.createPlaylist(name)`: Create new playlist
|
||||
|
||||
**Child Components:**
|
||||
- `MobileSidebar`: Props - `isOpen`, `onClose`
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleSync`: Triggers library scan, shows error toast on failure
|
||||
- `handleCreatePlaylist`: Creates playlist, invalidates query, navigates to new playlist
|
||||
|
||||
**Next.js APIs:**
|
||||
- `Link`: Navigation
|
||||
- `usePathname`: Route detection
|
||||
- `useRouter`: Programmatic navigation
|
||||
- `Image`: Optimized image loading
|
||||
|
||||
---
|
||||
|
||||
#### TopBar
|
||||
**Path:** `/components/layout/TopBar.tsx` (399 lines)
|
||||
|
||||
**Props:** None (self-contained)
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `searchQuery`, `scanJobId`, `lastScanTime`
|
||||
- `refs`: `searchTimeoutRef`, `searchInputRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Handle scan completion/failure (depends: `scanStatus`, `scanJobId`)
|
||||
- `useEffect`: Auto-search with debounce (depends: `searchQuery`, `router`, `pathname`)
|
||||
- `useEffect`: Sync search query with URL on page change (depends: `pathname`)
|
||||
- `useEffect`: Global "/" keyboard shortcut to focus search
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePathname`, `useRouter`: Next.js navigation
|
||||
- `useAuth`: `logout`
|
||||
- `useToast`: `toast.success()`, `toast.error()`
|
||||
- `useDownloadContext`: `pendingDownloads`, `downloadStatus`
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
- `useQuery`: Fetch scan status
|
||||
- `useQueryClient`: Invalidate queries
|
||||
|
||||
**API Calls:**
|
||||
- `api.scanLibrary()`: Trigger library scan
|
||||
|
||||
**Child Components:**
|
||||
- `ActivityPanelToggle`: Standalone button component
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleSync`: Trigger library scan with 5s cooldown
|
||||
- `handleLogout`: Logout user with toast feedback
|
||||
- `handleSearch`: Navigate to search page
|
||||
|
||||
**Next.js APIs:**
|
||||
- `Link`: Navigation
|
||||
- `usePathname`: Route detection
|
||||
- `useRouter`: Programmatic navigation
|
||||
- `Image`: Logo display
|
||||
|
||||
---
|
||||
|
||||
#### BottomNavigation
|
||||
**Path:** `/components/layout/BottomNavigation.tsx` (99 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:** None (derived from props/hooks)
|
||||
|
||||
**Effects:** None
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePathname`: Route matching
|
||||
- `useIsMobile`, `useIsTablet`: Conditional rendering
|
||||
|
||||
**API Calls:** None
|
||||
|
||||
**Child Components:** None (renders `Link` directly)
|
||||
|
||||
**Event Handlers:** None (navigation via `Link`)
|
||||
|
||||
**Next.js APIs:**
|
||||
- `Link`: Navigation with prefetch
|
||||
|
||||
---
|
||||
|
||||
#### MobileSidebar
|
||||
**Path:** `/components/layout/MobileSidebar.tsx` (229 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface MobileSidebarProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `isSyncing`
|
||||
- `refs`: `isFirstRender`, `syncTimeoutRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Close on route change (skip initial mount)
|
||||
- `useEffect`: Cleanup sync timeout
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePathname`: Route detection
|
||||
- `useAuth`: `logout`
|
||||
- `useToast`: Toast feedback
|
||||
- `useQueryClient`: Invalidate notifications
|
||||
|
||||
**API Calls:**
|
||||
- `api.scanLibrary()`: Trigger library sync
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleSync`: Sync library, close menu on success
|
||||
- `handleLogout`: Logout, close menu
|
||||
|
||||
---
|
||||
|
||||
#### AuthenticatedLayout
|
||||
**Path:** `/components/layout/AuthenticatedLayout.tsx` (198 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AuthenticatedLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (derived)
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Listen for activity panel events (toggle/open/close/set-tab)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAuth`: `isAuthenticated`, `isLoading`
|
||||
- `usePathname`: Route detection for public paths
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
- `useIsTV`: TV detection
|
||||
- `useActivityPanel`: Panel state management
|
||||
- `useImportToasts`: Import notification toasts
|
||||
|
||||
**API Calls:** None (delegates to child components)
|
||||
|
||||
**Child Components:**
|
||||
- `Sidebar`: Conditional rendering
|
||||
- `TopBar`: Always rendered
|
||||
- `TVLayout`: When `isTV`
|
||||
- `BottomNavigation`: When mobile/tablet
|
||||
- `ActivityPanel`: Mobile overlay
|
||||
- `UnifiedPanel`: Desktop side panel
|
||||
- `UniversalPlayer`: Player wrapper
|
||||
- `MediaControlsHandler`: Media session controls
|
||||
- `PlayerModeWrapper`: Player mode state
|
||||
- `GalaxyBackground`: Background gradient
|
||||
|
||||
**Layout Logic:**
|
||||
- Public pages (`/login`, `/register`, `/onboarding`, `/sync`, `/share/*`): Render children only
|
||||
- TV: `TVLayout` with full keyboard navigation
|
||||
- Mobile/Tablet: `TopBar` + `Sidebar` (mobile drawer) + `BottomNavigation` + `ActivityPanel` overlay
|
||||
- Desktop: `TopBar` + `Sidebar` + `UnifiedPanel` side panel
|
||||
|
||||
---
|
||||
|
||||
#### TVLayout
|
||||
**Path:** `/components/layout/TVLayout.tsx` (328 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface TVLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `focusedTabIndex`, `isNavFocused`, `isSyncing`
|
||||
- `refs`: `navRef`, `currentTimeRef`, `durationRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Add/remove `tv-mode` class on mount/unmount
|
||||
- `useEffect`: Sync `currentTime` to ref
|
||||
- `useEffect`: Sync `duration` to ref
|
||||
- `useEffect`: Listen for keyboard events (global)
|
||||
- `useEffect`: Focus correct nav tab when `isNavFocused` changes
|
||||
- `useEffect`: Set correct focused tab on pathname change
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePathname`, `useRouter`: Navigation
|
||||
- `useAudio`: Full audio control (`currentTrack`, `isPlaying`, `pause`, `resumeWithGesture`, `next`, `previous`, `seek`, etc.)
|
||||
- `useTVNavigation`: Content navigation hook
|
||||
|
||||
**API Calls:**
|
||||
- `api.scanLibrary()`: Trigger sync
|
||||
- `api.getCoverArtUrl()`: Generate cover art URLs
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleKeyDown`: Global keyboard handler (DPAD, media keys)
|
||||
- `handleSync`: Library sync
|
||||
|
||||
**TV-Specific Features:**
|
||||
- DPAD navigation for menu tabs
|
||||
- Media key support (play/pause, next/previous, fast forward/rewind)
|
||||
- Focus management with `data-tv-tab` attributes
|
||||
- Content navigation via `useTVNavigation` hook
|
||||
|
||||
---
|
||||
|
||||
#### ActivityPanel
|
||||
**Path:** `/components/layout/ActivityPanel.tsx` (301 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ActivityPanelProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
activeTab?: ActivityTab; // "notifications" | "active" | "imports" | "history" | "settings"
|
||||
onTabChange?: (tab: ActivityTab) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `internalActiveTab` (fallback if prop not provided)
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Auto-switch to notifications if settings tab has no content
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useNotifications`: `unreadCount`
|
||||
- `useActiveDownloads`: `downloads`
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
- `useActivityPanelSettings`: `settingsContent`, `setSettingsContent`
|
||||
|
||||
**Child Components (Tabs):**
|
||||
- `NotificationsTab`: Notification feed
|
||||
- `ActiveDownloadsTab`: Download progress
|
||||
- `ImportsTab`: Import history
|
||||
- `HistoryTab`: Playback history
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleTabClick`: Switch tabs, clear settings content when leaving settings tab
|
||||
|
||||
**Layout:**
|
||||
- Mobile/Tablet: Full-screen overlay with backdrop
|
||||
- Desktop: Collapsible side panel (48px collapsed, 450px expanded)
|
||||
|
||||
---
|
||||
|
||||
#### UnifiedPanel
|
||||
**Path:** `/components/layout/UnifiedPanel.tsx` (109 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface UnifiedPanelProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `activeTab` (default: "now-playing"), `expandedActivity`
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useNotifications`, `useActiveDownloads`: Activity badges
|
||||
- `ActivityIconBar`: Activity type selector
|
||||
- `ActivityHeader`, `ActivityContent`: Expanded activity views
|
||||
- `TabBar`, `TabContent`: Default tab system
|
||||
|
||||
**Child Components:**
|
||||
- `ActivityIconBar`: Props - `expandedActivity`, `onToggleActivity`
|
||||
- `ActivityHeader`: Props - `type`, `onClose`
|
||||
- `TabBar`: Props - `activeTab`, `onTabClick`
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleToggleActivity`: Expand/collapse activity type
|
||||
|
||||
**Layout:** Desktop-only collapsible panel (380px expanded, 48px collapsed)
|
||||
|
||||
---
|
||||
|
||||
### Player Components
|
||||
|
||||
#### UniversalPlayer
|
||||
**Path:** `/components/player/UniversalPlayer.tsx` (58 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:**
|
||||
- `useRef`: `lastMediaIdRef`, `hasAutoSwitchedRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Auto-switch to overlay mode on mobile when media starts playing (fires once per mount)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAudio`: `playerMode`, `setPlayerMode`, `currentTrack`, `isPlaying`
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
|
||||
**Child Components (Conditional):**
|
||||
- `OverlayPlayer`: When `playerMode === "overlay"` and has media
|
||||
- `MiniPlayer`: Mobile/tablet default
|
||||
- `FullPlayer`: Desktop default
|
||||
|
||||
**Logic:**
|
||||
- Auto-opens overlay on mobile when user initiates playback
|
||||
- Prevents re-opening on auto-advances (skips, queue)
|
||||
|
||||
---
|
||||
|
||||
#### MiniPlayer
|
||||
**Path:** `/components/player/MiniPlayer.tsx` (689 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `isMinimized`, `isDismissed`, `swipeOffset`, `lastMediaId`
|
||||
- `refs`: `touchStartX`
|
||||
|
||||
**Effects:**
|
||||
- Derived state: Reset dismissed/minimized on media change or resume
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAudioState`: `currentTrack`, `playbackType`, `isShuffle`, `repeatMode`, `activeOperation`
|
||||
- `useAudioPlayback`: `isPlaying`, `isBuffering`, `canSeek`, `downloadProgress`, `audioError`, `clearAudioError`
|
||||
- `useAudioControls`: `pause`, `resumeWithGesture`, `next`, `previous`, `toggleShuffle`, `toggleRepeat`, `seek`, `skipForward`, `skipBackward`, `setPlayerMode`
|
||||
- `usePlaybackProgress`: `duration`, `progress`
|
||||
- `useMediaInfo`: `title`, `subtitle`, `coverUrl`, `mediaLink`, `hasMedia`
|
||||
- `useFeatures`: `vibeEmbeddings`, `loading`
|
||||
- `useVibeToggle`: `handleVibeToggle`, `isVibeLoading`
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Swipe gestures (right=minimize, left=open overlay/dismiss)
|
||||
|
||||
**Child Components:**
|
||||
- `KeyboardShortcutsTooltip`: Desktop hints
|
||||
- `SeekSlider`: Progress bar with seek
|
||||
- `SleepTimer`: Sleep timer popover
|
||||
- `ChevronLeft`: Minimized tab indicator
|
||||
|
||||
**Mobile-Specific Features:**
|
||||
- Swipe RIGHT → minimize to corner tab
|
||||
- Swipe LEFT + playing → open overlay
|
||||
- Swipe LEFT + not playing → dismiss completely
|
||||
- Gradient border with animated background
|
||||
- Touch gesture feedback with opacity/transform
|
||||
|
||||
**Desktop Features:**
|
||||
- Full-width bottom player
|
||||
- Volume slider
|
||||
- Shuffle/repeat/vibe/lyrics/queue toggles
|
||||
- 30s skip buttons
|
||||
- Keyboard shortcuts tooltip
|
||||
|
||||
---
|
||||
|
||||
#### FullPlayer
|
||||
**Path:** `/components/player/FullPlayer.tsx` (584 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `showPlaylistSelector`
|
||||
- `useMemo`: `vibeMatchScore`
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAudioState`: Full state access
|
||||
- `useAudioPlayback`: Playback status
|
||||
- `useAudioControls`: All controls
|
||||
- `usePlaybackProgress`: Time display
|
||||
- `useMediaInfo`: Track metadata
|
||||
- `useVibeToggle`: Vibe match
|
||||
- `useFeatures`: Embeddings availability
|
||||
- `useToast`: Success/error toasts
|
||||
- `useAddToPlaylistMutation`: Add to playlist
|
||||
- `useLyricsToggle`: Lyrics visibility
|
||||
- `router`, `pathname`: Navigation
|
||||
|
||||
**Child Components:**
|
||||
- `SeekSlider`: Progress bar
|
||||
- `SleepTimer`: Sleep timer
|
||||
- `KeyboardShortcutsTooltip`: Hints
|
||||
- `PlaylistSelector`: Modal (conditional)
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleVolumeChange`: Volume slider
|
||||
|
||||
**Features:**
|
||||
- Full playback controls
|
||||
- Volume slider with mute toggle
|
||||
- Vibe match score display (when active)
|
||||
- Lyrics toggle button
|
||||
- Queue navigation
|
||||
- Add to playlist modal
|
||||
- 30s skip buttons
|
||||
- Shuffle/repeat controls
|
||||
|
||||
---
|
||||
|
||||
#### OverlayPlayer
|
||||
**Path:** `/components/player/OverlayPlayer.tsx` (467 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `swipeOffset`
|
||||
- `refs`: `touchStartX`
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAudioState`, `useAudioPlayback`, `useAudioControls`: Full audio control
|
||||
- `usePlaybackProgress`: Time display
|
||||
- `useMediaInfo`: Track metadata
|
||||
- `useVibeToggle`: Vibe mode
|
||||
- `useLyricsToggle`: Lyrics visibility (mobile only)
|
||||
- `useIsMobile`, `useIsTablet`: Device detection
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Swipe for track skip (left=next, right=prev)
|
||||
|
||||
**Child Components:**
|
||||
- `SeekSlider`: Progress bar
|
||||
- `SleepTimer`: Sleep timer
|
||||
- `MobileLyricsView`: Lyrics display (mobile, when active)
|
||||
|
||||
**Mobile-Specific Features:**
|
||||
- Full-screen overlay (z-index 9999)
|
||||
- Swipe LEFT/RIGHT on artwork to skip tracks
|
||||
- Swipe feedback with transform/opacity
|
||||
- Album art / lyrics swap (mobile only)
|
||||
- Portrait vs landscape layout
|
||||
- Safe area padding for iOS
|
||||
|
||||
---
|
||||
|
||||
#### SeekSlider
|
||||
**Path:** `/components/player/SeekSlider.tsx` (273 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SeekSliderProps {
|
||||
progress: number; // 0-100 percentage
|
||||
duration: number; // seconds
|
||||
onSeek: (time: number) => void;
|
||||
canSeek: boolean;
|
||||
hasMedia: boolean;
|
||||
downloadProgress?: number | null;
|
||||
className?: string;
|
||||
showHandle?: boolean; // default: true
|
||||
variant?: "default" | "minimal" | "overlay";
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `isDragging`, `previewProgress`
|
||||
- `refs`: `sliderRef`, `touchIdentifierRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Add/remove global mouse listeners when dragging
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Touch seeking with identifier tracking
|
||||
- `handleMouseDown`, `handleMouseMove`, `handleMouseUp`: Mouse seeking
|
||||
- `handleClick`: Click-to-seek
|
||||
|
||||
**Features:**
|
||||
- Drag handle shows on hover/drag (configurable)
|
||||
- Preview progress while dragging
|
||||
- Tooltip text based on state (downloading, seeking disabled, etc.)
|
||||
- Variant-specific styling (minimal for mini player, overlay for full screen)
|
||||
- Prevents scroll/parent swipe during touch drag
|
||||
|
||||
---
|
||||
|
||||
#### SleepTimer
|
||||
**Path:** `/components/player/SleepTimer.tsx` (151 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SleepTimerProps {
|
||||
size?: "sm" | "md"; // default: "md"
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `isOpen`, `customMinutes`
|
||||
- `refs`: `popoverRef`, `buttonRef`
|
||||
|
||||
**Effects:**
|
||||
- `useEffect`: Close popover on outside click or Escape key
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useSleepTimer`: `isActive`, `remainingSeconds`, `displayRemaining`, `setTimer`, `clearTimer`
|
||||
|
||||
**Event Handlers:**
|
||||
- `handlePreset`: Set timer from preset (15, 30, 45, 60, 90, 120 mins)
|
||||
- `handleCustom`: Set custom timer (1-480 mins)
|
||||
|
||||
**Features:**
|
||||
- Preset buttons for common durations
|
||||
- Custom input field
|
||||
- Cancel button when active
|
||||
- Popover closes on outside click or Escape
|
||||
|
||||
---
|
||||
|
||||
#### PlayerModeWrapper
|
||||
**Path:** `/components/player/PlayerModeWrapper.tsx` (11 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface PlayerModeWrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Wrapper component that initializes `usePlayerMode` hook (must be client component)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `usePlayerMode`: Manages player mode state (mini/full/overlay)
|
||||
|
||||
---
|
||||
|
||||
### UI Components
|
||||
|
||||
#### Button
|
||||
**Path:** `/components/ui/Button.tsx` (59 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "ghost" | "danger" | "ai" | "icon";
|
||||
isLoading?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:** `forwardRef` + `memo` with `displayName`
|
||||
|
||||
**Variants:**
|
||||
- `primary`: Brand color (#fca200), black text, shadow
|
||||
- `secondary`: Dark bg (#1a1a1a), white text, border
|
||||
- `ghost`: Transparent, gray text, hover bg
|
||||
- `danger`: Red text, red border
|
||||
- `ai`: Dark bg, brand text, brand hover
|
||||
- `icon`: Fixed size (32x32), icon button
|
||||
|
||||
**Features:**
|
||||
- Loading state with `GradientSpinner`
|
||||
- Disabled state handling
|
||||
- Focus ring accessibility
|
||||
|
||||
---
|
||||
|
||||
#### CachedImage
|
||||
**Path:** `/components/ui/CachedImage.tsx` (34 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface CachedImageProps extends Omit<ImageProps, "src"> {
|
||||
src: string | null | undefined;
|
||||
fill?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:** `memo` wrapper around Next.js `Image`
|
||||
|
||||
**Features:**
|
||||
- Null src handling (returns null)
|
||||
- Lazy loading by default
|
||||
- Unoptimized (covers served via API, not static)
|
||||
- Service Worker caching for `/api/library/cover-art/*`
|
||||
|
||||
---
|
||||
|
||||
#### EmptyState
|
||||
**Path:** `/components/ui/EmptyState.tsx` (47 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface EmptyStateProps {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
children?: ReactNode;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: "primary" | "secondary" | "ghost";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:** `memo`
|
||||
|
||||
**Usage:** Generic empty state for lists, searches, etc.
|
||||
|
||||
---
|
||||
|
||||
#### HomeHero
|
||||
**Path:** `/features/home/components/HomeHero.tsx` (40 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Features:**
|
||||
- Time-based greeting (morning/afternoon/evening)
|
||||
- System status indicator
|
||||
- Two-line title with brand color accent
|
||||
- Subtitle with page description
|
||||
|
||||
---
|
||||
|
||||
#### AlbumHero
|
||||
**Path:** `/features/album/components/AlbumHero.tsx` (181 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AlbumHeroProps {
|
||||
album: Album;
|
||||
source: AlbumSource; // "library" | "discover" | "browse"
|
||||
coverUrl: string | null;
|
||||
colors: ColorPalette | null; // From VibrantJS
|
||||
onReload: () => void;
|
||||
children?: ReactNode; // Action bar
|
||||
}
|
||||
```
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAlbumDisplayData`: Title/artist/year with user overrides
|
||||
- `lazy` + `Suspense`: MetadataEditor (library albums only)
|
||||
|
||||
**Features:**
|
||||
- Dynamic background gradient from album art colors
|
||||
- Album cover with fallback icon
|
||||
- Edit badge for user-overridden metadata
|
||||
- MetadataEditor modal (lazy-loaded, library albums only)
|
||||
- Duration formatting (hours/minutes)
|
||||
- Genre tag display
|
||||
|
||||
---
|
||||
|
||||
## Feature Components Summary
|
||||
|
||||
### Album Features
|
||||
- **TrackList**: Renders album tracks with play/download actions
|
||||
- **AlbumActionBar**: Play all, shuffle, download, add to playlist buttons
|
||||
- **SimilarAlbums**: Carousel of similar albums based on genre/year
|
||||
|
||||
### Artist Features
|
||||
- **AvailableAlbums**: Grid of artist's albums
|
||||
- **SimilarArtists**: Carousel of similar artists
|
||||
- **ArtistHero**: Artist info with cover art and bio
|
||||
- **PopularTracks**: Top tracks list
|
||||
- **Discography**: Album grid by year
|
||||
- **ArtistBio**: Artist biography (if available)
|
||||
- **ArtistActionBar**: Follow, download, shuffle actions
|
||||
|
||||
### Audiobook Features
|
||||
- **ChapterList**: Chapter list with progress tracking
|
||||
- **AudiobookActionBar**: Play, download, add to library actions
|
||||
- **AudiobookHero**: Book info with cover and author
|
||||
|
||||
### Discover Features
|
||||
- **TrackList**: Discover tracks with preview player
|
||||
- **HowItWorks**: Feature explanation modal
|
||||
- **DiscoverActionBar**: Search, filter actions
|
||||
- **UnavailableAlbums**: Shows albums not yet in library
|
||||
- **DiscoverHero**: Discover page header
|
||||
|
||||
### Home Features
|
||||
- **SectionHeader**: Reusable section title with "See all" link
|
||||
- **HomeHero**: Greeting and status
|
||||
- **PopularArtistsGrid**: Artist cards grid
|
||||
- **FeaturedPlaylistsGrid**: Playlist cards
|
||||
- **PodcastsGrid**: Podcast cards
|
||||
- **AudiobooksGrid**: Audiobook cards
|
||||
- **ContinueListening**: Recently played items
|
||||
- **LibraryRadioStations**: Genre-based radio stations
|
||||
- **MixesGrid**: AI-generated mix cards
|
||||
- **ArtistsGrid**: Generic artist grid component
|
||||
- **LibraryRadioStations**: Radio station generator
|
||||
|
||||
### Library Features
|
||||
- **TracksList**: Library track list with delete/move actions
|
||||
- **LibraryHeader**: Library stats and filters
|
||||
- **LibraryToolbar**: View toggle (grid/list), sort dropdown
|
||||
- **LibraryTabs**: Tab switcher (tracks/albums/artists)
|
||||
- **AlbumsGrid**: Album grid with cover art
|
||||
- **ArtistsGrid**: Artist grid with portraits
|
||||
|
||||
### Podcast Features
|
||||
- **PodcastActionBar**: Subscribe, download actions
|
||||
- **SimilarPodcasts**: Recommendations
|
||||
- **PodcastHero**: Podcast info with cover
|
||||
- **PreviewEpisodes**: Latest episodes (not in library)
|
||||
- **ContinueListening**: In-progress episodes
|
||||
- **EpisodeList**: Full episode list with progress
|
||||
|
||||
### Search Features
|
||||
- **TopResult**: Featured result (album/artist/podcast)
|
||||
- **UnifiedSongsList**: Combined search results
|
||||
- **LibraryTracksList**: Library-only track results
|
||||
- **LibraryAlbumsGrid**: Library album results
|
||||
- **LibraryPodcastsGrid**: Library podcast results
|
||||
- **LibraryAudiobooksGrid**: Library audiobook results
|
||||
- **SoulseekBrowser**: External Soulseek search
|
||||
- **SearchFilters**: Filter dropdown (type, source, quality)
|
||||
- **AliasResolutionBanner**: Shows resolved artist aliases
|
||||
- **SimilarArtistsGrid**: Artist recommendations
|
||||
- **EmptyState**: No results message
|
||||
- **TVSearchInput**: Search bar with TV navigation support
|
||||
|
||||
### Settings Features
|
||||
**Sections:**
|
||||
- **StoragePathsSection**: Library path configuration
|
||||
- **SoulseekSection**: Soulseek client settings
|
||||
- **SubsonicSection**: Subsonic API configuration
|
||||
- **AccountSection**: User profile and password
|
||||
- **CacheSection**: Cache size and cleanup
|
||||
- **UserManagementSection**: Admin user management
|
||||
- **CorruptTracksSection**: Track repair tools
|
||||
- **PlaybackSection**: Audio playback settings
|
||||
- **DownloadPreferencesSection**: Download quality/format
|
||||
- **LidarrSection**: Lidarr integration
|
||||
- **AIServicesSection**: CLAP/embedding service config
|
||||
- **AudiobookshelfSection**: Audiobookshelf integration
|
||||
|
||||
**UI Components:**
|
||||
- **SettingsLayout**: Main settings container with sidebar
|
||||
- **SettingsSidebar**: Navigation sidebar
|
||||
- **SettingsSection**: Section wrapper with title/description
|
||||
- **SettingsRow**: Key-value row for settings
|
||||
- **SettingsToggle**: Boolean toggle switch
|
||||
- **SettingsSelect**: Dropdown selector
|
||||
- **SettingsInput**: Text/number input
|
||||
|
||||
### Vibe Features
|
||||
- **VibeMap**: Main 3D visualization component
|
||||
- **VibeToolbar**: Control toolbar
|
||||
- **VibePanelSheet**: Side panel with tabs
|
||||
- **VibeSongPath**: Song path visualization
|
||||
- **ActivityIconBar**: Activity type selector
|
||||
- **GravityGridScene**: Three.js gravity simulation scene
|
||||
- **LyricsTab**: Lyrics display in panel
|
||||
- **NowPlayingTab**: Current track info
|
||||
- **QueueTab**: Play queue in panel
|
||||
- **panel-shared.tsx**: Shared panel components
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
### React Libraries
|
||||
- **next**: Next.js 15 (App Router, Server Components)
|
||||
- **react**: React 19 (hooks, memo, forwardRef, Suspense, lazy)
|
||||
- **@tanstack/react-query**: Data fetching and caching
|
||||
- **lucide-react**: Icon library
|
||||
|
||||
### UI/Styling
|
||||
- **tailwindcss**: Utility-first CSS
|
||||
- **clsx**: Conditional className merging
|
||||
- **tailwind-merge**: Smart className merging
|
||||
|
||||
### Audio/Media
|
||||
- **three**: 3D visualization (Vibe mode)
|
||||
- **@react-three/fiber**: Three.js React renderer
|
||||
- **@react-three/drei**: Three.js helpers
|
||||
|
||||
### Utilities
|
||||
- **vibrant-js**: Image color extraction
|
||||
- **howler**: Audio playback (via custom wrapper)
|
||||
|
||||
---
|
||||
|
||||
## Next.js-Specific APIs Used
|
||||
|
||||
### Routing
|
||||
- `usePathname`: Active route detection
|
||||
- `useRouter`: Programmatic navigation
|
||||
- `Link`: Client-side navigation with prefetch
|
||||
- File-based routing in `/app` directory
|
||||
|
||||
### Image Optimization
|
||||
- `next/image`: Optimized image loading with lazy loading
|
||||
- `unoptimized` prop used for API-served images
|
||||
|
||||
### Server Components
|
||||
- Default: All components are Server Components unless marked `"use client"`
|
||||
- Client components used for:
|
||||
- Hooks (`useState`, `useEffect`, etc.)
|
||||
- Browser APIs
|
||||
- Event handlers
|
||||
- Context providers/consumers
|
||||
|
||||
### API Routes
|
||||
- `/app/api/events/route.ts`: SSE endpoint
|
||||
- `/app/api/events/ticket/route.ts`: SSE ticket generation
|
||||
|
||||
---
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### 1. forwardRef + memo Wrapper
|
||||
All UI components use this pattern for performance and debugging:
|
||||
```typescript
|
||||
const Component = memo(forwardRef<HTMLDivElement, ComponentProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return <div ref={ref} className={cn(baseStyles, className)} {...props} />;
|
||||
}
|
||||
));
|
||||
Component.displayName = "ComponentName";
|
||||
```
|
||||
|
||||
### 2. cn() Utility
|
||||
Class name merging using `clsx` + `tailwind-merge`:
|
||||
```typescript
|
||||
className={cn("base-styles", conditional && "conditional-styles", props.className)}
|
||||
```
|
||||
|
||||
### 3. Split Context Pattern
|
||||
Audio state split into 4 contexts to avoid re-renders:
|
||||
- `AudioStateProvider`: Core state (current track, queue, shuffle, repeat)
|
||||
- `AudioPlaybackProvider`: Playback status (playing, buffering, error)
|
||||
- `AudioControlsProvider`: Control functions (play, pause, seek, skip)
|
||||
- `AudioController`: Low-level audio element management
|
||||
|
||||
### 4. Query Key Factory
|
||||
Type-safe query keys in `/hooks/useQueries.ts`:
|
||||
```typescript
|
||||
export const queryKeys = {
|
||||
albums: (artistId?: string) => ["albums", artistId] as const,
|
||||
tracks: (albumId?: string) => ["tracks", albumId] as const,
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Lazy Loading
|
||||
Modal components lazy-loaded to reduce initial bundle:
|
||||
```typescript
|
||||
const MetadataEditor = lazy(() => import("./MetadataEditor"));
|
||||
// Usage with Suspense
|
||||
<Suspense fallback={null}>
|
||||
<MetadataEditor ... />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### 6. Custom Event Bus
|
||||
Cross-component communication via custom events:
|
||||
```typescript
|
||||
window.dispatchEvent(new CustomEvent("toggle-activity-panel"));
|
||||
window.addEventListener("toggle-activity-panel", handler);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Considerations
|
||||
|
||||
### Next.js → SvelteKit Mapping
|
||||
|
||||
| Next.js | SvelteKit | Notes |
|
||||
|---------|-----------|-------|
|
||||
| `usePathname` | `$page.url.pathname` | Reactive in Svelte |
|
||||
| `useRouter` | `goto()` from `$app/navigation` | Client-side navigation |
|
||||
| `Link` | `a` tag with `data-sveltekit-preload-data` | Native SvelteKit prefetch |
|
||||
| `next/image` | `svelte/image` or custom | Need caching strategy |
|
||||
| Server Components | SSR + `{#if browser}` | Client-only code in `{@render}` or components |
|
||||
| API Routes | Endpoint routes (`+server.ts`) | Similar pattern |
|
||||
| `useEffect` | `onMount` | Lifecycle hook |
|
||||
| `useState` | `let` with reactivity | Native Svelte reactivity |
|
||||
| Context | Svelte `setContext`/`getContext` | Built-in |
|
||||
| React Query | Svelte stores + custom hooks | Need to reimplement caching |
|
||||
|
||||
### Component Conversion Priority
|
||||
|
||||
**High Priority (Core UI):**
|
||||
1. Button, EmptyState, CachedImage (UI primitives)
|
||||
2. Sidebar, TopBar, BottomNavigation (layout)
|
||||
3. MiniPlayer, FullPlayer, OverlayPlayer (player)
|
||||
4. SeekSlider, SleepTimer (player controls)
|
||||
|
||||
**Medium Priority (Feature Pages):**
|
||||
5. HomeHero, AlbumHero, ArtistHero (page headers)
|
||||
6. Grid components (AlbumsGrid, ArtistsGrid, etc.)
|
||||
7. List components (TrackList, EpisodeList, etc.)
|
||||
|
||||
**Low Priority (Advanced Features):**
|
||||
8. VibeMap (Three.js integration - complex)
|
||||
9. Settings sections (form-heavy)
|
||||
10. Search features (Soulseek browser)
|
||||
|
||||
### State Management Migration
|
||||
|
||||
| React Pattern | Svelte Equivalent |
|
||||
|---------------|-------------------|
|
||||
| `useState` | `let` variable |
|
||||
| `useReducer` | `writable` store |
|
||||
| `useContext` | `setContext`/`getContext` |
|
||||
| React Query | Custom stores + `derived` |
|
||||
| `useMemo` | `derived` store or computed property |
|
||||
| `useCallback` | Function in store or memoized with `derived` |
|
||||
|
||||
### Audio System Migration
|
||||
|
||||
The split audio context pattern should be preserved:
|
||||
- Svelte stores for each concern (`audioState.svelte`, `audioPlayback.svelte`, etc.)
|
||||
- Use `$state` runes in Svelte 5 for fine-grained reactivity
|
||||
- Keep `AudioController` as a singleton service
|
||||
|
||||
---
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Total Components:** ~142 files
|
||||
- **Layout Components:** 8 files
|
||||
- **Player Components:** 9 files
|
||||
- **UI Components:** 18 files
|
||||
- **Feature Components:** ~90+ files across 10 features
|
||||
- **Activity Tabs:** 5 files
|
||||
- **Settings Sections:** 11 files
|
||||
- **Settings UI:** 6 files
|
||||
- **Vibe Components:** ~10 files (including scenes/tabs)
|
||||
|
||||
**Lines of Code (approximate):**
|
||||
- Layout: ~2,000 lines
|
||||
- Player: ~1,600 lines
|
||||
- UI: ~500 lines
|
||||
- Features: ~8,000+ lines
|
||||
- **Total:** ~12,000+ lines of component code
|
||||
|
||||
**Most Complex Components:**
|
||||
1. `MiniPlayer` (689 lines) - Gesture handling, multiple modes
|
||||
2. `Sidebar` (512 lines) - Playlist management, responsive
|
||||
3. `FullPlayer` (584 lines) - Full playback controls
|
||||
4. `OverlayPlayer` (467 lines) - Full-screen mobile player
|
||||
5. `TopBar` (399 lines) - Search, sync, notifications
|
||||
6. `TVLayout` (328 lines) - Keyboard navigation
|
||||
7. `ActivityPanel` (301 lines) - Multi-tab panel
|
||||
8. `SeekSlider` (273 lines) - Touch/mouse seeking
|
||||
|
||||
**Components with Direct API Calls:**
|
||||
- Sidebar, TopBar, MobileSidebar (sync, playlists)
|
||||
- All feature action bar components
|
||||
- Settings sections (save/load settings)
|
||||
|
||||
**Components Using Contexts:**
|
||||
- All player components (audio contexts)
|
||||
- Layout components (auth, toast, activity panel)
|
||||
- Feature components (audio, features, query)
|
||||
|
||||
---
|
||||
|
||||
*End of Component Catalog*
|
||||
@@ -0,0 +1,999 @@
|
||||
# Feature Components Catalog for SvelteKit Migration
|
||||
|
||||
**Generated:** 2026-03-20
|
||||
**Status:** Comprehensive inventory of all feature-specific components
|
||||
|
||||
---
|
||||
|
||||
## Album Features (`/features/album/components/`)
|
||||
|
||||
### TrackList
|
||||
**Path:** `/features/album/components/TrackList.tsx` (297 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface TrackListProps {
|
||||
tracks: Track[];
|
||||
album: Album;
|
||||
source: AlbumSource; // "library" | "discover" | "browse"
|
||||
currentTrackId: string | undefined;
|
||||
colors: ColorPalette | null;
|
||||
onPlayTrack: (track: Track, index: number) => void;
|
||||
onAddToQueue: (track: Track) => void;
|
||||
onAddToPlaylist: (trackId: string) => void;
|
||||
previewTrack: string | null;
|
||||
previewPlaying: boolean;
|
||||
onPreview: (track: Track, e: React.MouseEvent) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (pure presentational)
|
||||
|
||||
**Consumed Hooks/Contexts:** None (all logic passed via props)
|
||||
|
||||
**Child Components:** `TrackRow` (memoized with custom comparison)
|
||||
|
||||
**Event Handlers:**
|
||||
- `handlePlayTrack`: Play local track
|
||||
- `handleAddToQueue`: Add to playback queue
|
||||
- `handleAddToPlaylist`: Open playlist selector
|
||||
- `handlePreview`: Play preview for unowned tracks
|
||||
- `handleRowClick`: Conditional play/preview based on ownership
|
||||
|
||||
**Features:**
|
||||
- Dual-mode playback (local file vs preview)
|
||||
- Missing track badges
|
||||
- Preview-only badges for unowned albums
|
||||
- Play count display (library only)
|
||||
- Double-tap support via `useDoubleTap` hook
|
||||
- TV navigation support (`data-tv-card`, `data-tv-card-index`)
|
||||
- Custom memo comparison for performance
|
||||
|
||||
**Patterns:**
|
||||
- `memo` with custom comparison function
|
||||
- `useCallback` for all event handlers
|
||||
- Conditional rendering based on `isOwned` prop
|
||||
- Accessibility via `aria-label` and keyboard support
|
||||
|
||||
---
|
||||
|
||||
### AlbumActionBar
|
||||
**Path:** `/features/album/components/AlbumActionBar.tsx` (103 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AlbumActionBarProps {
|
||||
album: Album;
|
||||
source: AlbumSource;
|
||||
colors: ColorPalette | null;
|
||||
onPlayAll: () => void;
|
||||
onShuffle: () => void;
|
||||
onDownloadAlbum: () => void;
|
||||
onAddToPlaylist: () => void;
|
||||
isPendingDownload: boolean;
|
||||
isPlaying?: boolean;
|
||||
isPlayingThisAlbum?: boolean;
|
||||
onPause?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (derived)
|
||||
|
||||
**Consumed Hooks/Contexts:** None
|
||||
|
||||
**Event Handlers:**
|
||||
- `handlePlayPauseClick`: Conditional play/pause based on state
|
||||
|
||||
**Features:**
|
||||
- Play all button (owned albums only)
|
||||
- Shuffle button (owned albums only)
|
||||
- Add to playlist button (owned albums only)
|
||||
- Download button (unowned albums with MBID)
|
||||
- Pause button when playing this album
|
||||
- Cooldown logic for download (via parent)
|
||||
|
||||
**Patterns:**
|
||||
- Conditional rendering based on `isOwned` and `showDownload`
|
||||
- Disabled state for pending downloads
|
||||
- Icon-only buttons for secondary actions
|
||||
|
||||
---
|
||||
|
||||
### SimilarAlbums
|
||||
**Path:** `/features/album/components/SimilarAlbums.tsx` (39 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SimilarAlbumsProps {
|
||||
similarAlbums: SimilarAlbum[];
|
||||
colors: ColorPalette | null;
|
||||
onNavigate: (albumId: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Consumed Hooks/Contexts:** None
|
||||
|
||||
**Child Components:** `PlayableCard`, `SectionHeader`
|
||||
|
||||
**Features:**
|
||||
- Grid layout (2-5 columns responsive)
|
||||
- Navigation to album detail pages
|
||||
- Owned badge for library albums
|
||||
- TV navigation support
|
||||
|
||||
**Patterns:**
|
||||
- Simple map/render pattern
|
||||
- Reuses `SectionHeader` component
|
||||
|
||||
---
|
||||
|
||||
## Artist Features (`/features/artist/components/`)
|
||||
|
||||
### ArtistHero
|
||||
**Path:** `/features/artist/components/ArtistHero.tsx` (177 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ArtistHeroProps {
|
||||
artist: Artist;
|
||||
source: ArtistSource;
|
||||
albums: Album[];
|
||||
heroImage: string | null;
|
||||
backgroundImage?: string | null;
|
||||
colors: ColorPalette | null;
|
||||
onReload: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (derived)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useArtistDisplayData`: Custom hook for metadata display with user overrides
|
||||
|
||||
**Child Components:**
|
||||
- `MetadataEditor` (lazy-loaded via `Suspense`, library albums only)
|
||||
|
||||
**Features:**
|
||||
- Dynamic background from hero image or gradient
|
||||
- VibrantJS color extraction for overlays
|
||||
- Circular artist image
|
||||
- Edit badge for user-overridden metadata
|
||||
- Listener count and album stats
|
||||
- Conditional MetadataEditor modal (library only)
|
||||
|
||||
**Patterns:**
|
||||
- Lazy loading with `Suspense`
|
||||
- Conditional rendering based on source
|
||||
- Background gradient from color palette
|
||||
|
||||
---
|
||||
|
||||
### ArtistActionBar
|
||||
**Path:** `/features/artist/components/ArtistActionBar.tsx` (106 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ArtistActionBarProps {
|
||||
artist: Artist;
|
||||
albums: Album[];
|
||||
source: ArtistSource;
|
||||
colors: ColorPalette | null;
|
||||
onPlayAll: () => void;
|
||||
onShuffle: () => void;
|
||||
onDownloadAll: () => void;
|
||||
onStartRadio?: () => void;
|
||||
isPendingDownload: boolean;
|
||||
isPlaying?: boolean;
|
||||
isPlayingThisArtist?: boolean;
|
||||
onPause?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (derived)
|
||||
|
||||
**Event Handlers:**
|
||||
- `handlePlayPauseClick`: Conditional play/pause
|
||||
|
||||
**Features:**
|
||||
- Play all button
|
||||
- Shuffle button
|
||||
- Radio button (library only)
|
||||
- Download all button (discovery/unowned)
|
||||
- Pause button when playing
|
||||
|
||||
**Patterns:**
|
||||
- Similar to `AlbumActionBar`
|
||||
- Conditional radio button for library artists
|
||||
|
||||
---
|
||||
|
||||
### PopularTracks
|
||||
**Path:** `/features/artist/components/PopularTracks.tsx` (202 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface PopularTracksProps {
|
||||
tracks: Track[];
|
||||
artist: Artist;
|
||||
currentTrackId: string | undefined;
|
||||
colors: ColorPalette | null;
|
||||
onPlayTrack: (track: Track) => void;
|
||||
previewTrack: string | null;
|
||||
previewPlaying: boolean;
|
||||
onPreview: (track: Track, e: React.MouseEvent) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useRef`: `lastTapRef` for double-tap detection
|
||||
|
||||
**Consumed Hooks/Contexts:** None
|
||||
|
||||
**Features:**
|
||||
- Top 10 tracks display
|
||||
- Play count badges
|
||||
- Preview mode for unowned tracks
|
||||
- Double-tap support (touch and keyboard)
|
||||
- Album art thumbnails
|
||||
- TV navigation support
|
||||
|
||||
**Patterns:**
|
||||
- Manual double-tap detection with `useRef`
|
||||
- Inline event handlers with closure capture
|
||||
- Conditional preview vs full playback
|
||||
|
||||
---
|
||||
|
||||
### AvailableAlbums
|
||||
**Path:** `/features/artist/components/AvailableAlbums.tsx` (185 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AvailableAlbumsProps {
|
||||
albums: Album[];
|
||||
artistName: string;
|
||||
source: ArtistSource;
|
||||
colors: ColorPalette | null;
|
||||
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
|
||||
isPendingDownload: (mbid: string) => boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `coverArt` (lazy-loaded)
|
||||
- `useState`: `fetchAttempted`
|
||||
- `useEffect`: Lazy cover art fetch with staggered timing
|
||||
|
||||
**Child Components:** `LazyAlbumCard`, `AlbumGrid`, `SectionHeader`
|
||||
|
||||
**Features:**
|
||||
- Lazy cover art loading for discovery albums
|
||||
- Staggered fetch to avoid thundering herd
|
||||
- Separation of studio albums vs EPs/singles
|
||||
- Download tracking by MBID
|
||||
- Year and type in subtitle
|
||||
|
||||
**Patterns:**
|
||||
- Sub-component pattern (`LazyAlbumCard`, `AlbumGrid`)
|
||||
- Effect-based lazy loading
|
||||
- Staggered timing via `setTimeout` with index
|
||||
|
||||
---
|
||||
|
||||
### Discography
|
||||
**Path:** `/features/artist/components/Discography.tsx` (84 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface DiscographyProps {
|
||||
albums: Album[];
|
||||
colors: ColorPalette | null;
|
||||
onPlayAlbum: (albumId: string, albumTitle: string) => Promise<void>;
|
||||
sortBy: "year" | "dateAdded";
|
||||
onSortChange: (sortBy: "year" | "dateAdded") => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (controlled component)
|
||||
|
||||
**Features:**
|
||||
- Sort dropdown (year/date added)
|
||||
- Play album action
|
||||
- Track count in subtitle
|
||||
- TV navigation support
|
||||
|
||||
**Patterns:**
|
||||
- Controlled sort state via props
|
||||
- Reuses `SectionHeader` with custom `rightAction`
|
||||
|
||||
---
|
||||
|
||||
### ArtistBio
|
||||
**Path:** `/features/artist/components/ArtistBio.tsx` (23 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ArtistBioProps {
|
||||
bio: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- HTML sanitization with `DOMPurify`
|
||||
- Styled prose with Tailwind `prose` plugin
|
||||
- Link styling overrides
|
||||
|
||||
**Patterns:**
|
||||
- Minimal component, pure presentation
|
||||
- `dangerouslySetInnerHTML` with sanitization
|
||||
|
||||
---
|
||||
|
||||
### SimilarArtists
|
||||
**Path:** `/features/artist/components/SimilarArtists.tsx` (112 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SimilarArtistsProps {
|
||||
similarArtists: SimilarArtist[];
|
||||
onNavigate: (artistId: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- Circular artist cards
|
||||
- Library indicator badge
|
||||
- Match percentage display
|
||||
- Owned album count
|
||||
- Navigation to artist pages
|
||||
- TV navigation support
|
||||
|
||||
**Patterns:**
|
||||
- Manual keyboard handling (`onKeyDown`)
|
||||
- Conditional library vs discovery navigation
|
||||
- Image fallback with icon
|
||||
|
||||
---
|
||||
|
||||
## Home Features (`/features/home/components/`)
|
||||
|
||||
### SectionHeader
|
||||
**Path:** `/features/home/components/SectionHeader.tsx` (56 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
showAllHref?: string;
|
||||
rightAction?: React.ReactNode;
|
||||
badge?: string;
|
||||
color?: "featured" | "tracks" | "albums" | "podcasts" | "audiobooks" | "artists" | "discover";
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- Color-coded gradient accent
|
||||
- Optional "Show all" link
|
||||
- Custom right action slot
|
||||
- Badge support (e.g., "AI-generated")
|
||||
|
||||
**Patterns:**
|
||||
- Reusable across all features
|
||||
- Gradient color mapping via constant object
|
||||
- `memo` wrapper
|
||||
|
||||
---
|
||||
|
||||
### PopularArtistsGrid
|
||||
**Path:** `/features/home/components/PopularArtistsGrid.tsx` (81 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface PopularArtistsGridProps {
|
||||
artists: PopularArtist[];
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Child Components:** `PopularArtistCard` (memoized), `HorizontalCarousel`
|
||||
|
||||
**Features:**
|
||||
- Horizontal carousel layout
|
||||
- Listener count display
|
||||
- Search navigation on click
|
||||
- Hover animations
|
||||
- TV navigation support
|
||||
|
||||
**Patterns:**
|
||||
- Sub-component pattern with memoization
|
||||
- Reuses `HorizontalCarousel` component
|
||||
|
||||
---
|
||||
|
||||
### FeaturedPlaylistsGrid
|
||||
**Path:** `/features/home/components/FeaturedPlaylistsGrid.tsx` (125 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface FeaturedPlaylistsGridProps {
|
||||
playlists: PlaylistPreview[];
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Child Components:** `PlaylistCard` (memoized), `HorizontalCarousel`, `FeaturedPlaylistsSkeleton`
|
||||
|
||||
**Features:**
|
||||
- Deezer playlist integration
|
||||
- Custom Deezer icon
|
||||
- Play button on hover
|
||||
- Navigation to playlist pages
|
||||
- Skeleton loading state
|
||||
- Limit to 20 playlists
|
||||
|
||||
**Patterns:**
|
||||
- Skeleton component for loading state
|
||||
- Memoized card component
|
||||
- `useCallback` for handler memoization
|
||||
|
||||
---
|
||||
|
||||
### ContinueListening
|
||||
**Path:** `/features/home/components/ContinueListening.tsx` (155 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ContinueListeningProps {
|
||||
items: ListenedItem[];
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Child Components:** `ContinueListeningCard` (memoized), `HorizontalCarousel`
|
||||
|
||||
**Features:**
|
||||
- Multi-type support (artist, podcast, audiobook)
|
||||
- Progress bar for podcasts/audiobooks
|
||||
- Type-specific icons and colors
|
||||
- Navigation to respective detail pages
|
||||
- Listener count in subtitle
|
||||
|
||||
**Patterns:**
|
||||
- Type-based conditional rendering
|
||||
- Color mapping by content type
|
||||
- Reusable card pattern
|
||||
|
||||
---
|
||||
|
||||
### MixesGrid
|
||||
**Path:** `/features/home/components/MixesGrid.tsx` (24 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface MixesGridProps {
|
||||
mixes: Mix[];
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Child Components:** `MixCard`, `HorizontalCarousel`
|
||||
|
||||
**Features:**
|
||||
- AI-generated mix display
|
||||
- Reuses `MixCard` component
|
||||
|
||||
**Patterns:**
|
||||
- Minimal wrapper component
|
||||
- Delegates to specialized `MixCard`
|
||||
|
||||
---
|
||||
|
||||
## Library Features (`/features/library/components/`)
|
||||
|
||||
### LibraryHeader
|
||||
**Path:** `/features/library/components/LibraryHeader.tsx` (40 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface LibraryHeaderProps {
|
||||
totalItems: number;
|
||||
activeTab: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- System status indicator
|
||||
- Large title display
|
||||
- Live item counter
|
||||
- Tab-specific label
|
||||
|
||||
**Patterns:**
|
||||
- Pure presentation component
|
||||
- No Next.js-specific APIs
|
||||
|
||||
---
|
||||
|
||||
### LibraryToolbar
|
||||
**Path:** `/features/library/components/LibraryToolbar.tsx` (93 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface LibraryToolbarProps {
|
||||
activeTab: Tab;
|
||||
filter: LibraryFilter;
|
||||
sortBy: SortOption;
|
||||
itemsPerPage: number;
|
||||
onFilterChange: (filter: LibraryFilter) => void;
|
||||
onSortChange: (sort: SortOption) => void;
|
||||
onItemsPerPageChange: (items: number) => void;
|
||||
onShuffleLibrary: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (controlled)
|
||||
|
||||
**Features:**
|
||||
- Filter pills (Owned/Discovery/All)
|
||||
- Sort dropdown (tab-specific options)
|
||||
- Items per page selector
|
||||
- Shuffle library button
|
||||
- Conditional filter display (artists/albums only)
|
||||
|
||||
**Patterns:**
|
||||
- Controlled component pattern
|
||||
- Filter pill constant array
|
||||
- Tab-specific sort options
|
||||
|
||||
---
|
||||
|
||||
### TracksList
|
||||
**Path:** `/features/library/components/TracksList.tsx` (255 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface TracksListProps {
|
||||
tracks: Track[];
|
||||
onPlay: (tracks: Track[], startIndex?: number) => void;
|
||||
onAddToQueue: (track: Track) => void;
|
||||
onAddToPlaylist: (playlistId: string, trackId: string) => void;
|
||||
onDelete: (trackId: string, trackTitle: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `showPlaylistSelector`
|
||||
- `useState`: `selectedTrackId`
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useAudioState`: `currentTrack`
|
||||
|
||||
**Child Components:** `TrackRow` (memoized), `PlaylistSelector`
|
||||
|
||||
**Event Handlers:**
|
||||
- `handleShowAddToPlaylist`: Open playlist selector
|
||||
- `handleAddToPlaylist`: Add track to playlist
|
||||
|
||||
**Features:**
|
||||
- Terminal-style header row
|
||||
- Album column (desktop only)
|
||||
- Action buttons on hover
|
||||
- Double-tap support
|
||||
- Loading state with spinner
|
||||
- Empty state fallback
|
||||
- Playlist selector modal
|
||||
|
||||
**Patterns:**
|
||||
- Memoized row with custom comparison
|
||||
- `useCallback` for handlers
|
||||
- Conditional modal rendering
|
||||
- `useDoubleTap` hook integration
|
||||
|
||||
---
|
||||
|
||||
## Search Features (`/features/search/components/`)
|
||||
|
||||
### TopResult
|
||||
**Path:** `/features/search/components/TopResult.tsx` (97 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface TopResultProps {
|
||||
libraryArtist?: Artist;
|
||||
discoveryArtist?: DiscoverResult;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- Large artist card display
|
||||
- Library vs discovery detection
|
||||
- Background image with overlay
|
||||
- Gradient border on hover
|
||||
- External link icon
|
||||
|
||||
**Patterns:**
|
||||
- Conditional rendering based on source
|
||||
- Large hero-style card
|
||||
- Hover animations
|
||||
|
||||
---
|
||||
|
||||
### SoulseekBrowser
|
||||
**Path:** `/features/search/components/SoulseekBrowser.tsx` (532 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface SoulseekBrowserProps {
|
||||
results: SoulseekResult[];
|
||||
isSearching: boolean;
|
||||
isPolling: boolean;
|
||||
isComplete: boolean;
|
||||
uniqueUserCount: number;
|
||||
downloadingFiles: Set<string>;
|
||||
onDownload: (result: SoulseekResult) => void;
|
||||
onBulkDownload: (results: SoulseekResult[]) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `formatFilters` (Set)
|
||||
- `useState`: `sortField`
|
||||
- `useState`: `viewMode` (flat/grouped)
|
||||
- `useState`: `selectedKeys` (Set)
|
||||
- `useState`: `displayLimit` (infinite scroll)
|
||||
- `useState`: `expandedGroups` (Set)
|
||||
- `useState`: `groupsInitialized`
|
||||
- `useRef`: `sentinelRef` (IntersectionObserver)
|
||||
- `useMemo`: `filtered`, `sorted`, `grouped`, `selectedResults`
|
||||
- `useEffect`: IntersectionObserver for infinite scroll
|
||||
- `useCallback`: All event handlers
|
||||
|
||||
**Child Components:** `FlatView`, `GroupedView`, `ResultRow`
|
||||
|
||||
**Features:**
|
||||
- Format filter pills (FLAC/320+/256+)
|
||||
- Sort by quality/bitrate/size/filename
|
||||
- Flat or grouped view (by username)
|
||||
- Infinite scroll with IntersectionObserver
|
||||
- Bulk selection and download
|
||||
- Real-time search status
|
||||
- File metadata parsing
|
||||
- Quality badges
|
||||
- Download progress tracking
|
||||
|
||||
**Patterns:**
|
||||
- Complex state management with Sets
|
||||
- Memoized filtered/sorted results
|
||||
- Sub-component pattern (FlatView/GroupedView)
|
||||
- Infinite scroll with sentinel
|
||||
- Group expansion state
|
||||
- Helper functions for filename parsing
|
||||
|
||||
**Third-party:**
|
||||
- Custom helper functions from `soulseekHelpers.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Podcast Features (`/features/podcast/components/`)
|
||||
|
||||
### PodcastHero
|
||||
**Path:** `/features/podcast/components/PodcastHero.tsx` (176 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface PodcastHeroProps {
|
||||
title: string;
|
||||
author: string;
|
||||
description?: string;
|
||||
genres?: string[];
|
||||
heroImage: string | null;
|
||||
colors: ColorPalette | null;
|
||||
episodeCount: number;
|
||||
inProgressCount: number;
|
||||
children?: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useRouter`: Navigation
|
||||
|
||||
**Features:**
|
||||
- Back navigation button
|
||||
- System status indicator
|
||||
- Cover art display
|
||||
- Episode count and progress
|
||||
- Genre tags
|
||||
- Description truncation (HTML stripped)
|
||||
|
||||
**Patterns:**
|
||||
- Similar to `AlbumHero`/`ArtistHero`
|
||||
- HTML sanitization for description
|
||||
- Conditional genre display
|
||||
|
||||
---
|
||||
|
||||
### PodcastActionBar
|
||||
**Path:** `/features/podcast/components/PodcastActionBar.tsx` (135 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface PodcastActionBarProps {
|
||||
isSubscribed: boolean;
|
||||
feedUrl?: string;
|
||||
colors: ColorPalette | null;
|
||||
isSubscribing: boolean;
|
||||
showDeleteConfirm: boolean;
|
||||
onSubscribe: () => void;
|
||||
onRemove: () => void;
|
||||
onShowDeleteConfirm: (show: boolean) => void;
|
||||
onPlayLatest?: () => void;
|
||||
isPlayingPodcast?: boolean;
|
||||
onPause?: () => void;
|
||||
onRefresh?: () => Promise<unknown>;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (controlled)
|
||||
|
||||
**Features:**
|
||||
- Subscribe/Unsubscribe toggle
|
||||
- Play latest episode
|
||||
- Refresh for new episodes
|
||||
- RSS feed link
|
||||
- Delete confirmation flow
|
||||
- Loading states
|
||||
|
||||
**Patterns:**
|
||||
- Confirmation dialog inline
|
||||
- Spinner for async operations
|
||||
- Conditional action buttons
|
||||
|
||||
---
|
||||
|
||||
### EpisodeList
|
||||
**Path:** `/features/podcast/components/EpisodeList.tsx` (280 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface EpisodeListProps {
|
||||
podcast: Podcast;
|
||||
episodes: Episode[];
|
||||
sortOrder: "newest" | "oldest";
|
||||
onSortOrderChange: (order: "newest" | "oldest") => void;
|
||||
isEpisodePlaying: (episodeId: string) => boolean;
|
||||
isPlaying: boolean;
|
||||
onPlayPause: (episode: Episode) => void;
|
||||
onPlay: (episode: Episode) => void;
|
||||
onMarkComplete?: (episodeId: string, duration: number) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:**
|
||||
- `useState`: `expanded` (per episode description)
|
||||
- `useRef`: `lastTapRef` (double-tap)
|
||||
|
||||
**Consumed Hooks/Contexts:** None
|
||||
|
||||
**Child Components:** `EpisodeRow`
|
||||
|
||||
**Features:**
|
||||
- Sort toggle (newest/oldest)
|
||||
- Progress bar per episode
|
||||
- Season/episode number display
|
||||
- Description expand/collapse
|
||||
- HTML sanitization
|
||||
- Double-tap support
|
||||
- Mark as complete button
|
||||
- Finished badge
|
||||
|
||||
**Patterns:**
|
||||
- Sub-component pattern
|
||||
- Inline description expansion
|
||||
- Progress tracking
|
||||
- Conditional complete button
|
||||
|
||||
---
|
||||
|
||||
## Audiobook Features (`/features/audiobook/components/`)
|
||||
|
||||
### AudiobookHero
|
||||
**Path:** `/features/audiobook/components/AudiobookHero.tsx` (183 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AudiobookHeroProps {
|
||||
audiobook: Audiobook;
|
||||
heroImage: string | null;
|
||||
colors: ColorPalette | null;
|
||||
metadata: {
|
||||
narrator: string | null;
|
||||
genre: string | null;
|
||||
publishedYear: string | null;
|
||||
description: string | null;
|
||||
} | null;
|
||||
formatTime: (seconds: number) => string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None (derived)
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `useRouter`: Navigation
|
||||
|
||||
**Features:**
|
||||
- Back navigation
|
||||
- Cover art display
|
||||
- Narrator and genre metadata
|
||||
- Progress percentage
|
||||
- Series information
|
||||
- Duration formatting
|
||||
- Description truncation
|
||||
|
||||
**Patterns:**
|
||||
- Similar to `PodcastHero`/`AlbumHero`
|
||||
- Type-specific color scheme (amber/orange)
|
||||
|
||||
---
|
||||
|
||||
### ChapterList
|
||||
**Path:** `/features/audiobook/components/ChapterList.tsx` (54 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ChapterListProps {
|
||||
chapters: AudiobookChapter[];
|
||||
onSeekToChapter: (startTime: number) => void;
|
||||
formatTime: (seconds: number) => string;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- Chapter list display
|
||||
- Click to seek
|
||||
- Start time display
|
||||
- Hidden if > 50 chapters
|
||||
|
||||
**Patterns:**
|
||||
- Minimal component
|
||||
- Reuses `SectionHeader` pattern
|
||||
|
||||
---
|
||||
|
||||
## Discover Features (`/features/discover/components/`)
|
||||
|
||||
### DiscoverHero
|
||||
**Path:** `/features/discover/components/DiscoverHero.tsx` (84 lines)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface DiscoverHeroProps {
|
||||
playlist: DiscoverPlaylist | null;
|
||||
config: DiscoverConfig | null;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Consumed Hooks/Contexts:**
|
||||
- `date-fns`: Date formatting
|
||||
|
||||
**Features:**
|
||||
- Settings button (top right)
|
||||
- Week start date display
|
||||
- Track count
|
||||
- Total duration calculation
|
||||
- Last generated timestamp
|
||||
|
||||
**Patterns:**
|
||||
- Absolute positioning for settings button
|
||||
- Duration formatting helper
|
||||
- Date formatting with `date-fns`
|
||||
|
||||
---
|
||||
|
||||
### HowItWorks
|
||||
**Path:** `/features/discover/components/HowItWorks.tsx` (56 lines)
|
||||
|
||||
**Props:** None
|
||||
|
||||
**Internal State:** None
|
||||
|
||||
**Features:**
|
||||
- Feature explanation modal content
|
||||
- Step-by-step guide
|
||||
- Chevron icons for steps
|
||||
|
||||
**Patterns:**
|
||||
- Pure presentation component
|
||||
- Reuses `Card` component
|
||||
|
||||
---
|
||||
|
||||
## Key Observations
|
||||
|
||||
### Common Patterns Across Features
|
||||
|
||||
1. **Hero Components**: All features use similar hero layout:
|
||||
- Background image/gradient
|
||||
- System status indicator
|
||||
- Title and metadata
|
||||
- Action bar as children
|
||||
- Consistent spacing and typography
|
||||
|
||||
2. **ActionBar Components**: Standard action patterns:
|
||||
- Play/pause toggle
|
||||
- Secondary actions (shuffle, download, etc.)
|
||||
- Loading states
|
||||
- Conditional rendering based on ownership
|
||||
|
||||
3. **List Components**: Track/episode/chapter lists:
|
||||
- Memoized row components
|
||||
- Double-tap support
|
||||
- Progress indicators
|
||||
- Action buttons on hover
|
||||
- TV navigation support
|
||||
|
||||
4. **Grid Components**: Album/artist/podcast grids:
|
||||
- Responsive column counts (2-5)
|
||||
- Horizontal carousel for home
|
||||
- PlayableCard reuse
|
||||
- Badge system (owned, download, etc.)
|
||||
|
||||
5. **State Management**:
|
||||
- Controlled components (state in parent)
|
||||
- Local state for UI concerns (modals, expansion)
|
||||
- Refs for gesture handling (double-tap)
|
||||
- No component-level data fetching (via React Query in pages)
|
||||
|
||||
### Next.js APIs Used in Features
|
||||
|
||||
- `useRouter` / `usePathname`: Navigation
|
||||
- `Link`: Client-side navigation
|
||||
- `next/image`: Optimized images
|
||||
- File-based routing for detail pages
|
||||
|
||||
### Third-Party Dependencies in Features
|
||||
|
||||
- `date-fns`: Date formatting
|
||||
- `DOMPurify`: HTML sanitization
|
||||
- `lucide-react`: Icons
|
||||
- `clsx` / `tailwind-merge`: className merging
|
||||
- VibrantJS (via hooks): Color extraction
|
||||
|
||||
### SvelteKit Migration Considerations
|
||||
|
||||
1. **Navigation**: Replace `useRouter` with `goto()` from `$app/navigation`
|
||||
2. **Images**: Replace `next/image` with custom image component or SvelteKit's image optimization
|
||||
3. **State**: Convert React hooks to Svelte stores or reactive declarations
|
||||
4. **Events**: Replace `onClick` with Svelte's event handlers
|
||||
5. **Conditional Rendering**: Replace `{condition && <Component />}` with `{#if condition}...{/if}`
|
||||
6. **Lists**: Replace `.map()` with `{#each}` blocks
|
||||
7. **Refs**: Replace `useRef` with Svelte bindings or `bind:this`
|
||||
8. **Effects**: Replace `useEffect` with Svelte's reactive statements or `onMount`
|
||||
9. **Memoization**: Replace `memo`/`useMemo` with Svelte's `$derived` or stores
|
||||
|
||||
---
|
||||
|
||||
*End of Feature Components Catalog*
|
||||
@@ -0,0 +1,24 @@
|
||||
# Task: Frontend Catalog for SvelteKit Migration
|
||||
|
||||
**Started:** 2026-03-20
|
||||
**Goal:** Complete inventory of all pages, components, hooks, contexts, stores, API calls, and dependencies for migration to SvelteKit + Svelte 5
|
||||
**Spec:** None (exploratory cataloging)
|
||||
|
||||
## Steps
|
||||
- [x] Phase 1: Directory tree analysis (app/ and lib/)
|
||||
- [ ] Phase 2: Pages catalog (routes, data fetching, params)
|
||||
- [ ] Phase 3: Components catalog (props, state, effects, children)
|
||||
- [ ] Phase 4: React Query hooks catalog (query keys, endpoints, cache config)
|
||||
- [ ] Phase 5: Context providers catalog (state, consumers)
|
||||
- [ ] Phase 6: Stores catalog (Zustand/other shape and actions)
|
||||
- [ ] Phase 7: WebSocket/SSE connections catalog
|
||||
- [ ] Phase 8: Third-party libraries catalog
|
||||
|
||||
## Agent Log
|
||||
- 12:05 orchestrator: Task started, reviewed existing catalog files
|
||||
|
||||
## Notes
|
||||
<!-- Key findings go here -->
|
||||
|
||||
## Blockers
|
||||
<!-- None -->
|
||||
@@ -50,7 +50,7 @@ export default function AlbumPage({ params }: AlbumPageProps) {
|
||||
const { previewTrack, previewPlaying, handlePreview } = useTrackPreview();
|
||||
|
||||
const combinedTracks = useMemo<AlbumTrack[]>(() => {
|
||||
const ownedTracks = (album?.tracks || []).map((track) => ({
|
||||
const ownedTracks = (album?.tracks || []).map((track: AlbumTrack) => ({
|
||||
...track,
|
||||
trackNumber: track.trackNumber ?? track.trackNo,
|
||||
isMissing: false,
|
||||
@@ -81,14 +81,14 @@ export default function AlbumPage({ params }: AlbumPageProps) {
|
||||
});
|
||||
|
||||
const numberedMissing = missingTracks
|
||||
.filter((track) => typeof track.trackNumber === "number")
|
||||
.filter((track: AlbumTrack) => typeof track.trackNumber === "number")
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a: AlbumTrack, b: AlbumTrack) =>
|
||||
(a.trackNumber as number) - (b.trackNumber as number)
|
||||
);
|
||||
|
||||
const unnumberedMissing = missingTracks.filter(
|
||||
(track) => typeof track.trackNumber !== "number"
|
||||
(track: AlbumTrack) => typeof track.trackNumber !== "number"
|
||||
);
|
||||
|
||||
const merged: AlbumTrack[] = [];
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function ArtistPage() {
|
||||
}));
|
||||
|
||||
const startIndex = formattedTracks.findIndex(
|
||||
(t) => t.id === track.id,
|
||||
(t: { id: string }) => t.id === track.id,
|
||||
);
|
||||
playTracks(formattedTracks, Math.max(0, startIndex));
|
||||
}
|
||||
|
||||
@@ -585,7 +585,7 @@ export default function PodcastsPage() {
|
||||
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"
|
||||
data-tv-section={`genre-${genreId}`}
|
||||
>
|
||||
{genrePodcasts.map((podcast, index) => (
|
||||
{genrePodcasts.map((podcast: { id: string; title: string; author: string; coverUrl?: string; episodeCount?: number }, index: number) => (
|
||||
<PodcastCard
|
||||
key={podcast.id}
|
||||
podcast={podcast}
|
||||
|
||||
@@ -386,8 +386,8 @@ export function MetadataEditor({
|
||||
<div className="mt-2">
|
||||
<Image
|
||||
src={
|
||||
formData.heroUrl ||
|
||||
formData.coverUrl
|
||||
(formData.heroUrl ||
|
||||
formData.coverUrl)!
|
||||
}
|
||||
alt="Preview"
|
||||
width={128}
|
||||
|
||||
@@ -122,10 +122,10 @@ export function AlbumHero({
|
||||
id={album.id}
|
||||
currentData={{
|
||||
title: displayData.title,
|
||||
year: displayData.year,
|
||||
year: displayData.year ?? undefined,
|
||||
genres: displayData.genres,
|
||||
mbid: album.mbid,
|
||||
coverUrl: displayData.coverUrl,
|
||||
coverUrl: displayData.coverUrl ?? undefined,
|
||||
// Pass originals for reset comparison
|
||||
_originalTitle: album.title,
|
||||
_originalYear: album.year,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ColorPalette } from "@/hooks/useImageColor";
|
||||
interface ArtistActionBarProps {
|
||||
artist: Artist;
|
||||
albums: Album[];
|
||||
source: ArtistSource;
|
||||
source: ArtistSource | null;
|
||||
colors: ColorPalette | null;
|
||||
onPlayAll: () => void;
|
||||
onShuffle: () => void;
|
||||
|
||||
@@ -12,7 +12,7 @@ const MetadataEditor = lazy(() => import("@/components/MetadataEditor").then(mod
|
||||
|
||||
interface ArtistHeroProps {
|
||||
artist: Artist;
|
||||
source: ArtistSource;
|
||||
source: ArtistSource | null;
|
||||
albums: Album[];
|
||||
heroImage: string | null;
|
||||
backgroundImage?: string | null;
|
||||
@@ -118,10 +118,10 @@ export function ArtistHero({
|
||||
id={artist.id}
|
||||
currentData={{
|
||||
name: displayData.name,
|
||||
bio: displayData.summary,
|
||||
bio: displayData.summary ?? undefined,
|
||||
genres: displayData.genres,
|
||||
mbid: artist.mbid,
|
||||
heroUrl: displayData.heroUrl,
|
||||
heroUrl: displayData.heroUrl ?? undefined,
|
||||
// Pass originals for reset comparison
|
||||
_originalName: artist.name,
|
||||
_originalBio:
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SectionHeader } from "@/features/home/components/SectionHeader";
|
||||
interface AvailableAlbumsProps {
|
||||
albums: Album[];
|
||||
artistName: string;
|
||||
source: ArtistSource;
|
||||
source: ArtistSource | null;
|
||||
colors: ColorPalette | null;
|
||||
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
|
||||
isPendingDownload: (mbid: string) => boolean;
|
||||
@@ -27,7 +27,7 @@ function LazyAlbumCard({
|
||||
index,
|
||||
}: {
|
||||
album: Album;
|
||||
source: ArtistSource;
|
||||
source: ArtistSource | null;
|
||||
colors: ColorPalette | null;
|
||||
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
|
||||
isPendingDownload: (mbid: string) => boolean;
|
||||
|
||||
@@ -57,7 +57,7 @@ export function useArtistActions() {
|
||||
}));
|
||||
|
||||
// Sort tracks within album by track number
|
||||
formattedTracks.sort((a, b) => a.trackNumber - b.trackNumber);
|
||||
formattedTracks.sort((a: FormattedTrack, b: FormattedTrack) => a.trackNumber - b.trackNumber);
|
||||
allTracks.push(...formattedTracks);
|
||||
});
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export function UnavailableAlbums({
|
||||
key={album.id}
|
||||
className={cn(
|
||||
"flex items-center gap-4 px-4 py-3 hover:bg-[#1a1a1a] transition-colors group",
|
||||
album.attemptNumber > 0 &&
|
||||
(album.attemptNumber ?? 0) > 0 &&
|
||||
"pl-12 bg-[#1a1a1a]/30"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useDiscoverData() {
|
||||
]);
|
||||
|
||||
setPlaylist(playlistData);
|
||||
setConfig(configData);
|
||||
setConfig(configData as DiscoverConfig | null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load discover data:', error);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function LibraryAlbumsGrid({ albums }: LibraryAlbumsGridProps) {
|
||||
subtitle={album.artist?.name || ""}
|
||||
imageUrl={
|
||||
album.coverUrl || album.albumId
|
||||
? api.getCoverArtUrl(album.coverUrl || album.albumId, 200)
|
||||
? api.getCoverArtUrl((album.coverUrl || album.albumId)!, 200)
|
||||
: null
|
||||
}
|
||||
fallbackIcon={Disc3}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
|
||||
album: {
|
||||
id: t.album.id,
|
||||
title: t.album.title,
|
||||
coverArt: t.album.coverUrl,
|
||||
coverArt: t.album.coverUrl ?? undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export function UnifiedSongsList({
|
||||
album: {
|
||||
id: t.album.id,
|
||||
title: t.album.title,
|
||||
coverArt: t.album.coverUrl,
|
||||
coverArt: t.album.coverUrl ?? undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -1165,7 +1165,7 @@ export function CacheSection({ settings, onUpdate }: CacheSectionProps) {
|
||||
? "Cleaning..."
|
||||
: "Cleanup Stale Jobs"}
|
||||
</button>
|
||||
{(enrichmentProgress?.audioAnalysis?.failed > 0 || enrichmentProgress?.audioAnalysis?.permanentlyFailed > 0) && (
|
||||
{((enrichmentProgress?.audioAnalysis?.failed ?? 0) > 0 || (enrichmentProgress?.audioAnalysis?.permanentlyFailed ?? 0) > 0) && (
|
||||
<button
|
||||
onClick={handleRetryFailedAnalysis}
|
||||
disabled={retryingFailed || isEnrichmentActive}
|
||||
@@ -1173,7 +1173,7 @@ export function CacheSection({ settings, onUpdate }: CacheSectionProps) {
|
||||
>
|
||||
{retryingFailed
|
||||
? "Retrying..."
|
||||
: `Retry Failed Analysis (${(enrichmentProgress.audioAnalysis.failed || 0) + (enrichmentProgress.audioAnalysis.permanentlyFailed || 0)})`}
|
||||
: `Retry Failed Analysis (${(enrichmentProgress?.audioAnalysis?.failed || 0) + (enrichmentProgress?.audioAnalysis?.permanentlyFailed || 0)})`}
|
||||
</button>
|
||||
)}
|
||||
{retryResult && (
|
||||
|
||||
@@ -621,10 +621,11 @@ export function usePodcastQuery(id: string | undefined) {
|
||||
return await api.getPodcast(id);
|
||||
} catch (error) {
|
||||
// If podcast not found (404), return null to allow preview mode
|
||||
const err = error as { status?: number; message?: string };
|
||||
if (
|
||||
error?.status === 404 ||
|
||||
error?.message?.includes("not found") ||
|
||||
error?.message?.includes("not subscribed")
|
||||
err.status === 404 ||
|
||||
err.message?.includes("not found") ||
|
||||
err.message?.includes("not subscribed")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface UseTVNavigationOptions {
|
||||
}
|
||||
|
||||
interface UseTVNavigationResult {
|
||||
containerRef: React.RefObject<HTMLElement>;
|
||||
containerRef: React.RefObject<HTMLElement | null>;
|
||||
focusedSectionIndex: number;
|
||||
focusedCardIndex: number;
|
||||
isContentFocused: boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
|
||||
Reference in New Issue
Block a user