veza/apps/web/src/components/library/playlists/AddToPlaylistModal.tsx

155 lines
5.1 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { X, Search, Plus, Check } from 'lucide-react';
import { useToast } from '../../../context/ToastContext';
2026-01-07 18:39:21 +00:00
interface AddToPlaylistModalProps {
onClose: () => void;
onAdd: (playlistIds: string[]) => void;
}
// Mock user playlists
const MOCK_USER_PLAYLISTS: any[] = [
{
id: 'p1',
title: 'Cyberpunk Essentials',
creator: 'Cyber_Producer',
userId: 'u1',
is_public: true,
cover_url: 'https://picsum.photos/100',
track_count: 45,
likes: 120,
tags: [],
},
{
id: 'p2',
title: 'Late Night Coding',
creator: 'Cyber_Producer',
userId: 'u1',
is_public: false,
cover_url: 'https://picsum.photos/101',
track_count: 22,
likes: 15,
tags: [],
},
{
id: 'p3',
title: 'Gym Phonk',
creator: 'Cyber_Producer',
userId: 'u1',
is_public: true,
cover_url: 'https://picsum.photos/102',
track_count: 105,
likes: 300,
tags: [],
},
];
export const AddToPlaylistModal: React.FC<AddToPlaylistModalProps> = ({
onClose,
onAdd,
}) => {
const { addToast } = useToast();
const [search, setSearch] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const filteredPlaylists = MOCK_USER_PLAYLISTS.filter((p) =>
p.title.toLowerCase().includes(search.toLowerCase()),
);
const toggleSelection = (id: string) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((pid) => pid !== id) : [...prev, id],
);
};
const handleConfirm = () => {
if (selectedIds.length === 0) return;
onAdd(selectedIds);
onClose();
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
onClick={onClose}
></div>
<div className="relative w-full max-w-md bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden flex flex-col max-h-[70vh]">
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
<h3 className="font-bold text-white">Add to Playlist</h3>
<button onClick={onClose}>
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
</button>
</div>
<div className="p-4">
<div className="relative mb-4">
<input
className="w-full bg-kodo-void border border-kodo-steel rounded-lg py-2 pl-9 pr-4 text-white text-sm focus:border-kodo-cyan outline-none"
placeholder="Find playlist"
value={search}
onChange={(e) => setSearch(e.target.value)}
autoFocus
/>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-kodo-content-dim" />
</div>
<Button
variant="ghost"
aesthetic-improvements: replace secondary cyan hover states with steel - Button outline variant: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - Header secondary nav: hover:text-kodo-cyan → hover:text-white, hover:bg-kodo-cyan/5 → hover:bg-white/5 - FileManagerView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProjectsManager: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - GroupDetailView: hover:border-kodo-cyan/30 → hover:border-kodo-steel/50 - AIToolsView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CloudFileBrowser: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProfileView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CourseCard: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - TwoFactorSetup: hover:border-kodo-cyan → hover:border-kodo-steel/50 - GearView: hover:text-kodo-cyan → hover:text-white, hover:border-kodo-cyan → hover:border-kodo-steel/50 - ChatInput: hover:text-kodo-cyan → hover:text-white (3 instances) - ChatMessage: hover:text-kodo-cyan → hover:text-white (2 instances) - ChatRoom: hover:text-kodo-cyan → hover:text-white - AddToPlaylistModal: hover:border-kodo-cyan → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - Preserved focus rings (cyan) and active/selected states (cyan) as per audit - Action 11.3.1.2 in progress (first batch of ~15 files)
2026-01-16 09:51:30 +00:00
className="w-full justify-start border border-dashed border-kodo-steel mb-4 hover:border-kodo-steel/50 hover:text-white group"
onClick={() => addToast('New Playlist Flow')}
>
<div className="w-8 h-8 bg-kodo-steel rounded flex items-center justify-center mr-3 group-hover:bg-kodo-cyan/20">
<Plus className="w-4 h-4" />
</div>
<span className="text-sm font-bold">New Playlist</span>
</Button>
<div className="space-y-1 overflow-y-auto max-h-64 custom-scrollbar">
{filteredPlaylists.map((playlist) => (
<div
key={playlist.id}
onClick={() => toggleSelection(playlist.id)}
className={`flex items-center p-2 rounded cursor-pointer group transition-colors ${selectedIds.includes(playlist.id) ? 'bg-kodo-cyan/10' : 'hover:bg-white/5'}`}
>
<img
src={playlist.cover_url}
className="w-10 h-10 rounded object-cover mr-3"
/>
<div className="flex-1 min-w-0">
<div
className={`text-sm font-bold truncate ${selectedIds.includes(playlist.id) ? 'text-kodo-cyan' : 'text-white'}`}
>
{playlist.title}
</div>
<div className="text-xs text-kodo-content-dim">
{playlist.track_count} tracks
</div>
</div>
<div
className={`w-5 h-5 rounded-full border flex items-center justify-center ${selectedIds.includes(playlist.id) ? 'bg-kodo-cyan border-kodo-cyan' : 'border-kodo-steel'}`}
>
{selectedIds.includes(playlist.id) && (
<Check className="w-3 h-3 text-black" />
)}
</div>
</div>
))}
</div>
</div>
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end">
<Button
variant="primary"
onClick={handleConfirm}
disabled={selectedIds.length === 0}
>
Done
</Button>
</div>
</div>
</div>
);
};