1
0
Fork 0
polaris-task-force/src/components/frontend/intelligence/SideOpNarrativeFields.tsx
Z8MB1E 03f699d852 feat(missions): add Friday Ops calendar page and side-op dialog UI
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 23:29:54 -04:00

352 lines
12 KiB
TypeScript

"use client";
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const CIVILIAN_PRESENCE = ["None", "Low", "Medium", "High"] as const;
const INFIL_EXFIL_METHODS = [
{ value: "helicopter", label: "Helicopter" },
{ value: "aircraft", label: "Aircraft" },
{ value: "paradrop", label: "HALO / Paradrop" },
{ value: "boat", label: "Boat" },
{ value: "car", label: "Car" },
{ value: "onFoot", label: "On Foot" },
] as const;
export interface SideOpNarrative {
briefing: { sectionName: string; details: string }[];
missionObjectives: {
title: string;
description: string;
missionCritical: boolean;
successOrFailConditions: string;
redacted: boolean;
}[];
civilianPresence: string;
radioPlan: string;
insertion: string[];
extraction: string[];
assetsAvailable: {
assetType: string;
assetCount: string;
doesRespawn: boolean;
assetCallsign: string;
}[];
}
interface SideOpNarrativeFieldsProps {
value: SideOpNarrative;
onChange: (next: SideOpNarrative) => void;
}
function toggleValue(list: string[], value: string): string[] {
return list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
}
export function SideOpNarrativeFields({ value, onChange }: SideOpNarrativeFieldsProps) {
const setBriefing = (briefing: SideOpNarrative["briefing"]) => onChange({ ...value, briefing });
const setObjectives = (missionObjectives: SideOpNarrative["missionObjectives"]) =>
onChange({ ...value, missionObjectives });
const setAssets = (assetsAvailable: SideOpNarrative["assetsAvailable"]) =>
onChange({ ...value, assetsAvailable });
return (
<div className="grid gap-5">
<p className="text-xs text-muted-foreground">
Plain text only — each line becomes a paragraph. Rich formatting written in the admin panel
is flattened if you save here.
</p>
<div className="grid gap-2">
<Label>Briefing entries</Label>
{value.briefing.map((entry, i) => (
<div key={i} className="grid gap-1.5 rounded-md border p-2.5">
<div className="flex items-center gap-2">
<Input
value={entry.sectionName}
onChange={(e) => {
const next = [...value.briefing];
next[i] = { ...entry, sectionName: e.target.value };
setBriefing(next);
}}
placeholder="Section name (e.g. Situation)"
/>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Remove briefing entry"
onClick={() => setBriefing(value.briefing.filter((_, j) => j !== i))}
>
<Trash2 className="size-4" />
</Button>
</div>
<Textarea
value={entry.details}
onChange={(e) => {
const next = [...value.briefing];
next[i] = { ...entry, details: e.target.value };
setBriefing(next);
}}
placeholder="Briefing text for this section"
rows={3}
/>
</div>
))}
{value.briefing.length < 8 && (
<Button
type="button"
variant="outline"
size="sm"
className="w-fit"
onClick={() =>
setBriefing([...value.briefing, { sectionName: "", details: "" }])
}
>
<Plus className="size-4" /> Add briefing entry
</Button>
)}
</div>
<div className="grid gap-2">
<Label>Mission objectives</Label>
{value.missionObjectives.map((obj, i) => (
<div key={i} className="grid gap-1.5 rounded-md border p-2.5">
<div className="flex items-center gap-2">
<Input
value={obj.title}
onChange={(e) => {
const next = [...value.missionObjectives];
next[i] = { ...obj, title: e.target.value };
setObjectives(next);
}}
placeholder="Objective title"
/>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Remove objective"
onClick={() => setObjectives(value.missionObjectives.filter((_, j) => j !== i))}
>
<Trash2 className="size-4" />
</Button>
</div>
<Textarea
value={obj.description}
onChange={(e) => {
const next = [...value.missionObjectives];
next[i] = { ...obj, description: e.target.value };
setObjectives(next);
}}
placeholder="Description"
rows={2}
/>
<Input
value={obj.successOrFailConditions}
onChange={(e) => {
const next = [...value.missionObjectives];
next[i] = { ...obj, successOrFailConditions: e.target.value };
setObjectives(next);
}}
placeholder="Success / fail conditions"
/>
<div className="flex flex-wrap gap-4">
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={obj.missionCritical}
onCheckedChange={(checked) => {
const next = [...value.missionObjectives];
next[i] = { ...obj, missionCritical: checked === true };
setObjectives(next);
}}
/>
Mission critical
</label>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={obj.redacted}
onCheckedChange={(checked) => {
const next = [...value.missionObjectives];
next[i] = { ...obj, redacted: checked === true };
setObjectives(next);
}}
/>
Redacted
</label>
</div>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="w-fit"
onClick={() =>
setObjectives([
...value.missionObjectives,
{
title: "",
description: "",
missionCritical: true,
successOrFailConditions: "",
redacted: false,
},
])
}
>
<Plus className="size-4" /> Add objective
</Button>
</div>
<div className="grid gap-2">
<Label>Civilian presence</Label>
<Select
value={value.civilianPresence}
onValueChange={(civilianPresence) => onChange({ ...value, civilianPresence })}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CIVILIAN_PRESENCE.map((c) => (
<SelectItem key={c} value={c}>
{c}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>Radio SOP</Label>
<Textarea
value={value.radioPlan}
onChange={(e) => onChange({ ...value, radioPlan: e.target.value })}
placeholder="How radios will be used: nets, contingency plans, radio silence, etc."
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label>Infiltration methods</Label>
<div className="grid gap-1.5">
{INFIL_EXFIL_METHODS.map((m) => (
<label key={m.value} className="flex items-center gap-2 text-sm">
<Checkbox
checked={value.insertion.includes(m.value)}
onCheckedChange={() =>
onChange({ ...value, insertion: toggleValue(value.insertion, m.value) })
}
/>
{m.label}
</label>
))}
</div>
</div>
<div className="grid gap-2">
<Label>Exfiltration methods</Label>
<div className="grid gap-1.5">
{INFIL_EXFIL_METHODS.map((m) => (
<label key={m.value} className="flex items-center gap-2 text-sm">
<Checkbox
checked={value.extraction.includes(m.value)}
onCheckedChange={() =>
onChange({ ...value, extraction: toggleValue(value.extraction, m.value) })
}
/>
{m.label}
</label>
))}
</div>
</div>
</div>
<div className="grid gap-2">
<Label>Logistics &amp; support assets</Label>
{value.assetsAvailable.map((asset, i) => (
<div key={i} className="grid gap-1.5 rounded-md border p-2.5">
<div className="flex items-center gap-2">
<Input
value={asset.assetType}
onChange={(e) => {
const next = [...value.assetsAvailable];
next[i] = { ...asset, assetType: e.target.value };
setAssets(next);
}}
placeholder="Asset type (e.g. Artillery, Fuel Truck, CAS)"
/>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Remove asset"
onClick={() => setAssets(value.assetsAvailable.filter((_, j) => j !== i))}
>
<Trash2 className="size-4" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<Input
value={asset.assetCount}
onChange={(e) => {
const next = [...value.assetsAvailable];
next[i] = { ...asset, assetCount: e.target.value };
setAssets(next);
}}
placeholder="Count"
/>
<Input
value={asset.assetCallsign}
onChange={(e) => {
const next = [...value.assetsAvailable];
next[i] = { ...asset, assetCallsign: e.target.value };
setAssets(next);
}}
placeholder="Callsign"
/>
</div>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={asset.doesRespawn}
onCheckedChange={(checked) => {
const next = [...value.assetsAvailable];
next[i] = { ...asset, doesRespawn: checked === true };
setAssets(next);
}}
/>
Respawnable
</label>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="w-fit"
onClick={() =>
setAssets([
...value.assetsAvailable,
{ assetType: "", assetCount: "", doesRespawn: false, assetCallsign: "" },
])
}
>
<Plus className="size-4" /> Add asset
</Button>
</div>
</div>
);
}