veza/apps/web/src/components/dashboard/TrackList.tsx
senke fa56dfa77e refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
  kodo-void → background, kodo-ink → card, kodo-graphite → muted,
  kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
  kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
  semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 01:51:49 +01:00

192 lines
6.5 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Play,
Heart,
MoreHorizontal,
AlertCircle,
BarChart3,
} from 'lucide-react';
import { Button } from '../ui/button';
import { Track } from '@/types/api';
import { useAudio } from '@/context/AudioContext';
import { useToast } from '@/components/feedback/ToastProvider';
import { trackService } from '@/features/tracks/services/trackService';
import { logger } from '@/utils/logger';
export const TrackList: React.FC = () => {
const { playTrack, currentTrack, isPlaying, togglePlay } = useAudio();
const { addToast } = useToast();
const [tracks, setTracks] = useState<Track[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
const loadTracks = async () => {
try {
setLoading(true);
// Fetch trending/top tracks for the dashboard
const response = await trackService.list({
limit: 5,
sort_by: 'play_count',
});
setTracks(response.tracks);
} catch (err) {
logger.error('Failed to load tracks', {
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
setError(true);
} finally {
setLoading(false);
}
};
loadTracks();
}, []);
const handlePlay = (track: Track) => {
if (currentTrack?.id === track.id) {
togglePlay();
} else {
playTrack(track, tracks);
}
};
const handleLike = async (e: React.MouseEvent, track: Track) => {
e.stopPropagation();
try {
await trackService.like(track.id);
addToast(`Liked ${track.title}`, 'success');
} catch (e) {
addToast('Action failed', 'error');
}
};
if (loading) {
return (
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className="h-16 bg-card/50 animate-pulse rounded-xl border border-border/30"
></div>
))}
</div>
);
}
if (error) {
return (
<div className="p-6 text-center border border-destructive/30 bg-destructive/10 rounded-xl text-destructive">
<AlertCircle className="w-6 h-6 mx-auto mb-2" />
<p className="text-sm">Unable to load trending audio.</p>
<Button
variant="ghost"
size="sm"
className="mt-2"
onClick={() => window.location.reload()}
>
Retry
</Button>
</div>
);
}
if (tracks.length === 0) {
return (
<div className="text-muted-foreground text-center py-12 bg-card/30 rounded-xl border border-dashed border-border">
<BarChart3 className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No tracks trending right now.</p>
</div>
);
}
return (
<div className="space-y-2">
{tracks.map((track, i) => {
const isCurrent = currentTrack?.id === track.id;
return (
<div
key={track.id}
className={`
animate-stagger-in group flex items-center gap-4 p-4 rounded-xl transition-all border cursor-pointer relative overflow-hidden
${isCurrent ? 'bg-primary/10 border-primary/30' : 'bg-card border-transparent hover:border-border/50 hover:bg-card/80'}
`}
style={{ animationDelay: `${Math.min(i * 50, 500)}ms` }}
onClick={() => handlePlay(track)}
>
{/* Active Indicator Bar */}
{isCurrent && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary"></div>
)}
<div className="w-8 text-center text-muted-foreground font-mono text-xs font-bold pl-2">
{isCurrent && isPlaying ? (
<div className="flex gap-0.5 justify-center items-end h-3">
<div className="w-0.5 bg-primary h-full animate-[bounce_1s_infinite]"></div>
<div className="w-0.5 bg-primary h-2/3 animate-[bounce_1.2s_infinite]"></div>
<div className="w-0.5 bg-primary h-full animate-[bounce_0.8s_infinite]"></div>
</div>
) : (
<span className="group-hover:hidden text-muted-foreground">
{i + 1}
</span>
)}
<Play
className={`w-4 h-4 mx-auto fill-current hidden group-hover:block ${isCurrent ? 'text-primary' : 'text-foreground'}`}
/>
</div>
<div className="relative w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 shadow-lg">
<img
src={track.coverUrl || track.cover_art_path || ''}
className="w-full h-full object-cover"
alt={track.title}
/>
{isCurrent && (
<div className="absolute inset-0 bg-muted/20 ring-1 ring-inset ring-border"></div>
)}
</div>
<div className="flex-1 min-w-0">
<h4
className={`font-bold text-sm truncate ${isCurrent ? 'text-primary' : 'text-foreground'}`}
>
{track.title}
</h4>
<p className="text-xs text-muted-foreground truncate hover:underline">
{track.artist}
</p>
</div>
<div className="hidden md:flex items-center gap-6 text-muted-foreground text-xs font-medium">
<span className="flex items-center gap-1.5 w-16 justify-end">
<Play className="w-3 h-3" />{' '}
{(track.plays || track.play_count) > 1000
? `${((track.plays || track.play_count) / 1000).toFixed(1)}k`
: track.plays || track.play_count}
</span>
<span className="flex items-center gap-1.5 w-12 justify-end font-mono">
{track.duration}
</span>
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 hover:text-destructive"
onClick={(e) => handleLike(e, track)}
>
<Heart className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
</div>
);
};