1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

Compare commits

...

76 Commits

Author SHA1 Message Date
49599d25ed chore(release): 2.13.0 [skip ci] 2023-07-22 17:47:02 +00:00
65e0f4f8b6 fix: avoid scrolling when changing language 2023-07-22 19:40:28 +02:00
8d60c2d53a feat: add Carolo project in Portfolio 2023-07-22 19:39:57 +02:00
0bbebeab99 build(deps): update latest
Some checks failed
Analyze / analyze (javascript) (push) Failing after 1m24s
Build / build (push) Successful in 3m0s
Lint / lint (push) Successful in 2m6s
Test / test-unit (push) Successful in 1m56s
Test / test-e2e (push) Successful in 3m17s
2023-07-19 00:09:28 +02:00
643e0e5821 chore(release): 2.12.1 [skip ci] 2023-07-14 22:03:03 +00:00
872b018673 style: fix prettier 2023-07-14 23:58:50 +02:00
2644cb0fb5 fix: update /curriculum-vitae to /curriculum-vitae/index.html 2023-07-14 23:54:29 +02:00
bc719578d2 fix: remove vercel cli + update dependencies to latest 2023-07-14 23:50:20 +02:00
117c41b1c3 chore(release): 2.12.0 [skip ci] 2023-07-02 14:59:12 +00:00
b92704b77d feat: increase duration work experience Numerize 2023-07-02 16:50:32 +02:00
bab7581283 fix: update dependencies to latest 2023-07-02 16:42:39 +02:00
988fceb2aa chore(release): 2.11.0 [skip ci] 2023-06-18 10:23:59 +00:00
5211ba1489 feat(skills): add Arch Linux 2023-06-18 12:18:24 +02:00
6886480cef chore(release): 2.10.0 [skip ci] 2023-06-16 21:30:51 +00:00
d78e50638e test: fix with new anchor link behavior in blog posts 2023-06-16 23:26:09 +02:00
3b76195d71 feat(blog): add anchor links for titles/headers 2023-06-16 23:14:25 +02:00
2dc63ba933 chore: maintenance 2023-06-16 22:56:53 +02:00
336f067c52 fix(posts): add explanations for Git (cherry-pick + merge squash) 2023-06-16 22:36:20 +02:00
5fd7f77b6d fix: justify align text in blog posts 2023-06-16 21:48:47 +02:00
db0c708c04 chore(release): 2.9.0 [skip ci] 2023-05-31 18:39:59 +00:00
9d44671fed feat: continue migrating to full name instead of nickname 2023-05-31 20:09:08 +02:00
7bcc5f972c chore(release): 2.8.0 [skip ci] 2023-05-30 19:57:02 +00:00
61172d59e3 feat: migrate progressively to full name instead of nickname 2023-05-30 21:51:27 +02:00
7c0f11ab7d chore(release): 2.7.3 [skip ci] 2023-05-29 15:45:42 +00:00
670897fa78 fix: improve spelling consistency 2023-05-29 17:44:26 +02:00
b88246b668 chore: usage of next start 2023-05-29 17:33:45 +02:00
87fbfe4940 chore(release): 2.7.2 [skip ci] 2023-05-29 15:29:07 +00:00
271aa60247 test: update with new changes 2023-05-29 17:26:06 +02:00
ba34e314c9 fix: update name with full name and nickname 2023-05-29 17:10:14 +02:00
f41bc644b1 fix(deps): remove next-pwa dependency 2023-05-29 16:24:49 +02:00
a18cec4826 chore(release): 2.7.1 [skip ci] 2023-05-21 16:27:24 +00:00
61e589f0f4 fix: responsive on blog post with code blocks and katex 2023-05-21 18:21:46 +02:00
dc5c3cee41 chore(release): 2.7.0 [skip ci] 2023-05-21 12:49:17 +00:00
20cb0c21d5 feat(posts): add programming-challenges 2023-05-21 14:42:53 +02:00
e5232c1394 build(deps): update latest 2023-05-21 12:15:08 +02:00
fd51609713 chore(release): 2.6.1 [skip ci] 2023-05-13 17:15:54 +00:00
edf16c2562 fix(deps): update latest 2023-05-13 19:09:54 +02:00
94e0d190ae chore(release): 2.6.0 [skip ci] 2023-05-10 18:12:22 +00:00
b1cf7f8517 chore: remove unneeded Lighthouse checking 2023-05-09 23:22:33 +02:00
a1a715d3b9 feat: add Numerize as work experience 2023-05-09 23:06:10 +02:00
eede46fb41 build(deps): update latest 2023-05-09 22:56:42 +02:00
e32c53caa1 chore(release): 2.5.6 [skip ci] 2023-04-02 21:18:14 +00:00
361ea37deb chore: fix CI issues 2023-04-02 23:16:51 +02:00
d49a8a7470 fix: update dependencies to latest 2023-04-02 22:44:09 +02:00
a4996c8251 chore: remove useless runner-dependencies in Dockerfile 2023-01-11 17:42:29 +01:00
b25451e631 chore(release): 2.5.5 [skip ci] 2023-01-10 23:05:24 +00:00
042a861f58 fix: update dependencies to latest 2023-01-10 23:56:46 +01:00
d76db36dbc chore(release): 2.5.4 [skip ci] 2022-12-08 08:54:03 +00:00
99d9dcf334 fix: improve Resume 2022-12-08 09:52:39 +01:00
ece5ded1b4 chore(release): 2.5.3 [skip ci] 2022-11-29 09:33:10 +00:00
1514600998 fix: improve Resume 2022-11-29 10:29:02 +01:00
5f5b328895 chore(release): 2.5.2 [skip ci] 2022-11-19 19:43:26 +00:00
c88887a322 fix: better resume 2022-11-19 20:24:13 +01:00
014044573a chore(release): 2.5.1 [skip ci] 2022-11-10 11:35:11 +00:00
df009c3f7b fix(posts): update broken link in thream-v1.0.0.md 2022-11-10 12:31:48 +01:00
5c85ca2ef1 chore: fix cypress unit tests 2022-11-08 11:00:31 +01:00
07f7942496 chore(release): 2.5.0 [skip ci] 2022-10-27 17:24:30 +00:00
213a3fa182 build(deps): bump Next.js to v13 2022-10-27 19:13:29 +02:00
28d9211583 fix(posts): update git-ultimate-guide 2022-10-23 20:15:07 +02:00
4d085cb148 fix: update biography description 2022-10-23 18:38:37 +02:00
e6c583f2cd ci: fix timeout 2022-10-20 23:57:53 +02:00
232b54588a feat(skills): add PHP and Laravel 2022-10-20 22:44:40 +02:00
c419fb3bb4 chore: remove usage of styled-jsx 2022-10-20 22:44:32 +02:00
03e7e22d74 chore: reduce docker image size 2022-10-20 22:44:32 +02:00
e85c241ed1 feat(posts): add git-ultimate-guide 2022-10-20 22:43:25 +02:00
c1877297f8 refactor: minor changes 2022-08-27 02:30:55 +02:00
83231197dd chore(release): 2.4.1 [skip ci] 2022-08-23 11:33:38 +00:00
a2fe2205bc fix(resume): wrong base path for assets 2022-08-23 13:31:17 +02:00
e1f3dceb07 chore(release): 2.4.0 [skip ci] 2022-08-23 10:33:09 +00:00
0f89fee52f feat: add giscus comments system for blog posts 2022-08-23 12:23:31 +02:00
2fcc7ac384 chore(release): 2.3.2 [skip ci] 2022-07-28 21:06:12 +00:00
9351edf626 chore: use the right resume.json 2022-07-28 23:01:19 +02:00
1f4aa54211 chore: remove jest -> cypress for unit tests 2022-07-28 22:51:12 +02:00
8bc1471cbb chore: easier development for jsonresume-theme-custom thanks to vite 2022-07-28 21:20:41 +02:00
1ebdab18a5 fix: update about, now second year of university 2022-07-23 23:00:58 +02:00
b9b76e839a build(deps): update latest 2022-07-01 23:12:47 +02:00
138 changed files with 10850 additions and 42087 deletions

