155 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|