'use client'; import React, { useEffect, useState } from 'react'; import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'; import { AppSidebar } from '@/components/app-sidebar'; import { SiteHeader } from '@/components/site-header'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card'; import { Field, FieldGroup, FieldLabel, FieldDescription, } from '@/components/ui/field'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { Trash2Icon, Edit2, Check, X } from 'lucide-react'; import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core'; import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; type Course = { id: string; title: string; code?: string }; type Playlist = { id: string; title: string; courseId: string }; function PlaylistsUI() { const router = useRouter(); const [courses, setCourses] = useState([]); const [playlists, setPlaylists] = useState([]); const [loading, setLoading] = useState(false); const [playlistTitle, setPlaylistTitle] = useState(''); const [playlistCourseId, setPlaylistCourseId] = useState(''); const [additionalCourseIds, setAdditionalCourseIds] = useState([]); const [deletingId, setDeletingId] = useState(null); const [editingId, setEditingId] = useState(null); const [editingTitle, setEditingTitle] = useState(''); const [savingEditId, setSavingEditId] = useState(null); useEffect(() => { fetchData(); }, []); async function fetchData() { try { const res = await fetch('/api/admin/meta'); if (res.ok) { const json = await res.json(); setCourses(json.courses || []); setPlaylists(json.playlists || []); if (!playlistCourseId && json.courses?.[0]) { setPlaylistCourseId(json.courses[0].id); } } } catch (err) { console.error('Failed to fetch data', err); toast.error('Failed to fetch data'); } } async function createPlaylist(e: React.FormEvent) { e.preventDefault(); if (!playlistCourseId) { toast.error('Pick a primary course first'); return; } setLoading(true); try { const res = await fetch('/api/admin/create-playlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: playlistTitle, courseId: playlistCourseId, additionalCourseIds, }), }); if (res.ok) { toast.success('Playlist created'); setPlaylistTitle(''); setAdditionalCourseIds([]); await fetchData(); router.refresh(); } else { const txt = await res.text(); toast.error('Failed to create playlist: ' + txt); } } catch (err: any) { toast.error('Error: ' + String(err.message ?? err)); } finally { setLoading(false); } } async function deletePlaylist(playlistId: string) { setDeletingId(playlistId); try { const res = await fetch(`/api/admin/delete-playlist`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playlistId }), }); if (res.ok) { toast.success('Playlist deleted'); await fetchData(); router.refresh(); } else { const txt = await res.text(); toast.error('Failed to delete playlist: ' + txt); } } catch (err: any) { toast.error('Error: ' + String(err.message ?? err)); } finally { setDeletingId(null); } } async function updatePlaylistTitle(playlistId: string, newTitle: string) { if (!newTitle.trim()) { toast.error('Title cannot be empty'); return; } setSavingEditId(playlistId); try { const res = await fetch(`/api/admin/update-playlist`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playlistId, title: newTitle }), }); if (res.ok) { toast.success('Playlist updated'); setEditingId(null); setEditingTitle(''); await fetchData(); router.refresh(); } else { const txt = await res.text(); toast.error('Failed to update playlist: ' + txt); } } catch (err: any) { toast.error('Error: ' + String(err.message ?? err)); } finally { setSavingEditId(null); } } // Organizer state & helpers const [selectedPlaylistId, setSelectedPlaylistId] = useState(''); const [videos, setVideos] = useState>([]); const sensors = useSensors(useSensor(PointerSensor)); function SortableItem({ id, title, thumbnail }: { id: string; title: string; thumbnail?: string }) { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, } as React.CSSProperties; return (
{thumbnail ? ( {title} ) : (
No image
)}
{title}
); } async function fetchPlaylistVideos(playlistId: string) { if (!playlistId) return; try { const res = await fetch(`/api/admin/playlist-videos?playlistId=${playlistId}`); if (res.ok) { const data = await res.json(); setVideos(data || []); } else { toast.error('Failed to load playlist videos'); } } catch (err) { console.error('Failed to fetch playlist videos', err); toast.error('Failed to load playlist videos'); } } async function handleDragEnd(e: any) { const { active, over } = e; if (!over || active.id === over.id) return; const oldIndex = videos.findIndex((v) => v.id === active.id); const newIndex = videos.findIndex((v) => v.id === over.id); if (oldIndex === -1 || newIndex === -1) return; const newOrder = arrayMove(videos, oldIndex, newIndex); setVideos(newOrder.map((v, idx) => ({ ...v, index: idx }))); // Persist order try { const orderedIds = newOrder.map((v) => v.id); const res = await fetch('/api/admin/reorder-videos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playlistId: selectedPlaylistId, orderedIds }), }); if (!res.ok) { toast.error('Failed to save new order'); // refetch to revert await fetchPlaylistVideos(selectedPlaylistId); } else { toast.success('Order saved'); } } catch (err) { console.error('Failed to save order', err); toast.error('Failed to save new order'); await fetchPlaylistVideos(selectedPlaylistId); } } const getCourseTitle = (courseId: string) => { return courses.find((c) => c.id === courseId)?.title || 'Unknown Course'; }; return (
Create Playlist
Playlist title e.g. Character Rigging Basics setPlaylistTitle(e.target.value)} placeholder="Playlist title" /> Assign to course (primary) Select the main course. The playlist will always be accessible from here. Assign to additional courses (optional) Select other courses where this playlist should appear.
{courses .filter((c) => c.id !== playlistCourseId) .map((c) => ( ))}
Organize Playlist
Select playlist
{videos.length === 0 ? (
No videos loaded
) : ( v.id)} strategy={verticalListSortingStrategy}>
{videos.map((v) => ( ))}
)}
Playlists {playlists.length === 0 ? (
No playlists yet
) : (
Title Course Actions {playlists.map((playlist) => ( {editingId === playlist.id ? (
setEditingTitle(e.target.value)} className="h-8" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') { updatePlaylistTitle(playlist.id, editingTitle); } if (e.key === 'Escape') { setEditingId(null); setEditingTitle(''); } }} />
) : (
{playlist.title}
)}
{getCourseTitle(playlist.courseId)} Delete Playlist Are you sure? This will delete the playlist and all associated videos.
Cancel deletePlaylist(playlist.id)} > Delete
))}
)}
); } export default function PlaylistsAdminClient() { return (
); }