View File

@ -1,2 +1 @@
ARG VARIANT="16" FROM mcr.microsoft.com/devcontainers/javascript-node:20
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -1,23 +1,24 @@
{ {
"name": "divlo", "name": "theoludwig",
"dockerComposeFile": "./docker-compose.yml", "dockerComposeFile": "./docker-compose.yml",
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": { "settings": {
"remote.autoForwardPorts": false "remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}
}, },
"extensions": [ "extensions": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax", "davidanson.vscode-markdownlint",
"divlo.vscode-styled-jsx-languageserver",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker" "ms-azuretools.vscode-docker"
], ]
"forwardPorts": [3000], },
"postAttachCommand": ["npm", "install"],
"remoteUser": "node" "remoteUser": "node"
} }

View File

@ -6,3 +6,4 @@ services:
volumes: volumes:
- '..:/workspace:cached' - '..:/workspace:cached'
command: 'sleep infinity' command: 'sleep infinity'
network_mode: 'host'

View File

@ -1,12 +1,4 @@
.vscode
.git
.env
build build
.next .next
coverage coverage
node_modules node_modules
tmp
temp
.DS_Store
.lighthouseci
.vercel

View File

@ -1,2 +1,2 @@
COMPOSE_PROJECT_NAME=divlo.fr COMPOSE_PROJECT_NAME=theoludwig
PORT=3000 PORT=3000

