add basic timeline view
This commit is contained in:
100
apps/web/src/components/group-form-drawer.tsx
Normal file
100
apps/web/src/components/group-form-drawer.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
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,
|
||||||
|
} from "./ui/drawer";
|
||||||
|
|
||||||
|
export default function GroupFormDrawer({ timelineId }: { timelineId: string }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
await createTimelineGroup({
|
||||||
|
data: { title: value.title, 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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerRoot open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger render={<Button variant="outline" />}>
|
||||||
|
Add Group
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerViewport>
|
||||||
|
<DrawerPopup>
|
||||||
|
<div className="flex items-center justify-between border-b border-border p-4">
|
||||||
|
<DrawerTitle>New Group</DrawerTitle>
|
||||||
|
<DrawerClose render={<Button variant="outline" size="sm" />}>
|
||||||
|
Close
|
||||||
|
</DrawerClose>
|
||||||
|
</div>
|
||||||
|
<DrawerContent className="p-4">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<form.Field name="title">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot>
|
||||||
|
<FieldLabel>Title</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
required
|
||||||
|
placeholder="Group title"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FieldError match="valueMissing">Title is required</FieldError>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
<form.Subscribe>
|
||||||
|
{(state) => (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!state.canSubmit || state.isSubmitting}
|
||||||
|
>
|
||||||
|
{state.isSubmitting ? "Creating..." : "Create Group"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
</DrawerContent>
|
||||||
|
</DrawerPopup>
|
||||||
|
</DrawerViewport>
|
||||||
|
</DrawerPortal>
|
||||||
|
</DrawerRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
166
apps/web/src/components/item-form-drawer.tsx
Normal file
166
apps/web/src/components/item-form-drawer.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
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,
|
||||||
|
} from "./ui/drawer";
|
||||||
|
|
||||||
|
export default function ItemFormDrawer({
|
||||||
|
timelineGroupId,
|
||||||
|
timelineId,
|
||||||
|
}: {
|
||||||
|
timelineGroupId: string;
|
||||||
|
timelineId: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
start: new Date().toISOString().split("T")[0],
|
||||||
|
end: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
await createTimelineItem({
|
||||||
|
data: {
|
||||||
|
title: value.title,
|
||||||
|
description: value.description,
|
||||||
|
start: value.start,
|
||||||
|
end: value.end || undefined,
|
||||||
|
timelineGroupId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerRoot open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger render={<Button variant="outline" size="sm" />}>
|
||||||
|
Add Item
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerViewport>
|
||||||
|
<DrawerPopup>
|
||||||
|
<div className="flex items-center justify-between border-b border-border p-4">
|
||||||
|
<DrawerTitle>New Item</DrawerTitle>
|
||||||
|
<DrawerClose render={<Button variant="outline" size="sm" />}>
|
||||||
|
Close
|
||||||
|
</DrawerClose>
|
||||||
|
</div>
|
||||||
|
<DrawerContent className="p-4">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<form.Field name="title">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot>
|
||||||
|
<FieldLabel>Title</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
required
|
||||||
|
placeholder="Item title"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FieldError match="valueMissing">Title is required</FieldError>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="description">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot>
|
||||||
|
<FieldLabel>Description</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
placeholder="Optional description"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="start">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot>
|
||||||
|
<FieldLabel>Start date</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FieldError match="valueMissing">Start date is required</FieldError>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="end">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot>
|
||||||
|
<FieldLabel>End date (optional)</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
type="date"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Subscribe>
|
||||||
|
{(state) => (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!state.canSubmit || state.isSubmitting}
|
||||||
|
>
|
||||||
|
{state.isSubmitting ? "Creating..." : "Create Item"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
</DrawerContent>
|
||||||
|
</DrawerPopup>
|
||||||
|
</DrawerViewport>
|
||||||
|
</DrawerPortal>
|
||||||
|
</DrawerRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
apps/web/src/functions/create-timeline-group.ts
Normal file
22
apps/web/src/functions/create-timeline-group.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { db } from "@zendegi/db";
|
||||||
|
import { timelineGroup } from "@zendegi/db/schema/timeline";
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { authMiddleware } from "@/middleware/auth";
|
||||||
|
|
||||||
|
export const createTimelineGroup = createServerFn({ method: "POST" })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.inputValidator(
|
||||||
|
z.object({ title: z.string().min(1), timelineId: z.string().uuid() })
|
||||||
|
)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const [newGroup] = await db
|
||||||
|
.insert(timelineGroup)
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
timelineId: data.timelineId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newGroup;
|
||||||
|
});
|
||||||
34
apps/web/src/functions/create-timeline-item.ts
Normal file
34
apps/web/src/functions/create-timeline-item.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { db } from "@zendegi/db";
|
||||||
|
import { timelineItem } from "@zendegi/db/schema/timeline";
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { authMiddleware } from "@/middleware/auth";
|
||||||
|
|
||||||
|
export const createTimelineItem = createServerFn({ method: "POST" })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.inputValidator(
|
||||||
|
z.object({
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().default(""),
|
||||||
|
start: z.string().transform((s) => new Date(s)),
|
||||||
|
end: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? new Date(s) : undefined)),
|
||||||
|
timelineGroupId: z.string().uuid(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const [newItem] = await db
|
||||||
|
.insert(timelineItem)
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
start: data.start,
|
||||||
|
end: data.end,
|
||||||
|
timelineGroupId: data.timelineGroupId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
});
|
||||||
20
apps/web/src/functions/create-timeline.ts
Normal file
20
apps/web/src/functions/create-timeline.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { db } from "@zendegi/db";
|
||||||
|
import { timeline } from "@zendegi/db/schema/timeline";
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { authMiddleware } from "@/middleware/auth";
|
||||||
|
|
||||||
|
export const createTimeline = createServerFn({ method: "POST" })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.inputValidator(z.object({ title: z.string().min(1) }))
|
||||||
|
.handler(async ({ data, context }) => {
|
||||||
|
const [newTimeline] = await db
|
||||||
|
.insert(timeline)
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
ownerId: context.session!.user.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newTimeline;
|
||||||
|
});
|
||||||
36
apps/web/src/functions/get-timeline.ts
Normal file
36
apps/web/src/functions/get-timeline.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@zendegi/db";
|
||||||
|
import { timeline } from "@zendegi/db/schema/timeline";
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getTimeline = createServerFn({ method: "GET" })
|
||||||
|
.inputValidator(z.object({ id: z.string().uuid() }))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const result = await db.query.timeline.findFirst({
|
||||||
|
where: eq(timeline.id, data.id),
|
||||||
|
with: {
|
||||||
|
groups: {
|
||||||
|
orderBy: (g, { asc }) => [asc(g.sortOrder)],
|
||||||
|
with: {
|
||||||
|
items: {
|
||||||
|
orderBy: (i, { asc }) => [asc(i.start)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("Timeline not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const timelineQueryOptions = (timelineId: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["timeline", timelineId],
|
||||||
|
queryFn: () => getTimeline({ data: { id: timelineId } }),
|
||||||
|
});
|
||||||
@@ -1,10 +1,34 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
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";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: HomeComponent,
|
component: HomeComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function HomeComponent() {
|
function HomeComponent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
await authClient.signIn.anonymous();
|
||||||
|
const timeline = await createTimeline({ data: { title: value.title } });
|
||||||
|
navigate({ to: "/timeline/$timelineId", params: { timelineId: timeline.id } });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to create timeline");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container mx-auto max-w-3xl px-4 py-2">
|
<section className="container mx-auto max-w-3xl px-4 py-2">
|
||||||
<div className="space-y-7">
|
<div className="space-y-7">
|
||||||
@@ -21,6 +45,44 @@ function HomeComponent() {
|
|||||||
productivity, and transform how you experience your world.
|
productivity, and transform how you experience your world.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="flex items-end gap-3"
|
||||||
|
>
|
||||||
|
<form.Field name="title">
|
||||||
|
{(field) => (
|
||||||
|
<FieldRoot className="flex-1">
|
||||||
|
<FieldLabel>Timeline name</FieldLabel>
|
||||||
|
<FieldControl
|
||||||
|
required
|
||||||
|
placeholder="My Timeline"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FieldError match="valueMissing">
|
||||||
|
Name is required
|
||||||
|
</FieldError>
|
||||||
|
</FieldRoot>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
<form.Subscribe>
|
||||||
|
{(state) => (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!state.canSubmit || state.isSubmitting}
|
||||||
|
>
|
||||||
|
{state.isSubmitting ? "Creating..." : "Create"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,64 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
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";
|
||||||
|
|
||||||
export const Route = createFileRoute("/timeline/$timelineId")({
|
export const Route = createFileRoute("/timeline/$timelineId")({
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
await context.queryClient.ensureQueryData(
|
||||||
|
timelineQueryOptions(params.timelineId)
|
||||||
|
);
|
||||||
|
},
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Timeline detail</div>;
|
const { timelineId } = Route.useParams();
|
||||||
|
const timelineQuery = useSuspenseQuery(timelineQueryOptions(timelineId));
|
||||||
|
const timeline = timelineQuery.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto max-w-3xl px-4 py-6 space-y-6">
|
||||||
|
<h1 className="text-3xl font-serif font-bold">{timeline.title}</h1>
|
||||||
|
|
||||||
|
{timeline.groups.length === 0 && (
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No groups yet. Add a group to start building your timeline.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{timeline.groups.map((group) => (
|
||||||
|
<div key={group.id} className="border border-border rounded-lg p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold">{group.title}</h2>
|
||||||
|
<ItemFormDrawer timelineGroupId={group.id} timelineId={timelineId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{group.items.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">No items in this group yet.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<li key={item.id} className="border border-border rounded p-3">
|
||||||
|
<div className="font-medium">{item.title}</div>
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{new Date(item.start).toLocaleDateString()}
|
||||||
|
{item.end && ` — ${new Date(item.end).toLocaleDateString()}`}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GroupFormDrawer timelineId={timelineId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,5 @@ export const Route = createFileRoute("/timeline")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return (
|
return <Outlet />;
|
||||||
<div>
|
|
||||||
Shell of timeline
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,17 @@ export const timelineRelations = relations(timeline, ({ many }) => ({
|
|||||||
groups: many(timelineGroup),
|
groups: many(timelineGroup),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const groupRelations = relations(timelineGroup, ({ many }) => ({
|
export const groupRelations = relations(timelineGroup, ({ one, many }) => ({
|
||||||
|
timeline: one(timeline, {
|
||||||
|
fields: [timelineGroup.timelineId],
|
||||||
|
references: [timeline.id],
|
||||||
|
}),
|
||||||
items: many(timelineItem),
|
items: many(timelineItem),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const itemRelations = relations(timelineItem, ({ one }) => ({
|
||||||
|
timelineGroup: one(timelineGroup, {
|
||||||
|
fields: [timelineItem.timelineGroupId],
|
||||||
|
references: [timelineGroup.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user