Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
87fbfe4940 | |||
271aa60247 | |||
ba34e314c9 | |||
f41bc644b1 | |||
a18cec4826 | |||
61e589f0f4 | |||
dc5c3cee41 | |||
20cb0c21d5 | |||
e5232c1394 | |||
fd51609713 | |||
edf16c2562 | |||
94e0d190ae | |||
b1cf7f8517 | |||
a1a715d3b9 | |||
eede46fb41 | |||
e32c53caa1 | |||
361ea37deb | |||
d49a8a7470 | |||
a4996c8251 | |||
b25451e631 | |||
042a861f58 | |||
d76db36dbc | |||
99d9dcf334 | |||
ece5ded1b4 | |||
1514600998 | |||
5f5b328895 | |||
c88887a322 | |||
014044573a | |||
df009c3f7b | |||
5c85ca2ef1 | |||
07f7942496 | |||
213a3fa182 | |||
28d9211583 | |||
4d085cb148 | |||
e6c583f2cd | |||
232b54588a | |||
c419fb3bb4 | |||
03e7e22d74 | |||
e85c241ed1 | |||
c1877297f8 | |||
83231197dd | |||
a2fe2205bc | |||
e1f3dceb07 | |||
0f89fee52f | |||
2fcc7ac384 | |||
9351edf626 | |||
1f4aa54211 | |||
8bc1471cbb | |||
1ebdab18a5 | |||
b9b76e839a | |||
bc065a2e19 | |||
5d3a287b27 | |||
fb689c9bc1 | |||
2c3a70df2a |
@ -1,2 +1 @@
|
|||||||
ARG VARIANT="16"
|
FROM mcr.microsoft.com/devcontainers/javascript-node:18
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "Divlo",
|
||||||
"dockerComposeFile": "./docker-compose.yml",
|
"dockerComposeFile": "./docker-compose.yml",
|
||||||
"service": "workspace",
|
"service": "workspace",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"settings": {
|
"customizations": {
|
||||||
"remote.autoForwardPorts": false
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"remote.autoForwardPorts": false
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"extensions": [
|
|
||||||
"editorconfig.editorconfig",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"divlo.vscode-styled-jsx-syntax",
|
|
||||||
"divlo.vscode-styled-jsx-languageserver",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
|
||||||
"mikestead.dotenv",
|
|
||||||
"davidanson.vscode-markdownlint",
|
|
||||||
"ms-azuretools.vscode-docker"
|
|
||||||
],
|
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
"postAttachCommand": ["npm", "install"],
|
"postAttachCommand": ["npm", "install"],
|
||||||
"remoteUser": "node"
|
"remoteUser": "node"
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
version: '3.0'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
workspace:
|
workspace:
|
||||||
build:
|
build:
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
.vscode
|
.*
|
||||||
.git
|
!.npmrc
|
||||||
.env
|
|
||||||
build
|
build
|
||||||
.next
|
|
||||||
coverage
|
coverage
|
||||||
node_modules
|
node_modules
|
||||||
tmp
|
|
||||||
temp
|
|
||||||
.DS_Store
|
|
||||||
.lighthouseci
|
|
||||||
.vercel
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
COMPOSE_PROJECT_NAME=divlo.fr
|
COMPOSE_PROJECT_NAME=divlo
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
.next
|
|
||||||
.lighthouseci
|
|
||||||
storybook-static
|
|
||||||
coverage
|
|
||||||
node_modules
|
|
||||||
next-env.d.ts
|
|
||||||
**/workbox-*.js
|
|
||||||
**/sw.js
|
|
@ -4,14 +4,7 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"browser": true,
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error"
|
||||||
"unicorn/prefer-node-protocol": "off",
|
|
||||||
"@typescript-eslint/no-misused-promises": "off"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
|||||||
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||||
|
|
||||||
## What changes this PR introduce?
|
# What changes this PR introduce?
|
||||||
|
|
||||||
## List any relevant issue numbers
|
## List any relevant issue numbers
|
||||||
|
|
||||||
|
6
.github/workflows/analyze.yml
vendored
@ -16,12 +16,12 @@ jobs:
|
|||||||
language: ['javascript']
|
language: ['javascript']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Initialize CodeQL'
|
- name: 'Initialize CodeQL'
|
||||||
uses: 'github/codeql-action/init@v1'
|
uses: 'github/codeql-action/init@v2'
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: 'Perform CodeQL Analysis'
|
- name: 'Perform CodeQL Analysis'
|
||||||
uses: 'github/codeql-action/analyze@v1'
|
uses: 'github/codeql-action/analyze@v2'
|
||||||
|
12
.github/workflows/build.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
|
21
.github/workflows/lint.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'lint:commit'
|
- name: 'lint:commit'
|
||||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
@ -30,8 +30,8 @@ jobs:
|
|||||||
- name: 'lint:markdown'
|
- name: 'lint:markdown'
|
||||||
run: 'npm run lint:markdown'
|
run: 'npm run lint:markdown'
|
||||||
|
|
||||||
- name: 'lint:typescript'
|
- name: 'lint:eslint'
|
||||||
run: 'npm run lint:typescript'
|
run: 'npm run lint:eslint'
|
||||||
|
|
||||||
- name: 'lint:prettier'
|
- name: 'lint:prettier'
|
||||||
run: 'npm run lint:prettier'
|
run: 'npm run lint:prettier'
|
||||||
@ -40,8 +40,3 @@ jobs:
|
|||||||
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
github_token: ${{ secrets.github_token }}
|
||||||
|
|
||||||
- name: 'lint:docker'
|
|
||||||
uses: 'hadolint/hadolint-action@v1.6.0'
|
|
||||||
with:
|
|
||||||
dockerfile: './Dockerfile'
|
|
||||||
|
14
.github/workflows/release.yml
vendored
@ -8,26 +8,26 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 'Import GPG key'
|
- name: 'Import GPG key'
|
||||||
uses: 'crazy-max/ghaction-import-gpg@v4'
|
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Release'
|
- name: 'Release'
|
||||||
run: 'npm run release'
|
run: 'npm run release'
|
||||||
|
48
.github/workflows/test.yml
vendored
@ -10,33 +10,33 @@ jobs:
|
|||||||
test-unit:
|
test-unit:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Unit Test'
|
- name: 'Unit Test'
|
||||||
run: 'npm run test:unit'
|
run: 'npm run test:unit'
|
||||||
|
|
||||||
test-lighthouse:
|
test-e2e:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.0'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
@ -44,27 +44,5 @@ jobs:
|
|||||||
- name: 'html-w3c-validator'
|
- name: 'html-w3c-validator'
|
||||||
run: 'npm run test:html-w3c-validator'
|
run: 'npm run test:html-w3c-validator'
|
||||||
|
|
||||||
- name: 'Lighthouse'
|
|
||||||
run: 'npm run test:lighthouse'
|
|
||||||
env:
|
|
||||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
||||||
|
|
||||||
test-e2e:
|
|
||||||
runs-on: 'ubuntu-latest'
|
|
||||||
steps:
|
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
|
||||||
with:
|
|
||||||
node-version: '16.x'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: 'Install'
|
|
||||||
run: 'npm install'
|
|
||||||
|
|
||||||
- name: 'Build'
|
|
||||||
run: 'npm run build'
|
|
||||||
|
|
||||||
- name: 'End To End (e2e) Test'
|
- name: 'End To End (e2e) Test'
|
||||||
run: 'npm run test:e2e'
|
run: 'npm run test:e2e'
|
||||||
|
6
.gitignore
vendored
@ -11,7 +11,7 @@ out
|
|||||||
# production
|
# production
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
public/*.html
|
public/curriculum-vitae
|
||||||
# PWA
|
# PWA
|
||||||
public/workbox-*.js
|
public/workbox-*.js
|
||||||
public/sw.js
|
public/sw.js
|
||||||
@ -49,3 +49,7 @@ npm-debug.log*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.lighthouseci
|
.lighthouseci
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"urls": [
|
"urls": [
|
||||||
"http://localhost:3000/",
|
"http://127.0.0.1:3000/",
|
||||||
"http://localhost:3000/blog",
|
"http://127.0.0.1:3000/blog",
|
||||||
"http://localhost:3000/blog/hello-world"
|
"http://127.0.0.1:3000/blog/hello-world"
|
||||||
],
|
],
|
||||||
"files": ["./public/curriculum-vitae.html"]
|
"files": ["./public/curriculum-vitae/index.html"]
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"ci": {
|
|
||||||
"collect": {
|
|
||||||
"startServerCommand": "npm run start",
|
|
||||||
"startServerReadyPattern": "ready on",
|
|
||||||
"startServerReadyTimeout": 20000,
|
|
||||||
"url": [
|
|
||||||
"http://localhost:3000/",
|
|
||||||
"http://localhost:3000/blog",
|
|
||||||
"http://localhost:3000/blog/hello-world"
|
|
||||||
],
|
|
||||||
"numberOfRuns": 1
|
|
||||||
},
|
|
||||||
"assert": {
|
|
||||||
"preset": "lighthouse:recommended",
|
|
||||||
"assertions": {
|
|
||||||
"csp-xss": "warning",
|
|
||||||
"non-composited-animations": "warning",
|
|
||||||
"unused-javascript": "warning",
|
|
||||||
"image-size-responsive": "warning",
|
|
||||||
"unsized-images": "warning",
|
|
||||||
"color-contrast": "warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload": {
|
|
||||||
"target": "temporary-public-storage"
|
|
||||||
},
|
|
||||||
"server": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,6 @@
|
|||||||
{
|
{
|
||||||
"*": ["editorconfig-checker"],
|
"*": ["editorconfig-checker"],
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix",
|
|
||||||
"jest --findRelatedTests"
|
|
||||||
],
|
|
||||||
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||||
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
|
||||||
"resume.json": ["resume validate"]
|
|
||||||
}
|
}
|
||||||
|
12
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"default": true,
|
||||||
|
"relative-links": true,
|
||||||
|
"extends": "markdownlint/style/prettier",
|
||||||
|
"MD024": false,
|
||||||
|
"MD033": false
|
||||||
|
},
|
||||||
|
"globs": ["**/*.{md,mdx}"],
|
||||||
|
"ignores": ["**/node_modules"],
|
||||||
|
"customRules": ["markdownlint-rule-relative-links"]
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"default": true,
|
|
||||||
"MD013": false,
|
|
||||||
"MD024": false,
|
|
||||||
"MD033": false,
|
|
||||||
"MD041": false
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
.next
|
|
||||||
.lighthouseci
|
|
||||||
storybook-static
|
|
||||||
coverage
|
|
||||||
node_modules
|
|
||||||
next-env.d.ts
|
|
||||||
**/workbox-*.js
|
|
||||||
**/sw.js
|
|
||||||
*.hbs
|
|
2
.vscode/extensions.json
vendored
@ -3,8 +3,6 @@
|
|||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"divlo.vscode-styled-jsx-syntax",
|
|
||||||
"divlo.vscode-styled-jsx-languageserver",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
"bradlc.vscode-tailwindcss",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"davidanson.vscode-markdownlint",
|
"davidanson.vscode-markdownlint",
|
||||||
|
6
.vscode/settings.json
vendored
@ -6,5 +6,9 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": true
|
||||||
}
|
},
|
||||||
|
"eslint.options": {
|
||||||
|
"ignorePath": ".gitignore"
|
||||||
|
},
|
||||||
|
"prettier.ignorePath": ".gitignore"
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
contact@divlo.fr.
|
<contact@divlo.fr>.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
**divlo.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||||
|
|
||||||
## Types of contributions
|
## Types of contributions
|
||||||
|
|
||||||
- Reporting a bug.
|
- Reporting a bug.
|
||||||
@ -21,29 +25,7 @@ If you're adding new features to **divlo.fr**, please include tests.
|
|||||||
|
|
||||||
## Commits
|
## Commits
|
||||||
|
|
||||||
The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases.
|
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
|
||||||
|
|
||||||
### Types
|
|
||||||
|
|
||||||
Types define which kind of changes you made to the project.
|
|
||||||
|
|
||||||
| Types | Description |
|
|
||||||
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| feat | A new feature. |
|
|
||||||
| fix | A bug fix. |
|
|
||||||
| docs | Documentation only changes. |
|
|
||||||
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
|
|
||||||
| refactor | A code change that neither fixes a bug nor adds a feature. |
|
|
||||||
| perf | A code change that improves performance. |
|
|
||||||
| test | Adding missing tests or correcting existing tests. |
|
|
||||||
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
|
|
||||||
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
|
|
||||||
| chore | Other changes that don't modify src or test files. |
|
|
||||||
| revert | Reverts a previous commit. |
|
|
||||||
|
|
||||||
### Scopes
|
|
||||||
|
|
||||||
Scopes define what part of the code changed.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@ -81,9 +63,9 @@ npm run dev
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Setup and run all the services for you
|
# Setup and run all the services for you
|
||||||
docker-compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Services started
|
### Services started
|
||||||
|
|
||||||
- website : `http://localhost:3000`
|
- website: `http://127.0.0.1:3000`
|
||||||
|
32
Dockerfile
@ -1,23 +1,21 @@
|
|||||||
FROM node:16.14.0 AS dependencies
|
FROM node:18.16.0 AS builder-dependencies
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm install
|
RUN npm clean-install
|
||||||
|
|
||||||
FROM node:16.14.0 AS builder
|
FROM node:18.16.0 AS builder
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
|
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:16.14.0 AS runner
|
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
COPY --from=builder /usr/src/app/public ./public
|
COPY --from=builder /usr/src/application/.next/standalone ./
|
||||||
COPY --from=builder /usr/src/app/.next ./.next
|
COPY --from=builder /usr/src/application/.next/static ./.next/static
|
||||||
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
|
COPY --from=builder /usr/src/application/public ./public
|
||||||
COPY --from=builder /usr/src/app/locales ./locales
|
COPY --from=builder /usr/src/application/locales ./locales
|
||||||
COPY --from=builder /usr/src/app/pages ./pages
|
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
|
||||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
CMD ["./server.js"]
|
||||||
RUN npx next telemetry disable
|
|
||||||
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
|
|
||||||
|
20
README.md
@ -1,7 +1,7 @@
|
|||||||
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
|
<h1 align="center"><a href="https://divlo.fr/">Théo LUDWIG (Divlo)</a></h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Developer Full Stack Junior • Passionate about High-Tech</strong>
|
<strong>Developer Full Stack • Open-Source enthusiast</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -21,19 +21,15 @@
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Divlo",
|
"name": "Théo LUDWIG (Divlo)",
|
||||||
"pronouns": "He/Him",
|
"pronouns": "He/Him",
|
||||||
"birthDate": "31/03/2003",
|
"birthdate": "31/03/2003",
|
||||||
"nationality": "Alsace, France",
|
"nationality": "Alsace, France",
|
||||||
"interests": [
|
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
|
||||||
"Developer Full Stack Junior",
|
|
||||||
"Passionate about High-Tech",
|
|
||||||
"Open-Source enthusiast"
|
|
||||||
],
|
|
||||||
"skills": {
|
"skills": {
|
||||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||||
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
"backEnd": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,45 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export interface ErrorPageProps {
|
import type { FooterProps } from './Footer'
|
||||||
|
import { Footer } from './Footer'
|
||||||
|
import { Header } from './Header'
|
||||||
|
|
||||||
|
export interface ErrorPageProps extends FooterProps {
|
||||||
statusCode: number
|
statusCode: number
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||||
const { message, statusCode } = props
|
const { message, statusCode, version } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className='my-6 text-4xl font-semibold'>
|
<div className='flex h-screen flex-col pt-0'>
|
||||||
{t('errors:error')}{' '}
|
<Header showLanguage />
|
||||||
<span
|
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
|
||||||
className='text-yellow dark:text-yellow-dark'
|
<h1 className='my-6 text-4xl font-semibold'>
|
||||||
data-cy='status-code'
|
{t('errors:error')}{' '}
|
||||||
>
|
<span
|
||||||
{statusCode}
|
className='text-yellow dark:text-yellow-dark'
|
||||||
</span>
|
data-cy='status-code'
|
||||||
</h1>
|
>
|
||||||
<p className='text-center text-lg'>
|
{statusCode}
|
||||||
{message}{' '}
|
</span>
|
||||||
<Link href='/'>
|
</h1>
|
||||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
<p className='text-center text-lg'>
|
||||||
{t('errors:return-to-home-page')}
|
{message}{' '}
|
||||||
</a>
|
<Link
|
||||||
</Link>
|
href='/'
|
||||||
</p>
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
|
>
|
||||||
<style jsx global>
|
{t('errors:return-to-home-page')}
|
||||||
{`
|
</Link>
|
||||||
main {
|
</p>
|
||||||
display: flex;
|
</main>
|
||||||
flex-direction: column;
|
<Footer version={version} />
|
||||||
justify-content: center;
|
</div>
|
||||||
align-items: center;
|
|
||||||
min-width: 100vw;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
#__next {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,18 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||||
<p>
|
<p>
|
||||||
<Link href='/'>
|
<Link
|
||||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
href='/'
|
||||||
Divlo
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
</a>
|
>
|
||||||
|
Théo LUDWIG (Divlo)
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
| {t('common:all-rights-reserved')}
|
| {t('common:all-rights-reserved')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1'>
|
<p className='mt-1'>
|
||||||
Version{' '}
|
Version{' '}
|
||||||
<a
|
<a
|
||||||
|
data-cy='version-link'
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
href={versionLink}
|
href={versionLink}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
|
@ -9,9 +9,9 @@ interface HeadProps {
|
|||||||
|
|
||||||
export const Head: React.FC<HeadProps> = (props) => {
|
export const Head: React.FC<HeadProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
title = 'Divlo',
|
title = 'Théo LUDWIG (Divlo)',
|
||||||
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
image = 'https://divlo.fr/images/icon-96x96.png',
|
||||||
description = 'Divlo - Developer Full Stack Junior • Passionate about High-Tech',
|
description = 'Théo LUDWIG (Divlo) - Developer Full Stack • Passionate about High-Tech',
|
||||||
url = 'https://divlo.fr/'
|
url = 'https://divlo.fr/'
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
{/* Meta Tag */}
|
{/* Meta Tag */}
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
<meta name='description' content={description} />
|
<meta name='description' content={description} />
|
||||||
<meta name='Language' content='fr, en' />
|
<meta name='Language' content='fr-FR, en-US' />
|
||||||
<meta name='theme-color' content='#ffd800' />
|
<meta name='theme-color' content='#ffd800' />
|
||||||
|
|
||||||
{/* Open Graph Metadata */}
|
{/* Open Graph Metadata */}
|
||||||
@ -32,7 +32,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
<meta property='og:url' content={url} />
|
<meta property='og:url' content={url} />
|
||||||
<meta property='og:image' content={image} />
|
<meta property='og:image' content={image} />
|
||||||
<meta property='og:description' content={description} />
|
<meta property='og:description' content={description} />
|
||||||
<meta property='og:locale' content='fr_FR, en_US' />
|
<meta property='og:locale' content='fr-FR, en-US' />
|
||||||
<meta property='og:site_name' content={title} />
|
<meta property='og:site_name' content={title} />
|
||||||
|
|
||||||
{/* Twitter card Metadata */}
|
{/* Twitter card Metadata */}
|
||||||
@ -46,12 +46,6 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
name='google-site-verification'
|
name='google-site-verification'
|
||||||
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
|
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* PWA Data */}
|
|
||||||
<link rel='manifest' href='/manifest.json' />
|
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
|
||||||
<meta name='mobile-web-app-capable' content='yes' />
|
|
||||||
<link rel='apple-touch-icon' href={image} />
|
|
||||||
</NextHead>
|
</NextHead>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import setLanguage from 'next-translate/setLanguage'
|
import setLanguage from 'next-translate/setLanguage'
|
||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
import i18n from 'i18n.json'
|
import i18n from 'i18n.json'
|
||||||
|
|
||||||
@ -11,31 +11,39 @@ import { LanguageFlag } from './LanguageFlag'
|
|||||||
export const Language: React.FC = () => {
|
export const Language: React.FC = () => {
|
||||||
const { lang: currentLanguage } = useTranslation()
|
const { lang: currentLanguage } = useTranslation()
|
||||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||||
|
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const handleHiddenMenu = useCallback(() => {
|
const handleHiddenMenu = useCallback(() => {
|
||||||
setHiddenMenu(!hiddenMenu)
|
setHiddenMenu((oldHiddenMenu) => {
|
||||||
}, [hiddenMenu])
|
return !oldHiddenMenu
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hiddenMenu) {
|
const handleClickEvent = (event: MouseEvent): void => {
|
||||||
window.document.addEventListener('click', handleHiddenMenu)
|
if (languageClickRef.current == null || event.target == null) {
|
||||||
} else {
|
return
|
||||||
window.document.removeEventListener('click', handleHiddenMenu)
|
}
|
||||||
|
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||||
|
setHiddenMenu(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.document.addEventListener('click', handleClickEvent)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.document.removeEventListener('click', handleHiddenMenu)
|
return window.removeEventListener('click', handleClickEvent)
|
||||||
}
|
}
|
||||||
}, [hiddenMenu, handleHiddenMenu])
|
}, [])
|
||||||
|
|
||||||
const handleLanguage = async (language: string): Promise<void> => {
|
const handleLanguage = async (language: string): Promise<void> => {
|
||||||
await setLanguage(language)
|
await setLanguage(language)
|
||||||
handleHiddenMenu()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||||
<div
|
<div
|
||||||
|
ref={languageClickRef}
|
||||||
data-cy='language-click'
|
data-cy='language-click'
|
||||||
className='mr-5 flex items-center'
|
className='mr-5 flex items-center'
|
||||||
onClick={handleHiddenMenu}
|
onClick={handleHiddenMenu}
|
||||||
@ -47,7 +55,7 @@ export const Language: React.FC = () => {
|
|||||||
<ul
|
<ul
|
||||||
data-cy='languages-list'
|
data-cy='languages-list'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
'absolute top-14 z-10 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||||
{ hidden: hiddenMenu }
|
{ hidden: hiddenMenu }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -59,7 +67,9 @@ export const Language: React.FC = () => {
|
|||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||||
onClick={async () => await handleLanguage(language)}
|
onClick={async () => {
|
||||||
|
return await handleLanguage(language)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LanguageFlag language={language} />
|
<LanguageFlag language={language} />
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import classNames from 'clsx'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
export const SwitchTheme: React.FC = () => {
|
export const SwitchTheme: React.FC = () => {
|
||||||
@ -18,109 +19,60 @@ export const SwitchTheme: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className='flex items-center'
|
||||||
className='flex items-center'
|
data-cy='switch-theme-click'
|
||||||
data-cy='switch-theme-click'
|
onClick={handleClick}
|
||||||
onClick={handleClick}
|
>
|
||||||
>
|
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
|
||||||
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
|
||||||
<div className='toggle-track'>
|
<div
|
||||||
<div
|
data-cy='switch-theme-dark'
|
||||||
data-cy='switch-theme-dark'
|
className={classNames(
|
||||||
className='toggle-track-check absolute'
|
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
|
||||||
>
|
{
|
||||||
<span className='toggle_Dark relative flex items-center justify-center'>
|
'opacity-100': theme === 'dark',
|
||||||
🌜
|
'opacity-0': theme === 'light'
|
||||||
</span>
|
}
|
||||||
</div>
|
)}
|
||||||
<div
|
>
|
||||||
data-cy='switch-theme-light'
|
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
||||||
className='toggle-track-x absolute'
|
🌜
|
||||||
>
|
</span>
|
||||||
<span className='toggle_Light relative flex items-center justify-center'>
|
</div>
|
||||||
🌞
|
<div
|
||||||
</span>
|
data-cy='switch-theme-light'
|
||||||
</div>
|
className={classNames(
|
||||||
|
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
|
||||||
|
{
|
||||||
|
'opacity-100': theme === 'light',
|
||||||
|
'opacity-0': theme === 'dark'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
||||||
|
🌞
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='toggle-thumb absolute' />
|
|
||||||
<input
|
|
||||||
data-cy='switch-theme-input'
|
|
||||||
type='checkbox'
|
|
||||||
aria-label='Dark mode toggle'
|
|
||||||
className='toggle-screenreader-only absolute overflow-hidden'
|
|
||||||
defaultChecked
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
|
||||||
|
{
|
||||||
|
'left-[27px]': theme === 'dark',
|
||||||
|
'left-0': theme === 'light'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={{ border: '1px solid #4d4d4d' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
data-cy='switch-theme-input'
|
||||||
|
type='checkbox'
|
||||||
|
aria-label='Dark mode toggle'
|
||||||
|
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.toggle-theme-button {
|
|
||||||
touch-action: pan-x;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.toggle-track {
|
|
||||||
width: 50px;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 30px;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.toggle-track-check {
|
|
||||||
width: 14px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
left: 8px;
|
|
||||||
opacity: ${theme === 'dark' ? 1 : 0};
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
.toggle-track-x {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
right: 10px;
|
|
||||||
opacity: ${theme === 'dark' ? 0 : 1};
|
|
||||||
}
|
|
||||||
.toggle_Dark,
|
|
||||||
.toggle_Light {
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
.toggle-thumb {
|
|
||||||
left: ${theme === 'dark' ? '27px' : '0px'};
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: 1px solid #4d4d4d;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fafafa;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
top: 1px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.toggle-screenreader-only {
|
|
||||||
border: 0;
|
|
||||||
clip: rect(0 0 0 0);
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Header } from '..'
|
|
||||||
|
|
||||||
describe('<Header />', () => {
|
|
||||||
it('should render', () => {
|
|
||||||
const { getByText } = render(<Header />)
|
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -14,33 +14,31 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||||
<Link href='/'>
|
<Link href='/'>
|
||||||
<a>
|
<div className='flex items-center justify-center'>
|
||||||
<div className='flex items-center justify-center'>
|
<Image
|
||||||
<Image
|
quality={100}
|
||||||
quality={100}
|
width={60}
|
||||||
width={60}
|
height={60}
|
||||||
height={60}
|
src='/images/divlo_icon_small.png'
|
||||||
src='/images/divlo_icon_small.png'
|
alt='Divlo'
|
||||||
alt='Divlo'
|
priority
|
||||||
/>
|
/>
|
||||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||||
Divlo
|
Théo LUDWIG (Divlo)
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<div className='flex flex-col items-center justify-center px-6'>
|
<div className='flex flex-col items-center justify-center px-6'>
|
||||||
<Link href='/blog'>
|
<Link
|
||||||
<a
|
href='/blog'
|
||||||
data-cy='header-blog-link'
|
data-cy='header-blog-link'
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{showLanguage && <Language />}
|
{showLanguage ? <Language /> : null}
|
||||||
<SwitchTheme />
|
<SwitchTheme />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
interface InterestItemProps {
|
interface InterestItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -10,7 +10,7 @@ export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
|||||||
const { fontAwesomeIcon, title } = props
|
const { fontAwesomeIcon, title } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
|
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||||
icon={fontAwesomeIcon}
|
icon={fontAwesomeIcon}
|
||||||
|
@ -7,10 +7,7 @@ export const InterestsList: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='my-4 flex justify-center'>
|
<div className='my-4 flex justify-center'>
|
||||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||||
<InterestItem
|
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
|
||||||
title='Developer Full Stack Junior'
|
|
||||||
fontAwesomeIcon={faCode}
|
|
||||||
/>
|
|
||||||
<InterestItem
|
<InterestItem
|
||||||
title='Passionate about High-Tech'
|
title='Passionate about High-Tech'
|
||||||
fontAwesomeIcon={faMicrochip}
|
fontAwesomeIcon={faMicrochip}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
import type { InterestParagraphProps } from './InterestParagraph'
|
||||||
|
import { InterestParagraph } from './InterestParagraph'
|
||||||
import { InterestsList } from './InterestsList'
|
import { InterestsList } from './InterestsList'
|
||||||
|
|
||||||
export const Interests: React.FC = () => {
|
export const Interests: React.FC = () => {
|
||||||
|
@ -24,7 +24,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
|||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<Image
|
<Image
|
||||||
quality={100}
|
quality={100}
|
||||||
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||||
width={300}
|
width={300}
|
||||||
height={300}
|
height={300}
|
||||||
src={image}
|
src={image}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
import type { PortfolioItemProps } from './PortfolioItem'
|
||||||
|
import { PortfolioItem } from './PortfolioItem'
|
||||||
|
|
||||||
export const Portfolio: React.FC = () => {
|
export const Portfolio: React.FC = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
@ -4,9 +4,9 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
|||||||
const { t, lang } = useTranslation()
|
const { t, lang } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||||
{t('home:about.description-bottom')}
|
{t('home:about.description-bottom')}
|
||||||
{lang === 'fr' && (
|
{lang === 'fr' ? (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@ -17,7 +17,7 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
|||||||
Curriculum vitæ
|
Curriculum vitæ
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,8 @@ export const ProfileInformation: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
||||||
<h1 className='mb-2 text-4xl'>
|
<h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||||
{t('home:about.i-am')}{' '}
|
Théo LUDWIG (Divlo)
|
||||||
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
|
|
||||||
Divlo
|
|
||||||
</strong>
|
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
|||||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||||
{title}
|
{title}
|
||||||
</strong>
|
</strong>
|
||||||
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||||
{link != null ? (
|
{link != null ? (
|
||||||
<a
|
<a
|
||||||
className='text-gray hover:underline dark:text-gray-dark'
|
className='text-gray hover:underline dark:text-gray-dark'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { DIVLO_BIRTHDAY, DIVLO_BIRTHDAY_DATE, getAge } from 'utils/getAge'
|
import { DIVLO_BIRTH_DATE, DIVLO_BIRTH_DATE_STRING, getAge } from 'utils/getAge'
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from './ProfileItem'
|
||||||
|
|
||||||
@ -9,15 +9,20 @@ export const ProfileList: React.FC = () => {
|
|||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
|
||||||
const age = useMemo(() => {
|
const age = useMemo(() => {
|
||||||
return getAge(DIVLO_BIRTHDAY)
|
return getAge(DIVLO_BIRTH_DATE)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className='m-0 list-none p-0'>
|
<ul className='m-0 list-none p-0'>
|
||||||
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
<ProfileItem
|
||||||
|
title={t('home:about.pronouns')}
|
||||||
|
value={t('home:about.pronouns-value')}
|
||||||
|
/>
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title={t('home:about.birth-date')}
|
title={t('home:about.birth-date')}
|
||||||
value={`${DIVLO_BIRTHDAY_DATE} (${age} ${t('home:about.years-old')})`}
|
value={`${DIVLO_BIRTH_DATE_STRING} (${age} ${t(
|
||||||
|
'home:about.years-old'
|
||||||
|
)})`}
|
||||||
/>
|
/>
|
||||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
|
@ -5,7 +5,7 @@ import DivloLogo from 'public/images/divlo_logo.png'
|
|||||||
export const ProfileLogo: React.FC = () => {
|
export const ProfileLogo: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||||
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
<Image quality={100} src={DivloLogo} alt='Divlo' priority />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className, ...rest } = props
|
||||||
|
@ -3,7 +3,9 @@ interface SocialMediaItemProps {
|
|||||||
ariaLabel: string
|
ariaLabel: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
export const SocialMediaItem: React.FC<
|
||||||
|
React.PropsWithChildren<SocialMediaItemProps>
|
||||||
|
> = (props) => {
|
||||||
const { link, ariaLabel, children } = props
|
const { link, ariaLabel, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,10 +2,11 @@ import { useTheme } from 'next-themes'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import type { SkillName } from './skills'
|
||||||
import { skills } from './skills'
|
import { skills } from './skills'
|
||||||
|
|
||||||
export interface SkillComponentProps {
|
export interface SkillComponentProps {
|
||||||
skill: string
|
skill: SkillName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||||
@ -14,10 +15,13 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
if (typeof skillProperties.image !== 'string') {
|
if (typeof skillProperties.image === 'string') {
|
||||||
return skillProperties.image[theme ?? 'light']
|
return skillProperties.image
|
||||||
}
|
}
|
||||||
return skillProperties.image
|
if (theme === 'light') {
|
||||||
|
return skillProperties.image.light
|
||||||
|
}
|
||||||
|
return skillProperties.image.dark
|
||||||
}, [skillProperties, theme])
|
}, [skillProperties, theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +32,14 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
|||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<Image quality={100} width={60} height={60} alt={skill} src={image} />
|
<Image
|
||||||
|
className='inline h-auto w-auto'
|
||||||
|
quality={100}
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
alt={skill}
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
<p className='mt-1'>{skill}</p>
|
<p className='mt-1'>{skill}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -13,6 +13,7 @@ export const Skills: React.FC = () => {
|
|||||||
<SkillComponent skill='TypeScript' />
|
<SkillComponent skill='TypeScript' />
|
||||||
<SkillComponent skill='Python' />
|
<SkillComponent skill='Python' />
|
||||||
<SkillComponent skill='C/C++' />
|
<SkillComponent skill='C/C++' />
|
||||||
|
<SkillComponent skill='PHP' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Front-end'>
|
<SkillsSection title='Front-end'>
|
||||||
@ -23,11 +24,10 @@ export const Skills: React.FC = () => {
|
|||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Back-end'>
|
<SkillsSection title='Back-end'>
|
||||||
|
<SkillComponent skill='Laravel' />
|
||||||
<SkillComponent skill='Node.js' />
|
<SkillComponent skill='Node.js' />
|
||||||
<SkillComponent skill='Fastify' />
|
<SkillComponent skill='Fastify' />
|
||||||
<SkillComponent skill='Prisma' />
|
|
||||||
<SkillComponent skill='PostgreSQL' />
|
<SkillComponent skill='PostgreSQL' />
|
||||||
<SkillComponent skill='MySQL' />
|
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title={t('home:skills.software-tools')}>
|
<SkillsSection title={t('home:skills.software-tools')}>
|
||||||
|
@ -3,11 +3,7 @@ export interface Skill {
|
|||||||
image: string | { [key: string]: string }
|
image: string | { [key: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Skills {
|
export const skills = {
|
||||||
[key: string]: Skill
|
|
||||||
}
|
|
||||||
|
|
||||||
export const skills: Skills = {
|
|
||||||
JavaScript: {
|
JavaScript: {
|
||||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
||||||
image: '/images/skills/JavaScript.png'
|
image: '/images/skills/JavaScript.png'
|
||||||
@ -24,6 +20,14 @@ export const skills: Skills = {
|
|||||||
link: 'https://isocpp.org/',
|
link: 'https://isocpp.org/',
|
||||||
image: '/images/skills/C-Cpp.png'
|
image: '/images/skills/C-Cpp.png'
|
||||||
},
|
},
|
||||||
|
PHP: {
|
||||||
|
link: 'https://www.php.net/',
|
||||||
|
image: '/images/skills/PHP.png'
|
||||||
|
},
|
||||||
|
Laravel: {
|
||||||
|
link: 'https://laravel.com/',
|
||||||
|
image: '/images/skills/Laravel.png'
|
||||||
|
},
|
||||||
Dart: {
|
Dart: {
|
||||||
link: 'https://dart.dev/',
|
link: 'https://dart.dev/',
|
||||||
image: '/images/skills/Dart.png'
|
image: '/images/skills/Dart.png'
|
||||||
@ -107,3 +111,5 @@ export const skills: Skills = {
|
|||||||
image: '/images/skills/Docker.png'
|
image: '/images/skills/Docker.png'
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export type SkillName = keyof typeof skills
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { ErrorPage } from '../ErrorPage'
|
|
||||||
|
|
||||||
describe('<ErrorPage />', () => {
|
|
||||||
it('should render the message and statusCode', () => {
|
|
||||||
const messageContent = 'message content'
|
|
||||||
const statusCode = 404
|
|
||||||
const { getByText } = render(
|
|
||||||
<ErrorPage statusCode={statusCode} message={messageContent} />
|
|
||||||
)
|
|
||||||
expect(getByText(messageContent)).toBeInTheDocument()
|
|
||||||
expect(getByText(statusCode)).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,16 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Footer } from '../Footer'
|
|
||||||
|
|
||||||
describe('<Footer />', () => {
|
|
||||||
it('should render with appropriate link tag version', () => {
|
|
||||||
const version = '1.0.0'
|
|
||||||
const { getByText } = render(<Footer version={version} />)
|
|
||||||
const versionLink = getByText(version) as HTMLAnchorElement
|
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
|
||||||
expect(versionLink).toBeInTheDocument()
|
|
||||||
expect(versionLink.href).toEqual(
|
|
||||||
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export const RevealFade: React.FC = (props) => {
|
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
const htmlElement = useRef<HTMLDivElement>(null)
|
const htmlElement = useRef<HTMLDivElement>(null)
|
||||||
|
@ -4,7 +4,7 @@ export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
|||||||
const { children, ...rest } = props
|
const { children, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
|
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,9 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
<div className='w-full px-3'>
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer style={{ marginTop: 50 }}>
|
<ShadowContainer style={{ marginTop: 50 }}>
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null ? (
|
||||||
|
<SectionHeading>{heading}</SectionHeading>
|
||||||
|
) : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
@ -34,7 +36,7 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
if (withoutShadowContainer) {
|
if (withoutShadowContainer) {
|
||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -42,16 +44,18 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && (
|
{heading != null ? (
|
||||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
<SectionHeading
|
||||||
|
style={{ ...(description != null ? { margin: 0 } : {}) }}
|
||||||
|
>
|
||||||
{heading}
|
{heading}
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
)}
|
) : null}
|
||||||
{description != null && (
|
{description != null ? (
|
||||||
<p style={{ marginTop: 7 }} className='text-center'>
|
<p style={{ marginTop: 7 }} className='text-center'>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
<div className='w-full px-3'>
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||||
|
|
||||||
|
17
cypress.config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: false,
|
||||||
|
video: false,
|
||||||
|
screenshotOnRunFailure: false,
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://127.0.0.1:3000',
|
||||||
|
supportFile: false
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: 'next',
|
||||||
|
bundler: 'webpack'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"supportFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"video": false,
|
|
||||||
"screenshotOnRunFailure": false
|
|
||||||
}
|
|
16
cypress/component/Footer.cy.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Footer } from '@/components/Footer'
|
||||||
|
|
||||||
|
describe('<Footer />', () => {
|
||||||
|
it('should render with appropriate link tag version', () => {
|
||||||
|
const version = '1.0.0'
|
||||||
|
cy.mount(<Footer version={version} />)
|
||||||
|
cy.contains('Divlo')
|
||||||
|
.get('[data-cy=version-link]')
|
||||||
|
.should('have.text', version)
|
||||||
|
.should(
|
||||||
|
'have.attr',
|
||||||
|
'href',
|
||||||
|
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
17
cypress/component/utils/getAge.cy.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { getAge } from '../../../utils/getAge'
|
||||||
|
|
||||||
|
describe('utils/getAge', () => {
|
||||||
|
it('should calculate the right age of a person', () => {
|
||||||
|
cy.clock(new Date('2018-03-20')).then(() => {
|
||||||
|
const birthDate = new Date('1980-02-20')
|
||||||
|
expect(getAge(birthDate)).equal(38)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should calculate the right age of a person (taking into account the months)', () => {
|
||||||
|
cy.clock(new Date('2018-03-20')).then(() => {
|
||||||
|
const birthDate = new Date('1980-07-20')
|
||||||
|
expect(getAge(birthDate)).equal(37)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,5 +1,7 @@
|
|||||||
describe('Common > Header', () => {
|
describe('Common > Header', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
it('should redirect to /blog on click of the blog link', () => {
|
it('should redirect to /blog on click of the blog link', () => {
|
||||||
cy.get('[data-cy=header-blog-link]')
|
cy.get('[data-cy=header-blog-link]')
|
||||||
@ -36,7 +38,7 @@ describe('Common > Header', () => {
|
|||||||
|
|
||||||
describe('Switch Language', () => {
|
describe('Switch Language', () => {
|
||||||
it('should switch language from EN (default) to FR', () => {
|
it('should switch language from EN (default) to FR', () => {
|
||||||
cy.get('h1').contains('I am Divlo')
|
cy.get('h1').contains('Divlo')
|
||||||
cy.get('[data-cy=language-flag-text]').contains('EN')
|
cy.get('[data-cy=language-flag-text]').contains('EN')
|
||||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
cy.get('[data-cy=language-click]').click()
|
cy.get('[data-cy=language-click]').click()
|
||||||
@ -44,7 +46,7 @@ describe('Common > Header', () => {
|
|||||||
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
|
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
|
||||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
cy.get('[data-cy=language-flag-text]').contains('FR')
|
cy.get('[data-cy=language-flag-text]').contains('FR')
|
||||||
cy.get('h1').contains('Je suis Divlo')
|
cy.get('h1').contains('Divlo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should close the language list menu when clicking outside', () => {
|
it('should close the language list menu when clicking outside', () => {
|
||||||
@ -56,3 +58,5 @@ describe('Common > Header', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -1,7 +1,11 @@
|
|||||||
describe('Page /404', () => {
|
describe('Page /404', () => {
|
||||||
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/404', { failOnStatusCode: false })
|
||||||
|
})
|
||||||
|
|
||||||
it('should display the statusCode of 404', () => {
|
it('should display the statusCode of 404', () => {
|
||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -1,7 +1,11 @@
|
|||||||
describe('Page /500', () => {
|
describe('Page /500', () => {
|
||||||
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/500', { failOnStatusCode: false })
|
||||||
|
})
|
||||||
|
|
||||||
it('should display the statusCode of 500', () => {
|
it('should display the statusCode of 500', () => {
|
||||||
cy.get('[data-cy=status-code]').contains('500')
|
cy.get('[data-cy=status-code]').contains('500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -11,3 +11,5 @@ describe('Page /blog/[slug]', () => {
|
|||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -20,3 +20,5 @@ describe('Page /blog', () => {
|
|||||||
.should('eq', '/blog/hello-world')
|
.should('eq', '/blog/hello-world')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -1,13 +1,10 @@
|
|||||||
describe('Page /', () => {
|
describe('Page /', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
it('should reveals the sections while scrolling except the about section', () => {
|
it('should reveals the sections while scrolling except the about section', () => {
|
||||||
const sectionsReveals = [
|
const sectionsReveals = ['#interests', '#skills', '#portfolio']
|
||||||
'#interests',
|
|
||||||
'#skills',
|
|
||||||
'#portfolio',
|
|
||||||
'#open-source'
|
|
||||||
]
|
|
||||||
cy.get('#about').should('be.visible')
|
cy.get('#about').should('be.visible')
|
||||||
for (const section of sectionsReveals) {
|
for (const section of sectionsReveals) {
|
||||||
cy.get(section)
|
cy.get(section)
|
||||||
@ -17,3 +14,5 @@ describe('Page /', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
3
cypress/support/commands.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export {}
|
14
cypress/support/component-index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>Components App</title>
|
||||||
|
<!-- Used by Next.js to inject CSS. -->
|
||||||
|
<div id="__next_css__DO_NOT_USE__"></div>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
cypress/support/component.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { mount } from 'cypress/react'
|
||||||
|
|
||||||
|
import './commands'
|
||||||
|
import '../../styles/global.css'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
mount: typeof mount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('mount', mount)
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": true,
|
|
||||||
"types": ["cypress"],
|
|
||||||
"isolatedModules": false
|
|
||||||
},
|
|
||||||
"include": ["../node_modules/cypress", "./**/*.ts"]
|
|
||||||
}
|
|
@ -1,12 +1,11 @@
|
|||||||
version: '3.0'
|
|
||||||
services:
|
services:
|
||||||
divlo.fr:
|
divlo:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
image: 'divlo.fr'
|
image: 'divlo'
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: './'
|
||||||
ports:
|
ports:
|
||||||
- '${PORT}:${PORT}'
|
- '${PORT-3000}:${PORT-3000}'
|
||||||
environment:
|
environment:
|
||||||
PORT: ${PORT}
|
PORT: ${PORT-3000}
|
||||||
env_file: './.env'
|
env_file: '.env'
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
const nextJest = require('next/jest')
|
|
||||||
|
|
||||||
const createJestConfig = nextJest()
|
|
||||||
const customJestConfig = {
|
|
||||||
moduleDirectories: ['node_modules', './'],
|
|
||||||
modulePathIgnorePatterns: ['<rootDir>/cypress'],
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
setupFilesAfterEnv: [
|
|
||||||
'@testing-library/jest-dom/extend-expect',
|
|
||||||
'@testing-library/react'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = createJestConfig(customJestConfig)
|
|
22
jsonresume-theme-custom/.gitignore
vendored
@ -1,4 +1,22 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
theme/index.html
|
|
||||||
dist
|
dist
|
||||||
.parcel-cache
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
20
jsonresume-theme-custom/build.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import { build } from 'vite'
|
||||||
|
|
||||||
|
const jsonResumeThemeCustom = new URL('./', import.meta.url)
|
||||||
|
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
|
||||||
|
const publicResumeOutputURL = new URL(
|
||||||
|
'../public/curriculum-vitae',
|
||||||
|
import.meta.url
|
||||||
|
)
|
||||||
|
|
||||||
|
await build({
|
||||||
|
root: fileURLToPath(jsonResumeThemeCustom),
|
||||||
|
base: '/curriculum-vitae/'
|
||||||
|
})
|
||||||
|
|
||||||
|
await fs.promises.cp(jsonResumeThemeCustomDist, publicResumeOutputURL, {
|
||||||
|
recursive: true
|
||||||
|
})
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |
244
jsonresume-theme-custom/index.html
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr-FR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title><%= locals.basics.name %></title>
|
||||||
|
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
||||||
|
<link rel="stylesheet" href="./styles/global.css" />
|
||||||
|
<script defer type="module" src="./scripts/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row main clearfix">
|
||||||
|
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
|
||||||
|
<div class="card profile-card">
|
||||||
|
<div class="profile-pic-container">
|
||||||
|
<div class="profile-pic">
|
||||||
|
<img
|
||||||
|
class="media-object img-circle center-block"
|
||||||
|
data-src="holder.js/100x100"
|
||||||
|
alt="<%= locals.basics.name %>"
|
||||||
|
src="<%= locals.basics.image %>"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="name-and-profession text-center">
|
||||||
|
<h3>
|
||||||
|
<strong><%= locals.basics.name %></strong>
|
||||||
|
</h3>
|
||||||
|
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
||||||
|
<h5 class="text-muted">
|
||||||
|
<%= locals.basics.age %> (<span id="year-old"></span> ans)
|
||||||
|
</h5>
|
||||||
|
<h5 class="text-muted">
|
||||||
|
<%= locals.basics.location.address %>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-details clearfix">
|
||||||
|
<div class="detail">
|
||||||
|
<span class="info">
|
||||||
|
<a
|
||||||
|
class="link-disguise"
|
||||||
|
href="mailto:<%= locals.basics.email %>"
|
||||||
|
>
|
||||||
|
<%= locals.basics.email %>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail">
|
||||||
|
<span class="info">
|
||||||
|
<a class="link-disguise" href="<%= locals.basics.url %>">
|
||||||
|
<%= locals.basics.url %>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
<div class="card background-card">
|
||||||
|
<div class="background-details">
|
||||||
|
<div class="detail" id="about">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="./images/user.svg" alt="user" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<h4 class="title text-uppercase">À propos</h4>
|
||||||
|
<div class="card card-nested">
|
||||||
|
<div class="content mop-wrapper">
|
||||||
|
<p><%- locals.basics.summary %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section class="section-separated">
|
||||||
|
<div class="detail" id="education">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="./images/graduation-cap.svg" alt="graduation" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<h4 class="title text-uppercase">Formations</h4>
|
||||||
|
<div class="content">
|
||||||
|
<ul class="list-unstyled clear-margin">
|
||||||
|
<% locals.education.forEach((degree) => { %>
|
||||||
|
<li class="card card-nested">
|
||||||
|
<div class="content">
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<strong><%= degree.studyType %></strong>
|
||||||
|
</p>
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<strong><%= degree.score %></strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted clear-margin">
|
||||||
|
<%= degree.institution %>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted clear-margin">
|
||||||
|
<small>
|
||||||
|
<%= degree.startDate %> <%= degree.endDate !=
|
||||||
|
null ? " - " + degree.endDate : "" %>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<% if (degree.courses != null) { %>
|
||||||
|
<ul class="education-courses">
|
||||||
|
<% degree.courses.forEach((course) => { %>
|
||||||
|
<li><%= course %></li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail" id="skills">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="./images/toolbox.svg" alt="toolbox" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<h4 class="title text-uppercase">Compétences</h4>
|
||||||
|
<div class="content">
|
||||||
|
<ul class="list-unstyled clear-margin">
|
||||||
|
<% locals.skills.forEach((skill) => { %>
|
||||||
|
<li class="card card-nested card-skills">
|
||||||
|
<div class="skill-info">
|
||||||
|
<strong><%= skill.name %></strong>
|
||||||
|
<div class="space-top labels">
|
||||||
|
<% skill.keywords.forEach((keyword) => { %>
|
||||||
|
<p class="label label-keyword"><%= keyword %></p>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section class="section-separated">
|
||||||
|
<div class="detail" id="work-experience">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="./images/building-columns.svg" alt="work" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<h4 class="title text-uppercase">Expériences</h4>
|
||||||
|
<ul class="list-unstyled clear-margin">
|
||||||
|
<% locals.work.filter((experience) =>
|
||||||
|
experience.description == null).forEach((experience) => {
|
||||||
|
%>
|
||||||
|
<li class="card card-nested clearfix">
|
||||||
|
<div class="content">
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<a href="<%= experience.website %>">
|
||||||
|
<strong><%= experience.name %></strong>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<strong><%- experience.position %></strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
<small>
|
||||||
|
<span class="space-right">
|
||||||
|
<%= date.format(new Date(experience.startDate),
|
||||||
|
'DD/MM/YYYY') %> - <%= date.format(new
|
||||||
|
Date(experience.endDate), 'DD/MM/YYYY') %> (<%=
|
||||||
|
experience.duration %>)
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<div class="experience-description">
|
||||||
|
<p><%- experience.summary %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail" id="interests">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="./images/heart.svg" alt="heart" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<h4 class="title text-uppercase">Intérets</h4>
|
||||||
|
<div class="content">
|
||||||
|
<ul class="list-unstyled clear-margin">
|
||||||
|
<% locals.interests.forEach((interest) => { %>
|
||||||
|
<li class="card card-nested">
|
||||||
|
<p><strong><%= interest.name %></strong></p>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="list-unstyled clear-margin">
|
||||||
|
<% locals.work.filter((experience) =>
|
||||||
|
experience.description != null).forEach((experience) =>
|
||||||
|
{ %>
|
||||||
|
<li class="card card-nested clearfix">
|
||||||
|
<div class="content">
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<a href="<%= experience.website %>">
|
||||||
|
<strong><%= experience.name %></strong>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="clear-margin relative">
|
||||||
|
<strong><%- experience.position %></strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
<small>
|
||||||
|
<span class="space-right">
|
||||||
|
<%= date.format(new
|
||||||
|
Date(experience.startDate), 'DD/MM/YYYY') %> -
|
||||||
|
<%= date.format(new Date(experience.endDate),
|
||||||
|
'DD/MM/YYYY') %> (<%= experience.duration %>)
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<div class="experience-description">
|
||||||
|
<p><%- experience.summary %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,28 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
import ejs from 'ejs'
|
|
||||||
import date from 'date-and-time'
|
|
||||||
import { Parcel } from '@parcel/core'
|
|
||||||
|
|
||||||
export const render = async (resume) => {
|
|
||||||
const themeIndexURL = new URL('./theme/index.ejs', import.meta.url)
|
|
||||||
const themeBuildURL = new URL('./theme/index.html', import.meta.url)
|
|
||||||
const indexHTMLURL = new URL('./dist/index.html', import.meta.url)
|
|
||||||
const themeBuildPath = fileURLToPath(themeBuildURL)
|
|
||||||
const html = await ejs.renderFile(fileURLToPath(themeIndexURL), {
|
|
||||||
date,
|
|
||||||
locals: {
|
|
||||||
...resume
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await fs.promises.writeFile(themeBuildURL, html, { encoding: 'utf-8' })
|
|
||||||
const bundler = new Parcel({
|
|
||||||
entries: themeBuildPath,
|
|
||||||
source: themeBuildPath,
|
|
||||||
mode: 'production',
|
|
||||||
defaultConfig: '@parcel/config-default'
|
|
||||||
})
|
|
||||||
await bundler.run()
|
|
||||||
return await fs.promises.readFile(indexHTMLURL, { encoding: 'utf-8' })
|
|
||||||
}
|
|
4492
jsonresume-theme-custom/package-lock.json
generated
@ -3,17 +3,19 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-and-time": "2.3.0",
|
"jsonc-parser": "3.2.0",
|
||||||
"ejs": "3.1.6",
|
"modern-normalize": "2.0.0"
|
||||||
"modern-normalize": "1.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/config-default": "2.4.0",
|
"@types/node": "20.2.5",
|
||||||
"@parcel/core": "2.4.0",
|
"date-and-time": "3.0.0",
|
||||||
"@parcel/optimizer-data-url": "2.4.0",
|
"vite": "4.3.9",
|
||||||
"@parcel/transformer-inline-string": "2.4.0",
|
"vite-plugin-html": "3.2.0"
|
||||||
"parcel": "2.4.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
|
|
||||||
import { render } from '../index.js'
|
|
||||||
|
|
||||||
const jsonResumeURL = new URL('../../resume.json', import.meta.url)
|
|
||||||
const publicResumeURL = new URL(
|
|
||||||
'../../public/curriculum-vitae.html',
|
|
||||||
import.meta.url
|
|
||||||
)
|
|
||||||
|
|
||||||
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
|
||||||
encoding: 'utf-8'
|
|
||||||
})
|
|
||||||
const dataResumeJSON = JSON.parse(dataResumeStringJSON)
|
|
||||||
const dataResumeIndexHTML = await render(dataResumeJSON)
|
|
||||||
await fs.promises.writeFile(publicResumeURL, dataResumeIndexHTML, {
|
|
||||||
encoding: 'utf-8'
|
|
||||||
})
|
|
5
jsonresume-theme-custom/scripts/main.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { DIVLO_BIRTH_DATE, getAge } from '../../utils/getAge.ts'
|
||||||
|
|
||||||
|
const yearOld = document.getElementById('year-old')
|
||||||
|
|
||||||
|
yearOld.textContent = getAge(DIVLO_BIRTH_DATE).toString()
|
@ -1,4 +1,4 @@
|
|||||||
@import 'npm:modern-normalize/modern-normalize.css';
|
@import 'modern-normalize/modern-normalize.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
||||||
@ -209,7 +209,6 @@ h5 {
|
|||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
@ -217,8 +216,6 @@ h5 {
|
|||||||
}
|
}
|
||||||
.label-keyword {
|
.label-keyword {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #7eb0db;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #357ebd;
|
border: 1px solid #357ebd;
|
||||||
@ -227,3 +224,6 @@ h5 {
|
|||||||
.label-keyword p {
|
.label-keyword p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.section-separated {
|
||||||
|
display: flex;
|
||||||
|
}
|
@ -1,206 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title><%= locals.basics.name %></title>
|
|
||||||
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@import './styles/global.css';
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row main clearfix">
|
|
||||||
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
|
|
||||||
<div class="card profile-card">
|
|
||||||
<div class="profile-pic-container">
|
|
||||||
<div class="profile-pic">
|
|
||||||
<img
|
|
||||||
class="media-object img-circle center-block"
|
|
||||||
data-src="holder.js/100x100"
|
|
||||||
alt="<%= locals.basics.name %>"
|
|
||||||
src="<%= locals.basics.image %>"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="name-and-profession text-center">
|
|
||||||
<h3>
|
|
||||||
<strong><%= locals.basics.name %></strong>
|
|
||||||
</h3>
|
|
||||||
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="contact-details clearfix">
|
|
||||||
<div class="detail">
|
|
||||||
<span class="info"><%= locals.basics.phone %></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="info">
|
|
||||||
<a
|
|
||||||
class="link-disguise"
|
|
||||||
href="mailto:<%= locals.basics.email %>"
|
|
||||||
>
|
|
||||||
<%= locals.basics.email %>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="info">
|
|
||||||
<a class="link-disguise" href="<%= locals.basics.url %>">
|
|
||||||
<%= locals.basics.url %>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
<div class="card background-card">
|
|
||||||
<div class="background-details">
|
|
||||||
<div class="detail" id="about">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="data-url:./images/user.svg" alt="user" />
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">À propos</h4>
|
|
||||||
<div class="card card-nested">
|
|
||||||
<div class="content mop-wrapper">
|
|
||||||
<p><%- locals.basics.summary %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="detail" id="work-experience">
|
|
||||||
<div class="icon">
|
|
||||||
<img
|
|
||||||
src="data-url:./images/building-columns.svg"
|
|
||||||
alt="work"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">Expériences</h4>
|
|
||||||
<ul class="list-unstyled clear-margin">
|
|
||||||
<% locals.work.forEach((experience) => { %>
|
|
||||||
<li class="card card-nested clearfix">
|
|
||||||
<div class="content">
|
|
||||||
<p class="clear-margin relative">
|
|
||||||
<a href="<%= experience.website %>">
|
|
||||||
<strong><%= experience.name %></strong>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p class="clear-margin relative">
|
|
||||||
<strong><%- experience.position %></strong>
|
|
||||||
</p>
|
|
||||||
<p class="text-muted">
|
|
||||||
<small>
|
|
||||||
<span class="space-right">
|
|
||||||
<%= date.format(new Date(experience.startDate),
|
|
||||||
'DD/MM/YYYY') %> - <%= date.format(new
|
|
||||||
Date(experience.endDate), 'DD/MM/YYYY') %>
|
|
||||||
</span>
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
<div class="experience-description">
|
|
||||||
<p><%- experience.summary %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="detail" id="skills">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="data-url:./images/toolbox.svg" alt="toolbox" />
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">Compétences</h4>
|
|
||||||
<div class="content">
|
|
||||||
<ul class="list-unstyled clear-margin">
|
|
||||||
<% locals.skills.forEach((skill) => { %>
|
|
||||||
<li class="card card-nested card-skills">
|
|
||||||
<div class="skill-info">
|
|
||||||
<strong><%= skill.name %></strong>
|
|
||||||
<div class="space-top labels">
|
|
||||||
<% skill.keywords.forEach((keyword) => { %>
|
|
||||||
<p class="label label-keyword"><%= keyword %></p>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="detail" id="education">
|
|
||||||
<div class="icon">
|
|
||||||
<img
|
|
||||||
src="data-url:./images/graduation-cap.svg"
|
|
||||||
alt="graduation"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">Éducation</h4>
|
|
||||||
<div class="content">
|
|
||||||
<ul class="list-unstyled clear-margin">
|
|
||||||
<% locals.education.forEach((degree) => { %>
|
|
||||||
<li class="card card-nested">
|
|
||||||
<div class="content">
|
|
||||||
<p class="clear-margin relative">
|
|
||||||
<strong><%= degree.studyType %></strong>
|
|
||||||
</p>
|
|
||||||
<p class="clear-margin relative">
|
|
||||||
<strong><%= degree.score %></strong>
|
|
||||||
</p>
|
|
||||||
<p class="text-muted clear-margin">
|
|
||||||
<%= degree.institution %>
|
|
||||||
</p>
|
|
||||||
<p class="text-muted clear-margin">
|
|
||||||
<small>
|
|
||||||
<%= degree.startDate %> - <%= degree.endDate %>
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="detail" id="interests">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="data-url:./images/heart.svg" alt="heart" />
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">Intérets</h4>
|
|
||||||
<div class="content">
|
|
||||||
<ul class="list-unstyled clear-margin">
|
|
||||||
<% locals.interests.forEach((interest) => { %>
|
|
||||||
<li class="card card-nested">
|
|
||||||
<p><strong><%= interest.name %></strong></p>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
36
jsonresume-theme-custom/vite.config.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { parse as JSONCParser } from 'jsonc-parser'
|
||||||
|
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||||
|
import date from 'date-and-time'
|
||||||
|
|
||||||
|
const jsonResumeURL = new URL('../resume.jsonc', import.meta.url)
|
||||||
|
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
||||||
|
encoding: 'utf-8'
|
||||||
|
})
|
||||||
|
const resume = JSONCParser(dataResumeStringJSON)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documentation: <https://vitejs.dev/config/>
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
assetsDir: './'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
createHtmlPlugin({
|
||||||
|
inject: {
|
||||||
|
data: {
|
||||||
|
date,
|
||||||
|
locals: {
|
||||||
|
...resume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
postcss: {}
|
||||||
|
}
|
||||||
|
})
|
@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"i-am": "I am",
|
"description": "Developer Full Stack • Open-Source enthusiast",
|
||||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
"pronouns": "Pronouns",
|
||||||
"full-name": "Full name",
|
"pronouns-value": "He/Him",
|
||||||
"birth-date": "Birth date",
|
"birth-date": "Birth date",
|
||||||
"years-old": "years old",
|
"years-old": "years old",
|
||||||
"nationality": "Nationality",
|
"nationality": "Nationality",
|
||||||
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (first year)."
|
"description-bottom": "I am a student in computer science following the French training \"BUT Informatique\" and I am also a self-taught."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interests",
|
"title": "Interests",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Developer Full Stack Junior",
|
"title": "Developer Full Stack",
|
||||||
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Open-Source enthusiast",
|
||||||
|
"description": "For me, everyone should work, solve problems, build things and think together.<br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionate about High-Tech",
|
"title": "Passionate about High-Tech",
|
||||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Open-Source enthusiast",
|
|
||||||
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
"return-to-home-page": "Revenir à la page d'accueil ?",
|
"return-to-home-page": "Revenir à la page d'accueil ?",
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"server-error": "Erreur Interne du Serveur !",
|
"server-error": "Erreur Interne du Serveur !",
|
||||||
"not-found": "Cette page n'existe pas!"
|
"not-found": "Cette page n'existe pas !"
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"i-am": "Je suis",
|
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
|
||||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
"pronouns": "Pronoms",
|
||||||
"full-name": "Prénom NOM",
|
"pronouns-value": "Il/Lui",
|
||||||
"birth-date": "Date de naissance",
|
"birth-date": "Date de naissance",
|
||||||
"years-old": "ans",
|
"years-old": "ans",
|
||||||
"nationality": "Nationalité",
|
"nationality": "Nationalité",
|
||||||
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année)."
|
"description-bottom": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Intérêts",
|
"title": "Intérêts",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Développeur Full Stack Junior",
|
"title": "Développeur Full Stack",
|
||||||
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Enthousiaste de l'Open-Source",
|
||||||
|
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionné de High-Tech",
|
"title": "Passionné de High-Tech",
|
||||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Enthousiaste de l'Open-Source",
|
|
||||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -1,41 +1,9 @@
|
|||||||
const nextPWA = require('next-pwa')
|
const nextTranslate = require('next-translate-plugin')
|
||||||
const nextTranslate = require('next-translate')
|
|
||||||
const { createSecureHeaders } = require('next-secure-headers')
|
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
module.exports = nextTranslate(
|
const nextConfig = {
|
||||||
nextPWA({
|
reactStrictMode: true,
|
||||||
reactStrictMode: true,
|
output: 'standalone'
|
||||||
pwa: {
|
}
|
||||||
disable: process.env.NODE_ENV !== 'production',
|
|
||||||
dest: 'public'
|
module.exports = nextTranslate(nextConfig)
|
||||||
},
|
|
||||||
headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: createSecureHeaders({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
scriptSrc: [
|
|
||||||
"'self'",
|
|
||||||
'data:',
|
|
||||||
"'unsafe-eval'",
|
|
||||||
"'unsafe-inline'"
|
|
||||||
],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
imgSrc: ['*', 'data:', 'blob:'],
|
|
||||||
mediaSrc: "'none'",
|
|
||||||
connectSrc: '*',
|
|
||||||
objectSrc: "'none'",
|
|
||||||
fontSrc: "'self'",
|
|
||||||
baseURI: "'none'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
42125
package-lock.json
generated
138
package.json
@ -1,102 +1,100 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "divlo",
|
||||||
"version": "2.2.1",
|
"version": "2.7.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Divlo/Divlo"
|
"url": "https://github.com/Divlo/Divlo"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0",
|
"node": ">=16.0.0",
|
||||||
"npm": ">=7.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"start": "next start",
|
"start": "node .next/standalone/server.js",
|
||||||
"build": "npm run resume:build && next build",
|
"build": "npm run resume:build && next build",
|
||||||
"export": "next export",
|
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint \"**/*.{md,mdx}\" --dot --ignore-path \".gitignore\"",
|
"lint:markdown": "markdownlint-cli2",
|
||||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
"lint:eslint": "eslint \".\" --ignore-path \".gitignore\"",
|
||||||
"lint:prettier": "prettier \".\" --check",
|
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test:unit": "jest",
|
"test:unit": "cypress run --component",
|
||||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"",
|
||||||
"test:lighthouse": "lhci autorun",
|
"test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
|
||||||
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"",
|
||||||
"test:e2e:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
"resume:build": "node ./jsonresume-theme-custom/build.js",
|
||||||
"resume:build": "node ./jsonresume-theme-custom/scripts/build.js",
|
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
"deploy": "vercel",
|
"deploy": "vercel",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/montserrat": "4.5.7",
|
"@fontsource/montserrat": "5.0.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.1.1",
|
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.1.18",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"classnames": "2.3.1",
|
"@giscus/react": "2.2.8",
|
||||||
"date-and-time": "2.3.0",
|
"clsx": "1.2.1",
|
||||||
|
"date-and-time": "3.0.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"html-react-parser": "1.4.9",
|
"html-react-parser": "3.0.16",
|
||||||
"next": "12.1.0",
|
"katex": "0.16.7",
|
||||||
"next-mdx-remote": "4.0.0",
|
"next": "13.4.4",
|
||||||
"next-pwa": "5.4.6",
|
"next-mdx-remote": "4.4.1",
|
||||||
"next-themes": "0.1.1",
|
"next-themes": "0.2.1",
|
||||||
"next-translate": "1.3.5",
|
"next-translate": "2.0.5",
|
||||||
"react": "17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"read-pkg": "7.1.0",
|
"read-pkg": "8.0.0",
|
||||||
|
"rehype-katex": "6.0.3",
|
||||||
"rehype-raw": "6.1.1",
|
"rehype-raw": "6.1.1",
|
||||||
"rehype-slug": "5.0.1",
|
"rehype-slug": "5.1.0",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"sharp": "0.30.3",
|
"remark-math": "5.1.1",
|
||||||
"shiki": "0.10.1",
|
"sharp": "0.32.1",
|
||||||
|
"shiki": "0.14.2",
|
||||||
"unified": "10.1.2",
|
"unified": "10.1.2",
|
||||||
"unist-util-visit": "4.1.0",
|
"unist-util-visit": "4.1.2",
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "16.2.3",
|
"@commitlint/cli": "17.6.3",
|
||||||
"@commitlint/config-conventional": "16.2.1",
|
"@commitlint/config-conventional": "17.6.3",
|
||||||
"@lhci/cli": "0.9.0",
|
"@saithodev/semantic-release-backmerge": "3.2.0",
|
||||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
|
||||||
"@semantic-release/git": "10.0.1",
|
"@semantic-release/git": "10.0.1",
|
||||||
"@tailwindcss/typography": "0.5.2",
|
"@tailwindcss/typography": "0.5.9",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@tsconfig/strictest": "2.0.1",
|
||||||
"@testing-library/react": "12.1.4",
|
"@types/node": "20.2.5",
|
||||||
"@types/jest": "27.4.1",
|
"@types/react": "18.2.7",
|
||||||
"@types/node": "17.0.23",
|
|
||||||
"@types/react": "17.0.42",
|
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.16.0",
|
"@typescript-eslint/eslint-plugin": "5.59.7",
|
||||||
"autoprefixer": "10.4.4",
|
"autoprefixer": "10.4.14",
|
||||||
"cypress": "9.5.2",
|
"cypress": "12.13.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "5.0.1",
|
||||||
"eslint": "8.11.0",
|
"eslint": "8.41.0",
|
||||||
"eslint-config-conventions": "1.1.2",
|
"eslint-config-conventions": "9.0.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "13.4.4",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.1.1",
|
||||||
"eslint-plugin-unicorn": "41.0.1",
|
"eslint-plugin-unicorn": "47.0.0",
|
||||||
"html-w3c-validator": "1.1.0",
|
"html-w3c-validator": "1.3.0",
|
||||||
"husky": "7.0.4",
|
"husky": "8.0.3",
|
||||||
"jest": "27.5.1",
|
|
||||||
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
||||||
"lint-staged": "12.3.7",
|
"lint-staged": "13.2.2",
|
||||||
"markdownlint-cli": "0.31.1",
|
"markdownlint-cli2": "0.7.1",
|
||||||
"next-secure-headers": "2.2.0",
|
"markdownlint-rule-relative-links": "1.2.0",
|
||||||
"postcss": "8.4.12",
|
"next-translate-plugin": "2.0.5",
|
||||||
"prettier": "2.6.0",
|
"postcss": "8.4.24",
|
||||||
"prettier-plugin-tailwindcss": "0.1.8",
|
"prettier": "2.8.8",
|
||||||
"semantic-release": "19.0.2",
|
"prettier-plugin-tailwindcss": "0.3.0",
|
||||||
"start-server-and-test": "1.14.0",
|
"semantic-release": "21.0.2",
|
||||||
"tailwindcss": "3.0.23",
|
"start-server-and-test": "2.0.0",
|
||||||
"typescript": "4.6.2",
|
"tailwindcss": "3.3.2",
|
||||||
"vercel": "24.0.1"
|
"typescript": "5.0.4",
|
||||||
|
"vercel": "30.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import type { FooterProps } from 'components/Footer'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
|
||||||
|
|
||||||
interface Error404Props extends FooterProps {}
|
interface Error404Props extends FooterProps {}
|
||||||
|
|
||||||
@ -14,13 +13,12 @@ const Error404: NextPage<Error404Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='404 | Divlo' />
|
<Head title='404 | Théo LUDWIG (Divlo)' />
|
||||||
|
<ErrorPage
|
||||||
<Header showLanguage />
|
statusCode={404}
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
message={t('errors:not-found')}
|
||||||
<ErrorPage statusCode={404} message={t('errors:not-found')} />
|
version={version}
|
||||||
</main>
|
/>
|
||||||
<Footer version={version} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import type { FooterProps } from 'components/Footer'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
|
||||||
|
|
||||||
interface Error500Props extends FooterProps {}
|
interface Error500Props extends FooterProps {}
|
||||||
|
|
||||||
@ -14,13 +13,12 @@ const Error500: NextPage<Error500Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='500 | Divlo' />
|
<Head title='500 | Théo LUDWIG (Divlo)' />
|
||||||
|
<ErrorPage
|
||||||
<Header showLanguage />
|
statusCode={500}
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
message={t('errors:server-error')}
|
||||||
<ErrorPage statusCode={500} message={t('errors:server-error')} />
|
version={version}
|
||||||
</main>
|
/>
|
||||||
<Footer version={version} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AppProps } from 'next/app'
|
import type { AppType } from 'next/app'
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import UniversalCookie from 'universal-cookie'
|
import UniversalCookie from 'universal-cookie'
|
||||||
@ -13,7 +13,7 @@ const universalCookie = new UniversalCookie()
|
|||||||
/** how long in seconds, until the cookie expires (10 years) */
|
/** how long in seconds, until the cookie expires (10 years) */
|
||||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||||
|
|
||||||
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
|
const Application: AppType = ({ Component, pageProps }) => {
|
||||||
const { lang } = useTranslation()
|
const { lang } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||||
|
import Image from 'next/image'
|
||||||
import { MDXRemote } from 'next-mdx-remote'
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
import Giscus from '@giscus/react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
import type { Post } from 'utils/blog'
|
import type { Post } from 'utils/blog'
|
||||||
|
|
||||||
interface BlogPostPageProps extends FooterProps {
|
interface BlogPostPageProps extends FooterProps {
|
||||||
@ -14,35 +20,68 @@ interface BlogPostPageProps extends FooterProps {
|
|||||||
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||||
const { version, post } = props
|
const { version, post } = props
|
||||||
|
|
||||||
|
const { theme = 'dark' } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head
|
<Head
|
||||||
title={`${post.frontmatter.title} | Divlo`}
|
title={`${post.frontmatter.title} | Théo LUDWIG (Divlo)`}
|
||||||
description={post.frontmatter.description}
|
description={post.frontmatter.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
|
||||||
<div className='my-10 flex flex-col items-center'>
|
<div className='my-10 flex flex-col items-center text-center'>
|
||||||
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
||||||
<p className='mt-2' data-cy='blog-post-date'>
|
<p className='mt-2' data-cy='blog-post-date'>
|
||||||
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='prose mb-10 px-8'>
|
<div className='prose mb-10'>
|
||||||
<MDXRemote
|
<div className='px-8'>
|
||||||
{...post.source}
|
<MDXRemote
|
||||||
components={{
|
{...post.source}
|
||||||
a: (props: React.ComponentPropsWithoutRef<'a'>) => {
|
components={{
|
||||||
if (props.href?.startsWith('#') ?? false) {
|
img: (properties) => {
|
||||||
return <a {...props} />
|
const { src = '', alt = 'Blog Image' } = properties
|
||||||
|
const source = src.replace('../public/', '/')
|
||||||
|
return (
|
||||||
|
<span className='flex flex-col items-center justify-center'>
|
||||||
|
<Image
|
||||||
|
src={source}
|
||||||
|
alt={alt}
|
||||||
|
width={1000}
|
||||||
|
height={1000}
|
||||||
|
className='h-auto w-auto'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
a: (props) => {
|
||||||
|
if (props.href?.startsWith('#') ?? false) {
|
||||||
|
return <a {...props} />
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a target='_blank' rel='noopener noreferrer' {...props} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return (
|
}}
|
||||||
<a target='_blank' rel='noopener noreferrer' {...props} />
|
/>
|
||||||
)
|
<Giscus
|
||||||
}
|
id='comments'
|
||||||
}}
|
repo='Divlo/Divlo'
|
||||||
/>
|
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
||||||
|
category='General'
|
||||||
|
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
||||||
|
mapping='pathname'
|
||||||
|
reactionsEnabled='1'
|
||||||
|
emitMetadata='0'
|
||||||
|
inputPosition='top'
|
||||||
|
theme={theme}
|
||||||
|
lang='en'
|
||||||
|
loading='lazy'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer version={version} />
|
<Footer version={version} />
|
||||||
@ -53,7 +92,7 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
|||||||
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const slug = context?.params?.slug
|
const slug = context?.params?.['slug']
|
||||||
const { getPostBySlug } = await import('utils/blog')
|
const { getPostBySlug } = await import('utils/blog')
|
||||||
const post = await getPostBySlug(slug)
|
const post = await getPostBySlug(slug)
|
||||||
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||||
import type { PostMetadata } from 'utils/blog'
|
import type { PostMetadata } from 'utils/blog'
|
||||||
|
|
||||||
@ -38,23 +39,26 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
|
|||||||
'DD/MM/YYYY'
|
'DD/MM/YYYY'
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Link href={`/blog/${post.slug}`} key={index} locale='en'>
|
<Link
|
||||||
<a data-cy={post.slug}>
|
href={`/blog/${post.slug}`}
|
||||||
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
key={index}
|
||||||
<h2
|
locale='en'
|
||||||
data-cy='blog-post-title'
|
data-cy={post.slug}
|
||||||
className='text-xl font-semibold'
|
>
|
||||||
>
|
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
||||||
{post.frontmatter.title}
|
<h2
|
||||||
</h2>
|
data-cy='blog-post-title'
|
||||||
<p data-cy='blog-post-date' className='mt-2'>
|
className='text-xl font-semibold'
|
||||||
{postPublishedOn}
|
>
|
||||||
</p>
|
{post.frontmatter.title}
|
||||||
<p data-cy='blog-post-description' className='mt-3'>
|
</h2>
|
||||||
{post.frontmatter.description}
|
<p data-cy='blog-post-date' className='mt-2'>
|
||||||
</p>
|
{postPublishedOn}
|
||||||
</ShadowContainer>
|
</p>
|
||||||
</a>
|
<p data-cy='blog-post-description' className='mt-3'>
|
||||||
|
{post.frontmatter.description}
|
||||||
|
</p>
|
||||||
|
</ShadowContainer>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { RevealFade } from 'components/design/RevealFade'
|
import { RevealFade } from 'components/design/RevealFade'
|
||||||
@ -11,7 +11,8 @@ import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
|||||||
import { Skills } from 'components/Skills'
|
import { Skills } from 'components/Skills'
|
||||||
import { OpenSource } from 'components/OpenSource'
|
import { OpenSource } from 'components/OpenSource'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
|
|
||||||
interface HomeProps extends FooterProps {}
|
interface HomeProps extends FooterProps {}
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@ publishedOn: '2022-02-23T08:00:18.758Z'
|
|||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
|
||||||
Have you already heard of "**Clean Code**" or "**Design Patterns**" ?
|
Have you already heard of "**Clean Code**" or "**Design Patterns**"?
|
||||||
|
|
||||||
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
|
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
|
||||||
|
|
||||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||||
|
|
||||||
## Definition : Clean Code
|
## Definition: Clean Code
|
||||||
|
|
||||||
A clean code is a code that is **easy** to **read** and easy to **understand**.
|
A clean code is a code that is **easy** to **read** and easy to **understand**.
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ We could ask ourselves, what is **easy** to **read** and easy to **understand**
|
|||||||
|
|
||||||
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
|
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
|
||||||
|
|
||||||
## Why is it so important ?
|
## Why is it so important?
|
||||||
|
|
||||||
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ For example the [Linux kernel](https://www.kernel.org/), is one of the biggest o
|
|||||||
|
|
||||||
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
|
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
|
||||||
|
|
||||||
## Definition : Design Patterns
|
## Definition: Design Patterns
|
||||||
|
|
||||||
These **rules** and **conventions** are so called **Design Patterns**.
|
These **rules** and **conventions** are so called **Design Patterns**.
|
||||||
|
|
||||||
@ -77,8 +77,8 @@ setTimeout(restart, 86400000)
|
|||||||
##### Example (good way)
|
##### Example (good way)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
|
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000
|
||||||
setTimeout(restart, MILLISECONDS_IN_A_DAY)
|
setTimeout(restart, MILLISECONDS_IN_ONE_DAY)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -131,7 +131,9 @@ const printCar = (car: Car): void => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Boolean names (Prefix: is, has, can)
|
#### Boolean names
|
||||||
|
|
||||||
|
The name of a boolean variable should be a question, and the answer should be true or false. We can use prefixes like `is`, `has`, `can` to make it more explicit and we should avoid negation.
|
||||||
|
|
||||||
##### Example (bad way)
|
##### Example (bad way)
|
||||||
|
|
||||||
@ -171,7 +173,10 @@ We have to keep it as simple as possible, not to implement features that are not
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const createFile = async (name: string, isTemporary: boolean = false) => {
|
const createFile = async (
|
||||||
|
name: string,
|
||||||
|
isTemporary: boolean = false
|
||||||
|
): Promise<void> => {
|
||||||
if (isTemporary) {
|
if (isTemporary) {
|
||||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
return await fs.promises.writeFile(path.join('temporary', name), '')
|
||||||
}
|
}
|
||||||
@ -187,11 +192,11 @@ const createFile = async (name: string, isTemporary: boolean = false) => {
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const createFile = async (name: string) => {
|
const createFile = async (name: string): Promise<void> => {
|
||||||
await fs.promises.writeFile(name, '')
|
await fs.promises.writeFile(name, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTemporaryFile = async (name: string) => {
|
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||||
await createFile(path.join('temporary', name))
|
await createFile(path.join('temporary', name))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -210,7 +215,7 @@ The End To End (e2e) and Unit tests should document what is the behavior intende
|
|||||||
|
|
||||||
### Avoid comments
|
### Avoid comments
|
||||||
|
|
||||||
One of the most important rule of "Clean Code" : If you need to add **comments**, it's because your code is **not clean**.
|
One of the most important rule of "Clean Code": If you need to add **comments**, it's because your code is **not clean**.
|
||||||
|
|
||||||
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.
|
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.
|
||||||
|
|