Files
dark-lord/app/raffle/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

216 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { useAccount } from "@/contexts/AccountContext";
const PRIZES = [
{ place: "1st", prize: "500 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "2nd", prize: "250 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "3rd", prize: "100 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "Runner-up ×5", prize: "Handler Pass", sub: "Digital access pass upgrade." },
];
const DRAW_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
const DRAW_EPOCH = new Date("2026-04-14T00: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 "00:00:00";
const totalSec = Math.floor(ms / 1000);
const d = Math.floor(totalSec / 86400);
const h = Math.floor((totalSec % 86400) / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
if (d > 0) return `${d}d ${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
}
const TICKET_PRICE_BTC = "0.0001";
const TICKET_ENTRY_KEY = "cyberlux-raffle-entries-v1";
type Entry = { handle: string; txid: string; ts: string };
function loadEntries(): Entry[] {
if (typeof window === "undefined") return [];
try {
return JSON.parse(localStorage.getItem(TICKET_ENTRY_KEY) ?? "[]") as Entry[];
} catch { return []; }
}
function saveEntries(e: Entry[]) {
if (typeof window === "undefined") return;
localStorage.setItem(TICKET_ENTRY_KEY, JSON.stringify(e));
}
export default function RafflePage() {
const { user } = useAccount();
const [remaining, setRemaining] = useState(0);
const [entries, setEntries] = useState<Entry[]>([]);
const [txid, setTxid] = useState("");
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const [errMsg, setErrMsg] = useState("");
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
useEffect(() => {
setEntries(loadEntries());
const tick = () => setRemaining(getNextDraw() - Date.now());
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
const handleEnter = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const t = txid.trim();
if (!t || t.length < 20) {
setErrMsg("Enter a valid Bitcoin transaction ID.");
setStatus("error");
return;
}
if (entries.some((x) => x.txid === t)) {
setErrMsg("This txid has already been submitted.");
setStatus("error");
return;
}
const entry: Entry = {
handle: user?.username ?? "anon",
txid: t,
ts: new Date().toISOString(),
};
const updated = [...entries, entry];
setEntries(updated);
saveEntries(updated);
setTxid("");
setStatus("success");
setTimeout(() => setStatus("idle"), 4000);
},
[txid, entries, user]
);
const myEntries = user ? entries.filter((e) => e.handle === user.username) : [];
return (
<ThemedLayout theme="terminal" title="Shadow Raffle">
<div className="mx-auto max-w-4xl px-4 pt-12 pb-20 font-mono text-white">
<div className="border-4 border-white bg-[#000080] p-10 shadow-[10px_10px_0_rgba(255,255,255,0.15)]">
<div className="mb-2 text-center text-[10px] uppercase tracking-widest text-white/50">
Prize pool is in LUX credits not BTC. Entry fee covers onchain raffle infrastructure.
</div>
<h1 className="mb-10 text-center text-4xl font-black uppercase tracking-tighter">
Weekly Shadow Raffle
</h1>
<div className="mb-10 grid grid-cols-1 gap-6 sm:grid-cols-3">
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Next Draw</div>
<div className="text-2xl font-black tabular-nums text-white">
{formatRemaining(remaining)}
</div>
</div>
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Ticket Price</div>
<div className="text-2xl font-black">{TICKET_PRICE_BTC} BTC</div>
</div>
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Entries This Round</div>
<div className="text-2xl font-black">{entries.length}</div>
</div>
</div>
<div className="mb-10">
<h2 className="mb-4 border-b-2 border-white pb-2 text-xl font-black uppercase">Prize Breakdown</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{PRIZES.map((p) => (
<div key={p.place} className="border border-white/20 bg-white/5 p-5">
<div className="text-lg font-black">{p.place}: {p.prize}</div>
<p className="mt-1 text-xs text-white/60">{p.sub}</p>
</div>
))}
</div>
</div>
<div className="border-4 border-dashed border-white/40 p-8">
<h3 className="mb-2 text-lg font-black uppercase">Enter Your Ticket</h3>
<p className="mb-4 text-xs text-white/70 leading-relaxed">
Send exactly {TICKET_PRICE_BTC} BTC to the address below. After at least 1 confirmation, paste your
transaction ID here. Your handle is registered as an entrant. Draw happens at the countdown above.
</p>
{btcReady ? (
<div className="mb-5 break-all border-2 border-white bg-black px-4 py-3 font-mono text-xs text-[#00ff41]">
{btcAddr}
</div>
) : (
<div className="mb-5 border-2 border-amber-500/50 bg-black px-4 py-3 text-xs text-amber-300">
Merchant address not configured. Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS in .env.local.
</div>
)}
<form onSubmit={handleEnter} className="flex flex-col gap-4 sm:flex-row">
<input
type="text"
value={txid}
onChange={(e) => setTxid(e.target.value)}
placeholder="Bitcoin transaction ID (64 hex chars)…"
className="flex-1 border-2 border-white bg-black px-4 py-3 text-sm text-white focus:border-[#00ff41] focus:outline-none"
maxLength={64}
/>
<button
type="submit"
disabled={!btcReady}
className="border-2 border-white bg-white px-6 py-3 font-black uppercase text-black transition-all hover:bg-[#00ff41] disabled:opacity-40"
>
Register Entry
</button>
</form>
{status === "success" && (
<p className="mt-3 text-sm text-[#00ff41]"> Entry registered for @{user?.username ?? "anon"}. Good luck.</p>
)}
{status === "error" && (
<p className="mt-3 text-sm text-red-400">{errMsg}</p>
)}
{!user && (
<p className="mt-3 text-xs text-white/50">
<Link href="/sign-in?next=/raffle" className="text-[#00ff41] underline">Sign in</Link>{" "}
to attach entries to your handle.
</p>
)}
{myEntries.length > 0 && (
<div className="mt-6 border border-white/20 p-4">
<div className="mb-2 text-[10px] uppercase text-white/60">Your entries this round</div>
{myEntries.map((e) => (
<div key={e.txid} className="text-xs text-[#00ff41]/70 truncate">
{e.txid.slice(0, 12)}{e.txid.slice(-8)} · {new Date(e.ts).toLocaleDateString()}
</div>
))}
</div>
)}
</div>
</div>
<div className="mt-6 text-center text-[10px] text-white/30 leading-relaxed">
Draw is via deterministic hash of the winning block header at the draw timestamp. Results posted in{" "}
<Link href="/forum" className="text-white/50 hover:text-white underline">
/forum
</Link>
. Prizes are LUX credits no fiat or BTC payout. Entry fees are non-refundable.
</div>
</div>
</ThemedLayout>
);
}