View File

@ -4,14 +4,13 @@
"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" "overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
} }
]
} }

View File

@ -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

View File

@ -1,27 +0,0 @@
name: 'Analyze'
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
analyze:
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
matrix:
language: ['javascript']
steps:
- uses: 'actions/checkout@v3.0.0'
- name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@v1'
with:
languages: ${{ matrix.language }}
- name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@v1'

View File

@ -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'

View File

@ -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'

View File

@ -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'
@ -35,10 +35,3 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }} GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
- name: 'Deploy to Vercel'
run: 'npm run deploy -- --token="${VERCEL_TOKEN}" --prod'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}

View File

@ -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'

8
.gitignore vendored
View File

@ -11,8 +11,7 @@ out
# production # production
build build
dist dist
public/*.html public/curriculum-vitae
jsonresume-theme-custom/theme/index.html
# PWA # PWA
public/workbox-*.js public/workbox-*.js
public/sw.js public/sw.js
@ -49,4 +48,7 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
.lighthouseci .lighthouseci
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -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"]
} }

View File

@ -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": {}
}
}

View File

@ -1,10 +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"]
} }

12
.markdownlint-cli2.jsonc Normal file
View 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"]
}

View File

@ -1,7 +0,0 @@
{
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false
}

View File

@ -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",

View File

@ -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"
} }

View File

@ -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

View File

@ -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
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Divlo/Divlo) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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
@ -86,4 +68,4 @@ docker compose up --build
### Services started ### Services started
- website : `http://localhost:3000` - `website`: <http://127.0.0.1:3000>

View File

@ -1,23 +1,21 @@
FROM node:16.15.0 AS dependencies FROM node:20.5.0 AS builder-dependencies
WORKDIR /usr/src/app WORKDIR /usr/src/application
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm install RUN npm clean-install
FROM node:16.15.0 AS builder FROM node:20.5.0 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
RUN npm run build RUN npm run build
FROM node:16.15.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}"]

View File

@ -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

View File

@ -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>

View File

@ -1,17 +1,24 @@
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 (
<> <>
<div className='flex h-screen flex-col pt-0'>
<Header showLanguage />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
<h1 className='my-6 text-4xl font-semibold'> <h1 className='my-6 text-4xl font-semibold'>
{t('errors:error')}{' '} {t('errors:error')}{' '}
<span <span
@ -23,31 +30,16 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
</h1> </h1>
<p className='text-center text-lg'> <p className='text-center text-lg'>
{message}{' '} {message}{' '}
<Link href='/'> <Link
<a className='text-yellow hover:underline dark:text-yellow-dark'> href='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
{t('errors:return-to-home-page')} {t('errors:return-to-home-page')}
</a>
</Link> </Link>
</p> </p>
</main>
<style jsx global> <Footer version={version} />
{` </div>
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-width: 100vw;
flex: 1;
}
#__next {
display: flex;
flex-direction: column;
padding-top: 0;
height: 100vh;
}
`}
</style>
</> </>
) )
} }

View File

@ -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'

View File

@ -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>
) )
} }

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState, useRef } 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'
@ -14,7 +14,9 @@ export const Language: React.FC = () => {
const languageClickRef = useRef<HTMLDivElement | null>(null) const languageClickRef = useRef<HTMLDivElement | null>(null)
const handleHiddenMenu = useCallback(() => { const handleHiddenMenu = useCallback(() => {
setHiddenMenu((oldHiddenMenu) => !oldHiddenMenu) setHiddenMenu((oldHiddenMenu) => {
return !oldHiddenMenu
})
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -35,7 +37,7 @@ export const Language: React.FC = () => {
}, []) }, [])
const handleLanguage = async (language: string): Promise<void> => { const handleLanguage = async (language: string): Promise<void> => {
await setLanguage(language) await setLanguage(language, false)
} }
return ( return (
@ -53,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 }
)} )}
> >
@ -65,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>

View File

@ -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='toggle-theme-button relative inline-block cursor-pointer bg-transparent'> <div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
<div className='toggle-track'> <div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
<div <div
data-cy='switch-theme-dark' data-cy='switch-theme-dark'
className='toggle-track-check absolute' className={classNames(
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
{
'opacity-100': theme === 'dark',
'opacity-0': theme === 'light'
}
)}
> >
<span className='toggle_Dark relative flex items-center justify-center'> <span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌜 🌜
</span> </span>
</div> </div>
<div <div
data-cy='switch-theme-light' data-cy='switch-theme-light'
className='toggle-track-x absolute' 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='toggle_Light relative flex items-center justify-center'> <span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌞 🌞
</span> </span>
</div> </div>
</div> </div>
<div className='toggle-thumb absolute' /> <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 <input
data-cy='switch-theme-input' data-cy='switch-theme-input'
type='checkbox' type='checkbox'
aria-label='Dark mode toggle' aria-label='Dark mode toggle'
className='toggle-screenreader-only absolute overflow-hidden' className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
defaultChecked defaultChecked
/> />
</div> </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>
</>
) )
} }

View File

@ -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()
})
})

View File

@ -14,33 +14,31 @@ export const Header: React.FC<HeaderProps> = (props) => {
return ( return (
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'> <header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
<Link href='/'> <Link href='/'>
<a>
<div className='flex items-center justify-center'> <div className='flex items-center justify-center'>
<Image <Image
quality={100} quality={100}
width={60} width={60}
height={60} height={60}
src='/images/divlo_icon_small.png' src='/images/icon_small.png'
alt='Divlo' alt='Théo LUDWIG'
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>

View File

@ -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}

View File

@ -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}

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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}

View File

@ -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')

View File

@ -4,20 +4,20 @@ 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 />
<a <a
href='/curriculum-vitae' href='/curriculum-vitae/index.html'
className='text-yellow hover:underline dark:text-yellow-dark' className='text-yellow hover:underline dark:text-yellow-dark'
> >
Curriculum vitæ Curriculum vitæ
</a> </a>
</> </>
)} ) : null}
</p> </p>
) )
} }

View File

@ -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>

View File

@ -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'

View File

@ -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>
) )

View File

@ -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>
) )
} }

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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' />

View File

@ -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

View File

@ -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()
})
})

View File

@ -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}`
)
})
})

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
export const RevealFade: React.FC<React.PropsWithChildren<{}>> = (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)
@ -8,13 +8,13 @@ export const RevealFade: React.FC<React.PropsWithChildren<{}>> = (props) => {
useEffect(() => { useEffect(() => {
const observer = new window.IntersectionObserver( const observer = new window.IntersectionObserver(
(entries, observer) => { (entries, observer) => {
entries.forEach((entry) => { for (const entry of entries) {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.className = entry.target.className =
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out' 'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
observer.unobserve(entry.target) observer.unobserve(entry.target)
} }
}) }
}, },
{ {
root: null, root: null,

View File

@ -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>
) )

View File

@ -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>

View File

@ -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
View 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'
}
}
})

View File

@ -1,8 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"pluginsFile": false,
"supportFile": false,
"fixturesFolder": false,
"video": false,
"screenshotOnRunFailure": false
}

View 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}`
)
})
})

View 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)
})
})
})

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -20,3 +20,5 @@ describe('Page /blog', () => {
.should('eq', '/blog/hello-world') .should('eq', '/blog/hello-world')
}) })
}) })
export {}

View File

@ -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 {}

View File

@ -0,0 +1,3 @@
/// <reference types="cypress" />
export {}

View 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>

View 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)

View File

@ -1,9 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["cypress"],
"isolatedModules": false
},
"include": ["../node_modules/cypress", "./**/*.ts"]
}

