feat: add react-query and navigate to edit after event-type creation (#528)

* feat: add react-query and navigate to edit after event-type creation

* fix: add types/toasts and add react-query mutations on event-types

Co-authored-by: Mihai Colceriu <colceriumi@gmail.com>
This commit is contained in:
Mihai C 2021-08-27 15:11:24 +03:00 committed by GitHub
parent a44bc63304
commit fc50821282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 287 additions and 1053 deletions

21
lib/app-providers.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from "react";
import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry";
import { Provider } from "next-auth/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { Hydrate } from "react-query/hydration";
export const queryClient = new QueryClient();
const AppProviders: React.FC = (props, pageProps) => {
return (
<TelemetryProvider value={createTelemetryClient()}>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Provider session={pageProps.session}>{props.children}</Provider>
</Hydrate>
</QueryClientProvider>
</TelemetryProvider>
);
};
export default AppProviders;

View File

@ -0,0 +1,19 @@
import { CreateEventType } from "@lib/types/event-type";
const createEventType = async (data: CreateEventType) => {
const response = await fetch("/api/availability/eventtype", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
};
export default createEventType;

View File

@ -0,0 +1,17 @@
const deleteEventType = async (data: { id: number }) => {
const response = await fetch("/api/availability/eventtype", {
method: "DELETE",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
};
export default deleteEventType;

View File

@ -0,0 +1,19 @@
import { EventTypeInput } from "@lib/types/event-type";
const updateEventType = async (data: EventTypeInput) => {
const response = await fetch("/api/availability/eventtype", {
method: "PATCH",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
};
export default updateEventType;

49
lib/types/event-type.ts Normal file
View File

@ -0,0 +1,49 @@
export type OpeningHours = {
days: number[];
startTime: number;
endTime: number;
};
export type DateOverride = {
date: string;
startTime: number;
endTime: number;
};
export type AdvancedOptions = {
eventName?: string;
periodType?: string;
periodDays?: number;
periodStartDate?: Date | string;
periodEndDate?: Date | string;
periodCountCalendarDays?: boolean;
requiresConfirmation?: boolean;
};
export type EventTypeCustomInput = {
id: number;
label: string;
placeholder: string;
required: boolean;
type: string;
};
export type CreateEventType = {
title: string;
slug: string;
description: string;
length: number;
};
export type EventTypeInput = AdvancedOptions & {
id: number;
title: string;
slug: string;
description: string;
length: number;
hidden: boolean;
locations: unknown;
customInputs: EventTypeCustomInput[];
timeZone: string;
availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] };
};

View File

@ -51,6 +51,7 @@
"react-hot-toast": "^2.1.0",
"react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.1.25",
"react-query": "^3.21.0",
"react-select": "^4.3.1",
"react-timezone-select": "^1.0.7",
"short-uuid": "^4.2.0",

View File

@ -1,19 +1,22 @@
import "../styles/globals.css";
import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry";
import { Provider } from "next-auth/client";
import type { AppProps } from "next/app";
import AppProviders from "@lib/app-providers";
import type { AppProps as NextAppProps } from "next/app";
import Head from "next/head";
function MyApp({ Component, pageProps }: AppProps) {
// Workaround for https://github.com/zeit/next.js/issues/8592
export type AppProps = NextAppProps & {
/** Will be defined only is there was an error */
err?: Error;
};
function MyApp({ Component, pageProps, err }: AppProps) {
return (
<TelemetryProvider value={createTelemetryClient()}>
<Provider session={pageProps.session}>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Component {...pageProps} />
</Provider>
</TelemetryProvider>
<AppProviders>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Component {...pageProps} err={err} />
</AppProviders>
);
}

View File

@ -1,9 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/client";
import prisma from "../../../lib/prisma";
import prisma from "@lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
@ -61,13 +62,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
if (req.method == "POST") {
await prisma.eventType.create({
const eventType = await prisma.eventType.create({
data: {
userId: session.user.id,
...data,
},
});
res.status(200).json({ message: "Event created successfully" });
res.status(201).json({ eventType });
} else if (req.method == "PATCH") {
if (req.body.timeZone) {
data.timeZone = req.body.timeZone;
@ -98,18 +99,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
await prisma.eventType.update({
const eventType = await prisma.eventType.update({
where: {
id: req.body.id,
},
data,
});
res.status(200).json({ message: "Event updated successfully" });
res.status(200).json({ eventType });
}
}
if (req.method == "DELETE") {
// Delete associations first
await prisma.eventTypeCustomInput.deleteMany({
where: {
eventTypeId: req.body.id,
@ -122,6 +122,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
res.status(200).json({ message: "Event deleted successfully" });
res.status(200).json({});
}
}

View File

@ -1,4 +1,3 @@
import { GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
@ -37,53 +36,16 @@ import { DateRangePicker, OrientationShape, toMomentObject } from "react-dates";
import Switch from "@components/ui/Switch";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { useMutation } from "react-query";
import { EventTypeInput } from "@lib/types/event-type";
import updateEventType from "@lib/mutations/event-types/update-event-type";
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
import showToast from "@lib/notification";
dayjs.extend(utc);
dayjs.extend(timezone);
type Props = {
user: User;
eventType: EventType;
locationOptions: OptionBase[];
availability: Availability[];
};
type OpeningHours = {
days: number[];
startTime: number;
endTime: number;
};
type DateOverride = {
date: string;
startTime: number;
endTime: number;
};
type AdvancedOptions = {
eventName?: string;
periodType?: string;
periodDays?: number;
periodStartDate?: Date | string;
periodEndDate?: Date | string;
periodCountCalendarDays?: boolean;
requiresConfirmation?: boolean;
};
type EventTypeInput = AdvancedOptions & {
id: number;
title: string;
slug: string;
description: string;
length: number;
hidden: boolean;
locations: unknown;
customInputs: EventTypeCustomInput[];
timeZone: string;
availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] };
};
const PERIOD_TYPES = [
{
type: "rolling",
@ -99,12 +61,8 @@ const PERIOD_TYPES = [
},
];
export default function EventTypePage({
user,
eventType,
locationOptions,
availability,
}: Props): JSX.Element {
const EventTypePage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const { user, eventType, locationOptions, availability } = props;
const router = useRouter();
const [successModalOpen, setSuccessModalOpen] = useState(false);
@ -118,6 +76,26 @@ export default function EventTypePage({
const [DATE_PICKER_ORIENTATION, setDatePickerOrientation] = useState<OrientationShape>("horizontal");
const [contentSize, setContentSize] = useState({ width: 0, height: 0 });
const updateMutation = useMutation(updateEventType, {
onSuccess: async ({ eventType }) => {
await router.push("/event-types");
showToast(`${eventType.title} event type updated successfully`, "success");
},
onError: (err: Error) => {
showToast(err.message, "error");
},
});
const deleteMutation = useMutation(deleteEventType, {
onSuccess: async () => {
await router.push("/event-types");
showToast("Event type deleted successfully", "success");
},
onError: (err: Error) => {
showToast(err.message, "error");
},
});
const handleResizeEvent = () => {
const elementWidth = parseFloat(getComputedStyle(document.body).width);
const elementHeight = parseFloat(getComputedStyle(document.body).height);
@ -230,31 +208,14 @@ export default function EventTypePage({
...advancedOptionsPayload,
};
await fetch("/api/availability/eventtype", {
method: "PATCH",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
});
router.push("/event-types");
showToast("Event Type updated", "success");
setSuccessModalOpen(true);
updateMutation.mutate(payload);
}
async function deleteEventTypeHandler(event) {
event.preventDefault();
await fetch("/api/availability/eventtype", {
method: "DELETE",
body: JSON.stringify({ id: eventType.id }),
headers: {
"Content-Type": "application/json",
},
});
showToast("Event Type deleted", "success");
router.push("/event-types");
const payload = { id: eventType.id };
deleteMutation.mutate(payload);
}
const openLocationModal = (type: LocationType) => {
@ -1070,9 +1031,10 @@ export default function EventTypePage({
</Shell>
</div>
);
}
};
export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query }) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, query } = context;
const session = await getSession({ req });
if (!session) {
return {
@ -1208,3 +1170,5 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query
},
};
};
export default EventTypePage;

View File

@ -19,12 +19,25 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useRef } from "react";
import Shell from "../../components/Shell";
import prisma from "../../lib/prisma";
import Shell from "@components/Shell";
import prisma from "@lib/prisma";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { useMutation } from "react-query";
import createEventType from "@lib/mutations/event-types/create-event-type";
export default function Availability({ user, types }) {
const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const { user, types } = props;
const [session, loading] = useSession();
const router = useRouter();
const createMutation = useMutation(createEventType, {
onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id);
showToast(`${eventType.title} event type created successfully`, "success");
},
onError: (err: Error) => {
showToast(err.message, "error");
},
});
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
@ -39,26 +52,16 @@ export default function Availability({ user, types }) {
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = lengthRef.current.value;
const enteredLength = parseInt(lengthRef.current.value);
// TODO: Add validation
await fetch("/api/availability/eventtype", {
method: "POST",
body: JSON.stringify({
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
}),
headers: {
"Content-Type": "application/json",
},
});
const body = {
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
};
if (enteredTitle && enteredLength) {
await router.replace(router.asPath);
}
showToast("Event Type created", "success");
createMutation.mutate(body);
}
function autoPopulateSlug() {
@ -637,10 +640,12 @@ export default function Availability({ user, types }) {
</Shell>
</div>
);
}
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req } = context;
const session = await getSession({ req });
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
@ -671,7 +676,13 @@ export async function getServerSideProps(context) {
hidden: true,
},
});
return {
props: { user, types }, // will be passed to the page component as props
props: {
user,
types,
},
};
}
};
export default EventTypesPage;

1004
yarn.lock

File diff suppressed because it is too large Load Diff