207 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|