From 649e79bdc79551fe0008060577f35ffdef6471eb Mon Sep 17 00:00:00 2001 From: Alex Johansson Date: Mon, 27 Sep 2021 17:09:19 +0100 Subject: [PATCH] statically render profile pages (#615) --- .github/workflows/build.yml | 14 ++++ .github/workflows/e2e.yml | 3 +- lib/hooks/useTheme.tsx | 8 ++- lib/trpc.ts | 3 + pages/[user].tsx | 128 ++++++++++++++---------------------- server/routers/_app.ts | 4 +- server/routers/booking.tsx | 73 ++++++++++++++++++++ 7 files changed, 152 insertions(+), 81 deletions(-) create mode 100644 server/routers/booking.tsx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9da6156587..b5b040de22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,19 @@ jobs: build: name: Build on Node ${{ matrix.node }} and ${{ matrix.os }} + env: + DATABASE_URL: postgresql://postgres:@localhost:5432/calendso + NODE_ENV: test + BASE_URL: http://localhost:3000 + JWT_SECRET: secret + services: + postgres: + image: postgres:12.1 + env: + POSTGRES_USER: postgres + POSTGRES_DB: calendso + ports: + - 5432:5432 runs-on: ${{ matrix.os }} strategy: matrix: @@ -28,5 +41,6 @@ jobs: path: ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs + - run: yarn prisma migrate deploy - run: yarn test - run: yarn build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dbadbf6116..973b4e1444 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -53,9 +53,10 @@ jobs: path: ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs - - run: yarn build + - run: yarn test - run: yarn prisma migrate deploy - run: yarn db-seed + - run: yarn build - run: yarn start & - run: npx wait-port 3000 --timeout 10000 - run: yarn cypress run diff --git a/lib/hooks/useTheme.tsx b/lib/hooks/useTheme.tsx index 09d505b920..99ad205ef1 100644 --- a/lib/hooks/useTheme.tsx +++ b/lib/hooks/useTheme.tsx @@ -1,15 +1,19 @@ +import { Maybe } from "@trpc/server"; import { useEffect, useState } from "react"; // makes sure the ui doesn't flash -export default function useTheme(theme?: string) { +export default function useTheme(theme?: Maybe) { const [isReady, setIsReady] = useState(false); useEffect(() => { + setIsReady(true); + if (!theme) { + return; + } if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.add(theme); } - setIsReady(true); }, []); return { diff --git a/lib/trpc.ts b/lib/trpc.ts index 12f74345a5..a008c73f19 100644 --- a/lib/trpc.ts +++ b/lib/trpc.ts @@ -26,3 +26,6 @@ export type inferQueryInput = inferProcedureInput< AppRouter["_def"]["mutations"][TRouteKey] >; + +export type inferMutationOutput = + inferProcedureOutput; diff --git a/pages/[user].tsx b/pages/[user].tsx index dad96ec055..cf6f536e78 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,48 +1,58 @@ import { ArrowRightIcon } from "@heroicons/react/outline"; -import { GetServerSidePropsContext } from "next"; +import { ssg } from "@server/ssg"; +import { GetStaticPaths, GetStaticPropsContext } from "next"; import Link from "next/link"; import React from "react"; import useTheme from "@lib/hooks/useTheme"; import prisma from "@lib/prisma"; +import { trpc } from "@lib/trpc"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import EventTypeDescription from "@components/eventtype/EventTypeDescription"; import { HeadSeo } from "@components/seo/head-seo"; import Avatar from "@components/ui/Avatar"; -export default function User(props: inferSSRProps) { - const { isReady } = useTheme(props.user.theme); +export default function User(props: inferSSRProps) { + const { username } = props; + // data of query below will be will be prepopulated b/c of `getStaticProps` + const query = trpc.useQuery(["booking.userEventTypes", { username }]); + const { isReady } = useTheme(query.data?.user.theme); + if (!query.data) { + // this shold never happen as we do `blocking: true` + return <>...; + } + const { user, eventTypes } = query.data; return ( <> {isReady && (

- {props.user.name || props.user.username} + {user.name || user.username}

-

{props.user.bio}

+

{user.bio}

- {props.eventTypes.map((type) => ( + {eventTypes.map((type) => ( ))}
- {props.eventTypes.length == 0 && ( + {eventTypes.length === 0 && (

Uh oh!

@@ -66,79 +76,43 @@ export default function User(props: inferSSRProps) { ); } -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const username = (context.query.user as string).toLowerCase(); - - const user = await prisma.user.findUnique({ - where: { - username, - }, +export const getStaticPaths: GetStaticPaths = async () => { + const allUsers = await prisma.user.findMany({ select: { - id: true, username: true, - email: true, - name: true, - bio: true, - avatar: true, - theme: true, - plan: true, + }, + where: { + // will statically render everyone on the PRO plan + // the rest will be statically rendered on first visit + plan: "PRO", }, }); - if (!user) { + const usernames = allUsers.flatMap((u) => (u.username ? [u.username] : [])); + return { + paths: usernames.map((user) => ({ + params: { user }, + })), + + // https://nextjs.org/docs/basic-features/data-fetching#fallback-blocking + fallback: "blocking", + }; +}; + +export async function getStaticProps(context: GetStaticPropsContext<{ user: string }>) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const username = context.params!.user; + const data = await ssg.fetchQuery("booking.userEventTypes", { username }); + + if (!data) { return { notFound: true, }; } - - const eventTypesWithHidden = await prisma.eventType.findMany({ - where: { - AND: [ - { - teamId: null, - }, - { - OR: [ - { - userId: user.id, - }, - { - users: { - some: { - id: user.id, - }, - }, - }, - ], - }, - ], - }, - select: { - id: true, - slug: true, - title: true, - length: true, - description: true, - hidden: true, - schedulingType: true, - price: true, - currency: true, - }, - take: user.plan === "FREE" ? 1 : undefined, - }); - const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden); return { props: { - eventTypes, - user, + trpcState: ssg.dehydrate(), + username, }, + revalidate: 1, }; -}; - -// Auxiliary methods -export function getRandomColorCode(): string { - let color = "#"; - for (let idx = 0; idx < 6; idx++) { - color += Math.floor(Math.random() * 10); - } - return color; } diff --git a/server/routers/_app.ts b/server/routers/_app.ts index 5809af5270..2093a8b5c0 100644 --- a/server/routers/_app.ts +++ b/server/routers/_app.ts @@ -2,6 +2,7 @@ * This file contains the root router of your tRPC-backend */ import { createRouter } from "../createRouter"; +import { bookingRouter } from "./booking"; import { viewerRouter } from "./viewer"; /** @@ -21,6 +22,7 @@ export const appRouter = createRouter() * @link https://trpc.io/docs/error-formatting */ // .formatError(({ shape, error }) => { }) - .merge("viewer.", viewerRouter); + .merge("viewer.", viewerRouter) + .merge("booking.", bookingRouter); export type AppRouter = typeof appRouter; diff --git a/server/routers/booking.tsx b/server/routers/booking.tsx new file mode 100644 index 0000000000..bb3443b94c --- /dev/null +++ b/server/routers/booking.tsx @@ -0,0 +1,73 @@ +import { z } from "zod"; + +import { createRouter } from "../createRouter"; + +export const bookingRouter = createRouter().query("userEventTypes", { + input: z.object({ + username: z.string().min(1), + }), + async resolve({ input, ctx }) { + const { prisma } = ctx; + const { username } = input; + + const user = await prisma.user.findUnique({ + where: { + username, + }, + select: { + id: true, + username: true, + email: true, + name: true, + bio: true, + avatar: true, + theme: true, + plan: true, + }, + }); + if (!user) { + return null; + } + + const eventTypesWithHidden = await prisma.eventType.findMany({ + where: { + AND: [ + { + teamId: null, + }, + { + OR: [ + { + userId: user.id, + }, + { + users: { + some: { + id: user.id, + }, + }, + }, + ], + }, + ], + }, + select: { + id: true, + slug: true, + title: true, + length: true, + description: true, + hidden: true, + schedulingType: true, + price: true, + currency: true, + }, + take: user.plan === "FREE" ? 1 : undefined, + }); + const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden); + return { + user, + eventTypes, + }; + }, +});