make pages/[user]/[type].tsx
type-safe (#484)
* make `pages/[user]/[type].tsx` type-safe * deprecate `whereAndSelect`
This commit is contained in:
parent
aed9757409
commit
2af160a13e
2 changed files with 85 additions and 78 deletions
|
@ -27,6 +27,11 @@ const pluck = (select: Record<string, boolean>, attr: string) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* This function will always return `any` type,
|
||||||
|
* See https://github.com/calendso/calendso/pull/460 for alternative approach
|
||||||
|
*/
|
||||||
const whereAndSelect = (modelQuery, criteria: Record<string, unknown>, pluckedAttributes: string[]) =>
|
const whereAndSelect = (modelQuery, criteria: Record<string, unknown>, pluckedAttributes: string[]) =>
|
||||||
modelQuery({
|
modelQuery({
|
||||||
where: criteria,
|
where: criteria,
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { useEffect, useState } from "react";
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
import { Availability } from "@prisma/client";
|
||||||
import Head from "next/head";
|
|
||||||
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
||||||
|
|
||||||
import prisma, { whereAndSelect } from "@lib/prisma";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
|
||||||
import AvailableTimes from "../../components/booking/AvailableTimes";
|
|
||||||
import TimeOptions from "../../components/booking/TimeOptions";
|
|
||||||
import Avatar from "../../components/Avatar";
|
|
||||||
import { timeZone } from "../../lib/clock";
|
|
||||||
import DatePicker from "../../components/booking/DatePicker";
|
|
||||||
import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
|
|
||||||
import Theme from "@components/Theme";
|
import Theme from "@components/Theme";
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Avatar from "../../components/Avatar";
|
||||||
|
import AvailableTimes from "../../components/booking/AvailableTimes";
|
||||||
|
import DatePicker from "../../components/booking/DatePicker";
|
||||||
|
import TimeOptions from "../../components/booking/TimeOptions";
|
||||||
|
import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
|
||||||
|
import { timeZone } from "../../lib/clock";
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
||||||
|
|
||||||
export default function Type(props): Type {
|
export default function Type(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
// Get router variables
|
// Get router variables
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
|
@ -64,7 +65,7 @@ export default function Type(props): Type {
|
||||||
shallow: true,
|
shallow: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [selectedDate]);
|
}, [router, selectedDate]);
|
||||||
|
|
||||||
const handleSelectTimeZone = (selectedTimeZone: string): void => {
|
const handleSelectTimeZone = (selectedTimeZone: string): void => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
|
@ -133,13 +134,13 @@ export default function Type(props): Type {
|
||||||
"mx-auto my-0 md:my-24 transition-max-width ease-in-out duration-500 " +
|
"mx-auto my-0 md:my-24 transition-max-width ease-in-out duration-500 " +
|
||||||
(selectedDate ? "max-w-5xl" : "max-w-3xl")
|
(selectedDate ? "max-w-5xl" : "max-w-3xl")
|
||||||
}>
|
}>
|
||||||
<div className="sm:dark:border-gray-600 dark:bg-gray-900 bg-white md:border border-gray-200 rounded-sm">
|
<div className="bg-white border-gray-200 rounded-sm sm:dark:border-gray-600 dark:bg-gray-900 md:border">
|
||||||
{/* mobile: details */}
|
{/* mobile: details */}
|
||||||
<div className="p-4 sm:p-8 block md:hidden">
|
<div className="block p-4 sm:p-8 md:hidden">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Avatar user={props.user} className="inline-block h-9 w-9 rounded-full" />
|
<Avatar user={props.user} className="inline-block rounded-full h-9 w-9" />
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm font-medium dark:text-gray-300 text-black">{props.user.name}</p>
|
<p className="text-sm font-medium text-black dark:text-gray-300">{props.user.name}</p>
|
||||||
<div className="flex gap-2 text-xs font-medium text-gray-600">
|
<div className="flex gap-2 text-xs font-medium text-gray-600">
|
||||||
{props.eventType.title}
|
{props.eventType.title}
|
||||||
<div>
|
<div>
|
||||||
|
@ -149,28 +150,28 @@ export default function Type(props): Type {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="dark:text-gray-200 text-gray-600 mt-3">{props.eventType.description}</p>
|
<p className="mt-3 text-gray-600 dark:text-gray-200">{props.eventType.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:flex px-4 sm:py-5 sm:p-4">
|
<div className="px-4 sm:flex sm:py-5 sm:p-4">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"hidden md:block pr-8 sm:border-r sm:dark:border-gray-800 " +
|
"hidden md:block pr-8 sm:border-r sm:dark:border-gray-800 " +
|
||||||
(selectedDate ? "sm:w-1/3" : "sm:w-1/2")
|
(selectedDate ? "sm:w-1/3" : "sm:w-1/2")
|
||||||
}>
|
}>
|
||||||
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
<Avatar user={props.user} className="w-16 h-16 mb-4 rounded-full" />
|
||||||
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2>
|
<h2 className="font-medium text-gray-500 dark:text-gray-300">{props.user.name}</h2>
|
||||||
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
|
<h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
|
||||||
{props.eventType.title}
|
{props.eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mb-1 px-2 py-1 -ml-2">
|
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{props.eventType.length} minutes
|
{props.eventType.length} minutes
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<TimezoneDropdown />
|
<TimezoneDropdown />
|
||||||
|
|
||||||
<p className="dark:text-gray-200 text-gray-600 mt-3 mb-8">{props.eventType.description}</p>
|
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{props.eventType.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
|
@ -188,7 +189,7 @@ export default function Type(props): Type {
|
||||||
minimumBookingNotice={props.eventType.minimumBookingNotice}
|
minimumBookingNotice={props.eventType.minimumBookingNotice}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="ml-1 mt-4 block sm:hidden">
|
<div className="block mt-4 ml-1 sm:hidden">
|
||||||
<TimezoneDropdown />
|
<TimezoneDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -216,7 +217,7 @@ export default function Type(props): Type {
|
||||||
function TimezoneDropdown() {
|
function TimezoneDropdown() {
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||||
<Collapsible.Trigger className="text-gray-500 mb-1 px-2 py-1 -ml-2 text-left min-w-32">
|
<Collapsible.Trigger className="px-2 py-1 mb-1 -ml-2 text-left text-gray-500 min-w-32">
|
||||||
<GlobeIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<GlobeIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{timeZone()}
|
{timeZone()}
|
||||||
{isTimeOptionsOpen ? (
|
{isTimeOptionsOpen ? (
|
||||||
|
@ -233,72 +234,73 @@ export default function Type(props): Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const dateQuery = context.query?.date ?? null;
|
// get query params and typecast them to string
|
||||||
const date = Array.isArray(dateQuery) && dateQuery.length > 0 ? dateQuery.pop() : dateQuery;
|
// (would be even better to assert them instead of typecasting)
|
||||||
|
const userParam = context.query.user as string;
|
||||||
|
const typeParam = context.query.type as string;
|
||||||
|
const dateParam = context.query.date as string | undefined;
|
||||||
|
|
||||||
const user = await whereAndSelect(
|
const user = await prisma.user.findFirst({
|
||||||
prisma.user.findFirst,
|
where: {
|
||||||
{
|
username: userParam.toLowerCase(),
|
||||||
username: context.query.user.toLowerCase(),
|
|
||||||
},
|
},
|
||||||
[
|
select: {
|
||||||
"id",
|
id: true,
|
||||||
"username",
|
username: true,
|
||||||
"name",
|
name: true,
|
||||||
"email",
|
email: true,
|
||||||
"bio",
|
bio: true,
|
||||||
"avatar",
|
avatar: true,
|
||||||
"startTime",
|
startTime: true,
|
||||||
"endTime",
|
endTime: true,
|
||||||
"timeZone",
|
timeZone: true,
|
||||||
"weekStart",
|
weekStart: true,
|
||||||
"availability",
|
availability: true,
|
||||||
"hideBranding",
|
hideBranding: true,
|
||||||
"theme",
|
theme: true,
|
||||||
]
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = await whereAndSelect(
|
const eventType = await prisma.eventType.findFirst({
|
||||||
prisma.eventType.findFirst,
|
where: {
|
||||||
{
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
slug: context.query.type,
|
slug: typeParam,
|
||||||
},
|
},
|
||||||
[
|
select: {
|
||||||
"id",
|
id: true,
|
||||||
"title",
|
title: true,
|
||||||
"description",
|
description: true,
|
||||||
"length",
|
length: true,
|
||||||
"availability",
|
availability: true,
|
||||||
"timeZone",
|
timeZone: true,
|
||||||
"periodType",
|
periodType: true,
|
||||||
"periodDays",
|
periodDays: true,
|
||||||
"periodStartDate",
|
periodStartDate: true,
|
||||||
"periodEndDate",
|
periodEndDate: true,
|
||||||
"periodCountCalendarDays",
|
periodCountCalendarDays: true,
|
||||||
"minimumBookingNotice",
|
minimumBookingNotice: true,
|
||||||
]
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkingHours = (providesAvailability) =>
|
const getWorkingHours = (providesAvailability: { availability: Availability[] }) =>
|
||||||
providesAvailability.availability && providesAvailability.availability.length
|
providesAvailability.availability && providesAvailability.availability.length
|
||||||
? providesAvailability.availability
|
? providesAvailability.availability
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const workingHours: [] =
|
const workingHours =
|
||||||
getWorkingHours(eventType) ||
|
getWorkingHours(eventType) ||
|
||||||
getWorkingHours(user) ||
|
getWorkingHours(user) ||
|
||||||
[
|
[
|
||||||
|
@ -319,7 +321,7 @@ export const getServerSideProps: GetServerSideProps = async (context: GetServerS
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
date,
|
date: dateParam,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue