Add i18n support with NL/EN language switching (ST-001)
- Add English translations to cv-data.ts with getCvData(lang) helper - Create app/[lang]/ routing with static generation for nl and en - Add language switcher in nav (NL / EN toggle) - Add middleware for Accept-Language auto-redirect on root path - Root layout reads x-lang header to set <html lang> correctly - All components accept lang prop and render translated content Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
61e603b5d7
commit
d352a7d496
13 changed files with 540 additions and 177 deletions
|
|
@ -1,27 +1,11 @@
|
|||
import Image from "next/image";
|
||||
import { FadeIn } from "./fade-in";
|
||||
import { getCvData, type Lang } from "@/lib/cv-data";
|
||||
|
||||
const APPS = [
|
||||
{
|
||||
title: "Inspannings Monitor",
|
||||
subtitle: "Wellness-first dagflow",
|
||||
description:
|
||||
"Een lichte app die helpt doseren en inzicht geeft zonder lange formulieren of overprikkeling.",
|
||||
screenshot: "/images/app-inspannings-monitor.png",
|
||||
screenshotMobile: "/images/app-inspannings-monitor-mobile.png",
|
||||
href: "https://inspannings-monitor.jp-visser.nl/dashboard",
|
||||
},
|
||||
{
|
||||
title: "Scrum4Me",
|
||||
subtitle: "DevPlanner voor kleine teams",
|
||||
description:
|
||||
"Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hierarchisch (product -> PBI -> story -> taak), biedt gesplitste planningsschermen met drag-and-drop, en integreert met Claude Code via een REST API.",
|
||||
screenshot: "/images/app-scrum4me.svg",
|
||||
href: "https://scrum4me.jp-visser.nl",
|
||||
},
|
||||
];
|
||||
export function AppsSection({ lang }: { lang: Lang }) {
|
||||
const data = getCvData(lang);
|
||||
const { label, heading, subtext } = data.ui.apps;
|
||||
|
||||
export function AppsSection() {
|
||||
return (
|
||||
<section
|
||||
id="apps"
|
||||
|
|
@ -33,7 +17,7 @@ export function AppsSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Portfolio
|
||||
{label}
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-4"
|
||||
|
|
@ -43,18 +27,18 @@ export function AppsSection() {
|
|||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Apps & Projecten
|
||||
{heading}
|
||||
</h2>
|
||||
<p
|
||||
className="text-[15px] leading-[1.7] mb-12 max-w-[500px]"
|
||||
style={{ color: "rgba(255,255,255,0.45)" }}
|
||||
>
|
||||
Hier komen links naar mijn apps die ik op Vercel host.
|
||||
{subtext}
|
||||
</p>
|
||||
</FadeIn>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{APPS.map((app, i) => (
|
||||
{data.apps.map((app, i) => (
|
||||
<FadeIn key={i} delay={i * 0.1}>
|
||||
<a
|
||||
href={app.href}
|
||||
|
|
@ -91,7 +75,7 @@ export function AppsSection() {
|
|||
>
|
||||
<Image
|
||||
src={app.screenshotMobile}
|
||||
alt={`${app.title} mobiel`}
|
||||
alt={`${app.title} mobile`}
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
sizes="72px"
|
||||
|
|
@ -124,7 +108,6 @@ export function AppsSection() {
|
|||
</a>
|
||||
</FadeIn>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
import { getCvData, type Lang } from "@/lib/cv-data";
|
||||
|
||||
const CONTACT_ITEMS = [
|
||||
{
|
||||
label: "E-mail",
|
||||
value: CV_DATA.contact.email,
|
||||
href: `mailto:${CV_DATA.contact.email}`,
|
||||
},
|
||||
{
|
||||
label: "Locatie",
|
||||
value: CV_DATA.contact.location,
|
||||
href: null,
|
||||
},
|
||||
{
|
||||
label: "Website",
|
||||
value: "jp-visser.nl",
|
||||
href: "https://jp-visser.nl",
|
||||
},
|
||||
];
|
||||
export function ContactSection({ lang }: { lang: Lang }) {
|
||||
const data = getCvData(lang);
|
||||
const { label, heading, emailLabel, locationLabel, websiteLabel } = data.ui.contact;
|
||||
|
||||
const CONTACT_ITEMS = [
|
||||
{ label: emailLabel, value: data.contact.email, href: `mailto:${data.contact.email}` },
|
||||
{ label: locationLabel, value: data.contact.location, href: null },
|
||||
{ label: websiteLabel, value: "jp-visser.nl", href: "https://jp-visser.nl" },
|
||||
];
|
||||
|
||||
export function ContactSection() {
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
|
|
@ -31,7 +22,7 @@ export function ContactSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Neem contact op
|
||||
{label}
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
|
|
@ -41,7 +32,7 @@ export function ContactSection() {
|
|||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Contact
|
||||
{heading}
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA, type Experience } from "@/lib/cv-data";
|
||||
import { getCvData, type Lang, type Experience } from "@/lib/cv-data";
|
||||
|
||||
function ExperienceCard({
|
||||
job,
|
||||
index,
|
||||
showMore,
|
||||
showLess,
|
||||
}: {
|
||||
job: Experience;
|
||||
index: number;
|
||||
showMore: string;
|
||||
showLess: string;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
|
@ -98,7 +102,7 @@ function ExperienceCard({
|
|||
className="text-[12px] mt-3"
|
||||
style={{ color: "rgba(255,255,255,0.25)" }}
|
||||
>
|
||||
{expanded ? "▲ Minder tonen" : "▼ Meer details"}
|
||||
{expanded ? showLess : showMore}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -106,7 +110,10 @@ function ExperienceCard({
|
|||
);
|
||||
}
|
||||
|
||||
export function ExperienceSection() {
|
||||
export function ExperienceSection({ lang }: { lang: Lang }) {
|
||||
const data = getCvData(lang);
|
||||
const { label, heading, educationLabel, showMore, showLess } = data.ui.experience;
|
||||
|
||||
return (
|
||||
<section
|
||||
id="ervaring"
|
||||
|
|
@ -118,7 +125,7 @@ export function ExperienceSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Loopbaan
|
||||
{label}
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
|
|
@ -128,12 +135,12 @@ export function ExperienceSection() {
|
|||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Werkervaring
|
||||
{heading}
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
{CV_DATA.experience.map((job, i) => (
|
||||
<ExperienceCard key={i} job={job} index={i} />
|
||||
{data.experience.map((job, i) => (
|
||||
<ExperienceCard key={i} job={job} index={i} showMore={showMore} showLess={showLess} />
|
||||
))}
|
||||
|
||||
<FadeIn delay={0.2}>
|
||||
|
|
@ -148,32 +155,32 @@ export function ExperienceSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Opleiding
|
||||
{educationLabel}
|
||||
</p>
|
||||
<h3
|
||||
className="font-serif text-2xl font-normal mb-1"
|
||||
style={{ color: "#e8e4df" }}
|
||||
>
|
||||
{CV_DATA.education.university}
|
||||
{data.education.university}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[15px] mb-1"
|
||||
style={{ color: "rgba(255,255,255,0.5)" }}
|
||||
>
|
||||
{CV_DATA.education.degree} —{" "}
|
||||
{CV_DATA.education.specialization}
|
||||
{data.education.degree} —{" "}
|
||||
{data.education.specialization}
|
||||
</p>
|
||||
<p
|
||||
className="text-[13px] mb-4"
|
||||
style={{ color: "rgba(255,255,255,0.35)" }}
|
||||
>
|
||||
{CV_DATA.education.period}
|
||||
{data.education.period}
|
||||
</p>
|
||||
<p
|
||||
className="text-[14px]"
|
||||
style={{ color: "rgba(255,255,255,0.4)" }}
|
||||
>
|
||||
{CV_DATA.education.secondary}
|
||||
{data.education.secondary}
|
||||
</p>
|
||||
</div>
|
||||
</FadeIn>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
import { getCvData, type Lang } from "@/lib/cv-data";
|
||||
|
||||
export function Hero() {
|
||||
export function Hero({ lang }: { lang: Lang }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const data = getCvData(lang);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setLoaded(true), 100);
|
||||
|
|
@ -116,7 +117,7 @@ export function Hero() {
|
|||
className="text-[16px] leading-[1.7] max-w-[480px]"
|
||||
style={{ color: "rgba(255,255,255,0.55)" }}
|
||||
>
|
||||
{CV_DATA.intro}
|
||||
{data.intro}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4 mt-8 flex-wrap">
|
||||
|
|
@ -139,7 +140,7 @@ export function Hero() {
|
|||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
Bekijk CV
|
||||
{data.ui.hero.viewCV}
|
||||
</a>
|
||||
<a
|
||||
href="#apps"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
import { getCvData, type Lang } from "@/lib/cv-data";
|
||||
|
||||
export function MotivationSection({ lang }: { lang: Lang }) {
|
||||
const data = getCvData(lang);
|
||||
|
||||
export function MotivationSection() {
|
||||
return (
|
||||
<section
|
||||
id="motivatie"
|
||||
|
|
@ -17,7 +19,7 @@ export function MotivationSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Motivatie
|
||||
{data.ui.motivation.label}
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-7"
|
||||
|
|
@ -27,11 +29,11 @@ export function MotivationSection() {
|
|||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Waar ik naar zoek
|
||||
{data.ui.motivation.heading}
|
||||
</h2>
|
||||
|
||||
<div className="max-w-[760px]">
|
||||
{CV_DATA.motivation.map((paragraph) => (
|
||||
{data.motivation.map((paragraph) => (
|
||||
<p
|
||||
key={paragraph}
|
||||
className="text-[15px] leading-[1.8] mb-5 last:mb-0"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { getCvData, type Lang } from "@/lib/cv-data";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: "Over", id: "over" },
|
||||
{ label: "Ervaring", id: "ervaring" },
|
||||
{ label: "Skills", id: "skills" },
|
||||
{ label: "Apps", id: "apps" },
|
||||
{ label: "Contact", id: "contact" },
|
||||
];
|
||||
|
||||
export function Nav() {
|
||||
export function Nav({ lang }: { lang: Lang }) {
|
||||
const [active, setActive] = useState("over");
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const data = getCvData(lang);
|
||||
const navItems = data.ui.nav;
|
||||
const otherLang: Lang = lang === "nl" ? "en" : "nl";
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 20);
|
||||
|
||||
const sections = [...NAV_ITEMS].reverse();
|
||||
const sections = [...navItems].reverse();
|
||||
for (const { id } of sections) {
|
||||
const el = document.getElementById(id);
|
||||
if (el && el.getBoundingClientRect().top < 200) {
|
||||
|
|
@ -30,7 +27,7 @@ export function Nav() {
|
|||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
}, [navItems]);
|
||||
|
||||
const handleNav = (id: string) => {
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
|
||||
|
|
@ -56,8 +53,8 @@ export function Nav() {
|
|||
JP<span style={{ color: "#c2339b" }}>.</span>
|
||||
</span>
|
||||
|
||||
<div className="hidden sm:flex gap-8">
|
||||
{NAV_ITEMS.map(({ label, id }) => (
|
||||
<div className="hidden sm:flex items-center gap-8">
|
||||
{navItems.map(({ label, id }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => handleNav(id)}
|
||||
|
|
@ -74,6 +71,22 @@ export function Nav() {
|
|||
{label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Language switcher */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 text-[12px] font-semibold uppercase tracking-[1px] ml-2"
|
||||
style={{ borderLeft: "1px solid rgba(255,255,255,0.1)", paddingLeft: "1.5rem" }}
|
||||
>
|
||||
<span style={{ color: "#c2339b" }}>{lang.toUpperCase()}</span>
|
||||
<span style={{ color: "rgba(255,255,255,0.2)" }}>/</span>
|
||||
<Link
|
||||
href={`/${otherLang}`}
|
||||
style={{ color: "rgba(255,255,255,0.4)" }}
|
||||
className="no-underline hover:text-white transition-colors duration-200"
|
||||
>
|
||||
{otherLang.toUpperCase()}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
|
|
@ -102,7 +115,7 @@ export function Nav() {
|
|||
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
{NAV_ITEMS.map(({ label, id }) => (
|
||||
{navItems.map(({ label, id }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => {
|
||||
|
|
@ -119,6 +132,17 @@ export function Nav() {
|
|||
{label}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-2 px-8 pt-3 mt-2" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
<span className="text-[12px] font-semibold uppercase" style={{ color: "#c2339b" }}>{lang.toUpperCase()}</span>
|
||||
<span className="text-[12px]" style={{ color: "rgba(255,255,255,0.2)" }}>/</span>
|
||||
<Link
|
||||
href={`/${otherLang}`}
|
||||
className="text-[12px] font-semibold uppercase no-underline"
|
||||
style={{ color: "rgba(255,255,255,0.4)" }}
|
||||
>
|
||||
{otherLang.toUpperCase()}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Image from "next/image";
|
||||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
import { getCvData, type Lang, type SpokenLanguage } from "@/lib/cv-data";
|
||||
|
||||
const DEVICONS = "https://cdn.jsdelivr.net/gh/devicons/devicon/icons";
|
||||
const SIMPLE = "https://cdn.simpleicons.org";
|
||||
|
|
@ -48,22 +48,9 @@ const BADGE_ICONS: Record<string, string> = {
|
|||
Vercel: "▲",
|
||||
};
|
||||
|
||||
const LANGUAGE_ICONS: Record<string, string> = {
|
||||
Nederlands: `${FLAGS}/nl.svg`,
|
||||
Engels: `${FLAGS}/gb.svg`,
|
||||
Duits: `${FLAGS}/de.svg`,
|
||||
};
|
||||
|
||||
const LANGUAGE_PROGRESS: Record<string, number> = {
|
||||
Nederlands: 100,
|
||||
Engels: 85,
|
||||
Duits: 55,
|
||||
};
|
||||
|
||||
function SkillPill({ label }: { label: string }) {
|
||||
const icon = SKILL_ICONS[label] ?? LANGUAGE_ICONS[label];
|
||||
const icon = SKILL_ICONS[label];
|
||||
const badge = BADGE_ICONS[label];
|
||||
const isLanguage = label in LANGUAGE_ICONS;
|
||||
|
||||
return (
|
||||
<span
|
||||
|
|
@ -78,14 +65,10 @@ function SkillPill({ label }: { label: string }) {
|
|||
<Image
|
||||
src={icon}
|
||||
alt={label}
|
||||
width={isLanguage ? 16 : 16}
|
||||
height={isLanguage ? 12 : 16}
|
||||
className={isLanguage ? "flex-shrink-0 rounded-[2px]" : "flex-shrink-0"}
|
||||
style={{
|
||||
height: isLanguage ? 12 : 16,
|
||||
objectFit: "contain",
|
||||
width: 16,
|
||||
}}
|
||||
width={16}
|
||||
height={16}
|
||||
className="flex-shrink-0"
|
||||
style={{ height: 16, objectFit: "contain", width: 16 }}
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
|
|
@ -105,25 +88,22 @@ function SkillPill({ label }: { label: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function LanguageProgress({ label }: { label: string }) {
|
||||
const icon = LANGUAGE_ICONS[label];
|
||||
const progress = LANGUAGE_PROGRESS[label] ?? 0;
|
||||
function LanguageProgress({ spoken }: { spoken: SpokenLanguage }) {
|
||||
const flagSrc = `${FLAGS}/${spoken.flag}.svg`;
|
||||
|
||||
return (
|
||||
<div className="mb-4 last:mb-0">
|
||||
<div className="mb-2 flex items-center gap-2 text-[13px] font-medium">
|
||||
{icon && (
|
||||
<Image
|
||||
src={icon}
|
||||
alt={label}
|
||||
width={16}
|
||||
height={12}
|
||||
className="flex-shrink-0 rounded-[2px]"
|
||||
style={{ height: 12, objectFit: "contain", width: 16 }}
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
<span style={{ color: "rgba(255,255,255,0.65)" }}>{label}</span>
|
||||
<Image
|
||||
src={flagSrc}
|
||||
alt={spoken.label}
|
||||
width={16}
|
||||
height={12}
|
||||
className="flex-shrink-0 rounded-[2px]"
|
||||
style={{ height: 12, objectFit: "contain", width: 16 }}
|
||||
unoptimized
|
||||
/>
|
||||
<span style={{ color: "rgba(255,255,255,0.65)" }}>{spoken.label}</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-1.5 overflow-hidden rounded-full"
|
||||
|
|
@ -132,7 +112,7 @@ function LanguageProgress({ label }: { label: string }) {
|
|||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
width: `${spoken.progress}%`,
|
||||
background: "linear-gradient(90deg, #c2339b, #e8e4df)",
|
||||
}}
|
||||
/>
|
||||
|
|
@ -141,15 +121,18 @@ function LanguageProgress({ label }: { label: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
const SKILL_GROUPS = [
|
||||
{ title: "Programmeertalen", items: CV_DATA.skills.languages },
|
||||
{ title: "Frameworks", items: CV_DATA.skills.frameworks },
|
||||
{ title: "Databases", items: CV_DATA.skills.databases },
|
||||
{ title: "AI / AI-tools", items: CV_DATA.skills.aiTools },
|
||||
{ title: "Talen", items: CV_DATA.skills.spoken },
|
||||
];
|
||||
export function SkillsSection({ lang }: { lang: Lang }) {
|
||||
const data = getCvData(lang);
|
||||
const { label, groupTitles, interestsHeading } = data.ui.skills;
|
||||
|
||||
const SKILL_GROUPS = [
|
||||
{ id: "languages", title: groupTitles.languages, items: data.skills.languages },
|
||||
{ id: "frameworks", title: groupTitles.frameworks, items: data.skills.frameworks },
|
||||
{ id: "databases", title: groupTitles.databases, items: data.skills.databases },
|
||||
{ id: "aiTools", title: groupTitles.aiTools, items: data.skills.aiTools },
|
||||
{ id: "spoken", title: groupTitles.spoken, items: [] },
|
||||
];
|
||||
|
||||
export function SkillsSection() {
|
||||
return (
|
||||
<section
|
||||
id="skills"
|
||||
|
|
@ -161,7 +144,7 @@ export function SkillsSection() {
|
|||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Technologie
|
||||
{label}
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
|
|
@ -177,7 +160,7 @@ export function SkillsSection() {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{SKILL_GROUPS.map((group, i) => (
|
||||
<FadeIn key={group.title} delay={i * 0.1}>
|
||||
<FadeIn key={group.id} delay={i * 0.1}>
|
||||
<div
|
||||
className="rounded-2xl p-7"
|
||||
style={{
|
||||
|
|
@ -195,8 +178,10 @@ export function SkillsSection() {
|
|||
{group.title}
|
||||
</h3>
|
||||
<div>
|
||||
{group.title === "Talen"
|
||||
? group.items.map((s) => <LanguageProgress key={s} label={s} />)
|
||||
{group.id === "spoken"
|
||||
? data.skills.spoken.map((s) => (
|
||||
<LanguageProgress key={s.label} spoken={s} />
|
||||
))
|
||||
: group.items.map((s) => <SkillPill key={s} label={s} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,10 +198,10 @@ export function SkillsSection() {
|
|||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
Interesses
|
||||
{interestsHeading}
|
||||
</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CV_DATA.interests.map((interest) => (
|
||||
{data.interests.map((interest) => (
|
||||
<span
|
||||
key={interest}
|
||||
className="px-5 py-2 rounded-full text-[14px]"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue