v3: allow users to edit dump title
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m54s

This commit is contained in:
khannurien
2026-06-21 12:57:46 +00:00
parent 34933a3d4f
commit 888aae45cf
12 changed files with 322 additions and 51 deletions

View File

@@ -5,7 +5,7 @@ import { Trans } from "@lingui/react/macro";
import { dumpUrl } from "../utils/urls.ts";
import { AddToPlaylistModal } from "../components/AddToPlaylistModal.tsx";
import { API_URL } from "../config/api.ts";
import { API_URL, VALIDATION } from "../config/api.ts";
import type {
Comment,
@@ -13,6 +13,7 @@ import type {
PublicUser,
RawComment,
RawDump,
UpdateDumpRequest,
} from "../model.ts";
import {
deserializeComment,
@@ -54,7 +55,12 @@ export function Dump() {
const [comments, setComments] = useState<Comment[]>([]);
const { user, token } = useAuth();
const [titleEditing, setTitleEditing] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const [titleSaving, setTitleSaving] = useState(false);
const [titleError, setTitleError] = useState<string | null>(null);
const { user, token, authFetch } = useAuth();
const {
voteCounts,
myVotes,
@@ -226,6 +232,41 @@ export function Dump() {
const { dump } = dumpState;
const canEdit = !!user && (dump.userId === user.id || user.isAdmin === true);
const handleTitleSave = async () => {
const trimmed = titleDraft.trim();
if (!trimmed || trimmed.length > VALIDATION.DUMP_TITLE_MAX) return;
setTitleSaving(true);
setTitleError(null);
try {
const res = await authFetch(`${API_URL}/api/dumps/${dump.id}`, {
method: "PUT",
body: JSON.stringify(
{ title: trimmed } satisfies UpdateDumpRequest,
),
});
const apiResponse = parseAPIResponse<RawDump>(await res.json());
if (!apiResponse.success) {
setTitleError(apiResponse.error.message);
return;
}
const updatedDump = deserializeDump(apiResponse.data);
setTitleEditing(false);
if (updatedDump.slug !== dump.slug) {
navigate(dumpUrl(updatedDump), {
replace: true,
state: { dump: updatedDump },
});
} else {
setDumpState({ status: "loaded", dump: updatedDump });
}
} catch (err) {
setTitleError(friendlyFetchError(err));
} finally {
setTitleSaving(false);
}
};
return (
<PageShell>
<div className="dump-detail">
@@ -252,7 +293,68 @@ export function Dump() {
)}
</div>
<div className="dump-header-right">
<h1 className="dump-title">{dump.title}</h1>
{titleEditing
? (
<div className="dump-title-editor">
<input
type="text"
className="dump-title-input"
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleTitleSave();
} else if (e.key === "Escape") {
setTitleEditing(false);
}
}}
maxLength={VALIDATION.DUMP_TITLE_MAX}
disabled={titleSaving}
autoFocus
/>
<button
type="button"
className="btn-primary"
onClick={handleTitleSave}
disabled={titleSaving || !titleDraft.trim()}
>
{titleSaving ? <Trans>Saving</Trans> : <Trans>Save</Trans>}
</button>
<button
type="button"
className="btn-border"
onClick={() => setTitleEditing(false)}
disabled={titleSaving}
>
<Trans>Cancel</Trans>
</button>
{titleError && (
<p className="dump-title-error">{titleError}</p>
)}
</div>
)
: (
<div
className={`dump-title-view${
canEdit ? " dump-title-view--editable" : ""
}`}
onClick={canEdit
? () => {
setTitleDraft(dump.title);
setTitleError(null);
setTitleEditing(true);
}
: undefined}
>
<h1 className="dump-title">{dump.title}</h1>
{canEdit && (
<span className="dump-title-edit-btn" aria-hidden>
</span>
)}
</div>
)}
<div className="dump-op">
<Avatar
userId={dump.userId}