format and fix

This commit is contained in:
2026-02-24 23:01:11 +01:00
parent 1bb9241116
commit 81a6daaa75
17 changed files with 133 additions and 59 deletions

View File

@@ -3,3 +3,5 @@
Zendegi is a web app for creating and exploring timelines. They could be personal or professional.
The timelines are shown horizontally and are interactive.
The app is built with Tanstack Start. The timeline is implemented in flutter (packages/z-timeline) and embedded in the web page.

View File

@@ -1,3 +1,3 @@
import reactConfig from "@zendegi/eslint-config/react";
export default reactConfig;
export default [{ ignores: ["public/flutter/**"] }, ...reactConfig];

View File

@@ -31,12 +31,14 @@ type FlutterViewProps = {
state: Record<string, unknown>;
onEvent: (event: { type: string; payload?: Record<string, unknown> }) => void;
className?: string;
height?: number;
};
export function FlutterView({
state,
onEvent,
className,
height,
}: FlutterViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error">(
@@ -142,7 +144,7 @@ export function FlutterView({
}, [state]);
return (
<div className={className} style={{ height: "100%", position: "relative" }}>
<div className={className} style={{ position: "relative" }}>
{status === "loading" && (
<div
style={{
@@ -170,7 +172,10 @@ export function FlutterView({
Failed to load Flutter view
</div>
)}
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
<div
ref={containerRef}
style={{ width: "100%", height: height ?? 400 }}
/>
</div>
);
}

View File

@@ -3,21 +3,25 @@ import { useForm } from "@tanstack/react-form";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { createTimelineGroup } from "@/functions/create-timeline-group";
import { Button } from "./ui/button";
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
import {
DrawerRoot,
DrawerTrigger,
DrawerPortal,
DrawerViewport,
DrawerPopup,
DrawerTitle,
DrawerContent,
DrawerClose,
DrawerContent,
DrawerPopup,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "./ui/drawer";
import { createTimelineGroup } from "@/functions/create-timeline-group";
export default function GroupFormDrawer({ timelineId }: { timelineId: string }) {
export default function GroupFormDrawer({
timelineId,
}: {
timelineId: string;
}) {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient();
@@ -30,12 +34,16 @@ export default function GroupFormDrawer({ timelineId }: { timelineId: string })
await createTimelineGroup({
data: { title: value.title, timelineId },
});
await queryClient.invalidateQueries({ queryKey: ["timeline", timelineId] });
await queryClient.invalidateQueries({
queryKey: ["timeline", timelineId],
});
setOpen(false);
form.reset();
toast.success("Group created");
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to create group");
toast.error(
error instanceof Error ? error.message : "Failed to create group"
);
}
},
});
@@ -73,10 +81,14 @@ export default function GroupFormDrawer({ timelineId }: { timelineId: string })
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
field.handleChange(
(e.target as HTMLInputElement).value
)
}
/>
<FieldError match="valueMissing">Title is required</FieldError>
<FieldError match="valueMissing">
Title is required
</FieldError>
</FieldRoot>
)}
</form.Field>

View File

@@ -3,19 +3,19 @@ import { useForm } from "@tanstack/react-form";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { createTimelineItem } from "@/functions/create-timeline-item";
import { Button } from "./ui/button";
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
import {
DrawerRoot,
DrawerTrigger,
DrawerPortal,
DrawerViewport,
DrawerPopup,
DrawerTitle,
DrawerContent,
DrawerClose,
DrawerContent,
DrawerPopup,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "./ui/drawer";
import { createTimelineItem } from "@/functions/create-timeline-item";
export default function ItemFormDrawer({
timelineGroupId,
@@ -45,12 +45,16 @@ export default function ItemFormDrawer({
timelineGroupId,
},
});
await queryClient.invalidateQueries({ queryKey: ["timeline", timelineId] });
await queryClient.invalidateQueries({
queryKey: ["timeline", timelineId],
});
setOpen(false);
form.reset();
toast.success("Item created");
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to create item");
toast.error(
error instanceof Error ? error.message : "Failed to create item"
);
}
},
});
@@ -88,10 +92,14 @@ export default function ItemFormDrawer({
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
field.handleChange(
(e.target as HTMLInputElement).value
)
}
/>
<FieldError match="valueMissing">Title is required</FieldError>
<FieldError match="valueMissing">
Title is required
</FieldError>
</FieldRoot>
)}
</form.Field>
@@ -105,7 +113,9 @@ export default function ItemFormDrawer({
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
field.handleChange(
(e.target as HTMLInputElement).value
)
}
/>
</FieldRoot>
@@ -122,10 +132,14 @@ export default function ItemFormDrawer({
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
field.handleChange(
(e.target as HTMLInputElement).value
)
}
/>
<FieldError match="valueMissing">Start date is required</FieldError>
<FieldError match="valueMissing">
Start date is required
</FieldError>
</FieldRoot>
)}
</form.Field>
@@ -139,7 +153,9 @@ export default function ItemFormDrawer({
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
field.handleChange(
(e.target as HTMLInputElement).value
)
}
/>
</FieldRoot>

View File

@@ -11,5 +11,5 @@ export const Route = createFileRoute("/.well-known/oauth-authorization-server")(
GET: ({ request }) => handler(request),
},
},
},
}
);

