1
0
Fork 0
polaris-task-force/src/components/frontend/intelligence/MissionObjectives.tsx

207 lines
7.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { CheckIcon, CrosshairIcon, EyeIcon, EyeOffIcon, Loader2Icon, XIcon } from "lucide-react";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { toggleObjectiveRedacted } from "@/app/(frontend)/intelligence/missions/objectives/actions";
import { redactText } from "@/lib/redaction";
interface Objective {
title: string;
description?: string | null;
missionCritical?: boolean | null;
successOrFailConditions?: string | null;
redacted?: boolean | null;
}
interface MissionObjectivesProps {
objectives: Objective[] | null;
missionId?: number;
isEditable?: boolean;
canReveal?: boolean;
}
function ObjectiveText({
text,
redacted,
canReveal,
className,
}: {
text: string | null | undefined;
redacted: boolean;
canReveal: boolean;
className?: string;
}) {
if (!redacted) {
return <span className={className}>{text ?? "-"}</span>;
}
const display = redactText(text) ?? "-";
if (!canReveal) {
return <span className={`${className ?? ""} select-none break-all`}>{display}</span>;
}
return (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<span className={`${className ?? ""} cursor-help select-none break-all`}>{display}</span>
</TooltipTrigger>
<TooltipContent className="max-w-sm whitespace-pre-wrap break-words">
{text ?? "-"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export function MissionObjectives({
objectives,
missionId,
isEditable = false,
canReveal = false,
}: MissionObjectivesProps) {
const router = useRouter();
const [togglingIndex, setTogglingIndex] = useState<number | null>(null);
const [toggleError, setToggleError] = useState<string | null>(null);
if (!objectives || objectives.length === 0) {
return (
<Item variant="outline">
<ItemHeader>
<div className="flex items-center gap-2">
<CrosshairIcon className="size-4 text-muted-foreground" />
<ItemTitle>Objectives</ItemTitle>
</div>
</ItemHeader>
<ItemContent>
<p className="text-sm text-muted-foreground italic">No objectives defined.</p>
</ItemContent>
</Item>
);
}
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<CrosshairIcon className="size-4 text-muted-foreground" />
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Objectives
</h2>
</div>
<div className="border border-border rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b border-border">
<th className="text-left px-4 py-2.5 font-semibold uppercase text-xs tracking-wider text-muted-foreground">
Title
</th>
<th className="text-left px-4 py-2.5 font-semibold uppercase text-xs tracking-wider text-muted-foreground">
Description
</th>
<th className="text-center px-4 py-2.5 font-semibold uppercase text-xs tracking-wider text-muted-foreground">
Critical
</th>
<th className="text-left px-4 py-2.5 font-semibold uppercase text-xs tracking-wider text-muted-foreground">
Success / Fail Conditions
</th>
{isEditable && (
<th className="text-center px-4 py-2.5 font-semibold uppercase text-xs tracking-wider text-muted-foreground">
Actions
</th>
)}
</tr>
</thead>
<tbody>
{objectives.map((obj, index) => (
<tr key={index} className="border-b border-border/50">
<td className="px-4 py-2 font-medium">
<ObjectiveText
text={obj.title}
redacted={!!obj.redacted}
canReveal={canReveal}
/>
</td>
<td className="px-4 py-2 text-muted-foreground max-w-md wrap-break-word">
<ObjectiveText
text={obj.description}
redacted={!!obj.redacted}
canReveal={canReveal}
/>
</td>
<td className="px-4 py-2 text-center">
{obj.missionCritical ? (
<CheckIcon className="size-3.5 text-orange-400 inline" />
) : (
<XIcon className="size-3.5 text-muted-foreground inline" />
)}
</td>
<td className="px-4 py-2 text-muted-foreground">
<ObjectiveText
text={obj.successOrFailConditions}
redacted={!!obj.redacted}
canReveal={canReveal}
/>
</td>
{isEditable && (
<td className="px-4 py-2 text-center whitespace-nowrap">
<Button
variant="outline"
size="sm"
disabled={togglingIndex !== null}
onClick={async () => {
if (!missionId || togglingIndex !== null) return;
setToggleError(null);
setTogglingIndex(index);
try {
const res = await toggleObjectiveRedacted({
missionId,
objectiveIndex: index,
redacted: !obj.redacted,
});
if (!res.success) {
setToggleError(res.error ?? "Failed to update objective.");
return;
}
router.refresh();
} catch {
setToggleError("Something went wrong. Please try again.");
} finally {
setTogglingIndex(null);
}
}}
>
{togglingIndex === index ? (
<Loader2Icon className="size-3.5 mr-1 animate-spin" />
) : obj.redacted ? (
<>
<EyeIcon className="size-3.5 mr-1" />
Show
</>
) : (
<>
<EyeOffIcon className="size-3.5 mr-1" />
Hide
</>
)}
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{toggleError && <p className="text-sm text-destructive">{toggleError}</p>}
</div>
);
}