View File

@ -1,11 +1,12 @@
services: services:
divlo.fr: theoludwig:
container_name: ${COMPOSE_PROJECT_NAME} container_name: ${COMPOSE_PROJECT_NAME}
image: 'divlo.fr' image: 'theoludwig'
restart: 'unless-stopped'
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'

View File

@ -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)

View File

@ -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?

View 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
})

View File

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1015 B

View File

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 986 B

View File

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View File

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 912 B

View File

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 528 B

View 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>

View File

@ -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' })
}

File diff suppressed because it is too large Load Diff

View File

@ -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.1", "jsonc-parser": "3.2.0",
"ejs": "3.1.7", "modern-normalize": "2.0.0"
"modern-normalize": "1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@parcel/config-default": "2.5.0", "@types/node": "20.4.4",
"@parcel/core": "2.5.0", "date-and-time": "3.0.2",
"@parcel/optimizer-data-url": "2.5.0", "vite": "4.4.6",
"@parcel/transformer-inline-string": "2.5.0", "vite-plugin-html": "3.2.0"
"parcel": "2.5.0"
} }
} }

View File

@ -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'
})

View 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()

View File

@ -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;
}

View File

@ -1,207 +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 != null
? " - " + 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>

View 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: {}
}
})

View File

@ -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>."
} }
] ]
}, },
@ -34,22 +34,22 @@
"title": "Portfolio", "title": "Portfolio",
"items": [ "items": [
{ {
"title": "function.divlo.fr", "title": "Carolo",
"description": "Learn programming through project-based alias function.", "description": "Strategy board game similar to chess which allows grandiose moves (only available in French).",
"link": "https://function.divlo.fr/", "link": "https://carolo.theoludwig.fr/",
"image": "/images/portfolio/functiondivlofr.png" "image": "/images/portfolio/Carolo.png"
}, },
{ {
"title": "thream.divlo.fr", "title": "Thream",
"description": "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.", "description": "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.",
"link": "https://thream.divlo.fr/", "link": "https://thream.theoludwig.fr/",
"image": "/images/portfolio/threamdivlofr.png" "image": "/images/portfolio/Thream.png"
}, },
{ {
"title": "Leon", "title": "Leon",
"description": "Leon is your open-source personal assistant.", "description": "Leon is your open-source personal assistant.",
"link": "https://getleon.ai/", "link": "https://getleon.ai/",
"image": "/images/portfolio/leon.png" "image": "/images/portfolio/Leon.png"
} }
] ]
}, },

