Files
dark-lord/app/dashboard/page.tsx
drjones 78a071ba02 Harden onion boot flow and deepen site surfaces
Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation.

Made-with: Cursor
2026-04-07 21:35:52 -07:00

204 lines
8.7 KiB
TypeScript

"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Navbar from "@/components/Navbar";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { computeUserActivityStats } from "@/lib/userActivityStats";
import { loadVendorApplications, type VendorApplication } from "@/lib/vendorApplicationState";
export default function DashboardPage() {
const router = useRouter();
const { user, hydrated, signOut, saveDisplayName } = useAccount();
const { isConnected, address, luxCredits, usdStoreCredit, vault } = useWallet();
const [dn, setDn] = useState("");
const [profileMsg, setProfileMsg] = useState<string | null>(null);
const [vendorApps, setVendorApps] = useState<VendorApplication[]>([]);
useEffect(() => {
if (hydrated && !user) router.replace("/sign-in");
}, [hydrated, user, router]);
useEffect(() => {
if (user) setDn(user.displayName);
}, [user]);
useEffect(() => {
if (!user) return;
setVendorApps(loadVendorApplications().filter((a) => a.accountUsername === user.username));
}, [user]);
const stats = useMemo(() => {
if (!user) return null;
return computeUserActivityStats(user.username, user.displayName);
}, [user]);
const onProfile = (e: FormEvent) => {
e.preventDefault();
setProfileMsg(null);
const res = saveDisplayName(dn);
if (!res.ok) setProfileMsg(res.error);
else setProfileMsg("Display name saved — use it when posting for accurate stats.");
};
if (!hydrated || !user) {
return (
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
);
}
const since = new Date(user.createdAt).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
return (
<div className="min-h-screen bg-[#0a0a0a] text-zinc-200">
<Navbar />
<div className="mx-auto max-w-3xl px-4 py-28">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-green/80">personal relay</p>
<h1 className="mt-2 font-orbitron text-3xl font-bold md:text-4xl">Dashboard</h1>
<p className="mt-2 font-mono text-sm text-zinc-500">
Signed in as <span className="text-neon-cyan">{user.displayName}</span>{" "}
<span className="text-zinc-600">(@{user.username})</span>
</p>
</div>
<button
type="button"
onClick={() => {
signOut();
router.push("/sign-in");
}}
className="rounded-xl border border-white/15 px-4 py-2 font-mono text-xs uppercase tracking-wider text-zinc-400 hover:border-red-500/50 hover:text-red-300"
>
Sign out
</button>
</div>
<div className="mt-10 grid gap-6 md:grid-cols-2">
<section className="glass rounded-2xl border border-white/10 p-6">
<h2 className="font-orbitron text-lg font-bold text-neon-cyan">Profile</h2>
<p className="mt-2 text-sm text-zinc-500">Member since {since}</p>
<form onSubmit={onProfile} className="mt-4 space-y-3">
<label className="block text-xs text-zinc-400">Display name (forum / listings)</label>
<input
value={dn}
onChange={(e) => setDn(e.target.value)}
className="glass w-full rounded-lg border border-white/10 bg-transparent px-3 py-2 font-mono text-sm"
/>
{profileMsg ? <p className="text-xs text-neon-green/90">{profileMsg}</p> : null}
<button type="submit" className="rounded-lg bg-white/10 px-4 py-2 text-xs font-bold uppercase hover:bg-white/15">
Save
</button>
</form>
</section>
<section className="glass rounded-2xl border border-neon-cyan/20 p-6">
<h2 className="font-orbitron text-lg font-bold text-neon-green">Wallet (sim)</h2>
<p className="mt-3 font-mono text-sm">
Status: {isConnected ? <span className="text-neon-green">connected</span> : <span className="text-zinc-500">not connected</span>}
</p>
{address ? (
<p className="mt-1 font-mono text-xs text-zinc-500">
{address.slice(0, 10)}{address.slice(-6)}
</p>
) : null}
<ul className="mt-4 space-y-2 font-mono text-sm">
<li className="flex justify-between">
<span className="text-zinc-500">LUX credits</span>
<span className="text-neon-green">{luxCredits.toLocaleString()}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">USD store credit</span>
<span className="text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">Vault keys</span>
<span>{vault.keys.length}</span>
</li>
</ul>
<Link
href="/checkout"
className="mt-4 inline-block text-xs font-bold uppercase tracking-wider text-neon-cyan hover:underline"
>
Checkout
</Link>
</section>
</div>
<section className="glass mt-6 rounded-2xl border border-amber-900/30 p-6">
<h2 className="font-orbitron text-lg font-bold text-amber-200/90">Vendor stall (sim)</h2>
<p className="mt-2 text-sm text-zinc-500">
Queue a fiction application stored in this browser only.{" "}
<Link href="/vendor/apply" className="text-amber-400 hover:text-amber-300 hover:underline">
Apply to vend
</Link>
</p>
{vendorApps.length ? (
<ul className="mt-4 space-y-3 font-mono text-xs text-zinc-400">
{vendorApps.map((a) => (
<li key={a.id} className="rounded-lg border border-white/10 bg-white/5 px-3 py-2">
<span className="text-amber-500/80">{a.desiredHandle}</span> · {a.stallName.slice(0, 48)}
{a.stallName.length > 48 ? "…" : ""}
<span className="mt-1 block text-[10px] text-zinc-600">
{new Date(a.ts).toLocaleString()} · ref <code className="text-zinc-500">{a.id.slice(0, 8)}</code>
</span>
</li>
))}
</ul>
) : (
<p className="mt-3 text-xs text-zinc-600">No applications logged for @{user.username} yet.</p>
)}
</section>
{stats ? (
<section className="glass mt-6 rounded-2xl border border-white/10 p-6">
<h2 className="font-orbitron text-lg font-bold">Activity (matched by display name or handle)</h2>
<ul className="mt-4 grid gap-3 sm:grid-cols-2 font-mono text-sm">
<li className="rounded-lg bg-white/5 px-4 py-3">
Forum threads <span className="float-right text-neon-cyan">{stats.forumThreads}</span>
</li>
<li className="rounded-lg bg-white/5 px-4 py-3">
Forum replies <span className="float-right text-neon-cyan">{stats.forumReplies}</span>
</li>
<li className="rounded-lg bg-white/5 px-4 py-3">
Exchange listings <span className="float-right text-neon-cyan">{stats.exchangeListings}</span>
</li>
<li className="rounded-lg bg-white/5 px-4 py-3">
Barter posts <span className="float-right text-neon-cyan">{stats.barterListings}</span>
</li>
</ul>
<div className="mt-6 flex flex-wrap gap-3 text-xs font-medium">
<Link href="/forum/submit" className="text-[#ff9a3c] hover:underline">
New forum post
</Link>
<Link href="/exchange" className="text-[#d4a574] hover:underline">
Exchange
</Link>
<Link href="/barter" className="text-[#7cfc9a] hover:underline">
Barter
</Link>
<Link href="/vault" className="text-zinc-400 hover:underline">
Vault
</Link>
</div>
</section>
) : null}
<p className="mt-10 text-center text-xs text-zinc-600">
<Link href="/" className="hover:text-zinc-400">
Hub
</Link>
</p>
</div>
</div>
);
}