Ends the war between tRPC and next-i18next (#939)

* Ends the war between tRPC and next-i18next

* Locale fixes

* Linting

* Linting

* trpc i18n (not working) (#942)

* simplify i18n handler and remove redundant(?) fn check

* split up viewer to a "logged in only" and "public"

* wip -- skip first render

Co-authored-by: Omar López <zomars@me.com>

* Linting

* I18n fixes

* We don't need serverSideTranslations in every page anymore

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
This commit is contained in:
Omar López 2021-10-14 04:57:49 -06:00 committed by GitHub
parent 26f20e2397
commit 0861d7cc61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 243 additions and 401 deletions

View file

@ -9,44 +9,42 @@ export default function AddToHomescreen() {
return null;
}
}
return (
!closeBanner && (
<div className="fixed sm:hidden bottom-0 inset-x-0 pb-2 sm:pb-5">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="p-2 rounded-lg shadow-lg sm:p-3" style={{ background: "#2F333D" }}>
<div className="flex items-center justify-between flex-wrap">
<div className="w-0 flex-1 flex items-center">
<span className="flex p-2 rounded-lg bg-opacity-30 bg-black">
<svg
className="h-7 w-7 text-indigo-500 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
enableBackground="new 0 0 50 50">
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z" />
<path d="M24 7h2v21h-2z" />
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
</svg>
return !closeBanner ? (
<div className="fixed sm:hidden bottom-0 inset-x-0 pb-2 sm:pb-5">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="p-2 rounded-lg shadow-lg sm:p-3" style={{ background: "#2F333D" }}>
<div className="flex items-center justify-between flex-wrap">
<div className="w-0 flex-1 flex items-center">
<span className="flex p-2 rounded-lg bg-opacity-30 bg-black">
<svg
className="h-7 w-7 text-indigo-500 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
enableBackground="new 0 0 50 50">
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z" />
<path d="M24 7h2v21h-2z" />
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
</svg>
</span>
<p className="ml-3 text-xs font-medium text-white">
<span className="inline">
Add this app to your home screen for faster access and improved experience.
</span>
<p className="ml-3 text-xs font-medium text-white">
<span className="inline">
Add this app to your home screen for faster access and improved experience.
</span>
</p>
</div>
</p>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
onClick={() => setCloseBanner(true)}
type="button"
className="-mr-1 flex p-2 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">Dismiss</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
onClick={() => setCloseBanner(true)}
type="button"
className="-mr-1 flex p-2 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">Dismiss</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
)
);
</div>
) : null;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ export function QueryCell<TData, TError extends ErrorLike>(
return opts.loading?.(query) ?? <Loader />;
}
if (query.status === "idle") {
return null;
return opts.idle?.(query) ?? <Loader />;
}
// impossible state
return null;

View file

@ -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<typeof I18nextAdapter>;
return <I18nextAdapter {...passedProps} />;
};
const AppProviders = (props: AppProps) => {
const session = trpc.useQuery(["viewer.session"]).data;
return (
<TelemetryProvider value={createTelemetryClient()}>
<IdProvider>
<DynamicIntercomProvider>
<Provider session={props.pageProps.session}>{props.children}</Provider>
<Provider session={session || undefined}>
<CustomI18nextProvider>{props.children}</CustomI18nextProvider>
</Provider>
</DynamicIntercomProvider>
</IdProvider>
</TelemetryProvider>

View file

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

View file

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

View file

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

View file

@ -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 (
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler localeProp={pageProps.localeProp} />
<I18nLanguageHandler />
<Component {...pageProps} err={err} />
</AppProviders>
);
@ -92,4 +91,4 @@ export default withTRPC<AppRouter>({
* @link https://trpc.io/docs/ssr
*/
ssr: false,
})(appWithTranslation(MyApp));
})(MyApp);

View file

