Feature/parallel booking availability (#3087)

pull/3106/head^2
Omar López 2022-06-19 09:02:00 -06:00 committed by GitHub
parent 7599f2384e
commit 2d28ca61a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 242 additions and 121 deletions

View File

@ -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,
});
}

View File

@ -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 });
},

View File

@ -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";
@ -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]";
@ -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 };
};
@ -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
@ -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}
@ -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
@ -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());
}
@ -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;

View File

@ -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"]);
},
});

View File

@ -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<

View File

@ -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",
};
@ -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 = (

View File

@ -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,

View File

@ -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",

View File

@ -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";
@ -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,
@ -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,
@ -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({
@ -264,6 +275,7 @@ async function getDynamicGroupPageProps({
profile,
isDynamic: true,
away: false,
trpcState: ssg.dehydrate(),
},
revalidate: 10, // seconds
};
@ -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);
}
};

View File

@ -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";
@ -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
@ -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
}),
}),
],
/**

View File

@ -1,10 +1,10 @@
import {
BookingStatus,
User,
Booking,
Attendee,
Booking,
BookingReference,
BookingStatus,
EventType,
User,
WebhookTriggerEvents,
} from "@prisma/client";
import dayjs from "dayjs";
@ -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";

View File

@ -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";

View File

@ -29,7 +29,23 @@ export default trpcNext.createNextApiHandler({
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
// responseMeta() {
// // ...
// },
responseMeta({ ctx, paths, type, errors }) {
// assuming we have all our public routes in `viewer.public`
const allPublic = paths && paths.every((path) => path.startsWith("viewer.public."));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === "query";
if (allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every 5 seconds
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: {
"cache-control": `s-maxage=5, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
},
};
}
return {};
},
});

View File

@ -84,7 +84,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
},
async onSettled() {
await utils.invalidateQueries(["viewer.i18n"]);
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
@ -481,7 +481,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
);
}
const WithQuery = withQuery(["viewer.i18n"]);
const WithQuery = withQuery(["viewer.public.i18n"]);
export default function Settings(props: Props) {
const { t } = useLocale();

View File

@ -37,7 +37,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
});
// always preload i18n
await ssg.fetchQuery("viewer.i18n");
await ssg.fetchQuery("viewer.public.i18n");
return ssg;
}

View File

@ -21,8 +21,8 @@ export async function ssrInit(context: GetServerSidePropsContext) {
ctx,
});
// always preload "viewer.i18n"
await ssr.fetchQuery("viewer.i18n");
// always preload "viewer.public.i18n"
await ssr.fetchQuery("viewer.public.i18n");
return ssr;
}

View File

@ -68,7 +68,8 @@ const publicViewerRouter = createRouter()
return await samlTenantProduct(prisma, email);
},
});
})
.merge("slots.", slotsRouter);
// routes only available to authenticated users
const loggedInViewerRouter = createProtectedRouter()
@ -944,12 +945,11 @@ const loggedInViewerRouter = createProtectedRouter()
});
export const viewerRouter = createRouter()
.merge(publicViewerRouter)
.merge("public.", publicViewerRouter)
.merge(loggedInViewerRouter)
.merge("bookings.", bookingsRouter)
.merge("eventTypes.", eventTypesRouter)
.merge("availability.", availabilityRouter)
.merge("teams.", viewerTeamsRouter)
.merge("webhook.", webhookRouter)
.merge("apiKeys.", apiKeysRouter)
.merge("slots.", slotsRouter);
.merge("apiKeys.", apiKeysRouter);

View File

@ -9,6 +9,7 @@ import { availabilityUserSelect } from "@calcom/prisma";
import { stringToDayjs } from "@calcom/prisma/zod-utils";
import { TimeRange, WorkingHours } from "@calcom/types/schedule";
import isOutOfBounds from "@lib/isOutOfBounds";
import getSlots from "@lib/slots";
import { createRouter } from "@server/createRouter";
@ -137,6 +138,11 @@ export const slotsRouter = createRouter().query("getSchedule", {
beforeEventBuffer: true,
afterEventBuffer: true,
schedulingType: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
periodDays: true,
schedule: {
select: {
availability: true,
@ -202,6 +208,14 @@ export const slotsRouter = createRouter().query("getSchedule", {
afterBufferTime: eventType.afterEventBuffer,
currentSeats,
};
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
!isOutOfBounds(_time, {
periodType: eventType.periodType,
periodStartDate: eventType.periodStartDate,
periodEndDate: eventType.periodEndDate,
periodCountCalendarDays: eventType.periodCountCalendarDays,
periodDays: eventType.periodDays,
});
let time = dayjs(startTime);
do {
@ -215,18 +229,17 @@ export const slotsRouter = createRouter().query("getSchedule", {
});
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
const filteredTimes =
const filterStrategy =
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
? times.filter((time) =>
userSchedules.every((schedule) =>
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
)
)
: times.filter((time) =>
userSchedules.some((schedule) =>
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
)
);
? ("every" as const)
: ("some" as const);
const filteredTimes = times
.filter(isWithinBounds)
.filter((time) =>
userSchedules[filterStrategy]((schedule) =>
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
)
);
slots[yyyymmdd(time.toDate())] = filteredTimes.map((time) => ({
time: time.toISOString(),

View File

@ -1,21 +1,18 @@
import { Credential, SelectedCalendar } from "@prisma/client";
import { createHash } from "crypto";
import _ from "lodash";
import cache from "memory-cache";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import getApps from "@calcom/app-store/utils";
import { getUid } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import notEmpty from "@calcom/lib/notEmpty";
import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar";
import type { Event } from "@calcom/types/Event";
import type { EventResult } from "@calcom/types/EventManager";
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
/** TODO: Remove once all references are updated to app-store */
export { getCalendar };
export const getCalendarCredentials = (credentials: Array<Credential>, userId: number) => {
const calendarCredentials = getApps(credentials)
.filter((app) => app.type.endsWith("_calendar"))
@ -77,20 +74,51 @@ export const getConnectedCalendars = async (
return connectedCalendars;
};
const CACHING_TIME = 30_000; // 30 seconds
const getCachedResults = (
withCredentials: Credential[],
dateFrom: string,
dateTo: string,
selectedCalendars: SelectedCalendar[]
) => {
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
const calendars = calendarCredentials.map((credential) => getCalendar(credential));
const results = calendars.map(async (c, i) => {
/** Filter out nulls */
if (!c) return [];
/** We rely on the index so we can match credentials with calendars */
const { id, type } = calendarCredentials[i];
/** We just pass the calendars that matched the credential type,
* TODO: Migrate credential type or appId
*/
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
/** We extract external Ids so we don't cache too much */
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
/** We create a unque hash key based on the input data */
const cacheKey = createHash("md5").update(JSON.stringify({ id, selectedCalendarIds })).digest("hex");
/** Check if we already have cached data and return */
const cachedAvailability = cache.get(cacheKey);
if (cachedAvailability) return cachedAvailability;
/** If we don't then we actually fetch external calendars (which can be very slow) */
const availability = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
/** We save the availability to a few seconds so recurrent calls are nearly instant */
cache.put(cacheKey, availability, CACHING_TIME);
return availability;
});
return Promise.all(results);
};
export const getBusyCalendarTimes = async (
withCredentials: Credential[],
dateFrom: string,
dateTo: string,
selectedCalendars: SelectedCalendar[]
) => {
const calendars = withCredentials
.filter((credential) => credential.type.endsWith("_calendar"))
.map((credential) => getCalendar(credential))
.filter(notEmpty);
let results: EventBusyDate[][] = [];
try {
results = await Promise.all(calendars.map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)));
results = await getCachedResults(withCredentials, dateFrom, dateTo, selectedCalendars);
} catch (error) {
log.warn(error);
}