View File

@ -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 !"
} }

View File

@ -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>."
} }
] ]
}, },
@ -34,22 +34,22 @@
"title": "Portfolio", "title": "Portfolio",
"items": [ "items": [
{ {
"title": "function.divlo.fr", "title": "Carolo",
"description": "Apprenez la programmation grâce à l'apprentissage par projet alias fonction.", "description": "Jeu de plateau stratégique similaire aux échecs qui permet des coups grandioses, reposant sur des enchaînements remarquables.",
"link": "https://function.divlo.fr/", "link": "https://carolo.theoludwig.fr/",
"image": "/images/portfolio/functiondivlofr.png" "image": "/images/portfolio/Carolo.png"
}, },
{ {
"title": "thream.divlo.fr", "title": "Thream",
"description": "Votre plateforme open source pour rester proche de vos amis et communautés, parler, discuter, collaborer, partager et amusez-vous.", "description": "Votre plateforme open source pour rester proche de vos amis et communautés, parler, discuter, collaborer, partager et amusez-vous.",
"link": "https://thream.divlo.fr/", "link": "https://thream.theoludwig.fr/",
"image": "/images/portfolio/threamdivlofr.png" "image": "/images/portfolio/Thream.png"
}, },
{ {
"title": "Leon", "title": "Leon",
"description": "Leon est votre assistant personnel open source.", "description": "Leon est votre assistant personnel open source.",
"link": "https://getleon.ai/", "link": "https://getleon.ai/",
"image": "/images/portfolio/leon.png" "image": "/images/portfolio/Leon.png"
} }
] ]
}, },

