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[]) => | ||||
|   modelQuery({ | ||||
|     where: criteria, | ||||
|  |  | |||
|  | @ -1,22 +1,23 @@ | |||
| import { useEffect, useState } from "react"; | ||||
| import { GetServerSideProps, GetServerSidePropsContext } from "next"; | ||||
| 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"; | ||||
| /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ | ||||
| import { Availability } from "@prisma/client"; | ||||
| 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
 | ||||
|   const router = useRouter(); | ||||
|   const { rescheduleUid } = router.query; | ||||
|  | @ -64,7 +65,7 @@ export default function Type(props): Type { | |||
|         shallow: true, | ||||
|       } | ||||
|     ); | ||||
|   }, [selectedDate]); | ||||
|   }, [router, selectedDate]); | ||||
| 
 | ||||
|   const handleSelectTimeZone = (selectedTimeZone: string): void => { | ||||
|     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 " + | ||||
|               (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 */} | ||||
|               <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"> | ||||
|                   <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"> | ||||
|                     <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"> | ||||
|                       {props.eventType.title} | ||||
|                       <div> | ||||
|  | @ -149,28 +150,28 @@ export default function Type(props): Type { | |||
|                     </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 className="sm:flex px-4 sm:py-5 sm:p-4"> | ||||
|               <div className="px-4 sm:flex sm:py-5 sm:p-4"> | ||||
|                 <div | ||||
|                   className={ | ||||
|                     "hidden md:block pr-8 sm:border-r sm:dark:border-gray-800 " + | ||||
|                     (selectedDate ? "sm:w-1/3" : "sm:w-1/2") | ||||
|                   }> | ||||
|                   <Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" /> | ||||
|                   <h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2> | ||||
|                   <h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4"> | ||||
|                   <Avatar user={props.user} className="w-16 h-16 mb-4 rounded-full" /> | ||||
|                   <h2 className="font-medium text-gray-500 dark:text-gray-300">{props.user.name}</h2> | ||||
|                   <h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white"> | ||||
|                     {props.eventType.title} | ||||
|                   </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" /> | ||||
|                     {props.eventType.length} minutes | ||||
|                   </p> | ||||
| 
 | ||||
|                   <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> | ||||
|                 <DatePicker | ||||
|                   date={selectedDate} | ||||
|  | @ -188,7 +189,7 @@ export default function Type(props): Type { | |||
|                   minimumBookingNotice={props.eventType.minimumBookingNotice} | ||||
|                 /> | ||||
| 
 | ||||
|                 <div className="ml-1 mt-4 block sm:hidden"> | ||||
|                 <div className="block mt-4 ml-1 sm:hidden"> | ||||
|                   <TimezoneDropdown /> | ||||
|                 </div> | ||||
| 
 | ||||
|  | @ -216,7 +217,7 @@ export default function Type(props): Type { | |||
|   function TimezoneDropdown() { | ||||
|     return ( | ||||
|       <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" /> | ||||
|           {timeZone()} | ||||
|           {isTimeOptionsOpen ? ( | ||||
|  | @ -233,72 +234,73 @@ export default function Type(props): Type { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => { | ||||
|   const dateQuery = context.query?.date ?? null; | ||||
|   const date = Array.isArray(dateQuery) && dateQuery.length > 0 ? dateQuery.pop() : dateQuery; | ||||
| export const getServerSideProps = async (context: GetServerSidePropsContext) => { | ||||
|   // get query params and typecast them to string
 | ||||
|   // (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( | ||||
|     prisma.user.findFirst, | ||||
|     { | ||||
|       username: context.query.user.toLowerCase(), | ||||
|   const user = await prisma.user.findFirst({ | ||||
|     where: { | ||||
|       username: userParam.toLowerCase(), | ||||
|     }, | ||||
|     [ | ||||
|       "id", | ||||
|       "username", | ||||
|       "name", | ||||
|       "email", | ||||
|       "bio", | ||||
|       "avatar", | ||||
|       "startTime", | ||||
|       "endTime", | ||||
|       "timeZone", | ||||
|       "weekStart", | ||||
|       "availability", | ||||
|       "hideBranding", | ||||
|       "theme", | ||||
|     ] | ||||
|   ); | ||||
|     select: { | ||||
|       id: true, | ||||
|       username: true, | ||||
|       name: true, | ||||
|       email: true, | ||||
|       bio: true, | ||||
|       avatar: true, | ||||
|       startTime: true, | ||||
|       endTime: true, | ||||
|       timeZone: true, | ||||
|       weekStart: true, | ||||
|       availability: true, | ||||
|       hideBranding: true, | ||||
|       theme: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (!user) { | ||||
|     return { | ||||
|       notFound: true, | ||||
|     }; | ||||
|     } as const; | ||||
|   } | ||||
| 
 | ||||
|   const eventType = await whereAndSelect( | ||||
|     prisma.eventType.findFirst, | ||||
|     { | ||||
|   const eventType = await prisma.eventType.findFirst({ | ||||
|     where: { | ||||
|       userId: user.id, | ||||
|       slug: context.query.type, | ||||
|       slug: typeParam, | ||||
|     }, | ||||
|     [ | ||||
|       "id", | ||||
|       "title", | ||||
|       "description", | ||||
|       "length", | ||||
|       "availability", | ||||
|       "timeZone", | ||||
|       "periodType", | ||||
|       "periodDays", | ||||
|       "periodStartDate", | ||||
|       "periodEndDate", | ||||
|       "periodCountCalendarDays", | ||||
|       "minimumBookingNotice", | ||||
|     ] | ||||
|   ); | ||||
|     select: { | ||||
|       id: true, | ||||
|       title: true, | ||||
|       description: true, | ||||
|       length: true, | ||||
|       availability: true, | ||||
|       timeZone: true, | ||||
|       periodType: true, | ||||
|       periodDays: true, | ||||
|       periodStartDate: true, | ||||
|       periodEndDate: true, | ||||
|       periodCountCalendarDays: true, | ||||
|       minimumBookingNotice: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (!eventType) { | ||||
|     return { | ||||
|       notFound: true, | ||||
|     }; | ||||
|     } as const; | ||||
|   } | ||||
| 
 | ||||
|   const getWorkingHours = (providesAvailability) => | ||||
|   const getWorkingHours = (providesAvailability: { availability: Availability[] }) => | ||||
|     providesAvailability.availability && providesAvailability.availability.length | ||||
|       ? providesAvailability.availability | ||||
|       : null; | ||||
| 
 | ||||
|   const workingHours: [] = | ||||
|   const workingHours = | ||||
|     getWorkingHours(eventType) || | ||||
|     getWorkingHours(user) || | ||||
|     [ | ||||
|  | @ -319,7 +321,7 @@ export const getServerSideProps: GetServerSideProps = async (context: GetServerS | |||
|   return { | ||||
|     props: { | ||||
|       user, | ||||
|       date, | ||||
|       date: dateParam, | ||||
|       eventType: eventTypeObject, | ||||
|       workingHours, | ||||
|     }, | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Alex Johansson
						Alex Johansson