statically render profile pages (#615)
parent
34300650e4
commit
649e79bdc7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
pages/[user].tsx
128
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<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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue