fix i18n flicker on booking pages (#1013)

This commit is contained in:
Alex Johansson 2021-10-20 18:00:11 +02:00 committed by GitHub
parent b8e8319b23
commit c28d800aa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 76 additions and 44 deletions

View file

@ -1,7 +1,7 @@
import { IdProvider } from "@radix-ui/react-id"; import { IdProvider } from "@radix-ui/react-id";
import { Provider } from "next-auth/client"; import { Provider } from "next-auth/client";
import { appWithTranslation } from "next-i18next"; import { appWithTranslation } from "next-i18next";
import { AppProps } from "next/dist/shared/lib/router/router"; import type { AppProps as NextAppProps } from "next/app";
import React, { ComponentProps, ReactNode } from "react"; import React, { ComponentProps, ReactNode } from "react";
import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic"; import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic";
@ -12,24 +12,38 @@ import { trpc } from "./trpc";
const I18nextAdapter = appWithTranslation(({ children }: { children?: ReactNode }) => <>{children}</>); const I18nextAdapter = appWithTranslation(({ children }: { children?: ReactNode }) => <>{children}</>);
const CustomI18nextProvider = (props: { children: ReactNode }) => { // Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = NextAppProps & {
/** Will be defined only is there was an error */
err?: Error;
};
type AppPropsWithChildren = AppProps & {
children: ReactNode;
};
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {}; const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {};
const passedProps = { const passedProps = {
...props, ...props,
pageProps: { ...i18n }, pageProps: {
router: { locale }, ...props.pageProps,
...i18n,
},
router: locale ? { locale } : props.router,
} as unknown as ComponentProps<typeof I18nextAdapter>; } as unknown as ComponentProps<typeof I18nextAdapter>;
return <I18nextAdapter {...passedProps} />; return <I18nextAdapter {...passedProps} />;
}; };
const AppProviders = (props: AppProps) => { const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.useQuery(["viewer.session"]).data; const session = trpc.useQuery(["viewer.session"]).data;
return ( return (
<TelemetryProvider value={createTelemetryClient()}> <TelemetryProvider value={createTelemetryClient()}>
<IdProvider> <IdProvider>
<DynamicIntercomProvider> <DynamicIntercomProvider>
<Provider session={session || undefined}> <Provider session={session || undefined}>
<CustomI18nextProvider>{props.children}</CustomI18nextProvider> <CustomI18nextProvider {...props}>{props.children}</CustomI18nextProvider>
</Provider> </Provider>
</DynamicIntercomProvider> </DynamicIntercomProvider>
</IdProvider> </IdProvider>

View file

@ -12,18 +12,22 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps>) { export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { isReady } = useTheme(props.user.theme); const { isReady } = useTheme(props.user.theme);
const { user, eventTypes } = props; const { user, eventTypes } = props;
const { t } = useLocale(); const { t } = useLocale();
const nameOrUsername = user.name || user.username || "";
return ( return (
<> <>
<HeadSeo <HeadSeo
title={user.name || user.username} title={nameOrUsername}
description={user.name || user.username} description={nameOrUsername}
name={user.name || user.username} name={nameOrUsername}
avatar={user.avatar} avatar={user.avatar || undefined}
/> />
{isReady && ( {isReady && (
<div className="bg-neutral-50 dark:bg-black h-screen"> <div className="bg-neutral-50 dark:bg-black h-screen">
@ -31,11 +35,11 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<Avatar <Avatar
imageSrc={user.avatar} imageSrc={user.avatar}
displayName={user.name}
className="mx-auto w-24 h-24 rounded-full mb-4" className="mx-auto w-24 h-24 rounded-full mb-4"
alt={nameOrUsername}
/> />
<h1 className="font-cal text-3xl font-bold text-neutral-900 dark:text-white mb-1"> <h1 className="font-cal text-3xl font-bold text-neutral-900 dark:text-white mb-1">
{user.name || user.username} {nameOrUsername}
</h1> </h1>
<p className="text-neutral-500 dark:text-white">{user.bio}</p> <p className="text-neutral-500 dark:text-white">{user.bio}</p>
</div> </div>
@ -72,6 +76,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
} }
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const username = (context.query.user as string).toLowerCase(); const username = (context.query.user as string).toLowerCase();
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@ -138,6 +144,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
props: { props: {
user, user,
eventTypes, eventTypes,
trpcState: ssr.dehydrate(),
}, },
}; };
}; };

View file

@ -7,6 +7,8 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage"; import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps>; export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type(props: AvailabilityPageProps) { export default function Type(props: AvailabilityPageProps) {
@ -14,6 +16,7 @@ export default function Type(props: AvailabilityPageProps) {
} }
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
// get query params and typecast them to string // get query params and typecast them to string
// (would be even better to assert them instead of typecasting) // (would be even better to assert them instead of typecasting)
const userParam = asStringOrNull(context.query.user); const userParam = asStringOrNull(context.query.user);
@ -185,6 +188,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
date: dateParam, date: dateParam,
eventType: eventTypeObject, eventType: eventTypeObject,
workingHours, workingHours,
trpcState: ssr.dehydrate(),
}, },
}; };
}; };

