@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"fullName": "Guus van Meerveld",
|
||||||
|
"role": "Computer programmer",
|
||||||
|
"description": "As a computer programmer working with precision and being methodical are of incredible importance, as a simple minor oversight can change people's lives. This is exactly the way I prefer to work. Methodically approaching a problem, taking it apart step by step and finding an optimal, yet creative solution.",
|
||||||
|
"contact": {
|
||||||
|
"website": "https://guusvanmeerveld.dev",
|
||||||
|
"email": "contact@guusvanmeerveld.dev",
|
||||||
|
"linkedIn": "https://linkedin.com/in/guus-van-meerveld-038357210",
|
||||||
|
"git": "https://github.com/guusvanmeerveld"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "DevOps",
|
||||||
|
"year": "1/1/2022",
|
||||||
|
"value": 0.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linux",
|
||||||
|
"year": "1/1/2020",
|
||||||
|
"value": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Web development",
|
||||||
|
"year": "1/1/2017",
|
||||||
|
"value": 0.7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"programmingLanguages": [
|
||||||
|
{
|
||||||
|
"name": "Rust",
|
||||||
|
"year": "1/1/2022",
|
||||||
|
"value": 0.8
|
||||||
|
},
|
||||||
|
{ "name": "Typescript & Javascript", "value": 0.9, "year": "1/1/2017" },
|
||||||
|
{ "name": "Java", "value": 0.6, "year": "1/8/2022" },
|
||||||
|
{ "name": "Python", "value": 0.6, "year": "1/2/2023" },
|
||||||
|
{ "name": "Scala", "value": 0.5, "year": "1/8/2023" }
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"title": "Bachelor Artificial Intelligence",
|
||||||
|
"timeFrame": "2022 - Present",
|
||||||
|
"institution": "Radboud University",
|
||||||
|
"location": "Nijmegen, Netherlands",
|
||||||
|
"skills": ["Mathematics", "Neuroscience", "Computer science"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Highschool VWO (NT&G)",
|
||||||
|
"timeFrame": "2016 - 2022",
|
||||||
|
"institution": "RSG Pantarijn MHV Wageningen",
|
||||||
|
"location": "Wageningen, Netherlands",
|
||||||
|
"skills": ["Biology", "Physics", "Mathematics"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"title": "Albert Heijn",
|
||||||
|
"timeFrame": "2020 - 2023",
|
||||||
|
"role": "Store employee",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,181 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardBody } from "@nextui-org/card";
|
||||||
|
import { Divider } from "@nextui-org/divider";
|
||||||
|
import { Image } from "@nextui-org/image";
|
||||||
|
import { Listbox, ListboxItem } from "@nextui-org/listbox";
|
||||||
|
import { Progress } from "@nextui-org/progress";
|
||||||
|
import { Spacer } from "@nextui-org/spacer";
|
||||||
|
import { Component } from "@typings/component";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import NextImage from "next/image";
|
||||||
|
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import CvProps, {
|
||||||
|
Education as EducationProps,
|
||||||
|
Experience as ExperienceProps,
|
||||||
|
Skill as SkillProps
|
||||||
|
} from "@models/cv";
|
||||||
|
|
||||||
|
const Skill: Component<{ skill: SkillProps }> = ({ skill }) => {
|
||||||
|
const duration = new Date().getTime() - skill.year.getTime();
|
||||||
|
|
||||||
|
const durationInYears = humanizeDuration(duration, {
|
||||||
|
units: ["y"],
|
||||||
|
round: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between my-4">
|
||||||
|
<Progress
|
||||||
|
label={skill.name}
|
||||||
|
aria-label={`Value for ${skill.name}`}
|
||||||
|
valueLabel={durationInYears}
|
||||||
|
showValueLabel
|
||||||
|
value={skill.value * 100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Education: Component<{ education: EducationProps }> = ({ education }) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1 className="text-md">{education.title}</h1>
|
||||||
|
<h2 className="text-small text-default-500">
|
||||||
|
{education.timeFrame}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col text-right">
|
||||||
|
<h1 className="text-md">{education.institution}</h1>
|
||||||
|
<h2 className="text-small text-default-500">
|
||||||
|
{education.location}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<Listbox aria-label="Actions">
|
||||||
|
{education.skills.map((skill) => (
|
||||||
|
<ListboxItem key={skill.toLowerCase()}>{skill}</ListboxItem>
|
||||||
|
))}
|
||||||
|
</Listbox>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Experience: Component<{ experience: ExperienceProps }> = ({
|
||||||
|
experience
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1 className="text-md">
|
||||||
|
{experience.role} <span className="text-default-500">at</span>{" "}
|
||||||
|
{experience.title}
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-small text-default-500">
|
||||||
|
{experience.timeFrame}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Cv: Component<{ data: CvProps }> = ({ data }) => {
|
||||||
|
return (
|
||||||
|
<div className="px-2 container mx-auto min-h-screen py-8">
|
||||||
|
<div className="md:flex items-center">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Image
|
||||||
|
alt={`Professional picture of ${data.fullName}`}
|
||||||
|
as={NextImage}
|
||||||
|
className="mx-auto md:mx-0"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
src="/cv.jpg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer x={8} />
|
||||||
|
|
||||||
|
<div className="text-center md:text-left flex justify-between">
|
||||||
|
<div className="w-full">
|
||||||
|
<h1 className="text-4xl">{data.fullName}</h1>
|
||||||
|
<Spacer y={4} />
|
||||||
|
<h1 className="text-2xl text-default-600">{data.role}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={8} />
|
||||||
|
|
||||||
|
<h2 className="text-xl">Professional profile</h2>
|
||||||
|
<h3 className="text-md text-default-600">{data.description}</h3>
|
||||||
|
|
||||||
|
<Spacer y={8} />
|
||||||
|
|
||||||
|
<div className="lg:flex">
|
||||||
|
<div className="w-full lg:w-1/3">
|
||||||
|
<h1 className="text-2xl">Skills</h1>
|
||||||
|
|
||||||
|
{data.skills
|
||||||
|
.sort((a, b) => b.value - a.value)
|
||||||
|
.map((skill) => (
|
||||||
|
<Skill skill={skill} key={skill.name.toLowerCase()} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Spacer y={8} />
|
||||||
|
|
||||||
|
<h1 className="text-2xl">Programming Languages</h1>
|
||||||
|
|
||||||
|
{data.programmingLanguages
|
||||||
|
.sort((a, b) => b.value - a.value)
|
||||||
|
.map((skill) => (
|
||||||
|
<Skill skill={skill} key={skill.name.toLowerCase()} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Spacer y={8} />
|
||||||
|
</div>
|
||||||
|
<Spacer x={8} />
|
||||||
|
<div className="w-full lg:w-2/3">
|
||||||
|
<h1 className="text-2xl">Experience</h1>
|
||||||
|
|
||||||
|
{data.experience.map((experience) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={experience.title.toLowerCase()}>
|
||||||
|
<Spacer y={4} />
|
||||||
|
<Experience experience={experience} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Spacer y={8} />
|
||||||
|
|
||||||
|
<h1 className="text-2xl">Education</h1>
|
||||||
|
|
||||||
|
{data.education.map((education) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={education.title.toLowerCase()}>
|
||||||
|
<Spacer y={4} />
|
||||||
|
<Education education={education} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Cv } from "./Cv";
|
||||||
|
|
||||||
|
import { dataDirLocation } from "@utils/constants";
|
||||||
|
import { readCvJson } from "@utils/cv";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const cv = await readCvJson(dataDirLocation);
|
||||||
|
|
||||||
|
return <Cv data={cv} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revalidate = 3600;
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,48 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const SkillModel = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
year: z.coerce.date(),
|
||||||
|
value: z.number().min(0).max(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Skill = z.infer<typeof SkillModel>;
|
||||||
|
|
||||||
|
const EducationModel = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
timeFrame: z.string(),
|
||||||
|
institution: z.string(),
|
||||||
|
location: z.string(),
|
||||||
|
skills: z.string().array()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Education = z.infer<typeof EducationModel>;
|
||||||
|
|
||||||
|
const ExperienceModel = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
timeFrame: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
description: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Experience = z.infer<typeof ExperienceModel>;
|
||||||
|
|
||||||
|
export const CvPropsModel = z.object({
|
||||||
|
fullName: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
contact: z.object({
|
||||||
|
website: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
linkedIn: z.string().url(),
|
||||||
|
git: z.string().url()
|
||||||
|
}),
|
||||||
|
skills: SkillModel.array(),
|
||||||
|
programmingLanguages: SkillModel.array(),
|
||||||
|
education: EducationModel.array(),
|
||||||
|
experience: ExperienceModel.array()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CvProps = z.infer<typeof CvPropsModel>;
|
||||||
|
|
||||||
|
export default CvProps;
|
@ -0,0 +1,15 @@
|
|||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import { readAndParseJsonFile } from "./json";
|
||||||
|
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
|
import CvProps, { CvPropsModel } from "@models/cv";
|
||||||
|
|
||||||
|
export const readCvJson = cache(
|
||||||
|
async (dataDirLocation: string): Promise<CvProps> => {
|
||||||
|
const cvJsonLocation = path.join(dataDirLocation, "cv.json");
|
||||||
|
|
||||||
|
return await readAndParseJsonFile(cvJsonLocation, CvPropsModel);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,23 @@
|
|||||||
|
import { readJson } from "fs-extra";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import exists from "@utils/fileExists";
|
||||||
|
|
||||||
|
export const readAndParseJsonFile = async <T>(
|
||||||
|
location: string,
|
||||||
|
model: z.ZodType<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
const fileExists = await exists(location);
|
||||||
|
|
||||||
|
if (!fileExists) {
|
||||||
|
throw new Error(`Could not find json file at: ${location}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawJson: unknown = await readJson(location);
|
||||||
|
|
||||||
|
const result = model.safeParse(rawJson);
|
||||||
|
|
||||||
|
if (!result.success) throw new Error(`Failed to parse json: ${result.error}`);
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
};
|
Loading…
Reference in new issue