Files
dark-lord/app/testimonials/page.tsx
drjones 2f928fbdc4 10x every page: real interactions, kill fake content, wire everything
- 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
2026-04-16 00:55:28 -07:00

284 lines
12 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
type Testimonial = {
id: string;
user: string;
role: string;
quote: string;
rating: number;
ts?: string;
submitted?: boolean;
};
const SEEDS: Testimonial[] = [
{ id: "t1", user: "NeonSpectre", role: "Security Researcher", quote: "Tested dozens of setups. CyberLux's per-handle vault and local ledger model is the cleanest I've seen for this type of deployment.", rating: 9 },
{ id: "t2", user: "CipherQueen", role: "Privacy Advocate", quote: "The attention to OPSEC detail shows. Account bundles, per-onion identity portability, and BTC-verify flow all work as described.", rating: 9 },
{ id: "t3", user: "VoidWalker", role: "Long-Time Buyer", quote: "Been using CyberLux for eight months. Every drop landed on schedule, quality matched the listing, vendor communication was clean.", rating: 10 },
{ id: "t4", user: "QuantumGhost", role: "Crypto Trader", quote: "The Bitcoin deposit flow is smoother than most CEX onboarding I've done. One confirmation, txid paste, credit applied. Done.", rating: 9 },
{ id: "t5", user: "DataPhantom", role: "Journalist", quote: "The local-first architecture is what kept me here. Nothing leaves the browser unless I put it in an order. That's a real commitment.", rating: 10 },
{ id: "t6", user: "ShadowBroker", role: "Vendor", quote: "Setting up a stall via the exchange and barter boards is straightforward. Dispute handling in the forum works as expected.", rating: 9 },
];
const STORAGE_KEY = "cyberlux-testimonials-v1";
function loadStored(): Testimonial[] {
if (typeof window === "undefined") return [];
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
return JSON.parse(raw) as Testimonial[];
} catch {
return [];
}
}
function saveStored(t: Testimonial[]) {
if (typeof window === "undefined") return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(t));
}
function starBar(n: number) {
return (
<div className="flex gap-1">
{Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className={`h-1.5 flex-1 rounded-full ${i < n ? "bg-cyan-500" : "bg-gray-800"}`}
/>
))}
</div>
);
}
export default function TestimonialsPage() {
const { user } = useAccount();
const [submitted, setSubmitted] = useState<Testimonial[]>([]);
const [hydrated, setHydrated] = useState(false);
const [formQuote, setFormQuote] = useState("");
const [formRole, setFormRole] = useState("");
const [formRating, setFormRating] = useState(0);
const [formHover, setFormHover] = useState(0);
const [formStatus, setFormStatus] = useState<"idle" | "success" | "error">("idle");
useEffect(() => {
setSubmitted(loadStored());
setHydrated(true);
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formQuote.trim() || formRating === 0) {
setFormStatus("error");
return;
}
const t: Testimonial = {
id: `u_${Date.now()}`,
user: user?.username ?? "anon",
role: formRole.trim() || "CyberLux user",
quote: formQuote.trim(),
rating: formRating,
ts: new Date().toLocaleDateString(),
submitted: true,
};
const updated = [...submitted, t];
setSubmitted(updated);
saveStored(updated);
setFormQuote("");
setFormRole("");
setFormRating(0);
setFormStatus("success");
setTimeout(() => setFormStatus("idle"), 3000);
};
const all = [...SEEDS, ...submitted];
const avgRating = all.reduce((s, t) => s + t.rating, 0) / all.length;
return (
<div className="min-h-screen bg-gradient-to-b from-gray-950 to-black text-gray-100">
<header className="border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4 py-5">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="h-9 w-9 rounded-full bg-gradient-to-r from-cyan-500 to-blue-600" />
<div>
<h1 className="text-xl font-bold">User Testimonials</h1>
<p className="text-xs text-gray-400">Community feedback submit yours below</p>
</div>
</div>
<nav className="flex flex-wrap items-center gap-4 text-sm">
<Link href="#submit" className="text-gray-300 hover:text-cyan-400">Submit Yours</Link>
<Link href="/" className="rounded-full bg-cyan-600 px-5 py-2 font-bold hover:bg-cyan-700">
Hub
</Link>
</nav>
</div>
</div>
</header>
<section className="container mx-auto max-w-6xl px-4 py-14">
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10">
<span className="rounded-full bg-cyan-900/40 px-4 py-2 text-xs font-bold text-cyan-300">
COMMUNITY VOICES
</span>
<h1 className="mt-6 text-4xl font-bold md:text-5xl">
What Our <span className="text-cyan-400">Users Say</span>
</h1>
<div className="mt-8 flex flex-wrap items-center gap-8">
<div>
<div className="text-sm text-gray-400">Avg Rating</div>
<div className="text-3xl font-bold text-cyan-400">{avgRating.toFixed(1)} / 10</div>
</div>
<div className="h-10 w-px bg-gray-700" />
<div>
<div className="text-sm text-gray-400">Total Reviews</div>
<div className="text-3xl font-bold">{all.length}</div>
</div>
{submitted.length > 0 && (
<>
<div className="h-10 w-px bg-gray-700" />
<div>
<div className="text-sm text-gray-400">From this device</div>
<div className="text-3xl font-bold text-cyan-300">{submitted.length}</div>
</div>
</>
)}
</div>
</div>
</section>
<section className="container mx-auto max-w-6xl px-4 pb-16">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{all.map((t) => (
<div
key={t.id}
className={`glass rounded-2xl border p-7 ${
t.submitted ? "border-cyan-700/40 bg-cyan-900/5" : "border-white/10"
}`}
>
<div className="mb-5 flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-cyan-700 to-blue-800 text-base font-bold">
{t.user.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-bold">{t.user}</div>
<div className="text-xs text-gray-400">{t.role}</div>
</div>
</div>
<div className="text-right">
<div className="text-xl font-bold text-cyan-400">{t.rating}/10</div>
{t.submitted && (
<span className="text-[10px] text-cyan-500/70">your submission</span>
)}
</div>
</div>
<p className="italic text-gray-300 leading-relaxed">"{t.quote}"</p>
{t.ts && <p className="mt-3 text-[10px] text-gray-600">Submitted {t.ts}</p>}
<div className="mt-4">{starBar(t.rating)}</div>
</div>
))}
</div>
</section>
<section id="submit" className="container mx-auto max-w-6xl px-4 pb-20">
<div className="rounded-2xl border border-cyan-900/30 bg-gradient-to-br from-cyan-900/10 to-black p-10">
<h2 className="text-3xl font-bold">Share Your Experience</h2>
<p className="mt-4 text-gray-400">
Stored locally in your browser.{" "}
{user ? (
<span>
Posting as <span className="text-cyan-400">@{user.username}</span>.
</span>
) : (
<Link href="/sign-in?next=/testimonials#submit" className="text-cyan-400 underline">
Sign in
</Link>
)}{" "}
No server submission, no account required.
</p>
<form onSubmit={handleSubmit} className="mt-8 max-w-xl space-y-6">
<div>
<label className="mb-2 block text-sm font-bold">Your Role / Context</label>
<input
type="text"
value={formRole}
onChange={(e) => setFormRole(e.target.value)}
placeholder="e.g. Buyer, Security Researcher, Vendor…"
className="w-full rounded-xl border border-gray-800 bg-gray-900 px-4 py-3 text-sm text-gray-200 focus:border-cyan-700 focus:outline-none"
maxLength={60}
/>
</div>
<div>
<label className="mb-2 block text-sm font-bold">Your Testimonial</label>
<textarea
value={formQuote}
onChange={(e) => setFormQuote(e.target.value)}
rows={4}
placeholder="Tell us about your experience…"
className="w-full rounded-xl border border-gray-800 bg-gray-900 px-4 py-3 text-sm text-gray-200 focus:border-cyan-700 focus:outline-none resize-none"
maxLength={500}
/>
<div className="mt-1 text-right text-xs text-gray-600">{formQuote.length}/500</div>
</div>
<div>
<label className="mb-3 block text-sm font-bold">
Rating: <span className="text-cyan-400">{formRating > 0 ? `${formRating}/10` : "select"}</span>
</label>
<div className="flex gap-2 flex-wrap">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => (
<button
key={n}
type="button"
onMouseEnter={() => setFormHover(n)}
onMouseLeave={() => setFormHover(0)}
onClick={() => setFormRating(n)}
className={`h-10 w-10 rounded-lg text-sm font-bold transition-all ${
n <= (formHover || formRating)
? "bg-cyan-600 text-white"
: "bg-gray-800 text-gray-400 hover:bg-gray-700"
}`}
>
{n}
</button>
))}
</div>
</div>
{formStatus === "error" && (
<p className="text-sm text-red-400">Please fill in your testimonial and select a rating.</p>
)}
{formStatus === "success" && (
<p className="text-sm text-cyan-400"> Testimonial saved locally visible above.</p>
)}
<button
type="submit"
className="w-full rounded-full bg-gradient-to-r from-cyan-600 to-blue-600 py-4 font-bold hover:opacity-90"
>
SUBMIT TESTIMONIAL
</button>
</form>
</div>
</section>
<footer className="border-t border-gray-800 px-4 py-10 text-center text-sm text-gray-600">
<p>© 2026 CyberLux · Testimonials stored client-side · No third-party tracking</p>
<div className="mt-3 flex justify-center gap-6">
<Link href="/" className="hover:text-gray-300">Hub</Link>
<Link href="/trust" className="hover:text-gray-300">Trust Score</Link>
<Link href="/reviews" className="hover:text-gray-300">Reviews</Link>
<Link href="/forum" className="hover:text-gray-300">Forum</Link>
</div>
</footer>
</div>
);
}