Skip to content

Commit

Permalink
Fixed all errors observed on next. Needs production testing since ser…
Browse files Browse the repository at this point in the history
…ver actions behave a little differently.
  • Loading branch information
beabravedude committed Jan 22, 2025
1 parent 1dcedb8 commit 9b5e949
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 54 deletions.
21 changes: 17 additions & 4 deletions actions/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { GridFilterItem } from "@mui/x-data-grid";
import { GridPaginationModel } from "@mui/x-data-grid";
import { GridSortModel } from "@mui/x-data-grid";
import { EventType, Prisma } from "@prisma/client";
import { z } from "zod";
import { SafeParseReturnType, z } from "zod";
import { UTApi } from "uploadthing/server";
import { revalidatePath } from "next/cache";
import { sendEventPositionRemovalEmail } from "./mail/event";
import { User } from "next-auth";
import { ZodErrorSlimResponse } from "@/types";

const MAX_FILE_SIZE = 1024 * 1024 * 4;
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
Expand Down Expand Up @@ -69,7 +70,7 @@ const getWhere = (filter?: GridFilterItem): Prisma.EventWhereInput => {
}
}

export const validateEvent = async (input: { [key: string]: any }) => {
export const validateEvent = async (input: { [key: string]: any }, zodResponse?: boolean): Promise<ZodErrorSlimResponse | SafeParseReturnType<any, any>> => {

const isAfterToday = (date: Date) => {
const today = new Date();
Expand Down Expand Up @@ -125,7 +126,19 @@ export const validateEvent = async (input: { [key: string]: any }) => {
path: ["bannerImage", "bannerUrl"],
})

return eventZ.safeParse(input);
const data = eventZ.safeParse(input);

if (zodResponse) {
return data;
}

return {
success: data.success,
errors: data.error ? data.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})) : [],
}
}

export const upsertEvent = async (formData: FormData) => {
Expand All @@ -140,7 +153,7 @@ export const upsertEvent = async (formData: FormData) => {
bannerImage: formData.get('bannerImage') as File,
bannerUrl: (formData.get('bannerUrl') as string) || undefined,
featuredFields: JSON.parse(formData.get('featuredFields') as string),
});
}, true) as SafeParseReturnType<any, any>;

