- 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
284 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|