Skip to content

Commit

Permalink
Move SSR approach into common hook (#104)
Browse files Browse the repository at this point in the history
* Move SSR approach into common hook
Get fully typed access on the initial data

* Change shape of SSR context data

* Merge parent context into current one

* Updates to support the cleaner SSR structure

* Add SSR around plan page
Add layout so data available in root page

* Rework admin award export
Default only

* Flip sort order on award picker

* Add award things to SSR
  • Loading branch information
byronwall authored Sep 29, 2023
1 parent e46ba3c commit 40ec15a
Show file tree
Hide file tree
Showing 24 changed files with 398 additions and 123 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const config = {
//no explicit any
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
// "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],

"import/order": [
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
3 changes: 2 additions & 1 deletion src/app/GlobalNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import Link from "next/link";

import { trpc } from "~/app/_trpc/client";
import { useQuerySsr } from "~/hooks/useQuerySsr";

export function GlobalNotifications() {
const { data: awards } = trpc.awardRouter.getAllAwardsForProfile.useQuery();
const { data: awards } = useQuerySsr(trpc.awardRouter.getAllAwardsForProfile);

const hasUnclaimedAwards = awards?.some((award) => !award.imageId) ?? false;

Expand Down
38 changes: 28 additions & 10 deletions src/app/SsrContext.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
"use client";

import { createContext } from "react";
import { type AnyProcedure, type AnyRouter } from "@trpc/server";
import { type inferTransformedProcedureOutput } from "@trpc/server/shared";
import { createContext, useContext } from "react";

import { type RouterOutputs } from "~/utils/api";
import { type appRouter } from "~/server/api/root";

export type SsrContextData = Partial<{
getPossibleSentences: RouterOutputs["questionRouter"]["getPossibleSentences"];
getSingleLearningPlan: RouterOutputs["planRouter"]["getSingleLearningPlan"];
getUserStats: RouterOutputs["questionRouter"]["getUserStats"];
}>;
import { deepMerge } from "./deepMerge";

export const SsrContext = createContext<SsrContextData>({});
export type SsrQueryShape<TRouter extends AnyRouter> = {
[TKey in keyof TRouter["_def"]["record"]]: TRouter["_def"]["record"][TKey] extends infer TRouterOrProcedure
? TRouterOrProcedure extends AnyRouter
? Partial<SsrQueryShape<TRouterOrProcedure>>
: TRouterOrProcedure extends AnyProcedure
? Partial<inferTransformedProcedureOutput<TRouterOrProcedure>>
: never
: undefined;
};

type SsrDataStructure = Partial<SsrQueryShape<typeof appRouter>>;

export const SsrContext = createContext<SsrDataStructure>({});

export function SsrContextProvider({
children,
initialData,
}: {
children: React.ReactNode;
initialData: SsrContextData;
initialData: SsrDataStructure;
}) {
const parentData = useContext(SsrContext);

// merge the data from the parent context with the data from the server
// this allows us to nest contexts and have them all be available on the client
const providerValue = deepMerge(parentData, initialData);

// console.log("SsrContextProvider", { providerValue, initialData, parentData });

return (
<SsrContext.Provider value={initialData}>{children}</SsrContext.Provider>
<SsrContext.Provider value={providerValue}>{children}</SsrContext.Provider>
);
}
1 change: 1 addition & 0 deletions src/app/_trpc/serverClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export async function getTrpcServer(_session?: any) {
prisma: prisma,
session: session,
} as any;

return appRouter.createCaller(options);
}
16 changes: 12 additions & 4 deletions src/app/admin/awards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ import {
} from "~/components/ui/card";
import { ButtonLoading } from "~/components/ButtonLoading";
import { Textarea } from "~/components/ui/textarea";
import { useQuerySsr } from "~/hooks/useQuerySsr";

export default function AdminAwards() {
const { data: allAwardImages } = trpc.awardRouter.getAllAwardImages.useQuery({
shouldLimitToProfile: false,
});
export default function AdminAwardPage() {
return <AdminAwards />;
}

function AdminAwards() {
const { data: allAwardImages } = useQuerySsr(
trpc.awardRouter.getAllAwardImages,
{
shouldLimitToProfile: false,
}
);

const utils = trpc.useContext();

Expand Down
4 changes: 1 addition & 3 deletions src/app/awards/AwardCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

import Image from "next/image";

import { cn } from "~/utils";
Expand All @@ -16,7 +14,7 @@ export function AwardCard({ award }: { award: Award }) {
);

const masteryAward = (
<p className="text-2xl">{award.word && <p>{award.word?.word}</p>}</p>
<p className="text-2xl">{award.word && <span>{award.word?.word}</span>}</p>
);

return (
Expand Down
28 changes: 28 additions & 0 deletions src/app/awards/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import { SsrContextProvider } from "../SsrContext";
import { deepMerge } from "../deepMerge";

export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const initialData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardsForProfile
);

const moreData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardImages,
{
shouldLimitToProfile: true,
}
);

const mergedData = deepMerge(initialData, moreData);

return (
<SsrContextProvider initialData={mergedData}>{children}</SsrContextProvider>
);
}
14 changes: 9 additions & 5 deletions src/app/awards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "~/components/ui/card";
import { trpc } from "~/app/_trpc/client";
import { type RouterOutputs } from "~/utils/api";
import { useQuerySsr } from "~/hooks/useQuerySsr";

import { AwardImageChoice } from "./AwardImageChoice";
import { AwardList } from "./AwardList";
Expand All @@ -20,18 +21,21 @@ export type AwardImage =
RouterOutputs["awardRouter"]["getAllAwardImages"][number];

export default function AwardsPage() {
const { data: awards } = trpc.awardRouter.getAllAwardsForProfile.useQuery();
const { data: awards } = useQuerySsr(trpc.awardRouter.getAllAwardsForProfile);

const { data: allAwardImages } = useQuerySsr(
trpc.awardRouter.getAllAwardImages,
{
shouldLimitToProfile: true,
}
);

const { data: currentWordCount } =
trpc.awardRouter.getProfileWordCount.useQuery();

const { data: currentSentenceCount } =
trpc.awardRouter.getProfileSentenceCount.useQuery();

const { data: allAwardImages } = trpc.awardRouter.getAllAwardImages.useQuery({
shouldLimitToProfile: true,
});

const wordCountAwards = awards?.filter(
(award) => award.awardType === "WORD_COUNT"
);
Expand Down
29 changes: 29 additions & 0 deletions src/app/deepMerge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type AnyObject = Record<string, any>;

export function deepMerge(obj1: AnyObject, obj2: AnyObject): AnyObject {
const output: AnyObject = { ...obj1 };

for (const key in obj2) {
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
if (
typeof obj2[key] === "object" &&
!Array.isArray(obj2[key]) &&
obj2[key] !== null
) {
if (
typeof obj1[key] === "object" &&
!Array.isArray(obj1[key]) &&
obj1[key] !== null
) {
output[key] = deepMerge(obj1[key], obj2[key]);
} else {
output[key] = obj2[key];
}
} else {
output[key] = obj2[key];
}
}
}

return output;
}
41 changes: 25 additions & 16 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { MainNav } from "~/components/main-nav";
import { marketingConfig } from "~/config/marketing";
import { getServerAuthSession } from "~/server/auth";
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import Provider from "./_trpc/Provider";
import { NextAuthProvider } from "./authProvider";
import { GlobalNotifications } from "./GlobalNotifications";
import { SentenceCreatorDialog } from "./SentenceCreatorDialog";
import { SsrContextProvider } from "./SsrContext";

import { UserMenuOrLogin } from "../components/UserMenuOrLogin";

Expand All @@ -32,27 +35,33 @@ export default async function RootLayout({
}) {
const session = await getServerAuthSession();

const initialData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardsForProfile
);

return (
<html>
<body>
<NextAuthProvider session={session}>
<Provider>
<div className="flex min-h-screen flex-col pb-20">
<header className="bg-background container z-40">
<div className="flex h-20 items-center justify-between py-6">
<MainNav items={marketingConfig.mainNav} />
<UserMenuOrLogin />
</div>
</header>
<GlobalNotifications />
<main className="flex-1">
<div className="container flex max-w-[96rem] flex-col items-center gap-4 text-center">
{children}
</div>
</main>
<SentenceCreatorDialog />
</div>
<ReactQueryDevtools initialIsOpen={false} />
<SsrContextProvider initialData={initialData}>
<div className="flex min-h-screen flex-col pb-20">
<header className="bg-background container z-40">
<div className="flex h-20 items-center justify-between py-6">
<MainNav items={marketingConfig.mainNav} />
<UserMenuOrLogin />
</div>
</header>
<GlobalNotifications />
<main className="flex-1">
<div className="container flex max-w-[96rem] flex-col items-center gap-4 text-center">
{children}
</div>
</main>
<SentenceCreatorDialog />
</div>
<ReactQueryDevtools initialIsOpen={false} />
</SsrContextProvider>
</Provider>
</NextAuthProvider>
<Analytics />
Expand Down
12 changes: 6 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { getServerAuthSession } from "~/server/auth";
import { QuestionPractice } from "~/components/QuestionPractice";
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import { getTrpcServer } from "./_trpc/serverClient";
import { SsrContextProvider } from "./SsrContext";

export default async function Home() {
const session = await getServerAuthSession();

const trpcServer = await getTrpcServer();

const getPossibleSentences =
await trpcServer.questionRouter.getPossibleSentences();
const initialData = await callQuerySsrServer(
appRouter.questionRouter.getPossibleSentences
);

if (!session) {
return (
Expand All @@ -21,7 +21,7 @@ export default async function Home() {
}

return (
<SsrContextProvider initialData={{ getPossibleSentences }}>
<SsrContextProvider initialData={initialData}>
<section className="flex flex-col items-center gap-4">
<QuestionPractice />
</section>
Expand Down
46 changes: 46 additions & 0 deletions src/app/plan/PlanPageComp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { useQuerySsr } from "~/hooks/useQuerySsr";

import { LearningPlanInputForm } from "./LearningPlanInputForm";
import { LearningPlanCard } from "./LearningPlanCard";

import { trpc } from "../_trpc/client";

export function PlanPageComp() {
const { data: learningPlans } = useQuerySsr(
trpc.planRouter.getAllLearningPlans
);

return (
<div>
<h1>Plan</h1>

<h2>All Learning Plans</h2>
<div className="flex flex-wrap p-4">
{learningPlans?.map((learningPlan) => (
<LearningPlanCard key={learningPlan.id} learningPlan={learningPlan} />
))}
</div>

<Card className="w-96">
<CardHeader>
<CardTitle>Add Learning Plan</CardTitle>
<CardDescription>
Use this section to create a new learning plan. You can add lessons
and linked words later.
</CardDescription>
</CardHeader>
<CardContent>
<LearningPlanInputForm />
</CardContent>
</Card>
</div>
);
}
11 changes: 3 additions & 8 deletions src/app/plan/[planNameSlugged]/LessonPlanSingle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import { useContext } from "react";

import { trpc } from "~/app/_trpc/client";
import {
Expand All @@ -9,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { SsrContext } from "~/app/SsrContext";
import { useQuerySsr } from "~/hooks/useQuerySsr";

import { LessonDetail } from "./LessonDetail";
import { findWordsNotInSentences } from "./findWordsNotInSentences";
Expand All @@ -18,14 +17,10 @@ import { LessonBulkImportWordsForm } from "../LessonBulkImportForm";
import { LessonInputForm } from "../LessonInputForm";

export function LearningPlanSingle({ planName }: { planName: string }) {
const { getSingleLearningPlan } = useContext(SsrContext);

const { data: learningPlan } = trpc.planRouter.getSingleLearningPlan.useQuery(
const { data: learningPlan } = useQuerySsr(
trpc.planRouter.getSingleLearningPlan,
{
learningPlanName: planName,
},
{
initialData: getSingleLearningPlan,
}
);

Expand Down
Loading

0 comments on commit 40ec15a

Please sign in to comment.