Initial commit
This commit is contained in:
commit
dc66b66d94
22 changed files with 7556 additions and 0 deletions
72
components/apps.tsx
Normal file
72
components/apps.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { FadeIn } from "./fade-in";
|
||||
|
||||
const PLACEHOLDER_APPS = [
|
||||
{ icon: "⚡", label: "App 1 — binnenkort" },
|
||||
{ icon: "🔧", label: "App 2 — binnenkort" },
|
||||
{ icon: "📱", label: "App 3 — binnenkort" },
|
||||
];
|
||||
|
||||
export function AppsSection() {
|
||||
return (
|
||||
<section
|
||||
id="apps"
|
||||
className="mx-auto max-w-[900px]"
|
||||
style={{ padding: "100px clamp(20px, 6vw, 80px)" }}
|
||||
>
|
||||
<FadeIn>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Portfolio
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-4"
|
||||
style={{
|
||||
fontSize: "clamp(32px, 4vw, 48px)",
|
||||
color: "#e8e4df",
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Apps & Projecten
|
||||
</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.
|
||||
</p>
|
||||
</FadeIn>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
||||
{PLACEHOLDER_APPS.map((app, i) => (
|
||||
<FadeIn key={i} delay={i * 0.1}>
|
||||
<div
|
||||
className="rounded-2xl p-8 flex flex-col items-center justify-center min-h-[200px] text-center"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
border: "1px dashed rgba(255,255,255,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center mb-4 text-xl"
|
||||
style={{
|
||||
background: "rgba(194,51,155,0.1)",
|
||||
color: "#c2339b",
|
||||
}}
|
||||
>
|
||||
{app.icon}
|
||||
</div>
|
||||
<p
|
||||
className="text-[14px]"
|
||||
style={{ color: "rgba(255,255,255,0.3)" }}
|
||||
>
|
||||
{app.label}
|
||||
</p>
|
||||
</div>
|
||||
</FadeIn>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
86
components/contact.tsx
Normal file
86
components/contact.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA } 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() {
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
className="mx-auto max-w-[900px]"
|
||||
style={{ padding: "100px clamp(20px, 6vw, 80px) 60px" }}
|
||||
>
|
||||
<FadeIn>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Neem contact op
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
style={{
|
||||
fontSize: "clamp(32px, 4vw, 48px)",
|
||||
color: "#e8e4df",
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Contact
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
||||
{CONTACT_ITEMS.map((item, i) => (
|
||||
<FadeIn key={item.label} delay={i * 0.1}>
|
||||
<div
|
||||
className="rounded-2xl p-7"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[12px] font-semibold uppercase mb-2"
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.35)",
|
||||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
{item.href ? (
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-[15px] no-underline"
|
||||
style={{ color: "#c2339b" }}
|
||||
>
|
||||
{item.value}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-[15px]" style={{ color: "#e8e4df" }}>
|
||||
{item.value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FadeIn>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
182
components/experience.tsx
Normal file
182
components/experience.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA, type Experience } from "@/lib/cv-data";
|
||||
|
||||
function ExperienceCard({
|
||||
job,
|
||||
index,
|
||||
}: {
|
||||
job: Experience;
|
||||
index: number;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<FadeIn delay={index * 0.1}>
|
||||
<div
|
||||
className="rounded-2xl p-7 mb-5 cursor-pointer transition-all duration-300"
|
||||
style={{
|
||||
background: hovered
|
||||
? "rgba(255,255,255,0.04)"
|
||||
: "rgba(255,255,255,0.03)",
|
||||
border: `1px solid ${
|
||||
hovered
|
||||
? "rgba(194,51,155,0.25)"
|
||||
: "rgba(255,255,255,0.06)"
|
||||
}`,
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-2">
|
||||
<div>
|
||||
<h3
|
||||
className="font-serif text-2xl font-normal mb-1"
|
||||
style={{ color: "#e8e4df" }}
|
||||
>
|
||||
{job.role}
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm font-medium"
|
||||
style={{ color: "#c2339b" }}
|
||||
>
|
||||
{job.company}, {job.location}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className="text-[13px] whitespace-nowrap"
|
||||
style={{ color: "rgba(255,255,255,0.35)" }}
|
||||
>
|
||||
{job.period}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="text-[15px] leading-[1.7] mt-4"
|
||||
style={{ color: "rgba(255,255,255,0.5)" }}
|
||||
>
|
||||
{job.description}
|
||||
</p>
|
||||
|
||||
{job.highlights.length > 0 && (
|
||||
<div
|
||||
className="overflow-hidden transition-all duration-500"
|
||||
style={{ maxHeight: expanded ? 600 : 0 }}
|
||||
>
|
||||
<div
|
||||
className="pt-4 mt-4"
|
||||
style={{
|
||||
borderTop: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
{job.highlights.map((h, i) => (
|
||||
<div key={i} className="mb-3">
|
||||
<p
|
||||
className="text-[13px] font-semibold mb-1"
|
||||
style={{ color: "#c2339b" }}
|
||||
>
|
||||
{h.title}
|
||||
</p>
|
||||
<p
|
||||
className="text-[14px] leading-[1.6]"
|
||||
style={{ color: "rgba(255,255,255,0.45)" }}
|
||||
>
|
||||
{h.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.highlights.length > 0 && (
|
||||
<p
|
||||
className="text-[12px] mt-3"
|
||||
style={{ color: "rgba(255,255,255,0.25)" }}
|
||||
>
|
||||
{expanded ? "▲ Minder tonen" : "▼ Meer details"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExperienceSection() {
|
||||
return (
|
||||
<section
|
||||
id="ervaring"
|
||||
className="mx-auto max-w-[900px]"
|
||||
style={{ padding: "100px clamp(20px, 6vw, 80px)" }}
|
||||
>
|
||||
<FadeIn>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Loopbaan
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
style={{
|
||||
fontSize: "clamp(32px, 4vw, 48px)",
|
||||
color: "#e8e4df",
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Werkervaring
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
{CV_DATA.experience.map((job, i) => (
|
||||
<ExperienceCard key={i} job={job} index={i} />
|
||||
))}
|
||||
|
||||
<FadeIn delay={0.2}>
|
||||
<div
|
||||
className="rounded-2xl p-7 mt-10"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Opleiding
|
||||
</p>
|
||||
<h3
|
||||
className="font-serif text-2xl font-normal mb-1"
|
||||
style={{ color: "#e8e4df" }}
|
||||
>
|
||||
{CV_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}
|
||||
</p>
|
||||
<p
|
||||
className="text-[13px] mb-4"
|
||||
style={{ color: "rgba(255,255,255,0.35)" }}
|
||||
>
|
||||
{CV_DATA.education.period}
|
||||
</p>
|
||||
<p
|
||||
className="text-[14px]"
|
||||
style={{ color: "rgba(255,255,255,0.4)" }}
|
||||
>
|
||||
{CV_DATA.education.secondary}
|
||||
</p>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
46
components/fade-in.tsx
Normal file
46
components/fade-in.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useState, useEffect, ReactNode } from "react";
|
||||
|
||||
interface FadeInProps {
|
||||
children: ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FadeIn({ children, delay = 0, className = "" }: FadeInProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const obs = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
obs.unobserve(el);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
);
|
||||
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? "translateY(0)" : "translateY(28px)",
|
||||
transition: `opacity 0.7s ease ${delay}s, transform 0.7s ease ${delay}s`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
components/footer.tsx
Normal file
15
components/footer.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className="flex justify-between items-center flex-wrap gap-4 text-[13px]"
|
||||
style={{
|
||||
padding: "40px clamp(20px, 6vw, 80px)",
|
||||
borderTop: "1px solid rgba(255,255,255,0.06)",
|
||||
color: "rgba(255,255,255,0.25)",
|
||||
}}
|
||||
>
|
||||
<span>© {new Date().getFullYear()} Janpeter Visser</span>
|
||||
<span>jp-visser.nl</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
150
components/hero.tsx
Normal file
150
components/hero.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
|
||||
export function Hero() {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setLoaded(true), 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="over"
|
||||
className="relative min-h-screen flex items-center overflow-hidden"
|
||||
style={{ padding: "100px clamp(20px, 6vw, 80px) 60px" }}
|
||||
>
|
||||
{/* Gradient orbs */}
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
top: -120,
|
||||
right: -80,
|
||||
width: 500,
|
||||
height: 500,
|
||||
background:
|
||||
"radial-gradient(circle, rgba(194,51,155,0.12) 0%, transparent 70%)",
|
||||
borderRadius: "50%",
|
||||
filter: "blur(60px)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
bottom: -100,
|
||||
left: -60,
|
||||
width: 400,
|
||||
height: 400,
|
||||
background:
|
||||
"radial-gradient(circle, rgba(80,60,160,0.1) 0%, transparent 70%)",
|
||||
borderRadius: "50%",
|
||||
filter: "blur(50px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex flex-wrap items-center mx-auto w-full"
|
||||
style={{
|
||||
gap: "clamp(40px, 6vw, 80px)",
|
||||
maxWidth: 1100,
|
||||
}}
|
||||
>
|
||||
{/* Portrait */}
|
||||
<div
|
||||
className="flex-shrink-0 rounded-2xl overflow-hidden relative"
|
||||
style={{
|
||||
width: "clamp(220px, 28vw, 320px)",
|
||||
height: "clamp(280px, 36vw, 420px)",
|
||||
boxShadow:
|
||||
"0 30px 80px rgba(194,51,155,0.15), 0 0 0 1px rgba(255,255,255,0.05)",
|
||||
opacity: loaded ? 1 : 0,
|
||||
transform: loaded ? "scale(1)" : "scale(0.92)",
|
||||
transition: "all 0.9s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/images/portrait.jpg"
|
||||
alt="Janpeter Visser"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
priority
|
||||
sizes="(max-width: 768px) 220px, 320px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1 min-w-[280px]">
|
||||
<div
|
||||
style={{
|
||||
opacity: loaded ? 1 : 0,
|
||||
transform: loaded ? "translateY(0)" : "translateY(30px)",
|
||||
transition: "all 0.8s ease 0.2s",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Software Engineer
|
||||
</p>
|
||||
<h1
|
||||
className="font-serif font-normal mb-6"
|
||||
style={{
|
||||
fontSize: "clamp(42px, 5.5vw, 68px)",
|
||||
color: "#e8e4df",
|
||||
lineHeight: 1.05,
|
||||
letterSpacing: -1.5,
|
||||
}}
|
||||
>
|
||||
Janpeter
|
||||
<br />
|
||||
Visser
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
opacity: loaded ? 1 : 0,
|
||||
transform: loaded ? "translateY(0)" : "translateY(30px)",
|
||||
transition: "all 0.8s ease 0.4s",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[16px] leading-[1.7] max-w-[480px]"
|
||||
style={{ color: "rgba(255,255,255,0.55)" }}
|
||||
>
|
||||
{CV_DATA.intro}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4 mt-8 flex-wrap">
|
||||
<a
|
||||
href="#contact"
|
||||
className="px-8 py-3 rounded-lg text-sm font-semibold no-underline text-white"
|
||||
style={{
|
||||
background: "#c2339b",
|
||||
boxShadow: "0 4px 20px rgba(194,51,155,0.3)",
|
||||
}}
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
<a
|
||||
href="#ervaring"
|
||||
className="px-8 py-3 rounded-lg text-sm font-semibold no-underline"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
Bekijk CV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
125
components/nav.tsx
Normal file
125
components/nav.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
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() {
|
||||
const [active, setActive] = useState("over");
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 20);
|
||||
|
||||
const sections = [...NAV_ITEMS].reverse();
|
||||
for (const { id } of sections) {
|
||||
const el = document.getElementById(id);
|
||||
if (el && el.getBoundingClientRect().top < 200) {
|
||||
setActive(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const handleNav = (id: string) => {
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between transition-all duration-300"
|
||||
style={{
|
||||
background: scrolled
|
||||
? "rgba(15,15,20,0.92)"
|
||||
: "rgba(15,15,20,0.6)",
|
||||
backdropFilter: "blur(16px)",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
||||
padding: "0 clamp(20px, 4vw, 60px)",
|
||||
height: 64,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-serif text-[22px] tracking-tight"
|
||||
style={{ color: "#e8e4df" }}
|
||||
>
|
||||
JP<span style={{ color: "#c2339b" }}>.</span>
|
||||
</span>
|
||||
|
||||
<div className="hidden sm:flex gap-8">
|
||||
{NAV_ITEMS.map(({ label, id }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => handleNav(id)}
|
||||
className="bg-transparent border-none cursor-pointer text-[13px] font-medium uppercase tracking-[1.2px] transition-colors duration-300 pb-1"
|
||||
style={{
|
||||
color:
|
||||
active === id ? "#c2339b" : "rgba(255,255,255,0.5)",
|
||||
borderBottom:
|
||||
active === id
|
||||
? "1px solid #c2339b"
|
||||
: "1px solid transparent",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="sm:hidden bg-transparent border-none cursor-pointer"
|
||||
style={{ color: "rgba(255,255,255,0.6)", fontSize: 20 }}
|
||||
onClick={() => {
|
||||
const menu = document.getElementById("mobile-menu");
|
||||
if (menu) {
|
||||
menu.style.display =
|
||||
menu.style.display === "flex" ? "none" : "flex";
|
||||
}
|
||||
}}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
|
||||
{/* Mobile dropdown */}
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className="sm:hidden absolute top-[64px] left-0 right-0 flex-col py-4 gap-1"
|
||||
style={{
|
||||
display: "none",
|
||||
background: "rgba(15,15,20,0.95)",
|
||||
backdropFilter: "blur(16px)",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
{NAV_ITEMS.map(({ label, id }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => {
|
||||
handleNav(id);
|
||||
const menu = document.getElementById("mobile-menu");
|
||||
if (menu) menu.style.display = "none";
|
||||
}}
|
||||
className="bg-transparent border-none cursor-pointer text-[14px] font-medium py-3 px-8 text-left transition-colors"
|
||||
style={{
|
||||
color:
|
||||
active === id ? "#c2339b" : "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
113
components/skills.tsx
Normal file
113
components/skills.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"use client";
|
||||
|
||||
import { FadeIn } from "./fade-in";
|
||||
import { CV_DATA } from "@/lib/cv-data";
|
||||
|
||||
function SkillPill({ label }: { label: string }) {
|
||||
return (
|
||||
<span
|
||||
className="inline-block px-4 py-1.5 rounded-full text-[13px] font-medium mr-1.5 mb-1.5 transition-all duration-200 hover:bg-[rgba(194,51,155,0.18)] hover:text-[#e8e4df]"
|
||||
style={{
|
||||
background: "rgba(194,51,155,0.08)",
|
||||
border: "1px solid rgba(194,51,155,0.15)",
|
||||
color: "rgba(255,255,255,0.65)",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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: "Talen", items: CV_DATA.skills.spoken },
|
||||
];
|
||||
|
||||
export function SkillsSection() {
|
||||
return (
|
||||
<section
|
||||
id="skills"
|
||||
className="mx-auto max-w-[900px]"
|
||||
style={{ padding: "100px clamp(20px, 6vw, 80px)" }}
|
||||
>
|
||||
<FadeIn>
|
||||
<p
|
||||
className="text-[13px] font-semibold uppercase mb-3"
|
||||
style={{ color: "#c2339b", letterSpacing: 3 }}
|
||||
>
|
||||
Technologie
|
||||
</p>
|
||||
<h2
|
||||
className="font-serif font-normal mb-12"
|
||||
style={{
|
||||
fontSize: "clamp(32px, 4vw, 48px)",
|
||||
color: "#e8e4df",
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Skills & Tools
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
<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}>
|
||||
<div
|
||||
className="rounded-2xl p-7"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="text-[12px] font-semibold uppercase mb-4"
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</h3>
|
||||
<div>
|
||||
{group.items.map((s) => (
|
||||
<SkillPill key={s} label={s} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FadeIn delay={0.3}>
|
||||
<div className="mt-10">
|
||||
<h3
|
||||
className="text-[12px] font-semibold uppercase mb-4"
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
Interesses
|
||||
</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CV_DATA.interests.map((interest) => (
|
||||
<span
|
||||
key={interest}
|
||||
className="px-5 py-2 rounded-full text-[14px]"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
}}
|
||||
>
|
||||
{interest}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue