Initial commit
This commit is contained in:
commit
dc66b66d94
22 changed files with 7556 additions and 0 deletions
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
87
README.md
Normal file
87
README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# jp-visser.nl
|
||||
|
||||
Persoonlijke website van Janpeter Visser — Software Engineer.
|
||||
|
||||
## Lokaal draaien
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Deployen naar Vercel
|
||||
|
||||
### 1. Push naar GitHub
|
||||
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/JOUW_USERNAME/jp-visser.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 2. Koppel aan Vercel
|
||||
|
||||
1. Ga naar [vercel.com/new](https://vercel.com/new)
|
||||
2. Importeer je GitHub repository
|
||||
3. Klik op **Deploy** — Vercel detecteert Next.js automatisch
|
||||
|
||||
### 3. Domein instellen
|
||||
|
||||
1. Ga naar je project in Vercel → **Settings** → **Domains**
|
||||
2. Voeg `jp-visser.nl` toe
|
||||
3. Bij je domeinregistrar, stel in:
|
||||
- **A-record**: `@` → `76.76.21.21`
|
||||
- **CNAME**: `www` → `cname.vercel-dns.com`
|
||||
|
||||
DNS-wijzigingen kunnen tot 48 uur duren, maar zijn meestal binnen een uur actief.
|
||||
|
||||
## Apps toevoegen
|
||||
|
||||
Om apps toe te voegen aan de portfolio:
|
||||
|
||||
**Optie A — Als aparte Vercel projecten (subdomein):**
|
||||
- Deploy een app als apart project op Vercel
|
||||
- Voeg een subdomein toe: bijv. `app1.jp-visser.nl`
|
||||
|
||||
**Optie B — Als routes binnen dit project:**
|
||||
- Maak een nieuwe pagina aan in `app/apps/naam/page.tsx`
|
||||
- De app is dan bereikbaar op `jp-visser.nl/apps/naam`
|
||||
|
||||
Pas daarna `components/apps.tsx` aan om de links naar je apps te tonen.
|
||||
|
||||
## Projectstructuur
|
||||
|
||||
```
|
||||
jp-visser/
|
||||
├── app/
|
||||
│ ├── globals.css # Global styles + Tailwind
|
||||
│ ├── layout.tsx # Root layout met fonts & metadata
|
||||
│ └── page.tsx # Homepage
|
||||
├── components/
|
||||
│ ├── nav.tsx # Navigatie (sticky)
|
||||
│ ├── hero.tsx # Hero sectie met portret
|
||||
│ ├── experience.tsx # Werkervaring
|
||||
│ ├── skills.tsx # Skills & tools
|
||||
│ ├── apps.tsx # Apps portfolio
|
||||
│ ├── contact.tsx # Contact info
|
||||
│ ├── footer.tsx # Footer
|
||||
│ └── fade-in.tsx # Scroll-animatie component
|
||||
├── lib/
|
||||
│ └── cv-data.ts # Alle CV data
|
||||
├── public/
|
||||
│ └── images/
|
||||
│ └── portrait.jpg # Portretfoto
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Tech stack
|
||||
|
||||
- **Next.js 15** (App Router)
|
||||
- **TypeScript**
|
||||
- **Tailwind CSS**
|
||||
- Hosted op **Vercel**
|
||||
79
app/globals.css
Normal file
79
app/globals.css
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--accent: #c2339b;
|
||||
--bg: #0f0f14;
|
||||
--text: #e8e4df;
|
||||
--text-muted: rgba(255, 255, 255, 0.5);
|
||||
--text-dim: rgba(255, 255, 255, 0.35);
|
||||
--surface: rgba(255, 255, 255, 0.03);
|
||||
--border: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(194, 51, 155, 0.35);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(194, 51, 155, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
a {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Fade-in animation */
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition: opacity 0.7s ease, transform 0.7s ease;
|
||||
}
|
||||
|
||||
.fade-in.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
49
app/layout.tsx
Normal file
49
app/layout.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Metadata } from "next";
|
||||
import { DM_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Janpeter Visser — Software Engineer",
|
||||
description:
|
||||
"Persoonlijke website van Janpeter Visser. Allround software engineer met 30 jaar ervaring in full-stack development, van C++ tot Angular en .NET.",
|
||||
metadataBase: new URL("https://jp-visser.nl"),
|
||||
openGraph: {
|
||||
title: "Janpeter Visser — Software Engineer",
|
||||
description:
|
||||
"Allround software engineer met 30 jaar ervaring in full-stack development.",
|
||||
url: "https://jp-visser.nl",
|
||||
siteName: "Janpeter Visser",
|
||||
locale: "nl_NL",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="nl" className={dmSans.variable}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
21
app/page.tsx
Normal file
21
app/page.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Nav } from "@/components/nav";
|
||||
import { Hero } from "@/components/hero";
|
||||
import { ExperienceSection } from "@/components/experience";
|
||||
import { SkillsSection } from "@/components/skills";
|
||||
import { AppsSection } from "@/components/apps";
|
||||
import { ContactSection } from "@/components/contact";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<Hero />
|
||||
<ExperienceSection />
|
||||
<SkillsSection />
|
||||
<AppsSection />
|
||||
<ContactSection />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
120
lib/cv-data.ts
Normal file
120
lib/cv-data.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
export const CV_DATA = {
|
||||
name: "Janpeter Visser",
|
||||
tagline: "Software Engineer · Full Stack Developer",
|
||||
intro:
|
||||
"Sinds mijn afstuderen werk ik met enthousiasme en nieuwsgierigheid in de IT. Ik ben een allround developer die graag nieuwe technologieën onderzoekt, evalueert en inzet in nieuwe projecten. De afgelopen jaren heb ik meerdere complexe projecten bij de hand gehad — ik vind het leuk om mij daarin vast te bijten en in een klein team naar oplossingen te zoeken.",
|
||||
contact: {
|
||||
email: "janpetervisser2@gmail.com",
|
||||
location: "Rotterdam",
|
||||
},
|
||||
experience: [
|
||||
{
|
||||
company: "QPIT BV",
|
||||
location: "Rotterdam",
|
||||
period: "januari 2004 – februari 2024",
|
||||
role: "Software Engineer",
|
||||
description:
|
||||
"Bij QPIT heb ik meerdere functies gehad in een multidisciplinaire rol. Ik heb bijgedragen aan het ontwikkelen van het proprietary softwaresysteem Quism (Servicedesk software) en het inrichten hiervan voor klanten.",
|
||||
highlights: [
|
||||
{
|
||||
title: "Software Engineer Quism",
|
||||
text: "Webapplicatie voor service management (ITIL) cross-browser compatible gemaakt. Tools ontwikkeld waarmee ASP, JavaScript en HTML geanalyseerd en aangepast kon worden. Ondersteunende tools gebouwd voor e-mail ticket integratie via IMAP, MAPI en POP3.",
|
||||
},
|
||||
{
|
||||
title: "Research & Development",
|
||||
text: "Onderzoek naar nieuwe ontwikkelomgevingen voor mobile devices. Dit leidde tot het ontwikkelen van full stack PWA applications met Angular, TypeScript en C#.",
|
||||
},
|
||||
{
|
||||
title: "Product Development",
|
||||
text: "Diverse klantenportalen gemaakt voor mobile devices. Webapplicatie voor scrum/agile projectontwikkeling met webbased Agile dashboards. Voor het Havenbedrijf een applicatie overgebracht naar een fullstack PWA.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "TNO Bouw",
|
||||
location: "Delft",
|
||||
period: "januari 2001 – januari 2003",
|
||||
role: "Wetenschappelijk Medewerker",
|
||||
description:
|
||||
"Op de afdeling Bouwinformatica gewerkt als wetenschappelijk medewerker, op het raakvlak van universiteiten (bouwkunde & informatica) en de bouwnijverheid.",
|
||||
highlights: [
|
||||
{
|
||||
title: "HSL-traject",
|
||||
text: "Configuratiemanagementsysteem gedefinieerd en geïmplementeerd voor het ontwerpen van het HSL-traject. Formalisering vastgelegd in UML.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "Logica",
|
||||
location: "Woerden",
|
||||
period: "oktober 1998 – januari 2001",
|
||||
role: "Software Engineer",
|
||||
description:
|
||||
"Gedetacheerd bij de Belastingdienst in Apeldoorn. Gewerkt aan het systeem voor digitale belastingaangifte. Medeverantwoordelijk voor testen, acceptatie en distributie van software.",
|
||||
highlights: [
|
||||
{
|
||||
title: "Productspecialist",
|
||||
text: "Verantwoordelijk voor integratie van nieuwe producten binnen de Belastingdienst. Coördinerende rol tussen verschillende afdelingen.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "Europe Transport Automation",
|
||||
location: "Rotterdam",
|
||||
period: "november 1994 – oktober 1998",
|
||||
role: "Systeemontwerper en -ontwikkelaar",
|
||||
description:
|
||||
"Verantwoordelijk voor de bouw van systemen voor de logistieke sector. Primaire bedrijfsprocessen gemodelleerd met grafische interfaces voor efficiënt vrachttransport, inclusief facturatiesystemen.",
|
||||
highlights: [],
|
||||
},
|
||||
],
|
||||
education: {
|
||||
university: "TU Delft",
|
||||
degree: "Technische Informatica",
|
||||
specialization:
|
||||
"Vakgroep Software Engineering, Programmeertalen en Programmeertaal Compilers",
|
||||
period: "1987 – 1994",
|
||||
secondary:
|
||||
"VWO Atheneum-B, OSG Ring van Putten te Spijkenisse (1981–1987)",
|
||||
},
|
||||
skills: {
|
||||
languages: [
|
||||
"C",
|
||||
"C++",
|
||||
"C#",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"HTML",
|
||||
"CSS",
|
||||
"Visual Basic",
|
||||
"Assembler",
|
||||
"LISP",
|
||||
"Prolog",
|
||||
"Perl",
|
||||
"Delphi",
|
||||
],
|
||||
frameworks: [
|
||||
"Angular",
|
||||
"Angular Material",
|
||||
"Nx",
|
||||
"ASP.NET",
|
||||
"ASP.NET Core",
|
||||
],
|
||||
databases: [
|
||||
"Microsoft SQL Server",
|
||||
"Oracle",
|
||||
"MySQL",
|
||||
"MariaDB",
|
||||
"Microsoft Access",
|
||||
],
|
||||
tools: ["Git", "Visual Sourcesafe"],
|
||||
spoken: ["Nederlands", "Engels", "Duits"],
|
||||
},
|
||||
interests: [
|
||||
"Reizen door Azië",
|
||||
"Yoga",
|
||||
"Vrijwilligerswerk digitale ondersteuning: Centrale Bibliotheek Rotterdam & Wijkcentrum Schiedam Oost",
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type Experience = (typeof CV_DATA.experience)[number];
|
||||
5
next.config.ts
Normal file
5
next.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
6285
package-lock.json
generated
Normal file
6285
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "jp-visser",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"postcss": "^8.4.49",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.1.0"
|
||||
}
|
||||
}
|
||||
9
postcss.config.mjs
Normal file
9
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/images/portrait.jpg
Normal file
BIN
public/images/portrait.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
25
tailwind.config.ts
Normal file
25
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
accent: "#c2339b",
|
||||
bg: "#0f0f14",
|
||||
surface: "rgba(255,255,255,0.03)",
|
||||
border: "rgba(255,255,255,0.06)",
|
||||
},
|
||||
fontFamily: {
|
||||
serif: ["Instrument Serif", "Georgia", "serif"],
|
||||
sans: ["DM Sans", "system-ui", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue