Skip to content

Commit

Permalink
Add zod-validation-error package and refactor event forms to use upda…
Browse files Browse the repository at this point in the history
…ted structure
  • Loading branch information
arjunkomath committed Jan 26, 2025
1 parent 6410416 commit 51d8074
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 315 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import PageSection from "@/components/core/section";
import { SaveButton } from "@/components/form/button";
import EventForm from "@/components/form/event";
import PageTitle from "@/components/layout/page-title";
import { buttonVariants } from "@/components/ui/button";
import { CardContent, CardFooter } from "@/components/ui/card";
import { calendarEvent } from "@/drizzle/schema";
import { database } from "@/lib/utils/useDatabase";
import { getOwner } from "@/lib/utils/useOwner";
import { allUsers } from "@/lib/utils/useUser";
import { eq } from "drizzle-orm";
import Link from "next/link";
import { notFound } from "next/navigation";
import { updateEvent } from "../../actions";

type Props = {
params: Promise<{
Expand All @@ -22,7 +16,6 @@ type Props = {

export default async function EditEvent(props: Props) {
const params = await props.params;
const { orgSlug } = await getOwner();
const { projectId, eventId } = params;

const users = await allUsers();
Expand Down Expand Up @@ -54,32 +47,12 @@ export default async function EditEvent(props: Props) {
return notFound();
}

const backUrl = `/${orgSlug}/projects/${projectId}/events`;

return (
<>
<PageTitle title="Edit Event" />

<PageSection topInset>
<form action={updateEvent}>
<input type="hidden" name="id" defaultValue={eventId} />
<input type="hidden" name="projectId" defaultValue={projectId} />
<CardContent>
<EventForm item={event} users={users} />
</CardContent>
<CardFooter>
<div className="ml-auto flex items-center justify-end gap-x-6">
<Link
href={backUrl}
className={buttonVariants({ variant: "ghost" })}
prefetch={false}
>
Cancel
</Link>
<SaveButton />
</div>
</CardFooter>
</form>
<EventForm users={users} projectId={projectId} item={event} />
</PageSection>
</>
);
Expand Down
234 changes: 124 additions & 110 deletions app/(dashboard)/[tenant]/projects/[projectId]/events/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { generateObjectDiffMessage, logActivity } from "@/lib/activity";
import { toEndOfDay, toMachineDateString } from "@/lib/utils/date";
import { database } from "@/lib/utils/useDatabase";
import { getOwner, getTimezone } from "@/lib/utils/useOwner";
import { isAfter } from "date-fns";
import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { type Frequency, RRule } from "rrule";
import { ZodError, ZodIssueCode, z } from "zod";
import { fromError } from "zod-validation-error";

const eventInputSchema = z.object({
projectId: z.string(),
Expand Down Expand Up @@ -71,7 +73,7 @@ function handleEventPayload(payload: FormData): {
event.end = toEndOfDay(event.end);
}

if (event.end && event.end > event.start) {
if (event.end && isAfter(event.start, event.end)) {
throw new ZodError([
{
message: "End date must be after start date",
Expand All @@ -84,138 +86,150 @@ function handleEventPayload(payload: FormData): {
return event;
}

export async function createEvent(payload: FormData) {
export async function createEvent(_: unknown, payload: FormData) {
const { userId, orgSlug } = await getOwner();

const {
projectId,
name,
description,
start,
end,
allDay,
repeatRule,
invites,
} = handleEventPayload(payload);

const db = await database();
const createdEvent = db
.insert(calendarEvent)
.values({
let redirectPath: string;
try {
const {
projectId,
name,
description,
start,
end,
allDay,
repeatRule,
projectId,
createdByUser: userId,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()
.get();
invites,
} = handleEventPayload(payload);

for (const userId of invites) {
db.insert(eventInvite)
const db = await database();
const createdEvent = db
.insert(calendarEvent)
.values({
eventId: createdEvent.id,
userId,
status: "invited",
name,
description,
start,
end,
allDay,
repeatRule,
projectId,
createdByUser: userId,
createdAt: new Date(),
updatedAt: new Date(),
})
.run();
}
.returning()
.get();

for (const userId of invites) {
db.insert(eventInvite)
.values({
eventId: createdEvent.id,
userId,
status: "invited",
})
.run();
}

await logActivity({
action: "created",
type: "event",
message: `Created event ${name}`,
projectId: +projectId,
});
await logActivity({
action: "created",
type: "event",
message: `Created event ${name}`,
projectId: +projectId,
});

const timezone = await getTimezone();
const timezone = await getTimezone();
redirectPath = `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`;
revalidatePath(`/${orgSlug}/projects/${projectId}/events`);
} catch (error) {
console.error(error);
return {
message: "An error occurred while creating the event",
};
}

revalidatePath(`/${orgSlug}/projects/${projectId}/events`);
redirect(
`/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`,
);
redirect(redirectPath);
}

export async function updateEvent(payload: FormData) {
const { orgSlug } = await getOwner();
const id = +(payload.get("id") as string);
export async function updateEvent(_: unknown, payload: FormData) {
let redirectPath: string;
try {
const { orgSlug } = await getOwner();
const id = +(payload.get("id") as string);

const {
projectId,
name,
description,
start,
end,
allDay,
repeatRule,
invites,
} = handleEventPayload(payload);

const db = await database();
db.delete(eventInvite).where(eq(eventInvite.eventId, id)).run();

for (const userId of invites) {
db.insert(eventInvite)
.values({
eventId: id,
userId,
status: "invited",
})
.run();
}

const currentEvent = await db.query.calendarEvent
.findFirst({
where: and(
eq(calendarEvent.id, id),
eq(calendarEvent.projectId, +projectId),
),
})
.execute();

db.update(calendarEvent)
.set({
const {
projectId,
name,
description,
start,
end,
allDay,
repeatRule: repeatRule?.toString(),
updatedAt: new Date(),
})
.where(
and(eq(calendarEvent.id, id), eq(calendarEvent.projectId, +projectId)),
)
.run();

if (currentEvent)
await logActivity({
action: "updated",
type: "event",
message: `Updated event ${name}, ${generateObjectDiffMessage(
currentEvent,
{
name,
description,
start,
end,
allDay,
},
)}`,
projectId: +projectId,
});
repeatRule,
invites,
} = handleEventPayload(payload);

const db = await database();
db.delete(eventInvite).where(eq(eventInvite.eventId, id)).run();

for (const userId of invites) {
db.insert(eventInvite)
.values({
eventId: id,
userId,
status: "invited",
})
.run();
}

const currentEvent = await db.query.calendarEvent
.findFirst({
where: and(
eq(calendarEvent.id, id),
eq(calendarEvent.projectId, +projectId),
),
})
.execute();

db.update(calendarEvent)
.set({
name,
description,
start,
end,
allDay,
repeatRule: repeatRule?.toString(),
updatedAt: new Date(),
})
.where(
and(eq(calendarEvent.id, id), eq(calendarEvent.projectId, +projectId)),
)
.run();

const timezone = await getTimezone();
if (currentEvent)
await logActivity({
action: "updated",
type: "event",
message: `Updated event ${name}, ${generateObjectDiffMessage(
currentEvent,
{
name,
description,
start,
end,
allDay,
},
)}`,
projectId: +projectId,
});

const timezone = await getTimezone();
revalidatePath(`/${orgSlug}/projects/${projectId}/events`);
redirectPath = `/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`;
} catch (error) {
return {
message: fromError(error).toString(),
};
}

revalidatePath(`/${orgSlug}/projects/${projectId}/events`);
redirect(
`/${orgSlug}/projects/${projectId}/events?on=${toMachineDateString(start, timezone)}`,
);
redirect(redirectPath);
}

export async function deleteEvent(payload: FormData) {
Expand Down
34 changes: 5 additions & 29 deletions app/(dashboard)/[tenant]/projects/[projectId]/events/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import PageSection from "@/components/core/section";
import { SaveButton } from "@/components/form/button";
import EventForm from "@/components/form/event";
import PageTitle from "@/components/layout/page-title";
import { buttonVariants } from "@/components/ui/button";
import { CardContent, CardFooter } from "@/components/ui/card";
import { getOwner } from "@/lib/utils/useOwner";
import { allUsers } from "@/lib/utils/useUser";
import Link from "next/link";
import { createEvent } from "../actions";

type Props = {
params: Promise<{
Expand All @@ -21,8 +16,6 @@ type Props = {
export default async function CreateEvent(props: Props) {
const params = await props.params;
const searchParams = await props.searchParams;
const { orgSlug } = await getOwner();
const backUrl = `/${orgSlug}/projects/${params.projectId}/events`;

const users = await allUsers();

Expand All @@ -31,28 +24,11 @@ export default async function CreateEvent(props: Props) {
<PageTitle title="Create Event" />

<PageSection topInset>
<form action={createEvent}>
<input
type="hidden"
name="projectId"
defaultValue={params.projectId}
/>
<CardContent>
<EventForm users={users} on={searchParams.on} />
</CardContent>
<CardFooter>
<div className="ml-auto flex items-center justify-end gap-x-6">
<Link
href={backUrl}
className={buttonVariants({ variant: "ghost" })}
prefetch={false}
>
Cancel
</Link>
<SaveButton />
</div>
</CardFooter>
</form>
<EventForm
users={users}
on={searchParams.on}
projectId={params.projectId}
/>
</PageSection>
</>
);
Expand Down
Loading

0 comments on commit 51d8074

Please sign in to comment.