Compare commits
5 Commits
additional
...
1100e35091
| Author | SHA1 | Date | |
|---|---|---|---|
| 1100e35091 | |||
| 246d7339fb | |||
| fb379b912a | |||
| ffa475e876 | |||
| d85cc205de |
165
src/app/_components/Home/HomeHero.tsx
Normal file
165
src/app/_components/Home/HomeHero.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGSAP } from "@gsap/react";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
// The centerpiece is the site's own "G" mark (public/GLIcon.svg). GSAP draws the
|
||||||
|
// outline on with a stroke-dash trick, fades the fill in, then keeps everything
|
||||||
|
// gently alive (floating logo, counter-rotating rings). A curved arrow in the
|
||||||
|
// lower-right corner, with its label above the tail, points at the chat FAB to
|
||||||
|
// nudge people toward the assistant.
|
||||||
|
export default function HomeHero() {
|
||||||
|
const root = useRef<HTMLDivElement>(null);
|
||||||
|
const gPath = useRef<SVGPathElement>(null);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
const logo = gPath.current;
|
||||||
|
|
||||||
|
if (logo) {
|
||||||
|
const len = logo.getTotalLength();
|
||||||
|
gsap.set(logo, { strokeDasharray: len, strokeDashoffset: len });
|
||||||
|
}
|
||||||
|
gsap.set(".hero-fill", { fillOpacity: 0 });
|
||||||
|
|
||||||
|
const tl = gsap.timeline({ defaults: { ease: "power3.out" } });
|
||||||
|
|
||||||
|
tl.from(
|
||||||
|
".hero-ring",
|
||||||
|
{ scale: 0, opacity: 0, duration: 0.9, stagger: 0.12, svgOrigin: "100 100" },
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (logo) tl.to(logo, { strokeDashoffset: 0, duration: 1.3 }, 0.25);
|
||||||
|
tl.to(".hero-fill", { fillOpacity: 1, duration: 0.6 }, "-=0.35");
|
||||||
|
tl.from(".hero-line", { yPercent: 120, opacity: 0, duration: 0.7, stagger: 0.12 }, "-=0.25");
|
||||||
|
tl.from(".hero-arrow-label", { yPercent: 120, opacity: 0, duration: 0.6 }, "-=0.1");
|
||||||
|
tl.from(
|
||||||
|
".hero-arrow-svg",
|
||||||
|
{ opacity: 0, scale: 0.7, transformOrigin: "top left", duration: 0.6 },
|
||||||
|
"<+=0.1",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Idle life — runs forever once the entrance has settled.
|
||||||
|
gsap.to(".hero-logo", { y: -12, duration: 3.2, ease: "sine.inOut", yoyo: true, repeat: -1 });
|
||||||
|
gsap.to(".hero-ring-spin", { rotation: 360, duration: 44, ease: "none", repeat: -1, svgOrigin: "100 100" });
|
||||||
|
gsap.to(".hero-ring-spin-rev", { rotation: -360, duration: 32, ease: "none", repeat: -1, svgOrigin: "100 100" });
|
||||||
|
gsap.to(".hero-arrow-wrap", { y: 10, duration: 1.5, ease: "sine.inOut", yoyo: true, repeat: -1 });
|
||||||
|
},
|
||||||
|
{ scope: root },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={root}
|
||||||
|
className="relative flex h-full w-full flex-col items-center justify-center overflow-hidden px-6 text-center"
|
||||||
|
>
|
||||||
|
{/* Logo + orbiting rings */}
|
||||||
|
<div className="hero-logo relative h-56 w-56 sm:h-64 sm:w-64">
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 h-full w-full text-primary/40"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="hero-ring hero-ring-spin"
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="92"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="2 10"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className="hero-ring hero-ring-spin-rev"
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="78"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeDasharray="14 8"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className="hero-ring"
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="64"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.35"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 h-full w-full p-10 text-foreground"
|
||||||
|
viewBox="0 0 74.193405 74.232162"
|
||||||
|
aria-label="Gregor Lohaus logo"
|
||||||
|
>
|
||||||
|
<g transform="translate(-24.550957,-64.437925)">
|
||||||
|
<path
|
||||||
|
ref={gPath}
|
||||||
|
className="hero-fill"
|
||||||
|
d="m 61.66652,64.437927 c -20.498425,1.81e-4 -37.115669,16.617653 -37.115564,37.116083 -1.05e-4,20.49842 16.617139,37.1159 37.115564,37.11608 16.081184,-0.0265 30.316081,-10.4061 35.258313,-25.70903 1.144195,-3.51294 1.757471,-7.1771 1.819527,-10.87117 H 87.864404 67.217603 v 10.87117 h 17.977714 c -4.361366,9.03731 -13.494221,14.79672 -23.528797,14.83786 -14.494622,0 -26.244916,-11.75029 -26.244909,-26.24491 -7e-6,-14.494627 11.750287,-26.244918 26.244909,-26.244912 z"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="0.9"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="hero-fill"
|
||||||
|
width="31.802109"
|
||||||
|
height="11.397169"
|
||||||
|
x="-96.2453"
|
||||||
|
y="67.460899"
|
||||||
|
transform="rotate(-90)"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headline */}
|
||||||
|
<div className="mt-10 max-w-xl">
|
||||||
|
<h1 className="overflow-hidden pb-2">
|
||||||
|
<span className="hero-line block text-4xl font-semibold leading-tight tracking-tight sm:text-6xl">
|
||||||
|
Gregor Lohaus
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<div className="mt-4 overflow-hidden">
|
||||||
|
<p className="hero-line text-lg text-muted-foreground sm:text-xl">
|
||||||
|
Full Stack Developer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lower-right arrow pointing at the chat FAB */}
|
||||||
|
<Link
|
||||||
|
href="/assistant"
|
||||||
|
aria-label="Chat with my AI assistant"
|
||||||
|
className="hero-arrow-wrap absolute bottom-10 right-8 z-40 flex flex-col items-start text-foreground transition-opacity hover:opacity-80 sm:right-16"
|
||||||
|
>
|
||||||
|
<span className="mb-1 max-w-[11rem] -translate-x-20 -translate-y-2 overflow-hidden sm:-translate-x-24">
|
||||||
|
<span className="hero-arrow-label block font-semibold leading-snug text-foreground drop-shadow-[0_2px_10px_rgba(0,0,0,0.7)] sm:text-lg">
|
||||||
|
Chat with my AI assistant
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className="hero-arrow-svg h-28 w-32 drop-shadow-[0_2px_10px_rgba(0,0,0,0.4)] sm:h-32 sm:w-36"
|
||||||
|
viewBox="0 0 776.09175 693.66538"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<g transform="matrix(2.7190747,0,0,3.1037754,-326.9763,-1172.9045)">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="m 130.838,381.118 c 1.125,28.749 5.277,54.82 12.695,78.018 7.205,22.53 18.847,40.222 36.812,53.747 52.018,39.16 153.369,16.572 153.369,16.572 l -4.632,-32.843 72.918,42.778 -58.597,58.775 -3.85,-27.303 c 0,0 -100.347,18.529 -163.905,-34.881 -37.659,-31.646 -53.293,-84.021 -51.593,-153.962 0.266,-0.247 4.728,-0.908 6.783,-0.901 z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
const date = typeof parsed.data.date === "string" ? parsed.data.date : post.date;
|
const date = typeof parsed.data.date === "string" ? parsed.data.date : post.date;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-2xl px-4 py-12">
|
<main className="mx-auto h-full max-w-2xl overflow-y-auto px-4 py-12">
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<h1 className="text-3xl font-bold">{title}</h1>
|
<h1 className="text-3xl font-bold">{title}</h1>
|
||||||
{date && (
|
{date && (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default async function BlogPage() {
|
|||||||
const posts = await servTrpc.blog.list();
|
const posts = await servTrpc.blog.list();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-2xl px-4 py-12">
|
<main className="mx-auto h-full max-w-2xl overflow-y-auto px-4 py-12">
|
||||||
<h1 className="mb-8 text-3xl font-bold">Blog</h1>
|
<h1 className="mb-8 text-3xl font-bold">Blog</h1>
|
||||||
{posts.length === 0 ? (
|
{posts.length === 0 ? (
|
||||||
<p className="text-muted-foreground">No posts yet.</p>
|
<p className="text-muted-foreground">No posts yet.</p>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cn } from "~/lib/utils"
|
|||||||
import { AnimatedCard, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
|
import { AnimatedCard, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
|
||||||
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 { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
|
|
||||||
export type CvCategoryData = ArrayElement<RouterOutputs['categoryv2']['listAllWithEntries']>
|
export type CvCategoryData = ArrayElement<RouterOutputs['categoryv2']['listAllWithEntries']>
|
||||||
@@ -18,22 +18,46 @@ type CvCategoryProps = {
|
|||||||
}
|
}
|
||||||
export default function CvCategory({ category, layout, position = 0, descriptions }: CvCategoryProps) {
|
export default function CvCategory({ category, layout, position = 0, descriptions }: CvCategoryProps) {
|
||||||
const entries = category.cvEntry
|
const entries = category.cvEntry
|
||||||
|
const isRowLayout = layout === "row"
|
||||||
|
const entryStart = position + 1
|
||||||
|
const entryStagger = 1.1
|
||||||
|
const entryItems = entries.map((entry, i) => {
|
||||||
|
const entryPosition = entryStart + i * entryStagger
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedDiv
|
||||||
|
className={cn(isRowLayout ? "min-w-[min(100%,18rem)] flex-1" : "w-full", "opacity-0 -translate-x-6")}
|
||||||
|
position={entryPosition}
|
||||||
|
debugId={`cv-entry-wrapper:${category.name}:${entry.title}:${entryPosition}`}
|
||||||
|
opacity={1}
|
||||||
|
x={0}
|
||||||
|
duration={0.4}
|
||||||
|
ease="power2.out"
|
||||||
|
key={entry.id}
|
||||||
|
>
|
||||||
|
<CvEntry position={entryPosition} entry={entry} description={descriptions[entry.id]} row={isRowLayout} className="w-full" />
|
||||||
|
</AnimatedDiv>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedCard position={position} className={cn(layout == "row" ? "w-full" : "", "h-screen")}>
|
<AnimatedCard position={position} className={cn(isRowLayout ? "h-fit min-w-[min(100%,18rem)] flex-1" : "lg:h-full lg:min-h-0")}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<AnimateTextIn once position={position + 0.2} animation="slide" debugId={`cv-category-title:${category.name}:${position + 0.2}`}>
|
<AnimateTextIn once position={position + 0.35} animation="slide" debugId={`cv-category-title:${category.name}:${position + 0.35}`}>
|
||||||
<CardTitle>{category.name}</CardTitle>
|
<CardTitle>{category.name}</CardTitle>
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{entries.length > 0 ?
|
{entries.length > 0 ?
|
||||||
<CardContent className={cn(layout == "row" ? "flex flex-row flex-wrap justify-center lg:justify-between" : "flex flex-col", "gap-4", "overflow-scroll")}>
|
<CardContent className={cn(isRowLayout ? "flex flex-row flex-wrap items-stretch justify-center lg:justify-between" : "flex flex-col flex-1 min-h-0", "gap-4")}>
|
||||||
<ScrollArea>
|
{isRowLayout ? (
|
||||||
{entries.map((entry, i) => (
|
entryItems
|
||||||
<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} description={descriptions[entry.id]} className={layout == "row" ? "w-full lg:w-fit" : undefined} />
|
<ScrollArea className="min-h-0 w-full flex-1">
|
||||||
</AnimatePopUp>
|
<div className="flex flex-col gap-4 pr-2">
|
||||||
))}
|
{entryItems}
|
||||||
</ScrollArea>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
:
|
:
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -9,30 +9,34 @@ import type { CvCategoryData } from "./CvCategory"
|
|||||||
|
|
||||||
export type CvEntryData = ArrayElement<CvCategoryData['cvEntry']>
|
export type CvEntryData = ArrayElement<CvCategoryData['cvEntry']>
|
||||||
|
|
||||||
export default function CvEntry({ entry, description, className, position = 0 }: {
|
export default function CvEntry({ entry, description, className, position = 0, row = false }: {
|
||||||
entry: CvEntryData,
|
entry: CvEntryData,
|
||||||
description?: ReactNode,
|
description?: ReactNode,
|
||||||
className?: string,
|
className?: string,
|
||||||
position?: number
|
position?: number,
|
||||||
|
row?: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
const from = format(new Date(entry.fromTime), 'MMMM yyyy')
|
||||||
|
const to = format(new Date(entry.toTime), 'MMMM yyyy')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className ? cn("w-fit", className) : "w-fit"}>
|
<Card className={cn("w-full ring-0", row && "h-full", className)}>
|
||||||
{entry.title ?
|
{entry.title ?
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<AnimateTextIn position={position} animation="slide" debugId={`cv-entry-title:${entry.title}:${position}`}>
|
<AnimateTextIn once position={position + 0.25} animation="slide" debugId={`cv-entry-title:${entry.title}:${position + 0.25}`}>
|
||||||
<CardTitle> {entry.title} </CardTitle>
|
<CardTitle> {entry.title} </CardTitle>
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
</CardHeader> :
|
</CardHeader> :
|
||||||
<></>
|
<></>
|
||||||
}
|
}
|
||||||
{entry.description ?
|
{entry.description ?
|
||||||
<CardContent className="text-sm lg:text-base">
|
<CardContent className={cn("text-sm lg:text-base", row && "flex flex-1 items-center justify-center")}>
|
||||||
{/* Fade the description in place instead of collapsing its height:
|
{/* Fade the description in place instead of collapsing its height:
|
||||||
the outer entry pop-up (CvCategory) measures height:auto when it
|
the outer entry pop-up (CvCategory) measures height:auto when it
|
||||||
plays, so the description must stay laid out at full height or the
|
plays, so the description must stay laid out at full height or the
|
||||||
entry reveals too short. */}
|
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}`}>
|
<AnimatedDiv once position={position + 0.75} className={cn("opacity-0", row && "w-full")} opacity={1} duration={0.5} debugId={`cv-entry-description:${entry.title}:${position + 0.75}`}>
|
||||||
<article className="prose prose-zinc dark:prose-invert max-w-none">
|
<article className={cn("prose prose-zinc dark:prose-invert max-w-none", row && "text-center")}>
|
||||||
{description ?? entry.description}
|
{description ?? entry.description}
|
||||||
</article>
|
</article>
|
||||||
</AnimatedDiv>
|
</AnimatedDiv>
|
||||||
@@ -40,9 +44,9 @@ export default function CvEntry({ entry, description, className, position = 0 }:
|
|||||||
<></>
|
<></>
|
||||||
}
|
}
|
||||||
{!entry.hideDates ?
|
{!entry.hideDates ?
|
||||||
<CardFooter className="text-sm">
|
<CardFooter className="border-t-0 text-sm">
|
||||||
<AnimateTextIn position={position + 0.4} debugId={`cv-entry-dates:${entry.title}:${position + 0.4}`}>
|
<AnimateTextIn once position={position + 1.15} debugId={`cv-entry-dates:${entry.title}:${position + 1.15}`}>
|
||||||
{`von ${format(new Date(entry.fromTime), 'M. yyyy')} bis zum ${format(new Date(entry.toTime), 'M. yyyy')}`}
|
{`${from} to ${to}`}
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
</CardFooter> :
|
</CardFooter> :
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Sidebar, SidebarContent, SidebarProvider } from "~/components/ui/sidebar";
|
import { Sidebar, SidebarContent, SidebarProvider, useSidebar } from "~/components/ui/sidebar";
|
||||||
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
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";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
export default function CvPage(props: {
|
export default function CvPage(props: {
|
||||||
cv: RouterOutputs['categoryv2']['listAllWithEntries'],
|
cv: RouterOutputs['categoryv2']['listAllWithEntries'],
|
||||||
descriptions: Record<string, ReactNode>,
|
descriptions: Record<string, ReactNode>,
|
||||||
@@ -17,43 +19,104 @@ export default function CvPage(props: {
|
|||||||
const headerCategories = byPosition("header")
|
const headerCategories = byPosition("header")
|
||||||
const col1Categories = byPosition("col1")
|
const col1Categories = byPosition("col1")
|
||||||
const col2Categories = byPosition("col2")
|
const col2Categories = byPosition("col2")
|
||||||
|
const hasSidebar = sidebarCategories.length > 0
|
||||||
|
const hasTwoMainColumns = col1Categories.length > 0 && col2Categories.length > 0
|
||||||
|
const mainColumnWidthClass = hasTwoMainColumns ? "lg:w-1/2 lg:h-full" : ""
|
||||||
|
const sequencePositions = <T extends { cvEntry: unknown[] }>(categories: T[], start = 0) => {
|
||||||
|
let cursor = start
|
||||||
|
const positions = categories.map((category) => {
|
||||||
|
const position = cursor
|
||||||
|
cursor += 1.8 + category.cvEntry.length * 1.2
|
||||||
|
return position
|
||||||
|
})
|
||||||
|
|
||||||
|
return { end: cursor, positions }
|
||||||
|
}
|
||||||
|
const headerSequence = sequencePositions(headerCategories)
|
||||||
|
const sidebarSequence = sequencePositions(sidebarCategories)
|
||||||
|
const contentStart = Math.max(headerSequence.end, sidebarSequence.end)
|
||||||
|
const col1Sequence = sequencePositions(col1Categories, contentStart)
|
||||||
|
const col2Sequence = sequencePositions(col2Categories, contentStart)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarProvider>
|
<SidebarProvider className="h-full min-h-0 overflow-hidden">
|
||||||
{sidebarCategories.length > 0 &&
|
{sidebarCategories.length > 0 &&
|
||||||
<>
|
<>
|
||||||
<SidebarTriggerDisappearsOnMobile />
|
<SidebarTriggerDisappearsOnMobile />
|
||||||
<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} descriptions={descriptions} key={cat.id} />
|
<CvCategory layout="col" position={sidebarSequence.positions[i] ?? 0} category={cat} descriptions={descriptions} key={cat.id} />
|
||||||
))}
|
))}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<div className="h-full w-full flex flex-wrap flex-row p-4 pt-8 ">
|
<MainContent
|
||||||
<div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]">
|
hasSidebar={hasSidebar}
|
||||||
<div id="header" className="flex w-full h-fit flex-row gap-4 flex-wrap">
|
mainColumnWidthClass={mainColumnWidthClass}
|
||||||
{headerCategories.map((cat, i) => (
|
headerCategories={headerCategories}
|
||||||
<CvCategory layout="row" position={i} category={cat} descriptions={descriptions} key={cat.id} />
|
headerSequence={headerSequence}
|
||||||
))}
|
col1Categories={col1Categories}
|
||||||
</div>
|
col1Sequence={col1Sequence}
|
||||||
<div id="colwrapper" className="flex flex-col lg:flex-row w-full h-3/4 gap-4">
|
col2Categories={col2Categories}
|
||||||
<div id="col1" className={`flex flex-col w-full ${col1Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}>
|
col2Sequence={col2Sequence}
|
||||||
{col1Categories.map((cat, i) => (
|
descriptions={descriptions}
|
||||||
<CvCategory layout="col" position={i} category={cat} descriptions={descriptions} key={cat.id} />
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div id="col2" className={`flex flex-col w-full ${col2Categories.length > 0 ? "lg:w-1/2" : ""} h-full gap-4`}>
|
|
||||||
{col2Categories.map((cat, i) => (
|
|
||||||
<CvCategory layout="col" position={i} category={cat} descriptions={descriptions} key={cat.id} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Sequence = { positions: number[] }
|
||||||
|
type Category = NonNullable<RouterOutputs['categoryv2']['listAllWithEntries']>[number]
|
||||||
|
|
||||||
|
function MainContent(props: {
|
||||||
|
hasSidebar: boolean,
|
||||||
|
mainColumnWidthClass: string,
|
||||||
|
headerCategories: Category[],
|
||||||
|
headerSequence: Sequence,
|
||||||
|
col1Categories: Category[],
|
||||||
|
col1Sequence: Sequence,
|
||||||
|
col2Categories: Category[],
|
||||||
|
col2Sequence: Sequence,
|
||||||
|
descriptions: Record<string, ReactNode>,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
hasSidebar, mainColumnWidthClass, headerCategories, headerSequence,
|
||||||
|
col1Categories, col1Sequence, col2Categories, col2Sequence, descriptions,
|
||||||
|
} = props
|
||||||
|
const { open } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea className="h-full min-h-0 w-full flex-1">
|
||||||
|
<div
|
||||||
|
id="mainwrap"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full flex-col gap-4 p-4 pt-8",
|
||||||
|
!hasSidebar && "lg:px-[15vw]",
|
||||||
|
hasSidebar && !open && "lg:px-[8vw]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div id="header" className="flex w-full flex-row flex-wrap items-stretch gap-4">
|
||||||
|
{headerCategories.map((cat, i) => (
|
||||||
|
<CvCategory layout="row" position={headerSequence.positions[i] ?? 0} category={cat} descriptions={descriptions} key={cat.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div id="colwrapper" className="flex w-full flex-col gap-4 lg:h-[80vh] lg:flex-row">
|
||||||
|
<div id="col1" className={cn("flex min-h-0 w-full flex-col gap-4", mainColumnWidthClass)}>
|
||||||
|
{col1Categories.map((cat, i) => (
|
||||||
|
<CvCategory layout="col" position={col1Sequence.positions[i] ?? 0} category={cat} descriptions={descriptions} key={cat.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div id="col2" className={cn("flex min-h-0 w-full flex-col gap-4", mainColumnWidthClass)}>
|
||||||
|
{col2Categories.map((cat, i) => (
|
||||||
|
<CvCategory layout="col" position={col2Sequence.positions[i] ?? 0} category={cat} descriptions={descriptions} key={cat.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import HomeHero from "./_components/Home/HomeHero";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="h-full w-full">
|
||||||
<div>
|
<HomeHero />
|
||||||
hello world
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,26 @@ function Figure({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bare markdown image () fills the prose width. Pass a numeric
|
||||||
|
// markdown title —  — to render it as a fixed-size, circular
|
||||||
|
// avatar instead (used by the CV header). Untitled images keep the old look.
|
||||||
|
function Img({ src, alt, title }: { src: string; alt?: string; title?: string }) {
|
||||||
|
const size = title && /^\d+$/.test(title) ? Number(title) : undefined;
|
||||||
|
if (size) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt ?? ""}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
className="mx-auto !my-0 shrink-0 rounded-full object-cover ring-2 ring-foreground/10"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <img src={src} alt={alt ?? ""} className="w-full rounded-md border object-cover" />;
|
||||||
|
}
|
||||||
|
|
||||||
function PullQuote({ children }: { children: ReactNode }) {
|
function PullQuote({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<blockquote className="border-primary my-8 border-l-4 pl-5 text-xl leading-8 font-medium">
|
<blockquote className="border-primary my-8 border-l-4 pl-5 text-xl leading-8 font-medium">
|
||||||
@@ -134,6 +154,7 @@ export const mdxComponents = {
|
|||||||
ButtonLink,
|
ButtonLink,
|
||||||
Callout,
|
Callout,
|
||||||
Figure,
|
Figure,
|
||||||
|
img: Img,
|
||||||
Lead,
|
Lead,
|
||||||
PullQuote,
|
PullQuote,
|
||||||
TagList,
|
TagList,
|
||||||
|
|||||||
Reference in New Issue
Block a user