Compare commits
72 Commits
Author | SHA1 | Date | |
---|---|---|---|
117c41b1c3
|
|||
b92704b77d
|
|||
bab7581283
|
|||
988fceb2aa
|
|||
5211ba1489
|
|||
6886480cef
|
|||
d78e50638e
|
|||
3b76195d71
|
|||
2dc63ba933
|
|||
336f067c52
|
|||
5fd7f77b6d
|
|||
db0c708c04
|
|||
9d44671fed
|
|||
7bcc5f972c | |||
61172d59e3 | |||
7c0f11ab7d | |||
670897fa78 | |||
b88246b668 | |||
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": "theoludwig",
|
||||||
"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=theoludwig
|
||||||
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.3'
|
||||||
|
|
||||||
- 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.3'
|
||||||
|
|
||||||
- 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.3'
|
||||||
|
|
||||||
- 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.3'
|
||||||
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.3'
|
||||||
|
|
||||||
- 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.3'
|
||||||
|
|
||||||
- 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@theoludwig.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
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
# 💡 Contributing
|
# 💡 Contributing
|
||||||
|
|
||||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
Thanks a lot for your interest in contributing to **theoludwig.fr**! 🎉
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
**theoludwig.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
|
||||||
|
|
||||||
@ -11,43 +15,21 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
|||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
- **Please first discuss** the change you wish to make via [issue](https://github.com/theoludwig/theoludwig/issues) before making a change. It might avoid a waste of your time.
|
||||||
|
|
||||||
- Ensure your code respect linting.
|
- Ensure your code respect linting.
|
||||||
|
|
||||||
- Make sure your **code passes the tests**.
|
- Make sure your **code passes the tests**.
|
||||||
|
|
||||||
If you're adding new features to **divlo.fr**, please include tests.
|
If you're adding new features to **theoludwig.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
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
[](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@ -58,10 +40,10 @@ Scopes define what part of the code changed.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/Divlo/Divlo.git
|
git clone git@github.com:theoludwig/theoludwig.git
|
||||||
|
|
||||||
# Go to the project root
|
# Go to the project root
|
||||||
cd Divlo
|
cd theoludwig
|
||||||
|
|
||||||
# Configure environment variables
|
# Configure environment variables
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@ -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.1 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.1 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}"]
|
|
||||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) Divlo
|
Copyright (c) Théo LUDWIG
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
40
README.md
@ -1,18 +1,18 @@
|
|||||||
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
|
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</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">
|
||||||
<a href="https://github.com/Divlo"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
|
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
|
||||||
<a href="https://gitlab.com/Divlo"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
|
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
|
||||||
<a href="https://www.npmjs.com/~divlo"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
|
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
|
||||||
<a href="https://twitter.com/Divlo_FR"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
|
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
|
||||||
<a href="https://www.youtube.com/channel/UCfEKQzI3c8vmZOrsTOi5spA"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
|
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
|
||||||
<a href="https://www.twitch.tv/divlo"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
|
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
|
||||||
<a href="https://www.divlo.fr"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
|
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
|
||||||
<a href="mailto:contact@divlo.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@divlo.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
|
<a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
@ -21,20 +21,16 @@
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Divlo",
|
"name": "Théo LUDWIG",
|
||||||
"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", "Arch Linux", "Visual Studio Code", "Git", "Docker"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -44,6 +40,6 @@
|
|||||||
## 📈 Statistics
|
## 📈 Statistics
|
||||||
|
|
||||||
<p align=center>
|
<p align=center>
|
||||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
|
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" />
|
||||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=Divlo&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,22 +11,24 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
|||||||
const { version } = props
|
const { version } = props
|
||||||
|
|
||||||
const versionLink = useMemo(() => {
|
const versionLink = useMemo(() => {
|
||||||
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
|
||||||
}, [version])
|
}, [version])
|
||||||
|
|
||||||
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
|
||||||
</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,10 +9,10 @@ interface HeadProps {
|
|||||||
|
|
||||||
export const Head: React.FC<HeadProps> = (props) => {
|
export const Head: React.FC<HeadProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
title = 'Divlo',
|
title = 'Théo LUDWIG',
|
||||||
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
image = 'https://theoludwig.fr/images/icon-96x96.png',
|
||||||
description = 'Divlo - Developer Full Stack Junior • Passionate about High-Tech',
|
description = 'Théo LUDWIG - Developer Full Stack • Passionate about High-Tech',
|
||||||
url = 'https://divlo.fr/'
|
url = 'https://theoludwig.fr/'
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -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 */}
|
||||||
@ -40,18 +40,6 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
<meta name='twitter:description' content={description} />
|
<meta name='twitter:description' content={description} />
|
||||||
<meta name='twitter:title' content={title} />
|
<meta name='twitter:title' content={title} />
|
||||||
<meta name='twitter:image' content={image} />
|
<meta name='twitter:image' content={image} />
|
||||||
|
|
||||||
{/* Google Verification */}
|
|
||||||
<meta
|
|
||||||
name='google-site-verification'
|
|
||||||
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/icon_small.png'
|
||||||
src='/images/divlo_icon_small.png'
|
alt='Théo LUDWIG'
|
||||||
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
|
||||||
</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 = () => {
|
||||||
|
@ -12,22 +12,22 @@ export const OpenSource: React.FC = () => {
|
|||||||
<Repository
|
<Repository
|
||||||
name='nodejs/node'
|
name='nodejs/node'
|
||||||
description='Node.js JavaScript runtime 🐢🚀'
|
description='Node.js JavaScript runtime 🐢🚀'
|
||||||
href='https://github.com/nodejs/node/commits?author=Divlo'
|
href='https://github.com/nodejs/node/commits?author=theoludwig'
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='standard/standard'
|
name='standard/standard'
|
||||||
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
||||||
href='https://github.com/standard/standard/commits?author=Divlo'
|
href='https://github.com/standard/standard/commits?author=theoludwig'
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='nrwl/nx'
|
name='nrwl/nx'
|
||||||
description='Smart, Extensible Build Framework'
|
description='Smart, Extensible Build Framework'
|
||||||
href='https://github.com/nrwl/nx/commits?author=Divlo'
|
href='https://github.com/nrwl/nx/commits?author=theoludwig'
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='vercel/next.js'
|
name='vercel/next.js'
|
||||||
description='The React Framework for Production'
|
description='The React Framework for Production'
|
||||||
href='https://github.com/vercel/next.js/commits?author=Divlo'
|
href='https://github.com/vercel/next.js/commits?author=theoludwig'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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
|
||||||
<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 { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from 'utils/getAge'
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from './ProfileItem'
|
||||||
|
|
||||||
@ -9,21 +9,24 @@ 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(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={`${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
|
||||||
title='Email'
|
title='Email'
|
||||||
value='contact@divlo.fr'
|
value='contact@theoludwig.fr'
|
||||||
link='mailto:contact@divlo.fr'
|
link='mailto:contact@theoludwig.fr'
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import DivloLogo from 'public/images/divlo_logo.png'
|
import Logo from 'public/images/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={Logo} alt='Théo LUDWIG' 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 (
|
||||||
|
@ -10,28 +10,34 @@ import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
|||||||
export const SocialMediaList: React.FC = () => {
|
export const SocialMediaList: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
||||||
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
|
<SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'>
|
||||||
<GitHubIcon />
|
<GitHubIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://gitlab.com/Divlo' ariaLabel='GitLab'>
|
<SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'>
|
||||||
<GitLabIcon />
|
<GitLabIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://www.npmjs.com/~divlo' ariaLabel='NPM'>
|
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='NPM'>
|
||||||
<NPMIcon />
|
<NPMIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
|
<SocialMediaItem
|
||||||
|
link='https://twitter.com/theoludwig_'
|
||||||
|
ariaLabel='Twitter'
|
||||||
|
>
|
||||||
<TwitterIcon />
|
<TwitterIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem
|
<SocialMediaItem
|
||||||
link='https://www.youtube.com/c/Divlo'
|
link='https://www.youtube.com/@theo_ludwig'
|
||||||
ariaLabel='YouTube'
|
ariaLabel='YouTube'
|
||||||
>
|
>
|
||||||
<YouTubeIcon />
|
<YouTubeIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
|
<SocialMediaItem
|
||||||
|
link='https://www.twitch.tv/theoludwig'
|
||||||
|
ariaLabel='Twitch'
|
||||||
|
>
|
||||||
<TwitchIcon />
|
<TwitchIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
|
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'>
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -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-16 w-16'
|
||||||
|
quality={100}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
alt={skill}
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
<p className='mt-1'>{skill}</p>
|
<p className='mt-1'>{skill}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -9,30 +9,29 @@ export const Skills: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SkillsSection title={t('home:skills.languages')}>
|
<SkillsSection title={t('home:skills.languages')}>
|
||||||
<SkillComponent skill='JavaScript' />
|
|
||||||
<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='Frontend'>
|
||||||
<SkillComponent skill='HTML' />
|
<SkillComponent skill='HTML' />
|
||||||
<SkillComponent skill='CSS' />
|
<SkillComponent skill='CSS' />
|
||||||
<SkillComponent skill='Tailwind CSS' />
|
<SkillComponent skill='Tailwind CSS' />
|
||||||
<SkillComponent skill='React.js (+ Next.js)' />
|
<SkillComponent skill='React.js (+ Next.js)' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Back-end'>
|
<SkillsSection title='Backend'>
|
||||||
|
<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')}>
|
||||||
<SkillComponent skill='GNU/Linux' />
|
<SkillComponent skill='GNU/Linux' />
|
||||||
<SkillComponent skill='Ubuntu' />
|
<SkillComponent skill='Arch Linux' />
|
||||||
<SkillComponent skill='Visual Studio Code' />
|
<SkillComponent skill='Visual Studio Code' />
|
||||||
<SkillComponent skill='Git' />
|
<SkillComponent skill='Git' />
|
||||||
<SkillComponent skill='Docker' />
|
<SkillComponent skill='Docker' />
|
||||||
|
@ -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'
|
||||||
@ -84,20 +88,20 @@ export const skills: Skills = {
|
|||||||
},
|
},
|
||||||
'Visual Studio Code': {
|
'Visual Studio Code': {
|
||||||
link: 'https://code.visualstudio.com/',
|
link: 'https://code.visualstudio.com/',
|
||||||
image: '/images/skills/Visual_Studio_Code.png'
|
image: '/images/skills/VisualStudioCode.png'
|
||||||
},
|
},
|
||||||
Git: {
|
Git: {
|
||||||
link: 'https://git-scm.com/',
|
link: 'https://git-scm.com/',
|
||||||
image: '/images/skills/Git.png'
|
image: '/images/skills/Git.png'
|
||||||
},
|
},
|
||||||
Hyper: {
|
|
||||||
link: 'https://hyper.is/',
|
|
||||||
image: '/images/skills/Hyper.svg'
|
|
||||||
},
|
|
||||||
Ubuntu: {
|
Ubuntu: {
|
||||||
link: 'https://ubuntu.com/',
|
link: 'https://ubuntu.com/',
|
||||||
image: '/images/skills/Ubuntu.png'
|
image: '/images/skills/Ubuntu.png'
|
||||||
},
|
},
|
||||||
|
'Arch Linux': {
|
||||||
|
link: 'https://archlinux.org/',
|
||||||
|
image: '/images/skills/ArchLinux.png'
|
||||||
|
},
|
||||||
'GNU/Linux': {
|
'GNU/Linux': {
|
||||||
link: 'https://www.gnu.org/',
|
link: 'https://www.gnu.org/',
|
||||||
image: '/images/skills/GNU-Linux.png'
|
image: '/images/skills/GNU-Linux.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('Théo LUDWIG')
|
||||||
|
.get('[data-cy=version-link]')
|
||||||
|
.should('have.text', version)
|
||||||
|
.should(
|
||||||
|
'have.attr',
|
||||||
|
'href',
|
||||||
|
`https://github.com/theoludwig/theoludwig/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('Théo LUDWIG')
|
||||||
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('Théo LUDWIG')
|
||||||
})
|
})
|
||||||
|
|
||||||
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 {}
|
@ -3,7 +3,7 @@ describe('Page /blog/[slug]', () => {
|
|||||||
cy.visit('/blog/hello-world')
|
cy.visit('/blog/hello-world')
|
||||||
cy.get('[data-cy=language-flag-text]').should('not.exist')
|
cy.get('[data-cy=language-flag-text]').should('not.exist')
|
||||||
cy.get('h1').should('have.text', '👋 Hello, world!')
|
cy.get('h1').should('have.text', '👋 Hello, world!')
|
||||||
cy.get('.prose a').should('have.attr', 'target', '_blank')
|
cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should redirect to /404 if the blog post doesn't exist", () => {
|
it("should redirect to /404 if the blog post doesn't exist", () => {
|
||||||
@ -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:
|
theoludwig:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
image: 'divlo.fr'
|
image: 'theoludwig'
|
||||||
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' })
|
|
||||||
}
|
|
4477
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.3.3",
|
||||||
"@parcel/core": "2.4.0",
|
"date-and-time": "3.0.2",
|
||||||
"@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 { BIRTH_DATE, getAge } from '../../utils/getAge.ts'
|
||||||
|
|
||||||
|
const yearOld = document.getElementById('year-old')
|
||||||
|
|
||||||
|
yearOld.textContent = getAge(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/theoludwig/theoludwig' 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/theoludwig/theoludwig' 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'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
42411
package-lock.json
generated
140
package.json
@ -1,102 +1,100 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "theoludwig",
|
||||||
"version": "2.2.1",
|
"version": "2.12.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Divlo/Divlo"
|
"url": "https://github.com/theoludwig/theoludwig"
|
||||||
},
|
},
|
||||||
"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": "next start",
|
||||||
"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.3",
|
||||||
"@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.3.0",
|
||||||
"date-and-time": "2.3.0",
|
"clsx": "1.2.1",
|
||||||
|
"date-and-time": "3.0.2",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"html-react-parser": "1.4.9",
|
"html-react-parser": "4.0.0",
|
||||||
"next": "12.1.0",
|
"katex": "0.16.8",
|
||||||
"next-mdx-remote": "4.0.0",
|
"next": "13.4.7",
|
||||||
"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.4.4",
|
||||||
"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.3",
|
||||||
"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.6",
|
||||||
"@commitlint/config-conventional": "16.2.1",
|
"@commitlint/config-conventional": "17.6.6",
|
||||||
"@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.3.3",
|
||||||
"@types/jest": "27.4.1",
|
"@types/react": "18.2.14",
|
||||||
"@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.60.1",
|
||||||
"autoprefixer": "10.4.4",
|
"autoprefixer": "10.4.14",
|
||||||
"cypress": "9.5.2",
|
"cypress": "12.16.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "5.1.1",
|
||||||
"eslint": "8.11.0",
|
"eslint": "8.44.0",
|
||||||
"eslint-config-conventions": "1.1.2",
|
"eslint-config-conventions": "10.0.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "13.4.7",
|
||||||
"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.4.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.3",
|
||||||
"markdownlint-cli": "0.31.1",
|
"markdownlint-cli2": "0.8.1",
|
||||||
"next-secure-headers": "2.2.0",
|
"markdownlint-rule-relative-links": "2.1.0",
|
||||||
"postcss": "8.4.12",
|
"next-translate-plugin": "2.4.4",
|
||||||
"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.6",
|
||||||
"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": "31.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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' />
|
||||||
|
<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' />
|
||||||
|
<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,48 +1,116 @@
|
|||||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
import { MDXRemote } from 'next-mdx-remote'
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||||
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 {
|
||||||
post: Post
|
post: Post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Heading = (
|
||||||
|
props: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>,
|
||||||
|
HTMLHeadingElement
|
||||||
|
>
|
||||||
|
): JSX.Element => {
|
||||||
|
const { children, id = '' } = props
|
||||||
|
return (
|
||||||
|
<h2 {...props} className='group'>
|
||||||
|
<Link
|
||||||
|
href={`#${id}`}
|
||||||
|
className='invisible !text-black group-hover:visible dark:!text-white'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
|
||||||
|
</Link>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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`}
|
||||||
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) {
|
h1: Heading,
|
||||||
return <a {...props} />
|
h2: Heading,
|
||||||
|
h3: Heading,
|
||||||
|
h4: Heading,
|
||||||
|
h5: Heading,
|
||||||
|
h6: Heading,
|
||||||
|
img: (properties) => {
|
||||||
|
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='theoludwig/theoludwig'
|
||||||
/>
|
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 +121,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)) {
|
||||||
|