v1 feature: added playlists
This commit is contained in:
149
src/pages/MyPlaylists.tsx
Normal file
149
src/pages/MyPlaylists.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user