Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 7ea939c0d2
Bump eslint-plugin-react from 7.24.0 to 7.32.2
1 year ago

@ -1,13 +1,8 @@
**
!.env
!next.config.js
!tailwind.config.js
!postcss.config.js
!tsconfig.json
!package.json
!yarn.lock
!public
!prisma
!src
!data
!src

@ -1,74 +0,0 @@
kind: pipeline
type: docker
name: test
platform:
os: linux
arch: amd64
steps:
- name: Test the newest commit
image: node:lts-alpine
volumes:
- name: cache
path: /drone/src/node_modules
commands:
- yarn install
- yarn run prisma:generate
- yarn lint
- yarn test-build
volumes:
- name: cache
host:
path: /tmp/drone/cache/node_modules
---
kind: pipeline
type: docker
name: build-linux-amd64
platform:
os: linux
arch: amd64
steps:
- name: Build Dockerfile and push to Dockerhub
image: plugins/docker
settings:
repo: guusvanmeerveld/portfolio
tags:
- latest
- latest-amd64
platforms: linux/amd64
username:
from_secret: docker_username
password:
from_secret: docker_password
depends_on:
- test
---
kind: pipeline
type: docker
name: build-linux-arm64
platform:
os: linux
arch: arm64
steps:
- name: Build Dockerfile and push to Dockerhub
image: plugins/docker
settings:
repo: guusvanmeerveld/portfolio
tags: latest-arm64
platforms: linux/arm64
username:
from_secret: docker_username
password:
from_secret: docker_password
depends_on:
- test

@ -1,2 +1 @@
DATABASE_URL=postgresql://portfolio:portfolio@localhost:5432/portfolio?schema=public
DATA_DIR=./data
NEXT_PUBLIC_GITHUB_USERNAME=Guusvanmeerveld

@ -1,3 +1,67 @@
{
"extends": "next/core-web-vitals"
}
"root": true,
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 8
},
"plugins": [
"prettier",
"css-modules"
],
"ignorePatterns": [
"node_modules/*",
".next/*",
".out/*"
],
"extends": [
"eslint:recommended",
"plugin:css-modules/recommended"
],
"overrides": [
{
"files": [
"**/*.ts",
"**/*.tsx"
],
"parser": "@typescript-eslint/parser",
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"rules": {
"prettier/prettier": "error",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/no-autofocus": "off",
"@typescript-eslint/no-unused-vars": [
"error"
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true,
"allowConciseArrowFunctionExpressionsStartingWithVoid": true,
"allowTypedFunctionExpressions": true
}
]
}
}
]
}

39
.gitignore vendored

@ -1,37 +1,4 @@
# compiled output
/dist
/node_modules
/.next
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
node_modules
.env.local
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.next
yarn-error.log

@ -1,26 +1,7 @@
{
"trailingComma": "none",
"useTabs": true,
"semi": true,
"printWidth": 80,
"arrowParens": "always",
"importOrderSeparation": true,
"importOrder": [
"^..?/.*",
"^react.*",
"^axios.*",
"@prisma/.*",
"^@src/.*",
"^@models/.*",
"^@interfaces/.*",
"^@styles/.*",
"^@shared/.*",
"^@utils/.*",
"^@components/.*",
"^@cache/.*",
".*scss$",
".*css$",
"^@svg/.*"
],
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
"trailingComma": "none",
"useTabs": true,
"semi": true,
"printWidth": 80,
"arrowParens": "always"
}

@ -0,0 +1,9 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-idiomatic-order"
],
"rules": {
"indentation": "tab"
}
}

@ -1,18 +1,17 @@
FROM node:lts-alpine AS deps
FROM node:alpine AS deps
WORKDIR /app
COPY package.json yarn.lock prisma ./
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
FROM node:lts-alpine AS builder
FROM node:alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
ENV NEXT_TELEMETRY_DISABLED 1;
RUN yarn run prisma:generate
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
FROM node:lts-alpine AS runner
FROM node:alpine AS runner
WORKDIR /app
ENV NODE_ENV production
@ -21,16 +20,13 @@ RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/postcss.config.js ./
COPY --from=builder /app/tailwind.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma
USER nextjs
EXPOSE 3000
CMD ["yarn", "run", "start:migrate"]
CMD ["yarn", "start"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

@ -1,63 +0,0 @@
{
"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": ""
}
]
}

