diff --git a/data/cv.json b/data/cv.json new file mode 100644 index 0000000..7fbefdb --- /dev/null +++ b/data/cv.json @@ -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": [] +} diff --git a/package.json b/package.json index 587b9e0..983d39e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "bcrypt": "^5.1.0", "framer-motion": "^11.0.8", "fs-extra": "^11.2.0", + "humanize-duration": "^3.31.0", "iron-session": "^6.3.1", "next": "^14.1.1", "next-themes": "^0.2.1", @@ -39,6 +40,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.0.0", "@types/bcrypt": "^5.0.0", "@types/fs-extra": "^11.0.4", + "@types/humanize-duration": "^3.27.4", "@types/node": "^15.12.1", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", diff --git a/public/cv.jpg b/public/cv.jpg new file mode 100644 index 0000000..89ae870 Binary files /dev/null and b/public/cv.jpg differ diff --git a/src/app/(landing)/Header.tsx b/src/app/(landing)/Header.tsx index 7740c86..ef63fa3 100644 --- a/src/app/(landing)/Header.tsx +++ b/src/app/(landing)/Header.tsx @@ -2,10 +2,10 @@ import { Button } from "@nextui-org/button"; 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 { Component } from "@typings/component"; -import Link from "next/link"; import { useMemo } from "react"; import { FiGithub, FiMail, FiLinkedin } from "react-icons/fi"; @@ -57,23 +57,19 @@ export const Header: Component<{ data: HeaderProps; avatar: string }> = ({ {socials.map((social) => ( - - - + + + + + ))} diff --git a/src/app/cv/Cv.tsx b/src/app/cv/Cv.tsx new file mode 100644 index 0000000..2161248 --- /dev/null +++ b/src/app/cv/Cv.tsx @@ -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 ( +
+ +
+ ); +}; + +const Education: Component<{ education: EducationProps }> = ({ education }) => { + return ( + + +
+
+

{education.title}

+

+ {education.timeFrame} +

+
+
+

{education.institution}

+

+ {education.location} +

+
+
+ + + + + {education.skills.map((skill) => ( + {skill} + ))} + +
+
+ ); +}; + +export const Cv: Component<{ data: CvProps }> = ({ data }) => { + return ( +
+
+ {`Professional + + + +
+
+

{data.fullName}

+ +

{data.role}

+
+
+
+ + + +

Professional profile

+

{data.description}

+ + + +
+
+

Skills

+ + {data.skills.map((skill) => ( + + ))} + + + +

Programming Languages

+ + {data.programmingLanguages.map((skill) => ( + + ))} + +
+ +
+

Education

+ + {data.education.map((education) => { + return ( + <> + + + + ); + })} +
+
+
+ ); +}; diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx new file mode 100644 index 0000000..ebaaed5 --- /dev/null +++ b/src/app/cv/page.tsx @@ -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 ; +} + +export const revalidate = 3600; diff --git a/src/models/cv.ts b/src/models/cv.ts new file mode 100644 index 0000000..a7d05a3 --- /dev/null +++ b/src/models/cv.ts @@ -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; + +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; + +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; + +export default CvProps; diff --git a/src/utils/cv.ts b/src/utils/cv.ts new file mode 100644 index 0000000..266bc5a --- /dev/null +++ b/src/utils/cv.ts @@ -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 => { + const cvJsonLocation = path.join(dataDirLocation, "cv.json"); + + return await readAndParseJsonFile(cvJsonLocation, CvPropsModel); + } +); diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 0000000..85e52d1 --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,23 @@ +import { readJson } from "fs-extra"; +import z from "zod"; + +import exists from "@utils/fileExists"; + +export const readAndParseJsonFile = async ( + location: string, + model: z.ZodType +): Promise => { + 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; +}; diff --git a/src/utils/landing.ts b/src/utils/landing.ts index 45478b7..debd293 100644 --- a/src/utils/landing.ts +++ b/src/utils/landing.ts @@ -2,33 +2,17 @@ import { readFile, readJson } from "fs-extra"; import path from "path"; import { avatarFileFormat } from "./constants"; +import { readAndParseJsonFile } from "./json"; import { cache } from "react"; import Landing, { LandingModel } from "@models/landing"; -import exists from "@utils/fileExists"; - export const readLandingJson = cache( async (dataDirLocation: string): Promise => { const landingJsonLocation = path.join(dataDirLocation, "landing.json"); - const fileExists = await exists(landingJsonLocation); - - 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; + return await readAndParseJsonFile(landingJsonLocation, LandingModel); } ); diff --git a/yarn.lock b/yarn.lock index 9b97a51..750bce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2199,6 +2199,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" 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": version "0.0.29" 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" 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: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"