statically render profile pages (#615)

pull/785/head^2
Alex Johansson 2021-09-27 17:09:19 +01:00 committed by GitHub
parent 34300650e4
commit 649e79bdc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 152 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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