- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid - Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px) - Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation - Modified files across all components to ensure consistent 8px grid alignment - Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
191 lines
6.4 KiB
TypeScript
191 lines
6.4 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 '@/context/ToastContext';
|
|
import { trackService } from '@/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-kodo-ink/50 animate-pulse rounded-xl border border-kodo-steel/30"
|
|
></div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="p-6 text-center border border-kodo-red/30 bg-kodo-red/10 rounded-xl text-kodo-red">
|
|
<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-kodo-content-dim text-center py-12 bg-kodo-ink/30 rounded-xl border border-dashed border-kodo-steel">
|
|
<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={`
|
|
group flex items-center gap-4 p-4 rounded-xl transition-all border cursor-pointer relative overflow-hidden
|
|
${isCurrent ? 'bg-kodo-cyan/10 border-kodo-cyan/30' : 'bg-kodo-ink border-transparent hover:border-kodo-steel/50 hover:bg-kodo-ink/80'}
|
|
`}
|
|
onClick={() => handlePlay(track)}
|
|
>
|
|
{/* Active Indicator Bar */}
|
|
{isCurrent && (
|
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-kodo-cyan"></div>
|
|
)}
|
|
|
|
<div className="w-8 text-center text-kodo-content-dim 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-kodo-cyan h-full animate-[bounce_1s_infinite]"></div>
|
|
<div className="w-0.5 bg-kodo-cyan h-2/3 animate-[bounce_1.2s_infinite]"></div>
|
|
<div className="w-0.5 bg-kodo-cyan h-full animate-[bounce_0.8s_infinite]"></div>
|
|
</div>
|
|
) : (
|
|
<span className="group-hover:hidden text-kodo-content-dim">
|
|
{i + 1}
|
|
</span>
|
|
)}
|
|
<Play
|
|
className={`w-4 h-4 mx-auto fill-current hidden group-hover:block ${isCurrent ? 'text-kodo-cyan' : 'text-white'}`}
|
|
/>
|
|
</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-kodo-steel/20 ring-1 ring-inset ring-kodo-steel"></div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<h4
|
|
className={`font-bold text-sm truncate ${isCurrent ? 'text-kodo-cyan' : 'text-white'}`}
|
|
>
|
|
{track.title}
|
|
</h4>
|
|
<p className="text-xs text-kodo-content-dim truncate hover:underline">
|
|
{track.artist}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="hidden md:flex items-center gap-6 text-kodo-content-dim 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-kodo-magenta"
|
|
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>
|
|
);
|
|
};
|