View file

@ -9,6 +9,8 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage"; import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -19,6 +21,8 @@ export default function Book(props: BookPageProps) {
} }
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
username: asStringOrThrow(context.query.user), username: asStringOrThrow(context.query.user),
@ -107,6 +111,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}, },
eventType: eventTypeObject, eventType: eventTypeObject,
booking, booking,
trpcState: ssr.dehydrate(),
}, },
}; };
} }

View file

@ -1,8 +1,7 @@
import { DefaultSeo } from "next-seo"; import { DefaultSeo } from "next-seo";
import type { AppProps as NextAppProps } from "next/app";
import superjson from "superjson"; import superjson from "superjson";
import AppProviders from "@lib/app-providers"; import AppProviders, { AppProps } from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config"; import { seoConfig } from "@lib/config/next-seo.config";
import I18nLanguageHandler from "@components/I18nLanguageHandler"; import I18nLanguageHandler from "@components/I18nLanguageHandler";
@ -16,12 +15,6 @@ import { Maybe } from "@trpc/server";
import "../styles/globals.css"; import "../styles/globals.css";
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = NextAppProps & {
/** Will be defined only is there was an error */
err?: Error;
};
function MyApp(props: AppProps) { function MyApp(props: AppProps) {
const { Component, pageProps, err } = props; const { Component, pageProps, err } = props;
return ( return (

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { NextApiRequest } from "next"; import { GetServerSidePropsContext, NextApiRequest } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { getSession, Session } from "@lib/auth"; import { getSession, Session } from "@lib/auth";
@ -11,7 +11,15 @@ import * as trpc from "@trpc/server";
import { Maybe } from "@trpc/server"; import { Maybe } from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next"; import * as trpcNext from "@trpc/server/adapters/next";
async function getUserFromSession({ session, req }: { session: Maybe<Session>; req: NextApiRequest }) { type CreateContextOptions = trpcNext.CreateNextContextOptions | GetServerSidePropsContext;
async function getUserFromSession({
session,
req,
}: {
session: Maybe<Session>;
req: CreateContextOptions["req"];
}) {
if (!session?.user?.id) { if (!session?.user?.id) {
return null; return null;
} }
@ -80,7 +88,7 @@ async function getUserFromSession({ session, req }: { session: Maybe<Session>; r
* Creates context for an incoming request * Creates context for an incoming request
* @link https://trpc.io/docs/context * @link https://trpc.io/docs/context
*/ */
export const createContext = async ({ req, res }: trpcNext.CreateNextContextOptions) => { export const createContext = async ({ req, res }: CreateContextOptions) => {
// for API-response caching see https://trpc.io/docs/caching // for API-response caching see https://trpc.io/docs/caching
const session = await getSession({ req }); const session = await getSession({ req });

22
server/lib/ssr.ts Normal file
View file

@ -0,0 +1,22 @@
import { GetServerSidePropsContext } from "next";
import superjson from "superjson";
import { createContext } from "@server/createContext";
import { createSSGHelpers } from "@trpc/react/ssg";
import { appRouter } from "../routers/_app";
export async function ssrInit(context: GetServerSidePropsContext) {
const ctx = await createContext(context);
const ssr = createSSGHelpers({
router: appRouter,
transformer: superjson,
ctx,
});
// always preload "viewer.i18n"
await ssr.fetchQuery("viewer.i18n");
return ssr;
}

View file

@ -1,21 +0,0 @@
import prisma from "@lib/prisma";
import { createSSGHelpers } from "@trpc/react/ssg";
import { appRouter } from "./routers/_app";
export const ssg = createSSGHelpers({
router: appRouter,
ctx: {
prisma,
session: null,
user: null,
i18n: {
_nextI18Next: {
initialI18nStore: null,
userConfig: null,
},
},
locale: "en",
},
});