v3: allow users to edit dump title
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m54s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m54s
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user