Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
190 lines
6.7 KiB
TypeScript
190 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { CalendarDaysIcon, ChevronRightIcon, PlusIcon } from "lucide-react";
|
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
|
import { Button } from "@/components/ui/button";
|
|
import { CreateSideOpDialog, type SideOpDialogOption } from "./CreateSideOpDialog";
|
|
|
|
export interface FridayCalendarMission {
|
|
id: number;
|
|
name: string;
|
|
codeName: string;
|
|
status: string;
|
|
startDateTime: string;
|
|
operationType: string;
|
|
mapName: string | null;
|
|
isOwn: boolean;
|
|
}
|
|
|
|
export interface FridayCalendarEntry {
|
|
fridayIso: string;
|
|
missions: FridayCalendarMission[];
|
|
}
|
|
|
|
interface FridayCalendarProps {
|
|
entries: FridayCalendarEntry[];
|
|
remainingSlots: number;
|
|
maxSlots: number;
|
|
maps: SideOpDialogOption[];
|
|
campaigns: SideOpDialogOption[];
|
|
modLists: SideOpDialogOption[];
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
Concept: "bg-muted text-muted-foreground",
|
|
Planning: "bg-muted text-muted-foreground",
|
|
Ready: "bg-blue-500/20 text-blue-400",
|
|
Scheduled: "bg-amber-500/20 text-amber-400",
|
|
Active: "bg-green-500/20 text-green-400",
|
|
Completed: "bg-emerald-500/20 text-emerald-400",
|
|
Cancelled: "bg-red-500/20 text-red-400",
|
|
};
|
|
|
|
function formatFriday(iso: string): string {
|
|
return new Date(iso).toLocaleDateString(undefined, {
|
|
weekday: "long",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
|
|
function formatTime(iso: string): string {
|
|
return new Date(iso).toLocaleTimeString(undefined, {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
function defaultStartForFriday(fridayIso: string): string {
|
|
const date = new Date(fridayIso);
|
|
date.setHours(20, 0, 0, 0);
|
|
return date.toISOString();
|
|
}
|
|
|
|
export function FridayCalendar({
|
|
entries,
|
|
remainingSlots,
|
|
maxSlots,
|
|
maps,
|
|
campaigns,
|
|
modLists,
|
|
}: FridayCalendarProps) {
|
|
const router = useRouter();
|
|
const [scheduleFor, setScheduleFor] = useState<string | null>(null);
|
|
|
|
const upcoming = useMemo(() => entries, [entries]);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Friday Operations</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Schedule and manage player-run Friday side operations. Each Friday gets Discord
|
|
roll-call and RSVP automatically.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`rounded-md border px-2.5 py-1 text-xs font-medium ${
|
|
remainingSlots > 0
|
|
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400"
|
|
: "border-red-500/30 bg-red-500/10 text-red-400"
|
|
}`}
|
|
>
|
|
{remainingSlots > 0
|
|
? `You can schedule ${remainingSlots} more mission${remainingSlots === 1 ? "" : "s"}`
|
|
: `Scheduling limit reached (${maxSlots} ahead)`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
{upcoming.map((entry) => {
|
|
const fridayTaken = entry.missions.some(
|
|
(m) => m.status !== "Completed" && m.status !== "Cancelled",
|
|
);
|
|
const canSchedule = remainingSlots > 0 && !fridayTaken;
|
|
return (
|
|
<Item key={entry.fridayIso} variant="outline">
|
|
<ItemHeader>
|
|
<div className="flex items-center gap-2">
|
|
<CalendarDaysIcon className="size-4 text-muted-foreground" />
|
|
<ItemTitle>{formatFriday(entry.fridayIso)}</ItemTitle>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={!canSchedule}
|
|
onClick={() => setScheduleFor(entry.fridayIso)}
|
|
>
|
|
<PlusIcon className="size-3.5" />
|
|
{fridayTaken ? "Taken" : "Schedule"}
|
|
</Button>
|
|
</ItemHeader>
|
|
<ItemContent>
|
|
{entry.missions.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
Nothing scheduled yet — claim this Friday.
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col gap-1.5">
|
|
{entry.missions.map((m) => (
|
|
<Link
|
|
key={m.id}
|
|
href={`/intelligence/missions/${m.id}`}
|
|
className="group flex items-center gap-3 rounded-md p-1.5 transition-colors hover:bg-muted/50"
|
|
>
|
|
<span className="w-14 shrink-0 font-mono text-xs text-muted-foreground">
|
|
{formatTime(m.startDateTime)}
|
|
</span>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-sm transition-colors group-hover:text-primary">
|
|
{m.name}
|
|
</span>
|
|
<span className="truncate font-mono text-[10px] text-muted-foreground">
|
|
{m.codeName}
|
|
{m.mapName ? ` · ${m.mapName}` : ""}
|
|
{m.isOwn ? " · yours" : ""}
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${
|
|
statusColors[m.status] ?? "bg-muted text-muted-foreground"
|
|
}`}
|
|
>
|
|
{m.status}
|
|
</span>
|
|
<ChevronRightIcon className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ItemContent>
|
|
</Item>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
Fridays beyond this date are unavailable for scheduling via the web app.<br/><br/>If you want to schedule an
|
|
op further out than the dates listed here, please contact <b>Jason (Z8MB1E)</b> directly.
|
|
</p>
|
|
|
|
<CreateSideOpDialog
|
|
open={scheduleFor !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) setScheduleFor(null);
|
|
}}
|
|
maps={maps}
|
|
campaigns={campaigns}
|
|
modLists={modLists}
|
|
defaultStartDateTime={scheduleFor ? defaultStartForFriday(scheduleFor) : undefined}
|
|
onSaved={() => router.refresh()}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|