"use client"; import { useState } from "react"; import { Bomb, Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { createMinefield } from "@/app/(frontend)/minigames/eod/custom/actions"; const MIN_SIZE = 9; const MAX_SIZE = 500; const PAINTER_MAX_CELLS = 2500; export function CreateMinefieldDialog({ unlocked, children, }: { unlocked: boolean; children?: React.ReactNode; }) { const router = useRouter(); const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [rows, setRows] = useState(9); const [cols, setCols] = useState(9); const [mines, setMines] = useState>(new Set()); const [exposeEdgeMines, setExposeEdgeMines] = useState(true); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const paintable = rows * cols <= PAINTER_MAX_CELLS; const mineCount = mines.size; const clampSize = (value: number) => Math.max(MIN_SIZE, Math.min(MAX_SIZE, Number.isFinite(value) ? value : MIN_SIZE)); const resetPainting = (nextRows: number, nextCols: number) => { const next = new Set(); for (const key of mines) { const row = Math.floor(key / cols); const col = key % cols; if (row < nextRows && col < nextCols) next.add(row * nextCols + col); } setMines(next); }; const toggleCell = (index: number) => { setMines((current) => { const next = new Set(current); if (next.has(index)) next.delete(index); else next.add(index); return next; }); }; const submit = async () => { setError(null); const trimmed = name.trim(); if (trimmed.length < 1 || trimmed.length > 100) { setError("Name must be 1-100 characters."); return; } if (mineCount < 1) { setError("Paint at least one mine."); return; } setSubmitting(true); const positions = Array.from(mines).map((key) => ({ row: Math.floor(key / cols), col: key % cols, })); const result = await createMinefield({ name: trimmed, rows, cols, mines: positions, exposeEdgeMines, }); setSubmitting(false); if (result.success) { setOpen(false); setName(""); setMines(new Set()); router.refresh(); } else { setError(result.error ?? "Could not publish the minefield."); } }; if (!unlocked) return null; return ( { setOpen(next); if (!next) setError(null); }} > {children ?? ( )} Author a custom minefield Pick a grid size, then paint charges. Published fields are instantly playable by every operator. No safe first click is guaranteed.
setName(event.target.value)} placeholder="Sector Ghost" className="w-48" />
{ const next = clampSize(Number(event.target.value)); setRows(next); resetPainting(next, cols); }} className="w-24" />
{ const next = clampSize(Number(event.target.value)); setCols(next); resetPainting(rows, next); }} className="w-24" />
{mineCount} mines
{paintable ? (
{Array.from({ length: rows * cols }, (_, index) => { const painted = mines.has(index); return ( ); })}
) : (

Painting supports grids up to {Math.sqrt(PAINTER_MAX_CELLS)}× {Math.sqrt(PAINTER_MAX_CELLS)} cells. Shrink the grid to author charges.

)}
setExposeEdgeMines(value === true)} className="mt-0.5" />

Failed runs show mines that touch the swept area. Turn off to reveal none of the remaining charges.

{error &&

{error}

}
); }