Files
dark-lord/app/dashboard/page.tsx
drjones 348e55b63e Add proper deposit desk: multi-coin, clear custody model, DepositWidget
- /account/add-funds: full rebuild as Deposit Desk
  - 3-step how-it-works explainer: you send crypto → we hold it →
    you shop with USD credit → we pay vendors with your crypto
  - Coin selector: BTC (auto-verify), ETH (manual review), XMR (manual review)
  - Each coin shows deposit address from env var with copy button
  - BTC: immediate on-chain verify via /api/btc/verify (mempool.space)
  - ETH/XMR: submit txhash to /api/pending-deposit for manual review
  - Pending deposit history stored in localStorage with status
  - Important notes block explaining custody, no-withdrawal, confirmations
  - Back-links to checkout, wallets, dashboard

- /api/pending-deposit: new route to log ETH/XMR manual review requests
  (logs to console, ready to wire to DB/webhook)

- DepositWidget component: reusable compact + full variants
  - compact: balance + "Deposit crypto →" CTA (used in checkout)
  - full: balance + how-it-works summary (used in dashboard)

- Dashboard: replace balance section with DepositWidget + stats
- Checkout: add compact DepositWidget above CheckoutFlow, fix back link
  from "Sanctuary" → "Market", update feature cards to be accurate

- .env.example: document all three coin address env vars + optional
  payment processor URL

- .env.example: add NEXT_PUBLIC_ETH_ADDRESS, NEXT_PUBLIC_XMR_ADDRESS

Made-with: Cursor
2026-04-16 01:03:14 -07:00

191 lines
8.0 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 DepositWidget from "@/components/DepositWidget";
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>
<DepositWidget />
<div className="mt-3 glass rounded-xl border border-white/10 p-4 font-mono text-xs">
<div className="flex justify-between text-zinc-500">
<span>Vault keys</span>
<span className="text-zinc-300">{vault.keys.length}</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>LUX credits</span>
<span className="text-neon-green">{luxCredits.toLocaleString()}</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>USD balance</span>
<span className="text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
</div>
</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>
);
}