261 lines
7.7 KiB
TypeScript
261 lines
7.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useNavigate, useParams } from "react-router";
|
|
|
|
import { API_URL } from "../config/api.ts";
|
|
import type { Dump, UpdateDumpRequest } from "../model.ts";
|
|
import { deserializeDump } from "../model.ts";
|
|
import { useRequiredAuth } from "../hooks/useAuth.ts";
|
|
import { formatBytes } from "../utils/format.ts";
|
|
import { PageShell } from "../components/PageShell.tsx";
|
|
import { PageError } from "../components/PageError.tsx";
|
|
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
|
import RichContentCard from "../components/RichContentCard.tsx";
|
|
import FilePreview from "../components/FilePreview.tsx";
|
|
|
|
type DumpEditState =
|
|
| { status: "loading" }
|
|
| { status: "error"; error: string }
|
|
| { status: "loaded"; dump: Dump };
|
|
|
|
export function DumpEdit() {
|
|
const { selectedDump } = useParams();
|
|
const navigate = useNavigate();
|
|
const { authFetch } = useRequiredAuth();
|
|
|
|
const [state, setState] = useState<DumpEditState>({ status: "loading" });
|
|
const [url, setUrl] = useState("");
|
|
const [comment, setComment] = useState("");
|
|
const [newFile, setNewFile] = useState<File | null>(null);
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!selectedDump) return;
|
|
|
|
setState({ status: "loading" });
|
|
|
|
(async () => {
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/dumps/${selectedDump}`, {
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
|
|
const apiResponse = await res.json();
|
|
|
|
if (apiResponse.success) {
|
|
const dump: Dump = deserializeDump(apiResponse.data);
|
|
setUrl(dump.url ?? "");
|
|
setComment(dump.comment ?? "");
|
|
setState({ status: "loaded", dump });
|
|
} else {
|
|
setState({ status: "error", error: apiResponse.error.message });
|
|
}
|
|
} catch (err) {
|
|
setState({
|
|
status: "error",
|
|
error: err instanceof Error ? err.message : "Load failed",
|
|
});
|
|
}
|
|
})();
|
|
}, [selectedDump]);
|
|
|
|
const handleSave = async () => {
|
|
if (state.status !== "loaded") return;
|
|
|
|
let res: Response;
|
|
|
|
if (state.dump.kind === "file" && newFile) {
|
|
const formData = new FormData();
|
|
formData.append("file", newFile);
|
|
if (comment.trim()) formData.append("comment", comment.trim());
|
|
res = await authFetch(`${API_URL}/api/dumps/${state.dump.id}/file`, {
|
|
method: "PUT",
|
|
body: formData,
|
|
});
|
|
} else {
|
|
const body: UpdateDumpRequest = state.dump.kind === "url"
|
|
? { url: url.trim() || undefined, comment: comment.trim() || undefined }
|
|
: { comment: comment.trim() || undefined };
|
|
res = await authFetch(`${API_URL}/api/dumps/${state.dump.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
if (!res.ok) {
|
|
setState({ status: "error", error: `Update failed (${res.status})` });
|
|
return;
|
|
}
|
|
|
|
const apiResponse = await res.json();
|
|
if (!apiResponse.success) {
|
|
setState({
|
|
status: "error",
|
|
error: apiResponse.error?.message ?? "Update failed",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const updatedDump: Dump = deserializeDump(apiResponse.data);
|
|
setState({ status: "loaded", dump: updatedDump });
|
|
setNewFile(null);
|
|
navigate(`/dumps/${updatedDump.id}`, { state: { dump: updatedDump } });
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (state.status !== "loaded") return;
|
|
|
|
const res = await authFetch(`${API_URL}/api/dumps/${state.dump.id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
setState({ status: "error", error: `Delete failed (${res.status})` });
|
|
return;
|
|
}
|
|
|
|
navigate("/", { state: { deletedDumpId: state.dump.id } });
|
|
};
|
|
|
|
if (state.status === "loading") {
|
|
return (
|
|
<PageShell>
|
|
<p className="page-loading">Loading dump…</p>
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
if (state.status === "error") {
|
|
return (
|
|
<PageError
|
|
message={state.error}
|
|
actions={
|
|
<>
|
|
<button
|
|
className="logout-btn"
|
|
type="button"
|
|
onClick={() => globalThis.location.reload()}
|
|
>
|
|
Retry
|
|
</button>
|
|
<button
|
|
className="logout-btn"
|
|
type="button"
|
|
onClick={() => navigate("/")}
|
|
>
|
|
← Back to all dumps
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const { dump } = state;
|
|
|
|
return (
|
|
<PageShell>
|
|
<div className="form-page form-page--two-col">
|
|
<div className="form-page-header">
|
|
<p className="form-page-eyebrow">Editing</p>
|
|
<h1 className="form-page-title">{dump.title}</h1>
|
|
</div>
|
|
|
|
<div className="dump-edit-preview">
|
|
{dump.kind === "file"
|
|
? <FilePreview dump={dump} />
|
|
: dump.richContent
|
|
? <RichContentCard richContent={dump.richContent} />
|
|
: dump.url && (
|
|
<a
|
|
href={dump.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="dump-url-link"
|
|
>
|
|
{dump.url}
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
<form
|
|
className="dump-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleSave();
|
|
}}
|
|
>
|
|
{dump.kind === "url"
|
|
? (
|
|
<div className="form-group">
|
|
<label htmlFor="url">URL</label>
|
|
<input
|
|
id="url"
|
|
type="url"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.currentTarget.value)}
|
|
placeholder="https://..."
|
|
required
|
|
/>
|
|
</div>
|
|
)
|
|
: (
|
|
<div className="form-group">
|
|
<p className="dump-file-notice">
|
|
<strong>{dump.fileName}</strong>
|
|
{dump.fileSize != null && ` — ${formatBytes(dump.fileSize)}`}
|
|
</p>
|
|
<label htmlFor="replace-file">Replace file</label>
|
|
<input
|
|
id="replace-file"
|
|
type="file"
|
|
onChange={(e) => setNewFile(e.target.files?.[0] ?? null)}
|
|
/>
|
|
{newFile && (
|
|
<p className="file-input-info">
|
|
{newFile.name} — {formatBytes(newFile.size)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="comment">Why are you dumping this?</label>
|
|
<textarea
|
|
id="comment"
|
|
value={comment}
|
|
onChange={(e) => setComment(e.currentTarget.value)}
|
|
placeholder="Tell the community what makes this worth their time..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-actions">
|
|
<button
|
|
type="button"
|
|
onClick={() => setConfirmDelete(true)}
|
|
className="btn-danger"
|
|
>
|
|
Delete dump
|
|
</button>
|
|
<div className="form-actions-right">
|
|
<Link to={`/dumps/${dump.id}`} className="form-cancel">
|
|
Cancel
|
|
</Link>
|
|
<button type="submit" className="btn-primary">Save</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{confirmDelete && (
|
|
<ConfirmModal
|
|
message="Delete this dump? This cannot be undone."
|
|
confirmLabel="Delete dump"
|
|
onConfirm={handleDelete}
|
|
onCancel={() => setConfirmDelete(false)}
|
|
/>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|