veza/apps/web/src/components/library/playlists/PlaylistDetailView.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

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>
);
};