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

157 lines
4.9 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { CalendarCheckIcon, CheckIcon, MinusIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import { setMissionAttendance } from "@/app/(frontend)/intelligence/missions/actions";
type AttendanceResponse = "yes" | "no" | "tentative";
interface Attendee {
id: number;
name: string;
}
interface MissionAttendanceProps {
missionId: number;
currentUserId: number | null;
currentResponse: AttendanceResponse | null;
attendanceClosed?: boolean;
yes: Attendee[];
no: Attendee[];
tentative: Attendee[];
}
const stateMeta: Record<
AttendanceResponse,
{ label: string; icon: typeof CheckIcon; color: string; active: string }
> = {
yes: {
label: "Yes",
icon: CheckIcon,
color: "text-green-400",
active: "border-green-500/60 bg-green-500/15 text-green-400 hover:bg-green-500/25",
},
no: {
label: "No",
icon: XIcon,
color: "text-red-400",
active: "border-red-500/60 bg-red-500/15 text-red-400 hover:bg-red-500/25",
},
tentative: {
label: "Tentative",
icon: MinusIcon,
color: "text-amber-400",
active: "border-amber-500/60 bg-amber-500/15 text-amber-400 hover:bg-amber-500/25",
},
};
const states = Object.keys(stateMeta) as AttendanceResponse[];
export function MissionAttendance({
missionId,
currentUserId,
currentResponse,
attendanceClosed = false,
yes,
no,
tentative,
}: MissionAttendanceProps) {
const router = useRouter();
const [selected, setSelected] = useState<AttendanceResponse | null>(currentResponse);
const [pending, setPending] = useState<AttendanceResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const groups: Record<AttendanceResponse, Attendee[]> = { yes, no, tentative };
async function handleSet(response: AttendanceResponse) {
setPending(response);
setSelected(response);
setError(null);
const res = await setMissionAttendance({ missionId, response });
if (!res.success) {
setSelected(currentResponse);
setError(res.error ?? "Failed to update attendance.");
}
setPending(null);
router.refresh();
}
return (
<Item variant="outline">
<ItemHeader>
<div className="flex items-center gap-2">
<CalendarCheckIcon className="size-4 text-muted-foreground" />
<ItemTitle>Attendance</ItemTitle>
</div>
</ItemHeader>
<ItemContent className="flex flex-col gap-3">
{currentUserId !== null && !attendanceClosed && (
<div className="flex items-center gap-1.5">
{states.map((response) => {
const meta = stateMeta[response];
const Icon = meta.icon;
return (
<Button
key={response}
size="sm"
variant="outline"
className={cn("flex-1", selected === response && meta.active)}
disabled={pending !== null}
onClick={() => handleSet(response)}
>
<Icon className="size-3.5" />
{meta.label}
</Button>
);
})}
</div>
)}
{currentUserId !== null && attendanceClosed && (
<p className="text-xs text-muted-foreground">
Attendance is closed for this mission.
</p>
)}
{error && <p className="text-xs text-red-400">{error}</p>}
{states.map((response) => {
const meta = stateMeta[response];
const Icon = meta.icon;
const people = groups[response]
.slice()
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div key={response} className="flex flex-col gap-1">
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider">
<Icon className={cn("size-3.5", meta.color)} />
<span className={meta.color}>{meta.label}</span>
<span className="font-mono text-muted-foreground">
({people.length})
</span>
</div>
{people.length > 0 && (
<ul className="flex flex-wrap gap-x-3 gap-y-0.5">
{people.map((person) => (
<li
key={person.id}
className={cn(
"text-sm",
person.id === currentUserId
? "text-foreground font-medium"
: "text-muted-foreground",
)}
>
{person.name}
</li>
))}
</ul>
)}
</div>
);
})}
</ItemContent>
</Item>
);
}