if (!result.success) {
return { errors: result.error.errors };
Expand Down
50 changes: 45 additions & 5 deletions actions/eventPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import prisma from "@/lib/db";
import { Event, EventPosition } from "@prisma/client";
import { getServerSession, User } from "next-auth";
import { after } from "next/server";
import { z } from "zod";
import { SafeParseReturnType, z } from "zod";
import { log } from "./log";
import { revalidatePath } from "next/cache";
import { sendEventPositionEmail, sendEventPositionRemovalEmail, sendEventPositionRequestDeletedEmail } from "./mail/event";
import { ZodErrorSlimResponse } from "@/types";

export const toggleManualPositionOpen = async (event: Event) => {

Expand Down Expand Up @@ -171,7 +172,7 @@ export const deleteEventPosition = async (event: Event, eventPositionId: string,

}

export const validateFinalEventPosition = async (event: Event, formData: FormData) => {
export const validateFinalEventPosition = async (event: Event, formData: FormData, zodResponse?: boolean): Promise<ZodErrorSlimResponse | SafeParseReturnType<any, any>> => {
const eventPositionZ = z.object({
finalPosition: z.string().min(1, { message: 'Final Position is required and could not be autofilled.' }).max(50, { message: 'Final Position must be less than 50 characters' }),
finalStartTime: z.date().min(event.start, { message: 'Final time must be within the event' }).max(event.end, { message: 'Final time must be within the event' }),
Expand All @@ -185,17 +186,29 @@ export const validateFinalEventPosition = async (event: Event, formData: FormDat
finalPosition = requestedPosition;
}

return eventPositionZ.safeParse({
const data = eventPositionZ.safeParse({
finalPosition,
finalStartTime: new Date(formData.get('finalStartTime') as string),
finalEndTime: new Date(formData.get('finalEndTime') as string),
finalNotes: formData.get('finalNotes'),
});

if (zodResponse) {
return data;
}

return {
success: data.success,
errors: data.error ? data.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})) : [],
};
}

export const adminSaveEventPosition = async (event: Event, position: EventPosition, formData: FormData) => {

const result = await validateFinalEventPosition(event, formData);
const result = await validateFinalEventPosition(event, formData, true) as SafeParseReturnType<any, any>;

if (!result.success) {
return { errors: result.error.errors };
Expand Down Expand Up @@ -232,12 +245,37 @@ export const adminSaveEventPosition = async (event: Event, position: EventPositi
}

export const publishEventPosition = async (event: Event, position: EventPosition) => {

const formData = new FormData();
let finalPosition = position.finalPosition || '';

if (!finalPosition && event.presetPositions.includes(position.requestedPosition)) {
finalPosition = position.requestedPosition;
}

formData.set('finalPosition', finalPosition);
formData.set('finalStartTime', position.finalStartTime?.toISOString() || position.requestedStartTime?.toISOString() || '');
formData.set('finalEndTime', position.finalEndTime?.toISOString() || position.requestedEndTime?.toISOString() || '');
formData.set('finalNotes', position.finalNotes || '');

const result = await validateFinalEventPosition(event, formData, true) as SafeParseReturnType<any, any>;

if (!result.success) {
return { error: {
success: false,
errors: result.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})),
} as ZodErrorSlimResponse };
}

const eventPosition = await prisma.eventPosition.update({
where: {
id: position.id,
},
data: {
finalPosition: position.finalPosition,
finalPosition,
published: true,
},
include: {
Expand Down Expand Up @@ -274,6 +312,8 @@ export const unpublishEventPosition = async (event: Event, position: EventPositi
after(async () => {
if (eventPosition) {
await log("UPDATE", "EVENT_POSITION", `Unpublished event position for ${eventPosition.user?.firstName} ${eventPosition.user?.lastName} for ${eventPosition.finalPosition} from ${eventPosition.finalStartTime?.toUTCString()} to ${eventPosition.finalEndTime?.toUTCString()}`);

sendEventPositionRemovalEmail(eventPosition.user as User, eventPosition, event);
}
});

Expand Down
76 changes: 42 additions & 34 deletions components/Event/EventForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import MarkdownEditor from "@uiw/react-markdown-editor";
import dayjs, { Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc";
import Form from "next/form";
import { ReactNode, useCallback, useEffect, useState } from "react";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import FormSaveButton from "../Form/FormSaveButton";
import { upsertEvent, validateEvent } from "@/actions/event";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import { SafeParseReturnType, ZodIssue } from "zod";
import Markdown from "react-markdown";
import { ZodErrorSlimResponse } from "@/types";

export default function EventForm({ event }: { event?: Event, }) {

Expand Down Expand Up @@ -44,32 +45,42 @@ export default function EventForm({ event }: { event?: Event, }) {
<CircularProgress key={5} color="inherit" size={20} />,
]);

const updateStatus = useCallback(async () => {
const debounce = (func: Function, wait: number) => {
let timeout: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};

const res = await validateEvent({
id: event?.id,
name,
start: start?.toDate(),
end: end?.toDate(),
type: type?.toString() || '',
description,
bannerUrl,
featuredFields,
});
const debouncedUpdateStatus = useMemo(
() => debounce(async () => {
const res = await validateEvent({
id: event?.id,
name,
start: start?.toDate(),
end: end?.toDate(),
type: type?.toString() || '',
description,
bannerUrl,
featuredFields,
}) as ZodErrorSlimResponse;

const firstStep = await getStepStatus(res, { name, start: start?.toDate(), end: end?.toDate(), });
const firstStep = await getStepStatus(res, { name, start: start?.toDate(), end: end?.toDate(), });

if (event?.archived) {
setStatus([<CheckCircle key={1} color="success" />, <CheckCircle key={2} color="success" />, <CheckCircle key={3} color="success" />, <CheckCircle key={4} color="success" />, <CheckCircle key={5} color="success" />]);
return;
}

const secondStep = await getStepStatus(res, { type: type?.toString() || '' });
const thirdStep = await getStepStatus(res, { description });
const fourthStep = await getStepStatus(res, { bannerUrl });
const fifthStep = await getStepStatus(res, { featuredFields });
setStatus([firstStep, secondStep, thirdStep, fourthStep, fifthStep]);
}, [event?.archived, event?.id, name, start, end, type, description, bannerUrl, featuredFields]);
if (event?.archived) {
setStatus([<CheckCircle key={1} color="success" />, <CheckCircle key={2} color="success" />, <CheckCircle key={3} color="success" />, <CheckCircle key={4} color="success" />, <CheckCircle key={5} color="success" />]);
return;
}

const secondStep = await getStepStatus(res, { type: type?.toString() || '' });
const thirdStep = await getStepStatus(res, { description });
const fourthStep = await getStepStatus(res, { bannerUrl });
const fifthStep = await getStepStatus(res, { featuredFields });
setStatus([firstStep, secondStep, thirdStep, fourthStep, fifthStep]);
}, 500),
[event?.archived, event?.id, name, start, end, type, description, bannerUrl, featuredFields]
);

const handleSubmit = async (formData: FormData) => {

Expand Down Expand Up @@ -101,21 +112,21 @@ export default function EventForm({ event }: { event?: Event, }) {
}

useEffect(() => {
updateStatus();
}, [updateStatus]);
debouncedUpdateStatus();
}, [debouncedUpdateStatus]);

const handleOpen = (panel: number) => (event: React.SyntheticEvent, isExpanded: boolean) => {
setOpen(isExpanded ? panel : -1);
}

const back = () => {
setOpen((prev) => prev - 1);
updateStatus();
debouncedUpdateStatus();
}

const forward = () => {
setOpen((prev) => prev + 1);
updateStatus();
debouncedUpdateStatus();
}

const NextButton =
Expand Down Expand Up @@ -334,16 +345,13 @@ const getDescription = (type: EventType) => {
}
}

const getStepStatus = async (parse: SafeParseReturnType<any, any>, input: { [key: string]: any }) => {
const getStepStatus = async (parse: ZodErrorSlimResponse, input: { [key: string]: any }) => {

if (parse.success) {
if (parse.success || parse.errors.length === 0) {
return <CheckCircle color="success" />;
}

const error = parse.error as Error;
const errors = JSON.parse(error.message) as ZodIssue[];

if (errors.filter((error) => Object.keys(input).includes(error.path[0] + '')).length > 0) {
if (parse.errors.filter((error) => Object.keys(input).includes(error.path)).length > 0) {
return <Info color="warning" />;
} else {
return <CheckCircle color="success" />;
Expand Down
10 changes: 8 additions & 2 deletions components/EventManager/EventPositionEditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function EventPositionEditButton({ event, position, }: { event: E
formData.set('finalEndTime', finalEndTime!.toISOString());
formData.set('finalNotes', finalNotes);

const { errors } = await adminSaveEventPosition(event, position, formData);
const { eventPosition, errors } = await adminSaveEventPosition(event, position, formData);

if (errors) {
toast.error(errors.map((error) => error.message).join('. '));
Expand All @@ -54,7 +54,13 @@ export default function EventPositionEditButton({ event, position, }: { event: E
toast.success('Position saved successfully!');

if (publish) {
await publishEventPosition(event, position);
const {error} = await publishEventPosition(event, eventPosition);

if (error) {
toast.error(error.errors.map((error) => error.message).join('. '));
return;
}

toast.success('Position published successfully!');
}

Expand Down
12 changes: 5 additions & 7 deletions components/EventManager/EventPositionPublishAllButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import { publishEventPosition, unpublishEventPosition, validateFinalEventPosition } from "@/actions/eventPosition";
import { EventPositionWithSolo } from "@/app/events/admin/events/[id]/manager/page";
import { ZodErrorSlimResponse } from "@/types";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Event, } from "@prisma/client";
import { User } from "next-auth";
Expand Down Expand Up @@ -117,20 +118,17 @@ const getErrors = async (event: Event, positions: EventPositionWithSolo[]): Prom
formData.set('finalEndTime', modPosition.finalEndTime.toISOString());
formData.set('finalNotes', modPosition.finalNotes);

const parse = await validateFinalEventPosition(event, formData);
const parse = await validateFinalEventPosition(event, formData) as ZodErrorSlimResponse;

if (parse.success) {
continue;
}

const error = parse.error as Error;
const zodErrors = JSON.parse(error.message) as ZodIssue[];

for (const zodError of zodErrors) {
for (const error of parse.errors) {
if (errors.find((error) => error.user === position.user)) {
errors.find((error) => error.user === position.user)?.errors.push(zodError.message);
errors.find((error) => error.user === position.user)?.errors.push(error.message);
} else {
errors.push({ user: position.user as User, errors: [zodError.message] });
errors.push({ user: position.user as User, errors: [error.message] });
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion components/EventManager/EventPositionPublishButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export default function EventPositionPublishButton({ event, position, }: { event
return;
}

await publishEventPosition(event, position);
const {error} = await publishEventPosition(event, position);

if (error) {
toast.error(error.errors.map((error) => error.message).join('. '));
return;
}

toast.success('Position published successfully!');
}

Expand Down
3 changes: 2 additions & 1 deletion types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './external';
export * from './external';
export * from './zod';
7 changes: 7 additions & 0 deletions types/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ZodErrorSlimResponse = {
success: boolean;
errors: {
path: string,
message: string,
}[],
};

0 comments on commit 9b5e949

Please sign in to comment.