5
next-env.d.ts vendored
View File

@ -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.

View File

@ -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,
pwa: { output: 'standalone'
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'"
}
}
})
}
]
}
})
)

44811
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +1,99 @@
{ {
"name": "divlo", "name": "theoludwig",
"version": "2.3.1", "version": "2.13.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}\" --ignore-path \".gitignore\"", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"", "lint:prettier": "prettier . --check",
"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",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fontsource/montserrat": "4.5.10", "@fontsource/montserrat": "5.0.5",
"@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.1", "clsx": "2.0.0",
"date-and-time": "3.0.2",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
"html-react-parser": "1.4.12", "html-react-parser": "4.2.0",
"next": "12.1.6", "katex": "0.16.8",
"next-mdx-remote": "4.0.3", "next": "13.4.12",
"next-pwa": "5.5.2", "next-mdx-remote": "4.4.1",
"next-themes": "0.1.1", "next-themes": "0.2.1",
"next-translate": "1.4.0", "next-translate": "2.5.2",
"react": "18.1.0", "react": "18.2.0",
"react-dom": "18.1.0", "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.4", "remark-math": "5.1.1",
"shiki": "0.10.1", "sharp": "0.32.4",
"shiki": "0.14.3",
"unified": "10.1.2", "unified": "10.1.2",
"unist-util-visit": "4.1.0", "unist-util-visit": "5.0.0",
"universal-cookie": "4.0.4" "universal-cookie": "4.0.4"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "16.2.4", "@commitlint/cli": "17.6.7",
"@commitlint/config-conventional": "16.2.4", "@commitlint/config-conventional": "17.6.7",
"@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.4", "@tsconfig/strictest": "2.0.1",
"@testing-library/react": "13.1.1", "@types/node": "20.4.4",
"@types/jest": "27.5.0", "@types/react": "18.2.15",
"@types/node": "17.0.31", "@types/unist": "3.0.0",
"@types/react": "18.0.8", "@typescript-eslint/eslint-plugin": "6.1.0",
"@types/unist": "2.0.6", "@typescript-eslint/parser": "6.1.0",
"@typescript-eslint/eslint-plugin": "5.22.0", "autoprefixer": "10.4.14",
"autoprefixer": "10.4.7", "cypress": "12.17.2",
"cypress": "9.6.0", "editorconfig-checker": "5.1.1",
"editorconfig-checker": "4.0.2", "eslint": "8.45.0",
"eslint": "8.14.0", "eslint-config-conventions": "11.0.1",
"eslint-config-conventions": "2.0.0", "eslint-config-next": "13.4.12",
"eslint-config-next": "12.1.6", "eslint-config-prettier": "8.8.0",
"eslint-config-prettier": "8.5.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-import": "2.26.0", "eslint-plugin-prettier": "5.0.0",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-promise": "6.0.0", "eslint-plugin-unicorn": "48.0.0",
"eslint-plugin-unicorn": "42.0.0", "html-w3c-validator": "1.4.0",
"html-w3c-validator": "1.2.0", "husky": "8.0.3",
"husky": "7.0.4",
"jest": "27.5.1",
"jsonresume-theme-custom": "file:./jsonresume-theme-custom", "jsonresume-theme-custom": "file:./jsonresume-theme-custom",
"lint-staged": "12.4.1", "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.13", "next-translate-plugin": "2.5.2",
"prettier": "2.6.2", "postcss": "8.4.27",
"prettier-plugin-tailwindcss": "0.1.10", "prettier": "3.0.0",
"semantic-release": "19.0.2", "prettier-plugin-tailwindcss": "0.4.1",
"start-server-and-test": "1.14.0", "semantic-release": "21.0.7",
"tailwindcss": "3.0.24", "start-server-and-test": "2.0.0",
"typescript": "4.6.4", "tailwindcss": "3.3.3",
"vercel": "24.2.0" "typescript": "5.1.6"
} }
} }

