v3: code quality pass

This commit is contained in:
khannurien
2026-03-24 18:47:05 +00:00
parent cd4076343b
commit c293f3e706
39 changed files with 1464 additions and 1555 deletions

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router";
import { API_URL } from "../config/api.ts";
@@ -10,19 +9,24 @@ import type {
RawDump,
RawPlaylistMembership,
} from "../model.ts";
import { deserializeDump, deserializePlaylistMembership } from "../model.ts";
import {
deserializeDump,
deserializePlaylistMembership,
parseAPIResponse,
} from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { dumpUrl } from "../utils/urls.ts";
import RichContentCard from "./RichContentCard.tsx";
import { MediaPlayer } from "./MediaPlayer.tsx";
import type { RichContent } from "../model.ts";
import { PlaylistCreateForm } from "./PlaylistCreateForm.tsx";
import { ErrorCard } from "./ErrorCard.tsx";
import { FileDropZone } from "./FileDropZone.tsx";
import { TextEditor } from "./TextEditor.tsx";
import { Modal } from "./Modal.tsx";
import { PlaylistMembershipPanel } from "./PlaylistMembershipPanel.tsx";
import { friendlyFetchError } from "../utils/apiError.ts";
import { MAX_FILE_SIZE } from "../config/upload.ts";
import { TextEditor } from "./TextEditor.tsx";
type Mode = "url" | "file";
type Phase = "create" | "playlist";
@@ -38,16 +42,10 @@ type UrlPreview =
| { status: "done"; richContent: RichContent | null };
function LocalFilePreview({ file }: { file: File }) {
const [src, setSrc] = useState<string | null>(null);
const src = useMemo(() => URL.createObjectURL(file), [file]);
const mime = file.type;
useEffect(() => {
const url = URL.createObjectURL(file);
setSrc(url);
return () => URL.revokeObjectURL(url);
}, [file]);
if (!src) return null;
useEffect(() => () => URL.revokeObjectURL(src), [src]);
if (mime.startsWith("image/")) {
return <img src={src} alt={file.name} className="local-preview-image" />;
@@ -58,7 +56,6 @@ function LocalFilePreview({ file }: { file: File }) {
if (mime.startsWith("audio/")) {
return <MediaPlayer key={src} src={src} kind="audio" mime={mime} />;
}
// For other types the drop zone chip already shows name + size.
return null;
}
@@ -69,7 +66,6 @@ interface DumpCreateModalProps {
export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
const { authFetch } = useAuth();
const { injectDump } = useWS();
const backdropRef = useRef<HTMLDivElement>(null);
const [phase, setPhase] = useState<Phase>("create");
const [createdDump, setCreatedDump] = useState<Dump | null>(null);
@@ -89,24 +85,6 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
// Playlist phase state
const [memberships, setMemberships] = useState<PlaylistMembership[]>([]);
const [playlistsLoading, setPlaylistsLoading] = useState(false);
const [showNewPlaylistForm, setShowNewPlaylistForm] = useState(false);
// Lock body scroll
useEffect(() => {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}, []);
// Escape key to close (skip if a picker/dropdown already handled it)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape" && !e.defaultPrevented) onClose();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [onClose]);
// Debounced URL preview
useEffect(() => {
@@ -172,7 +150,7 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
return () => globalThis.removeEventListener("paste", handler);
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setSubmitState({ status: "submitting" });
@@ -215,9 +193,9 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
});
}
const apiResponse = await res.json();
const apiResponse = parseAPIResponse<RawDump>(await res.json());
if (apiResponse.success) {
const dump = deserializeDump(apiResponse.data as RawDump);
const dump = deserializeDump(apiResponse.data);
injectDump(dump);
setCreatedDump(dump);
setPhase("playlist");
@@ -238,7 +216,7 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
} else {
setSubmitState({
status: "error",
error: apiResponse.error?.message ?? "Failed to create dump.",
error: apiResponse.error.message,
});
}
} catch (err) {
@@ -274,255 +252,189 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
const submitting = submitState.status === "submitting";
return createPortal(
<div
className="modal-backdrop"
ref={backdropRef}
onClick={(e) => {
if (e.target === backdropRef.current) onClose();
}}
return (
<Modal
title={phase === "create" ? "New dump" : "Add to playlist"}
onClose={onClose}
wide
>
<div className="modal-card modal-card--wide">
<div className="modal-header">
<span className="modal-title">
{phase === "create" ? "New dump" : "Add to playlist"}
</span>
<button
type="button"
className="modal-close-btn"
onClick={onClose}
aria-label="Close"
>
</button>
</div>
{phase === "create"
? (
<>
<div className="visibility-toggle">
<button
type="button"
className={mode === "url" ? "active" : ""}
onClick={() => {
setMode("url");
setFile(null);
setSubmitState({ status: "idle" });
}}
disabled={submitting}
>
🔗 URL
</button>
<button
type="button"
className={mode === "file" ? "active" : ""}
onClick={() => {
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
setSubmitState({ status: "idle" });
}}
disabled={submitting}
>
📎 File
</button>
</div>
<div className="modal-body">
{phase === "create"
? (
<>
<div className="visibility-toggle">
<button
type="button"
className={mode === "url" ? "active" : ""}
onClick={() => {
setMode("url");
setFile(null);
setSubmitState({ status: "idle" });
}}
disabled={submitting}
>
🔗 URL
</button>
<button
type="button"
className={mode === "file" ? "active" : ""}
onClick={() => {
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
setSubmitState({ status: "idle" });
}}
disabled={submitting}
>
📎 File
</button>
</div>
<form onSubmit={handleSubmit} className="dump-form">
{submitState.status === "error" && (
<ErrorCard
title="Failed to post"
message={submitState.error}
/>
)}
<form onSubmit={handleSubmit} className="dump-form">
{submitState.status === "error" && (
<ErrorCard
title="Failed to post"
message={submitState.error}
/>
)}
{mode === "url"
? (
<>
<div className="form-group">
<label htmlFor="dc-url">URL</label>
<input
id="dc-url"
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
onPaste={(e) => {
const pastedFile = e.clipboardData.files[0];
if (pastedFile) {
e.preventDefault();
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
setFile(pastedFile);
setSubmitState({ status: "idle" });
}
}}
disabled={submitting}
placeholder="https://..."
required
autoFocus
/>
</div>
{urlPreview.status === "loading" && (
<p className="preview-loading">Fetching preview</p>
)}
{urlPreview.status === "done" &&
urlPreview.richContent && (
<RichContentCard
richContent={urlPreview.richContent}
/>
)}
</>
)
: (
<>
<FileDropZone
file={file}
onChange={setFile}
disabled={submitting}
/>
{file && <LocalFilePreview file={file} />}
</>
)}
<div className="form-group">
<label htmlFor="dc-comment">
Why are you dumping this?
</label>
<TextEditor
id="dc-comment"
value={comment}
onChange={setComment}
disabled={submitting}
placeholder="Tell the community what makes this worth their time..."
rows={3}
/>
</div>
<div className="visibility-toggle">
<button
type="button"
className={!isPrivate ? "active" : ""}
disabled={submitting}
onClick={() => setIsPrivate(false)}
>
Public
</button>
<button
type="button"
className={isPrivate ? "active" : ""}
disabled={submitting}
onClick={() => setIsPrivate(true)}
>
Private
</button>
</div>
<div className="form-actions">
<div className="form-actions-right">
<button
type="button"
className="form-cancel"
onClick={onClose}
>
Cancel
</button>
<button
type="submit"
className="btn-primary"
{mode === "url"
? (
<>
<div className="form-group">
<label htmlFor="dc-url">URL</label>
<input
id="dc-url"
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
onPaste={(e) => {
const pastedFile = e.clipboardData.files[0];
if (pastedFile) {
e.preventDefault();
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
setFile(pastedFile);
setSubmitState({ status: "idle" });
}
}}
disabled={submitting}
>
{submitting
? (mode === "url" ? "Fetching…" : "Uploading…")
: "Dump it"}
</button>
placeholder="https://..."
required
autoFocus
/>
</div>
</div>
</form>
</>
)
: (
<>
{createdDump && (
<p className="dump-create-success">
Dumped!{" "}
<Link to={dumpUrl(createdDump)} onClick={onClose}>
View dump
</Link>
</p>
{urlPreview.status === "loading" && (
<p className="preview-loading">Fetching preview</p>
)}
{urlPreview.status === "done" &&
urlPreview.richContent && (
<RichContentCard
richContent={urlPreview.richContent}
/>
)}
</>
)
: (
<>
<FileDropZone
file={file}
onChange={setFile}
disabled={submitting}
/>
{file && <LocalFilePreview file={file} />}
</>
)}
{playlistsLoading
? <p className="page-loading">Loading playlists</p>
: memberships.length === 0 && !showNewPlaylistForm
? <p className="empty-state">No playlists yet.</p>
: (
<ul className="playlist-membership-list">
{memberships.map((m) => (
<li
key={m.playlist.id}
className={`playlist-membership-row${
m.hasDump ? " playlist-membership-row--active" : ""
}`}
onClick={() => toggleMembership(m)}
>
<span className="playlist-membership-check">
{m.hasDump ? "✓" : "○"}
</span>
<span className="playlist-membership-name">
{m.playlist.title}
</span>
{!m.playlist.isPublic && (
<span className="playlist-badge playlist-badge--private">
private
</span>
)}
</li>
))}
</ul>
)}
<div className="form-group">
<label htmlFor="dc-comment">
Why are you dumping this?
</label>
<TextEditor
id="dc-comment"
value={comment}
onChange={setComment}
disabled={submitting}
placeholder="Tell the community what makes this worth their time..."
rows={3}
/>
</div>
{showNewPlaylistForm
? (
<PlaylistCreateForm
dumpId={createdDump?.id}
onCreated={(playlist) => {
setMemberships((prev) => [
{ playlist, hasDump: true },
...prev,
]);
setShowNewPlaylistForm(false);
}}
onCancel={() => setShowNewPlaylistForm(false)}
/>
)
: (
<button
type="button"
className="modal-new-playlist-toggle"
onClick={() => setShowNewPlaylistForm(true)}
>
+ New playlist
</button>
)}
<div className="visibility-toggle">
<button
type="button"
className={!isPrivate ? "active" : ""}
disabled={submitting}
onClick={() => setIsPrivate(false)}
>
Public
</button>
<button
type="button"
className={isPrivate ? "active" : ""}
disabled={submitting}
onClick={() => setIsPrivate(true)}
>
Private
</button>
</div>
<div className="form-actions">
<div className="form-actions-right">
<button
type="button"
className="btn-primary"
onClick={onClose}
>
Done
</button>
</div>
<div className="form-actions">
<div className="form-actions-right">
<button
type="button"
className="form-cancel"
onClick={onClose}
>
Cancel
</button>
<button
type="submit"
className="btn-primary"
disabled={submitting}
>
{submitting
? (mode === "url" ? "Fetching…" : "Uploading…")
: "Dump it"}
</button>
</div>
</>
</div>
</form>
</>
)
: (
<>
{createdDump && (
<p className="dump-create-success">
Dumped!{" "}
<Link to={dumpUrl(createdDump)} onClick={onClose}>
View dump
</Link>
</p>
)}
</div>
</div>
</div>,
document.body,
<PlaylistMembershipPanel
dumpId={createdDump?.id ?? ""}
memberships={memberships}
loading={playlistsLoading}
onToggle={toggleMembership}
onPlaylistCreated={(membership) =>
setMemberships((prev) => [membership, ...prev])}
/>
<div className="form-actions">
<div className="form-actions-right">
<button
type="button"
className="btn-primary"
onClick={onClose}
>
Done
</button>
</div>
</div>
</>
)}
</Modal>
);
}