Files
zendegi/apps/web/src/routes/timeline.$timelineId.tsx
2026-03-07 14:47:42 +01:00

155 lines
5.0 KiB
TypeScript

import { useCallback, useMemo, useState } from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { useSuspenseQuery } from "@tanstack/react-query";
import { ArrowLeft, Moon, Plus, Sun } from "lucide-react";
import type { FlutterEvent, FlutterTimelineState } from "@/lib/flutter-bridge";
import { timelineQueryOptions } from "@/functions/get-timeline";
import { FlutterView } from "@/components/flutter-view";
import { Button } from "@/components/ui/button";
import UserMenu from "@/components/user-menu";
import ItemFormDrawer from "@/components/item-form-drawer";
import { useEntryMovedMutation } from "@/hooks/use-entry-moved-mutation";
import { useEntryResizedMutation } from "@/hooks/use-entry-resized-mutation";
import { useTheme } from "@/lib/theme";
export const Route = createFileRoute("/timeline/$timelineId")({
loader: async ({ context, params }) => {
await context.queryClient.ensureQueryData(
timelineQueryOptions(params.timelineId)
);
},
component: RouteComponent,
});
function RouteComponent() {
const { timelineId } = Route.useParams();
const { data: timeline } = useSuspenseQuery(timelineQueryOptions(timelineId));
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [flutterHeight, setFlutterHeight] = useState<number | undefined>();
const entryMoved = useEntryMovedMutation(timelineId);
const entryResized = useEntryResizedMutation(timelineId);
const { theme, toggleTheme } = useTheme();
const editItem = selectedItemId
? (timeline.items[selectedItemId] ?? null)
: null;
const flutterState: FlutterTimelineState = useMemo(
() => ({
timeline: { id: timeline.id, title: timeline.title },
groups: timeline.groups,
items: Object.fromEntries(
Object.entries(timeline.items).map(([id, item]) => [
id,
{
id: item.id,
groupId: item.groupId,
title: item.title,
description: item.description,
start: item.start.toISOString(),
end: item.end?.toISOString() ?? null,
lane: item.lane,
},
])
),
groupOrder: timeline.groupOrder,
selectedItemId,
darkMode: theme === "dark",
}),
[timeline, selectedItemId, theme]
);
const handleEvent = useCallback(
(event: FlutterEvent) => {
switch (event.type) {
case "content_height":
setFlutterHeight(event.payload.height);
break;
case "item_selected":
setSelectedItemId(event.payload.itemId);
setDrawerOpen(true);
break;
case "item_deselected":
setSelectedItemId(null);
setDrawerOpen(false);
break;
case "entry_moved":
entryMoved.mutate(event.payload);
break;
case "entry_resized":
entryResized.mutate(event.payload);
break;
}
},
[entryMoved, entryResized]
);
const handleDrawerOpenChange = useCallback((open: boolean) => {
setDrawerOpen(open);
if (!open) {
setSelectedItemId(null);
}
}, []);
const handleAddItem = useCallback(() => {
setSelectedItemId(null);
setDrawerOpen(true);
}, []);
return (
<div className="flex flex-col">
<div className="flex flex-row items-center justify-between px-2 py-1">
<div className="flex items-center gap-2">
<Link
to="/timelines"
className="text-muted-foreground hover:text-foreground inline-flex size-8 items-center justify-center rounded-md transition-colors"
aria-label="Back to timelines"
>
<ArrowLeft className="size-4" />
</Link>
<h1 className="text-lg font-serif font-bold">{timeline.title}</h1>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleAddItem}>
<Plus className="size-4" />
Add Item
</Button>
<button
type="button"
onClick={toggleTheme}
className="text-muted-foreground hover:text-foreground inline-flex size-8 items-center justify-center rounded-md transition-colors"
aria-label={
theme === "dark" ? "Switch to light mode" : "Switch to dark mode"
}
>
{theme === "dark" ? (
<Sun className="size-4" />
) : (
<Moon className="size-4" />
)}
</button>
<UserMenu />
</div>
</div>
<hr />
<FlutterView
state={flutterState}
onEvent={handleEvent}
className="overflow-hidden"
height={flutterHeight}
/>
<ItemFormDrawer
open={drawerOpen}
onOpenChange={handleDrawerOpenChange}
timelineId={timelineId}
groups={timeline.groups}
groupOrder={timeline.groupOrder}
editItem={editItem}
/>
</div>
);
}