- 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>
257 lines
9.5 KiB
TypeScript
257 lines
9.5 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Button } from '../../ui/button';
|
|
import {
|
|
Play,
|
|
Shuffle,
|
|
Heart,
|
|
MoreHorizontal,
|
|
Clock,
|
|
Edit3,
|
|
} from 'lucide-react';
|
|
import { Playlist, Track } from '../../../types';
|
|
import { useToast } from '../../../components/feedback/ToastProvider';
|
|
import { useAudio } from '../../../context/AudioContext';
|
|
import { EditPlaylistModal } from './EditPlaylistModal';
|
|
import { OptimizedImage } from '../../ui/optimized-image';
|
|
|
|
interface PlaylistDetailViewProps {
|
|
playlistId: string;
|
|
onBack: () => void;
|
|
}
|
|
|
|
// Mock Data Fetcher
|
|
const getPlaylistById = (id: string): any => ({
|
|
id,
|
|
title: 'Cyberpunk 2077 Vibes',
|
|
creator: 'Cyber_Producer',
|
|
userId: 'u1',
|
|
track_count: 12,
|
|
likes: 1240,
|
|
cover_url: 'https://picsum.photos/id/55/600/600',
|
|
tags: ['Synthwave', 'Dark'],
|
|
description:
|
|
'High octane sounds for the street samurai. A mix of heavy bass, retro synths, and futuristic atmosphere.',
|
|
is_public: true,
|
|
isCollaborative: false,
|
|
duration: '45 min',
|
|
followers: 850,
|
|
tracks: Array.from({ length: 12 }).map((_, i) => ({
|
|
id: `t${i}`,
|
|
title: `Neon Track ${i + 1}`,
|
|
artist: 'Various Artists',
|
|
album: 'Compilation',
|
|
cover_url: `https://picsum.photos/id/${60 + i}/200/200`,
|
|
duration: '3:45',
|
|
durationSec: 225,
|
|
plays: 1000 + i * 100,
|
|
likes: 50 + i,
|
|
})),
|
|
});
|
|
|
|
export const PlaylistDetailView: React.FC<PlaylistDetailViewProps> = ({
|
|
playlistId,
|
|
onBack,
|
|
}) => {
|
|
const { addToast } = useToast();
|
|
const { playTrack } = useAudio();
|
|
const [playlist, setPlaylist] = useState<any>(getPlaylistById(playlistId));
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [tracks, setTracks] = useState<Track[]>(playlist.tracks || []);
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
|
|
const handleUpdate = (data: Partial<Playlist>) => {
|
|
setPlaylist((prev: any) => ({ ...prev, ...data }));
|
|
addToast('Playlist updated', 'success');
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
addToast('Playlist deleted', 'info');
|
|
onBack();
|
|
};
|
|
|
|
// Drag and Drop Logic
|
|
const handleDragStart = (e: React.DragEvent, index: number) => {
|
|
setDraggedIndex(index);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
const ghost = document.createElement('div');
|
|
ghost.style.opacity = '0';
|
|
document.body.appendChild(ghost);
|
|
e.dataTransfer.setDragImage(ghost, 0, 0);
|
|
setTimeout(() => document.body.removeChild(ghost), 0);
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
e.preventDefault();
|
|
setDragOverIndex(index);
|
|
if (draggedIndex === null || draggedIndex === index) return;
|
|
|
|
const newTracks = [...tracks];
|
|
const [removed] = newTracks.splice(draggedIndex, 1);
|
|
newTracks.splice(index, 0, removed);
|
|
|
|
setTracks(newTracks);
|
|
setDraggedIndex(index);
|
|
};
|
|
|
|
const handleDragEnd = () => {
|
|
setDraggedIndex(null);
|
|
setDragOverIndex(null);
|
|
};
|
|
|
|
return (
|
|
<div className="animate-fadeIn pb-20">
|
|
{/* Header Section */}
|
|
<div className="flex flex-col md:flex-row gap-8 items-end mb-8 p-8 bg-card/40 rounded-2xl border-t border-border">
|
|
<div className="w-52 h-52 shadow-2xl shadow-sm rounded-lg overflow-hidden flex-shrink-0 group relative">
|
|
<OptimizedImage
|
|
src={playlist.cover_url}
|
|
alt={playlist.title || 'Playlist cover'}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<div
|
|
className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
|
|
onClick={() => setIsEditing(true)}
|
|
>
|
|
<Edit3 className="w-8 h-8 text-foreground" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 w-full">
|
|
<div className="flex items-center gap-2 mb-2 text-xs font-bold text-foreground uppercase tracking-widest">
|
|
<span>
|
|
{playlist.is_public ? 'Public Playlist' : 'Private Playlist'}
|
|
</span>
|
|
{/* {playlist.isCollaborative && <span className="bg-success/20 text-success px-2 py-0.5 rounded">Collaborative</span>} */}
|
|
</div>
|
|
<h1 className="text-4xl md:text-6xl font-heading font-bold text-foreground mb-4 leading-tight">
|
|
{playlist.title}
|
|
</h1>
|
|
<p className="text-muted-foreground text-sm mb-6 max-w-2xl">
|
|
{playlist.description}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-4 text-sm text-foreground font-medium mb-6">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-6 h-6 rounded-full bg-muted"></div>
|
|
<span className="text-foreground hover:underline cursor-pointer">
|
|
{playlist.creator}
|
|
</span>
|
|
</div>
|
|
<span className="w-1 h-1 bg-muted rounded-full"></span>
|
|
<span>{playlist.likes} likes</span>
|
|
<span className="w-1 h-1 bg-muted rounded-full"></span>
|
|
<span>
|
|
{tracks.length} songs, {playlist.duration}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-4">
|
|
<Button
|
|
variant="primary"
|
|
size="lg"
|
|
icon={<Play className="w-5 h-5 fill-current" />}
|
|
onClick={() => playTrack(tracks[0], tracks)}
|
|
>
|
|
PLAY
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="lg"
|
|
icon={<Shuffle className="w-5 h-5" />}
|
|
onClick={() => addToast('Shuffle play started')}
|
|
>
|
|
SHUFFLE
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="border border-border hover:border-border text-foreground hover:text-foreground"
|
|
onClick={() => addToast('Saved to Library')}
|
|
aria-label="Ajouter à la bibliothèque"
|
|
>
|
|
<Heart className="w-5 h-5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="border border-border hover:border-border text-foreground hover:text-foreground"
|
|
onClick={() => setIsEditing(true)}
|
|
aria-label="Plus d'options"
|
|
>
|
|
<MoreHorizontal className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tracks List */}
|
|
<div className="px-2">
|
|
<div className="grid grid-cols-[auto_1fr_auto_auto_auto] gap-4 text-xs font-bold text-muted-foreground uppercase px-4 pb-2 border-b border-white/10 mb-2">
|
|
<div className="w-8 text-center">#</div>
|
|
<div>Title</div>
|
|
<div className="hidden md:block">Album</div>
|
|
<div className="hidden sm:block">Date Added</div>
|
|
<div className="text-right pr-4">
|
|
<Clock className="w-4 h-4 ml-auto" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
{tracks.map((track, i) => (
|
|
<div
|
|
key={track.id}
|
|
draggable
|
|
onDragStart={(e) => handleDragStart(e, i)}
|
|
onDragOver={(e) => handleDragOver(e, i)}
|
|
onDragEnd={handleDragEnd}
|
|
className={`grid grid-cols-[auto_1fr_auto_auto_auto] gap-4 items-center p-2 rounded-lg hover:bg-muted/50 group transition-colors ${draggedIndex === i ? 'opacity-50 bg-primary/10 shadow-lg cursor-grabbing' : ''} ${dragOverIndex === i && draggedIndex !== null && draggedIndex !== i ? 'border-t-2 border-t-primary bg-primary/5' : ''}`}
|
|
>
|
|
<div className="w-8 text-center flex justify-center text-muted-foreground group-hover:text-foreground cursor-grab active:cursor-grabbing">
|
|
<span className="group-hover:hidden">{i + 1}</span>
|
|
<Play
|
|
className="w-4 h-4 fill-current hidden group-hover:block cursor-pointer"
|
|
onClick={() => playTrack(track, tracks)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-4 min-w-0">
|
|
<img
|
|
src={track.coverUrl}
|
|
className="w-10 h-10 rounded object-cover"
|
|
/>
|
|
<div className="min-w-0">
|
|
<div className="text-foreground font-bold text-sm truncate">
|
|
{track.title}
|
|
</div>
|
|
<div className="text-muted-foreground text-xs truncate hover:underline cursor-pointer">
|
|
{track.artist}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="hidden md:block text-muted-foreground text-sm truncate">
|
|
{track.album}
|
|
</div>
|
|
<div className="hidden sm:block text-muted-foreground text-xs">
|
|
2 days ago
|
|
</div>
|
|
<div className="text-right pr-2 flex items-center justify-end gap-4 text-sm text-muted-foreground font-mono">
|
|
<Heart className="w-4 h-4 opacity-0 group-hover:opacity-100 hover:text-destructive cursor-pointer transition-all" />
|
|
<span>{track.duration}</span>
|
|
<MoreHorizontal className="w-4 h-4 opacity-0 group-hover:opacity-100 hover:text-foreground cursor-pointer" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{isEditing && (
|
|
<EditPlaylistModal
|
|
playlist={playlist}
|
|
onClose={() => setIsEditing(false)}
|
|
onSave={handleUpdate}
|
|
onDelete={handleDelete}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|