View File

@@ -18,7 +18,7 @@ export const Route = createFileRoute("/.well-known/oauth-protected-resource")({
"Cache-Control":
"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
},
},
}
);
},
},

View File

@@ -47,7 +47,7 @@ function RootDocument() {
<HeadContent />
</head>
<body>
<div className="grid h-svh grid-rows-[auto_1fr]">
<div className="grid min-h-svh grid-rows-[auto_1fr]">
<Header />
<Outlet />
</div>

View File

@@ -5,8 +5,8 @@ import { Button } from "@/components/ui/button";
export const Route = createFileRoute("/consent")({
validateSearch: (search: Record<string, unknown>) => ({
client_id: (search.client_id as string) ?? "",
scope: (search.scope as string) ?? "",
client_id: (search.client_id as string | undefined) ?? "",
scope: (search.scope as string | undefined) ?? "",
}),
component: ConsentComponent,
});

View File

@@ -5,7 +5,12 @@ import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import { createTimeline } from "@/functions/create-timeline";
import { Button } from "@/components/ui/button";
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "@/components/ui/field";
import {
FieldControl,
FieldError,
FieldLabel,
FieldRoot,
} from "@/components/ui/field";
export const Route = createFileRoute("/")({
component: HomeComponent,
@@ -22,9 +27,14 @@ function HomeComponent() {
try {
await authClient.signIn.anonymous();
const timeline = await createTimeline({ data: { title: value.title } });
navigate({ to: "/timeline/$timelineId", params: { timelineId: timeline.id } });
navigate({
to: "/timeline/$timelineId",
params: { timelineId: timeline.id },
});
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to create timeline");
toast.error(
error instanceof Error ? error.message : "Failed to create timeline"
);
}
},
});
@@ -66,9 +76,7 @@ function HomeComponent() {
field.handleChange((e.target as HTMLInputElement).value)
}
/>
<FieldError match="valueMissing">
Name is required
</FieldError>
<FieldError match="valueMissing">Name is required</FieldError>
</FieldRoot>
)}
</form.Field>

View File

@@ -2,8 +2,6 @@ import { useCallback, useMemo, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { useSuspenseQuery } from "@tanstack/react-query";
import { timelineQueryOptions } from "@/functions/get-timeline";
import GroupFormDrawer from "@/components/group-form-drawer";
import ItemFormDrawer from "@/components/item-form-drawer";
import { FlutterView } from "@/components/flutter-view";
export const Route = createFileRoute("/timeline/$timelineId")({
@@ -44,11 +42,16 @@ function RouteComponent() {
[timeline, selectedItemId]
);
const [flutterHeight, setFlutterHeight] = useState<number | undefined>();
const handleEvent = useCallback(
(event: { type: string; payload?: Record<string, unknown> }) => {
switch (event.type) {
case "content_height":
setFlutterHeight(event.payload?.height as number);
break;
case "item_selected":
setSelectedItemId((event.payload?.itemId as string) ?? null);
// setSelectedItemId((event.payload?.itemId as string) ?? null);
break;
case "item_deselected":
setSelectedItemId(null);
@@ -59,13 +62,16 @@ function RouteComponent() {
);
return (
<div className="flex h-screen flex-col px-4 py-6">
<h1 className="text-3xl font-serif font-bold mb-6">{timeline.title}</h1>
<div className="flex flex-col">
<h1 className="text-3xl font-serif font-bold mb-6 mx-4">
{timeline.title}
</h1>
<FlutterView
state={flutterState}
onEvent={handleEvent}
className="min-h-0 flex-1 rounded-lg overflow-hidden"
className="overflow-hidden"
height={flutterHeight}
/>
</div>
);