@ -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<typeof getServerSideProps>) {
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<typeof getServerSid
return `${h}:${m}`;
}
const fetchAvailability = (date) => {
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 <Loader />;
}
return (
<div>
<Shell heading={t("troubleshoot")} subtitle={t("troubleshoot_description")}>
<div className="bg-white max-w-xl overflow-hidden shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline border-none h-8 p-0"
defaultValue={selectedDate.format("YYYY-MM-DD")}
onBlur={(e) => {
setSelectedDate(dayjs(e.target.value));
}}
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-black overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{availability.map((slot) => (
<div key={slot.start} className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-black">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
</div>
</div>
))}
{availability.length === 0 && <Loader />}
<div className="bg-black overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
<div className="bg-white max-w-xl overflow-hidden shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline border-none h-8 p-0"
defaultValue={selectedDate.format("YYYY-MM-DD")}
onChange={(e) => {
setSelectedDate(dayjs(e.target.value));
}}
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-black overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{loading ? (
<Loader />
) : availability.length > 0 ? (
availability.map((slot) => (
<div key={slot.start} className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-black">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
</div>
</div>
))
) : (
<div className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-black">{t("calendar_no_busy_slots")}</div>
</div>
)}
<div className="bg-black overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
</div>
</div>
</div>
</div>
</div>
);
};
export default function Troubleshoot() {
const query = trpc.useQuery(["viewer.me"]);
const { t } = useLocale();
return (
<div>
<Shell heading={t("troubleshoot")} subtitle={t("troubleshoot_description")}>
<QueryCell query={query} success={({ data }) => <AvailabilityView user={data} />} />
</Shell>
</div>
);
}
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"])),
},
};
};

View file

@ -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 (
<Shell heading={t("bookings")} subtitle={t("bookings_description")}>

View file

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

View file

@ -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<typeof getServerSideProps>) => {
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"])),
},
};
};

View file

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

View file

@ -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<typeof getServerSideProps>) {
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<typeof getServerSideProps>) {
getWebhooks();
}, []);
if (loading) {
return <Loader />;
}
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_APP_URL}/${props.user?.username}" frameborder="0" allowfullscreen></iframe>`;
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_BASE_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${t(
"schedule_a_meeting"
)}</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
@ -111,6 +103,10 @@ export default function Embed(props: inferSSRProps<typeof getServerSideProps>) {
setEditWebhookEnabled(false);
};
if (loading) {
return <Loader />;
}
return (
<Shell heading={t("embed_and_webhooks")} subtitle={t("integrate_using_embed_or_webhooks")}>
<SettingsShell>
@ -280,41 +276,10 @@ export default function Embed(props: inferSSRProps<typeof getServerSideProps>) {
</a>
</div>
)}
{!!editWebhookEnabled && <EditWebhook webhook={webhookToEdit} onCloseEdit={onCloseEdit} />}
{!!editWebhookEnabled && webhookToEdit && (
<EditWebhook webhook={webhookToEdit} onCloseEdit={onCloseEdit} />
)}
</SettingsShell>
</Shell>
);
}
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"])),
},
};
}

View file

@ -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<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const descriptionRef = useRef<HTMLTextAreaElement>();
const avatarRef = useRef<HTMLInputElement>(null);
const hideBrandingRef = useRef<HTMLInputElement>(null);
const [selectedTheme, setSelectedTheme] = useState<null | { value: string | null }>({
value: props.user.theme,
});
const usernameRef = useRef<HTMLInputElement>(null!);
const nameRef = useRef<HTMLInputElement>(null!);
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
const avatarRef = useRef<HTMLInputElement>(null!);
const hideBrandingRef = useRef<HTMLInputElement>(null!);
const [selectedTheme, setSelectedTheme] = useState<undefined | { value: string; label: string }>(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}
/>
</div>
@ -464,7 +459,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
...(await serverSideTranslations(locale, ["common"])),
},
};
};

View file

@ -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<typeof getServerSideProps>) {
export default function Security() {
const user = trpc.useQuery(["viewer.me"]).data;
const { t } = useLocale();
return (
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
<SettingsShell>
<ChangePasswordSection />
<TwoFactorAuthSection twoFactorEnabled={user.twoFactorEnabled} />
<TwoFactorAuthSection twoFactorEnabled={user?.twoFactorEnabled} />
</SettingsShell>
</Shell>
);
}
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"])),
},
};
}

View file

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

View file

@ -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<typeof getServerSideProps>) {
}
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<Prisma.UserSelect>()({
@ -165,9 +162,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
localeProp: locale,
team,
...(await serverSideTranslations(locale, ["common"])),
},
};
};

View file

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

View file

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

View file

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

View file

@ -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 &amp; Webhooks",
"embed_and_webhooks": "Incrustrar y Webhooks",
"enabled": "Activado",
"disabled": "Desactivado",
"billing": "Facturación",

View file

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

View file

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

View file

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