diff --git a/components/AddToHomescreen.tsx b/components/AddToHomescreen.tsx index e37e3099..4c4e95b4 100644 --- a/components/AddToHomescreen.tsx +++ b/components/AddToHomescreen.tsx @@ -9,44 +9,42 @@ export default function AddToHomescreen() { return null; } } - return ( - !closeBanner && ( -
-
-
-
-
- - - - - - + return !closeBanner ? ( +
+
+
+
+
+ + + + + + + +

+ + Add this app to your home screen for faster access and improved experience. -

- - Add this app to your home screen for faster access and improved experience. - -

-
+

+
-
- -
+
+
- ) - ); +
+ ) : null; } diff --git a/components/I18nLanguageHandler.tsx b/components/I18nLanguageHandler.tsx index a76ccc25..47ce20d3 100644 --- a/components/I18nLanguageHandler.tsx +++ b/components/I18nLanguageHandler.tsx @@ -1,21 +1,18 @@ import { useTranslation } from "next-i18next"; -import { useRouter } from "next/router"; -interface Props { - localeProp: string; -} +import { trpc } from "@lib/trpc"; -const I18nLanguageHandler = ({ localeProp }: Props): null => { +/** + * Auto-switches locale client-side to the logged in user's preference + */ +const I18nLanguageHandler = (): null => { const { i18n } = useTranslation("common"); - const router = useRouter(); - const { pathname } = router; - if (!localeProp) - console.warn( - `You may forgot to return 'localeProp' from 'getServerSideProps' or 'getStaticProps' in ${pathname}` - ); - if (i18n.language !== localeProp) { - i18n.changeLanguage(localeProp); + const locale = trpc.useQuery(["viewer.i18n"]).data?.locale; + + if (locale && i18n.language && i18n.language !== locale) { + if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale); } + return null; }; diff --git a/components/Shell.tsx b/components/Shell.tsx index 8326a106..1d96a201 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -11,7 +11,7 @@ import { import { signOut, useSession } from "next-auth/client"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { Fragment, ReactNode, useEffect } from "react"; +import React, { ReactNode, useEffect } from "react"; import { Toaster } from "react-hot-toast"; import LicenseBanner from "@ee/components/LicenseBanner"; @@ -49,6 +49,7 @@ function useMeQuery() { function useRedirectToLoginIfUnauthenticated() { const [session, loading] = useSession(); const router = useRouter(); + const query = useMeQuery(); useEffect(() => { if (!loading && !session) { @@ -60,6 +61,27 @@ function useRedirectToLoginIfUnauthenticated() { }); } }, [loading, session, router]); + + if (query.status !== "loading" && !query.data) { + router.replace("/auth/login"); + } +} + +function useRedirectToOnboardingIfNeeded() { + const [session, loading] = useSession(); + const router = useRouter(); + const query = useMeQuery(); + const user = query.data; + + useEffect(() => { + if (!loading && user) { + if (shouldShowOnboarding(user)) { + router.replace({ + pathname: "/getting-started", + }); + } + } + }, [loading, session, router, user]); } export function ShellSubHeading(props: { @@ -90,20 +112,10 @@ export default function Shell(props: { CTA?: ReactNode; }) { const router = useRouter(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars useRedirectToLoginIfUnauthenticated(); + useRedirectToOnboardingIfNeeded(); const telemetry = useTelemetry(); - const query = useMeQuery(); - - useEffect( - function redirectToOnboardingIfNeeded() { - if (query.data && shouldShowOnboarding(query.data)) { - router.push("/getting-started"); - } - }, - [query.data, router] - ); const navigation = [ { @@ -142,11 +154,7 @@ export default function Shell(props: { telemetry.withJitsu((jitsu) => { return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath)); }); - }, [telemetry]); - - if (query.status !== "loading" && !query.data) { - router.replace("/auth/login"); - } + }, [telemetry, router.asPath]); const pageTitle = typeof props.heading === "string" ? props.heading : props.title; diff --git a/components/webhook/EditWebhook.tsx b/components/webhook/EditWebhook.tsx index 56ea9fc9..b773f9fc 100644 --- a/components/webhook/EditWebhook.tsx +++ b/components/webhook/EditWebhook.tsx @@ -1,5 +1,4 @@ import { ArrowLeftIcon } from "@heroicons/react/solid"; -import { EventType } from "@prisma/client"; import { useEffect, useRef, useState } from "react"; import showToast from "@lib/notification"; @@ -8,11 +7,7 @@ import { Webhook } from "@lib/webhook"; import Button from "@components/ui/Button"; import Switch from "@components/ui/Switch"; -export default function EditTeam(props: { - webhook: Webhook; - eventTypes: EventType[]; - onCloseEdit: () => void; -}) { +export default function EditTeam(props: { webhook: Webhook; onCloseEdit: () => void }) { const [bookingCreated, setBookingCreated] = useState( props.webhook.eventTriggers.includes("booking_created") ); diff --git a/ee/pages/payment/[uid].tsx b/ee/pages/payment/[uid].tsx index 5666119b..df293246 100644 --- a/ee/pages/payment/[uid].tsx +++ b/ee/pages/payment/[uid].tsx @@ -1,18 +1,14 @@ import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { PaymentData } from "@ee/lib/stripe/server"; import { asStringOrThrow } from "@lib/asStringOrNull"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; export type PaymentPageProps = inferSSRProps; export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const locale = await getOrSetUserLocaleFromHeaders(context.req); - const rawPayment = await prisma.payment.findFirst({ where: { uid: asStringOrThrow(context.query.uid), @@ -103,8 +99,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => booking, payment, profile, - localeProp: locale, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/lib/QueryCell.tsx b/lib/QueryCell.tsx index 968121ca..84a7a45b 100644 --- a/lib/QueryCell.tsx +++ b/lib/QueryCell.tsx @@ -66,7 +66,7 @@ export function QueryCell( return opts.loading?.(query) ?? ; } if (query.status === "idle") { - return null; + return opts.idle?.(query) ?? ; } // impossible state return null; diff --git a/lib/app-providers.tsx b/lib/app-providers.tsx index b748f692..21dd795b 100644 --- a/lib/app-providers.tsx +++ b/lib/app-providers.tsx @@ -1,18 +1,36 @@ import { IdProvider } from "@radix-ui/react-id"; import { Provider } from "next-auth/client"; +import { appWithTranslation } from "next-i18next"; import { AppProps } from "next/dist/shared/lib/router/router"; -import React from "react"; +import React, { ComponentProps, ReactNode } from "react"; import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic"; import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry"; +import { trpc } from "./trpc"; + +const I18nextAdapter = appWithTranslation(({ children }: { children?: ReactNode }) => <>{children}); + +const CustomI18nextProvider = (props: { children: ReactNode }) => { + const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {}; + const passedProps = { + ...props, + pageProps: { ...i18n }, + router: { locale }, + } as unknown as ComponentProps; + return ; +}; + const AppProviders = (props: AppProps) => { + const session = trpc.useQuery(["viewer.session"]).data; return ( - {props.children} + + {props.children} + diff --git a/pages/[user].tsx b/pages/[user].tsx index 7af16450..1928b0d6 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,10 +1,8 @@ import { ArrowRightIcon } from "@heroicons/react/outline"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import Link from "next/link"; import React from "react"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import prisma from "@lib/prisma"; @@ -75,7 +73,6 @@ export default function User(props: inferSSRProps) { export const getServerSideProps = async (context: GetServerSidePropsContext) => { const username = (context.query.user as string).toLowerCase(); - const locale = await getOrSetUserLocaleFromHeaders(context.req); const user = await prisma.user.findUnique({ where: { @@ -139,10 +136,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { - localeProp: locale, user, eventTypes, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 4714d25f..df86c594 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -1,9 +1,7 @@ import { Prisma } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrNull } from "@lib/asStringOrNull"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -16,7 +14,6 @@ export default function Type(props: AvailabilityPageProps) { } export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const locale = await getOrSetUserLocaleFromHeaders(context.req); // get query params and typecast them to string // (would be even better to assert them instead of typecasting) const userParam = asStringOrNull(context.query.user); @@ -178,7 +175,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { - localeProp: locale, profile: { name: user.name, image: user.avatar, @@ -189,7 +185,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => date: dateParam, eventType: eventTypeObject, workingHours, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 20758e29..e9709e32 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -2,10 +2,8 @@ import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrThrow } from "@lib/asStringOrNull"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -21,8 +19,6 @@ export default function Book(props: BookPageProps) { } export async function getServerSideProps(context: GetServerSidePropsContext) { - const locale = await getOrSetUserLocaleFromHeaders(context.req); - const user = await prisma.user.findUnique({ where: { username: asStringOrThrow(context.query.user), @@ -103,7 +99,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { return { props: { - localeProp: locale, profile: { slug: user.username, name: user.name, @@ -112,7 +107,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, eventType: eventTypeObject, booking, - ...(await serverSideTranslations(locale, ["common"])), }, }; } diff --git a/pages/_app.tsx b/pages/_app.tsx index a66ddec5..11e34129 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,7 +3,6 @@ import { loggerLink } from "@trpc/client/links/loggerLink"; import { withTRPC } from "@trpc/next"; import type { TRPCClientErrorLike } from "@trpc/react"; import { Maybe } from "@trpc/server"; -import { appWithTranslation } from "next-i18next"; import { DefaultSeo } from "next-seo"; import type { AppProps as NextAppProps } from "next/app"; import superjson from "superjson"; @@ -28,7 +27,7 @@ function MyApp(props: AppProps) { return ( - + ); @@ -92,4 +91,4 @@ export default withTRPC({ * @link https://trpc.io/docs/ssr */ ssr: false, -})(appWithTranslation(MyApp)); +})(MyApp); diff --git a/pages/availability/troubleshoot.tsx b/pages/availability/troubleshoot.tsx index 9bdd3b6f..d6b46a00 100644 --- a/pages/availability/troubleshoot.tsx +++ b/pages/availability/troubleshoot.tsx @@ -1,21 +1,19 @@ -import dayjs from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; -import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useEffect, useState } from "react"; -import { getSession } from "@lib/auth"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; +import { QueryCell } from "@lib/QueryCell"; import { useLocale } from "@lib/hooks/useLocale"; -import prisma from "@lib/prisma"; -import { inferSSRProps } from "@lib/types/inferSSRProps"; +import { inferQueryOutput, trpc } from "@lib/trpc"; import Loader from "@components/Loader"; import Shell from "@components/Shell"; dayjs.extend(utc); -export default function Troubleshoot({ user }: inferSSRProps) { +type User = inferQueryOutput<"viewer.me">; + +const AvailabilityView = ({ user }: { user: User }) => { const { t } = useLocale(); const [loading, setLoading] = useState(true); const [availability, setAvailability] = useState([]); @@ -29,108 +27,92 @@ export default function Troubleshoot({ user }: inferSSRProps { - const dateFrom = date.startOf("day").utc().format(); - const dateTo = date.endOf("day").utc().format(); - - fetch(`/api/availability/${user.username}?dateFrom=${dateFrom}&dateTo=${dateTo}`) - .then((res) => { - return res.json(); - }) - .then((availableIntervals) => { - setAvailability(availableIntervals.busy); - setLoading(false); - }) - .catch((e) => { - console.error(e); - }); - }; - useEffect(() => { + const fetchAvailability = (date: Dayjs) => { + const dateFrom = date.startOf("day").utc().format(); + const dateTo = date.endOf("day").utc().format(); + setLoading(true); + + fetch(`/api/availability/${user.username}?dateFrom=${dateFrom}&dateTo=${dateTo}`) + .then((res) => { + return res.json(); + }) + .then((availableIntervals) => { + setAvailability(availableIntervals.busy); + }) + .catch((e) => { + console.error(e); + }) + .finally(() => { + setLoading(false); + }); + }; fetchAvailability(selectedDate); }, [selectedDate]); - if (loading) { - return ; - } - return ( -
- -
-
- {t("overview_of_day")}{" "} - { - setSelectedDate(dayjs(e.target.value)); - }} - /> - {t("hover_over_bold_times_tip")} -
-
-
- {t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)} -
-
- {availability.map((slot) => ( -
-
- {t("calendar_shows_busy_between")}{" "} - - {dayjs(slot.start).format("HH:mm")} - {" "} - {t("and")}{" "} - - {dayjs(slot.end).format("HH:mm")} - {" "} - {t("on")} {dayjs(slot.start).format("D")}{" "} - {t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")} -
-
- ))} - {availability.length === 0 && } -
-
- {t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)} +
+
+ {t("overview_of_day")}{" "} + { + setSelectedDate(dayjs(e.target.value)); + }} + /> + {t("hover_over_bold_times_tip")} +
+
+
+ {t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)} +
+
+ {loading ? ( + + ) : availability.length > 0 ? ( + availability.map((slot) => ( +
+
+ {t("calendar_shows_busy_between")}{" "} + + {dayjs(slot.start).format("HH:mm")} + {" "} + {t("and")}{" "} + + {dayjs(slot.end).format("HH:mm")} + {" "} + {t("on")} {dayjs(slot.start).format("D")}{" "} + {t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
+ )) + ) : ( +
+
{t("calendar_no_busy_slots")}
+
+ )} + +
+
+ {t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
+
+
+ ); +}; + +export default function Troubleshoot() { + const query = trpc.useQuery(["viewer.me"]); + const { t } = useLocale(); + return ( +
+ + } />
); } - -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - - if (!session?.user?.id) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - const user = await prisma.user.findFirst({ - where: { - id: session.user.id, - }, - select: { - startTime: true, - endTime: true, - username: true, - }, - }); - - if (!user) return { redirect: { permanent: false, destination: "/auth/login" } }; - - return { - props: { - session, - user, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -}; diff --git a/pages/bookings/[status].tsx b/pages/bookings/[status].tsx index 6feaa6d5..d672e0f0 100644 --- a/pages/bookings/[status].tsx +++ b/pages/bookings/[status].tsx @@ -23,7 +23,11 @@ export default function Bookings() { const router = useRouter(); const status = router.query?.status as BookingListingStatus; - const query = trpc.useQuery(["viewer.bookings", { status }]); + + const query = trpc.useQuery(["viewer.bookings", { status }], { + // first render has status `undefined` + enabled: !!status, + }); return ( diff --git a/pages/cancel/[uid].tsx b/pages/cancel/[uid].tsx index 00aa8b82..870bec90 100644 --- a/pages/cancel/[uid].tsx +++ b/pages/cancel/[uid].tsx @@ -2,11 +2,9 @@ import { CalendarIcon, XIcon } from "@heroicons/react/solid"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { getSession } from "next-auth/client"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; import { useState } from "react"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; import prisma from "@lib/prisma"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; @@ -145,7 +143,6 @@ export default function Type(props) { export async function getServerSideProps(context) { const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); const booking = await prisma.booking.findUnique({ where: { uid: context.query.uid, @@ -202,7 +199,6 @@ export async function getServerSideProps(context) { booking: bookingObj, cancellationAllowed: (!!session?.user && session.user.id == booking.user?.id) || booking.startTime >= new Date(), - ...(await serverSideTranslations(locale, ["common"])), }, }; } diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index a2b61091..aea6f0ad 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -19,7 +19,6 @@ import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; import React, { useEffect, useRef, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; @@ -37,7 +36,6 @@ import { import { getSession } from "@lib/auth"; import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations"; import { LocationType } from "@lib/location"; @@ -1097,8 +1095,6 @@ const EventTypePage = (props: inferSSRProps) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req, query } = context; const session = await getSession({ req }); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - const typeParam = parseInt(asStringOrThrow(query.type)); if (!session?.user?.id) { @@ -1278,7 +1274,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { session, - localeProp: locale, eventType: eventTypeObject, locationOptions, availability, @@ -1286,7 +1281,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => teamMembers, hasPaymentIntegration, currency, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/settings/billing.tsx b/pages/settings/billing.tsx index a6ce3664..8a28bce5 100644 --- a/pages/settings/billing.tsx +++ b/pages/settings/billing.tsx @@ -1,11 +1,6 @@ import { ExternalLinkIcon } from "@heroicons/react/solid"; -import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { getSession } from "@lib/auth"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; -import prisma from "@lib/prisma"; import SettingsShell from "@components/SettingsShell"; import Shell from "@components/Shell"; @@ -55,36 +50,3 @@ export default function Billing() { ); } - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - - if (!session) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - username: true, - name: true, - email: true, - bio: true, - avatar: true, - timeZone: true, - weekStart: true, - }, - }); - - return { - props: { - session, - user, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -} diff --git a/pages/settings/embed.tsx b/pages/settings/embed.tsx index 8b8044c0..77eeb03d 100644 --- a/pages/settings/embed.tsx +++ b/pages/settings/embed.tsx @@ -1,14 +1,9 @@ import { PlusIcon } from "@heroicons/react/outline"; -import { GetServerSidePropsContext } from "next"; import { useSession } from "next-auth/client"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; -import { getSession } from "@lib/auth"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; -import prisma from "@lib/prisma"; -import { inferSSRProps } from "@lib/types/inferSSRProps"; +import { trpc } from "@lib/trpc"; import { Webhook } from "@lib/webhook"; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTrigger } from "@components/Dialog"; @@ -20,7 +15,8 @@ import Switch from "@components/ui/Switch"; import EditWebhook from "@components/webhook/EditWebhook"; import WebhookList from "@components/webhook/WebhookList"; -export default function Embed(props: inferSSRProps) { +export default function Embed() { + const user = trpc.useQuery(["viewer.me"]).data; const [, loading] = useSession(); const { t } = useLocale(); @@ -51,11 +47,7 @@ export default function Embed(props: inferSSRProps) { getWebhooks(); }, []); - if (loading) { - return ; - } - - const iframeTemplate = ``; + const iframeTemplate = ``; const htmlTemplate = `${t( "schedule_a_meeting" )}${iframeTemplate}`; @@ -111,6 +103,10 @@ export default function Embed(props: inferSSRProps) { setEditWebhookEnabled(false); }; + if (loading) { + return ; + } + return ( @@ -280,41 +276,10 @@ export default function Embed(props: inferSSRProps) {
)} - {!!editWebhookEnabled && } + {!!editWebhookEnabled && webhookToEdit && ( + + )} ); } - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - - if (!session?.user?.email) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - const user = await prisma.user.findFirst({ - where: { - email: session?.user?.email, - }, - select: { - id: true, - username: true, - name: true, - email: true, - bio: true, - avatar: true, - timeZone: true, - weekStart: true, - }, - }); - - return { - props: { - session, - user, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -} diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 63763cd3..e8f0d137 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -1,7 +1,6 @@ import { InformationCircleIcon } from "@heroicons/react/outline"; import crypto from "crypto"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { RefObject, useEffect, useRef, useState } from "react"; import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; @@ -105,14 +104,12 @@ export default function Settings(props: Props) { { value: "dark", label: t("dark") }, ]; - const usernameRef = useRef(null); - const nameRef = useRef(null); - const descriptionRef = useRef(); - const avatarRef = useRef(null); - const hideBrandingRef = useRef(null); - const [selectedTheme, setSelectedTheme] = useState({ - value: props.user.theme, - }); + const usernameRef = useRef(null!); + const nameRef = useRef(null!); + const descriptionRef = useRef(null!); + const avatarRef = useRef(null!); + const hideBrandingRef = useRef(null!); + const [selectedTheme, setSelectedTheme] = useState(undefined); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart, @@ -128,25 +125,12 @@ export default function Settings(props: Props) { useEffect(() => { setSelectedTheme( - props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : null + props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : undefined ); setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart }); setSelectedLanguage({ value: props.localeProp, label: props.localeLabels[props.localeProp] }); }, []); - const handleAvatarChange = (newAvatar) => { - avatarRef.current.value = newAvatar; - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, - "value" - ).set; - nativeInputValueSetter.call(avatarRef.current, newAvatar); - const ev2 = new Event("input", { bubbles: true }); - avatarRef.current.dispatchEvent(ev2); - updateProfileHandler(ev2); - setImageSrc(newAvatar); - }; - async function updateProfileHandler(event) { event.preventDefault(); @@ -273,7 +257,18 @@ export default function Settings(props: Props) { target="avatar" id="avatar-upload" buttonMsg={t("change_avatar")} - handleAvatarChange={handleAvatarChange} + handleAvatarChange={(newAvatar) => { + avatarRef.current.value = newAvatar; + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + "value" + ).set; + nativeInputValueSetter.call(avatarRef.current, newAvatar); + const ev2 = new Event("input", { bubbles: true }); + avatarRef.current.dispatchEvent(ev2); + updateProfileHandler(ev2); + setImageSrc(newAvatar); + }} imageSrc={imageSrc} />
@@ -464,7 +459,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => ...user, emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), }, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/settings/security.tsx b/pages/settings/security.tsx index 4f1fbe7c..93b798d9 100644 --- a/pages/settings/security.tsx +++ b/pages/settings/security.tsx @@ -1,59 +1,22 @@ -import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import React from "react"; -import { getSession } from "@lib/auth"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; -import prisma from "@lib/prisma"; -import { inferSSRProps } from "@lib/types/inferSSRProps"; +import { trpc } from "@lib/trpc"; import SettingsShell from "@components/SettingsShell"; import Shell from "@components/Shell"; import ChangePasswordSection from "@components/security/ChangePasswordSection"; import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection"; -export default function Security({ user }: inferSSRProps) { +export default function Security() { + const user = trpc.useQuery(["viewer.me"]).data; const { t } = useLocale(); return ( - + ); } - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - - if (!session?.user?.id) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - const user = await prisma.user.findFirst({ - where: { - id: session.user.id, - }, - select: { - id: true, - username: true, - name: true, - twoFactorEnabled: true, - }, - }); - - if (!user) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - return { - props: { - session, - user, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -} diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index f5782833..dffe907d 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,13 +1,8 @@ import { UsersIcon } from "@heroicons/react/outline"; import { PlusIcon } from "@heroicons/react/solid"; -import { GetServerSideProps } from "next"; -import type { Session } from "next-auth"; import { useSession } from "next-auth/client"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useEffect, useRef, useState } from "react"; -import { getSession } from "@lib/auth"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; import { Member } from "@lib/member"; import { Team } from "@lib/team"; @@ -200,20 +195,3 @@ export default function Teams() { ); } - -// Export the `session` prop to use sessions with Server Side Rendering -export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (context) => { - const session = await getSession(context); - const locale = await getOrSetUserLocaleFromHeaders(context.req); - if (!session) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - return { - props: { - session, - localeProp: locale, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -}; diff --git a/pages/team/[slug].tsx b/pages/team/[slug].tsx index f432879c..0bb40fd1 100644 --- a/pages/team/[slug].tsx +++ b/pages/team/[slug].tsx @@ -1,11 +1,9 @@ import { ArrowRightIcon } from "@heroicons/react/solid"; import { Prisma } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import Link from "next/link"; import React from "react"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; @@ -102,7 +100,6 @@ function TeamPage({ team }: inferSSRProps) { } export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const locale = await getOrSetUserLocaleFromHeaders(context.req); const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; const userSelect = Prisma.validator()({ @@ -165,9 +162,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { - localeProp: locale, team, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/team/[slug]/[type].tsx b/pages/team/[slug]/[type].tsx index d25b8fee..bffc8007 100644 --- a/pages/team/[slug]/[type].tsx +++ b/pages/team/[slug]/[type].tsx @@ -1,8 +1,6 @@ import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrNull } from "@lib/asStringOrNull"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -15,7 +13,6 @@ export default function TeamType(props: AvailabilityTeamPageProps) { } export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const locale = await getOrSetUserLocaleFromHeaders(context.req); const slugParam = asStringOrNull(context.query.slug); const typeParam = asStringOrNull(context.query.type); const dateParam = asStringOrNull(context.query.date); @@ -81,7 +78,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { - localeProp: locale, profile: { name: team.name, slug: team.slug, @@ -91,7 +87,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => date: dateParam, eventType: eventTypeObject, workingHours, - ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/team/[slug]/book.tsx b/pages/team/[slug]/book.tsx index 905319d0..4d94593a 100644 --- a/pages/team/[slug]/book.tsx +++ b/pages/team/[slug]/book.tsx @@ -1,9 +1,7 @@ import { GetServerSidePropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import "react-phone-number-input/style.css"; import { asStringOrThrow } from "@lib/asStringOrNull"; -import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -16,7 +14,6 @@ export default function TeamBookingPage(props: TeamBookingPageProps) { } export async function getServerSideProps(context: GetServerSidePropsContext) { - const locale = await getOrSetUserLocaleFromHeaders(context.req); const eventTypeId = parseInt(asStringOrThrow(context.query.type)); if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) { return { @@ -89,7 +86,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { return { props: { - localeProp: locale, profile: { ...eventTypeObject.team, slug: "team/" + eventTypeObject.slug, @@ -98,7 +94,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, eventType: eventTypeObject, booking, - ...(await serverSideTranslations(locale, ["common"])), }, }; } diff --git a/public/static/locales/en/common.json b/public/static/locales/en/common.json index 6f489927..88a67503 100644 --- a/public/static/locales/en/common.json +++ b/public/static/locales/en/common.json @@ -22,6 +22,7 @@ "on": "on", "and": "and", "calendar_shows_busy_between": "Your calendar shows you as busy between", + "calendar_no_busy_slots": "Your don't have busy slots in this date.", "troubleshoot": "Troubleshoot", "troubleshoot_description": "Understand why certain times are available and others are blocked.", "overview_of_day": "Here is an overview of your day on", diff --git a/public/static/locales/es/common.json b/public/static/locales/es/common.json index 728409f2..be040bec 100644 --- a/public/static/locales/es/common.json +++ b/public/static/locales/es/common.json @@ -174,7 +174,7 @@ "profile_updated_successfully": "Perfil Actualizado con Éxito", "your_user_profile_updated_successfully": "Su perfil de usuario se ha actualizado correctamente.", "user_cannot_found_db": "El usuario parece haber iniciado sesión pero no se puede encontrar en la base de datos", - "embed_and_webhooks": "Insertar & Webhooks", + "embed_and_webhooks": "Incrustrar y Webhooks", "enabled": "Activado", "disabled": "Desactivado", "billing": "Facturación", diff --git a/server/createContext.ts b/server/createContext.ts index 5eee4f65..bf892408 100644 --- a/server/createContext.ts +++ b/server/createContext.ts @@ -3,6 +3,7 @@ import * as trpc from "@trpc/server"; import { Maybe } from "@trpc/server"; import * as trpcNext from "@trpc/server/adapters/next"; import { NextApiRequest } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { getSession, Session } from "@lib/auth"; import { getLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; @@ -83,7 +84,9 @@ export const createContext = async ({ req, res }: trpcNext.CreateNextContextOpti const user = await getUserFromSession({ session, req }); const locale = user?.locale ?? getLocaleFromHeaders(req); + const i18n = await serverSideTranslations(locale, ["common"]); return { + i18n, prisma, session, user, diff --git a/server/createRouter.ts b/server/createRouter.ts index d274a506..8334b732 100644 --- a/server/createRouter.ts +++ b/server/createRouter.ts @@ -11,13 +11,14 @@ export function createRouter() { export function createProtectedRouter() { return createRouter().middleware(({ ctx, next }) => { - if (!ctx.user) { + if (!ctx.user || !ctx.session) { throw new trpc.TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { ...ctx, - // infers that `user` is non-nullable to downstream procedures + // infers that `user` and `session` are non-nullable to downstream procedures + session: ctx.session, user: ctx.user, }, }); diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx index 94c60c8b..8aff7302 100644 --- a/server/routers/viewer.tsx +++ b/server/routers/viewer.tsx @@ -11,14 +11,31 @@ import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations"; import slugify from "@lib/slugify"; import { getCalendarAdapterOrNull } from "../../lib/calendarClient"; -import { createProtectedRouter } from "../createRouter"; +import { createProtectedRouter, createRouter } from "../createRouter"; import { resizeBase64Image } from "../lib/resizeBase64Image"; const checkUsername = process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? checkPremiumUsername : checkRegularUsername; +// things that unauthenticated users can query about themselves +const publicViewerRouter = createRouter() + .query("session", { + resolve({ ctx }) { + return ctx.session; + }, + }) + .query("i18n", { + async resolve({ ctx }) { + const { locale, i18n } = ctx; + return { + i18n, + locale, + }; + }, + }); + // routes only available to authenticated users -export const viewerRouter = createProtectedRouter() +const loggedInViewerRouter = createProtectedRouter() .query("me", { resolve({ ctx }) { const { @@ -34,6 +51,7 @@ export const viewerRouter = createProtectedRouter() avatar, createdDate, completedOnboarding, + twoFactorEnabled, } = ctx.user; const me = { id, @@ -47,6 +65,7 @@ export const viewerRouter = createProtectedRouter() avatar, createdDate, completedOnboarding, + twoFactorEnabled, }; return me; }, @@ -251,3 +270,5 @@ export const viewerRouter = createProtectedRouter() }); }, }); + +export const viewerRouter = createRouter().merge(publicViewerRouter).merge(loggedInViewerRouter);