Compare commits

..

7 Commits

Author SHA1 Message Date
Valentijn
ba8bb0b023 removed package lock 2026-05-16 13:36:52 +02:00
Valentijn
badbfd7a74 added CV button, and made contact form work 2026-05-15 21:50:30 +02:00
Valentijn
8b40305b22 fixed a LOT of repetition 2026-05-15 18:35:47 +02:00
Valentijn
f6c44128b3 added contact form, css class name clarification 2026-05-15 14:52:28 +02:00
Valentijn
72137466dc added experience page 2026-05-15 14:19:45 +02:00
Valentijn
ec2f7704e7 description text 2026-05-01 17:51:50 +02:00
Valentijn
408afdfd1f added projects, removed ugly skill chart 2026-04-24 15:49:28 +02:00
25 changed files with 790 additions and 5692 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ dist/
# dependencies
node_modules/
package-lock.json
# logs
npm-debug.log*

5349
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"dependencies": {
"@iconify-json/mdi": "^1.2.3",
"astro": "^6.1.8",
"astro-icon": "^1.1.5"
"astro-icon": "^1.1.5",
"sweetalert2": "^11.26.24"
}
}

BIN
public/CV.pdf Normal file

Binary file not shown.

61
src/components/Box.astro Normal file
View File

@@ -0,0 +1,61 @@
---
interface Props {
accent?: string;
isTitle?: boolean
}
const { accent, isTitle } = Astro.props as Props;
let opachity = 0.05;
if (isTitle) {
opachity = 0.2;
}
---
<div class="container">
{accent && <div class="accent" style={`background-color: ${accent}; width:4px; border-radius: 999px; flex-shrink: 0;`}></div>}
<div class="content">
<slot />
</div>
</div>
<style define:vars={{color: `rgba(255,255,255,${opachity})`}}>
.container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
position: relative;
display: flex;
align-items: stretch;
gap: 1rem;
min-height: 120px;
padding: 1rem;
background: var(--color);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
box-shadow: 0 8px 20px rgba(0,0,0,0.18);
color: white;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
margin-top: 0.5rem;
}
.container:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(0,0,0,0.24);
border-color: rgba(255,255,255,0.14);
}
.content {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
@media (max-width: 768px) {
.container {
min-height: 100px;
padding: 0.85rem;
gap: 0.8rem;
}
}
</style>

View File

@@ -13,7 +13,7 @@ const navigationHandler = `window.open('${href}', '_blank')`;
<div class="button" onclick={navigationHandler}>
<Icon name={icon} class="icon" />
<a href={href} target="_blank" rel="noreferrer">
<a rel="noreferrer">
{label}
</a>
</div>

View File

@@ -0,0 +1,169 @@
---
const { title, description } = Astro.props;
import Box from "../Box.astro";
---
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<Box>
<form id="contactForm">
<input type="text" name="_honey" id="honey" style="display:none;" tabindex="-1" autocomplete="off" />
<input type="text" name="name" id="nameInput" class="name"placeholder="John Doe" autocomplete="name" required>
<input type="email" name="email" id="emailInput" class="email" placeholder="john@example.com" autocomplete="email" required>
<textarea name="message" id="messageInput" placeholder="Your message here" required></textarea>
<input type="submit" value="Indienen" class="submitBtn">
</form>
</Box>
<script>
import Swal from 'sweetalert2';
const form = document.getElementById('contactForm') as HTMLFormElement | null;
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const nameInput = document.getElementById('nameInput') as HTMLInputElement | null;
const emailInput = document.getElementById('emailInput') as HTMLInputElement | null;
const messageInput = document.getElementById('messageInput') as HTMLTextAreaElement | null;
const honeyInput = document.getElementById('honey') as HTMLInputElement | null;
if (!nameInput || !emailInput || !messageInput || !honeyInput) {
console.error("One or more form fields were not found in the DOM.");
return;
}
const formData = new FormData();
formData.append('name', nameInput.value);
formData.append('email', emailInput.value);
formData.append('message', messageInput.value);
formData.append('_honey', honeyInput.value);
Swal.fire({
title: 'Sending...',
didOpen: () => Swal.showLoading()
});
try {
const response = await fetch('https://contact.herpiederpiee.nl/submit', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.status === 201) {
Swal.fire({
icon: 'success',
title: 'Success!',
text: 'Your message has been received!',
});
form.reset();
} else if (response.status === 429) {
Swal.fire({
icon: 'error',
title: 'Slow down!',
text: result.detail,
});
} else {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: result.detail || 'Something went wrong.',
});
}
} catch (error) {
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Could not connect to the contact server.',
});
}
});
}
</script>
<style>
input, textarea {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
border-radius: 10px;
color: white;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(18, 18, 28, 0.24);
backdrop-filter: blur(2px);
transition: all 200ms ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
font-size: 0.75rem;
padding: 0.8rem 1rem;
}
input::placeholder, textarea::placeholder {
color: rgba(255, 255, 255, 0.5);
}
input:focus, textarea:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.3);
background: rgba(18, 18, 28, 0.4);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 3px rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
input:hover, textarea:hover {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(18, 18, 28, 0.35);
}
.submitBtn {
background: linear-gradient(135deg, rgba(140, 90, 20, 0.4), rgba(96, 67, 8, 0.3));
border: 1px solid rgba(255, 255, 255, 0.15);
color: white;
font-weight: 600;
cursor: pointer;
padding: unset !important;
transition: all 200ms ease;
height: 3rem !important;
border-radius: 10px;
}
.submitBtn:hover {
background: linear-gradient(135deg, rgba(160, 110, 30, 0.5), rgba(116, 87, 18, 0.4));
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.submitBtn:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
form {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
gap: 0.6rem;
}
form textarea {
height: 8rem;
resize: none;
}
@media (max-width: 768px) {
form {
width: 100%;
gap: 0.6rem;
}
input, textarea {
font-size: 0.75rem;
padding: 0.8rem 1rem;
}
}
</style>

View File

@@ -0,0 +1,14 @@
---
import Box from "../Box.astro";
---
<Box>
<div class="image"></div>
</Box>
<style>
.image {
width: 40vw;
height: 400px;
background: white;
}
</style>

View File

@@ -0,0 +1,43 @@
---
import TitleCard from '../TitleCard.astro';
import ContactForm from './ContactForm.astro';
import ContactImage from './ContactImage.astro';
import Section from '../Section.astro';
---
<Section className="contact-section" label="Contact">
<TitleCard title="Contact" description="Neem gerust contact met me op!"/>
<div class="formContaner">
<ContactForm />
<div class="imgContainer">
<ContactImage />
</div>
</div>
</Section>
<style>
.formContaner {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1rem;
}
.image {
width: 40vw;
height: 400px;
background: white;
}
/* Mobile: collapse to one column,
so "Kennis" ends up fully under contact */
@media (max-width: 768px) {
.imgContainer {
display: none;
}
.formContaner {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,96 @@
---
const { experience } = Astro.props;
import Box from "../Box.astro";
const color: string = experience.color
---
<Box accent={color}>
<div class="titleRow">
<div class="titleContainer">
<h3 class="main-title">{experience.name}</h3>
<p class="sub-title">{experience.role}</p>
<p class="description">
{experience.description}
</p>
</div>
<div class="dateContainer">
<p>
{experience.start.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })}
-
{experience.end
? experience.end.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })
: 'Nu'}
</p>
</div>
</div>
</Box>
<style define:vars={{color}}>
.content {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.titleRow {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.titleContainer {
min-width: 0;
}
.main-title {
font-size: 1.1rem;
line-height: 1.2;
margin: 0 0 0.3rem 0;
font-weight: 700;
}
.sub-title {
font-size: 0.92rem;
line-height: 1.45;
margin: 0;
opacity: 0.8;
}
.dateContainer {
flex-shrink: 0;
}
.dateContainer p {
font-size: 0.8rem;
margin: 0;
white-space: nowrap;
opacity: 0.7;
}
@media (max-width: 768px) {
.titleRow {
flex-direction: column;
gap: 0.55rem;
}
.main-title {
font-size: 1rem;
}
.sub-title {
font-size: 0.84rem;
}
.dateContainer p {
font-size: 0.74rem;
white-space: normal;
}
}
</style>

View File

@@ -0,0 +1,34 @@
---
import { siteData } from '../../data/site';
import ExperienceBox from './ExperienceBox.astro';
import TitleCard from '../TitleCard.astro';
import Section from '../Section.astro';
const experience = [...siteData.experience];
const sortedExperience = experience.sort((a, b) => b.start.getTime() - a.start.getTime());
---
<Section className="experience-section" label="Ervaring">
<div class="experience-list">
<TitleCard title="Ervaringen" description="Hieronder zie je alle werkervaring die ik tot nu toe hebv opgedaan. "/>
{sortedExperience.map((experience) => (
<ExperienceBox experience={experience} />
))}
</div>
</Section>
<style>
.experience-list {
display: flex;
flex-direction: column;
gap: 1.3rem;
position: relative;
}
/* Mobile: collapse to one column,
so "Kennis" ends up fully under experience */
@media (max-width: 768px) {
.experience-section {
margin-top: 2rem;
}
}
</style>

View File

@@ -56,6 +56,7 @@ const clipboardHandler = `navigator.clipboard.writeText('${safeEmail}').then(()=
transform: translateY(-2px);
box-shadow: 0 24px 65px rgba(0, 0, 0, 0.22);
width: min(15rem, 100%);
transition: all .3s;
}
.email-button:hover .label {

View File

@@ -1,55 +1,32 @@
---
import { siteData } from '../../data/site';
import {age} from "../../utils/getAge"
import {study} from "../../utils/getCurrentStudy"
import hero_img from "../../assets/hero.png"
import Button from "./Button.astro"
import Button from "../Button.astro"
import EmailButton from "./EmailButton.astro"
import Section from '../Section.astro';
const hero_img_src = `url(${hero_img.src})`;
const socials = [...siteData.socials];
const occupation = siteData.occupation;
---
<div id="container">
<Section className="hero-section" label="Hero" isHero={true}>
<div class="titleContainer">
<h1 class="main-title">Valentijn van der Jagt</h1>
<h3 class="sub-title">{age} Jaar - {study?.label}</h3>
<h3 class="sub-title">{age} Jaar - {occupation}</h3>
<h3></h3>
</div>
<div class="buttonContainer">
<Button icon="mdi:git" label="Gitea" href="https://git.herpiederpiee.nl/valentijn?tab=repositories"/>
<Button icon="mdi:linkedin" label="LinkedIn" href="https://nl.linkedin.com/in/valentijn-van-der-jagt"/>
{socials.map((link) => (
<Button icon={link.icon} label={link.platform} href={link.url} />
))}
<EmailButton icon="mdi:email-outline" label="E-mail" />
</div>
</Section>
</div>
<style define:vars={{hero_img_src}}>
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
position: relative;
min-height: clamp(420px, 58vh, 680px);
margin: 10px;
padding: clamp(1.5rem, 3vw, 2.5rem);
background: radial-gradient(circle at 15% 18%, rgba(255, 246, 228, 0.32), transparent 24%),
linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(10, 10, 10, 0.24)),
var(--hero_img_src);
background-size: cover;
background-position: center;
border-radius: 20px;
box-shadow: 0 24px 60px rgba(16, 14, 12, 0.18);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
animation: driftBackground 16s ease-in-out infinite alternate;
}
<style>
.buttonContainer {
display: flex;
flex-wrap: wrap;
@@ -105,10 +82,6 @@ const hero_img_src = `url(${hero_img.src})`;
}
@media (max-width: 900px) {
#container {
padding: 1.5rem 1.2rem;
}
.buttonContainer {
justify-content: center;
}
@@ -122,10 +95,6 @@ const hero_img_src = `url(${hero_img.src})`;
}
@media (max-width: 600px) {
#container {
min-height: 360px;
}
.buttonContainer {
justify-content: center;
gap: 0.8rem;

View File

@@ -0,0 +1,35 @@
---
import { siteData } from '../../data/site';
import ProjectBox from './ProjectBox.astro';
import TitleCard from '../TitleCard.astro';
import Section from '../Section.astro';
const projects = [...siteData.projects];
---
<Section className="projects-section" label="Projecten">
<div class="projects-list">
<TitleCard title="Projecten" description="Hieronder zie je een aantal van mijn favoriete persoonlijke projecten!"/>
{projects.map((project) => (
<ProjectBox project={project} />
))}
</div>
</Section>
<style>
.projects-list {
display: flex;
flex-direction: column;
gap: 1.3rem;
position: relative;
}
/* Mobile: collapse to one column,
so "Kennis" ends up fully under studies */
@media (max-width: 768px) {
.projects-section {
margin-top: 2rem;
}
}
</style>

View File

@@ -0,0 +1,102 @@
---
const { project } = Astro.props;
import {techColors} from "../../data/site"
const colors: { [id: string] : string; } = {...techColors}
import Button from "../Button.astro";
import Box from "../Box.astro";
---
<Box>
<div class="titleRow">
<div class="titleContainer">
<h3 class="main-title">{project.title}</h3>
<p class="sub-title">{project.description}</p>
<div class="tech-stack">
{project.tech.map((item: string) => {
const color = colors[item] || colors["Default"];
return (
<span class="tech-badge" style={`--brand-color: ${color}`}>
{item}
</span>
);
})}
</div>
</div>
<Button label="Bekijk Project" href={project.link} icon="mdi:link"/>
</div>
</Box>
<style>
.titleRow {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.titleContainer {
min-width: 0;
}
.main-title {
font-size: 1.1rem;
line-height: 1.2;
margin: 0 0 0.3rem 0;
font-weight: 700;
}
.sub-title {
font-size: 0.92rem;
line-height: 1.45;
margin: 0;
opacity: 0.8;
}
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: 1rem; /* Space between tags and title */
}
.tech-badge {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0.2rem 0.6rem;
/* The Magic: Using the variable from the style prop */
background: rgba(0, 0, 0, 0.2);
border: 1.5px solid var(--brand-color);
color: var(--brand-color);
border-radius: 4px;
}
/* Keep your existing media queries */
@media (max-width: 768px) {
.titleRow {
flex-direction: column;
gap: 0.55rem;
}
.main-title {
font-size: 1rem;
}
.sub-title {
font-size: 0.84rem;
}
/* Ensure badges don't get too small on mobile */
.tech-stack {
margin-top: 0.6rem;
}
}
</style>

View File

@@ -0,0 +1,74 @@
---
interface Props {
className: string;
label: string;
isHero?: boolean;
}
const { className, label, isHero } = Astro.props;
import hero_img from '../assets/hero.png';
let containerBackground;
if (isHero) {
containerBackground = `radial-gradient(circle at 15% 18%, rgba(255, 246, 228, 0.32), transparent 24%),
linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(10, 10, 10, 0.24)),
url("${hero_img.src}")`;
} else {
containerBackground = `linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)),
rgb(74, 74, 74)`;
}
---
<div id="container">
<section class={className} aria-label={label}>
<slot/>
</section>
</div>
<style define:vars={{containerBackground}}>
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
position: relative;
margin: 10px;
padding: clamp(1.5rem, 3vw, 2.5rem);
background: var(--containerBackground);
background-size: cover;
background-position: center;
border-radius: 20px;
box-shadow: 0 24px 60px rgba(16, 14, 12, 0.18);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
animation: driftBackground 10s ease-in-out infinite alternate;
}
/* Mobile: collapse to one column,
so "Kennis" ends up fully under studies */
@media (max-width: 768px) {
#container {
margin: 5px;
padding: 1rem;
grid-template-columns: 1fr;
gap: 1rem;
}
section {
margin-top: 2rem;
}
}
@keyframes driftBackground {
0% { background-position: center top; }
100% { background-position: 12% 62%; }
}
</style>

View File

@@ -1,90 +1,23 @@
---
import { siteData } from '../../data/site';
import StudyBox from './StudyBox.astro';
import StudyBoxTitle from './StudyBoxTitle.astro';
import SkillChart from './SkillChart.astro';
import TitleCard from '../TitleCard.astro';
import Section from '../Section.astro';
const studies = [...siteData.studies];
const sortedStudies = studies.sort((a, b) => b.start.getTime() - a.start.getTime());
---
<div id="container">
<div class="left-column">
<aside class="sidebar-card">
<h2>Kennis</h2>
<div class="skillChart">
<SkillChart skills={siteData.skills} />
</div>
</aside>
</div>
<section class="studies-section" aria-label="Studies">
<Section className="studies-section" label="Studies">
<div class="studies-list">
<StudyBoxTitle />
<TitleCard title="Opleidingen" description="Hieronder zie je alle opleidingen die ik heb gevolgd tot nu toe. "/>
{sortedStudies.map((study) => (
<StudyBox study={study} />
))}
</div>
</section>
</div>
</Section>
<style>
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
position: relative;
margin: 10px;
padding: clamp(1.5rem, 3vw, 2.5rem);
background:
linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)),
rgb(74, 74, 74);
border-radius: 20px;
box-shadow: 0 24px 60px rgba(16, 14, 12, 0.18);
overflow: hidden;
display: grid;
grid-template-columns: minmax(220px, 280px) 1fr;
gap: 1.5rem;
align-items: start;
}
.left-column {
position: sticky;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.25rem;
color: white;
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
margin-bottom: 1rem;
}
.sidebar-card h2 {
margin: 0;
font-size: clamp(1.4rem, 2vw, 1.9rem);
line-height: 1.1;
}
.sidebar-copy {
margin: 0.8rem 0 1.2rem 0;
font-size: 0.95rem;
line-height: 1.55;
opacity: 0.82;
}
.skillChart {
margin-top: 1rem;
}
.studies-section {
min-width: 0;
}
.studies-list {
display: flex;
flex-direction: column;
@@ -92,22 +25,5 @@ const sortedStudies = studies.sort((a, b) => b.start.getTime() - a.start.getTime
position: relative;
}
/* Mobile: collapse to one column,
so "Kennis" ends up fully under studies */
@media (max-width: 768px) {
#container {
margin: 5px;
padding: 1rem;
grid-template-columns: 1fr;
gap: 1rem;
}
.left-column {
position: static;
}
.studies-section {
margin-top: 2rem;
}
}
</style>

View File

@@ -1,55 +0,0 @@
---
const { skills } = Astro.props;
interface Skill {
name: string;
level: number;
}
---
<div class="skills-chart">
{skills.map((skill: Skill) => (
<div class="skill">
<span class="skill-name">{skill.name}</span>
<div class="skill-bar">
<span
style={{
width: `${(skill.level / 10) * 100}%`
}}
></span>
</div>
</div>
))}
</div>
<style>
.skills-chart {
display: flex;
flex-direction: column;
gap: 0.7rem;
}
.skill {
display: grid;
gap: 0.3rem;
}
.skill-name {
font-size: 0.88rem;
opacity: 0.9;
}
.skill-bar {
height: 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 999px;
overflow: hidden;
}
.skill-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #7dd3fc, #38bdf8);
}
</style>

View File

@@ -1,13 +1,12 @@
---
const { study } = Astro.props;
import Box from "../Box.astro";
const color: string = study.color
---
<div class="container">
<div class="accent" aria-hidden="true"></div>
<div class="content">
<Box accent={color}>
<div class="titleRow">
<div class="titleContainer">
<h3 class="main-title">{study.study}</h3>
@@ -24,8 +23,7 @@ const color: string = study.color
</p>
</div>
</div>
</div>
</div>
</Box>
<style define:vars={{color}}>
.container {

View File

@@ -1,90 +0,0 @@
---
---
<div class="container">
<div class="content">
<div class="titleRow">
<div class="titleContainer">
<h2 class="main-title">Opleidingen</h2>
<p class="sub-title">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Consequuntur amet, quos </p>
</div>
</div>
</div>
</div>
<style>
.container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
position: relative;
display: flex;
align-items: stretch;
gap: 1rem;
min-height: 80px;
padding: 1rem 1rem 1rem 0.9rem;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
box-shadow: 0 8px 20px rgba(0,0,0,0.18);
color: white;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
.container:hover {
transform: translateY(-2px);
box-shadow: 0 14px 28px rgba(0,0,0,0.24);
border-color: rgba(255,255,255,0.14);
}
.content {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.titleRow {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.titleContainer {
min-width: 0;
}
.main-title {
font-size: clamp(1.4rem,2vw,1.9rem);
line-height: 1.2;
margin: 0 0 0.3rem 0;
font-weight: 700;
}
.sub-title {
font-size: 0.92rem;
line-height: 1.45;
margin: 0;
opacity: 0.8;
}
@media (max-width: 768px) {
.container {
min-height: 100px;
padding: 0.85rem 0.85rem 0.85rem 0.75rem;
gap: 0.8rem;
}
.titleRow {
flex-direction: column;
gap: 0.55rem;
}
.sub-title {
font-size: 0.84rem;
}
}
</style>

View File

@@ -0,0 +1,53 @@
---
const { title, description } = Astro.props;
import Box from "./Box.astro";
---
<Box isTitle={true}>
<div class="titleRow">
<div class="titleContainer">
<h2 class="main-title">{title}</h2>
<p class="sub-title">{description}</p>
</div>
</div>
</Box>
<style>
.titleRow {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.titleContainer {
min-width: 0;
}
.main-title {
font-size: clamp(1.4rem,2vw,1.9rem);
line-height: 1.2;
margin: 0 0 0.3rem 0;
font-weight: 700;
}
.sub-title {
font-size: 0.92rem;
line-height: 1.45;
margin: 0;
opacity: 0.8;
}
@media (max-width: 768px) {
.titleRow {
flex-direction: column;
gap: 0.55rem;
}
.sub-title {
font-size: 0.84rem;
}
}
</style>

View File

@@ -1,25 +1,51 @@
export const siteData = {
/*
Personal Information
*/
name: "Valentijn van der Jagt",
email: "valentijnkijkuit@outlook.com",
about: "Softwareontwikkelaar met passie voor praktische oplossingen. Ervaring met Java, Python, IoT en moderne webtechnologieën. Momenteel student Technische Informatica aan Hogeschool Rotterdam.",
occupation: "Student Technische Informatica",
birthDate: new Date("2006-06-27"),
skills: [
experience: [
{
name: "C++ / Arduino",
level: 7
},
{
name: "Java",
level: 6
},
{
name: "Python",
level: 8
},
{
name: "Web Development",
level: 5
name: "Brightpark",
role: "Stagaire Junior Developer",
description: "Bij Brightpark heb ik mijn eerste ervaring opgedaan in het werkveld van Software Ontwikkeling. Ik heb hier gewerkt aan meerdere projecten. Een van de leukere was een iOS app die helpt bij het meten van huizen door gebruik te maken van de Bosch GLM- series meetapperatuur. Deze app heb ik gemaakt in Flutter, met wat kleine stappen Swift in om de meetapperatuur werkend te krijgen.",
start: new Date("2024-09-10"),
end: new Date("2025-05-1"),
tech: ["Flutter", "Swift", "Svelte", "Laravel"],
color: "#30e7ffe9"
}
],
/*
Online Presence
*/
socials: [
{ icon: "mdi:git", platform: "Gitea", url: "https://git.herpiederpiee.nl/valentijn?tab=repositories" },
{ icon: "mdi:linkedin", platform: "LinkedIn", url: "https://nl.linkedin.com/in/valentijn-van-der-jagt" },
{ icon: "mdi:document", platform: "CV", url: "/CV.pdf"}
],
projects: [
{
title: "Albert Heijn Bonus Scraper",
description: "Een Java-applicatie die via Puppeteer real-time bonusacties scrapt en visualiseert. Bevat een krachtige zoekfunctionaliteit ondersteund door een zelfgeïmplementeerd fuzzy-search algoritme, waardoor er snel en flexibel door de weekelijkse aanbiedingen genavigeerd kan worden.",
tech: ["Java", "Puppeteer", "HTML/CSS"],
link: "https://git.herpiederpiee.nl/valentijn/Appie-Bonus-Scraper/"
},
{
title: "Spotify Windows Integratie ",
description: "Een Python-applicatie die via de Spotify Web API real-time playback-data synchroniseert met een custom Windows Taskbar-widget. Maakt gebruik van Tkinter voor een hardware-versnelde, transparante overlay die naadloos integreert met de Windows Shell-omgeving",
tech: ["Python", "Tkinter", "Spotify API"],
link: "https://git.herpiederpiee.nl/valentijn/Windows-Spotify-Taskbar/"
},
],
/*
Academic Path
*/
studies: [
{
school: "Hogeschool Rotterdam",
@@ -41,3 +67,18 @@ export const siteData = {
}
]
}
export const techColors = {
"Default": "#888888",
"Java": "#ED8B00",
"Python": "#3776AB",
"HTML/CSS": "#E34F26",
"Puppeteer": "#40B5A4",
"Tkinter": "#3e759e",
"Spotify API": "#1DB954",
"C++": "#00599C",
"Arduino": "#00979D",
"Flutter": "#02569B",
"Svelte": "#FF3E00",
"Laravel": "#FF2D20",
};

View File

@@ -12,6 +12,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<slot />

View File

@@ -2,10 +2,16 @@
import Layout from '../layouts/Layout.astro';
import Hero from '../components/Hero/Page.astro';
import Studies from "../components/Studies/Page.astro"
import Projects from "../components/Projects/Page.astro"
import Experience from "../components/Experience/Page.astro"
import Contact from "../components/Contact/Page.astro"
---
<Layout>
<Hero />
<Studies/>
<Projects/>
<Experience/>
<!-- <Studies/> -->
<Contact/>
</Layout>

View File

@@ -1,23 +0,0 @@
import { siteData } from '../data/site';
type Study = {
school: string;
study: string;
label: string;
start: Date;
end: Date | null;
};
function getStudy(studies: Study[]){
if (studies.length === 0) return null;
return studies.reduce((latest, study) => {
if (latest.end === null) return latest;
if (study.end === null) return study;
return study.start.getTime() > latest.start.getTime() ? study : latest;
});
}
export const study = getStudy(siteData.studies)