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,122 +1,119 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router";
import { Link, useLocation } from "react-router";
import { API_URL } from "../config/api.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { type Dump } from "../model.ts";
import { Avatar } from "../components/Avatar.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { AppHeader } from "../components/AppHeader.tsx";
type DumpsState =
| { status: "loading" }
| { status: "error"; error: string }
| { status: "loaded"; dumps: Dump[] };
type SortMode = "new" | "hot";
function hotScore(dump: Dump): number {
const ageHours = (Date.now() - new Date(dump.createdAt).getTime()) / 3_600_000;
return (dump.voteCount + 1) / Math.pow(ageHours + 2, 1.5);
}
export function Index() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const justDeletedId = (location.state as { deletedDumpId?: string } | null)?.deletedDumpId;
const handleCreateDump = () => {
navigate("/dumps/new");
};
const { user } = useAuth();
const { onlineUsers, voteCounts, myVotes, recentDumps, deletedDumpIds, castVote, removeVote } = useWS();
const handleRegister = () => {
navigate("/register");
};
const handleLogin = () => {
navigate("/login");
};
const handleLogout = () => {
logout();
navigate("/", { replace: true });
};
const [dumpsState, setDumpsState] = useState<DumpsState>({
status: "loading",
});
const [dumpsState, setDumpsState] = useState<DumpsState>({ status: "loading" });
const [sort, setSort] = useState<SortMode>("hot");
useEffect(() => {
(async () => {
try {
const response = await fetch(`${API_URL}/api/dumps/`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const apiResponse = await response.json();
setDumpsState({ status: "loaded", dumps: apiResponse.data });
const res = await fetch(`${API_URL}/api/dumps/`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
setDumpsState({ status: "loaded", dumps: body.data });
} catch (err) {
setDumpsState({
status: "error",
error: err instanceof Error ? err.message : "Failed to load dumps",
});
setDumpsState({ status: "error", error: err instanceof Error ? err.message : "Failed to load" });
}
})();
}, []);
if (dumpsState.status === "loading") {
return (
<main id="content">
<div className="loading">Loading dumps...</div>
</main>
);
}
const loading = dumpsState.status === "loading";
const error = dumpsState.status === "error" ? dumpsState.error : null;
const dumps = dumpsState.status === "loaded" ? dumpsState.dumps : [];
const restIds = new Set(dumps.map((d) => d.id));
const combined = [...recentDumps.filter((d) => !restIds.has(d.id)), ...dumps]
.filter((d) => !deletedDumpIds.has(d.id) && d.id !== justDeletedId);
if (dumpsState.status === "error") {
return (
<main id="content">
<div className="error-container">
<h2>Error</h2>
<p>{dumpsState.error}</p>
<button type="button" onClick={() => globalThis.location.reload()}>
Retry
</button>
</div>
</main>
);
}
const sortedDumps = [...combined].sort(
sort === "hot"
? (a, b) => hotScore(b) - hotScore(a)
: (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
const { dumps } = dumpsState;
const presenceRow = (
<div className="index-presence">
{onlineUsers.map((u) => (
<Link key={u.userId} to={`/users/${u.username}`} title={u.username} className="index-presence-avatar">
<Avatar userId={u.userId} username={u.username} hasAvatar={u.hasAvatar} size={32} />
</Link>
))}
</div>
);
const sortButtons = !loading && !error && combined.length > 0 && (
<div className="feed-sort">
<button className={`feed-sort-btn${sort === "hot" ? " active" : ""}`} onClick={() => setSort("hot")}>Hot</button>
<button className={`feed-sort-btn${sort === "new" ? " active" : ""}`} onClick={() => setSort("new")}>New</button>
</div>
);
return (
<main id="content">
<h1>🚚 Dumps</h1>
<div className="index-page">
<AppHeader centerSlot={
<div className="header-center-slot">
{presenceRow}
{sortButtons}
</div>
} />
<p>Welcome, {user?.username ?? "guest"}!</p>
{/* Shown only on narrow viewports */}
<div className="index-below-header">
{sortButtons}
{presenceRow}
</div>
{user &&
<button type="button" onClick={handleCreateDump}>New dump</button>}
{loading && <p className="index-status">Loading</p>}
{error && <p className="index-status index-status--error">{error}</p>}
<p>Click on a dump below to participate.</p>
{!loading && !error && combined.length === 0 && (
<p className="index-status">No dumps yet. Be the first!</p>
)}
{dumps.length === 0
? <p className="empty-state">No dumps available yet.</p>
: (
<ul>
{dumps.map((dump) => (
<li key={dump.id}>
<Link to={`/dumps/${dump.id}`} className="dump">
{dump.title}
</Link>
</li>
{!loading && !error && combined.length > 0 && (
<>
<ul className="dump-feed">
{sortedDumps.map((dump) => (
<DumpCard
key={dump.id}
dump={dump}
voteCount={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
canVote={!!user}
castVote={castVote}
removeVote={removeVote}
/>
))}
</ul>
)}
{user
? (
<form>
<button type="button" onClick={handleLogout}>Logout</button>
</form>
)
: (
<form>
<button type="button" onClick={handleRegister}>Register</button>
<button type="button" onClick={handleLogin}>Log in</button>
</form>
)}
</main>
</>
)}
</div>
);
}