v2: global player, infinite scroll, image picker, threaded comments
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { API_URL } from "../config/api.ts";
|
||||
import type { Playlist, RawPlaylist } from "../model.ts";
|
||||
import { deserializePlaylist } 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[] };
|
||||
| { status: "loaded"; playlists: Playlist[]; hasMore: boolean; page: number; loadingMore: boolean };
|
||||
|
||||
export function MyPlaylists() {
|
||||
const { user, authFetch, token } = useAuth();
|
||||
@@ -22,27 +25,62 @@ export function MyPlaylists() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
fetch(`${API_URL}/api/users/${user.username}/playlists`, {
|
||||
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<RawPlaylist>;
|
||||
setState({
|
||||
status: "loaded",
|
||||
playlists: (body.data as RawPlaylist[]).map(deserializePlaylist),
|
||||
playlists: items.map(deserializePlaylist),
|
||||
hasMore,
|
||||
page: 1,
|
||||
loadingMore: false,
|
||||
});
|
||||
})
|
||||
.catch((err) =>
|
||||
setState({
|
||||
status: "error",
|
||||
error: err instanceof Error
|
||||
? err.message
|
||||
: "Failed to load playlists",
|
||||
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<RawPlaylist>;
|
||||
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;
|
||||
@@ -133,6 +171,11 @@ export function MyPlaylists() {
|
||||
)
|
||||
)}
|
||||
|
||||
<div ref={sentinelRef} />
|
||||
{state.status === "loaded" && state.loadingMore && (
|
||||
<p className="feed-loading-more">Loading more…</p>
|
||||
)}
|
||||
|
||||
{confirmDeleteId && (
|
||||
<ConfirmModal
|
||||
message="Delete this playlist? This cannot be undone."
|
||||
|
||||
Reference in New Issue
Block a user