v2: global player, infinite scroll, image picker, threaded comments

This commit is contained in:
khannurien
2026-03-21 13:55:22 +00:00
parent be426eb150
commit 7c098e7c4c
48 changed files with 4346 additions and 711 deletions

View File

@@ -3,15 +3,11 @@ import { createPortal } from "react-dom";
import { API_URL } from "../config/api.ts";
import { useAuth } from "../hooks/useAuth.ts";
import type {
CreatePlaylistRequest,
PlaylistMembership,
RawPlaylist,
RawPlaylistMembership,
} from "../model.ts";
import {
deserializePlaylist,
deserializePlaylistMembership,
} from "../model.ts";
import { deserializePlaylistMembership } from "../model.ts";
import { PlaylistCreateForm } from "./PlaylistCreateForm.tsx";
interface AddToPlaylistModalProps {
dumpId: string;
@@ -25,10 +21,6 @@ export function AddToPlaylistModal(
const [memberships, setMemberships] = useState<PlaylistMembership[]>([]);
const [loading, setLoading] = useState(true);
const [showNewForm, setShowNewForm] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newDescription, setNewDescription] = useState("");
const [newIsPublic, setNewIsPublic] = useState(true);
const [creating, setCreating] = useState(false);
const backdropRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -87,41 +79,6 @@ export function AddToPlaylistModal(
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
setCreating(true);
try {
const req: CreatePlaylistRequest = {
title: newTitle.trim(),
description: newDescription.trim() || undefined,
isPublic: newIsPublic,
};
const res = await authFetch(`${API_URL}/api/playlists`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
const body = await res.json();
if (!body.success) return;
const playlist = deserializePlaylist(body.data as RawPlaylist);
await authFetch(
`${API_URL}/api/playlists/${playlist.id}/dumps/${dumpId}`,
{
method: "POST",
},
);
setMemberships((prev) => [{ playlist, hasDump: true }, ...prev]);
setNewTitle("");
setNewDescription("");
setShowNewForm(false);
} finally {
setCreating(false);
}
};
return createPortal(
<div
className="modal-backdrop"
@@ -176,49 +133,17 @@ export function AddToPlaylistModal(
{showNewForm
? (
<form className="modal-new-playlist-form" onSubmit={handleCreate}>
<input
type="text"
placeholder="Title"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
autoFocus
required
/>
<textarea
placeholder="Description (optional)"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
rows={2}
/>
<div className="dump-mode-toggle">
<button
type="button"
className={newIsPublic ? "active" : ""}
onClick={() => setNewIsPublic(true)}
>
Public
</button>
<button
type="button"
className={!newIsPublic ? "active" : ""}
onClick={() => setNewIsPublic(false)}
>
Private
</button>
</div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button type="submit" disabled={creating}>
{creating ? "Creating…" : "Create & Add"}
</button>
<button
type="button"
onClick={() => setShowNewForm(false)}
>
Cancel
</button>
</div>
</form>
<PlaylistCreateForm
dumpId={dumpId}
onCreated={(playlist) => {
setMemberships((prev) => [
{ playlist, hasDump: true },
...prev,
]);
setShowNewForm(false);
}}
onCancel={() => setShowNewForm(false)}
/>
)
: (
<button