Compare commits
63 Commits
dependabot
...
main
@ -1,8 +1,13 @@
|
|||||||
**
|
**
|
||||||
|
|
||||||
|
!.env
|
||||||
!next.config.js
|
!next.config.js
|
||||||
|
!tailwind.config.js
|
||||||
|
!postcss.config.js
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
!package.json
|
!package.json
|
||||||
!yarn.lock
|
!yarn.lock
|
||||||
!public
|
!public
|
||||||
!src
|
!prisma
|
||||||
|
!src
|
||||||
|
!data
|
@ -0,0 +1,74 @@
|
|||||||
|
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 +1,2 @@
|
|||||||
NEXT_PUBLIC_GITHUB_USERNAME=Guusvanmeerveld
|
DATABASE_URL=postgresql://portfolio:portfolio@localhost:5432/portfolio?schema=public
|
||||||
|
DATA_DIR=./data
|
@ -1,67 +1,3 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"extends": "next/core-web-vitals"
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,37 @@
|
|||||||
node_modules
|
# 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
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
.next
|
|
||||||
yarn-error.log
|
# 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
|
@ -1,7 +1,26 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"arrowParens": "always"
|
"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"]
|
||||||
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"stylelint-config-standard",
|
|
||||||
"stylelint-config-idiomatic-order"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"indentation": "tab"
|
|
||||||
}
|
|
||||||
}
|
|
After Width: | Height: | Size: 31 KiB |
@ -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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
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
|
// @ts-check
|
||||||
/**
|
/**
|
||||||
* @type {import('next/dist/next-server/server/config').NextConfig}
|
* @type {import('next').NextConfig}
|
||||||
**/
|
**/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
}
|
};
|
||||||
|
@ -1,42 +1,55 @@
|
|||||||
{
|
{
|
||||||
|
"name": "portfolio-website",
|
||||||
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"start:migrate": "prisma migrate deploy && yarn run start",
|
||||||
"export": "next build && next export",
|
"export": "next build && next export",
|
||||||
|
"format": "prettier src --write",
|
||||||
"test-build": "tsc",
|
"test-build": "tsc",
|
||||||
"lint": "eslint src",
|
"lint": "next lint",
|
||||||
"stylelint": "npx stylelint **/*.scss",
|
"stylelint": "npx stylelint **/*.scss",
|
||||||
"full-test": "yarn test-build && yarn lint && yarn stylelint"
|
"full-test": "yarn test-build && yarn lint && yarn stylelint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.1.2",
|
||||||
"configcat-node": "^8.0.0",
|
"bcrypt": "^5.1.0",
|
||||||
"next": "^12.1.0",
|
"framer-motion": "^11.0.8",
|
||||||
"next-seo": "^4.24.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"humanize-duration": "^3.31.0",
|
||||||
|
"iron-session": "^6.3.1",
|
||||||
|
"next": "^14.1.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^17.0.2",
|
"postcss": "^8.4.35",
|
||||||
"react-dom": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"spectre.css": "^0.5.9"
|
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/node": "^15.12.1",
|
||||||
"@types/react": "^17.0.9",
|
"@types/react": "^18.2.64",
|
||||||
"@types/react-dom": "^17.0.6",
|
"@types/react-dom": "^18.2.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-plugin-css-modules": "^2.11.0",
|
"eslint-config-next": "13.1.6",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
|
||||||
"eslint-plugin-react": "^7.24.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
|
"prisma": "^5.8.1",
|
||||||
"sass": "^1.34.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"
|
"typescript": "^4.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
-- 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;
|
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
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;
|
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
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;
|
@ -0,0 +1,12 @@
|
|||||||
|
-- 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;
|
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
@ -0,0 +1,42 @@
|
|||||||
|
// 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
|
||||||
|
}
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,10 @@
|
|||||||
|
(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
|
@ -0,0 +1,97 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,55 @@
|
|||||||
|
"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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
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;
|
@ -0,0 +1,45 @@
|
|||||||
|
"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">
|
||||||
|
❤️
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
"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 />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,180 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
@ -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,28 @@
|
|||||||
|
"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;
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,30 @@
|
|||||||
|
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"
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,40 +0,0 @@
|
|||||||
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;
|
|
@ -1,46 +0,0 @@
|
|||||||
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;
|
|
@ -1,16 +0,0 @@
|
|||||||
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;
|
|
@ -1,66 +0,0 @@
|
|||||||
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;
|
|
@ -1,30 +0,0 @@
|
|||||||
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;
|
|
@ -1,6 +0,0 @@
|
|||||||
$margin: 1rem;
|
|
||||||
|
|
||||||
.main {
|
|
||||||
margin-top: $margin;
|
|
||||||
margin-bottom: $margin;
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
.main {
|
|
||||||
padding-top: 10rem;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
.card {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -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,14 @@
|
|||||||
|
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;
|
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
@ -0,0 +1,12 @@
|
|||||||
|
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;
|
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
@ -1,8 +0,0 @@
|
|||||||
.main {
|
|
||||||
height: 100vh;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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;
|
|
@ -1,19 +0,0 @@
|
|||||||
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;
|
|
@ -1,75 +0,0 @@
|
|||||||
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;
|
|
@ -1,66 +0,0 @@
|
|||||||
[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
$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%);
|
|
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
@ -1,6 +0,0 @@
|
|||||||
@use "spectre.css/src/spectre.scss";
|
|
||||||
@use "spectre.css/src/spectre-icons.scss";
|
|
||||||
|
|
||||||
@import "variables";
|
|
||||||
|
|
||||||
@import "dark";
|
|
@ -0,0 +1,3 @@
|
|||||||
|
import { FC, PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export type Component<P = unknown> = FC<PropsWithChildren<P>>;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Component } from "./component";
|
||||||
|
|
||||||
|
export type ErrorPage = Component<{
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}>;
|
@ -0,0 +1,3 @@
|
|||||||
|
export const dataDirLocation = process.env.DATA_DIR ?? "/app/data";
|
||||||
|
|
||||||
|
export const avatarFileFormat = process.env.AVATAR_FILE_FORMAT ?? "jpg";
|
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
@ -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,9 @@
|
|||||||
|
import { stat } from "fs-extra";
|
||||||
|
|
||||||
|
const fileExists = async (fileName: string): Promise<boolean> => {
|
||||||
|
return await stat(fileName)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fileExists;
|
@ -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;
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
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}`;
|
||||||
|
}
|
||||||
|
);
|
@ -1,5 +0,0 @@
|
|||||||
const multipleClassNames = (...classNames: string[]): string => {
|
|
||||||
return classNames.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
export default multipleClassNames;
|
|
@ -0,0 +1,16 @@
|
|||||||
|
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,29 +1,61 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
"allowJs": true,
|
"dom",
|
||||||
"skipLibCheck": true,
|
"dom.iterable",
|
||||||
"strict": true,
|
"esnext"
|
||||||
"forceConsistentCasingInFileNames": true,
|
],
|
||||||
"noEmit": true,
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"skipLibCheck": true,
|
||||||
"module": "esnext",
|
"strict": true,
|
||||||
"moduleResolution": "node",
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"noEmit": true,
|
||||||
"isolatedModules": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "preserve",
|
"module": "esnext",
|
||||||
"incremental": true,
|
"moduleResolution": "node",
|
||||||
"tsBuildInfoFile": ".next/tsbuildinfo.json",
|
"resolveJsonModule": true,
|
||||||
"baseUrl": "src",
|
"isolatedModules": true,
|
||||||
"paths": {
|
"jsx": "preserve",
|
||||||
"@styles/*": ["styles/*"],
|
"incremental": true,
|
||||||
"@components/*": ["components/*"],
|
"tsBuildInfoFile": ".next/tsbuildinfo.json",
|
||||||
"@interfaces/*": ["interfaces/*"],
|
"baseUrl": "src",
|
||||||
"@utils/*": ["utils/*"],
|
"paths": {
|
||||||
"@src/*": ["src/*"]
|
"@styles/*": [
|
||||||
}
|
"styles/*"
|
||||||
},
|
],
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"@typings/*": [
|
||||||
"exclude": ["node_modules"]
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue