Skip to content

Commit

Permalink
Feature/parallel booking availability (calcom#3087)
Browse files Browse the repository at this point in the history
  • Loading branch information
zomars authored Jun 19, 2022
1 parent 7599f23 commit 2d28ca6
Show file tree
Hide file tree
Showing 21 changed files with 242 additions and 121 deletions.
2 changes: 1 addition & 1 deletion apps/web/components/I18nLanguageHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect } from "react";
import { trpc } from "@lib/trpc";

export function useViewerI18n() {
return trpc.useQuery(["viewer.i18n"], {
return trpc.useQuery(["viewer.public.i18n"], {
staleTime: Infinity,
});
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/auth/SAMLLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function SAMLLogin(props: Props) {
const methods = useFormContext();
const telemetry = useTelemetry();

const mutation = trpc.useMutation("viewer.samlTenantProduct", {
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
onSuccess: async (data) => {
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
},
Expand Down
77 changes: 54 additions & 23 deletions apps/web/components/booking/pages/AvailabilityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { localStorage } from "@calcom/lib/webstorage";
import DatePicker from "@calcom/ui/booker/DatePicker";
import DatePicker, { Day } from "@calcom/ui/booker/DatePicker";

import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
Expand All @@ -54,8 +54,6 @@ import { HeadSeo } from "@components/seo/head-seo";
import AvatarGroup from "@components/ui/AvatarGroup";
import PoweredByCal from "@components/ui/PoweredByCal";

import type { Slot } from "@server/routers/viewer/slots";

import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
Expand Down Expand Up @@ -123,14 +121,18 @@ const useSlots = ({
startTime: Date;
endTime: Date;
}) => {
const { data, isLoading } = trpc.useQuery([
"viewer.slots.getSchedule",
{
eventTypeId,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
},
]);
const { data, isLoading } = trpc.useQuery(
[
"viewer.public.slots.getSchedule",
{
eventTypeId,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
},
],
/** Prevents fetching past dates */
{ enabled: dayjs(startTime).isAfter(dayjs().subtract(1, "day")) }
);

return { slots: data?.slots || {}, isLoading };
};
Expand Down Expand Up @@ -165,18 +167,10 @@ const SlotPicker = ({

const { slots, isLoading } = useSlots({
eventTypeId: eventType.id,
startTime: startDate,
startTime: dayjs(startDate).startOf("day").toDate(),
endTime: dayjs(startDate).endOf("month").toDate(),
});

const [times, setTimes] = useState<Slot[]>([]);

useEffect(() => {
if (selectedDate && slots[yyyymmdd(selectedDate)]) {
setTimes(slots[yyyymmdd(selectedDate)]);
}
}, [selectedDate, slots]);

return (
<>
<DatePicker
Expand All @@ -187,19 +181,27 @@ const SlotPicker = ({
? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
: "sm:pl-4")
}
locale={isLocaleReady ? i18n.language : "en"}
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={setSelectedDate}
onMonthChange={setStartDate}
onMonthChange={(startDate) => {
// set the minimum day to today in the current month, not the beginning of the month
setStartDate(
dayjs(startDate).isBefore(dayjs().subtract(1, "day"))
? dayjs(new Date()).startOf("day").toDate()
: startDate
);
}}
weekStart={weekStart}
// DayComponent={(props) => <DayContainer {...props} eventTypeId={eventType.id} />}
/>

<div className="mt-4 ml-1 block sm:hidden">{timezoneDropdown}</div>

{selectedDate && (
<AvailableTimes
slots={times}
slots={slots[yyyymmdd(selectedDate)]}
date={dayjs(selectedDate)}
timeFormat={timeFormat}
eventTypeId={eventType.id}
Expand Down Expand Up @@ -261,6 +263,9 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
const [selectedDate, _setSelectedDate] = useState<Date>();

useEffect(() => {
/** TODO: router.query.date is comming as `null` even when set like this:
* `/user/type?date=2022-06-22-0600`
*/
const dateString = asStringOrNull(router.query.date);
if (dateString) {
const offsetString = dateString.substr(11, 14); // hhmm
Expand All @@ -275,6 +280,7 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
(offsetMinute !== "" ? parseInt(offsetMinute) : 0));

const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true);
console.log("date.isValid()", date.isValid());
if (date.isValid()) {
setSelectedDate(date.toDate());
}
Expand Down Expand Up @@ -674,4 +680,29 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
);
};

const DayContainer = (props: React.ComponentProps<typeof Day> & { eventTypeId: number }) => {
const { eventTypeId, ...rest } = props;
/** :
* Fetch each individual day here. All these are batched with tRPC anyways.
**/
const { slots } = useSlots({
eventTypeId,
startTime: dayjs(props.date).startOf("day").toDate(),
endTime: dayjs(props.date).endOf("day").toDate(),
});
const includedDates = Object.keys(slots).filter((k) => slots[k].length > 0);
const disabled = includedDates.length > 0 ? !includedDates.includes(yyyymmdd(props.date)) : props.disabled;
return <Day {...{ ...rest, disabled }} />;
};

const AvailableTimesContainer = (props: React.ComponentProps<typeof AvailableTimes>) => {
const { date, eventTypeId } = props;
const { slots } = useSlots({
eventTypeId,
startTime: dayjs(date).startOf("day").toDate(),
endTime: dayjs(date).endOf("day").toDate(),
});
return <AvailableTimes {...props} slots={slots[date.format("YYYY-MM-DD")]} />;
};

export default AvailabilityPage;
2 changes: 1 addition & 1 deletion apps/web/components/security/DisableUserImpersonation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
await utils.invalidateQueries(["viewer.me"]);
},
async onSettled() {
await utils.invalidateQueries(["viewer.i18n"]);
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});

Expand Down
8 changes: 7 additions & 1 deletion apps/web/lib/QueryCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ type TError = TRPCClientErrorLike<AppRouter>;

const withQuery = <TPath extends keyof TQueryValues & string>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
params?: UseTRPCQueryOptions<TPath, TQueryValues[TPath]["input"], TQueryValues[TPath]["output"], TError>
params?: UseTRPCQueryOptions<
TPath,
TQueryValues[TPath]["input"],
TQueryValues[TPath]["output"],
TQueryValues[TPath]["output"],
TError
>
) => {
return function WithQuery(
opts: Omit<
Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/app-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type AppPropsWithChildren = AppProps & {
};

const CustomI18nextProvider = (props: AppPropsWithChildren) => {
const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"]).data ?? {
locale: "en",
};

Expand All @@ -42,7 +42,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
};

const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.useQuery(["viewer.session"]).data;
const session = trpc.useQuery(["viewer.public.session"]).data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const RemainingProviders = (
Expand Down
3 changes: 3 additions & 0 deletions apps/web/lib/isOutOfBounds.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { EventType, PeriodType } from "@prisma/client";
import dayjs from "dayjs";
import dayjsBusinessTime from "dayjs-business-days2";

dayjs.extend(dayjsBusinessTime);

function isOutOfBounds(
time: dayjs.ConfigType,
Expand Down
8 changes: 4 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@
"@radix-ui/react-tooltip": "^0.1.0",
"@stripe/react-stripe-js": "^1.8.0",
"@stripe/stripe-js": "^1.29.0",
"@trpc/client": "^9.23.4",
"@trpc/next": "^9.23.4",
"@trpc/react": "^9.23.4",
"@trpc/server": "^9.23.4",
"@trpc/client": "^9.25.2",
"@trpc/next": "^9.25.2",
"@trpc/react": "^9.25.2",
"@trpc/server": "^9.25.2",
"@vercel/edge-functions-ui": "^0.2.1",
"@wojtekmaj/react-daterange-picker": "^3.3.1",
"accept-language-parser": "^1.5.0",
Expand Down
38 changes: 23 additions & 15 deletions apps/web/pages/[user]/[type].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UserPlan } from "@prisma/client";
import dayjs from "dayjs";
import { GetStaticPropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
Expand Down Expand Up @@ -54,7 +55,10 @@ export default function Type(props: AvailabilityPageProps) {
);
}

async function getUserPageProps({ username, slug }: { username: string; slug: string }) {
async function getUserPageProps(context: GetStaticPropsContext) {
const { type: slug, user: username } = paramsSchema.parse(context.params);
const { ssgInit } = await import("@server/lib/ssg");
const ssg = await ssgInit(context);
const user = await prisma.user.findUnique({
where: {
username,
Expand Down Expand Up @@ -150,6 +154,13 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st

const profile = eventType.users[0] || user;

const startTime = new Date();
await ssg.fetchQuery("viewer.public.slots.getSchedule", {
eventTypeId: eventType.id,
startTime: dayjs(startTime).startOf("day").toISOString(),
endTime: dayjs(startTime).endOf("day").toISOString(),
});

return {
props: {
eventType: eventTypeObject,
Expand All @@ -168,18 +179,18 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st
},
away: user?.away,
isDynamic: false,
trpcState: ssg.dehydrate(),
},
revalidate: 10, // seconds
};
}

async function getDynamicGroupPageProps({
usernameList,
length,
}: {
usernameList: string[];
length: number;
}) {
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
const { ssgInit } = await import("@server/lib/ssg");
const ssg = await ssgInit(context);
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
const usernameList = getUsernameList(userParam);
const length = parseInt(typeParam);
const eventType = getDefaultEvent("" + length);

const users = await prisma.user.findMany({
Expand Down Expand Up @@ -264,6 +275,7 @@ async function getDynamicGroupPageProps({
profile,
isDynamic: true,
away: false,
trpcState: ssg.dehydrate(),
},
revalidate: 10, // seconds
};
Expand All @@ -272,17 +284,13 @@ async function getDynamicGroupPageProps({
const paramsSchema = z.object({ type: z.string(), user: z.string() });

export const getStaticProps = async (context: GetStaticPropsContext) => {
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);

const { user: userParam } = paramsSchema.parse(context.params);
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
const isDynamicGroup = userParam.includes("+");
if (isDynamicGroup) {
return await getDynamicGroupPageProps({
usernameList: getUsernameList(userParam),
length: parseInt(typeParam),
});
return await getDynamicGroupPageProps(context);
} else {
return await getUserPageProps({ username: userParam, slug: typeParam });
return await getUserPageProps(context);
}
};

Expand Down
22 changes: 20 additions & 2 deletions apps/web/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import I18nLanguageHandler from "@components/I18nLanguageHandler";

import type { AppRouter } from "@server/routers/_app";
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
import { httpLink } from "@trpc/client/links/httpLink";
import { loggerLink } from "@trpc/client/links/loggerLink";
import { splitLink } from "@trpc/client/links/splitLink";
import { withTRPC } from "@trpc/next";
import type { TRPCClientErrorLike } from "@trpc/react";
import { Maybe } from "@trpc/server";
Expand Down Expand Up @@ -56,6 +58,13 @@ function MyApp(props: AppProps) {

export default withTRPC<AppRouter>({
config() {
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `http://${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;

/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
Expand All @@ -70,8 +79,17 @@ export default withTRPC<AppRouter>({
enabled: (opts) =>
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `/api/trpc`,
splitLink({
// check for context property `skipBatch`
condition: (op) => op.context.skipBatch === true,
// when condition is true, use normal request
true: httpLink({ url }),
// when condition is false, use batching
false: httpBatchLink({
url,
/** @link https://github.com/trpc/trpc/issues/2008 */
// maxBatchSize: 7
}),
}),
],
/**
Expand Down
8 changes: 4 additions & 4 deletions apps/web/pages/api/book/request-reschedule.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
BookingStatus,
User,
Booking,
Attendee,
Booking,
BookingReference,
BookingStatus,
EventType,
User,
WebhookTriggerEvents,
} from "@prisma/client";
import dayjs from "dayjs";
Expand All @@ -13,7 +13,7 @@ import { getSession } from "next-auth/react";
import type { TFunction } from "next-i18next";
import { z, ZodError } from "zod";

import { getCalendar } from "@calcom/core/CalendarManager";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/pages/api/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import async from "async";
import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next";

import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getCalendar } from "@calcom/core/CalendarManager";
import { deleteMeeting } from "@calcom/core/videoClient";
import { sendCancelledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
Expand Down
Loading

0 comments on commit 2d28ca6

Please sign in to comment.