View File

@ -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} />
</> </>
) )
} }

View File

@ -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} />
</> </>
) )
} }

View File

@ -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(() => {

View File

@ -1,40 +1,94 @@
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'>
<div className='px-8'>
<MDXRemote <MDXRemote
{...post.source} {...post.source}
components={{ components={{
a: (props: React.ComponentPropsWithoutRef<'a'>) => { h1: Heading,
if (props.href?.startsWith('#') ?? false) { 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) => {
const { href = '' } = props
if (href.startsWith('#')) {
return <a {...props} /> return <a {...props} />
} }
return ( return (
@ -43,6 +97,21 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (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 +122,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)) {

View File

@ -1,10 +1,11 @@
import { GetStaticProps, NextPage } from 'next' import type { GetStaticProps, NextPage } from 'next'
import Link from 'next/link' import Link from 'next/link'
import date from 'date-and-time' import date from 'date-and-time'
import { Head } from 'components/Head' import { Head } from 'components/Head'
import { Header } from 'components/Header' import { Header } from 'components/Header'
import { Footer, FooterProps } from 'components/Footer' import type { FooterProps } from 'components/Footer'
import { Footer } from 'components/Footer'
import { ShadowContainer } from 'components/design/ShadowContainer' import { ShadowContainer } from 'components/design/ShadowContainer'
import type { PostMetadata } from 'utils/blog' import type { PostMetadata } from 'utils/blog'
@ -20,7 +21,7 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
return ( return (
<> <>
<Head title='Blog | Divlo' description={blogDescription} /> <Head title='Blog | Théo LUDWIG' description={blogDescription} />
<Header /> <Header />
<main className='flex flex-1 flex-col flex-wrap items-center'> <main className='flex flex-1 flex-col flex-wrap items-center'>
@ -38,8 +39,12 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
'DD/MM/YYYY' 'DD/MM/YYYY'
) )
return ( return (
<Link href={`/blog/${post.slug}`} key={index} locale='en'> <Link
<a data-cy={post.slug}> href={`/blog/${post.slug}`}
key={index}
locale='en'
data-cy={post.slug}
>
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'> <ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
<h2 <h2
data-cy='blog-post-title' data-cy='blog-post-title'
@ -54,7 +59,6 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
{post.frontmatter.description} {post.frontmatter.description}
</p> </p>
</ShadowContainer> </ShadowContainer>
</a>
</Link> </Link>
) )
})} })}

View File

@ -1,4 +1,4 @@
import { GetStaticProps, NextPage } from 'next' import type { GetStaticProps, NextPage } from 'next'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import { RevealFade } from 'components/design/RevealFade' import { RevealFade } from 'components/design/RevealFade'
@ -11,7 +11,8 @@ import { SocialMediaList } from 'components/Profile/SocialMediaList'
import { Skills } from 'components/Skills' import { Skills } from 'components/Skills'
import { OpenSource } from 'components/OpenSource' import { OpenSource } from 'components/OpenSource'
import { Header } from 'components/Header' import { Header } from 'components/Header'
import { Footer, FooterProps } from 'components/Footer' import type { FooterProps } from 'components/Footer'
import { Footer } from 'components/Footer'
interface HomeProps extends FooterProps {} interface HomeProps extends FooterProps {}

View File

@ -7,13 +7,13 @@ publishedOn: '2022-02-23T08:00:18.758Z'
Hello! 👋 Hello! 👋
Have you already heard of "**Clean Code**" or "**Design Patterns**" ? Have you already heard of "**Clean Code**" or "**Design Patterns**"?
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term. Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
**Note:** Sources used to write this blog post are available at the [end of this post](#sources). **Note:** Sources used to write this blog post are available at the [end of this post](#sources).
## Definition : Clean Code ## Definition: Clean Code
A clean code is a code that is **easy** to **read** and easy to **understand**. A clean code is a code that is **easy** to **read** and easy to **understand**.
@ -23,7 +23,7 @@ We could ask ourselves, what is **easy** to **read** and easy to **understand**
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**. It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
## Why is it so important ? ## Why is it so important?
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine. Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
@ -31,7 +31,7 @@ For example the [Linux kernel](https://www.kernel.org/), is one of the biggest o
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code. With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
## Definition : Design Patterns ## Definition: Design Patterns
These **rules** and **conventions** are so called **Design Patterns**. These **rules** and **conventions** are so called **Design Patterns**.
@ -77,8 +77,8 @@ setTimeout(restart, 86400000)
##### Example (good way) ##### Example (good way)
```typescript ```typescript
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000 const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000
setTimeout(restart, MILLISECONDS_IN_A_DAY) setTimeout(restart, MILLISECONDS_IN_ONE_DAY)
``` ```
--- ---
@ -131,7 +131,9 @@ const printCar = (car: Car): void => {
--- ---
#### Boolean names (Prefix: is, has, can) #### Boolean names
The name of a boolean variable should be a question, and the answer should be true or false. We can use prefixes like `is`, `has`, `can` to make it more explicit and we should avoid negation.
##### Example (bad way) ##### Example (bad way)
@ -171,7 +173,10 @@ We have to keep it as simple as possible, not to implement features that are not
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
const createFile = async (name: string, isTemporary: boolean = false) => { const createFile = async (
name: string,
isTemporary: boolean = false
): Promise<void> => {
if (isTemporary) { if (isTemporary) {
return await fs.promises.writeFile(path.join('temporary', name), '') return await fs.promises.writeFile(path.join('temporary', name), '')
} }
@ -187,11 +192,11 @@ const createFile = async (name: string, isTemporary: boolean = false) => {
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
const createFile = async (name: string) => { const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, '') await fs.promises.writeFile(name, '')
} }
const createTemporaryFile = async (name: string) => { const createTemporaryFile = async (name: string): Promise<void> => {
await createFile(path.join('temporary', name)) await createFile(path.join('temporary', name))
} }
``` ```
@ -210,7 +215,7 @@ The End To End (e2e) and Unit tests should document what is the behavior intende
### Avoid comments ### Avoid comments
One of the most important rule of "Clean Code" : If you need to add **comments**, it's because your code is **not clean**. One of the most important rule of "Clean Code": If you need to add **comments**, it's because your code is **not clean**.
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does. I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.

Some files were not shown because too many files have changed in this diff Show More