add and edit
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "./ui/button";
|
||||
import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
|
||||
import { inputClasses } from "./ui/input";
|
||||
import {
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
@@ -12,63 +13,101 @@ import {
|
||||
DrawerPortal,
|
||||
DrawerRoot,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
DrawerViewport,
|
||||
} from "./ui/drawer";
|
||||
import type { NormalizedGroup, NormalizedItem } from "@/functions/get-timeline";
|
||||
import { updateTimelineItem } from "@/functions/update-timeline-item";
|
||||
import { createTimelineItem } from "@/functions/create-timeline-item";
|
||||
|
||||
export default function ItemFormDrawer({
|
||||
timelineGroupId,
|
||||
timelineId,
|
||||
}: {
|
||||
timelineGroupId: string;
|
||||
type ItemFormDrawerProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
timelineId: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
groups: Record<string, NormalizedGroup | undefined>;
|
||||
groupOrder: Array<string>;
|
||||
editItem?: NormalizedItem | null;
|
||||
};
|
||||
|
||||
export default function ItemFormDrawer({
|
||||
open,
|
||||
onOpenChange,
|
||||
timelineId,
|
||||
groups,
|
||||
groupOrder,
|
||||
editItem,
|
||||
}: ItemFormDrawerProps) {
|
||||
const isEdit = !!editItem;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const getDefaultValues = useCallback(
|
||||
() => ({
|
||||
title: editItem?.title ?? "",
|
||||
description: editItem?.description ?? "",
|
||||
start: editItem
|
||||
? editItem.start.toISOString().split("T")[0]
|
||||
: new Date().toISOString().split("T")[0],
|
||||
end: editItem?.end ? editItem.end.toISOString().split("T")[0] : "",
|
||||
groupId: (editItem?.groupId ?? groupOrder[0]) || "",
|
||||
}),
|
||||
[editItem, groupOrder]
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
start: new Date().toISOString().split("T")[0],
|
||||
end: "",
|
||||
},
|
||||
defaultValues: getDefaultValues(),
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
await createTimelineItem({
|
||||
data: {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
start: value.start,
|
||||
end: value.end || undefined,
|
||||
timelineGroupId,
|
||||
},
|
||||
});
|
||||
if (isEdit) {
|
||||
await updateTimelineItem({
|
||||
data: {
|
||||
id: editItem.id,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
start: value.start,
|
||||
end: value.end || null,
|
||||
timelineGroupId: value.groupId,
|
||||
lane: editItem.lane,
|
||||
},
|
||||
});
|
||||
toast.success("Item updated");
|
||||
} else {
|
||||
await createTimelineItem({
|
||||
data: {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
start: value.start,
|
||||
end: value.end || null,
|
||||
timelineGroupId: value.groupId,
|
||||
},
|
||||
});
|
||||
toast.success("Item created");
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["timeline", timelineId],
|
||||
});
|
||||
setOpen(false);
|
||||
onOpenChange(false);
|
||||
form.reset();
|
||||
toast.success("Item created");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to create item"
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Failed to ${isEdit ? "update" : "create"} item`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
form.reset(getDefaultValues());
|
||||
}, [open, getDefaultValues, form]);
|
||||
|
||||
return (
|
||||
<DrawerRoot open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger render={<Button variant="outline" size="sm" />}>
|
||||
Add Item
|
||||
</DrawerTrigger>
|
||||
<DrawerRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DrawerPortal>
|
||||
<DrawerViewport>
|
||||
<DrawerPopup>
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
<DrawerTitle>New Item</DrawerTitle>
|
||||
<DrawerTitle>{isEdit ? "Edit Item" : "New Item"}</DrawerTitle>
|
||||
<DrawerClose render={<Button variant="outline" size="sm" />}>
|
||||
Close
|
||||
</DrawerClose>
|
||||
@@ -122,6 +161,26 @@ export default function ItemFormDrawer({
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="groupId">
|
||||
{(field) => (
|
||||
<FieldRoot>
|
||||
<FieldLabel>Group</FieldLabel>
|
||||
<select
|
||||
className={inputClasses}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
>
|
||||
{groupOrder.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
{groups[id]?.title ?? id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FieldRoot>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="start">
|
||||
{(field) => (
|
||||
<FieldRoot>
|
||||
@@ -168,7 +227,13 @@ export default function ItemFormDrawer({
|
||||
type="submit"
|
||||
disabled={!state.canSubmit || state.isSubmitting}
|
||||
>
|
||||
{state.isSubmitting ? "Creating..." : "Create Item"}
|
||||
{state.isSubmitting
|
||||
? isEdit
|
||||
? "Saving..."
|
||||
: "Creating..."
|
||||
: isEdit
|
||||
? "Save Changes"
|
||||
: "Create Item"}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
|
||||
@@ -26,7 +26,14 @@ const { viewport, popup, title, description, close, content } = drawerStyles();
|
||||
type DrawerRootProps = React.ComponentProps<typeof BaseDrawer.Root>;
|
||||
|
||||
function DrawerRoot(props: DrawerRootProps) {
|
||||
return <BaseDrawer.Root modal={false} swipeDirection="right" {...props} />;
|
||||
return (
|
||||
<BaseDrawer.Root
|
||||
modal={false}
|
||||
swipeDirection="right"
|
||||
disablePointerDismissal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type DrawerTriggerProps = React.ComponentProps<typeof BaseDrawer.Trigger>;
|
||||
|
||||
@@ -15,8 +15,8 @@ export const createTimelineItem = createServerFn({ method: "POST" })
|
||||
start: z.string().transform((s) => new Date(s)),
|
||||
end: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((s) => (s ? new Date(s) : undefined)),
|
||||
.nullable()
|
||||
.transform((s) => (s ? new Date(s) : null)),
|
||||
timelineGroupId: z.string().uuid(),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ export const updateTimelineItem = createServerFn({ method: "POST" })
|
||||
.transform((s) => (s ? new Date(s) : null)),
|
||||
timelineGroupId: z.string().uuid(),
|
||||
lane: z.number().int().min(1),
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.handler(async ({ data }) => {
|
||||
@@ -27,6 +29,10 @@ export const updateTimelineItem = createServerFn({ method: "POST" })
|
||||
end: data.end,
|
||||
timelineGroupId: data.timelineGroupId,
|
||||
lane: data.lane,
|
||||
...(data.title !== undefined && { title: data.title }),
|
||||
...(data.description !== undefined && {
|
||||
description: data.description,
|
||||
}),
|
||||
})
|
||||
.where(eq(timelineItem.id, data.id))
|
||||
.returning();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* lanes on item creation.
|
||||
*/
|
||||
export function assignLane(
|
||||
existingItems: { start: Date; end: Date | null; lane: number }[],
|
||||
existingItems: Array<{ start: Date; end: Date | null; lane: number }>,
|
||||
newStart: Date,
|
||||
newEnd: Date | null
|
||||
): number {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -24,11 +25,16 @@ 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 },
|
||||
@@ -62,9 +68,11 @@ function RouteComponent() {
|
||||
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);
|
||||
@@ -77,6 +85,18 @@ function RouteComponent() {
|
||||
[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">
|
||||
@@ -91,7 +111,7 @@ function RouteComponent() {
|
||||
<h1 className="text-lg font-serif font-bold">{timeline.title}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" onClick={handleAddItem}>
|
||||
<Plus className="size-4" />
|
||||
Add Item
|
||||
</Button>
|
||||
@@ -120,6 +140,15 @@ function RouteComponent() {
|
||||
className="overflow-hidden"
|
||||
height={flutterHeight}
|
||||
/>
|
||||
|
||||
<ItemFormDrawer
|
||||
open={drawerOpen}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
timelineId={timelineId}
|
||||
groups={timeline.groups}
|
||||
groupOrder={timeline.groupOrder}
|
||||
editItem={editItem}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user