Merge pull request 'nextui' (#2) from nextui into main
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
Reviewed-on: #2main
commit
300c7048f2
@ -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