format and fix
This commit is contained in:
@@ -5,4 +5,4 @@
|
|||||||
"url": "http://localhost:3001/api/mcp"
|
"url": "http://localhost:3001/api/mcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
**/routeTree.gen.ts
|
**/routeTree.gen.ts
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
**/.claude/**
|
**/.claude/**
|
||||||
|
**/public/flutter/**
|
||||||
|
**/z-timeline/**
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
Zendegi is a web app for creating and exploring timelines. They could be personal or professional.
|
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 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.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import reactConfig from "@zendegi/eslint-config/react";
|
import reactConfig from "@zendegi/eslint-config/react";
|
||||||
|
|
||||||
export default reactConfig;
|
export default [{ ignores: ["public/flutter/**"] }, ...reactConfig];
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ type FlutterViewProps = {
|
|||||||
state: Record<string, unknown>;
|
state: Record<string, unknown>;
|
||||||
onEvent: (event: { type: string; payload?: Record<string, unknown> }) => void;
|
onEvent: (event: { type: string; payload?: Record<string, unknown> }) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FlutterView({
|
export function FlutterView({
|
||||||
state,
|
state,
|
||||||
onEvent,
|
onEvent,
|
||||||
className,
|
className,
|
||||||
|
height,
|
||||||
}: FlutterViewProps) {
|
}: FlutterViewProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [status, setStatus] = useState<"loading" | "ready" | "error">(
|
const [status, setStatus] = useState<"loading" | "ready" | "error">(
|
||||||
@@ -142,7 +144,7 @@ export function FlutterView({
|
|||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} style={{ height: "100%", position: "relative" }}>
|
<div className={className} style={{ position: "relative" }}>
|
||||||
{status === "loading" && (
|
{status === "loading" && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -170,7 +172,10 @@ export function FlutterView({
|
|||||||
Failed to load Flutter view
|
Failed to load Flutter view
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ width: "100%", height: height ?? 400 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,25 @@ import { useForm } from "@tanstack/react-form";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { createTimelineGroup } from "@/functions/create-timeline-group";
|
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
|
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
|
||||||
import {
|
import {
|
||||||
DrawerRoot,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerViewport,
|
|
||||||
DrawerPopup,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerPopup,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerRoot,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerViewport,
|
||||||
} from "./ui/drawer";
|
} 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 [open, setOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -30,12 +34,16 @@ export default function GroupFormDrawer({ timelineId }: { timelineId: string })
|
|||||||
await createTimelineGroup({
|
await createTimelineGroup({
|
||||||
data: { title: value.title, timelineId },
|
data: { title: value.title, timelineId },
|
||||||
});
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ["timeline", timelineId] });
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["timeline", timelineId],
|
||||||
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
toast.success("Group created");
|
toast.success("Group created");
|
||||||
} catch (error) {
|
} 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}
|
value={field.state.value}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
onChange={(e) =>
|
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>
|
</FieldRoot>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import { useForm } from "@tanstack/react-form";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { createTimelineItem } from "@/functions/create-timeline-item";
|
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
|
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
|
||||||
import {
|
import {
|
||||||
DrawerRoot,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerViewport,
|
|
||||||
DrawerPopup,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerPopup,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerRoot,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerViewport,
|
||||||
} from "./ui/drawer";
|
} from "./ui/drawer";
|
||||||
|
import { createTimelineItem } from "@/functions/create-timeline-item";
|
||||||
|
|
||||||
export default function ItemFormDrawer({
|
export default function ItemFormDrawer({
|
||||||
timelineGroupId,
|
timelineGroupId,
|
||||||
@@ -45,12 +45,16 @@ export default function ItemFormDrawer({
|
|||||||
timelineGroupId,
|
timelineGroupId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ["timeline", timelineId] });
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["timeline", timelineId],
|
||||||
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
toast.success("Item created");
|
toast.success("Item created");
|
||||||
} catch (error) {
|
} 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}
|
value={field.state.value}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
onChange={(e) =>
|
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>
|
</FieldRoot>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -105,7 +113,9 @@ export default function ItemFormDrawer({
|
|||||||
value={field.state.value}
|
value={field.state.value}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.handleChange((e.target as HTMLInputElement).value)
|
field.handleChange(
|
||||||
|
(e.target as HTMLInputElement).value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRoot>
|
</FieldRoot>
|
||||||
@@ -122,10 +132,14 @@ export default function ItemFormDrawer({
|
|||||||
value={field.state.value}
|
value={field.state.value}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
onChange={(e) =>
|
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>
|
</FieldRoot>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -139,7 +153,9 @@ export default function ItemFormDrawer({
|
|||||||
value={field.state.value}
|
value={field.state.value}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.handleChange((e.target as HTMLInputElement).value)
|
field.handleChange(
|
||||||
|
(e.target as HTMLInputElement).value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRoot>
|
</FieldRoot>
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export const Route = createFileRoute("/.well-known/oauth-authorization-server")(
|
|||||||
GET: ({ request }) => handler(request),
|
GET: ({ request }) => handler(request),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const Route = createFileRoute("/.well-known/oauth-protected-resource")({
|
|||||||
"Cache-Control":
|
"Cache-Control":
|
||||||
"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function RootDocument() {
|
|||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div className="grid h-svh grid-rows-[auto_1fr]">
|
<div className="grid min-h-svh grid-rows-[auto_1fr]">
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/consent")({
|
export const Route = createFileRoute("/consent")({
|
||||||
validateSearch: (search: Record<string, unknown>) => ({
|
validateSearch: (search: Record<string, unknown>) => ({
|
||||||
client_id: (search.client_id as string) ?? "",
|
client_id: (search.client_id as string | undefined) ?? "",
|
||||||
scope: (search.scope as string) ?? "",
|
scope: (search.scope as string | undefined) ?? "",
|
||||||
}),
|
}),
|
||||||
component: ConsentComponent,
|
component: ConsentComponent,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { toast } from "sonner";
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { createTimeline } from "@/functions/create-timeline";
|
import { createTimeline } from "@/functions/create-timeline";
|
||||||
import { Button } from "@/components/ui/button";
|
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("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: HomeComponent,
|
component: HomeComponent,
|
||||||
@@ -22,9 +27,14 @@ function HomeComponent() {
|
|||||||
try {
|
try {
|
||||||
await authClient.signIn.anonymous();
|
await authClient.signIn.anonymous();
|
||||||
const timeline = await createTimeline({ data: { title: value.title } });
|
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) {
|
} 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)
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FieldError match="valueMissing">
|
<FieldError match="valueMissing">Name is required</FieldError>
|
||||||
Name is required
|
|
||||||
</FieldError>
|
|
||||||
</FieldRoot>
|
</FieldRoot>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { timelineQueryOptions } from "@/functions/get-timeline";
|
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";
|
import { FlutterView } from "@/components/flutter-view";
|
||||||
|
|
||||||
export const Route = createFileRoute("/timeline/$timelineId")({
|
export const Route = createFileRoute("/timeline/$timelineId")({
|
||||||
@@ -44,11 +42,16 @@ function RouteComponent() {
|
|||||||
[timeline, selectedItemId]
|
[timeline, selectedItemId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [flutterHeight, setFlutterHeight] = useState<number | undefined>();
|
||||||
|
|
||||||
const handleEvent = useCallback(
|
const handleEvent = useCallback(
|
||||||
(event: { type: string; payload?: Record<string, unknown> }) => {
|
(event: { type: string; payload?: Record<string, unknown> }) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case "content_height":
|
||||||
|
setFlutterHeight(event.payload?.height as number);
|
||||||
|
break;
|
||||||
case "item_selected":
|
case "item_selected":
|
||||||
setSelectedItemId((event.payload?.itemId as string) ?? null);
|
// setSelectedItemId((event.payload?.itemId as string) ?? null);
|
||||||
break;
|
break;
|
||||||
case "item_deselected":
|
case "item_deselected":
|
||||||
setSelectedItemId(null);
|
setSelectedItemId(null);
|
||||||
@@ -59,13 +62,16 @@ function RouteComponent() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col px-4 py-6">
|
<div className="flex flex-col">
|
||||||
<h1 className="text-3xl font-serif font-bold mb-6">{timeline.title}</h1>
|
<h1 className="text-3xl font-serif font-bold mb-6 mx-4">
|
||||||
|
{timeline.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<FlutterView
|
<FlutterView
|
||||||
state={flutterState}
|
state={flutterState}
|
||||||
onEvent={handleEvent}
|
onEvent={handleEvent}
|
||||||
className="min-h-0 flex-1 rounded-lg overflow-hidden"
|
className="overflow-hidden"
|
||||||
|
height={flutterHeight}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
jsonb,
|
||||||
pgTable,
|
pgTable,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
boolean,
|
|
||||||
uuid,
|
uuid,
|
||||||
jsonb,
|
|
||||||
index,
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
@@ -43,7 +43,7 @@ export const session = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
},
|
},
|
||||||
(table) => [index("session_userId_idx").on(table.userId)],
|
(table) => [index("session_userId_idx").on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const account = pgTable(
|
export const account = pgTable(
|
||||||
@@ -69,7 +69,7 @@ export const account = pgTable(
|
|||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull(),
|
||||||
},
|
},
|
||||||
(table) => [index("account_userId_idx").on(table.userId)],
|
(table) => [index("account_userId_idx").on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const verification = pgTable(
|
export const verification = pgTable(
|
||||||
@@ -87,7 +87,7 @@ export const verification = pgTable(
|
|||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull(),
|
||||||
},
|
},
|
||||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
(table) => [index("verification_identifier_idx").on(table.identifier)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const jwks = pgTable("jwks", {
|
export const jwks = pgTable("jwks", {
|
||||||
@@ -240,7 +240,7 @@ export const oauthRefreshTokenRelations = relations(
|
|||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
oauthAccessTokens: many(oauthAccessToken),
|
oauthAccessTokens: many(oauthAccessToken),
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const oauthAccessTokenRelations = relations(
|
export const oauthAccessTokenRelations = relations(
|
||||||
@@ -262,7 +262,7 @@ export const oauthAccessTokenRelations = relations(
|
|||||||
fields: [oauthAccessToken.refreshId],
|
fields: [oauthAccessToken.refreshId],
|
||||||
references: [oauthRefreshToken.id],
|
references: [oauthRefreshToken.id],
|
||||||
}),
|
}),
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({
|
export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ class _MainAppState extends State<MainApp> {
|
|||||||
end: domain.end,
|
end: domain.end,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_emitContentHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TimelineGroup> _convertGroups(List<TimelineGroupData> groups) {
|
List<TimelineGroup> _convertGroups(List<TimelineGroupData> groups) {
|
||||||
@@ -168,6 +170,25 @@ class _MainAppState extends State<MainApp> {
|
|||||||
e,
|
e,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_emitContentHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitContentHeight() {
|
||||||
|
var totalHeight = 0.0;
|
||||||
|
for (final group in _groups) {
|
||||||
|
totalHeight += ZTimelineConstants.groupHeaderHeight;
|
||||||
|
final groupEntries = _entries.where((e) => e.groupId == group.id);
|
||||||
|
var maxLane = 0;
|
||||||
|
for (final e in groupEntries) {
|
||||||
|
if (e.lane > maxLane) maxLane = e.lane;
|
||||||
|
}
|
||||||
|
final lanesCount = maxLane.clamp(0, 1000);
|
||||||
|
totalHeight += lanesCount * ZTimelineConstants.laneHeight
|
||||||
|
+ (lanesCount > 0 ? (lanesCount - 1) * ZTimelineConstants.laneVerticalSpacing : 0)
|
||||||
|
+ ZTimelineConstants.verticalOuterPadding * 2;
|
||||||
|
}
|
||||||
|
emitEvent('content_height', {'height': totalHeight});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _labelForEntry(TimelineEntry entry) {
|
String _labelForEntry(TimelineEntry entry) {
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ class ZTimelineView extends StatelessWidget {
|
|||||||
: ZTimelineConstants.minContentWidth;
|
: ZTimelineConstants.minContentWidth;
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
itemCount: groups.length,
|
itemCount: groups.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final group = groups[index];
|
final group = groups[index];
|
||||||
@@ -128,6 +130,7 @@ class _GroupHeader extends StatelessWidget {
|
|||||||
final scheme = Theme.of(context).colorScheme;
|
final scheme = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
height: height,
|
height: height,
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: scheme.surfaceContainerHighest,
|
color: scheme.surfaceContainerHighest,
|
||||||
|
|||||||
Submodule timeline_poc deleted from cd7679dca0
Reference in New Issue
Block a user