Skip to content

Commit

Permalink
statically render profile pages (calcom#615)
Browse files Browse the repository at this point in the history
  • Loading branch information
KATT authored Sep 27, 2021
1 parent 3430065 commit 649e79b
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 81 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
3 changes: 2 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions lib/hooks/useTheme.tsx
Original file line number Diff line number Diff line change
@@ -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<string>) {
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 {
Expand Down
3 changes: 3 additions & 0 deletions lib/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ export type inferQueryInput<TRouteKey extends keyof AppRouter["_def"]["queries"]
export type inferMutationInput<TRouteKey extends keyof AppRouter["_def"]["mutations"]> = inferProcedureInput<
AppRouter["_def"]["mutations"][TRouteKey]
>;

export type inferMutationOutput<TRouteKey extends keyof AppRouter["_def"]["mutations"]> =
inferProcedureOutput<AppRouter["_def"]["mutations"][TRouteKey]>;
128 changes: 51 additions & 77 deletions pages/[user].tsx
Original file line number Diff line number Diff line change
@@ -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<typeof getServerSideProps>) {
const { isReady } = useTheme(props.user.theme);
export default function User(props: inferSSRProps<typeof getStaticProps>) {
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 (
<>
<HeadSeo
title={props.user.name || props.user.username}
description={props.user.name || props.user.username}
name={props.user.name || props.user.username}
avatar={props.user.avatar}
title={user.name || user.username}
description={user.name || user.username}
name={user.name || user.username}
avatar={user.avatar}
/>
{isReady && (
<div className="bg-neutral-50 dark:bg-black h-screen">
<main className="max-w-3xl mx-auto py-24 px-4">
<div className="mb-8 text-center">
<Avatar
imageSrc={props.user.avatar}
displayName={props.user.name}
imageSrc={user.avatar}
displayName={user.name}
className="mx-auto w-24 h-24 rounded-full mb-4"
/>
<h1 className="font-cal text-3xl font-bold text-neutral-900 dark:text-white mb-1">
{props.user.name || props.user.username}
{user.name || user.username}
</h1>
<p className="text-neutral-500 dark:text-white">{props.user.bio}</p>
<p className="text-neutral-500 dark:text-white">{user.bio}</p>
</div>
<div className="space-y-6" data-testid="event-types">
{props.eventTypes.map((type) => (
{eventTypes.map((type) => (
<div
key={type.id}
className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-black rounded-sm">
<ArrowRightIcon className="absolute transition-opacity h-4 w-4 right-3 top-3 text-black dark:text-white opacity-0 group-hover:opacity-100" />
<Link href={`/${props.user.username}/${type.slug}`}>
<Link href={`/${user.username}/${type.slug}`}>
<a className="block px-6 py-4">
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
<EventTypeDescription eventType={type} />
Expand All @@ -51,7 +61,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
</div>
))}
</div>
{props.eventTypes.length == 0 && (
{eventTypes.length === 0 && (
<div className="shadow overflow-hidden rounded-sm">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2>
Expand All @@ -66,79 +76,43 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
);
}

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;
}
4 changes: 3 additions & 1 deletion server/routers/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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;
73 changes: 73 additions & 0 deletions server/routers/booking.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
},
});

0 comments on commit 649e79b

Please sign in to comment.