vibe coded v1

This commit is contained in:
khannurien
2026-03-16 07:34:49 +00:00
parent 6207a7549f
commit e88fed4e98
48 changed files with 4303 additions and 595 deletions

View File

@@ -1,11 +1,18 @@
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router";
import { Link, useLocation, useParams } from "react-router";
import { API_URL } from "../config/api.ts";
import type { Dump } from "../model.ts";
import type { Dump, PublicUser } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { relativeTime } from "../utils/relativeTime.ts";
import { useWS } from "../hooks/useWS.ts";
import { Avatar } from "../components/Avatar.tsx";
import RichContentCard from "../components/RichContentCard.tsx";
import FilePreview from "../components/FilePreview.tsx";
import { VoteButton } from "../components/VoteButton.tsx";
import { PageShell } from "../components/PageShell.tsx";
type DumpState =
| { status: "loading" }
@@ -14,26 +21,44 @@ type DumpState =
export function Dump() {
const { selectedDump } = useParams();
const location = useLocation();
const preloaded = (location.state as { dump?: Dump } | null)?.dump ?? null;
const [dumpState, setDumpState] = useState<DumpState>({ status: "loading" });
const [dumpState, setDumpState] = useState<DumpState>(
preloaded ? { status: "loaded", dump: preloaded } : { status: "loading" },
);
const [op, setOp] = useState<PublicUser | null>(null);
const { user } = useAuth();
const { voteCounts, myVotes, castVote, removeVote } = useWS();
// Fetch dump data
useEffect(() => {
if (!selectedDump) return;
if (preloaded) {
fetch(`${API_URL}/api/users/by-id/${preloaded.userId}`)
.then((r) => r.json())
.then((r) => r.success && setOp(r.data))
.catch(() => {});
return;
}
setDumpState({ status: "loading" });
setOp(null);
(async () => {
try {
const res = await fetch(`${API_URL}/api/dumps/${selectedDump}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
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();
setDumpState({ status: "loaded", dump: apiResponse.data });
const dump: Dump = apiResponse.data;
setDumpState({ status: "loaded", dump });
fetch(`${API_URL}/api/users/by-id/${dump.userId}`)
.then((r) => r.json())
.then((r) => r.success && setOp(r.data))
.catch(() => {});
} catch (err) {
setDumpState({
status: "error",
@@ -44,49 +69,82 @@ export function Dump() {
}, [selectedDump]);
if (dumpState.status === "loading") {
return <div className="loading">Loading dump...</div>;
return <PageShell><p className="page-loading">Loading dump</p></PageShell>;
}
if (dumpState.status === "error") {
return (
<div className="error-container">
<h2>Error</h2>
<p>{dumpState.error}</p>
<button type="button" onClick={() => globalThis.location.reload()}>
Retry
</button>
<p>
<PageShell>
<div className="page-error">
<h2>Error</h2>
<p>{dumpState.error}</p>
<button type="button" onClick={() => globalThis.location.reload()}>Retry</button>
<Link to="/"> Back to all dumps</Link>
</p>
</div>
</div>
</PageShell>
);
}
const { dump } = dumpState;
const canEdit = !!user &&
(dump.userId === user.id || user.isAdmin === true);
const canEdit = !!user && (dump.userId === user.id || user.isAdmin === true);
return (
<div className="dump-container">
<div className="dump-meta">
<h1>{dump.title}</h1>
{dump.description && (
<p className="dump-description">{dump.description}</p>
)}
</div>
<PageShell>
<div className="dump-detail">
{/* Post header */}
<div className="dump-post-header">
<div className="dump-header-block">
<VoteButton
dumpId={dump.id}
count={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
disabled={!user}
onCast={castVote}
onRemove={removeVote}
/>
<div className="dump-header-info">
<h1 className="dump-title">{dump.title}</h1>
<div className="dump-op">
<Avatar
userId={dump.userId}
username={op?.username ?? "?"}
hasAvatar={!!op?.avatarMime}
size={22}
/>
{op
? <Link to={`/users/${op.username}`} className="dump-op-link">{op.username}</Link>
: <span className="dump-op-link"></span>}
<time className="dump-card-date" dateTime={dump.createdAt} title={new Date(dump.createdAt).toLocaleString()}>
{relativeTime(dump.createdAt)}
</time>
</div>
</div>
</div>
<div className="dump-grid">
~
</div>
{dump.comment && (
<blockquote className="dump-comment">{dump.comment}</blockquote>
)}
</div>
<div className="dump-actions">
{canEdit && (
<Link to={`/dumps/${dump.id}/edit`}>
Edit dump
</Link>
)}
<Link to="/"> Back to all dumps</Link>
{/* Main content */}
<div className="dump-rich-content">
{dump.kind === "file"
? <FilePreview dump={dump} />
: dump.richContent
? <RichContentCard richContent={dump.richContent} />
: (
<a href={dump.url} target="_blank" rel="noopener noreferrer" className="dump-url-link">
{dump.url}
</a>
)}
</div>
{/* Actions */}
<div className="dump-actions">
{canEdit && <Link to={`/dumps/${dump.id}/edit`}>Edit</Link>}
<Link to="/"> Back to all dumps</Link>
</div>
</div>
</div>
</PageShell>
);
}