import { createFileRoute, Link } from '@tanstack/react-router'
import { computeDuration, formatDate, CONTACT_EMAIL } from '#/data/portfolio'
import type { TExperience, TEducation, TSkill } from '#/types/strapi'
import {
fetchExperiences,
fetchEducations,
fetchProfile,
} from '#/data/loaders/portfolio'
export const Route = createFileRoute('/')({
loader: async () => {
const [experiences, educations, profile] = await Promise.all([
fetchExperiences(),
fetchEducations(),
fetchProfile(),
])
return { experiences, educations, profile }
},
errorComponent: StrapiErrorPage,
component: HomePage,
})
// ── Error page ────────────────────────────────────────────────────────────
function StrapiErrorPage() {
return (
Service Unavailable
We'll be right back
The portfolio data is temporarily unavailable. Please check back in a
few minutes.
)
}
// ── Shared UI primitives ──────────────────────────────────────────────────
type ButtonVariant = 'primary' | 'ghost'
const BUTTON_VARIANT_CLASSES: Record = {
primary:
'rounded-full border border-[rgba(50,143,151,0.35)] bg-[rgba(79,184,178,0.15)] px-5 py-2.5 text-sm font-semibold text-[var(--lagoon-deep)] no-underline transition hover:-translate-y-0.5 hover:bg-[rgba(79,184,178,0.26)]',
ghost:
'rounded-full border border-[var(--line)] bg-[var(--surface-strong)] px-5 py-2.5 text-sm font-semibold text-[var(--sea-ink)] no-underline transition hover:-translate-y-0.5 hover:border-[rgba(23,58,64,0.3)]',
}
function ButtonLink({
href,
variant,
children,
}: {
readonly href: string
readonly variant: ButtonVariant
readonly children: React.ReactNode
}) {
return (
{children}
)
}
function TechChip({ label }: { readonly label: string }) {
return (
{label}
)
}
// ── Section heading ───────────────────────────────────────────────────────
function SectionHeading({
kicker,
title,
id,
}: {
readonly kicker: string
readonly title: string
readonly id: string
}) {
return (
)
}
// ── Experience card ───────────────────────────────────────────────────────
const EMPLOYMENT_TYPE_LABEL: Record<'full-time' | 'part-time', string> = {
'full-time': 'Full-time',
'part-time': 'Part-time',
}
function ExperienceCard({
exp,
index,
}: {
readonly exp: TExperience
readonly index: number
}) {
const startIso = exp.startDate.slice(0, 7)
const endIso = exp.endDate?.slice(0, 7)
const duration = computeDuration(startIso, endIso)
const startLabel = formatDate(startIso)
const endLabel = endIso ? formatDate(endIso) : 'Present'
return (
{/* Timeline dot + connector line */}
{/* Card */}
{exp.company}
{exp.role}
{exp.type != null && (
{EMPLOYMENT_TYPE_LABEL[exp.type]}
)}
{startLabel} — {endLabel}
{duration}
{(exp.objectives ?? []).length > 0 && (
{(exp.objectives ?? []).map((obj) => (
{obj.description}
))}
)}
{(exp.technologies ?? []).length > 0 && (
{(exp.technologies ?? []).map((tech) => (
))}
)}
)
}
// ── Education card ────────────────────────────────────────────────────────
function EducationCard({ edu }: { readonly edu: TEducation }) {
const startYear = new Date(edu.startDate).getFullYear()
const endYear = edu.endDate ? new Date(edu.endDate).getFullYear() : null
return (
{edu.institution}
{edu.degree}
{edu.fieldOfStudy ? ` ${edu.fieldOfStudy}` : ''}
{startYear} – {endYear ?? 'Present'}
{edu.grade != null && (
<>
Grade: {edu.grade}
>
)}
)
}
// ── Skill group card ──────────────────────────────────────────────────────
function SkillGroupCard({
category,
skills,
}: {
readonly category: string
readonly skills: readonly TSkill[]
}) {
return (
{category}
{skills.map((skill) => (
))}
)
}
// ── Page ──────────────────────────────────────────────────────────────────
function HomePage() {
const { experiences, educations, profile } = Route.useLoaderData()
const skillsByGroup = profile.skills.reduce>(
(acc, skill) => {
const key = skill.group
acc[key] = [...(acc[key] ?? []), skill]
return acc
},
{},
)
return (
{/* ── Hero ──────────────────────────────────────────── */}
Software Engineer · Copenhagen, Denmark
Hi, I'm {profile.firstname}.
{profile.about ??
'Crafting robust full-stack solutions with a passion for clean architecture, great developer experience, and shipping products people love.'}
View Experience
Get in Touch
{/* ── Experience ────────────────────────────────────── */}
{experiences.map((exp, i) => (
))}
{/* ── Education ─────────────────────────────────────── */}
{educations.map((edu) => (
))}
{/* ── Skills ────────────────────────────────────────── */}
{Object.entries(skillsByGroup).map(([groupKey, skills]) => (
))}
{/* ── Contact CTA ───────────────────────────────────── */}
)
}