Refine homepage and restore hero

This commit is contained in:
MOH
2026-03-15 18:26:31 +01:00
parent 6caf723695
commit cf957cbbe2
14 changed files with 1565 additions and 155 deletions
+57
View File
@@ -0,0 +1,57 @@
import type { ReactNode } from "react";
import { AppCard } from "@/components/ui/app-card";
import { cn } from "@/lib/utils";
type BentoCardProps = {
eyebrow: string;
title: string;
description?: string;
icon?: ReactNode;
className?: string;
children?: ReactNode;
};
export function BentoCard({
eyebrow,
title,
description,
icon,
className,
children,
}: BentoCardProps) {
return (
<AppCard
layer="single"
interactive
className={cn(
"group h-full border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:bg-background/80 hover:shadow-lg",
className,
)}
>
<div className="flex h-full flex-col gap-6 p-6">
<div className="flex items-start justify-between gap-4">
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{eyebrow}
</p>
<div className="space-y-2">
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground transition-colors duration-300 group-hover:text-foreground/95">
{title}
</h3>
{description ? (
<p className="text-sm leading-7 text-muted-foreground">{description}</p>
) : null}
</div>
</div>
{icon ? (
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-nested border border-border/70 bg-background text-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:border-border-strong group-hover:shadow-sm">
{icon}
</div>
) : null}
</div>
{children}
</div>
</AppCard>
);
}
+125
View File
@@ -0,0 +1,125 @@
import {
Activity,
BriefcaseBusiness,
Layers3,
Mail,
Sparkles,
TimerReset,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { SectionHeading } from "./section-heading";
import { BentoCard } from "./bento-card";
import type { BentoSectionCopy } from "./types";
type BentoGridSectionProps = {
copy: BentoSectionCopy;
};
export function BentoGridSection({ copy }: BentoGridSectionProps) {
return (
<section className="space-y-8">
<SectionHeading
eyebrow={copy.eyebrow}
title={copy.title}
description={copy.description}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4 xl:gap-6">
<BentoCard
eyebrow={copy.about.eyebrow}
title={copy.about.title}
description={copy.about.description}
icon={<BriefcaseBusiness className="h-5 w-5" />}
className="xl:col-span-2 xl:row-span-2"
>
<ul className="grid gap-3 sm:grid-cols-2">
{copy.about.points.map((item) => (
<li
key={item}
className="rounded-nested border border-border/70 bg-background px-4 py-3 text-sm text-foreground/88"
>
{item}
</li>
))}
</ul>
</BentoCard>
<BentoCard
eyebrow={copy.coreStack.eyebrow}
title={copy.coreStack.title}
icon={<Layers3 className="h-5 w-5" />}
>
<div className="flex flex-wrap gap-2">
{copy.coreStack.items.map((item) => (
<Badge key={item} variant="outline" className="border-border/80 bg-background">
{item}
</Badge>
))}
</div>
</BentoCard>
<BentoCard
eyebrow={copy.availability.eyebrow}
title={copy.availability.title}
description={copy.availability.description}
icon={<Activity className="h-5 w-5" />}
>
<Badge variant="success" className="w-fit">
{copy.availability.status}
</Badge>
</BentoCard>
<BentoCard
eyebrow={copy.currentFocus.eyebrow}
title={copy.currentFocus.title}
icon={<Sparkles className="h-5 w-5" />}
>
<ul className="space-y-3 text-sm text-foreground/88">
{copy.currentFocus.items.map((item) => (
<li key={item} className="rounded-nested border border-border/70 bg-background px-4 py-3">
{item}
</li>
))}
</ul>
</BentoCard>
<BentoCard
eyebrow={copy.contact.eyebrow}
title={copy.contact.title}
description={copy.contact.description}
icon={<Mail className="h-5 w-5" />}
>
<div className="rounded-nested border border-border/70 bg-background px-4 py-4">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{copy.contact.emailLabel}
</p>
<p className="mt-2 text-base font-medium text-foreground">
{copy.contact.emailValue}
</p>
</div>
</BentoCard>
<BentoCard
eyebrow={copy.highlights.eyebrow}
title={copy.highlights.title}
icon={<TimerReset className="h-5 w-5" />}
>
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
{copy.highlights.items.map((item) => (
<div
key={item.label}
className="rounded-nested border border-border/70 bg-background px-4 py-4"
>
<p className="text-2xl font-semibold tracking-[-0.05em] text-foreground">
{item.value}
</p>
<p className="mt-1 text-sm text-muted-foreground">{item.label}</p>
</div>
))}
</div>
</BentoCard>
</div>
</section>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { Bot, Database, Globe, ServerCog } from "lucide-react";
import { AppCard } from "@/components/ui/app-card";
import { SectionHeading } from "./section-heading";
import type { CapabilitiesSectionCopy } from "./types";
type CapabilitiesSectionProps = {
copy: CapabilitiesSectionCopy;
};
const capabilityIcons = [Globe, Database, ServerCog, Bot];
export function CapabilitiesSection({ copy }: CapabilitiesSectionProps) {
return (
<section className="space-y-8">
<SectionHeading
eyebrow={copy.eyebrow}
title={copy.title}
description={copy.description}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4 xl:gap-6">
{copy.items.map((item, index) => {
const Icon = capabilityIcons[index] ?? Globe;
return (
<AppCard
key={item.title}
layer="single"
interactive
className="group border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:shadow-panel"
>
<div className="flex h-full flex-col gap-5 p-6">
<div className="flex h-11 w-11 items-center justify-center rounded-nested border border-border/70 bg-background text-foreground transition-transform duration-300 group-hover:-translate-y-0.5">
<Icon className="h-5 w-5" />
</div>
<div className="space-y-3">
<h3 className="text-xl font-semibold tracking-[-0.04em] text-foreground">
{item.title}
</h3>
<p className="text-sm leading-7 text-muted-foreground">{item.description}</p>
</div>
</div>
</AppCard>
);
})}
</div>
</section>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { ArrowUpRight, Mail } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { AppCard } from "@/components/ui/app-card";
import type { ContactCtaSectionCopy } from "./types";
type ContactCtaSectionProps = {
copy: ContactCtaSectionCopy;
contactHref: string;
};
export function ContactCtaSection({
copy,
contactHref,
}: ContactCtaSectionProps) {
return (
<section>
<AppCard className="overflow-hidden">
<div className="grid gap-6 rounded-nested border border-border/70 bg-background p-6 sm:p-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end lg:p-10">
<div className="space-y-4">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{copy.eyebrow}
</p>
<h2 className="max-w-3xl text-balance text-3xl font-semibold tracking-[-0.05em] text-foreground sm:text-4xl lg:text-5xl">
{copy.title}
</h2>
<p className="max-w-2xl text-sm leading-7 text-muted-foreground sm:text-base">
{copy.description}
</p>
<div className="flex flex-wrap gap-6 text-sm">
<div className="space-y-1">
<p className="font-semibold text-foreground">{copy.emailLabel}</p>
<a href={`mailto:${copy.emailValue}`} className="text-muted-foreground hover:text-foreground">
{copy.emailValue}
</a>
</div>
<div className="space-y-1">
<p className="font-semibold text-foreground">{copy.availabilityLabel}</p>
<p className="text-muted-foreground">{copy.availabilityValue}</p>
</div>
</div>
</div>
<div className="flex flex-wrap gap-3 lg:justify-end">
<Button asChild size="lg">
<Link href={contactHref}>
{copy.contactCta}
<Mail className="h-4 w-4" />
</Link>
</Button>
<Button asChild size="lg" variant="outline">
<a href={copy.githubHref} target="_blank" rel="noreferrer">
{copy.githubCta}
<ArrowUpRight className="h-4 w-4" />
</a>
</Button>
</div>
</div>
</AppCard>
</section>
);
}
+113
View File
@@ -0,0 +1,113 @@
import { ArrowRight, MapPin, Sparkles } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { AppCard } from "@/components/ui/app-card";
import { cn } from "@/lib/utils";
import type { HeroCopy } from "./types";
type HeroSectionProps = {
copy: HeroCopy;
contactHref: string;
portfolioHref: string;
};
export function HeroSection({
copy,
contactHref,
portfolioHref,
}: HeroSectionProps) {
return (
<section className="relative pt-8 sm:pt-12 lg:pt-16">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)] lg:items-end">
<div className="space-y-8">
<Badge
variant="outline"
className="w-fit border-border/80 bg-background/80 text-foreground shadow-xs"
>
<span className="mr-2 h-2 w-2 rounded-full bg-status-success" aria-hidden="true" />
{copy.availability}
</Badge>
<div className="space-y-5">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-muted-foreground">
{copy.name}
</p>
<h1 className="max-w-4xl text-balance text-5xl font-semibold tracking-[-0.07em] text-foreground sm:text-6xl lg:text-7xl">
{copy.headline}
</h1>
<p className="max-w-2xl text-base leading-8 text-muted-foreground sm:text-lg">
{copy.description}
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button asChild size="lg" className="shadow-sm">
<Link href={contactHref}>
{copy.primaryCta}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="border-border/80 bg-background/70 hover:border-border-strong hover:bg-background"
>
<Link href={portfolioHref}>{copy.secondaryCta}</Link>
</Button>
</div>
<div className="flex flex-wrap gap-3">
{copy.highlights.map((item) => (
<Badge
key={item}
variant="secondary"
className="bg-secondary/70 text-foreground shadow-none"
>
{item}
</Badge>
))}
</div>
</div>
<AppCard className="overflow-hidden">
<div className="grid gap-3">
<div className="grid gap-3 sm:grid-cols-2">
<MetricCard label={copy.roleLabel} value={copy.roleValue} />
<MetricCard label={copy.locationLabel} value={copy.locationValue} icon={MapPin} />
</div>
<MetricCard label={copy.focusLabel} value={copy.focusValue} icon={Sparkles} strong />
</div>
</AppCard>
</div>
</section>
);
}
type MetricCardProps = {
label: string;
value: string;
strong?: boolean;
icon?: typeof MapPin;
};
function MetricCard({ label, value, strong = false, icon: Icon }: MetricCardProps) {
return (
<div
className={cn(
"rounded-nested border border-border/70 bg-background p-5 transition-all duration-300",
strong && "bg-surface-2",
)}
>
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{Icon ? <Icon className="h-3.5 w-3.5" /> : null}
<span>{label}</span>
</div>
<p className="mt-3 text-xl font-semibold tracking-[-0.04em] text-foreground">
{value}
</p>
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { StackedMarqueeSection, type MarqueeRow } from "@/components/layout/stacked-marquee-section";
import { AppCard } from "@/components/ui/app-card";
import { SectionHeading } from "./section-heading";
import type { MarqueeSectionCopy } from "./types";
type MarqueeSectionProps = {
copy: MarqueeSectionCopy;
rows: MarqueeRow[];
};
export function MarqueeSection({ copy, rows }: MarqueeSectionProps) {
return (
<section className="space-y-8 overflow-hidden">
<SectionHeading
eyebrow={copy.eyebrow}
title={copy.title}
description={copy.description}
/>
<AppCard className="overflow-hidden">
<div className="rounded-nested border border-border/70 bg-background py-6">
<StackedMarqueeSection rows={rows} className="py-0" />
</div>
</AppCard>
</section>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { AppCard } from "@/components/ui/app-card";
import { SectionHeading } from "./section-heading";
import type { ProcessSectionCopy } from "./types";
type ProcessSectionProps = {
copy: ProcessSectionCopy;
};
export function ProcessSection({ copy }: ProcessSectionProps) {
return (
<section className="space-y-8">
<SectionHeading
eyebrow={copy.eyebrow}
title={copy.title}
description={copy.description}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4 xl:gap-6">
{copy.steps.map((step, index) => (
<AppCard
key={step.title}
layer="single"
interactive
className="group border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:shadow-panel"
>
<div className="flex h-full flex-col gap-5 p-6">
<p className="text-sm font-semibold tracking-[0.18em] text-muted-foreground">
{String(index + 1).padStart(2, "0")}
</p>
<div className="space-y-3">
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground">
{step.title}
</h3>
<p className="text-sm leading-7 text-muted-foreground">{step.description}</p>
</div>
</div>
</AppCard>
))}
</div>
</section>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { ArrowUpRight } from "lucide-react";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { AppCard } from "@/components/ui/app-card";
import { SectionHeading } from "./section-heading";
import type { ProjectCardCopy, ProjectsSectionCopy } from "./types";
type ProjectsSectionProps = {
copy: ProjectsSectionCopy;
items: ProjectCardCopy[];
};
export function ProjectsSection({ copy, items }: ProjectsSectionProps) {
return (
<section className="space-y-10">
<SectionHeading
eyebrow={copy.eyebrow}
title={copy.title}
description={copy.description}
/>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:gap-6">
{items.map((item, index) => (
<AppCard
key={`${item.title}-${index}`}
layer="single"
interactive
className={[
"group h-full border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:bg-background/80 hover:shadow-lg",
index === 0 ? "lg:col-span-2" : "",
].join(" ")}
>
<div className="flex h-full flex-col gap-6 p-6">
<div className="flex flex-wrap items-center gap-2">
{item.meta.map((metaItem) => (
<Badge
key={metaItem}
variant="outline"
className="border-border/80 bg-background text-muted-foreground transition-colors duration-300 group-hover:border-border-strong group-hover:text-foreground/80"
>
{metaItem}
</Badge>
))}
</div>
<div className="space-y-3">
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground transition-colors duration-300 group-hover:text-foreground/95">
{item.title}
</h3>
<p className="text-sm leading-7 text-muted-foreground">{item.summary}</p>
</div>
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{copy.stackLabel}
</p>
<div className="flex flex-wrap gap-2">
{item.stack.map((stackItem) => (
<Badge
key={stackItem}
variant="secondary"
className="bg-secondary/70 text-foreground transition-colors duration-300 group-hover:bg-secondary"
>
{stackItem}
</Badge>
))}
</div>
</div>
<div className="mt-auto">
<Link
href={item.href}
className="inline-flex items-center gap-2 text-sm font-medium text-foreground transition-all duration-200 group-hover:translate-x-0.5"
>
{item.cta}
<ArrowUpRight className="h-4 w-4 transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
</Link>
</div>
</div>
</AppCard>
))}
</div>
</section>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { cn } from "@/lib/utils";
type SectionHeadingProps = {
eyebrow: string;
title: string;
description: string;
align?: "left" | "center";
className?: string;
};
export function SectionHeading({
eyebrow,
title,
description,
align = "left",
className,
}: SectionHeadingProps) {
return (
<div
className={cn(
"space-y-4",
align === "center" && "mx-auto max-w-3xl text-center",
className,
)}
>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{eyebrow}
</p>
<h2 className="max-w-4xl text-balance text-3xl font-semibold tracking-[-0.05em] text-foreground sm:text-4xl lg:text-5xl">
{title}
</h2>
<p className="max-w-3xl text-sm leading-7 text-muted-foreground sm:text-[1.02rem]">
{description}
</p>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
export type HeroCopy = {
availability: string;
name: string;
headline: string;
description: string;
primaryCta: string;
secondaryCta: string;
locationLabel: string;
locationValue: string;
roleLabel: string;
roleValue: string;
focusLabel: string;
focusValue: string;
highlights: string[];
};
export type BentoSectionCopy = {
eyebrow: string;
title: string;
description: string;
about: {
eyebrow: string;
title: string;
description: string;
points: string[];
};
coreStack: {
eyebrow: string;
title: string;
items: string[];
};
availability: {
eyebrow: string;
title: string;
description: string;
status: string;
};
currentFocus: {
eyebrow: string;
title: string;
items: string[];
};
contact: {
eyebrow: string;
title: string;
description: string;
emailLabel: string;
emailValue: string;
};
highlights: {
eyebrow: string;
title: string;
items: Array<{
value: string;
label: string;
}>;
};
};
export type MarqueeSectionCopy = {
eyebrow: string;
title: string;
description: string;
};
export type ProjectCardCopy = {
title: string;
summary: string;
stack: string[];
href: string;
cta: string;
meta: string[];
featured?: boolean;
};
export type ProjectsSectionCopy = {
eyebrow: string;
title: string;
description: string;
stackLabel: string;
fallbackItems: Array<{
title: string;
summary: string;
stack: string[];
href: string;
}>;
};
export type CapabilitiesSectionCopy = {
eyebrow: string;
title: string;
description: string;
items: Array<{
title: string;
description: string;
}>;
};
export type ProcessSectionCopy = {
eyebrow: string;
title: string;
description: string;
steps: Array<{
title: string;
description: string;
}>;
};
export type ContactCtaSectionCopy = {
eyebrow: string;
title: string;
description: string;
contactCta: string;
githubCta: string;
emailLabel: string;
emailValue: string;
availabilityLabel: string;
availabilityValue: string;
githubHref: string;
};