import { useCallback, useEffect, useState } from "react"; import { API_URL } from "../config/api.ts"; import type { Playlist, RawPlaylist } from "../model.ts"; import { deserializePlaylist, type PaginatedData } from "../model.ts"; import { useAuth } from "../hooks/useAuth.ts"; import { useWS } from "../hooks/useWS.ts"; import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts"; import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx"; import { ConfirmModal } from "../components/ConfirmModal.tsx"; import { PlaylistCard } from "../components/PlaylistCard.tsx"; import { PageShell } from "../components/PageShell.tsx"; const PAGE_SIZE = 20; type State = | { status: "loading" } | { status: "error"; error: string } | { status: "loaded"; playlists: Playlist[]; hasMore: boolean; page: number; loadingMore: boolean }; export function MyPlaylists() { const { user, authFetch, token } = useAuth(); const { lastPlaylistEvent, deletedPlaylistIds } = useWS(); const [state, setState] = useState({ status: "loading" }); const [confirmDeleteId, setConfirmDeleteId] = useState(null); useEffect(() => { if (!user) return; fetch(`${API_URL}/api/users/${user.username}/playlists?page=1&limit=${PAGE_SIZE}`, { headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .then((body) => { if (!body.success) throw new Error("Failed to load"); const { items, hasMore } = body.data as PaginatedData; setState({ status: "loaded", playlists: items.map(deserializePlaylist), hasMore, page: 1, loadingMore: false, }); }) .catch((err) => setState({ status: "error", error: err instanceof Error ? err.message : "Failed to load playlists", }) ); }, [user?.username]); const loadMore = useCallback(() => { if (state.status !== "loaded" || !state.hasMore || state.loadingMore || !user) return; const nextPage = state.page + 1; setState((s) => s.status === "loaded" ? { ...s, loadingMore: true } : s); fetch( `${API_URL}/api/users/${user.username}/playlists?page=${nextPage}&limit=${PAGE_SIZE}`, { headers: { Authorization: `Bearer ${token}` } }, ) .then((r) => r.json()) .then((body) => { const { items, hasMore } = body.data as PaginatedData; setState((s) => s.status === "loaded" ? { ...s, playlists: [...s.playlists, ...items.map(deserializePlaylist)], hasMore, page: nextPage, loadingMore: false, } : s ); }) .catch(() => setState((s) => s.status === "loaded" ? { ...s, loadingMore: false } : s) ); }, [state, user, token]); const sentinelRef = useInfiniteScroll( loadMore, state.status === "loaded" && state.hasMore && !state.loadingMore, ); // Real-time WS updates useEffect(() => { if (!lastPlaylistEvent || !user) return; const ev = lastPlaylistEvent; if (ev.type === "created" && ev.playlist?.userId === user.id) { setState((s) => { if (s.status !== "loaded") return s; if (s.playlists.some((p) => p.id === ev.playlist!.id)) return s; return { ...s, playlists: [ev.playlist!, ...s.playlists] }; }); } else if (ev.type === "updated" && ev.playlist?.userId === user.id) { setState((s) => s.status === "loaded" ? { ...s, playlists: s.playlists.map((p) => p.id === ev.playlist!.id ? ev.playlist! : p ), } : s ); } else if (ev.type === "deleted") { setState((s) => s.status === "loaded" ? { ...s, playlists: s.playlists.filter((p) => p.id !== ev.playlistId), } : s ); } }, [lastPlaylistEvent, user]); useEffect(() => { if (!deletedPlaylistIds.size) return; setState((s) => s.status === "loaded" ? { ...s, playlists: s.playlists.filter((p) => !deletedPlaylistIds.has(p.id)), } : s ); }, [deletedPlaylistIds]); const handleDelete = async (playlistId: string) => { await authFetch(`${API_URL}/api/playlists/${playlistId}`, { method: "DELETE", }); setState((s) => s.status === "loaded" ? { ...s, playlists: s.playlists.filter((p) => p.id !== playlistId) } : s ); }; return (

My Playlists

setState((s) => { if (s.status !== "loaded") return s; if (s.playlists.some((pl) => pl.id === p.id)) return s; return { ...s, playlists: [p, ...s.playlists] }; })} />
{state.status === "loading" &&

Loading…

} {state.status === "error" &&

{state.error}

} {state.status === "loaded" && ( state.playlists.length === 0 ?

No playlists yet. Create one!

: (
    {state.playlists.map((p) => ( setConfirmDeleteId(p.id)} /> ))}
) )}
{state.status === "loaded" && state.loadingMore && (

Loading more…

)} {confirmDeleteId && ( { handleDelete(confirmDeleteId); setConfirmDeleteId(null); }} onCancel={() => setConfirmDeleteId(null)} /> )} ); }