@ -1,49 +0,0 @@
{
"header": {
"fullName": "Guus van Meerveld",
"name": "guus",
"description": "AI student at Radboud University. Creating software as a hobby.",
"contact": {
"email": "contact@guusvanmeerveld.dev",
"git": "https://github.com/Guusvanmeerveld",
"linkedin": "https://linkedin.com/in/guus-van-meerveld-038357210"
}
},
"projects": [
{
"name": "Dust-Mail",
"avatarUrl": "https://avatars.githubusercontent.com/u/130915639?s=200&v=4",
"description": "Dust-Mail is a free and open source project that aims to replace all desktop and web email clients by providing a fast and simple experience.",
"url": "https://github.com/Dust-Mail/"
},
{
"name": "Argo-Client",
"avatarUrl": "https://avatars.githubusercontent.com/u/71986232?s=200&v=4",
"description": "Argo is a modern client for Magister 6 that is available for Android and IOS.",
"url": "https://argo-magister.nl"
},
{
"name": "MaterialTube",
"avatarUrl": "https://raw.githubusercontent.com/Guusvanmeerveld/MaterialTube/master/src/svg/logo.svg",
"description": "MaterialTube is a beautiful and elegant web client for Invidious servers, built using Next.js and MUI.",
"url": "https://github.com/Guusvanmeerveld/MaterialTube"
}
],
"footer": {
"columns": [
{
"title": "Built with",
"links": [
{ "url": "https://nextjs.org/", "text": "Next.js" },
{ "url": "https://nextui.org/", "text": "NextUI" }
]
},
{
"title": "Social",
"links": [
{ "url": "https://github.com/Guusvanmeerveld", "text": "Github" }
]
}
]
}
}

@ -3,25 +3,6 @@ version: "3"
services:
app:
build: .
container_name: portfolio
volumes:
- ./data:/app/data
environment:
DATABASE_URL: "postgres://portfolio:portfolio@db:5432/portfolio"
container_name: app
ports:
- 3000:3000
db:
image: postgres:14
container_name: portfolio-db
environment:
POSTGRES_USER: "portfolio"
POSTGRES_PASSWORD: "portfolio"
POSTGRES_DB: "portfolio"
ports:
- 5432:5432
volumes:
- db:/var/lib/postgresql/data
volumes:
db:

@ -1,56 +0,0 @@
{
"nodes": {
"flake-compat": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

@ -1,37 +0,0 @@
{
description = "Portfolio website";
inputs = {
systems.url = "github:nix-systems/default";
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
};
outputs =
{ systems
, nixpkgs
, ...
}:
let
eachSystem = f:
nixpkgs.lib.genAttrs (import systems) (
system:
f nixpkgs.legacyPackages.${system}
);
in
{
devShells = eachSystem
(pkgs: {
default = pkgs.mkShell
{
buildInputs = with pkgs; [
nodejs_20
yarn
nodePackages.typescript
nodePackages.typescript-language-server
];
};
});
};
}

@ -1,7 +1,7 @@
// @ts-check
/**
* @type {import('next').NextConfig}
* @type {import('next/dist/next-server/server/config').NextConfig}
**/
module.exports = {
reactStrictMode: true,
};
reactStrictMode: true,
}

@ -1,55 +1,42 @@
{
"name": "portfolio-website",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"prisma:generate": "prisma generate",
"start:migrate": "prisma migrate deploy && yarn run start",
"export": "next build && next export",
"format": "prettier src --write",
"test-build": "tsc",
"lint": "next lint",
"lint": "eslint src",
"stylelint": "npx stylelint **/*.scss",
"full-test": "yarn test-build && yarn lint && yarn stylelint"
},
"dependencies": {
"@nextui-org/react": "^2.2.10",
"@prisma/client": "4.10.1",
"@tanstack/react-query": "^4.24.9",
"autoprefixer": "^10.4.18",
"axios": "^1.1.2",
"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",
"configcat-node": "^8.0.0",
"next": "^12.1.0",
"next-seo": "^4.24.0",
"next-themes": "^0.2.1",
"postcss": "^8.4.35",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"sharp": "^0.33.2",
"tailwindcss": "^3.4.1",
"timeago.js": "^4.0.2",
"zod": "^3.20.6"
"react": "^17.0.2",
"react-dom": "^17.0.2",
"spectre.css": "^0.5.9"
},
"devDependencies": {
"@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",
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"eslint": "^7.28.0",
"eslint-config-next": "13.1.6",
"eslint-plugin-css-modules": "^2.11.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.3.1",
"prisma": "^5.8.1",
"sass": "^1.34.1",
"stylelint": "^13.13.1",
"stylelint-config-idiomatic-order": "^8.1.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.3.2"
}
}

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

