added basic cv page
continuous-integration/drone/push Build is failing Details

pull/2/head
Guus van Meerveld 9 months ago
parent 59a3056c0b
commit 5493f0c140

@ -0,0 +1,56 @@
{
"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": "VWO (NT&G)",
"timeFrame": "2016 - 2022",
"institution": "RSG Pantarijn MHV Wageningen",
"location": "Wageningen, Netherlands",
"skills": ["Biology", "Physics", "Mathematics"]
}
],
"experience": []
}

@ -24,6 +24,7 @@
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"framer-motion": "^11.0.8", "framer-motion": "^11.0.8",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"humanize-duration": "^3.31.0",
"iron-session": "^6.3.1", "iron-session": "^6.3.1",
"next": "^14.1.1", "next": "^14.1.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
@ -39,6 +40,7 @@
"@trivago/prettier-plugin-sort-imports": "^4.0.0", "@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/humanize-duration": "^3.27.4",
"@types/node": "^15.12.1", "@types/node": "^15.12.1",
"@types/react": "^18.2.64", "@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21", "@types/react-dom": "^18.2.21",

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@ -2,10 +2,10 @@
import { Button } from "@nextui-org/button"; import { Button } from "@nextui-org/button";
import { Image } from "@nextui-org/image"; import { Image } from "@nextui-org/image";
import { Spacer } from "@nextui-org/react"; import { Link } from "@nextui-org/link";
import { Spacer } from "@nextui-org/spacer";
import { Tooltip } from "@nextui-org/tooltip"; import { Tooltip } from "@nextui-org/tooltip";
import { Component } from "@typings/component"; import { Component } from "@typings/component";
import Link from "next/link";
import { useMemo } from "react"; import { useMemo } from "react";
import { FiGithub, FiMail, FiLinkedin } from "react-icons/fi"; import { FiGithub, FiMail, FiLinkedin } from "react-icons/fi";
@ -57,23 +57,19 @@ export const Header: Component<{ data: HeaderProps; avatar: string }> = ({
<Spacer y={4} /> <Spacer y={4} />
{socials.map((social) => ( {socials.map((social) => (
<Tooltip <Link isExternal href={social.link} key={social.name.toLowerCase()}>
key={social.name.toLowerCase()} <Tooltip showArrow content={social.name}>
showArrow <Button
content={social.name} className="text-2xl mr-4"
> color="primary"
<Button variant="shadow"
href={social.link} isIconOnly
as={Link} aria-label={social.name}
className="text-2xl mr-4" >
color="primary" {social.icon}
variant="shadow" </Button>
isIconOnly </Tooltip>
aria-label={social.name} </Link>
>
{social.icon}
</Button>
</Tooltip>
))} ))}
</div> </div>
</div> </div>

@ -0,0 +1,136 @@
"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 CvProps, {
Education as EducationProps,
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>
);
};
export const Cv: Component<{ data: CvProps }> = ({ data }) => {
return (
<div className="container mx-auto min-h-screen py-8">
<div className="flex items-center">
<Image
alt={`Professional picture of ${data.fullName}`}
as={NextImage}
width={200}
height={200}
src="/cv.jpg"
/>
<Spacer x={8} />
<div className="w-full flex justify-between">
<div>
<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.map((skill) => (
<Skill skill={skill} key={skill.name.toLowerCase()} />
))}
<Spacer y={8} />
<h1 className="text-2xl">Programming Languages</h1>
{data.programmingLanguages.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">Education</h1>
{data.education.map((education) => {
return (
<>
<Spacer y={4} />
<Education
education={education}
key={education.title.toLowerCase()}
/>
</>
);
})}
</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;

@ -0,0 +1,46 @@
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 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,16 @@
import { readJson } from "fs-extra";
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;
};

@ -2,33 +2,17 @@ import { readFile, readJson } from "fs-extra";
import path from "path"; import path from "path";
import { avatarFileFormat } from "./constants"; import { avatarFileFormat } from "./constants";
import { readAndParseJsonFile } from "./json";
import { cache } from "react"; import { cache } from "react";
import Landing, { LandingModel } from "@models/landing"; import Landing, { LandingModel } from "@models/landing";
import exists from "@utils/fileExists";
export const readLandingJson = cache( export const readLandingJson = cache(
async (dataDirLocation: string): Promise<Landing> => { async (dataDirLocation: string): Promise<Landing> => {
const landingJsonLocation = path.join(dataDirLocation, "landing.json"); const landingJsonLocation = path.join(dataDirLocation, "landing.json");
const fileExists = await exists(landingJsonLocation); return await readAndParseJsonFile(landingJsonLocation, LandingModel);
if (!fileExists) {
throw new Error(
`Could not find landing json file at: ${landingJsonLocation}`
);
}
const rawJson: unknown = await readJson(landingJsonLocation);
const landingResult = LandingModel.safeParse(rawJson);
if (!landingResult.success)
throw new Error(`Failed to parse landing json: ${landingResult.error}`);
return landingResult.data;
} }
); );

@ -2199,6 +2199,11 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
"@types/humanize-duration@^3.27.4":
version "3.27.4"
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.4.tgz#51d6d278213374735440bc3749de920935e9127e"
integrity sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==
"@types/json5@^0.0.29": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -3732,6 +3737,11 @@ https-proxy-agent@^5.0.0:
agent-base "6" agent-base "6"
debug "4" debug "4"
humanize-duration@^3.31.0:
version "3.31.0"
resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.31.0.tgz#a0384d22555024cd17e6e9f8561540d37756bf4c"
integrity sha512-fRrehgBG26NNZysRlTq1S+HPtDpp3u+Jzdc/d5A4cEzOD86YLAkDaJyJg8krSdCi7CJ+s7ht3fwRj8Dl+Btd0w==
ieee754@^1.2.1: ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"

Loading…
Cancel
Save