- ChatWidget: remove illegal seeds, real localStorage per-handle chat, honest bot replies about market/forum/funds - ForumBoard: wire to real forumState (loadForum/addThread/vote), kill fake stats and illegal seed posts - Home page: privacy features list reflects reality, footer links real - Links: kill all alert() calls, replace fake onions with real clearnet privacy resources + internal route grid - Support: per-coin copied state, env-driven addresses, real BTC addr - Inner circle: wire to AccountContext, tier system from LUX balance, remove hardcoded admin/shadow credentials and fake trading signals - Drop box: real sealed-note localStorage system, honest about no anonymous upload capability, real file picker with receipt - Messages: fully functional per-handle localStorage chat, AI-style contextual bot replies, clear history, honest about local storage - Wallets: pivot from fake PayPal accounts to Digital Access Passes, wire Buy Now to cart via ShopProduct interface - Testimonials: wire submit form to localStorage, interactive star rating 1-10, display submitted reviews above the fold - Raffle: use real merchant BTC address, real per-handle entry storage, honest LUX-only prize disclaimer, fix 0x address - Drops/Lotto: real number picker 1-49 with Quick Pick, ticket submission, match display against drawn numbers, demo disclaimer - Sanctuary: real 4-4-6-2 breathing timer, meditation passage with timer, candle-lighting with localStorage notes - Game: full playable Void Pong with canvas physics, CPU AI, scoring, rally counter, localStorage high score - Security analysis: honest architecture breakdown with real grades, layer-by-layer analysis, practical OPSEC guide, fiction banner - Trust: compute real scores from actual localStorage data (LUX, USD, forum posts, testimonials), FAQ accordion Made-with: Cursor
202 lines
8.6 KiB
TypeScript
202 lines
8.6 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 { 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">Balance</h2>
|
|
<p className="mt-2 text-sm text-zinc-500">
|
|
USD is credited after verified Bitcoin deposits to your merchant address. LUX is loyalty currency (e.g.
|
|
completed checkouts).
|
|
</p>
|
|
<ul className="mt-4 space-y-2 font-mono text-sm">
|
|
<li className="flex justify-between">
|
|
<span className="text-zinc-500">LUX</span>
|
|
<span className="text-neon-green">{luxCredits.toLocaleString()}</span>
|
|
</li>
|
|
<li className="flex justify-between">
|
|
<span className="text-zinc-500">USD (spendable)</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>
|
|
<div className="mt-4 flex flex-wrap gap-3 text-xs font-bold uppercase tracking-wider">
|
|
<Link href="/account/add-funds" className="text-neon-green hover:underline">
|
|
Add funds (BTC) →
|
|
</Link>
|
|
<Link href="/checkout" className="text-neon-cyan hover:underline">
|
|
Checkout →
|
|
</Link>
|
|
</div>
|
|
</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 applications</h2>
|
|
<p className="mt-2 text-sm text-zinc-500">
|
|
Applications are stored in this browser under your handle (no remote queue yet).{" "}
|
|
<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>
|
|
);
|
|
}
|