View File

@ -34,19 +34,15 @@ export type DatePickerProps = {
isLoading?: boolean;
};
const Day = ({
export const Day = ({
date,
active,
...props
}: JSX.IntrinsicElements["button"] & { active: boolean; date: Date }) => {
return (
<button
style={props.disabled ? {} : {}}
className={classNames(
"absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm border border-transparent text-center",
props.disabled
? "text-bookinglighter cursor-default font-light"
: "hover:border-brand font-medium dark:hover:border-white",
"hover:border-brand disabled:text-bookinglighter absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm border border-transparent text-center font-medium disabled:cursor-default disabled:border-transparent disabled:font-light dark:hover:border-white disabled:dark:border-transparent",
active
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast"
: !props.disabled
@ -68,9 +64,11 @@ const Days = ({
includedDates = [],
browsingDate,
weekStart,
DayComponent = Day,
selected,
...props
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
browsingDate: Date;
weekStart: number;
}) => {
@ -90,16 +88,11 @@ const Days = ({
return (
<>
{days.map((day, idx) => (
<div
key={day === null ? `e-${idx}` : `day-${day}`}
style={{
paddingTop: "100%",
}}
className="relative w-full">
<div key={day === null ? `e-${idx}` : `day-${day}`} className="relative w-full pt-[100%]">
{day === null ? (
<div key={`e-${idx}`} />
) : (
<Day
<DayComponent
date={day}
onClick={() => props.onChange(day)}
disabled={
@ -139,7 +132,7 @@ const DatePicker = ({
onMonthChange,
isLoading = false,
...passThroughProps
}: DatePickerProps) => {
}: DatePickerProps & Partial<React.ComponentProps<typeof Days>>) => {
const [month, setMonth] = useState(selected ? selected.getMonth() : new Date().getMonth());
const changeMonth = (newMonth: number) => {

View File

@ -3140,32 +3140,32 @@
javascript-natural-sort "0.7.1"
lodash "4.17.21"
"@trpc/client@^9.23.4":
version "9.23.4"
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.23.4.tgz#d887d013ca9146299df1a33a1f13d57366a3483f"
integrity sha512-usZgbydmqqzUqJQD5yY9f+KftYBKz6ed0J90MYETqRwXl9XtR9kKOwRRMBQwZDdeMhai3Hlk4sDWeC9HR3auRQ==
"@trpc/client@^9.25.2":
version "9.25.2"
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.25.2.tgz#3ece63bc58436a12a49a7ef3aa3ff54f03bdd750"
integrity sha512-wW4KEvG+mF5ParqmyTgSjxwOjvxtsQCso6nZeZ3PI19d9bBUprFx9E+UkS5R3gqNm9lddQx7tx+ie+OFkDmp3g==
dependencies:
"@babel/runtime" "^7.9.0"
"@trpc/next@^9.23.4":
version "9.23.4"
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.23.4.tgz#45c4da34cd99b882aefa0ddc065dd23c5d5467a2"
integrity sha512-fPFU/PNkGawiUS56wRx3KsnRh/cvI3RtGYbZKIxLr38oYyHLct/v1uqA+0xJIoyPlcFDLoB6et4MZJuv3/v8Dg==
"@trpc/next@^9.25.2":
version "9.25.2"
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.25.2.tgz#721182d1426e28848c87ef473dff781e9c812ab4"
integrity sha512-OiRcmNd5sxTR23O2oVxBVoSj4DNVZe84GzXBKxShYjeDyld0OKmQzqB1H2rMsCmNU7/Yd4lYDQBjGnDVP5VxPQ==
dependencies:
"@babel/runtime" "^7.9.0"
react-ssr-prepass "^1.5.0"
"@trpc/react@^9.23.4":
version "9.23.4"
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.23.4.tgz#6abb2bbabd76d72cc1a3fb72f6ecc33702cb1c3d"
integrity sha512-adori41F2hgjijThOmoeMkIXcaRjScZi6lczRwIIy6UqoUjYb+FfO46Jr7cP/QSX1Ur8VatD6UgLCNViURTfCQ==
"@trpc/react@^9.25.2":
version "9.25.2"
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.25.2.tgz#93c9c9c46bfee16ede9b326c2c964a38b4f8b84d"
integrity sha512-osP1JmhhBLWmSkoCc19YcGiQdeEq0RtvO0uvLe2DfzGpQL1orB72nvqWHXlIGHWA4E0cnc1FRmoXokWYCwHPxQ==
dependencies:
"@babel/runtime" "^7.9.0"
"@trpc/server@^9.23.4":
version "9.23.4"
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.23.4.tgz#78ecebbdda79db6252067d66c4a0edef025a501b"
integrity sha512-nlOgft5g4BziNplDHhw7f4m9+k8lRPFtVVi8VGxOINt7sQ8pzzEti0WMIl2BX6gugw92t+kae2O5e793AFJs9g==
"@trpc/server@^9.25.2":
version "9.25.2"
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.25.2.tgz#3ac58753b4cf7b2aa1a2a51d94bd0798b495a78a"
integrity sha512-E5ibK5jLgWremiPs2pO+Y/YktRH7+CqmMwp97mTp9ymYZn3od4C9TuFg6bxEK1bQKnUezpzHJyGRADVKCWrjsw==
"@tryvital/vital-node@^1.3.6":
version "1.3.6"
@ -3560,11 +3560,16 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*", "@types/node@16.9.1", "@types/node@>=12.0.0", "@types/node@>=8.1.0", "@types/node@^12.12.6":
"@types/node@*", "@types/node@16.9.1", "@types/node@>=12.0.0", "@types/node@>=8.1.0":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@^12.12.6":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
"@types/nodemailer@^6.4.4":
version "6.4.4"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
@ -17625,4 +17630,4 @@ zwitch@^1.0.0:
zwitch@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==