Feature/parallel booking availability (#3087)
parent
7599f2384e
commit
2d28ca61a4
|
@ -4,7 +4,7 @@ import { useEffect } from "react";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
export function useViewerI18n() {
|
export function useViewerI18n() {
|
||||||
return trpc.useQuery(["viewer.i18n"], {
|
return trpc.useQuery(["viewer.public.i18n"], {
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function SAMLLogin(props: Props) {
|
||||||
const methods = useFormContext();
|
const methods = useFormContext();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const mutation = trpc.useMutation("viewer.samlTenantProduct", {
|
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
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 { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
@ -54,8 +54,6 @@ import { HeadSeo } from "@components/seo/head-seo";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||||
|
|
||||||
import type { Slot } from "@server/routers/viewer/slots";
|
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
@ -123,14 +121,18 @@ const useSlots = ({
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
endTime: Date;
|
endTime: Date;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = trpc.useQuery([
|
const { data, isLoading } = trpc.useQuery(
|
||||||
"viewer.slots.getSchedule",
|
[
|
||||||
{
|
"viewer.public.slots.getSchedule",
|
||||||
eventTypeId,
|
{
|
||||||
startTime: startTime.toISOString(),
|
eventTypeId,
|
||||||
endTime: endTime.toISOString(),
|
startTime: startTime.toISOString(),
|
||||||
},
|
endTime: endTime.toISOString(),
|
||||||
]);
|
},
|
||||||
|
],
|
||||||
|
/** Prevents fetching past dates */
|
||||||
|
{ enabled: dayjs(startTime).isAfter(dayjs().subtract(1, "day")) }
|
||||||
|
);
|
||||||
|
|
||||||
return { slots: data?.slots || {}, isLoading };
|
return { slots: data?.slots || {}, isLoading };
|
||||||
};
|
};
|
||||||
|
@ -165,18 +167,10 @@ const SlotPicker = ({
|
||||||
|
|
||||||
const { slots, isLoading } = useSlots({
|
const { slots, isLoading } = useSlots({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
startTime: startDate,
|
startTime: dayjs(startDate).startOf("day").toDate(),
|
||||||
endTime: dayjs(startDate).endOf("month").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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DatePicker
|
<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:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
|
||||||
: "sm:pl-4")
|
: "sm:pl-4")
|
||||||
}
|
}
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
|
||||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
||||||
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
onChange={setSelectedDate}
|
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}
|
weekStart={weekStart}
|
||||||
|
// DayComponent={(props) => <DayContainer {...props} eventTypeId={eventType.id} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4 ml-1 block sm:hidden">{timezoneDropdown}</div>
|
<div className="mt-4 ml-1 block sm:hidden">{timezoneDropdown}</div>
|
||||||
|
|
||||||
{selectedDate && (
|
{selectedDate && (
|
||||||
<AvailableTimes
|
<AvailableTimes
|
||||||
slots={times}
|
slots={slots[yyyymmdd(selectedDate)]}
|
||||||
date={dayjs(selectedDate)}
|
date={dayjs(selectedDate)}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
eventTypeId={eventType.id}
|
eventTypeId={eventType.id}
|
||||||
|
@ -261,6 +263,9 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
|
||||||
const [selectedDate, _setSelectedDate] = useState<Date>();
|
const [selectedDate, _setSelectedDate] = useState<Date>();
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
const dateString = asStringOrNull(router.query.date);
|
||||||
if (dateString) {
|
if (dateString) {
|
||||||
const offsetString = dateString.substr(11, 14); // hhmm
|
const offsetString = dateString.substr(11, 14); // hhmm
|
||||||
|
@ -275,6 +280,7 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
|
||||||
(offsetMinute !== "" ? parseInt(offsetMinute) : 0));
|
(offsetMinute !== "" ? parseInt(offsetMinute) : 0));
|
||||||
|
|
||||||
const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true);
|
const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true);
|
||||||
|
console.log("date.isValid()", date.isValid());
|
||||||
if (date.isValid()) {
|
if (date.isValid()) {
|
||||||
setSelectedDate(date.toDate());
|
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;
|
export default AvailabilityPage;
|
||||||
|
|
|
@ -17,7 +17,7 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
||||||
await utils.invalidateQueries(["viewer.me"]);
|
await utils.invalidateQueries(["viewer.me"]);
|
||||||
},
|
},
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
await utils.invalidateQueries(["viewer.i18n"]);
|
await utils.invalidateQueries(["viewer.public.i18n"]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,13 @@ type TError = TRPCClientErrorLike<AppRouter>;
|
||||||
|
|
||||||
const withQuery = <TPath extends keyof TQueryValues & string>(
|
const withQuery = <TPath extends keyof TQueryValues & string>(
|
||||||
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
|
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(
|
return function WithQuery(
|
||||||
opts: Omit<
|
opts: Omit<
|
||||||
|
|
|
@ -26,7 +26,7 @@ type AppPropsWithChildren = AppProps & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||||
const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {
|
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"]).data ?? {
|
||||||
locale: "en",
|
locale: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppProviders = (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
|
// No need to have intercom on public pages - Good for Page Performance
|
||||||
const isPublicPage = usePublicPage();
|
const isPublicPage = usePublicPage();
|
||||||
const RemainingProviders = (
|
const RemainingProviders = (
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { EventType, PeriodType } from "@prisma/client";
|
import { EventType, PeriodType } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import dayjsBusinessTime from "dayjs-business-days2";
|
||||||
|
|
||||||
|
dayjs.extend(dayjsBusinessTime);
|
||||||
|
|
||||||
function isOutOfBounds(
|
function isOutOfBounds(
|
||||||
time: dayjs.ConfigType,
|
time: dayjs.ConfigType,
|
||||||
|
|
|
@ -58,10 +58,10 @@
|
||||||
"@radix-ui/react-tooltip": "^0.1.0",
|
"@radix-ui/react-tooltip": "^0.1.0",
|
||||||
"@stripe/react-stripe-js": "^1.8.0",
|
"@stripe/react-stripe-js": "^1.8.0",
|
||||||
"@stripe/stripe-js": "^1.29.0",
|
"@stripe/stripe-js": "^1.29.0",
|
||||||
"@trpc/client": "^9.23.4",
|
"@trpc/client": "^9.25.2",
|
||||||
"@trpc/next": "^9.23.4",
|
"@trpc/next": "^9.25.2",
|
||||||
"@trpc/react": "^9.23.4",
|
"@trpc/react": "^9.25.2",
|
||||||
"@trpc/server": "^9.23.4",
|
"@trpc/server": "^9.25.2",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { UserPlan } from "@prisma/client";
|
import { UserPlan } from "@prisma/client";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import { GetStaticPropsContext } from "next";
|
import { GetStaticPropsContext } from "next";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
import { z } from "zod";
|
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({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
|
@ -150,6 +154,13 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st
|
||||||
|
|
||||||
const profile = eventType.users[0] || user;
|
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 {
|
return {
|
||||||
props: {
|
props: {
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
|
@ -168,18 +179,18 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st
|
||||||
},
|
},
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDynamicGroupPageProps({
|
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
||||||
usernameList,
|
const { ssgInit } = await import("@server/lib/ssg");
|
||||||
length,
|
const ssg = await ssgInit(context);
|
||||||
}: {
|
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
||||||
usernameList: string[];
|
const usernameList = getUsernameList(userParam);
|
||||||
length: number;
|
const length = parseInt(typeParam);
|
||||||
}) {
|
|
||||||
const eventType = getDefaultEvent("" + length);
|
const eventType = getDefaultEvent("" + length);
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
|
@ -264,6 +275,7 @@ async function getDynamicGroupPageProps({
|
||||||
profile,
|
profile,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
|
@ -272,17 +284,13 @@ async function getDynamicGroupPageProps({
|
||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
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.
|
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
||||||
const isDynamicGroup = userParam.includes("+");
|
const isDynamicGroup = userParam.includes("+");
|
||||||
if (isDynamicGroup) {
|
if (isDynamicGroup) {
|
||||||
return await getDynamicGroupPageProps({
|
return await getDynamicGroupPageProps(context);
|
||||||
usernameList: getUsernameList(userParam),
|
|
||||||
length: parseInt(typeParam),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return await getUserPageProps({ username: userParam, slug: typeParam });
|
return await getUserPageProps(context);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
import type { AppRouter } from "@server/routers/_app";
|
import type { AppRouter } from "@server/routers/_app";
|
||||||
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
|
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
|
||||||
|
import { httpLink } from "@trpc/client/links/httpLink";
|
||||||
import { loggerLink } from "@trpc/client/links/loggerLink";
|
import { loggerLink } from "@trpc/client/links/loggerLink";
|
||||||
|
import { splitLink } from "@trpc/client/links/splitLink";
|
||||||
import { withTRPC } from "@trpc/next";
|
import { withTRPC } from "@trpc/next";
|
||||||
import type { TRPCClientErrorLike } from "@trpc/react";
|
import type { TRPCClientErrorLike } from "@trpc/react";
|
||||||
import { Maybe } from "@trpc/server";
|
import { Maybe } from "@trpc/server";
|
||||||
|
@ -56,6 +58,13 @@ function MyApp(props: AppProps) {
|
||||||
|
|
||||||
export default withTRPC<AppRouter>({
|
export default withTRPC<AppRouter>({
|
||||||
config() {
|
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
|
* If you want to use SSR, you need to use the server's full URL
|
||||||
* @link https://trpc.io/docs/ssr
|
* @link https://trpc.io/docs/ssr
|
||||||
|
@ -70,8 +79,17 @@ export default withTRPC<AppRouter>({
|
||||||
enabled: (opts) =>
|
enabled: (opts) =>
|
||||||
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
||||||
}),
|
}),
|
||||||
httpBatchLink({
|
splitLink({
|
||||||
url: `/api/trpc`,
|
// 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
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import {
|
import {
|
||||||
BookingStatus,
|
|
||||||
User,
|
|
||||||
Booking,
|
|
||||||
Attendee,
|
Attendee,
|
||||||
|
Booking,
|
||||||
BookingReference,
|
BookingReference,
|
||||||
|
BookingStatus,
|
||||||
EventType,
|
EventType,
|
||||||
|
User,
|
||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
@ -13,7 +13,7 @@ import { getSession } from "next-auth/react";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { z, ZodError } from "zod";
|
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 { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
||||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
||||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||||
|
|
|
@ -3,8 +3,8 @@ import async from "async";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
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 { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||||
import { getCalendar } from "@calcom/core/CalendarManager";
|
|
||||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||||
import { sendCancelledEmails } from "@calcom/emails";
|
import { sendCancelledEmails } from "@calcom/emails";
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
|
|
|
@ -29,7 +29,23 @@ export default trpcNext.createNextApiHandler({
|
||||||
/**
|
/**
|
||||||
* @link https://trpc.io/docs/caching#api-response-caching
|
* @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 {};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -84,7 +84,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
},
|
},
|
||||||
async onSettled() {
|
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) {
|
export default function Settings(props: Props) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
|
@ -37,7 +37,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
||||||
});
|
});
|
||||||
|
|
||||||
// always preload i18n
|
// always preload i18n
|
||||||
await ssg.fetchQuery("viewer.i18n");
|
await ssg.fetchQuery("viewer.public.i18n");
|
||||||
|
|
||||||
return ssg;
|
return ssg;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ export async function ssrInit(context: GetServerSidePropsContext) {
|
||||||
ctx,
|
ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
// always preload "viewer.i18n"
|
// always preload "viewer.public.i18n"
|
||||||
await ssr.fetchQuery("viewer.i18n");
|
await ssr.fetchQuery("viewer.public.i18n");
|
||||||
|
|
||||||
return ssr;
|
return ssr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,8 @@ const publicViewerRouter = createRouter()
|
||||||
|
|
||||||
return await samlTenantProduct(prisma, email);
|
return await samlTenantProduct(prisma, email);
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
.merge("slots.", slotsRouter);
|
||||||
|
|
||||||
// routes only available to authenticated users
|
// routes only available to authenticated users
|
||||||
const loggedInViewerRouter = createProtectedRouter()
|
const loggedInViewerRouter = createProtectedRouter()
|
||||||
|
@ -944,12 +945,11 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const viewerRouter = createRouter()
|
export const viewerRouter = createRouter()
|
||||||
.merge(publicViewerRouter)
|
.merge("public.", publicViewerRouter)
|
||||||
.merge(loggedInViewerRouter)
|
.merge(loggedInViewerRouter)
|
||||||
.merge("bookings.", bookingsRouter)
|
.merge("bookings.", bookingsRouter)
|
||||||
.merge("eventTypes.", eventTypesRouter)
|
.merge("eventTypes.", eventTypesRouter)
|
||||||
.merge("availability.", availabilityRouter)
|
.merge("availability.", availabilityRouter)
|
||||||
.merge("teams.", viewerTeamsRouter)
|
.merge("teams.", viewerTeamsRouter)
|
||||||
.merge("webhook.", webhookRouter)
|
.merge("webhook.", webhookRouter)
|
||||||
.merge("apiKeys.", apiKeysRouter)
|
.merge("apiKeys.", apiKeysRouter);
|
||||||
.merge("slots.", slotsRouter);
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { availabilityUserSelect } from "@calcom/prisma";
|
||||||
import { stringToDayjs } from "@calcom/prisma/zod-utils";
|
import { stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||||
import { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
import { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
|
|
||||||
|
import isOutOfBounds from "@lib/isOutOfBounds";
|
||||||
import getSlots from "@lib/slots";
|
import getSlots from "@lib/slots";
|
||||||
|
|
||||||
import { createRouter } from "@server/createRouter";
|
import { createRouter } from "@server/createRouter";
|
||||||
|
@ -137,6 +138,11 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
||||||
beforeEventBuffer: true,
|
beforeEventBuffer: true,
|
||||||
afterEventBuffer: true,
|
afterEventBuffer: true,
|
||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
|
periodType: true,
|
||||||
|
periodStartDate: true,
|
||||||
|
periodEndDate: true,
|
||||||
|
periodCountCalendarDays: true,
|
||||||
|
periodDays: true,
|
||||||
schedule: {
|
schedule: {
|
||||||
select: {
|
select: {
|
||||||
availability: true,
|
availability: true,
|
||||||
|
@ -202,6 +208,14 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
||||||
afterBufferTime: eventType.afterEventBuffer,
|
afterBufferTime: eventType.afterEventBuffer,
|
||||||
currentSeats,
|
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);
|
let time = dayjs(startTime);
|
||||||
do {
|
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()
|
// 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
|
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||||
? times.filter((time) =>
|
? ("every" as const)
|
||||||
userSchedules.every((schedule) =>
|
: ("some" as const);
|
||||||
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
const filteredTimes = times
|
||||||
)
|
.filter(isWithinBounds)
|
||||||
)
|
.filter((time) =>
|
||||||
: times.filter((time) =>
|
userSchedules[filterStrategy]((schedule) =>
|
||||||
userSchedules.some((schedule) =>
|
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
||||||
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
)
|
||||||
)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
slots[yyyymmdd(time.toDate())] = filteredTimes.map((time) => ({
|
slots[yyyymmdd(time.toDate())] = filteredTimes.map((time) => ({
|
||||||
time: time.toISOString(),
|
time: time.toISOString(),
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import { Credential, SelectedCalendar } from "@prisma/client";
|
import { Credential, SelectedCalendar } from "@prisma/client";
|
||||||
|
import { createHash } from "crypto";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||||
import getApps from "@calcom/app-store/utils";
|
import getApps from "@calcom/app-store/utils";
|
||||||
import { getUid } from "@calcom/lib/CalEventParser";
|
import { getUid } from "@calcom/lib/CalEventParser";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import notEmpty from "@calcom/lib/notEmpty";
|
|
||||||
import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar";
|
import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar";
|
||||||
import type { Event } from "@calcom/types/Event";
|
|
||||||
import type { EventResult } from "@calcom/types/EventManager";
|
import type { EventResult } from "@calcom/types/EventManager";
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
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) => {
|
export const getCalendarCredentials = (credentials: Array<Credential>, userId: number) => {
|
||||||
const calendarCredentials = getApps(credentials)
|
const calendarCredentials = getApps(credentials)
|
||||||
.filter((app) => app.type.endsWith("_calendar"))
|
.filter((app) => app.type.endsWith("_calendar"))
|
||||||
|
@ -77,20 +74,51 @@ export const getConnectedCalendars = async (
|
||||||
return connectedCalendars;
|
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 (
|
export const getBusyCalendarTimes = async (
|
||||||
withCredentials: Credential[],
|
withCredentials: Credential[],
|
||||||
dateFrom: string,
|
dateFrom: string,
|
||||||
dateTo: string,
|
dateTo: string,
|
||||||
selectedCalendars: SelectedCalendar[]
|
selectedCalendars: SelectedCalendar[]
|
||||||
) => {
|
) => {
|
||||||
const calendars = withCredentials
|
|
||||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
|
||||||
.map((credential) => getCalendar(credential))
|
|
||||||
.filter(notEmpty);
|
|
||||||
|
|
||||||
let results: EventBusyDate[][] = [];
|
let results: EventBusyDate[][] = [];
|
||||||
try {
|
try {
|
||||||
results = await Promise.all(calendars.map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)));
|
results = await getCachedResults(withCredentials, dateFrom, dateTo, selectedCalendars);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(error);
|
log.warn(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,19 +34,15 @@ export type DatePickerProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Day = ({
|
export const Day = ({
|
||||||
date,
|
date,
|
||||||
active,
|
active,
|
||||||
...props
|
...props
|
||||||
}: JSX.IntrinsicElements["button"] & { active: boolean; date: Date }) => {
|
}: JSX.IntrinsicElements["button"] & { active: boolean; date: Date }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
style={props.disabled ? {} : {}}
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm border border-transparent text-center",
|
"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",
|
||||||
props.disabled
|
|
||||||
? "text-bookinglighter cursor-default font-light"
|
|
||||||
: "hover:border-brand font-medium dark:hover:border-white",
|
|
||||||
active
|
active
|
||||||
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast"
|
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast"
|
||||||
: !props.disabled
|
: !props.disabled
|
||||||
|
@ -68,9 +64,11 @@ const Days = ({
|
||||||
includedDates = [],
|
includedDates = [],
|
||||||
browsingDate,
|
browsingDate,
|
||||||
weekStart,
|
weekStart,
|
||||||
|
DayComponent = Day,
|
||||||
selected,
|
selected,
|
||||||
...props
|
...props
|
||||||
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
|
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
|
||||||
|
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
|
||||||
browsingDate: Date;
|
browsingDate: Date;
|
||||||
weekStart: number;
|
weekStart: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -90,16 +88,11 @@ const Days = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{days.map((day, idx) => (
|
{days.map((day, idx) => (
|
||||||
<div
|
<div key={day === null ? `e-${idx}` : `day-${day}`} className="relative w-full pt-[100%]">
|
||||||
key={day === null ? `e-${idx}` : `day-${day}`}
|
|
||||||
style={{
|
|
||||||
paddingTop: "100%",
|
|
||||||
}}
|
|
||||||
className="relative w-full">
|
|
||||||
{day === null ? (
|
{day === null ? (
|
||||||
<div key={`e-${idx}`} />
|
<div key={`e-${idx}`} />
|
||||||
) : (
|
) : (
|
||||||
<Day
|
<DayComponent
|
||||||
date={day}
|
date={day}
|
||||||
onClick={() => props.onChange(day)}
|
onClick={() => props.onChange(day)}
|
||||||
disabled={
|
disabled={
|
||||||
|
@ -139,7 +132,7 @@ const DatePicker = ({
|
||||||
onMonthChange,
|
onMonthChange,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
...passThroughProps
|
...passThroughProps
|
||||||
}: DatePickerProps) => {
|
}: DatePickerProps & Partial<React.ComponentProps<typeof Days>>) => {
|
||||||
const [month, setMonth] = useState(selected ? selected.getMonth() : new Date().getMonth());
|
const [month, setMonth] = useState(selected ? selected.getMonth() : new Date().getMonth());
|
||||||
|
|
||||||
const changeMonth = (newMonth: number) => {
|
const changeMonth = (newMonth: number) => {
|
||||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -3140,32 +3140,32 @@
|
||||||
javascript-natural-sort "0.7.1"
|
javascript-natural-sort "0.7.1"
|
||||||
lodash "4.17.21"
|
lodash "4.17.21"
|
||||||
|
|
||||||
"@trpc/client@^9.23.4":
|
"@trpc/client@^9.25.2":
|
||||||
version "9.23.4"
|
version "9.25.2"
|
||||||
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.23.4.tgz#d887d013ca9146299df1a33a1f13d57366a3483f"
|
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.25.2.tgz#3ece63bc58436a12a49a7ef3aa3ff54f03bdd750"
|
||||||
integrity sha512-usZgbydmqqzUqJQD5yY9f+KftYBKz6ed0J90MYETqRwXl9XtR9kKOwRRMBQwZDdeMhai3Hlk4sDWeC9HR3auRQ==
|
integrity sha512-wW4KEvG+mF5ParqmyTgSjxwOjvxtsQCso6nZeZ3PI19d9bBUprFx9E+UkS5R3gqNm9lddQx7tx+ie+OFkDmp3g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.0"
|
"@babel/runtime" "^7.9.0"
|
||||||
|
|
||||||
"@trpc/next@^9.23.4":
|
"@trpc/next@^9.25.2":
|
||||||
version "9.23.4"
|
version "9.25.2"
|
||||||
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.23.4.tgz#45c4da34cd99b882aefa0ddc065dd23c5d5467a2"
|
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.25.2.tgz#721182d1426e28848c87ef473dff781e9c812ab4"
|
||||||
integrity sha512-fPFU/PNkGawiUS56wRx3KsnRh/cvI3RtGYbZKIxLr38oYyHLct/v1uqA+0xJIoyPlcFDLoB6et4MZJuv3/v8Dg==
|
integrity sha512-OiRcmNd5sxTR23O2oVxBVoSj4DNVZe84GzXBKxShYjeDyld0OKmQzqB1H2rMsCmNU7/Yd4lYDQBjGnDVP5VxPQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.0"
|
"@babel/runtime" "^7.9.0"
|
||||||
react-ssr-prepass "^1.5.0"
|
react-ssr-prepass "^1.5.0"
|
||||||
|
|
||||||
"@trpc/react@^9.23.4":
|
"@trpc/react@^9.25.2":
|
||||||
version "9.23.4"
|
version "9.25.2"
|
||||||
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.23.4.tgz#6abb2bbabd76d72cc1a3fb72f6ecc33702cb1c3d"
|
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.25.2.tgz#93c9c9c46bfee16ede9b326c2c964a38b4f8b84d"
|
||||||
integrity sha512-adori41F2hgjijThOmoeMkIXcaRjScZi6lczRwIIy6UqoUjYb+FfO46Jr7cP/QSX1Ur8VatD6UgLCNViURTfCQ==
|
integrity sha512-osP1JmhhBLWmSkoCc19YcGiQdeEq0RtvO0uvLe2DfzGpQL1orB72nvqWHXlIGHWA4E0cnc1FRmoXokWYCwHPxQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.0"
|
"@babel/runtime" "^7.9.0"
|
||||||
|
|
||||||
"@trpc/server@^9.23.4":
|
"@trpc/server@^9.25.2":
|
||||||
version "9.23.4"
|
version "9.25.2"
|
||||||
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.23.4.tgz#78ecebbdda79db6252067d66c4a0edef025a501b"
|
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.25.2.tgz#3ac58753b4cf7b2aa1a2a51d94bd0798b495a78a"
|
||||||
integrity sha512-nlOgft5g4BziNplDHhw7f4m9+k8lRPFtVVi8VGxOINt7sQ8pzzEti0WMIl2BX6gugw92t+kae2O5e793AFJs9g==
|
integrity sha512-E5ibK5jLgWremiPs2pO+Y/YktRH7+CqmMwp97mTp9ymYZn3od4C9TuFg6bxEK1bQKnUezpzHJyGRADVKCWrjsw==
|
||||||
|
|
||||||
"@tryvital/vital-node@^1.3.6":
|
"@tryvital/vital-node@^1.3.6":
|
||||||
version "1.3.6"
|
version "1.3.6"
|
||||||
|
@ -3560,11 +3560,16 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
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"
|
version "16.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
||||||
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
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":
|
"@types/nodemailer@^6.4.4":
|
||||||
version "6.4.4"
|
version "6.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
|
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:
|
zwitch@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"
|
||||||
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==
|
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==
|
||||||
|
|
Loading…
Reference in New Issue