@ -1,25 +0,0 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Post" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "tags" TEXT[];
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL;

@ -1,9 +0,0 @@
/*
Warnings:
- Made the column `name` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "admin" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "name" SET NOT NULL;

@ -1,12 +0,0 @@
-- CreateTable
CREATE TABLE "Link" (
"id" SERIAL NOT NULL,
"remoteAddress" TEXT NOT NULL,
"location" TEXT NOT NULL,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Link_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Link" ADD CONSTRAINT "Link_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

@ -1,42 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
admin Boolean @default(false)
password String
name String
posts Post[]
links Link[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
tags String[]
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Link {
id Int @id @default(autoincrement())
remoteAddress String
location String
author User @relation(fields: [authorId], references: [id])
authorId Int
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,10 +0,0 @@
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
fetchTarball {
url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; }
).shellNix

@ -1,97 +0,0 @@
"use client";
import { Button } from "@nextui-org/button";
import { Image } from "@nextui-org/image";
import { Link } from "@nextui-org/link";
import { Spacer } from "@nextui-org/spacer";
import { Tooltip } from "@nextui-org/tooltip";
import { Component } from "@typings/component";
import NextLink from "next/link";
import { useMemo } from "react";
import { FiGithub, FiMail, FiLinkedin, FiFileText } from "react-icons/fi";
import HeaderProps from "@models/header";
interface Social {
link: string;
name: string;
icon: React.ReactElement;
isExternal?: boolean;
}
export const Header: Component<{ data: HeaderProps; avatar: string }> = ({
data,
avatar
}) => {
const socials = useMemo<Social[]>(
() => [
{
link: `mailto:${data.contact.email}`,
name: "Email address",
icon: <FiMail />
},
{
link: data.contact.git,
name: "Github",
icon: <FiGithub />
},
{
link: data.contact.linkedin,
name: "LinkedIn",
icon: <FiLinkedin />
},
{
link: "/cv",
name: "Cv",
icon: <FiFileText />,
isExternal: false
}
],
[data.contact]
);
return (
<div className="container mx-auto flex items-center min-h-screen">
<div className="flex items-center">
<Image
isBlurred
src={avatar}
width={300}
alt={`A picture of ${data.fullName}`}
/>
<Spacer x={8} />
<div>
<h1 className="text-4xl">{data.fullName}</h1>
<Spacer y={4} />
<h2 className="text-2xl">{data.description}</h2>
<Spacer y={4} />
{socials.map((social) => (
<Link
isExternal={social.isExternal ?? true}
as={NextLink}
href={social.link}
key={social.name.toLowerCase()}
>
<Tooltip showArrow content={social.name}>
<Button
className="text-2xl mr-4"
color="primary"
variant="shadow"
isIconOnly
aria-label={social.name}
>
{social.icon}
</Button>
</Tooltip>
</Link>
))}
</div>
</div>
</div>
);
};

@ -1,55 +0,0 @@
"use client";
import { Card, CardHeader, CardBody, CardFooter } from "@nextui-org/card";
import { Divider } from "@nextui-org/divider";
import { Image } from "@nextui-org/image";
import { Link } from "@nextui-org/link";
import { Spacer } from "@nextui-org/spacer";
import { Component } from "@typings/component";
import ProjectProps from "@models/project";
export const Projects: Component<{ data: ProjectProps[] }> = ({ data }) => {
return (
<>
<div className="container mx-auto p-4 min-h-96">
<h1 className="text-4xl text-center mb-8">Projects</h1>
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 ">
{data.map((project) => {
const url = new URL(project.url);
return (
<Card key={project.name}>
<CardHeader className="flex gap-3">
<Image
alt={`${project.name} logo`}
height={40}
radius="sm"
src={project.avatarUrl}
width={40}
/>
<div className="flex flex-col">
<p className="text-md">{project.name}</p>
<p className="text-small text-default-500">{url.host}</p>
</div>
</CardHeader>
<Divider />
<CardBody>
<p>{project.description}</p>
</CardBody>
<Divider />
<CardFooter>
<Link isExternal showAnchorIcon href={project.url}>
Visit the project.
</Link>
</CardFooter>
</Card>
);
})}
</div>
</div>
<Spacer y={24} />
</>
);
};

@ -1,22 +0,0 @@
import { Footer } from "../Footer";
import { Header } from "./Header";
import { Projects } from "./Projects";
import { dataDirLocation } from "@utils/constants";
import { readAvatarFile, readLandingJson } from "@utils/landing";
export default async function Page() {
// Any error will get handled by the `error.tsx` file.
const landing = await readLandingJson(dataDirLocation);
const avatar = await readAvatarFile(dataDirLocation);
return (
<>
<Header data={landing.header} avatar={avatar} />
<Projects data={landing.projects} />
<Footer data={landing.footer} />
</>
);
}
export const revalidate = 3600;

@ -1,45 +0,0 @@
"use client";
import { Divider } from "@nextui-org/divider";
import { Link } from "@nextui-org/link";
import { Component } from "@typings/component";
import { ThemeSwitcher } from "./ThemeSwitcher";
import FooterProps from "@models/footer";
export const Footer: Component<{ data: FooterProps }> = ({ data }) => {
return (
<div className="container mx-auto grid grid-flow-col justify-stretch my-4">
<div className="mx-4">
<h1 className="text-xl">
Created with{" "}
<span role="img" aria-label="Red Heart">
&#x2764;&#xfe0f;
</span>{" "}
by Guus van Meerveld
</h1>
<Divider className="my-4" />
<ThemeSwitcher />
</div>
{data.columns.map((column) => (
<div className="mx-4" key={column.title.toLowerCase()}>
<h1 className="text-xl">{column.title}</h1>
<Divider className="mt-4" />
{column.links.map((link) => (
<div className="my-4" key={link.url}>
<Link className="text-default-500" href={link.url}>
{link.text}
</Link>
</div>
))}
</div>
))}
</div>
);
};

@ -1,48 +0,0 @@
"use client";
import {
Button,
Link,
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem
} from "@nextui-org/react";
import { Component } from "@typings/component";
export const Nav: Component = () => {
return (
<Navbar position="sticky">
<NavbarBrand>
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link href="#" aria-current="page">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
};

@ -1,32 +0,0 @@
"use client";
import { Switch } from "@nextui-org/react";
import { Component } from "@typings/component";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { FiMoon, FiSun } from "react-icons/fi";
export const ThemeSwitcher: Component = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<Switch
defaultSelected
size="lg"
color="primary"
onValueChange={(value) => {
value ? setTheme("dark") : setTheme("light");
}}
startContent={<FiSun />}
endContent={<FiMoon />}
/>
);
};

@ -1,180 +0,0 @@
"use client";
import { Card, CardBody } from "@nextui-org/card";
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>
);
};

@ -1,12 +0,0 @@
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;

@ -1,28 +0,0 @@
"use client";
import { Link } from "@nextui-org/react";
import { ErrorPage } from "@typings/errorPage";
import { useEffect } from "react";
const MainErrorPage: ErrorPage = ({ error, reset }) => {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="container mx-auto flex items-center justify-center min-h-screen text-center">
<div>
<p className="text-3xl">Something went loading the page!</p>
<p className="text-xl">{error.toString()}</p>
<div>
<Link href="#" onClick={() => reset()}>
Try again
</Link>
</div>
</div>
</div>
);
};
export default MainErrorPage;

@ -1,30 +0,0 @@
import { Metadata } from "next";
import { Footer } from "./Footer";
import { Providers } from "./providers";
import "@styles/global.scss";
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
export const metadata: Metadata = {
title: {
default: "Portfolio",
template: "%s | Portfolio"
},
description: "Guus van Meerveld's portfolio",
applicationName: "Portfolio",
manifest: "/manifest.json"
};

@ -1,15 +0,0 @@
"use client";
import { NextUIProvider } from "@nextui-org/react";
import { Component } from "@typings/component";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export const Providers: Component = ({ children }) => {
return (
<NextUIProvider>
<NextThemesProvider attribute="class" defaultTheme="dark">
{children}
</NextThemesProvider>
</NextUIProvider>
);
};

@ -0,0 +1,40 @@
import Link from "next/link";
import { FC } from "react";
import { BestRepository } from "@interfaces/repository";
const BestRepository: FC<{ repository: BestRepository }> = ({ repository }) => {
return (
<div className="hero bg-primary">
<div className="container">
<div className="columns">
<div className="column col-8 col-md-12 col-mx-auto">
<h3 className="text-secondary">My most popular project:</h3>
<h1>{repository.name}</h1>
<h3 className="text-secondary">
{repository.stargazers_count} Star(s)
</h3>
<h5>{repository.description}</h5>
<p className="text-secondary">
Written in {repository.language}, has{" "}
{repository.open_issues_count} issue(s) and{" "}
{repository.forks_count} fork(s).
</p>
<Link href={repository.url}>
<a className="btn mr-2">Github</a>
</Link>
{repository.homepage && (
<Link href={repository.homepage}>
<a className="btn">Website</a>
</Link>
)}
</div>
</div>
</div>
</div>
);
};
export default BestRepository;

@ -0,0 +1,52 @@
import { FC } from "react";
import Link from "next/link";
import multipleClassNames from "@utils/multipleClassNames";
import styles from "./footer.module.scss";
const Footer: FC = () => {
return (
<footer className={multipleClassNames("container", styles.main)}>
<div className="columns">
<div className="column col-8 col-md-12 col-mx-auto">
<h3>Guus van Meerveld</h3>
<div className="columns mb-2">
<div className="column col-12">
<Link
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}`}
>
<a className="mr-2">Github</a>
</Link>
&middot;
<Link href="https://twitter.com/Guusvanmeerveld">
<a className="mx-2">Twitter</a>
</Link>
&middot;
<Link href="https://ko-fi.com/Guusvanmeerveld">
<a className="mx-2">Ko-fi</a>
</Link>
&middot;
<Link href="https://youtube.com/channel/UCYuqpoMay5SezCBrA_HKVWQ">
<a className="mx-2">Youtube</a>
</Link>
</div>
</div>
<p>
Built with{" "}
<span role="img" aria-label="heart emoji">
</span>{" "}
by Guus van Meerveld, using{" "}
<Link href="https://picturepan2.github.io/spectre">
<a>Spectre.css</a>
</Link>
</p>
</div>
</div>
</footer>
);
};
export default Footer;

@ -0,0 +1,46 @@
import { FC } from "react";
import styles from "./intro.module.scss";
const Intro: FC<{ isAvailable: boolean }> = ({ isAvailable }) => {
return (
<div className={styles.main}>
<div className="container">
<div className="columns">
<div className="column col-8 col-md-12 col-mx-auto text-center">
<h1>Guus van Meerveld</h1>
<h3>
Open source <u>web developer</u>
</h3>
<p>
<a
target="_blank"
rel="noreferrer"
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}`}
className="btn btn-primary mr-2"
>
Github
</a>
<a
href="mailto:contact@guusvanmeerveld.dev"
className="btn btn-primary"
>
Contact
</a>
</p>
<p className="text-gray">
Availibility: {isAvailable && "Available"}
{!isAvailable && "Not available"}
</p>
</div>
</div>
</div>
</div>
);
};
export default Intro;

@ -0,0 +1,16 @@
import { FC } from "react";
import Footer from "@components/Footer";
import ThemeChanger from "@components/ThemeChanger";
const Layout: FC = ({ children }) => {
return (
<>
<ThemeChanger />
{children}
<Footer />
</>
);
};
export default Layout;

@ -0,0 +1,66 @@
import { FC } from "react";
import Link from "next/link";
import { RecentRepository } from "@interfaces/repository";
import multipleClassNames from "@utils/multipleClassNames";
import styles from "./repositories.module.scss";
const RecentRepositories: FC<{ repositories: RecentRepository[] }> = ({
repositories
}) => {
return (
<div className={multipleClassNames("container", styles.main)}>
<div className="columns">
<div className="column col-6 col-mx-auto text-center">
<h3>Some of my recent projects:</h3>
</div>
</div>
<div className="columns">
<div className="column col-9 col-mx-auto">
<div className="columns">
{repositories.map((repository) => {
return (
<div
key={repository.name}
className="column col-3 col-md-12 col-mx-auto mb-2"
>
<div
className={multipleClassNames(
"card",
"text-center",
styles.card
)}
>
<div className="card-header text-primary">
<div className="card-title h5">{repository.name}</div>
<div className="card-subtitle text-gray">
{repository.stargazers_count} Star(s)
</div>
</div>
<div className="card-body">{repository.description}</div>
<div className="card-footer">
<Link href={repository.url}>
<a className="btn btn-primary">Github</a>
</Link>
{repository.homepage && (
<Link href={repository.homepage}>
<a className="btn btn-primary ml-2">Website</a>
</Link>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
};
export default RecentRepositories;

@ -0,0 +1,30 @@
import { FC, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import styles from "./themeChanger.module.scss";
const ThemeChanger: FC = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return <></>;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<a
role="button"
tabIndex={0}
className={styles.main}
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
>
{theme}
</a>
);
};
export default ThemeChanger;

@ -0,0 +1,6 @@
$margin: 1rem;
.main {
margin-top: $margin;
margin-bottom: $margin;
}

@ -0,0 +1,3 @@
.main {
padding-top: 10rem;
}

@ -0,0 +1,7 @@
.card {
border: 0;
}
.main {
margin-bottom: 3rem;
}

@ -0,0 +1,7 @@
.main {
position: absolute;
top: 1rem;
right: 1rem;
cursor: pointer;
}

@ -0,0 +1,124 @@
export interface GithubAPIRepository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner;
html_url: string;
description: null | string;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
created_at: Date;
updated_at: Date;
pushed_at: Date;
git_url: string;
ssh_url: string;
clone_url: string;
svn_url: string;
homepage: null | string;
size: number;
stargazers_count: number;
watchers_count: number;
language: null | string;
has_issues: boolean;
has_projects: boolean;
has_downloads: boolean;
has_wiki: boolean;
has_pages: boolean;
forks_count: number;
mirror_url: null;
archived: boolean;
disabled: boolean;
open_issues_count: number;
license: License | null;
allow_forking: boolean;
is_template: boolean;
web_commit_signoff_required: boolean;
topics: string[];
visibility: string;
forks: number;
open_issues: number;
watchers: number;
default_branch: string;
}
export interface License {
key: string;
name: string;
spdx_id: string;
url: string;
node_id: string;
}
export interface Owner {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface RecentRepository {
name: string;
description: string;
url: string;
homepage?: string;
stargazers_count: number;
}
export interface BestRepository extends RecentRepository {
forks_count: number;
language: string;
open_issues_count: number;
pushed_at: Date;
}

@ -1,48 +0,0 @@
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;

@ -1,14 +0,0 @@
import z from "zod";
const FooterColumnModel = z.object({
title: z.string(),
links: z.object({ url: z.string().url(), text: z.string() }).array()
});
export const FooterPropsModel = z.object({
columns: FooterColumnModel.array()
});
export type FooterProps = z.infer<typeof FooterPropsModel>;
export default FooterProps;

@ -1,16 +0,0 @@
import z from "zod";
export const HeaderPropsModel = z.object({
fullName: z.string(),
name: z.string(),
description: z.string(),
contact: z.object({
email: z.string().email(),
linkedin: z.string().url(),
git: z.string().url()
})
});
export type HeaderProps = z.infer<typeof HeaderPropsModel>;
export default HeaderProps;

@ -1,15 +0,0 @@
import z from "zod";
import { FooterPropsModel } from "./footer";
import { HeaderPropsModel } from "./header";
import { ProjectPropsModel } from "./project";
export const LandingModel = z.object({
header: HeaderPropsModel,
projects: ProjectPropsModel.array(),
footer: FooterPropsModel
});
export type Landing = z.infer<typeof LandingModel>;
export default Landing;

@ -1,12 +0,0 @@
import z from "zod";
export const ProjectPropsModel = z.object({
name: z.string(),
avatarUrl: z.string().url(),
description: z.string(),
url: z.string().url()
});
export type ProjectProps = z.infer<typeof ProjectPropsModel>;
export default ProjectProps;

@ -0,0 +1,9 @@
import type { DefaultSeoProps } from "next-seo";
const SEO: DefaultSeoProps = {
titleTemplate: "%s | Guus van Meerveld",
defaultTitle: "Guus van Meerveld",
description: "Guus van Meerveld's portfolio"
};
export default SEO;

@ -0,0 +1,8 @@
.main {
height: 100vh;
margin-bottom: 1rem;
display: flex;
justify-content: center;
align-items: center;
}

@ -0,0 +1,35 @@
import { NextPage } from "next";
import { useRouter } from "next/router";
import Layout from "@components/Layout";
import multipleClassNames from "@utils/multipleClassNames";
import styles from "./404.module.scss";
const NotFound: NextPage = () => {
const router = useRouter();
return (
<Layout>
<div className={multipleClassNames("empty", styles.main)}>
<div>
<div className="empty-icon">
<i className="icon icon-stop"></i>
</div>
<p className="empty-title h5">Page not found</p>
<p className="empty-subtitle">
The page has either been deleted or moved
</p>
<div className="empty-action">
<button onClick={() => router.back()} className="btn btn-primary">
Go back
</button>
</div>
</div>
</div>
</Layout>
);
};
export default NotFound;

@ -0,0 +1,19 @@
import "@styles/globals.scss";
import SEO from "../next-seo.config";
import { DefaultSeo } from "next-seo";
import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<DefaultSeo {...SEO} />
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</>
);
export default App;

@ -0,0 +1,75 @@
import { NextSeo } from "next-seo";
import { GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import axios from "axios";
import Intro from "@components/Intro";
import Layout from "@components/Layout";
import RecentRepositories from "@components/RecentRepositories";
import BestRepository from "@components/BestRepository";
import { GithubAPIRepository } from "@interfaces/repository";
import createConfigCatClient from "@utils/createConfigCatClient";
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get<GithubAPIRepository[]>(
`https://api.github.com/users/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}/repos`
);
const configCatClient = createConfigCatClient();
const isAvailable: boolean =
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
const bestRepository = data.sort(
(a, b) => b.stargazers_count - a.stargazers_count
)[0];
return {
props: {
isAvailable,
repositories: data
.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
.map(({ name, description, html_url, stargazers_count, homepage }) => ({
name,
url: html_url,
stargazers_count,
homepage,
description
}))
.slice(0, 3),
bestRepository: {
name: bestRepository.name,
description: bestRepository.description,
url: bestRepository.html_url,
homepage: bestRepository.homepage,
stargazers_count: bestRepository.stargazers_count,
forks_count: bestRepository.forks_count,
language: bestRepository.language,
open_issues_count: bestRepository.open_issues_count,
pushed_at: bestRepository.pushed_at
}
},
revalidate: 60 * 5
};
};
const Index: NextPage = ({
repositories,
isAvailable,
bestRepository
}: InferGetStaticPropsType<typeof getStaticProps>) => (
<Layout>
<NextSeo title="Home" />
<Intro isAvailable={isAvailable} />
<RecentRepositories repositories={repositories} />
<BestRepository repository={bestRepository} />
</Layout>
);
export default Index;

@ -0,0 +1,66 @@
[data-theme="dark"] {
body {
background-color: $bg-dark;
color: $text-primary;
}
.empty {
background-color: $dark-color-secondary;
color: $text-primary;
}
.card {
background-color: $bg-dark;
}
a:hover {
color: $primary-color-dark;
}
a {
color: $primary-color;
}
.text-primary {
color: $primary-color !important;
}
.bg-primary {
background-color: $primary-color !important;
}
.btn {
background-color: $bg-dark-secondary;
color: $primary-color;
border-color: $primary-color;
&:hover {
border-color: $primary-color;
}
&:focus {
border-color: $primary-color;
}
}
.btn-primary {
background-color: $primary-color;
border-color: $primary-color;
color: $text-primary;
&:hover {
background-color: $primary-color-dark;
border-color: $primary-color-dark;
}
&:focus {
background-color: $primary-color-dark;
border-color: $primary-color-dark;
}
}
}

@ -0,0 +1,13 @@
$light-color: #eee;
$dark-color: #212121;
$primary-color: #7e57c2;
$dark-color-secondary: lighten($dark-color, 2%);
$primary-color-dark: darken($primary-color, 10%);
$bg-dark: $dark-color;
$bg-dark-secondary: $dark-color-secondary;
$text-primary: $light-color;
$text-secondary: darken($light-color, 10%);

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,6 @@
@use "spectre.css/src/spectre.scss";
@use "spectre.css/src/spectre-icons.scss";
@import "variables";
@import "dark";

@ -1,3 +0,0 @@
import { FC, PropsWithChildren } from "react";
export type Component<P = unknown> = FC<PropsWithChildren<P>>;

@ -1,6 +0,0 @@
import { Component } from "./component";
export type ErrorPage = Component<{
error: Error & { digest?: string };
reset: () => void;
}>;

@ -1,3 +0,0 @@
export const dataDirLocation = process.env.DATA_DIR ?? "/app/data";
export const avatarFileFormat = process.env.AVATAR_FILE_FORMAT ?? "jpg";

@ -0,0 +1,21 @@
import * as configcat from "configcat-node";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const createConfigCatClient = () => {
if (!process.env.CONFIG_CAT_SDK_KEY) return;
const logger = configcat.createConsoleLogger(
process.env.NODE_ENV == "production" ? 0 : 3
);
const configCatClient = configcat.createClient(
process.env.CONFIG_CAT_SDK_KEY,
{
logger
}
);
return configCatClient;
};
export default createConfigCatClient;

@ -1,15 +0,0 @@
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);
}
);

@ -1,9 +0,0 @@
import { stat } from "fs-extra";
const fileExists = async (fileName: string): Promise<boolean> => {
return await stat(fileName)
.then(() => true)
.catch(() => false);
};
export default fileExists;

@ -1,23 +0,0 @@
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;
};

@ -1,32 +0,0 @@
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";
export const readLandingJson = cache(
async (dataDirLocation: string): Promise<Landing> => {
const landingJsonLocation = path.join(dataDirLocation, "landing.json");
return await readAndParseJsonFile(landingJsonLocation, LandingModel);
}
);
export const readAvatarFile = cache(
async (dataDirLocation: string): Promise<string> => {
const avatarFileLocation = path.join(
dataDirLocation,
`avatar.${avatarFileFormat}`
);
const imageData = await readFile(avatarFileLocation);
const base64Image = Buffer.from(imageData).toString("base64");
return `data:image/${avatarFileFormat};base64,${base64Image}`;
}
);

@ -0,0 +1,5 @@
const multipleClassNames = (...classNames: string[]): string => {
return classNames.join(" ");
};
export default multipleClassNames;

@ -1,16 +0,0 @@
import { nextui } from "@nextui-org/react";
/** @type {import('tailwindcss').Config} */
const config = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {}
},
darkMode: "class",
plugins: [nextui()]
};
export default config;

@ -1,61 +1,29 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"tsBuildInfoFile": ".next/tsbuildinfo.json",
"baseUrl": "src",
"paths": {
"@styles/*": [
"styles/*"
],
"@typings/*": [
"typings/*"
],
"@components/*": [
"components/*"
],
"@interfaces/*": [
"interfaces/*"
],
"@models/*": [
"models/*"
],
"@utils/*": [
"utils/*"
],
"@src/*": [
"src/*"
]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"tsBuildInfoFile": ".next/tsbuildinfo.json",
"baseUrl": "src",
"paths": {
"@styles/*": ["styles/*"],
"@components/*": ["components/*"],
"@interfaces/*": ["interfaces/*"],
"@utils/*": ["utils/*"],
"@src/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save