1
0
Fork 0
polaris-task-force/src/components/frontend/minigames/radio/CreateTestDialog.tsx
2026-09-09 00:02:59 -04:00

164 lines
5.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { Loader2, Radio } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { createTest } from "@/app/(frontend)/minigames/radio/custom/actions";
const MAX_MESSAGES = 100;
export function CreateTestDialog({
unlocked,
children,
}: {
unlocked: boolean;
children?: React.ReactNode;
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [poolText, setPoolText] = useState("");
const [order, setOrder] = useState<"fixed" | "shuffled">("fixed");
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const pool = poolText
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
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 (pool.length < 1 || pool.length > MAX_MESSAGES) {
setError(`Message pool must contain 1-${MAX_MESSAGES} messages (one per line).`);
return;
}
if (pool.some((message) => message.length > 500)) {
setError("Each message must be 1-500 characters.");
return;
}
setSubmitting(true);
const result = await createTest({ name: trimmed, messages: pool, order });
setSubmitting(false);
if (result.success) {
setOpen(false);
setName("");
setPoolText("");
setOrder("fixed");
router.refresh();
} else {
setError(result.error ?? "Could not publish the test.");
}
};
if (!unlocked) return null;
return (
<Dialog
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) setError(null);
}}
>
<DialogTrigger asChild>
{children ?? (
<Button type="button">
<Radio /> Author radio test
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[85dvh] overflow-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>Author a custom radio test</DialogTitle>
<DialogDescription>
Build a message pool, one transmission per line. Published tests are instantly
playable by every operator and scored like the official game.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="radio-test-name" className="text-xs font-medium">
Name
</label>
<Input
id="radio-test-name"
value={name}
maxLength={100}
onChange={(event) => setName(event.target.value)}
placeholder="Night Watch Drill"
className="w-48"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="radio-test-order" className="text-xs font-medium">
Order
</label>
<Select value={order} onValueChange={(value) => setOrder(value as "fixed" | "shuffled")}>
<SelectTrigger id="radio-test-order" className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fixed">Fixed Order</SelectItem>
<SelectItem value="shuffled">Shuffled</SelectItem>
</SelectContent>
</Select>
</div>
<span className="pb-2 text-xs text-muted-foreground">{pool.length} messages</span>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="radio-test-pool" className="text-xs font-medium">
Message pool (one per line)
</label>
<textarea
id="radio-test-pool"
value={poolText}
onChange={(event) => setPoolText(event.target.value)}
rows={8}
placeholder={"VIKTOR ONE, EN ROUTE TO GRID 341-512, STATUS GREEN, OVER\nECHO TWO, CONTACT AT CHECKPOINT ALPHA, WEAPONS FREE, OUT"}
className="w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-sm placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="button" disabled={submitting} onClick={submit}>
{submitting ? <Loader2 className="animate-spin" /> : <Radio />}
Publish test
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}