Files
gregorlohaus.com/src/app/cv/_components/CvEntry.tsx
2026-06-18 01:58:53 +02:00

53 lines
2.0 KiB
TypeScript

import type { ReactNode } from "react"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { cn } from "~/lib/utils"
import { format } from 'date-fns'
import type { ArrayElement } from "type-fest"
import AnimateTextIn from "~/app/_components/Animated/AnimateIn"
import AnimatedDiv from "~/app/_components/Animated/AnimatedDiv"
import type { CvCategoryData } from "./CvCategory"
export type CvEntryData = ArrayElement<CvCategoryData['cvEntry']>
export default function CvEntry({ entry, description, className, position = 0 }: {
entry: CvEntryData,
description?: ReactNode,
className?: string,
position?: number
}) {
return (
<Card className={className ? cn("w-fit", className) : "w-fit"}>
{entry.title ?
<CardHeader>
<AnimateTextIn position={position} animation="slide" debugId={`cv-entry-title:${entry.title}:${position}`}>
<CardTitle> {entry.title} </CardTitle>
</AnimateTextIn>
</CardHeader> :
<></>
}
{entry.description ?
<CardContent className="text-sm lg:text-base">
{/* 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">
{description ?? entry.description}
</article>
</AnimatedDiv>
</CardContent> :
<></>
}
{!entry.hideDates ?
<CardFooter className="text-sm">
<AnimateTextIn position={position + 0.4} debugId={`cv-entry-dates:${entry.title}:${position + 0.4}`}>
{`von ${format(new Date(entry.fromTime), 'M. yyyy')} bis zum ${format(new Date(entry.toTime), 'M. yyyy')}`}
</AnimateTextIn>
</CardFooter> :
<></>
}
</Card>
)
}