fix height problems

This commit is contained in:
2026-06-18 01:58:53 +02:00
parent 0d79adb104
commit c742b8e457
4 changed files with 58 additions and 16 deletions

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from "react"
import CvEntry from "./CvEntry" import CvEntry from "./CvEntry"
import type { RouterOutputs } from "~/server/routers/_app" import type { RouterOutputs } from "~/server/routers/_app"
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils"
@@ -13,8 +14,9 @@ type CvCategoryProps = {
category: CvCategoryData, category: CvCategoryData,
layout: "row" | "col", layout: "row" | "col",
position?: number, position?: number,
descriptions: Record<string, ReactNode>,
} }
export default function CvCategory({ category, layout, position = 0 }: CvCategoryProps) { export default function CvCategory({ category, layout, position = 0, descriptions }: CvCategoryProps) {
const entries = category.cvEntry const entries = category.cvEntry
return ( return (
<AnimatedCard position={position} className={cn(layout == "row" ? "w-full" : "", "h-screen")}> <AnimatedCard position={position} className={cn(layout == "row" ? "w-full" : "", "h-screen")}>
@@ -28,7 +30,7 @@ export default function CvCategory({ category, layout, position = 0 }: CvCategor
<ScrollArea> <ScrollArea>
{entries.map((entry, i) => ( {entries.map((entry, i) => (
<AnimatePopUp position={position + 0.4 + i * 0.2} debugId={`cv-entry-wrapper:${category.name}:${entry.title}:${position + 0.4 + i * 0.2}`} key={entry.id}> <AnimatePopUp position={position + 0.4 + i * 0.2} debugId={`cv-entry-wrapper:${category.name}:${entry.title}:${position + 0.4 + i * 0.2}`} key={entry.id}>
<CvEntry position={position + 0.4 + i * 0.2} entry={entry} className={layout == "row" ? "w-full lg:w-fit" : undefined} /> <CvEntry position={position + 0.4 + i * 0.2} entry={entry} description={descriptions[entry.id]} className={layout == "row" ? "w-full lg:w-fit" : undefined} />
</AnimatePopUp> </AnimatePopUp>
))} ))}
</ScrollArea> </ScrollArea>

View File

@@ -1,17 +1,17 @@
import type { ReactNode } from "react"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils"
import { format } from 'date-fns' import { format } from 'date-fns'
import type { ArrayElement } from "type-fest" import type { ArrayElement } from "type-fest"
import AnimateTextIn from "~/app/_components/Animated/AnimateIn" import AnimateTextIn from "~/app/_components/Animated/AnimateIn"
import AnimatePopUp from "~/app/_components/Animated/AnimatePopUp" import AnimatedDiv from "~/app/_components/Animated/AnimatedDiv"
import { MDXRemote } from "next-mdx-remote/rsc";
import type { CvCategoryData } from "./CvCategory" import type { CvCategoryData } from "./CvCategory"
import { ClientMdx } from "~/components/ClientMdx"
export type CvEntryData = ArrayElement<CvCategoryData['cvEntry']> export type CvEntryData = ArrayElement<CvCategoryData['cvEntry']>
export default function CvEntry({ entry, className, position = 0 }: { export default function CvEntry({ entry, description, className, position = 0 }: {
entry: CvEntryData, entry: CvEntryData,
description?: ReactNode,
className?: string, className?: string,
position?: number position?: number
}) { }) {
@@ -27,11 +27,15 @@ export default function CvEntry({ entry, className, position = 0 }: {
} }
{entry.description ? {entry.description ?
<CardContent className="text-sm lg:text-base"> <CardContent className="text-sm lg:text-base">
<AnimatePopUp once position={position + 0.2} debugId={`cv-entry-description:${entry.title}:${position + 0.2}`}> {/* Fade the description in place instead of collapsing its height:
the outer entry pop-up (CvCategory) measures height:auto when it
plays, so the description must stay laid out at full height or the
entry reveals too short. */}
<AnimatedDiv once position={position + 0.2} className="opacity-0" opacity={1} duration={0.5} debugId={`cv-entry-description:${entry.title}:${position + 0.2}`}>
<article className="prose prose-zinc dark:prose-invert max-w-none"> <article className="prose prose-zinc dark:prose-invert max-w-none">
<ClientMdx source={entry.description} fallback={entry.description} /> {description ?? entry.description}
</article> </article>
</AnimatePopUp> </AnimatedDiv>
</CardContent> : </CardContent> :
<></> <></>
} }

View File

@@ -1,11 +1,16 @@
'use client' 'use client'
import type { ReactNode } from "react";
import { Sidebar, SidebarContent, SidebarProvider } from "~/components/ui/sidebar"; import { Sidebar, SidebarContent, SidebarProvider } from "~/components/ui/sidebar";
import type { RouterOutputs } from "~/server/routers/_app" import type { RouterOutputs } from "~/server/routers/_app"
import SidebarTriggerDisappearsOnMobile from "./SidebarTriggerDisappearsOnMobile"; import SidebarTriggerDisappearsOnMobile from "./SidebarTriggerDisappearsOnMobile";
import CvCategory from "./CvCategory"; import CvCategory from "./CvCategory";
import { useTimeLine } from "~/app/_providers/GsapProvicer"; import { useTimeLine } from "~/app/_providers/GsapProvicer";
export default function CvPage(props: { cv: RouterOutputs['categoryv2']['listAllWithEntries'] }) { export default function CvPage(props: {
cv: RouterOutputs['categoryv2']['listAllWithEntries'],
descriptions: Record<string, ReactNode>,
}) {
useTimeLine(props.cv) useTimeLine(props.cv)
const { descriptions } = props
const byPosition = (pos: "sidebar" | "header" | "col1" | "col2") => const byPosition = (pos: "sidebar" | "header" | "col1" | "col2") =>
props.cv?.filter((c) => c.layoutPosition === pos) ?? [] props.cv?.filter((c) => c.layoutPosition === pos) ?? []
const sidebarCategories = byPosition("sidebar") const sidebarCategories = byPosition("sidebar")
@@ -21,7 +26,7 @@ export default function CvPage(props: { cv: RouterOutputs['categoryv2']['listAll
<Sidebar> <Sidebar>
<SidebarContent className="p-2 lg:pt-[3.2rem]"> <SidebarContent className="p-2 lg:pt-[3.2rem]">
{sidebarCategories.map((cat, i) => ( {sidebarCategories.map((cat, i) => (
<CvCategory layout="col" position={i} category={cat} key={cat.id} /> <CvCategory layout="col" position={i} category={cat} descriptions={descriptions} key={cat.id} />
))} ))}
</SidebarContent> </SidebarContent>
</Sidebar> </Sidebar>
@@ -31,18 +36,18 @@ export default function CvPage(props: { cv: RouterOutputs['categoryv2']['listAll
<div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]"> <div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]">
<div id="header" className="flex w-full h-fit flex-row gap-4 flex-wrap"> <div id="header" className="flex w-full h-fit flex-row gap-4 flex-wrap">
{headerCategories.map((cat, i) => ( {headerCategories.map((cat, i) => (
<CvCategory layout="row" position={i} category={cat} key={cat.id} /> <CvCategory layout="row" position={i} category={cat} descriptions={descriptions} key={cat.id} />
))} ))}
</div> </div>
<div id="colwrapper" className="flex flex-col lg:flex-row w-full h-3/4 gap-4"> <div id="colwrapper" className="flex flex-col lg:flex-row w-full h-3/4 gap-4">
<div id="col1" className={`flex flex-col w-full ${col1Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}> <div id="col1" className={`flex flex-col w-full ${col1Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}>
{col1Categories.map((cat, i) => ( {col1Categories.map((cat, i) => (
<CvCategory layout="col" position={i} category={cat} key={cat.id} /> <CvCategory layout="col" position={i} category={cat} descriptions={descriptions} key={cat.id} />
))} ))}
</div> </div>
<div id="col2" className={`flex flex-col w-full ${col2Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}> <div id="col2" className={`flex flex-col w-full ${col2Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}>
{col2Categories.map((cat, i) => ( {col2Categories.map((cat, i) => (
<CvCategory layout="col" position={i} category={cat} key={cat.id} /> <CvCategory layout="col" position={i} category={cat} descriptions={descriptions} key={cat.id} />
))} ))}
</div> </div>
</div> </div>

View File

@@ -1,11 +1,42 @@
import { Suspense } from "react"; import { Suspense, type ReactNode } from "react";
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypeHighlight from "rehype-highlight";
import remarkGfm from "remark-gfm";
import { servTrpc as trpc } from "../_trpc/ServerClient"; import { servTrpc as trpc } from "../_trpc/ServerClient";
import { mdxComponents } from "~/components/mdx-components";
import Page from "./_components/Page"; import Page from "./_components/Page";
export default async function CvPage() { export default async function CvPage() {
const cv = await trpc.categoryv2.listAllWithEntries(); const cv = await trpc.categoryv2.listAllWithEntries();
// Render the MDX descriptions on the server so they exist at first paint.
// The client tree (which runs the GSAP entrance via useTimeLine) only places
// these already-rendered nodes — it never invokes the MDX renderer itself, so
// the 'use client' boundary stays intact and the animations no longer play
// against an un-rendered fallback.
const descriptions: Record<string, ReactNode> = {};
for (const category of cv ?? []) {
for (const entry of category.cvEntry) {
if (!entry.description?.trim()) continue;
descriptions[entry.id] = (
<MDXRemote
source={entry.description}
components={mdxComponents}
options={{
mdxOptions: {
format: "md",
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeHighlight],
},
}}
/>
);
}
}
return ( return (
<Suspense> <Suspense>
<Page cv={cv}/> <Page cv={cv} descriptions={descriptions} />
</Suspense> </Suspense>
) )
} }