vibe coded v1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user