v1 feature: added playlists

This commit is contained in:
khannurien
2026-03-16 16:52:53 +00:00
parent 867e64cb5b
commit be426eb150
25 changed files with 2958 additions and 101 deletions

149
src/pages/MyPlaylists.tsx Normal file
View File

@@ -0,0 +1,149 @@
import { useEffect, useState } from "react";
import { API_URL } from "../config/api.ts";
import type { Playlist, RawPlaylist } from "../model.ts";
import { deserializePlaylist } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.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";
type State =
| { status: "loading" }
| { status: "error"; error: string }
| { status: "loaded"; playlists: Playlist[] };
export function MyPlaylists() {
const { user, authFetch, token } = useAuth();
const { lastPlaylistEvent, deletedPlaylistIds } = useWS();
const [state, setState] = useState<State>({ status: "loading" });
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
useEffect(() => {
if (!user) return;
fetch(`${API_URL}/api/users/${user.username}/playlists`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then((body) => {
if (!body.success) throw new Error("Failed to load");
setState({
status: "loaded",
playlists: (body.data as RawPlaylist[]).map(deserializePlaylist),
});
})
.catch((err) =>
setState({
status: "error",
error: err instanceof Error
? err.message
: "Failed to load playlists",
})
);
}, [user?.username]);
// 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 (
<PageShell>
<div className="my-playlists-header">
<h1 className="my-playlists-title">My Playlists</h1>
<NewPlaylistForm
toggleClassName="btn-primary"
onCreated={(p) =>
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] };
})}
/>
</div>
{state.status === "loading" && <p className="page-loading">Loading</p>}
{state.status === "error" && <p className="form-error">{state.error}</p>}
{state.status === "loaded" && (
state.playlists.length === 0
? <p className="empty-state">No playlists yet. Create one!</p>
: (
<ul className="dump-feed">
{state.playlists.map((p) => (
<PlaylistCard
key={p.id}
playlist={p}
onDelete={() => setConfirmDeleteId(p.id)}
/>
))}
</ul>
)
)}
{confirmDeleteId && (
<ConfirmModal
message="Delete this playlist? This cannot be undone."
confirmLabel="Delete playlist"
onConfirm={() => {
handleDelete(confirmDeleteId);
setConfirmDeleteId(null);
}}
onCancel={() => setConfirmDeleteId(null)}
/>
)}
</PageShell>
);
}