vibe coded v1
This commit is contained in:
@@ -2,10 +2,12 @@ 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 { useRequiredAuth } from "../hooks/useAuth.ts";
|
||||
import { formatBytes } from "../utils/format.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import RichContentCard from "../components/RichContentCard.tsx";
|
||||
import FilePreview from "../components/FilePreview.tsx";
|
||||
|
||||
type DumpEditState =
|
||||
| { status: "loading" }
|
||||
@@ -18,8 +20,9 @@ export function DumpEdit() {
|
||||
const { authFetch } = useRequiredAuth();
|
||||
|
||||
const [state, setState] = useState<DumpEditState>({ status: "loading" });
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [comment, setComment] = useState("");
|
||||
const [newFile, setNewFile] = useState<File | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDump) return;
|
||||
@@ -28,21 +31,18 @@ export function DumpEdit() {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/dumps/${selectedDump}`);
|
||||
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 = apiResponse.data;
|
||||
setTitle(dump.title);
|
||||
setDescription(dump.description ?? "");
|
||||
setUrl(dump.url ?? "");
|
||||
setComment(dump.comment ?? "");
|
||||
setState({ status: "loaded", dump });
|
||||
} else {
|
||||
setState({
|
||||
status: "error",
|
||||
error: apiResponse.error.message,
|
||||
});
|
||||
setState({ status: "error", error: apiResponse.error.message });
|
||||
}
|
||||
} catch (err) {
|
||||
setState({
|
||||
@@ -56,25 +56,41 @@ export function DumpEdit() {
|
||||
const handleSave = async () => {
|
||||
if (state.status !== "loaded") return;
|
||||
|
||||
const body: UpdateDumpRequest = {
|
||||
title,
|
||||
description: description || undefined,
|
||||
};
|
||||
let res: Response;
|
||||
|
||||
const res = await authFetch(`${API_URL}/api/dumps/${state.dump.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
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})`,
|
||||
});
|
||||
setState({ status: "error", error: `Update failed (${res.status})` });
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/dumps/${state.dump.id}`);
|
||||
const apiResponse = await res.json();
|
||||
if (!apiResponse.success) {
|
||||
setState({ status: "error", error: apiResponse.error?.message ?? "Update failed" });
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedDump: Dump = apiResponse.data;
|
||||
setState({ status: "loaded", dump: updatedDump });
|
||||
setNewFile(null);
|
||||
navigate(`/dumps/${updatedDump.id}`, { state: { dump: updatedDump } });
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -85,85 +101,110 @@ export function DumpEdit() {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
setState({
|
||||
status: "error",
|
||||
error: `Delete failed (${res.status})`,
|
||||
});
|
||||
setState({ status: "error", error: `Delete failed (${res.status})` });
|
||||
return;
|
||||
}
|
||||
|
||||
navigate("/");
|
||||
navigate("/", { state: { deletedDumpId: state.dump.id } });
|
||||
};
|
||||
|
||||
if (state.status === "loading") {
|
||||
return <div className="loading">Loading dump...</div>;
|
||||
return <PageShell><p className="page-loading">Loading dump…</p></PageShell>;
|
||||
}
|
||||
|
||||
if (state.status === "error") {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h2>Error</h2>
|
||||
<p>{state.error}</p>
|
||||
<button type="button" onClick={() => globalThis.location.reload()}>
|
||||
Retry
|
||||
</button>
|
||||
<p>
|
||||
<PageShell>
|
||||
<div className="page-error">
|
||||
<h2>Error</h2>
|
||||
<p>{state.error}</p>
|
||||
<button type="button" onClick={() => globalThis.location.reload()}>Retry</button>
|
||||
<Link to="/">← Back to all dumps</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
const { dump } = state;
|
||||
|
||||
return (
|
||||
<div className="dump-container">
|
||||
<div className="dump-meta">
|
||||
<h1>Edit Dump</h1>
|
||||
<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={handleDelete} 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>
|
||||
|
||||
<form
|
||||
className="dump-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label htmlFor="title">
|
||||
<strong>Title</strong>
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">
|
||||
<strong>Description (optional)</strong>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.currentTarget.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="dump-actions">
|
||||
<button type="submit">Save</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
style={{ backgroundColor: "#a02b2b" }}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<Link to={`/dumps/${state.dump.id}`}>Cancel</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user