"use client"; import { useState, useTransition, useEffect, use } from "react"; import { Asterisk, CheckCircle2, Camera, CameraOff, PartyPopper, ExternalLink, ArrowLeft, Copy } from "lucide-react"; import { Button } from "@/components/ui/button"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Scanner } from "@yudiel/react-qr-scanner"; import { toast } from "sonner"; import { motion } from "motion/react"; import { Item, ItemContent, ItemTitle, ItemMedia, ItemActions } from "@/components/ui/item"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { cfg } from "@/lib/cfg"; import { getAvatarProfile } from "@/lib/avatar"; interface PageProps { params: Promise<{ method: string; step: string; }>; } export default function AccessPage({ params }: PageProps) { const router = useRouter(); const resolvedParams = use(params); const method = resolvedParams.method; // 'code' or 'qr' const step = resolvedParams.step; // 'verify', 'details', 'success' // Access Code State const [length, setLength] = useState(6); const [code, setCode] = useState(""); const [verified, setVerified] = useState(false); const [whitelistSuccess, setWhitelistSuccess] = useState(false); const [status, setStatus] = useState<{ ok: boolean; message?: string }>({ ok: false }); const [isPending, startTransition] = useTransition(); // Details State const [preference, setPreference] = useState<"java" | "bedrock" | "both">("java"); const [javaName, setJavaName] = useState(""); const [bedrockName, setBedrockName] = useState(""); // QR State const [showScanner, setShowScanner] = useState(false); const [hasPermission, setHasPermission] = useState(null); const [scannerError, setScannerError] = useState(null); // Success State const [copied, setCopied] = useState(false); const [profileData, setProfileData] = useState<{ avatarUrl: string; gamertag: string } | null>(null); // Loading state to prevent premature redirects const [isLoading, setIsLoading] = useState(true); // Handle redirects based on verification state useEffect(() => { if (!isLoading) { if (step === 'details' && !verified) { router.push(`/access/${method}/verify`); } else if (step === 'success' && !whitelistSuccess) { router.push(`/access/${method}/verify`); } } }, [step, verified, whitelistSuccess, method, router, isLoading]); useEffect(() => { if (method === 'qr' && step === 'verify' && hasPermission === null) { const requestCameraPermission = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); stream.getTracks().forEach(track => track.stop()); // Stop immediately after getting permission setHasPermission(true); } catch (error) { console.error('Camera permission denied:', error); setHasPermission(false); setScannerError("Camera permission denied. Please enable camera access to scan QR codes."); } }; requestCameraPermission(); } }, [method, step, hasPermission]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; const savedVerified = localStorage.getItem(`${prefix}verified`); const savedWhitelistSuccess = localStorage.getItem(`${prefix}whitelist-success`); const savedCode = localStorage.getItem(`${prefix}auth-code`); const savedJavaName = localStorage.getItem(`${prefix}java-name`); const savedBedrockName = localStorage.getItem(`${prefix}bedrock-name`); const savedPreference = localStorage.getItem(`${prefix}preference`); if (savedVerified === 'true') { setVerified(true); } if (savedWhitelistSuccess === 'true') { setWhitelistSuccess(true); } if (savedCode) { setCode(savedCode); } if (savedJavaName) { setJavaName(savedJavaName); } if (savedBedrockName) { setBedrockName(savedBedrockName); } if (savedPreference) { setPreference(savedPreference as "java" | "bedrock" | "both"); } // Fetch profile data for success step if (step === 'success') { fetchProfileData(); } // Set loading to false after state is loaded setIsLoading(false); }, [method, step]); // Save state to localStorage useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}verified`, verified.toString()); }, [verified, method]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}whitelist-success`, whitelistSuccess.toString()); }, [whitelistSuccess, method]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}auth-code`, code); }, [code, method]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}java-name`, javaName); }, [javaName, method]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}bedrock-name`, bedrockName); }, [bedrockName, method]); useEffect(() => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.setItem(`${prefix}preference`, preference); }, [preference, method]); useEffect(() => { const fetchConfig = async () => { try { const response = await fetch("/api/access-code-config"); const config = await response.json(); setLength(config.length || 6); } catch (error) { console.error("Failed to fetch access code config:", error); } }; fetchConfig(); }, []); const fetchProfileData = async () => { try { const res = await fetch( `/api/xbox/profile/get?gamertag=${encodeURIComponent(cfg.server.bedrockFriend)}` ); const data = await res.json(); setProfileData(data); } catch (e) { console.error("Failed to fetch profile", e); } }; const needsJavaName = preference === "java" || preference === "both"; const needsBedrockName = preference === "bedrock" || preference === "both"; const handleVerifyCode = async () => { startTransition(async () => { try { const response = await fetch("/api/access-code", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ code }), }); const result = await response.json(); if (!result.success) { setVerified(false); setStatus({ ok: false, message: result.message }); return; } setVerified(true); setStatus({ ok: true, message: "Access code verified!", }); // Navigate to details step router.push(`/access/${method}/details`); } catch { setVerified(false); setStatus({ ok: false, message: "We couldn't reach the server. Double-check your connection and try again.", }); } }); }; const handleQRScan = (detectedCodes: any[]) => { if (detectedCodes.length > 0) { const result = detectedCodes[0].rawValue; const sanitizedResult = result.replace(/[^0-9A-Za-z]/g, ""); startTransition(async () => { try { const response = await fetch("/api/qr-verify", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ code: sanitizedResult, }), }); const verifyResult = await response.json(); if (verifyResult.success) { setVerified(true); setStatus({ ok: true, message: verifyResult.message || "QR code verified successfully!", }); // Navigate to details step router.push(`/access/${method}/details`); } else { setVerified(false); setStatus({ ok: false, message: verifyResult.message || "QR code verification failed", }); } } catch { setVerified(false); setStatus({ ok: false, message: "We couldn't reach the server. Try again soon.", }); } }); } }; const handleWhitelistSubmit = async () => { if ( (needsJavaName && !javaName.trim()) || (needsBedrockName && !bedrockName.trim()) ) { setStatus({ ok: false, message: "Please fill out every username we need to whitelist.", }); return; } startTransition(async () => { try { const response = await fetch("/api/whitelist", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ javaName: needsJavaName ? javaName.trim() : undefined, bedrockName: needsBedrockName ? bedrockName.trim() : undefined, preference, accessCode: method === 'qr' ? "qr_verified" : code, }), }); const result = await response.json(); if (result.success) { setStatus({ ok: true, message: result.results.join(" ") || "Successfully added to whitelist!", }); setWhitelistSuccess(true); // Navigate to success step router.push(`/access/${method}/success`); } else { setStatus({ ok: false, message: result.errors.join(" ") || "Failed to add to whitelist.", }); } } catch { setStatus({ ok: false, message: "We couldn't reach the server. Double-check your connection and try again.", }); } }); }; const handleScannerError = (error: any) => { console.error("QR Scanner error:", error); setScannerError("Unable to access camera. Please check permissions."); setHasPermission(false); }; const handleCopyIp = async () => { const serverIp = cfg.server.javaIp; if (serverIp) { await navigator.clipboard.writeText(serverIp); toast.success("Copied to clipboard"); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; // Get server information based on user's preference const getServerInfo = () => { switch (preference) { case 'java': return { address: cfg.server.javaIp, label: 'Java Server Address', showProfile: false }; case 'bedrock': return { address: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''), label: 'Bedrock Server Address', showProfile: true }; case 'both': return { javaAddress: cfg.server.javaIp, bedrockAddress: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''), label: 'Server Addresses', showProfile: true }; default: return { address: cfg.server.javaIp, label: 'Server Address', showProfile: false }; } }; const serverInfo = getServerInfo(); const handleReset = () => { const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; localStorage.removeItem(`${prefix}verified`); localStorage.removeItem(`${prefix}whitelist-success`); localStorage.removeItem(`${prefix}auth-code`); localStorage.removeItem(`${prefix}java-name`); localStorage.removeItem(`${prefix}bedrock-name`); localStorage.removeItem(`${prefix}preference`); setVerified(false); setWhitelistSuccess(false); setCode(""); setJavaName(""); setBedrockName(""); setStatus({ ok: false }); router.push(`/access/${method}/verify`); }; // Render different steps if (step === 'verify') { return (
{/* Header */}
Back to home

{method === 'code' ? 'Access Code Verification' : 'QR Code Verification'}

{method === 'code' ? `Use the ${length}-character invite your friend gave you so we can safely whitelist you.` : 'Scan a QR code to verify your access.' }

{/* Verification Content */}
{method === 'code' ? (
{Array.from({ length }).map((_, i) => ( ))}
) : (
{scannerError ? (

{scannerError}

) : ( <>
{hasPermission === null && (

Requesting camera permission...

)}
)}
)} {/* Status */}
{status.message && (

{status.ok && } {status.message}

)}
{/* Actions */} {method === 'code' && ( )}
); } if (step === 'details') { if (!verified) { return null; // Redirect handled by useEffect } return (
{/* Header */}
Back to verification

Server Details

Tell us which Minecraft edition you play and your username(s).

{/* Form */}
value && setPreference(value as typeof preference) } className="w-full" > Java Bedrock Both
{needsJavaName && (
setJavaName(event.target.value)} />
)} {needsBedrockName && (
setBedrockName(event.target.value)} />
)} {/* Status */}
{status.message && (

{status.ok && } {status.message}

)}
{/* Actions */}
); } if (step === 'success') { if (!whitelistSuccess) { return null; // Redirect handled by useEffect } const serverIp = cfg.server.javaIp; return (
{/* Header */}

You're whitelisted!

Welcome to FRG Network! You can now join the server. As soon as you join, come back and click "Claim my reward" to get a gift on behalf of the team here.

{/* Server IP */}

Server Address

{/* Profile */}

Add on Bedrock

? {cfg.server.bedrockFriend}
{/* Actions */}
); } // Invalid step - redirect handled by useEffect return null; }