Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
164 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|