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

@@ -1,9 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { API_URL } from "../config/api.ts";
import { useAuth } from "../hooks/useAuth.ts";
import type { Playlist, RawPlaylist } from "../model.ts";
import { deserializePlaylist } from "../model.ts";
import type { Playlist } from "../model.ts";
import { PlaylistCreateForm } from "./PlaylistCreateForm.tsx";
interface NewPlaylistFormProps {
onCreated: (playlist: Playlist) => void;
@@ -18,15 +16,11 @@ export function NewPlaylistForm(
toggleClassName = "new-playlist-toggle",
}: NewPlaylistFormProps,
) {
const { authFetch } = useAuth();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [isPublic, setIsPublic] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const backdropRef = useRef<HTMLDivElement>(null);
const close = () => setOpen(false);
useEffect(() => {
if (!open) return;
document.body.style.overflow = "hidden";
@@ -44,43 +38,6 @@ export function NewPlaylistForm(
return () => document.removeEventListener("keydown", handler);
}, [open]);
const close = () => {
setOpen(false);
setTitle("");
setDescription("");
setIsPublic(true);
setError(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
setSubmitting(true);
setError(null);
try {
const res = await authFetch(`${API_URL}/api/playlists`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.trim(),
description: description.trim() || undefined,
isPublic,
}),
});
const body = await res.json();
if (!body.success) {
setError(body.error?.message ?? "Failed to create playlist");
return;
}
onCreated(deserializePlaylist(body.data as RawPlaylist));
close();
} catch {
setError("Failed to create playlist");
} finally {
setSubmitting(false);
}
};
return (
<>
<button
@@ -112,45 +69,13 @@ export function NewPlaylistForm(
</button>
</div>
<div className="modal-body">
<form className="modal-new-playlist-form" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
autoFocus
required
/>
<textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
/>
<div className="dump-mode-toggle">
<button
type="button"
className={isPublic ? "active" : ""}
onClick={() => setIsPublic(true)}
>
Public
</button>
<button
type="button"
className={!isPublic ? "active" : ""}
onClick={() => setIsPublic(false)}
>
Private
</button>
</div>
{error && <p className="form-error">{error}</p>}
<div style={{ display: "flex", gap: "0.5rem" }}>
<button type="submit" disabled={submitting}>
{submitting ? "Creating…" : "Create"}
</button>
<button type="button" onClick={close}>Cancel</button>
</div>
</form>
<PlaylistCreateForm
onCreated={(playlist) => {
onCreated(playlist);
close();
}}
onCancel={close}
/>
</div>
</div>
</div>,