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 (
+
+
+
+
+
+
+
+
+
{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"