From b59fb2b3af5b182f06ddf31d7b1c3f727e2c450a Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Tue, 16 Jun 2026 19:16:31 +0200 Subject: [PATCH] fetch on render instead of fetch as you render --- src/app/cv/_components/CvCategory.tsx | 62 ++++++++--------- src/app/cv/_components/CvEntry.tsx | 97 ++++++++++----------------- src/app/cv/page.tsx | 41 ++++++----- src/server/routers/cvCategory.ts | 14 ++++ 4 files changed, 99 insertions(+), 115 deletions(-) diff --git a/src/app/cv/_components/CvCategory.tsx b/src/app/cv/_components/CvCategory.tsx index 27225a1..90bbff7 100644 --- a/src/app/cv/_components/CvCategory.tsx +++ b/src/app/cv/_components/CvCategory.tsx @@ -1,42 +1,42 @@ 'use client' -import { trpc } from "~/app/_trpc/Client" import CvEntry from "./CvEntry" import type { RouterOutputs } from "~/server/routers/_app" import { cn } from "~/lib/utils" import { AnimatedCard, CardContent, CardHeader, CardTitle } from "~/components/ui/card" import type { ArrayElement } from "type-fest" -import AnimatePopUp from "~/app/_components/Animated/AnimatePopUp"; +import AnimateTextIn from "~/app/_components/Animated/AnimateIn" +import AnimatePopUp from "~/app/_components/Animated/AnimatePopUp" import { ScrollArea } from "~/components/ui/scroll-area"; + +export type CvCategoryData = ArrayElement + type CvCategoryProps = { - initialData: ArrayElement, - layout: "row"|"col", + category: CvCategoryData, + layout: "row" | "col", position?: number, - children?: React.ReactElement> } -export default function CvCategory(props:CvCategoryProps) { - const category = trpc.categoryv2.getById.useQuery(props.initialData? props.initialData.id : ""); - const entries = trpc.entryv2.byCategoryAndToDateDescending.useQuery(category.data?.id || "") - const position = props.position ?? 0 - return ( - - - - {category.data?.name} - - - {(entries.data?.length ? entries.data?.length : 0 ) > 0 ? - - - {entries.data?.map((entry,i) => ( - - - - ))} - - - : - <> - } - - ) +export default function CvCategory({ category, layout, position = 0 }: CvCategoryProps) { + const entries = category.cvEntry + return ( + + + + {category.name} + + + {entries.length > 0 ? + + + {entries.map((entry, i) => ( + + + + ))} + + + : + <> + } + + ) } diff --git a/src/app/cv/_components/CvEntry.tsx b/src/app/cv/_components/CvEntry.tsx index 9559d69..8119585 100644 --- a/src/app/cv/_components/CvEntry.tsx +++ b/src/app/cv/_components/CvEntry.tsx @@ -1,78 +1,49 @@ -import { trpc } from "~/app/_trpc/Client" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card" -import { Skeleton } from "~/components/ui/skeleton" -import { cn, type Defined } from "~/lib/utils" +import { cn } from "~/lib/utils" import Markdown from 'react-markdown' import { format } from 'date-fns' import rehypeHighlight from 'rehype-highlight' import rehypeRaw from 'rehype-raw' -import type { RouterOutputs } from "~/server/routers/_app" import type { ArrayElement } from "type-fest" import AnimateTextIn from "~/app/_components/Animated/AnimateIn" import AnimatePopUp from "~/app/_components/Animated/AnimatePopUp" -export default function CvEntry(params: { - initialData: ArrayElement['cvEntry']>, +import type { CvCategoryData } from "./CvCategory" + +export type CvEntryData = ArrayElement + +export default function CvEntry({ entry, className, position = 0 }: { + entry: CvEntryData, className?: string, position?: number }) { - const query = trpc.entryv2.getById.useQuery(params.initialData.id); - const { data, isError, error } = query - const position = params.position ?? 0 return ( - <> - { - data ? - <> - - { - data.title ? - - - {data.title} - - : - <> - } - { - data.description ? - - -
- {data.description} -
-
-
: - <> - } - { - !data.hideDates ? - - - {`von ${format((new Date()).setTime(Date.parse(data.fromTime)), 'M. yyyy')} bis zum ${format((new Date()).setTime(Date.parse(data.toTime)), 'M. yyyy')}`} - - : - <> - } -
- : - <> - - -
- - - -
-
- -
- - - -
-
-
- + + {entry.title ? + + + {entry.title} + + : + <> } - + {entry.description ? + + +
+ {entry.description} +
+
+
: + <> + } + {!entry.hideDates ? + + + {`von ${format(new Date(entry.fromTime), 'M. yyyy')} bis zum ${format(new Date(entry.toTime), 'M. yyyy')}`} + + : + <> + } +
) } diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx index b25ebef..a8dce36 100644 --- a/src/app/cv/page.tsx +++ b/src/app/cv/page.tsx @@ -5,26 +5,25 @@ import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sideba import SidebarTriggerDisappearsOnMobile from "./_components/SidebarTriggerDisappearsOnMobile"; import CvCategory from "./_components/CvCategory"; export default function CvPage() { - const sidebarCategories = trpc.categoryv2.listByLayoutPosition.useQuery("sidebar"); - const col1Categories = trpc.categoryv2.listByLayoutPosition.useQuery("col1"); - const headerCategories = trpc.categoryv2.listByLayoutPosition.useQuery("header"); - const col2Categories = trpc.categoryv2.listByLayoutPosition.useQuery("col2"); - useTimeLine(col2Categories.data) + const cv = trpc.categoryv2.listAllWithEntries.useQuery(); + useTimeLine(cv.data) + const byPosition = (pos: "sidebar" | "header" | "col1" | "col2") => + cv.data?.filter((c) => c.layoutPosition === pos) ?? [] + const sidebarCategories = byPosition("sidebar") + const headerCategories = byPosition("header") + const col1Categories = byPosition("col1") + const col2Categories = byPosition("col2") return ( <> - {sidebarCategories.data && + {sidebarCategories.length > 0 && <> - {sidebarCategories.data?.map((cat, i) => { - if (cat !== undefined) { - return ( - - ) - } - })} + {sidebarCategories.map((cat, i) => ( + + ))} @@ -32,19 +31,19 @@ export default function CvPage() {
-
0 ? "lg:w-1/2" : ""} h-full gap-4`}> - {col1Categories.data?.map((cat, i) => ( - +
0 ? "lg:w-1/2" : ""} h-full gap-4`}> + {col1Categories.map((cat, i) => ( + ))}
-
0 ? "lg:w-1/2" : ""} h-full gap-4`}> - {col2Categories.data?.map((cat, i) => ( - +
0 ? "lg:w-1/2" : ""} h-full gap-4`}> + {col2Categories.map((cat, i) => ( + ))}
diff --git a/src/server/routers/cvCategory.ts b/src/server/routers/cvCategory.ts index 05660e5..3d13dc9 100644 --- a/src/server/routers/cvCategory.ts +++ b/src/server/routers/cvCategory.ts @@ -13,6 +13,20 @@ export const cvCategoryRouter = router({ console.log(res); return res; }), + // Single round-trip for the whole CV page: every category (across all layout + // positions) with its entries already populated. Lets the page fetch-then-render + // instead of waterfalling per-category/per-entry queries, so all content is + // present before the entrance animation runs. + listAllWithEntries: publicProcedure.query(async () => { + const res = await db.query.cvCategory.findMany({ + with: { + cvEntry: { + orderBy: (t, { desc }) => desc(t.toTime), + }, + }, + }) + return res; + }), getById: publicProcedure.input(z.string()).query(async ({input}) => { const res = await db.query.cvCategory.findFirst({ where(fields, operators) {