Replace remote with local repo
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
/** Total time for the progress bar to reach 100% (ms). */
|
||||
const DURATION_MS = 3500;
|
||||
/** Hard cap — always complete after this (ms) even if something glitches. */
|
||||
const MAX_WAIT_MS = 9000;
|
||||
/** Wall-clock polling (Tor Browser throttles requestAnimationFrame aggressively). */
|
||||
const TICK_MS = 50;
|
||||
|
||||
export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState("Checking your browser…");
|
||||
const [rayId, setRayId] = useState("");
|
||||
const doneRef = useRef(false);
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
onCompleteRef.current = onComplete;
|
||||
|
||||
const finish = () => {
|
||||
if (doneRef.current) return;
|
||||
doneRef.current = true;
|
||||
setProgress(100);
|
||||
setStatus("Access granted.");
|
||||
window.setTimeout(() => onCompleteRef.current(), 400);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const start = Date.now();
|
||||
doneRef.current = false;
|
||||
|
||||
const progressInterval = window.setInterval(() => {
|
||||
const elapsed = Date.now() - start;
|
||||
const pct = Math.min(100, (elapsed / DURATION_MS) * 100);
|
||||
setProgress(pct);
|
||||
if (pct >= 100 || elapsed >= MAX_WAIT_MS) {
|
||||
window.clearInterval(progressInterval);
|
||||
finish();
|
||||
}
|
||||
}, TICK_MS);
|
||||
|
||||
const statusUpdates = [
|
||||
"Checking your browser…",
|
||||
"Verifying client challenge…",
|
||||
"Analyzing request signature…",
|
||||
"Negotiating TLS + Tor circuit…",
|
||||
"Establishing encrypted tunnel…",
|
||||
"Access granted.",
|
||||
];
|
||||
let statusIndex = 0;
|
||||
const statusInterval = window.setInterval(() => {
|
||||
if (statusIndex < statusUpdates.length - 1) {
|
||||
statusIndex++;
|
||||
setStatus(statusUpdates[statusIndex]!);
|
||||
} else {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
}, 650);
|
||||
|
||||
const maxTimer = window.setTimeout(() => {
|
||||
if (!doneRef.current) finish();
|
||||
}, MAX_WAIT_MS);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(progressInterval);
|
||||
clearInterval(statusInterval);
|
||||
clearTimeout(maxTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[10000] flex flex-col items-center justify-center bg-black p-8 font-mono text-[#00ff41]"
|
||||
style={{ backgroundColor: "#000000", color: "#00ff41" }}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md border-2 border-[#00ff41]/20 bg-[#050505] p-12 shadow-[0_0_50px_rgba(0,255,65,0.1)]"
|
||||
style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
|
||||
>
|
||||
<div className="mb-8 animate-pulse text-4xl">🛡️</div>
|
||||
<h1 className="mb-2 text-2xl font-bold uppercase tracking-tighter">DDoS Protection</h1>
|
||||
<p className="mb-8 text-xs leading-relaxed opacity-60">
|
||||
Origin shield active — wait while we validate your session. Automated requests are throttled at the edge.
|
||||
</p>
|
||||
|
||||
<div className="mb-4 h-2 w-full overflow-hidden bg-[#00ff41]/10">
|
||||
<div
|
||||
className="h-full bg-[#00ff41] transition-[width] duration-100 ease-linear"
|
||||
style={{ width: `${Math.min(100, progress)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[10px] uppercase tracking-widest">
|
||||
<span>{status}</span>
|
||||
<span>{Math.floor(Math.min(100, progress))}%</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={finish}
|
||||
className="mt-8 w-full border border-[#00ff41]/40 bg-[#00ff41]/10 py-3 text-xs font-bold uppercase tracking-wider text-[#00ff41] transition hover:bg-[#00ff41]/20"
|
||||
>
|
||||
Continue to site
|
||||
</button>
|
||||
<p className="mt-2 text-center text-[9px] text-[#00ff41]/40">
|
||||
Stuck on Tor or slow device? Tap above — no challenge required.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 text-[8px] leading-relaxed opacity-20">
|
||||
Ray ID: {rayId || "—"}
|
||||
<br />
|
||||
Performance & Security by ShadowGuard™
|
||||
</div>
|
||||
</div>
|
||||
<p className="pointer-events-none fixed bottom-6 left-0 right-0 px-4 text-center text-sm font-mono uppercase tracking-[0.2em] text-neon-cyan/80 sm:text-base">
|
||||
Tor-ready ingress · verify circuit ·{" "}
|
||||
<Link href="/launch" className="pointer-events-auto text-neon-purple underline hover:text-neon-cyan">
|
||||
/launch
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
156
components/RaffleCountdownBanner.tsx
Normal file
156
components/RaffleCountdownBanner.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
|
||||
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function getNextDraw() {
|
||||
const now = Date.now();
|
||||
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
|
||||
}
|
||||
|
||||
function fmt(ms: number) {
|
||||
if (ms <= 0) return "DRAWING NOW";
|
||||
const s = Math.floor(ms / 1000);
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
if (d > 0) return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
type RaffleStats = { totalTickets: number; participants: number; drawAt: number };
|
||||
|
||||
export default function RaffleCountdownBanner() {
|
||||
const { user } = useAccount();
|
||||
const [remaining, setRemaining] = useState(() => getNextDraw() - Date.now());
|
||||
const [stats, setStats] = useState<RaffleStats | null>(null);
|
||||
const [buying, setBuying] = useState(false);
|
||||
const [qty, setQty] = useState(1);
|
||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setRemaining(getNextDraw() - Date.now()), 1000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
const r = await fetch("/api/raffle/buy");
|
||||
const d = (await r.json()) as RaffleStats & { ok: boolean };
|
||||
if (d.ok) setStats(d);
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void fetchStats(); }, [fetchStats]);
|
||||
|
||||
const buyTickets = async () => {
|
||||
if (!user) return;
|
||||
setBuying(true);
|
||||
setMsg(null);
|
||||
try {
|
||||
const r = await fetch("/api/raffle/buy", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ handle: user.username, quantity: qty }),
|
||||
});
|
||||
const d = (await r.json()) as { ok: boolean; error?: string; ticketsBought?: number; voidBalance?: number };
|
||||
if (d.ok) {
|
||||
setMsg({ text: `✓ ${d.ticketsBought} ticket${(d.ticketsBought ?? 1) > 1 ? "s" : ""} entered! Balance: ${d.voidBalance} VOID`, ok: true });
|
||||
void fetchStats();
|
||||
} else {
|
||||
setMsg({ text: d.error ?? "Failed", ok: false });
|
||||
}
|
||||
} catch {
|
||||
setMsg({ text: "Network error", ok: false });
|
||||
} finally {
|
||||
setBuying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (dismissed) return null;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-50 w-full bg-gradient-to-r from-[#0d0d0d] via-[#111827] to-[#0d0d0d] border-b border-[#00ff66]/20 shadow-[0_2px_20px_rgba(0,255,102,0.08)]">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-2 px-4 py-1.5 text-xs">
|
||||
{/* Left: timer + pot */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-[#00ff66]/50 font-mono hidden sm:inline">◈ WEEKLY VOID RAFFLE</span>
|
||||
<div className="font-mono text-[#00ff66] text-sm font-bold tabular-nums tracking-widest">
|
||||
{fmt(remaining)}
|
||||
</div>
|
||||
{stats && (
|
||||
<span className="text-zinc-500 hidden md:inline">
|
||||
{stats.totalTickets} tickets · {stats.participants} players
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Centre: quick buy */}
|
||||
<div className="flex items-center gap-2">
|
||||
{user ? (
|
||||
open ? (
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
|
||||
<button
|
||||
onClick={() => setQty((q) => Math.max(1, q - 1))}
|
||||
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
|
||||
>−</button>
|
||||
<span className="font-mono text-[#00ff66] w-6 text-center">{qty}</span>
|
||||
<button
|
||||
onClick={() => setQty((q) => Math.min(100, q + 1))}
|
||||
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
|
||||
>+</button>
|
||||
<button
|
||||
onClick={buyTickets}
|
||||
disabled={buying}
|
||||
className="rounded bg-[#00ff66]/20 border border-[#00ff66]/40 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/30 disabled:opacity-50 transition-colors text-xs"
|
||||
>
|
||||
{buying ? "…" : `BUY ${qty} · ${qty} VOID`}
|
||||
</button>
|
||||
{msg && (
|
||||
<span className={msg.ok ? "text-[#00ff66]" : "text-red-400"}>
|
||||
{msg.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
|
||||
>
|
||||
🎟 BUY TICKET · 1 VOID
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<Link
|
||||
href="/raffle"
|
||||
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
|
||||
>
|
||||
🎟 WEEKLY RAFFLE
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: prize + close */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/raffle" className="text-zinc-500 hover:text-[#00ff66] hidden sm:inline transition-colors">
|
||||
500 VOID top prize →
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => { setOpen(false); setDismissed(true); }}
|
||||
className="text-zinc-600 hover:text-zinc-400 text-base leading-none transition-colors"
|
||||
aria-label="Close raffle banner"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user