- 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
247 lines
9.7 KiB
TypeScript
247 lines
9.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import Link from "next/link";
|
||
import { useAccount } from "@/contexts/AccountContext";
|
||
|
||
const NUMBERS_COUNT = 6;
|
||
const TICKET_KEY = "cyberlux-lotto-tickets-v1";
|
||
const DRAW_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||
const DRAW_EPOCH = new Date("2026-04-14T12:00:00Z").getTime();
|
||
|
||
function getNextDraw() {
|
||
const now = Date.now();
|
||
const elapsed = now - DRAW_EPOCH;
|
||
const cycles = Math.floor(elapsed / DRAW_INTERVAL_MS);
|
||
return DRAW_EPOCH + (cycles + 1) * DRAW_INTERVAL_MS;
|
||
}
|
||
|
||
function formatRemaining(ms: number) {
|
||
if (ms <= 0) return "DRAWING SOON";
|
||
const h = Math.floor(ms / 3600000);
|
||
const m = Math.floor((ms % 3600000) / 60000);
|
||
const s = Math.floor((ms % 60000) / 1000);
|
||
return `${h.toString().padStart(2, "0")}h ${m.toString().padStart(2, "0")}m ${s.toString().padStart(2, "0")}s`;
|
||
}
|
||
|
||
type Ticket = { nums: number[]; handle: string; ts: number };
|
||
|
||
function loadTickets(): Ticket[] {
|
||
if (typeof window === "undefined") return [];
|
||
try { return JSON.parse(localStorage.getItem(TICKET_KEY) ?? "[]") as Ticket[]; }
|
||
catch { return []; }
|
||
}
|
||
|
||
function saveTickets(t: Ticket[]) {
|
||
if (typeof window === "undefined") return;
|
||
localStorage.setItem(TICKET_KEY, JSON.stringify(t.slice(-50)));
|
||
}
|
||
|
||
export default function LotteryPage() {
|
||
const { user } = useAccount();
|
||
const [numbers, setNumbers] = useState<number[]>([]);
|
||
const [isDrawing, setIsDrawing] = useState(false);
|
||
const [myTickets, setMyTickets] = useState<Ticket[]>([]);
|
||
const [remaining, setRemaining] = useState(0);
|
||
const [picked, setPicked] = useState<number[]>([]);
|
||
const [disclaimer, setDisclaimer] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setMyTickets(loadTickets());
|
||
const tick = () => setRemaining(getNextDraw() - Date.now());
|
||
tick();
|
||
const id = setInterval(tick, 1000);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
|
||
const togglePick = (n: number) => {
|
||
if (picked.includes(n)) {
|
||
setPicked((p) => p.filter((x) => x !== n));
|
||
} else if (picked.length < NUMBERS_COUNT) {
|
||
setPicked((p) => [...p, n]);
|
||
}
|
||
};
|
||
|
||
const quickPick = () => {
|
||
const pool = Array.from({ length: 49 }, (_, i) => i + 1);
|
||
const result: number[] = [];
|
||
while (result.length < NUMBERS_COUNT) {
|
||
const idx = Math.floor(Math.random() * pool.length);
|
||
result.push(pool.splice(idx, 1)[0]!);
|
||
}
|
||
setPicked(result.sort((a, b) => a - b));
|
||
};
|
||
|
||
const submitTicket = useCallback(() => {
|
||
if (picked.length !== NUMBERS_COUNT) return;
|
||
const ticket: Ticket = {
|
||
nums: [...picked].sort((a, b) => a - b),
|
||
handle: user?.username ?? "anon",
|
||
ts: Date.now(),
|
||
};
|
||
const updated = [...myTickets, ticket];
|
||
setMyTickets(updated);
|
||
saveTickets(updated);
|
||
setPicked([]);
|
||
}, [picked, myTickets, user]);
|
||
|
||
const drawNumbers = useCallback(() => {
|
||
if (!disclaimer) { setDisclaimer(true); return; }
|
||
setIsDrawing(true);
|
||
setNumbers([]);
|
||
let count = 0;
|
||
const used = new Set<number>();
|
||
const interval = setInterval(() => {
|
||
let n: number;
|
||
do { n = Math.floor(Math.random() * 49) + 1; } while (used.has(n));
|
||
used.add(n);
|
||
setNumbers((prev) => [...prev, n]);
|
||
count++;
|
||
if (count >= NUMBERS_COUNT) {
|
||
clearInterval(interval);
|
||
setIsDrawing(false);
|
||
}
|
||
}, 400);
|
||
}, [disclaimer]);
|
||
|
||
const drawn = numbers.length === NUMBERS_COUNT;
|
||
const myMatches = drawn
|
||
? myTickets.map((t) => ({ ticket: t, matches: t.nums.filter((n) => numbers.includes(n)).length }))
|
||
: [];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#050505] text-[#ff00ff] font-mono p-6 flex flex-col items-center">
|
||
<div className="w-full max-w-2xl">
|
||
<div className="border-2 border-[#ff00ff]/30 bg-[#111] p-10 shadow-[0_0_50px_rgba(255,0,255,0.08)] mb-6">
|
||
<div className="mb-3 text-center text-[10px] uppercase tracking-widest text-[#ff00ff]/40">
|
||
Demo lotto — prizes are LUX credits · no real-money payouts
|
||
</div>
|
||
<h1 className="text-4xl font-black text-center mb-2 tracking-widest uppercase italic">Shadow Lotto</h1>
|
||
<p className="text-center text-[#ff00ff]/50 text-xs mb-8">
|
||
Pick 6 numbers (1–49) or Quick Pick. Daily demo draw — matches earn LUX credits.
|
||
</p>
|
||
|
||
<div className="grid grid-cols-2 gap-6 mb-8 text-center">
|
||
<div className="border border-[#ff00ff]/20 p-4">
|
||
<div className="text-xs uppercase opacity-50 mb-1">Next Draw</div>
|
||
<div className="text-lg font-bold text-white">{formatRemaining(remaining)}</div>
|
||
</div>
|
||
<div className="border border-[#ff00ff]/20 p-4">
|
||
<div className="text-xs uppercase opacity-50 mb-1">Your Tickets</div>
|
||
<div className="text-lg font-bold text-white">{myTickets.length}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Number picker */}
|
||
<div className="mb-6">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<span className="text-xs uppercase tracking-wider text-[#ff00ff]/70">
|
||
Pick {NUMBERS_COUNT} numbers · selected: {picked.length}/{NUMBERS_COUNT}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={quickPick}
|
||
className="border border-[#ff00ff]/40 px-3 py-1 text-xs font-bold uppercase hover:bg-[#ff00ff]/10 transition-all"
|
||
>
|
||
Quick Pick
|
||
</button>
|
||
</div>
|
||
<div className="grid grid-cols-7 gap-1.5">
|
||
{Array.from({ length: 49 }, (_, i) => i + 1).map((n) => {
|
||
const isSelected = picked.includes(n);
|
||
const isDrawn = numbers.includes(n);
|
||
return (
|
||
<button
|
||
key={n}
|
||
type="button"
|
||
onClick={() => togglePick(n)}
|
||
className={`h-9 w-full border text-xs font-bold transition-all ${
|
||
isDrawn && drawn
|
||
? "border-[#ff00ff] bg-[#ff00ff]/30 text-white"
|
||
: isSelected
|
||
? "border-[#ff00ff] bg-[#ff00ff]/20 text-[#ff00ff]"
|
||
: "border-[#ff00ff]/15 bg-transparent text-[#ff00ff]/50 hover:border-[#ff00ff]/40"
|
||
}`}
|
||
>
|
||
{n}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-3 mb-6">
|
||
<button
|
||
type="button"
|
||
onClick={submitTicket}
|
||
disabled={picked.length !== NUMBERS_COUNT}
|
||
className="flex-1 border-2 border-[#ff00ff] py-3 text-sm font-black uppercase transition-all hover:bg-[#ff00ff]/15 disabled:opacity-30"
|
||
>
|
||
Lock In Ticket ({picked.join("-") || "select 6"})
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={drawNumbers}
|
||
disabled={isDrawing}
|
||
className={`flex-1 py-3 text-sm font-black uppercase transition-all ${
|
||
isDrawing ? "bg-[#333] text-[#666] cursor-not-allowed" : "bg-[#ff00ff] text-black hover:bg-[#cc00cc]"
|
||
}`}
|
||
>
|
||
{isDrawing ? "Drawing…" : "Run Demo Draw"}
|
||
</button>
|
||
</div>
|
||
|
||
{disclaimer && !drawn && (
|
||
<div className="mb-4 border border-amber-500/50 bg-amber-950/30 p-4 text-xs text-amber-200 text-center">
|
||
This is a demo draw — no real prizes are awarded. LUX credit prizes are in-app only.
|
||
<button type="button" onClick={drawNumbers} className="ml-3 underline font-bold">
|
||
OK, Draw Anyway
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Draw result */}
|
||
{numbers.length > 0 && (
|
||
<div className="border border-[#ff00ff]/30 bg-[#ff00ff]/5 p-6">
|
||
<div className="mb-3 text-xs uppercase text-[#ff00ff]/60">Draw Result</div>
|
||
<div className="flex justify-center gap-3 flex-wrap">
|
||
{numbers.map((n, i) => (
|
||
<div
|
||
key={i}
|
||
className={`flex h-12 w-12 items-center justify-center border-2 font-black text-lg ${
|
||
isDrawing
|
||
? "border-[#ff00ff]/40 text-[#ff00ff]/60 animate-pulse"
|
||
: "border-[#ff00ff] text-white bg-[#ff00ff]/20"
|
||
}`}
|
||
>
|
||
{n}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{drawn && myMatches.length > 0 && (
|
||
<div className="mt-5 space-y-2">
|
||
<div className="text-xs uppercase text-[#ff00ff]/60">Your Ticket Results</div>
|
||
{myMatches.map((m, i) => (
|
||
<div key={i} className="flex items-center justify-between text-xs">
|
||
<span className="text-[#ff00ff]/60">{m.ticket.nums.join("-")}</span>
|
||
<span className={m.matches >= 3 ? "text-[#ff00ff] font-bold" : "text-white/40"}>
|
||
{m.matches} match{m.matches !== 1 ? "es" : ""}
|
||
{m.matches >= 6 ? " — JACKPOT (LUX prize)" : m.matches >= 4 ? " — LUX reward" : ""}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-center text-[10px] text-[#ff00ff]/25 leading-relaxed">
|
||
Shadow Lotto is a demonstration game. No real-money gambling. LUX credits are in-app only.{" "}
|
||
<Link href="/inner-circle" className="text-[#ff00ff]/40 underline hover:text-[#ff00ff]/60">Inner Circle →</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|