mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
160 Commits
Author | SHA1 | Date | |
---|---|---|---|
5f3dfad988
|
|||
b231381cb3
|
|||
bbb2e56512
|
|||
66cf6d7438
|
|||
2a635bf3ba
|
|||
9f79b88202
|
|||
23d9caf578
|
|||
7febe6d1f9
|
|||
c4650c34d9
|
|||
0eb780485c
|
|||
cd5e92b64a
|
|||
982b148329
|
|||
0febee5b51
|
|||
3502f51735
|
|||
493df4e2f2
|
|||
c2c9b59c7a
|
|||
f6e3008ab9
|
|||
15e94cec64
|
|||
5185c6758b
|
|||
b633eef833
|
|||
d2e627ff13
|
|||
1e0567b538
|
|||
c8d32c6acc
|
|||
05503cda26
|
|||
303b6f3011
|
|||
0272cf7080
|
|||
e8ea42a260
|
|||
f337e14260
|
|||
f5020cad19
|
|||
b8ceefb2f6
|
|||
1523c8cac0
|
|||
548ddc8425
|
|||
bac65ad61a
|
|||
b91f3165b7
|
|||
5478e202a7
|
|||
a89b5932c2
|
|||
339e42acfa
|
|||
c123815a86
|
|||
dd26a277a2
|
|||
62222dbb0c
|
|||
ee0a02bc8b
|
|||
2e04053ec3
|
|||
45a9a69122
|
|||
e566ef6c38
|
|||
c7ad15a465
|
|||
f4a842efb5
|
|||
424c97019b
|
|||
c0508dc0b9
|
|||
f04d8a0c11
|
|||
d29064745c
|
|||
95febe2a99
|
|||
fdab2a7ea8
|
|||
35211fa279
|
|||
137cceffa1
|
|||
f6bfc466de
|
|||
e4cf714d95
|
|||
d3c86b2a26
|
|||
d2578abeec
|
|||
e51e3bdc19
|
|||
56520830e9
|
|||
2e0138194c
|
|||
4b2e7bae90
|
|||
caa6a90418
|
|||
e82db952db
|
|||
6b29ce9b15
|
|||
5640f1b434
|
|||
6d0dcb50a7
|
|||
70603f1444
|
|||
f42fdbfd0c
|
|||
6a3f335f9f
|
|||
f1509d0af1
|
|||
49599d25ed
|
|||
65e0f4f8b6
|
|||
8d60c2d53a
|
|||
0bbebeab99
|
|||
643e0e5821
|
|||
872b018673
|
|||
2644cb0fb5
|
|||
bc719578d2
|
|||
117c41b1c3
|
|||
b92704b77d
|
|||
bab7581283
|
|||
988fceb2aa
|
|||
5211ba1489
|
|||
6886480cef
|
|||
d78e50638e
|
|||
3b76195d71
|
|||
2dc63ba933
|
|||
336f067c52
|
|||
5fd7f77b6d
|
|||
db0c708c04
|
|||
9d44671fed
|
|||
7bcc5f972c | |||
61172d59e3 | |||
7c0f11ab7d | |||
670897fa78 | |||
b88246b668 | |||
87fbfe4940 | |||
271aa60247 | |||
ba34e314c9 | |||
f41bc644b1 | |||
a18cec4826 | |||
61e589f0f4 | |||
dc5c3cee41 | |||
20cb0c21d5 | |||
e5232c1394 | |||
fd51609713 | |||
edf16c2562 | |||
94e0d190ae | |||
b1cf7f8517 | |||
a1a715d3b9 | |||
eede46fb41 | |||
e32c53caa1 | |||
361ea37deb | |||
d49a8a7470 | |||
a4996c8251 | |||
b25451e631 | |||
042a861f58 | |||
d76db36dbc | |||
99d9dcf334 | |||
ece5ded1b4 | |||
1514600998 | |||
5f5b328895 | |||
c88887a322 | |||
014044573a | |||
df009c3f7b | |||
5c85ca2ef1 | |||
07f7942496 | |||
213a3fa182 | |||
28d9211583 | |||
4d085cb148 | |||
e6c583f2cd | |||
232b54588a | |||
c419fb3bb4 | |||
03e7e22d74 | |||
e85c241ed1 | |||
c1877297f8 | |||
83231197dd | |||
a2fe2205bc | |||
e1f3dceb07 | |||
0f89fee52f | |||
2fcc7ac384 | |||
9351edf626 | |||
1f4aa54211 | |||
8bc1471cbb | |||
1ebdab18a5 | |||
b9b76e839a | |||
bc065a2e19 | |||
5d3a287b27 | |||
fb689c9bc1 | |||
2c3a70df2a | |||
bce254a355 | |||
f67d331416 | |||
6abc881e94 | |||
a67d6665ea | |||
1152039663 | |||
919ebd5f3e | |||
94212f9b5c | |||
bf9347f685 | |||
896b6051e8 |
@ -1,2 +1 @@
|
||||
ARG VARIANT="16"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:20
|
||||
|
9
.devcontainer/compose.yaml
Normal file
9
.devcontainer/compose.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
services:
|
||||
workspace:
|
||||
build:
|
||||
context: "./"
|
||||
dockerfile: "./Dockerfile"
|
||||
volumes:
|
||||
- "..:/workspace:cached"
|
||||
command: "sleep infinity"
|
||||
network_mode: "host"
|
@ -1,23 +1,24 @@
|
||||
{
|
||||
"name": "divlo",
|
||||
"dockerComposeFile": "./docker-compose.yml",
|
||||
"name": "theoludwig",
|
||||
"dockerComposeFile": "./compose.yaml",
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"remote.autoForwardPorts": false
|
||||
"remote.autoForwardPorts": false,
|
||||
"remote.localPortHost": "allInterfaces"
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker"
|
||||
],
|
||||
"forwardPorts": [3000],
|
||||
"postAttachCommand": ["npm", "install"],
|
||||
]
|
||||
},
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
workspace:
|
||||
build:
|
||||
context: './'
|
||||
dockerfile: './Dockerfile'
|
||||
volumes:
|
||||
- '..:/workspace:cached'
|
||||
command: 'sleep infinity'
|
@ -1,12 +1,21 @@
|
||||
.vscode
|
||||
.git
|
||||
**/.turbo
|
||||
**/.next
|
||||
**/out
|
||||
**/build
|
||||
**/coverage
|
||||
**/node_modules
|
||||
|
||||
# envs
|
||||
.env
|
||||
build
|
||||
.next
|
||||
coverage
|
||||
node_modules
|
||||
tmp
|
||||
temp
|
||||
.env.production
|
||||
.env.development
|
||||
secrets
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.lighthouseci
|
||||
.vercel
|
||||
*.pem
|
||||
Dockerfile
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
@ -1,4 +1,4 @@
|
||||
# For more information see: https://editorconfig.org/
|
||||
# https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
|
@ -1,2 +1,4 @@
|
||||
COMPOSE_PROJECT_NAME=divlo.fr
|
||||
COMPOSE_PROJECT_NAME=theoludwig
|
||||
HOSTNAME=0.0.0.0
|
||||
PORT=3000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
@ -1,8 +0,0 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
@ -1,16 +1,39 @@
|
||||
{
|
||||
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier", "unicorn"],
|
||||
"root": true,
|
||||
"extends": [
|
||||
"conventions",
|
||||
"next/core-web-vitals",
|
||||
"plugin:tailwindcss/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["prettier"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"jest": true
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["classNames"]
|
||||
},
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"unicorn/prefer-node-protocol": "off"
|
||||
"react/self-closing-comp": [
|
||||
"error",
|
||||
{
|
||||
"component": true,
|
||||
"html": true
|
||||
}
|
||||
],
|
||||
"react/void-dom-elements-no-children": "error",
|
||||
"react/jsx-boolean-value": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🐛 Bug Report'
|
||||
about: 'Report an unexpected problem or unintended behavior.'
|
||||
title: '[Bug]'
|
||||
labels: 'bug'
|
||||
name: "🐛 Bug Report"
|
||||
about: "Report an unexpected problem or unintended behavior."
|
||||
title: "[Bug]"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
<!--
|
||||
|
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '📜 Documentation'
|
||||
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
||||
title: '[Documentation]'
|
||||
labels: 'documentation'
|
||||
name: "📜 Documentation"
|
||||
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
|
||||
title: "[Documentation]"
|
||||
labels: "documentation"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '✨ Feature Request'
|
||||
about: 'Suggest a new feature idea.'
|
||||
title: '[Feature]'
|
||||
labels: 'feature request'
|
||||
name: "✨ Feature Request"
|
||||
about: "Suggest a new feature idea."
|
||||
title: "[Feature]"
|
||||
labels: "feature request"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🔧 Improvement'
|
||||
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
||||
title: '[Improvement]'
|
||||
labels: 'improvement'
|
||||
name: "🔧 Improvement"
|
||||
about: "Improve structure/format/performance/refactor/tests of the code."
|
||||
title: "[Improvement]"
|
||||
labels: "improvement"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🙋 Question'
|
||||
about: 'Further information is requested.'
|
||||
title: '[Question]'
|
||||
labels: 'question'
|
||||
name: "🙋 Question"
|
||||
about: "Further information is requested."
|
||||
title: "[Question]"
|
||||
labels: "question"
|
||||
---
|
||||
|
||||
### Question
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
||||
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||
|
||||
## What changes this PR introduce?
|
||||
# What changes this PR introduce?
|
||||
|
||||
## List any relevant issue numbers
|
||||
|
||||
|
188
.github/workflows/Divlo.yml
vendored
188
.github/workflows/Divlo.yml
vendored
@ -1,188 +0,0 @@
|
||||
name: 'Divlo'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@v1'
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@v1'
|
||||
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
lint:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'lint:commit'
|
||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
|
||||
- name: 'lint:editorconfig'
|
||||
run: 'npm run lint:editorconfig'
|
||||
|
||||
- name: 'lint:markdown'
|
||||
run: 'npm run lint:markdown'
|
||||
|
||||
- name: 'lint:typescript'
|
||||
run: 'npm run lint:typescript'
|
||||
|
||||
- name: 'lint:prettier'
|
||||
run: 'npm run lint:prettier'
|
||||
|
||||
- name: 'resume:validate'
|
||||
run: 'npm run resume:validate'
|
||||
|
||||
- name: 'lint:dotenv'
|
||||
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
- name: 'lint:docker'
|
||||
uses: 'hadolint/hadolint-action@v1.6.0'
|
||||
with:
|
||||
dockerfile: './Dockerfile'
|
||||
|
||||
test-unit:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Unit Test'
|
||||
run: 'npm run test:unit'
|
||||
|
||||
test-lighthouse:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: '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@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'End To End (e2e) Test'
|
||||
run: 'npm run test:e2e'
|
||||
|
||||
release:
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
needs: [analyze, build, lint, test-unit, test-lighthouse, test-e2e]
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Import GPG key'
|
||||
uses: 'crazy-max/ghaction-import-gpg@v4'
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Release'
|
||||
run: 'npm run release'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||
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 }}
|
25
.github/workflows/build.yml
vendored
Normal file
25
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.2"
|
||||
with:
|
||||
node-version: "20.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Build"
|
||||
run: "npm run build"
|
42
.github/workflows/lint.yml
vendored
Normal file
42
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: "Lint"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.2"
|
||||
with:
|
||||
node-version: "20.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "lint:commit"
|
||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
|
||||
- name: "lint:editorconfig"
|
||||
run: "npm run lint:editorconfig"
|
||||
|
||||
- name: "lint:markdown"
|
||||
run: "npm run lint:markdown"
|
||||
|
||||
- name: "lint:eslint"
|
||||
run: "npm run lint:eslint"
|
||||
|
||||
- name: "lint:prettier"
|
||||
run: "npm run lint:prettier"
|
||||
|
||||
- name: "lint:dotenv"
|
||||
uses: "dotenv-linter/action-dotenv-linter@v2.18.0"
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Import GPG key"
|
||||
uses: "crazy-max/ghaction-import-gpg@v6.0.0"
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.2"
|
||||
with:
|
||||
node-version: "20.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Release"
|
||||
run: "npm run release"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
|
48
.github/workflows/test.yml
vendored
Normal file
48
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: "Test"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.2"
|
||||
with:
|
||||
node-version: "20.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Unit Test"
|
||||
run: "npm run test:unit"
|
||||
|
||||
test-e2e:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.2"
|
||||
with:
|
||||
node-version: "20.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Build"
|
||||
run: "npm run build"
|
||||
|
||||
- name: "html-w3c-validator"
|
||||
run: "npm run test:html-w3c-validator"
|
||||
|
||||
- name: "End To End (e2e) Test"
|
||||
run: "npm run test:e2e"
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -11,10 +11,7 @@ out
|
||||
# production
|
||||
build
|
||||
dist
|
||||
public/*.html
|
||||
# PWA
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
public/curriculum-vitae
|
||||
|
||||
# testing
|
||||
coverage
|
||||
@ -48,4 +45,7 @@ npm-debug.log*
|
||||
# misc
|
||||
.DS_Store
|
||||
.lighthouseci
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
20
.gitpod.yml
20
.gitpod.yml
@ -1,20 +1,10 @@
|
||||
image: 'gitpod/workspace-full'
|
||||
image: "gitpod/workspace-full"
|
||||
|
||||
tasks:
|
||||
- before: 'cp .env.example .env'
|
||||
init: 'npm install'
|
||||
command: 'npm run dev'
|
||||
- before: "cp .env.example .env"
|
||||
init: "npm clean-install"
|
||||
command: "npm run dev"
|
||||
|
||||
ports:
|
||||
- port: 3000
|
||||
onOpen: 'open-preview'
|
||||
|
||||
github:
|
||||
prebuilds:
|
||||
master: true
|
||||
branches: true
|
||||
pullRequests: true
|
||||
pullRequestsFromForks: true
|
||||
addComment: true
|
||||
addBadge: true
|
||||
addLabel: true
|
||||
onOpen: "open-preview"
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"urls": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
]
|
||||
"$schema": "https://raw.githubusercontent.com/theoludwig/html-w3c-validator/master/schema/html-w3c-validatorrc-schema.json",
|
||||
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
|
||||
"files": ["./public/curriculum-vitae/index.html"],
|
||||
"severities": ["error"]
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npm run lint:commit -- --edit
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npm run lint:staged
|
||||
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"startServerCommand": "npm run start",
|
||||
"startServerReadyPattern": "ready on",
|
||||
"startServerReadyTimeout": 20000,
|
||||
"url": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
],
|
||||
"numberOfRuns": 1
|
||||
},
|
||||
"assert": {
|
||||
"preset": "lighthouse:recommended",
|
||||
"assertions": {
|
||||
"csp-xss": "warning",
|
||||
"non-composited-animations": "warning",
|
||||
"unused-javascript": "warning",
|
||||
"image-size-responsive": "warning",
|
||||
"unsized-images": "warning",
|
||||
"color-contrast": "warning"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
},
|
||||
"server": {}
|
||||
}
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
{
|
||||
"*": ["editorconfig-checker"],
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests"
|
||||
],
|
||||
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
||||
"resume.json": ["resume validate"]
|
||||
"**/*": ["editorconfig-checker", "prettier --write --ignore-unknown"],
|
||||
"**/*.md": ["markdownlint-cli2 --fix --no-globs"],
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix --max-warnings 0 --report-unused-disable-directives"
|
||||
]
|
||||
}
|
||||
|
12
.markdownlint-cli2.jsonc
Normal file
12
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"extends": "markdownlint/style/prettier",
|
||||
"default": true,
|
||||
"relative-links": true,
|
||||
"no-duplicate-heading": false,
|
||||
"no-inline-html": false,
|
||||
},
|
||||
"globs": ["**/*.md"],
|
||||
"ignores": ["**/node_modules"],
|
||||
"customRules": ["markdownlint-rule-relative-links"],
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
"MD024": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
*.hbs
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
@ -30,6 +30,7 @@
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"branches": [{ "from": "master", "to": "develop" }],
|
||||
"backmergeStrategy": "merge"
|
||||
}
|
||||
]
|
||||
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -3,8 +3,6 @@
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"davidanson.vscode-markdownlint",
|
||||
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -5,6 +5,10 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.options": {
|
||||
"ignorePath": ".gitignore"
|
||||
},
|
||||
"prettier.ignorePath": ".gitignore"
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
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 community leaders are obligated to respect the privacy and security of the
|
||||
|
@ -1,6 +1,10 @@
|
||||
# 💡 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
|
||||
|
||||
@ -11,63 +15,41 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
|
||||
## 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.
|
||||
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
|
||||
|
||||
## Getting Started
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
||||
[](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
# Clone the repository
|
||||
git clone https://github.com/Divlo/Divlo.git
|
||||
git clone git@github.com:theoludwig/theoludwig.git
|
||||
|
||||
# Go to the project root
|
||||
cd Divlo
|
||||
cd theoludwig
|
||||
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Install
|
||||
npm install
|
||||
npm clean-install
|
||||
```
|
||||
|
||||
### Local Development environment
|
||||
@ -81,9 +63,9 @@ npm run dev
|
||||
|
||||
```sh
|
||||
# Setup and run all the services for you
|
||||
docker-compose up --build
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
### Services started
|
||||
|
||||
- website : `http://localhost:3000`
|
||||
- `website`: <http://127.0.0.1:3000>
|
||||
|
39
Dockerfile
39
Dockerfile
@ -1,23 +1,28 @@
|
||||
FROM node:16.14.0 AS dependencies
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:20.12.2 AS builder-dependencies
|
||||
WORKDIR /usr/src/application
|
||||
COPY ./package*.json ./
|
||||
RUN npm install
|
||||
RUN npm clean-install
|
||||
|
||||
FROM node:16.14.0 AS builder
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:20.12.2 AS builder
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV IS_STANDALONE=true
|
||||
WORKDIR /usr/src/application
|
||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||
COPY ./ ./
|
||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
|
||||
FROM node:16.14.0 AS runner
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:20.12.2-slim AS runner
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||
COPY --from=builder /usr/src/app/public ./public
|
||||
COPY --from=builder /usr/src/app/.next ./.next
|
||||
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
|
||||
COPY --from=builder /usr/src/app/locales ./locales
|
||||
COPY --from=builder /usr/src/app/pages ./pages
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
RUN npx next telemetry disable
|
||||
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV IS_STANDALONE=true
|
||||
WORKDIR /usr/src/application
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
|
||||
USER applicationrunner
|
||||
COPY --from=builder-dependencies --chown=applicationrunner:nodejs /usr/src/application/node_modules ./node_modules
|
||||
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/standalone ./
|
||||
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/static ./.next/static
|
||||
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/public ./public
|
||||
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/i18n/translations ./i18n/translations
|
||||
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/next.config.js ./next.config.js
|
||||
CMD ["./server.js"]
|
||||
|
4
LICENSE
4
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
# MIT License
|
||||
|
||||
Copyright (c) Divlo
|
||||
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
41
README.md
41
README.md
@ -1,19 +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">
|
||||
<strong>Developer Full Stack Junior • Passionate about High-Tech</strong>
|
||||
<strong>Developer Full Stack • Open-Source Enthusiast</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Divlo/Divlo/actions/workflows/Divlo.yml"><img src="https://github.com/Divlo/Divlo/actions/workflows/Divlo.yml/badge.svg?branch=master" alt="Divlo's CI" /></a>
|
||||
<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://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://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://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://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.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.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="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="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/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/~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/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/@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/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&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@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>
|
||||
|
||||
<hr />
|
||||
@ -22,20 +21,16 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Divlo",
|
||||
"name": "Théo LUDWIG",
|
||||
"pronouns": "He/Him",
|
||||
"birthDate": "31/03/2003",
|
||||
"nationality": "Alsace, France",
|
||||
"interests": [
|
||||
"Developer Full Stack Junior",
|
||||
"Passionate about High-Tech",
|
||||
"Open-Source enthusiast"
|
||||
],
|
||||
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
|
||||
"skills": {
|
||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
||||
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -45,6 +40,6 @@
|
||||
## 📈 Statistics
|
||||
|
||||
<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/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?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" />
|
||||
<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" alt="Théo LUDWIG's Programming Languages" />
|
||||
</p>
|
||||
|
11
app/blog/[slug]/loading.tsx
Normal file
11
app/blog/[slug]/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
44
app/blog/[slug]/page.tsx
Normal file
44
app/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { getBlogPostBySlug } from "@/blog/blog"
|
||||
import { BlogPost } from "@/blog/BlogPost"
|
||||
|
||||
interface BlogPostPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = async (
|
||||
props: BlogPostPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const blogPost = await getBlogPostBySlug(props.params.slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
|
||||
const description = blogPost.frontmatter.description
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
|
||||
const { params } = props
|
||||
|
||||
return <BlogPost slug={params.slug} />
|
||||
}
|
||||
|
||||
export default BlogPostPage
|
11
app/blog/loading.tsx
Normal file
11
app/blog/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
42
app/blog/page.tsx
Normal file
42
app/blog/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Suspense } from "react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { BlogPosts } from "@/blog/BlogPosts"
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const title = "Blog | Théo LUDWIG"
|
||||
const description =
|
||||
"The latest news about my journey of learning computer science."
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
|
||||
const BlogPage = async (): Promise<JSX.Element> => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col flex-wrap items-center">
|
||||
<div className="mt-10 flex flex-col items-center">
|
||||
<h1 className="text-4xl font-semibold text-primary dark:text-primary-dark">
|
||||
Blog
|
||||
</h1>
|
||||
<p className="mt-6 text-center" data-cy="blog-post-date">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<Suspense fallback={<Loader className="mt-8" />}>
|
||||
<BlogPosts />
|
||||
</Suspense>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPage
|
32
app/error.tsx
Normal file
32
app/error.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
export interface ErrorHandlingProps {
|
||||
error: Error
|
||||
}
|
||||
|
||||
const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
|
||||
const { error } = props
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 className="my-6 text-4xl font-semibold">
|
||||
Error{" "}
|
||||
<span
|
||||
className="text-primary dark:text-primary-dark"
|
||||
data-cy="status-code"
|
||||
>
|
||||
500
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-center text-lg">Server error</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandling
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
78
app/globals.css
Normal file
78
app/globals.css
Normal file
@ -0,0 +1,78 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.break-wrap-words {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.prose {
|
||||
@apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
@apply text-justify;
|
||||
}
|
||||
|
||||
.prose [id]::before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 90px;
|
||||
margin-top: -90px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.prose a,
|
||||
.prose strong {
|
||||
@apply !font-semibold text-primary dark:text-primary-dark;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4,
|
||||
.prose h5,
|
||||
.prose h6 {
|
||||
@apply mt-1 text-gray dark:text-gray-dark;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
color: #ce9178;
|
||||
}
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::before,
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
||||
content: "";
|
||||
}
|
||||
.shiki {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
code {
|
||||
counter-reset: step;
|
||||
counter-increment: step 0;
|
||||
}
|
||||
code .line::before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 1rem;
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: rgba(133, 133, 133, 0.8);
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.katex .base {
|
||||
display: inline !important;
|
||||
white-space: normal !important;
|
||||
width: 100% !important;
|
||||
}
|
80
app/layout.tsx
Normal file
80
app/layout.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import type { Metadata } from "next"
|
||||
import classNames from "clsx"
|
||||
|
||||
import "@fontsource/montserrat/400.css"
|
||||
import "@fontsource/montserrat/600.css"
|
||||
import "./globals.css"
|
||||
|
||||
import { Header } from "@/components/Header"
|
||||
import { Footer } from "@/components/Footer"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
import { getTheme } from "@/theme/theme.server"
|
||||
|
||||
const title = "Théo LUDWIG"
|
||||
const description =
|
||||
"Théo LUDWIG - Developer Full Stack • Open-Source Enthusiast"
|
||||
const image = "/images/logo.png"
|
||||
const url = new URL("https://theoludwig.fr")
|
||||
const locale = "fr-FR, en-US"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
metadataBase: url,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
siteName: title,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
width: 96,
|
||||
height: 96,
|
||||
},
|
||||
],
|
||||
locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title,
|
||||
description,
|
||||
images: [image],
|
||||
},
|
||||
}
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const RootLayout = (props: RootLayoutProps): JSX.Element => {
|
||||
const { children } = props
|
||||
|
||||
const i18n = getI18n()
|
||||
const theme = getTheme()
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={i18n.locale}
|
||||
className={classNames(
|
||||
{
|
||||
dark: theme === "dark",
|
||||
light: theme === "light",
|
||||
},
|
||||
"scroll-smooth",
|
||||
)}
|
||||
style={{
|
||||
colorScheme: theme,
|
||||
}}
|
||||
>
|
||||
<body className="flex min-h-screen flex-col bg-white font-headline text-black dark:bg-black dark:text-white">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootLayout
|
11
app/loading.tsx
Normal file
11
app/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
32
app/not-found.tsx
Normal file
32
app/not-found.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
const NotFound = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 className="my-6 text-4xl font-semibold">
|
||||
{i18n.translate("errors.error")}{" "}
|
||||
<span
|
||||
className="text-primary dark:text-primary-dark"
|
||||
data-cy="status-code"
|
||||
>
|
||||
404
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-center text-lg">
|
||||
{i18n.translate("errors.not-found")}{" "}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
{i18n.translate("errors.return-to-home-page")}
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
59
app/page.tsx
Normal file
59
app/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { RevealFade } from "@/components/design/RevealFade"
|
||||
import { Section } from "@/components/design/Section"
|
||||
import { Interests } from "@/components/Interests"
|
||||
import { Portfolio } from "@/components/Portfolio"
|
||||
import { Profile } from "@/components/Profile"
|
||||
import { SocialMediaList } from "@/components/Profile/SocialMediaList"
|
||||
import { Skills } from "@/components/Skills"
|
||||
import { OpenSource } from "@/components/OpenSource"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
const HomePage = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl">
|
||||
<Section isMain id="about">
|
||||
<Profile />
|
||||
<SocialMediaList />
|
||||
</Section>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="interests"
|
||||
heading={i18n.translate("home.interests.title")}
|
||||
>
|
||||
<Interests />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="skills"
|
||||
heading={i18n.translate("home.skills.title")}
|
||||
withoutShadowContainer
|
||||
>
|
||||
<Skills />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="portfolio"
|
||||
heading={i18n.translate("home.portfolio.title")}
|
||||
withoutShadowContainer
|
||||
>
|
||||
<Portfolio />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section id="open-source" heading="Open source" withoutShadowContainer>
|
||||
<OpenSource />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
37
blog/BlogPost.tsx
Normal file
37
blog/BlogPost.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import date from "date-and-time"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { getBlogPostBySlug } from "@/blog/blog"
|
||||
import { BlogPostContent } from "@/blog/BlogPostContent"
|
||||
|
||||
export interface BlogPostProps {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
|
||||
const { slug } = props
|
||||
|
||||
const blogPost = await getBlogPostBySlug(slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center">
|
||||
<div className="my-10 flex flex-col items-center text-center">
|
||||
<h1 className="text-3xl font-semibold text-primary dark:text-primary-dark">
|
||||
{blogPost.frontmatter.title}
|
||||
</h1>
|
||||
<p className="mt-2" data-cy="blog-post-date">
|
||||
{date.format(
|
||||
new Date(blogPost.frontmatter.publishedOn),
|
||||
"DD/MM/YYYY",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<BlogPostContent content={blogPost.content} />
|
||||
</main>
|
||||
)
|
||||
}
|
33
blog/BlogPostComments.tsx
Normal file
33
blog/BlogPostComments.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import Giscus from "@giscus/react"
|
||||
|
||||
import { useTheme } from "@/theme/theme.client"
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
|
||||
interface BlogPostCommentsProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
|
||||
const theme = useTheme(cookiesStore)
|
||||
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
)
|
||||
}
|
121
blog/BlogPostContent.tsx
Normal file
121
blog/BlogPostContent.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { cookies } from "next/headers"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons"
|
||||
import { MDXRemote } from "next-mdx-remote/rsc"
|
||||
import { nodeTypes } from "@mdx-js/mdx"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import rehypeSlug from "rehype-slug"
|
||||
import remarkMath from "remark-math"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import { getHighlighter } from "shiki"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { getTheme } from "@/theme/theme.server"
|
||||
import { remarkSyntaxHighlightingPlugin } from "@/blog/remarkSyntaxHighlightingPlugin"
|
||||
import { BlogPostComments } from "@/blog/BlogPostComments"
|
||||
|
||||
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 size-4" icon={faLink} />
|
||||
</Link>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export interface BlogPostContentProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const BlogPostContent = async (
|
||||
props: BlogPostContentProps,
|
||||
): Promise<JSX.Element> => {
|
||||
const { content } = props
|
||||
|
||||
const cookiesStore = cookies()
|
||||
const theme = getTheme()
|
||||
|
||||
const highlighter = await getHighlighter({
|
||||
theme: `${theme}-plus`,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="prose mb-10">
|
||||
<div className="px-8">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
||||
remarkMath,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeRaw, { passThrough: nodeTypes }],
|
||||
rehypeKatex,
|
||||
],
|
||||
},
|
||||
}}
|
||||
components={{
|
||||
h1: Heading,
|
||||
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="size-auto"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
a: (props) => {
|
||||
const { href = "", ...rest } = props
|
||||
if (href.startsWith("#")) {
|
||||
return <a {...props} />
|
||||
}
|
||||
if (href.startsWith("../posts/")) {
|
||||
return (
|
||||
<a
|
||||
href={href
|
||||
.replace("../posts/", "/blog/")
|
||||
.replace(".md", "")}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <a target="_blank" rel="noopener noreferrer" {...props} />
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
42
blog/BlogPosts.tsx
Normal file
42
blog/BlogPosts.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import Link from "next/link"
|
||||
import date from "date-and-time"
|
||||
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
import { getBlogPosts } from "@/blog/blog"
|
||||
|
||||
export const BlogPosts = async (): Promise<JSX.Element> => {
|
||||
const posts = await getBlogPosts()
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center p-8">
|
||||
<ul className="w-[1600px]" data-cy="blog-posts">
|
||||
{posts.map((post) => {
|
||||
const postPublishedOn = date.format(
|
||||
new Date(post.frontmatter.publishedOn),
|
||||
"DD/MM/YYYY",
|
||||
)
|
||||
return (
|
||||
<li key={post.slug}>
|
||||
<Link href={`/blog/${post.slug}`} locale="en" data-cy={post.slug}>
|
||||
<ShadowContainer className="cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.02]">
|
||||
<h2
|
||||
data-cy="blog-post-title"
|
||||
className="text-xl font-semibold text-primary dark:text-primary-dark"
|
||||
>
|
||||
{post.frontmatter.title}
|
||||
</h2>
|
||||
<p data-cy="blog-post-date" className="mt-2">
|
||||
{postPublishedOn}
|
||||
</p>
|
||||
<p data-cy="blog-post-description" className="mt-3">
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
</ShadowContainer>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
65
blog/blog.ts
Normal file
65
blog/blog.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
import { cache } from "react"
|
||||
import matter from "gray-matter"
|
||||
|
||||
export const BLOG_POSTS_PATH = path.join(process.cwd(), "blog", "posts")
|
||||
|
||||
export interface FrontMatter {
|
||||
title: string
|
||||
description: string
|
||||
isPublished: boolean
|
||||
publishedOn: string
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
frontmatter: FrontMatter
|
||||
slug: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
||||
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||
const blogPostsWithTime = await Promise.all(
|
||||
blogPosts.map(async (blogPostFilename) => {
|
||||
const [slug, extension] = blogPostFilename.split(".")
|
||||
if (slug == null || extension == null) {
|
||||
throw new Error("Invalid blog post filename.")
|
||||
}
|
||||
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||
encoding: "utf8",
|
||||
})
|
||||
const { data, content } = matter(blogPostContent) as unknown as {
|
||||
data: FrontMatter
|
||||
content: string
|
||||
}
|
||||
const date = new Date(data.publishedOn)
|
||||
return {
|
||||
slug,
|
||||
content,
|
||||
frontmatter: data,
|
||||
time: date.getTime(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||
.filter((post) => {
|
||||
return post.frontmatter.isPublished
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.time - a.time
|
||||
})
|
||||
return blogPostsSortedByPublicationDate
|
||||
})
|
||||
|
||||
export const getBlogPostBySlug = cache(
|
||||
async (slug: string): Promise<BlogPost | undefined> => {
|
||||
const blogPosts = await getBlogPosts()
|
||||
const blogPost = blogPosts.find((blogPost) => {
|
||||
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
||||
})
|
||||
return blogPost
|
||||
},
|
||||
)
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
title: '🧼 Clean Code'
|
||||
title: "🧼 Clean Code"
|
||||
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
|
||||
isPublished: true
|
||||
publishedOn: '2022-02-23T08:00:18.758Z'
|
||||
publishedOn: "2022-02-23T08:00:18.758Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
@ -25,7 +25,7 @@ It depends of many factors, and is somewhat relative to each one of us. The **pe
|
||||
|
||||
## 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 that works is great, but not enough, even if the code will be read and understood by the computer, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
||||
|
||||
For example the [Linux kernel](https://www.kernel.org/), is one of the biggest open source project with many contributors worldwide. Last data shows that it is about **20 millions** lines of code.
|
||||
|
||||
@ -77,8 +77,8 @@ setTimeout(restart, 86400000)
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
|
||||
setTimeout(restart, MILLISECONDS_IN_A_DAY)
|
||||
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000
|
||||
setTimeout(restart, MILLISECONDS_IN_ONE_DAY)
|
||||
```
|
||||
|
||||
---
|
||||
@ -110,7 +110,7 @@ const transaction = charge(user, subscription)
|
||||
```typescript
|
||||
interface Car {
|
||||
carModel: string
|
||||
carColor: 'red' | 'blue' | 'yellow'
|
||||
carColor: "red" | "blue" | "yellow"
|
||||
}
|
||||
const printCar = (car: Car): void => {
|
||||
console.log(`${car.carModel} (${car.carColor})`)
|
||||
@ -122,7 +122,7 @@ const printCar = (car: Car): void => {
|
||||
```typescript
|
||||
interface Car {
|
||||
model: string
|
||||
color: 'red' | 'blue' | 'yellow'
|
||||
color: "red" | "blue" | "yellow"
|
||||
}
|
||||
const printCar = (car: Car): void => {
|
||||
console.log(`${car.model} (${car.color})`)
|
||||
@ -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)
|
||||
|
||||
@ -168,14 +170,17 @@ We have to keep it as simple as possible, not to implement features that are not
|
||||
### Example (bad way)
|
||||
|
||||
```typescript
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import fs from "node:fs"
|
||||
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) {
|
||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
||||
return await fs.promises.writeFile(path.join("temporary", name), "")
|
||||
}
|
||||
return await fs.promises.writeFile(name, '')
|
||||
return await fs.promises.writeFile(name, "")
|
||||
}
|
||||
```
|
||||
|
||||
@ -184,15 +189,15 @@ const createFile = async (name: string, isTemporary: boolean = false) => {
|
||||
### Example (good way)
|
||||
|
||||
```typescript
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const createFile = async (name: string) => {
|
||||
await fs.promises.writeFile(name, '')
|
||||
const createFile = async (name: string): Promise<void> => {
|
||||
await fs.promises.writeFile(name, "")
|
||||
}
|
||||
|
||||
const createTemporaryFile = async (name: string) => {
|
||||
await createFile(path.join('temporary', name))
|
||||
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||
await createFile(path.join("temporary", name))
|
||||
}
|
||||
```
|
||||
|
258
blog/posts/git-ultimate-guide.md
Normal file
258
blog/posts/git-ultimate-guide.md
Normal file
@ -0,0 +1,258 @@
|
||||
---
|
||||
title: "🗓️ Git version control: Ultimate Guide"
|
||||
description: "What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow."
|
||||
isPublished: true
|
||||
publishedOn: "2022-10-27T14:33:07.465Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
Welcome to the Ultimate Guide to master `git` in your daily workflow, we will see what are the most used commands, what are the best practices, and tips and tricks.
|
||||
|
||||
This guide is a summary of the most important things to know when working with `git`, and in general, will link to the official documentation of `git` or other resources for more details, it is on purpose to not go in depth in each topic, it allows to summarize `git` and vocabulary about it (you can use it as a `git` cheatsheet).
|
||||
|
||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||
|
||||
## Introduction
|
||||
|
||||
**Git** is a free and open-source distributed **version control system** for keeping track of changes across a set of files.
|
||||
|
||||
Git was originally authored by [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds) in 2005 for the development of the [Linux kernel](https://kernel.org/).
|
||||
|
||||
Git allows:
|
||||
|
||||
- to work with several people on the same codebase.
|
||||
- track changes to know who did what and when.
|
||||
- revert changes.
|
||||
|
||||
Git is **decentralized**, which means that every developer has a full copy of the repository and the complete history of the project.
|
||||
|
||||
## Get started with `git` and `.gitconfig` config file
|
||||
|
||||
The first thing you should do when you install Git is to set your user name and email address.
|
||||
|
||||
```sh
|
||||
git config --global user.name "Username"
|
||||
git config --global user.email "email@example.com"
|
||||
```
|
||||
|
||||
These configurations are stored in the `.gitconfig` file in your home directory (e.g: `~/.gitconfig`) with this format:
|
||||
|
||||
```sh
|
||||
[user]
|
||||
name = Username
|
||||
email = email@example.com
|
||||
```
|
||||
|
||||
You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config).
|
||||
|
||||
## How `git` works?
|
||||
|
||||
Each `git` project is called a **repository** (or **repo** for short) and it contains all the files and folders for a project, as well as each file's revision history (**commits**) stored in the `.git` folder.
|
||||
|
||||
The history of a repository is represented by a graph.
|
||||
|
||||
Each node is called commit and contains:
|
||||
|
||||
- an instantaneous view (snapshot) of the state of the repository at a specific moment
|
||||
- metadata: message, author, creation date, etc.
|
||||
|
||||
Commits are **snapshots** (not diffs on each file) of the project at specific moments in time.
|
||||
|
||||
There are several areas where the files in your project will live in Git:
|
||||
|
||||
- **Working directory**: the files that you see in your computer's file system.
|
||||
- **Staging area**: the files that will go into your next commit (files added with `git add <filename>` command).
|
||||
- **Local repository**: the `.git` directory, which contains all of your project's commits, branches, etc. (files added with `git commit -m "message"` command).
|
||||
- **Remote repository**: the `.git` directory in a remote server (files added with `git push` command).
|
||||
|
||||
## Commands cheatsheet
|
||||
|
||||
You can find the official documentation of `git` commands at [git-scm.com/docs](https://git-scm.com/docs).
|
||||
|
||||
```sh
|
||||
# Initialize a new git repository
|
||||
git init
|
||||
|
||||
# Clone a repository
|
||||
git clone <url>
|
||||
|
||||
# Add all the files to staging area
|
||||
git add .
|
||||
|
||||
# Add specific file to staging area
|
||||
git add <file>
|
||||
|
||||
# Commit changes
|
||||
git commit -m "Commit message"
|
||||
|
||||
# Commit changes in the past
|
||||
git commit --date "10 day ago" -m "Commit message"
|
||||
|
||||
# Add remote repository
|
||||
git remote add <remote> <url>
|
||||
# The main <remote> is often called `origin`
|
||||
|
||||
# Add forked repository
|
||||
git remote add <remote> <url>
|
||||
# The forked <remote> is often called `upstream`
|
||||
|
||||
# List all the remotes
|
||||
git remote
|
||||
|
||||
# Sync forked repository
|
||||
git fetch <remote>
|
||||
git merge <remote>/<branch>
|
||||
|
||||
# Push changes to remote repository
|
||||
git push <remote>
|
||||
|
||||
# Pull changes from remote repository
|
||||
git pull <remote>
|
||||
|
||||
# Show the status of the working tree
|
||||
git status
|
||||
|
||||
# Show the commit history
|
||||
git log
|
||||
|
||||
# Create a new branch
|
||||
git checkout -b <branch>
|
||||
|
||||
# Switch to a branch (or tag or commit)
|
||||
git checkout <branch>
|
||||
|
||||
# Merge a branch into the current branch
|
||||
git merge <branch>
|
||||
|
||||
# Note: Merge creates a "Merge commit" when the base branch and the branch to merge have diverged (they have different commits).
|
||||
|
||||
# To avoid creating a "Merge commit", we can use rebase instead of merge.
|
||||
git rebase --interactive <branch-to-rebase-on>
|
||||
|
||||
# Combine multiple commits of a branch into one for a merge
|
||||
git merge --squash <branch>
|
||||
|
||||
# Change several past commits (interactive rebase)
|
||||
# HEAD points to the current consulted commit.
|
||||
git rebase --interactive HEAD~<number-of-commits>
|
||||
|
||||
# Delete a branch
|
||||
git branch --delete <branch>
|
||||
git push <remote> --delete <branch>
|
||||
|
||||
# Fetch branches from remote repository and prune
|
||||
git fetch --prune
|
||||
|
||||
# Revert a commit
|
||||
git revert <commit>
|
||||
|
||||
# Reset the current branch, delete all commits since <branch> (without removing the changes)
|
||||
git reset --soft <branch>
|
||||
|
||||
# Apply the changes introduced by some existing commits
|
||||
# (by first being on the branch where you want to apply the commit)
|
||||
git cherry-pick <commit>
|
||||
|
||||
# To list all commits that differ between two branches
|
||||
git log <branch1>..<branch2> # commits in branch2 that are not in branch1 (branch2 ahead of branch1, branch2 behind branch1)
|
||||
git log <branch2>..<branch1> # commits in branch1 that are not in branch2 (branch1 ahead of branch2, branch1 behind branch2)
|
||||
|
||||
# Summary of commit authors across all branches, excluding merge commits.
|
||||
git shortlog --summary --numbered --all --no-merges
|
||||
```
|
||||
|
||||
## `.gitignore` file
|
||||
|
||||
The `.gitignore` file is a text file that tells `git` which files (or patterns) it should ignore.
|
||||
|
||||
The `.gitignore` file is usually placed in the root directory of the repository.
|
||||
|
||||
We usually ignore files that are generated by the build process or files that contain sensitive information.
|
||||
|
||||
Example of `.gitignore` file:
|
||||
|
||||
```sh
|
||||
.env
|
||||
build
|
||||
*.exe
|
||||
```
|
||||
|
||||
## `.gitkeep` file
|
||||
|
||||
The `.gitkeep` file is a file that is used to keep an empty directory in a Git repository.
|
||||
|
||||
This is useful when you want to keep an empty directory in your repository but you don't want to commit any file inside it.
|
||||
|
||||
## Git remote repositories (GitHub/GitLab)
|
||||
|
||||
Once you are ready to share your code over the internet, you will need to create a remote repository on a service like [GitHub](https://github.com) or [GitLab](https://gitlab.com).
|
||||
|
||||
There are many other services, you can also self-host your own Git server.
|
||||
|
||||
### SSH vs HTTPS authentication
|
||||
|
||||
Once you have created a remote repository, you will need to authenticate to push and pull changes.
|
||||
|
||||
There are two main ways to authenticate:
|
||||
|
||||
- **SSH**: you will need to generate an SSH key pair and add the public key to your remote repository.
|
||||
- **HTTPS**: you will need to provide your username and password each time you push or pull changes.
|
||||
|
||||
SSH authentication is the recommended way to authenticate to a remote repository.
|
||||
|
||||
You can find more information about SSH authentication in the [official documentation](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key).
|
||||
|
||||
### Sign `git` commits with `gpg`
|
||||
|
||||
As we have seen in the [Get started with `git` and `.gitconfig` config file](#get-started-with-git-and-gitconfig-config-file) section, we can configure `git` with a name and email address with a value of our choice.
|
||||
|
||||
That means that **anyone can create a commit with any name and email address and claim to be whoever they want** when they create a commit.
|
||||
|
||||
To avoid this, you can sign your commits with a [GNU Privacy Guard](https://gnupg.org/) (<abbr>gpg</abbr>) key.
|
||||
|
||||
You can find more information about signing commits in the [official documentation](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work).
|
||||
|
||||
### Continous Integration/Continuous Delivery (CI/CD)
|
||||
|
||||
Once you have your code in a remote repository, everyone (with access) can potentially start contributing to the project. This is great, but it also means that you need to have a way to ensure that your code is working as expected for each change in the project.
|
||||
|
||||
You could do it manually, depending on the size and the complexity of the project, but it could be a tedious task.
|
||||
|
||||
Instead, you can use a **Continuous Integration** (CI) service to automate the process of testing your code, running linting, unit tests, e2e tests, etc.
|
||||
|
||||
There are many CI services, but the most popular ones are [GitHub Actions](https://github.com/features/actions), [GitLab CI](https://docs.gitlab.com/ee/ci/), [CircleCI](https://circleci.com/), [Travis CI](https://travis-ci.org/), and many others...
|
||||
|
||||
Then, once your code is ready, tested and working as expected, you can use a **Continuous Delivery** (CD) service to automate the process of **deploying your code**.
|
||||
|
||||
CI/CD services are usually integrated with remote repositories, so you can configure them to run automatically when you push changes to the remote repository.
|
||||
|
||||
## Best practices and `git` workflows
|
||||
|
||||
Commit messages are very important, they are a way to easily know what has changed in the project.
|
||||
|
||||
There are many conventions for commit messages, but the most popular one is the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
Then, we can use the commit messages to automatically determine a [semantic version](https://semver.org/) for the next release of the project.
|
||||
|
||||
When multiple developers are working on the same project, it is important to organize the work in a way that everyone can work on different features without conflicts (changes in the same files).
|
||||
|
||||
There are many ways to organize the work, but the most popular ones are:
|
||||
|
||||
- [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)
|
||||
- [GitHub Flow](https://guides.github.com/introduction/flow/)
|
||||
- [Trunk-based development](https://trunkbaseddevelopment.com/)
|
||||
|
||||
They are called **Git workflows**, or **Git branching strategies**.
|
||||
|
||||
## Conclusion
|
||||
|
||||
`git` is the tool that every programmer should know to do collaborative work (not only, `git` is also very powerful even when working alone) and keep track of changes across a set of files.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Git official website and documentation](https://git-scm.com/)
|
||||
- [Git Explained in 100 Seconds](https://www.youtube.com/watch?v=hwP7WQkmECE)
|
||||
- [Understand Git in 7 minutes](https://www.jesuisundev.com/en/understand-git-in-7-minutes/)
|
||||
- [How (and why) to sign Git commits | With Blue Ink](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html?utm_source=tiktok&utm_campaign=codetok-sign)
|
||||
- [What Are the Best Git Branching Strategies](https://www.flagship.io/git-branching-strategies/)
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
title: '👋 Hello, world!'
|
||||
description: 'First post of the blog, introduction and explanation of how this blog is made.'
|
||||
title: "👋 Hello, world!"
|
||||
description: "First post of the blog, introduction and explanation of how this blog is made."
|
||||
isPublished: true
|
||||
publishedOn: '2022-02-20T08:00:18.758Z'
|
||||
publishedOn: "2022-02-20T08:00:18.758Z"
|
||||
---
|
||||
|
||||
Hello, world! 👋
|
||||
@ -13,11 +13,11 @@ This blog is here to document my journey of learning computer science, explainin
|
||||
|
||||
The idea is that I will share my knowledge with you (readers), and hopefully help you to learn too.
|
||||
|
||||
Keep in mind that I will not translate the posts in French, all the posts will be written in English, as I'm not a native English speaker, I will probably make mistakes, feel free to open pull requests on [GitHub](https://github.com/Divlo/Divlo) to correct them. 😊
|
||||
Keep in mind that I will not translate the posts in French, all the posts will be written in English, as I'm not a native English speaker, I will probably make mistakes, feel free to open pull requests on [GitHub](https://github.com/theoludwig/theoludwig) to correct them. 😊
|
||||
|
||||
I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned!
|
||||
|
||||
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@Divlo_FR](https://twitter.com/Divlo_FR).
|
||||
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@theoludwig\_](https://twitter.com/theoludwig_).
|
||||
|
||||
## Project based learning
|
||||
|
||||
@ -33,20 +33,13 @@ I learn something new, because it solved a "real life" problem I had encountered
|
||||
|
||||
In this section, I will explain what technologies I used to make this blog, and what are the technical choices I had to do.
|
||||
|
||||
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
||||
|
||||
I decided to keep things simple, here are the 2 main features missing on my blog:
|
||||
|
||||
- Comments (you can interact with me on my Twitter account)
|
||||
- Views counter
|
||||
|
||||
That not mean that these features will never be implemented, but to avoid the need of a database now, I dropped out these features.
|
||||
The code of this website is open source on [GitHub](https://github.com/theoludwig/theoludwig), so you can see the code and contribute to it.
|
||||
|
||||
### Technologies
|
||||
|
||||
- [Next.js](https://nextjs.org/)
|
||||
|
||||
It allows to have a server-side rendered website, that means that it is faster and easier to have a good SEO (Search Engine Optimization) than a SPA (Single Page Application).
|
||||
It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>.
|
||||
|
||||
- [MDX](https://mdxjs.com/)
|
||||
|
66
blog/posts/mistakes-as-junior-developer.md
Normal file
66
blog/posts/mistakes-as-junior-developer.md
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "❌ Mistakes I made as a junior developer"
|
||||
description: "Here are mistakes I made when I started, to prevent you from making the same mistakes."
|
||||
isPublished: true
|
||||
publishedOn: "2022-03-14T07:42:52.989Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
I will explain some of the mistakes I made as a junior developer, so you can avoid doing them.
|
||||
|
||||
## 1. Skipped learning how to do automated tests
|
||||
|
||||
Probably one of the most common errors junior developers does.
|
||||
|
||||
When you begin in programming, you learn a programming language, so you learn variables, conditions, loops, functions, etc.
|
||||
|
||||
With these concepts, you might start a new project, but as the project grows, you will end up using functions at multiple places in code, so if you change the behavior of a function, it will affect the whole project.
|
||||
|
||||
And because the code grows, you might do some refactoring to make it more maintainable, but because we are humans, we make mistakes, you could accidentally break the entire project even with a tiny change you thought was safe to do.
|
||||
|
||||
If you had automated tests, you would have a way to know if you made a mistake even before deploying to production.
|
||||
|
||||
Depending on the programming language you are using, and what is the project you are working on, writing tests will be different.
|
||||
|
||||
Be aware that there are 3 main testing strategies:
|
||||
|
||||
- [Unit testing](https://en.wikipedia.org/wiki/Unit_testing)
|
||||
- [Integration testing](https://en.wikipedia.org/wiki/Integration_testing)
|
||||
- [End-to-end testing](https://en.wikipedia.org/wiki/End-to-end_testing)
|
||||
|
||||
After you learned the basics of programming, learn how to write automated tests, it will save you a lot of time and debugging.
|
||||
|
||||
I would even say that you should get used-to writing tests, it should be an automatism, you should not even have to think about it to do it.
|
||||
|
||||
## 2. Thinking too big, with too much abstraction
|
||||
|
||||
Abstraction is great, but it can be harder to understand what is going on if actually don't need this abstraction.
|
||||
|
||||
Find the right balance, between abstraction and simple implementation, start simple, and then gradually improve and add more features.
|
||||
|
||||
When you start a new project, you should focus on the core of the project, not on the details, to release as soon as possible, a working usable version of your project also called a [**Minimum Viable Product** (MVP)](https://en.wikipedia.org/wiki/Minimum_viable_product), it is better than a half-functioning, over-engineered project.
|
||||
|
||||
I made this mistake while developing [Thream](../posts/thream-v1-0-0.md), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
|
||||
|
||||
Basically, I thought it was cool, to do a "big" v1.0.0 release with a lot of features, but in fact, it was not, because I could not even show what I was developing (to the end-users, not technical people) as I was making multiple features at the same time and also mainly focused on the **REST API** side and not at all the **website (frontend)**.
|
||||
|
||||
What I recommend you to do is to start with a **v1.0.0** release as soon as possible with the minimum required features needed for your project idea, and then gradually add new features and release new versions.
|
||||
|
||||
In my example for [Thream](../posts/thream-v1-0-0.md), I could release a v1.0.0 without these features:
|
||||
|
||||
- English/French translation (could be only English)
|
||||
- Light/Dark theme (could be only Dark)
|
||||
- OAuth2 Authentication (could be only simple email/password authentication)
|
||||
- User public profile
|
||||
- Channels (maybe could be only one channel per guild to start with)
|
||||
|
||||
And probably more, what was really required with [Thream](../posts/thream-v1-0-0.md), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough.
|
||||
|
||||
And then with this basis, I could release, v1.1.0, v1.2.0 etc. with more features, and release new versions more often to show the progress of the project, it is also more motivating to have users testing our project and to **get feedback sooner**.
|
||||
|
||||
**Start simple, improve later.**
|
||||
|
||||
## Conclusion
|
||||
|
||||
The real key to success is to **be passionate**, **keep learning** on your own, and **look at mistakes as learning experiences**.
|
343
blog/posts/programming-challenges.md
Normal file
343
blog/posts/programming-challenges.md
Normal file
@ -0,0 +1,343 @@
|
||||
---
|
||||
title: "🧠 Programming Challenges"
|
||||
description: "What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation."
|
||||
isPublished: true
|
||||
publishedOn: "2023-05-21T10:20:18.837Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
As **performance** and **reliability** is more and more important in software development, it is important to know how to write **efficient code**, and also learn to **not rely on every possible dependency of the world**, when it is not worth it.
|
||||
|
||||
The more dependencies we add to our projects, the greater the complexity and maintenance overhead becomes. Each additional dependency requires understanding its functionality, <abbr title="Application Programming Interface">API</abbr>, and potential conflicts with other dependencies. This complexity makes the codebase harder to maintain, and it also poses significant security risks.
|
||||
|
||||
We don't want to "reinvent the wheel" and rewrite everything from scratch for each project. In fact, you are **always depending on something** when you are writing your software. At the very least, you are dependent on the programming language you are using. Even if you are doing very low-level stuff, you are still depending on something: hardware.
|
||||
|
||||
However, it is important to draw a line between what dependencies are worth the cost and which are not.
|
||||
|
||||
Most likely adding a [JavaScript npm package `is-odd`](https://www.npmjs.com/package/is-odd) to check if a number is odd or even for example, is not worth it. Writing it ourselves allows a better maintenance in the long term.
|
||||
|
||||
Learning **how to solve problems** and how to write efficient code is very important and also a very broad and complicated topic, so this blog post will only be an **introduction to the subject**, and will not go in depth.
|
||||
|
||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||
|
||||
## What is Competitive Programming?
|
||||
|
||||
**Competitive programming** consists of solving correctly and efficiently **well-defined problems** by writing **computer programs** under specified **constraints**. Typically a solution to a problem is a combination of well-known techniques and new insights.
|
||||
|
||||
There are many famous competitions: [Google Code Jam](https://codingcompetitions.withgoogle.com/codejam), [Facebook Hacker Cup](https://www.facebook.com/codingcompetitions/hacker-cup), [International Olympiad in Informatics](https://ioinformatics.org/), [International Collegiate Programming Contest](https://icpc.global/), [LeetCode](https://leetcode.com/), [CodinGame](https://www.codingame.com/), etc.
|
||||
|
||||
The most common programming languages used for Competitive Programming are: **C++**, **Python** and **Java**. However the design of the algorithms and data structures are applicable to **any programming language**.
|
||||
|
||||
All examples solutions on this blog post will be done in **Python**.
|
||||
|
||||
## Topics to explore/learn with Competitive Programming
|
||||
|
||||
- Time/Space complexity and Big O Notation
|
||||
|
||||
- Sorting: Sorting algorithms and Binary search
|
||||
|
||||
- Data structures: Arrays (1D, 2D: Matrix, 3D, Multidimensional), Dictionaries, Linked lists, Stack, Queue, Trees, Graphs, Heaps, etc.
|
||||
|
||||
- Complete search: Generating Subsets, Permutations, Combinations, etc.
|
||||
|
||||
- Greedy algorithms: Coin problem, Scheduling, Minimizing sums, etc.
|
||||
|
||||
- Dynamic programming: Fibonacci, Coin problem, Knapsack, etc.
|
||||
|
||||
- Bit manipulation: Bit representation, Bit operations, etc.
|
||||
|
||||
- Shortest path: Dijkstra, Bellman-Ford, Floyd-Warshall, etc.
|
||||
|
||||
- String: Trie structure, String hashing, Z-algorithm, etc.
|
||||
|
||||
You can see there are lot of concepts to learn and explore, and it is not an exhaustive list. On this blog post, we will only see the first topic: **Time/Space complexity and Big O Notation**.
|
||||
|
||||
## Time/Space complexity and Big O Notation
|
||||
|
||||
### Definition
|
||||
|
||||
An Algorithm is a finite sequence of well-defined instructions, that have to be given to the computer to perform a specific task. In this context, the variation can occur the way how the instructions are defined. There can be **any number of ways**, a specific set of instructions can be defined **to perform the same task**. Also, with options available to choose any one of the available programming languages, the instructions can take any form of syntax along with the performance boundaries of the chosen programming language. We also indicated the algorithm to be performed in a computer, which leads to the next variation, in terms of the operating system, processor, hardware, etc. that are used, which can also influence the way an algorithm can be performed.
|
||||
|
||||
Different factors can influence the outcome of an algorithm being executed, it is wise to understand how efficiently such programs are used to perform a task. To gauge this, we require to evaluate:
|
||||
|
||||
- The **Space complexity** of an algorithm **quantifies** the amount of **space or memory taken** by an algorithm to run based on the size of the input.
|
||||
- The **Time complexity** of an algorithm **quantifies** the amount of **time taken** by an algorithm to run based on the size of the input.
|
||||
|
||||
We more often talk about the **time complexity** than space complexity of an algorithm, because we can reuse memory unlike time and memory is cheap nowadays.
|
||||
|
||||
**Big O Notation** describes the complexity of an algorithm in terms of **how quickly it grows relative to the input size $n$ (e.g: length of the string, size of the array etc.)** by defining the $N$ number of operations that are done on it.
|
||||
Example of Big O notation: $O(n^2)$.
|
||||
|
||||
### Time complexity
|
||||
|
||||
Time complexity **measures** the **time taken** **to execute each statement** of code in an algorithm. It is not going to examine the total execution time of an algorithm. Rather, it is going to give information about the variation (increase or decrease) in execution time when the number of operations (increase or decrease) in an algorithm.
|
||||
|
||||
There are many rules to calculate the time complexity of an algorithm.
|
||||
|
||||
#### Loops
|
||||
|
||||
A common reason why an algorithm is slow is that it contains many loops that go through the input. The more nested loops the algorithm contains, the slower it will run.
|
||||
|
||||
If there are $k$ nested loops, the time complexity of the algorithm will be $O(n^k)$.
|
||||
|
||||
##### Example $O(n)$
|
||||
|
||||
```python
|
||||
for iteration in range(n):
|
||||
pass
|
||||
|
||||
# or with a while loop
|
||||
iteration = 0
|
||||
while iteration < n:
|
||||
pass
|
||||
```
|
||||
|
||||
##### Example $O(n^2)$
|
||||
|
||||
```python
|
||||
for iteration in range(n):
|
||||
for iteration2 in range(n):
|
||||
pass
|
||||
```
|
||||
|
||||
##### Example $O(n^3)$
|
||||
|
||||
```python
|
||||
for iteration in range(n):
|
||||
for iteration2 in range(n):
|
||||
for iteration3 in range(n):
|
||||
pass
|
||||
```
|
||||
|
||||
etc.
|
||||
|
||||
#### Order of magnitude
|
||||
|
||||
A time complexity does not tell us the exact number of times the code inside a loop is executed, but it only shows the **order of magnitude**.
|
||||
|
||||
In the following examples, the time complexity of the algorithms is $O(n)$ but the number of operations is different.
|
||||
|
||||
##### Example 1
|
||||
|
||||
```python
|
||||
for iteration in range(0, n * 3, 1):
|
||||
pass
|
||||
```
|
||||
|
||||
Number of operations: $3n$
|
||||
|
||||
##### Example 2
|
||||
|
||||
```python
|
||||
for iteration in range(0, n + 5, 1):
|
||||
pass
|
||||
```
|
||||
|
||||
Number of operations: $n + 5$
|
||||
|
||||
##### Example 3
|
||||
|
||||
```python
|
||||
for iteration in range(0, n, 2):
|
||||
pass
|
||||
```
|
||||
|
||||
Number of operations: ${n \over 2}$
|
||||
|
||||
#### Phases
|
||||
|
||||
If the algorithms consists of consecutive phases, the total time complexity is the largest time complexity of a single phase because it is usually the bottleneck of the code.
|
||||
|
||||
The following code consists of 3 phases, with time complexities $O(n)$, $O(n^2)$ and $O(n)$. Thus the total time complexity is $O(n^2)$.
|
||||
|
||||
```python
|
||||
for iteration in range(n):
|
||||
pass
|
||||
|
||||
for iteration in range(n):
|
||||
for iteration2 in range(n):
|
||||
pass
|
||||
|
||||
for iteration in range(n):
|
||||
pass
|
||||
```
|
||||
|
||||
#### Several variables
|
||||
|
||||
Sometimes the time complexity depends on several factors. In this case, the time complexity formula contains several variables: $O(nm)$.
|
||||
|
||||
```python
|
||||
for iteration in range(n):
|
||||
for iteration2 in range(m):
|
||||
pass
|
||||
```
|
||||
|
||||
#### Recursion
|
||||
|
||||
The time complexity of a recursive function depends on the number of times it is called and the time complexity of a single call. The total time complexity is the product of these values.
|
||||
|
||||
##### Example 1
|
||||
|
||||
The call `recursive(n)` causes $n$ calls and the time complexity of each call is $O(1)$. Thus the total time complexity is $O(n)$.
|
||||
|
||||
```python
|
||||
def recursive(n: int):
|
||||
if n != 1:
|
||||
recursive(n - 1)
|
||||
```
|
||||
|
||||
##### Example 2
|
||||
|
||||
```python
|
||||
def recursive(n: int):
|
||||
if n != 1:
|
||||
recursive(n - 1)
|
||||
recursive(n - 1)
|
||||
```
|
||||
|
||||
In this case, `recursive(n)` causes 2 other calls except for $n = 1$.
|
||||
|
||||
The following table shows the function calls produced by this single call:
|
||||
|
||||
| function call | number of calls |
|
||||
| ------------- | --------------- |
|
||||
| $g(n)$ | $1$ |
|
||||
| $g(n - 1)$ | $2$ |
|
||||
| $g(n - 2)$ | $4$ |
|
||||
| ... | ... |
|
||||
| $g(1)$ | $2^{n - 1}$ |
|
||||
|
||||
Based on this, the time complexity is:
|
||||
|
||||
$$
|
||||
1 + 2 + 4 + ... + 2^{n - 1} = 2^n - 1 = O(2^n)
|
||||
$$
|
||||
|
||||
#### Complexity Classes (from fastest to slowest)
|
||||
|
||||

|
||||
|
||||
Here is a list of classes of functions that are commonly encountered when analyzing the running time of an algorithm.
|
||||
|
||||
- $O(1)$: **Constant** (does not depend on the input size). A typical constant-time algorithm is a direct formula that calculates the answer.
|
||||
|
||||
- $O(\log_2(n))$: **Logarithmic** algorithm often halves the input size at each step. $\log_2(n)$ equals the number of times $n$ must be divided by 2 to get 1.
|
||||
|
||||
- $O(\sqrt{n})$: **Square root** algorithm is slower than $O(\log_2(n))$ but faster than $O(n)$.
|
||||
|
||||
- $O(n)$: **Linear** algorithm goes through the input a constant number of times. This is often the best possible time complexity, because it is usually necessary to access each input element at least once before reporting the answer.
|
||||
|
||||
- $O(n \log_2(n))$: **Log linear** often indicates that the algorithm sorts the input, because the time complexity of efficient sorting algorithms is $O(n \log_2(n))$. Another possibility is that the algorithm uses a data structure where each operation takes $O(\log_2(n))$ time.
|
||||
|
||||
- $O(n^2)$: **Quadratic** algorithm often contains 2 nested loops. It is possible to go trough all pairs of the input elements in $O(n^2)$ time.
|
||||
|
||||
- $O(n^3)$: **Cubic** algorithm often contains 3 nested loops. It is possible to go trough all triplets of the input elements in $O(n^3)$ time.
|
||||
|
||||
- $O(2^n)$: **Exponential** often indicates that the algorithm iterates through all subsets of the input elements. For example, the subsets of $\{1, 2, 3\}$ are $S = \{\{\empty\}, \{1\}, \{2\}, \{3\}, \{1, 2\}, \{1, 3\}, \{2, 3\}, \{1, 2, 3\} \}$.
|
||||
|
||||
- $O(n!)$: **Factorial** often indicates that the algorithm iterates through all permutations of the input elements. For example, the permutations of $\{1, 2, 3\}$ are $P = \{\{1, 2, 3\}, \{1, 3, 2\}, \{2, 1, 3\}, \{2, 3, 1\}, \{3, 1, 2\}, \{3, 2, 1\} \}$.
|
||||
|
||||
### Estimating efficiency
|
||||
|
||||
By checking the time complexity of an algorithm, it is possible to check before implementing the algorithm, that it is efficient enough for the problem.
|
||||
|
||||
Example: assume that the time limit for a problem is 1 second and the input size is $n = 10^5$. If the time complexity is $O(n^2)$, the algorithm will perform about $(10^5)^2 = 10^{10}$ operations.
|
||||
|
||||
Given that a modern computer can perform some hundred of millions of operations per second. This should take at least 10 seconds, so the algorithm seems to be too slow for solving the problem.
|
||||
|
||||
## Practical problem: Maximum subarray sum
|
||||
|
||||
There are often several possible algorithms for solving a problem such that their time complexities are different. This section discusses a classic problem that can be solved using several different algorithmic techniques, including brute force, divide and conquer, dynamic programming, and reduction to shortest paths, each technique with different time complexity.
|
||||
|
||||
**Maximum subarray sum**: Given an array of $n$ integers, find the contiguous subarray with the largest sum.
|
||||
|
||||
Contiguous subarray is any sub series of elements in a given array that are contiguous ie their indices are continuous. The problem is interesting when there may be negative values in the array, because if the array only contains positive values, the maximum subarray sum is basically the sum of the array (the subarray being the complete array).
|
||||
|
||||
### Example 1
|
||||
|
||||
#### Input
|
||||
|
||||
```txt
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
```txt
|
||||
21
|
||||
```
|
||||
|
||||
**Explanation:** The subarray with the largest sum is the array itself (as there is no negative values) `[1, 2, 3, 4, 5, 6]` which has a sum of `21`.
|
||||
|
||||
### Example 2
|
||||
|
||||
#### Input
|
||||
|
||||
```txt
|
||||
[-1, 2, 4, -3, 5, 2, -5, 2]
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
```txt
|
||||
10
|
||||
```
|
||||
|
||||
**Explanation:** The subarray with the largest sum is `[2, 4, -3, 5, 2]` which has a sum of `10`.
|
||||
|
||||
### Worst solution: Brute force ($O(n^3)$)
|
||||
|
||||
```python
|
||||
def maximum_subarray_sum_cubic(array: list[int]) -> int:
|
||||
"""
|
||||
Time complexity: O((array_length)^3)
|
||||
|
||||
We go through all possible subarrays, calculate the sum in each subarray and maintain the maximum sum.
|
||||
"""
|
||||
if len(array) == 0:
|
||||
return 0
|
||||
best_sum = array[0]
|
||||
length = len(array)
|
||||
for i in range(length):
|
||||
for j in range(i, length):
|
||||
sum = 0
|
||||
for k in range(i, j + 1):
|
||||
sum += array[k]
|
||||
if sum > best_sum:
|
||||
best_sum = sum
|
||||
return best_sum
|
||||
```
|
||||
|
||||
### Better solution: Linear time ($O(n)$)
|
||||
|
||||
```python
|
||||
def maximum_subarray_sum_linear(array: list[int]) -> int:
|
||||
"""
|
||||
Time complexity: O(array_length)
|
||||
|
||||
We loop through the array and for each array position, we calculate the maximum sum of a subarray that ends at that position. After this, the answer for the problem is the maximum of those sums.
|
||||
"""
|
||||
if len(array) == 0:
|
||||
return 0
|
||||
best_sum = array[0]
|
||||
length = len(array)
|
||||
sum = 0
|
||||
for i in range(length):
|
||||
sum = max(array[i], sum + array[i])
|
||||
best_sum = max(best_sum, sum)
|
||||
return best_sum
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Problems solving is a very complicated and large topic, and also a very important skill to have as a software developer.
|
||||
|
||||
To improve our problems solving skills, we can regularly practice with [programming challenges](https://github.com/theoludwig/programming-challenges).
|
||||
|
||||
## Sources
|
||||
|
||||
- [Wikipedia - Competitive programming](https://en.wikipedia.org/wiki/Competitive_programming)
|
||||
- [Frontend Masters - The Last Algorithms Course You'll Need](https://frontendmasters.com/courses/algorithms/)
|
||||
- [Big-O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||
- [programming challenges](https://github.com/theoludwig/programming-challenges)
|
127
blog/posts/thream-v1-0-0.md
Normal file
127
blog/posts/thream-v1-0-0.md
Normal file
@ -0,0 +1,127 @@
|
||||
---
|
||||
title: "🟢 Thream v1.0.0"
|
||||
description: "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun."
|
||||
isPublished: true
|
||||
publishedOn: "2022-04-11T10:24:55.206Z"
|
||||
---
|
||||
|
||||
⚠️ **Thream** is **not maintained anymore**, and is no longer accessible on ~~[thream.theoludwig.fr](https://thream.theoludwig.fr)`~~.
|
||||
|
||||
While the project taught me a lot, it had too much ambitions for new features, with nearly no users, and no contributors.
|
||||
|
||||
You can still use the code as you wish and fork it to maintain it yourself, as the code is completely open source on [GitHub](https://github.com/Thream).
|
||||
|
||||
This blog post is still available to explain the project, and how it was implemented.
|
||||
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
After months of hard work, [Thream v1.0.0](https://github.com/Thream) has been released! 🎉
|
||||
|
||||
[**Thream**](https://github.com/Thream) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
|
||||
|
||||
## Presentation
|
||||
|
||||
[**Thream**](https://github.com/Thream) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
|
||||
|
||||
The project is largely inspired by [Discord](https://discord.com), a proprietary instant messaging service, but differentiates itself by its **non-profit open source philosophy** and will integrate special features.
|
||||
|
||||
The source code is available on [GitHub](https://github.com/Thream).
|
||||
|
||||
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
|
||||
|
||||

|
||||
|
||||
[**Thream**](https://github.com/Thream) is a website that works on any recent browser.
|
||||
|
||||
## History
|
||||
|
||||
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG.
|
||||
|
||||
The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features.
|
||||
|
||||
The development of the project begins under the name of **SocialProject**, on August 20, 2020.
|
||||
|
||||

|
||||
|
||||
When I started the project, I had little knowledge of database design, real-time management or the architecture of such a large <abbr title="Information Technology">IT</abbr> project, so this will be accompanied by many technical problems, to which we will need to find appropriate solutions.
|
||||
|
||||
On October 19, 2020, **SocialProject** becomes **Thream**, an invented name, not yet used and more original than the previous one, and also changes colors so that the application is accessible in two distinct themes (light and dark).
|
||||
|
||||
With the help of [Walidoux](https://github.com/Walidoux), a junior developer really good at making beautiful <abbr title="User Interface">UI</abbr> with <abbr title="Cascading Style Sheets">CSS</abbr>, we were able to collaborate on this side project together.
|
||||
|
||||
Since the project is mainly developed during free time (mainly on weekends), the project took longer to be developed than desired, but now we finally released the first version. 🥳
|
||||
|
||||
## Implementation and Technical Difficulties
|
||||
|
||||
### Architecture
|
||||
|
||||
**Thream** is divided into two distinct projects:
|
||||
|
||||
- The **server** part, called **backend**, which the user does not see, allows actions to be taken to save or recover data in the **database**, it is the technical and functional aspect of the project. This part uses a style of software architecture defining a set of constraints to be used to create web services that establish interoperability between computers on the Internet, called REST <abbr title="Application Programming Interface">API</abbr>.
|
||||
|
||||
- The **client** part, called **frontend**, what **the user sees on the screen**, such as forms, buttons and all the **graphic elements** with which the user can interact from a browser.
|
||||
|
||||

|
||||
|
||||
This design allows the separation between the client and the server, as long as they both structure their communication according to the <abbr title="Representational state transfer">REST</abbr> architectural guidelines, using the <abbr title="Hypertext Transfer Protocol">HTTP</abbr> protocol, they will be able to communicate with each other, which makes it possible to work independently on the backend and on the frontend using different technologies and skills, really useful in teamwork.
|
||||
|
||||
To allow the development of this design, it is necessary to think about its architecture in order to solve the following problem: how to store and structure the folders and files of a source code in order to find which file to modify to correct a problem with a particular feature, or to add a feature?
|
||||
|
||||
There are two main architectures to solve this problem:
|
||||
|
||||
- The **Monorepo** architecture is a single directory containing several distinct projects with well-defined relationships, i.e. it allows the modification of the code in the client and server part simultaneously in the same place by verifying that a modification in the server part does not impact the client part.
|
||||
- The **Polyrepo** architecture are several directories, a directory corresponding to a project.
|
||||
|
||||
Both architectures track source code file history using version control systems such as [git](https://git-scm.com/).
|
||||
|
||||
**Thream**, uses the **Polyrepo** architecture, to make it easier to set up and allows complete independence between client and server code.
|
||||
|
||||
### Technologies
|
||||
|
||||
Now that we have discussed, on the architecture of the source code, we will discuss the choice of technologies. The chosen technologies must meet the need, and allow the developer to be productive to quickly have a result. Often there are several possible technologies to meet the same need, so it is a question of choosing the technology that you prefer and that you know best.
|
||||
|
||||
To ease the development, we chose for **Thream** to use the [TypeScript](https://www.typescriptlang.org/), an open source programming language made by Microsoft. It's a stricter syntactic superset of **JavaScript**, and adds optional static typing to the language, meaning we can assign a "type" (`string`, `number`, `boolean` etc.) to each data/variable in the code, which has the advantage of identifying program errors even before execution and thus greatly improves developer productivity.
|
||||
|
||||
The **TypeScript** code is then compiled into **JavaScript** language which is one of the basic technologies of the **World Wide Web**, alongside HTML and CSS (in the client part), this makes it possible to make the pages of websites **interactive** by executing code depending on a certain event, for example when clicking on a button, or when pressing a key on the keyboard.
|
||||
|
||||
Since the creation of **Node.js** in 2009, it is now also possible to execute **JavaScript** outside the browser, for example on a server. **Node.js is a runtime environment** that offers modules that handle various basic features for interacting with files/folders, networking (DNS, HTTP, TCP, TLS/SSL, or UDP), and other functions inaccessible from a browser and designed to reduce the complexity of developing server applications.
|
||||
|
||||
**TypeScript** allows you to code with the same programming language, the client part and the server part, with different needs.
|
||||
|
||||
### User Interface (frontend)
|
||||
|
||||
The needs of the graphical interface (the client part):
|
||||
|
||||
- be accessible through a web browser
|
||||
- allow the user to install the application
|
||||
- adapts to screen size
|
||||
- real-time data update (example: when a new message is sent)
|
||||
- save the authenticated user (in cookie)
|
||||
|
||||
In order to meet its needs, Thream is a **<abbr title="Progressive Web App">PWA</abbr>**, this consists of making a website appear to the user as a native application, which makes it possible to combine the functionalities of browsers with those of the experience offered by native applications, such as the possibility of installing the application. The main advantage is to be able to **code once** and to provide the **application on several platforms (iOS, Android, Windows, GNU/Linux etc.)** without the need to develop specifically according to each platform.
|
||||
|
||||
To design a **<abbr title="Progressive Web App">PWA</abbr>**, and allow updating the data on the graphical interface, we can use a framework, a development infrastructure to offer us a set of tools and software components. For **Thream**, we use [Next.js](https://nextjs.org/), a framework based on [React.js](https://reactjs.org/), which allows you to create interactive user interfaces in JavaScript, to **update the graphical interface when the data changes**.
|
||||
|
||||
### Server (backend)
|
||||
|
||||
By using the protocol, **<abbr title="Hypertext Transfer Protocol">HTTP</abbr>**, it is the client who sends a request to the server, but to allow the transfer of data in real time, the **<abbr title="Hypertext Transfer Protocol">HTTP</abbr>** protocol is no longer sufficient.
|
||||
|
||||
We use **WebSockets** so that it is the server that send a response to all connected clients without the client requesting to the server to get a response, the server sends responses according to events, for example when creating or deleting a message.
|
||||
|
||||
Thanks to [Fastify](https://www.fastify.io/), a fast and low overhead web framework, for Node.js and [Socket.io](https://socket.io/), a bidirectional and low-latency communication for every platform, we can easily make REST API and real time communication.
|
||||
|
||||
To store the data, we use [PostgreSQL](https://www.postgresql.org/) database, and [Prisma](https://www.prisma.io/), a <abbr title="Object-Relational Mapping">ORM</abbr> for Node.js, which allows us to easily interact with the database without the need of writing <abbr title="Structured Query Language">SQL</abbr> ourselves.
|
||||
|
||||
## Current and future state
|
||||
|
||||
The main interest of **Thream** is to be able to put into practice the computer knowledge acquired as an autodidact on a concrete project, in order to learn, and understand the problems and potential solutions to complex computer applications such as social networks.
|
||||
|
||||
Now that the first version of **Thream** has been released, there may not be any major evolution thereafter, the project will continue to be maintained to fix any bugs, and remain accessible, for as long as possible.
|
||||
|
||||
The other interest of the project is that it is completely **open-source**, and allows those who want to contribute to the development, and add new features.
|
||||
|
||||
**Thream** is **non-profit** and therefore has no financial goal, deadline or specific feature target, which makes the design of the project a hobby and a way to learn new concepts.
|
||||
|
||||
Feel free to give feebacks and suggestions to improve the project, and to report any bug you find.
|
@ -1,28 +1,30 @@
|
||||
import { Plugin, Transformer } from 'unified'
|
||||
import { Literal } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { Highlighter } from 'shiki'
|
||||
import type { Plugin, Transformer } from "unified"
|
||||
import type { Literal, Node } from "unist"
|
||||
import { visit } from "unist-util-visit"
|
||||
import type { Highlighter } from "shiki"
|
||||
|
||||
export interface RemarkSyntaxHighlightingPluginOptions {
|
||||
highlighter: Highlighter
|
||||
}
|
||||
|
||||
export type RemarkSyntaxHighlightingNode = Literal<string> & {
|
||||
export interface RemarkSyntaxHighlightingNode extends Node {
|
||||
lang: string
|
||||
meta: string
|
||||
children: undefined
|
||||
value: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const remarkSyntaxHighlightingPlugin: Plugin<
|
||||
[RemarkSyntaxHighlightingPluginOptions],
|
||||
Literal<string, RemarkSyntaxHighlightingNode>
|
||||
Literal
|
||||
> = (options) => {
|
||||
const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => {
|
||||
visit<RemarkSyntaxHighlightingNode, string>(tree, 'code', (node) => {
|
||||
node.type = 'html'
|
||||
visit<RemarkSyntaxHighlightingNode, string>(tree, "code", (node) => {
|
||||
node.type = "html"
|
||||
node.children = undefined
|
||||
node.value = options.highlighter.codeToHtml(node.value, {
|
||||
lang: node.lang
|
||||
lang: node.lang,
|
||||
})
|
||||
})
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface ErrorPageProps {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||
const { message, statusCode } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
{t('errors:error')}{' '}
|
||||
<span
|
||||
className='text-yellow dark:text-yellow-dark'
|
||||
data-cy='status-code'
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
</h1>
|
||||
<p className='text-center text-lg'>
|
||||
{message}{' '}
|
||||
<Link href='/'>
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
{t('errors:return-to-home-page')}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export interface FooterProps {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const Footer: React.FC<FooterProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
}, [version])
|
||||
|
||||
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'>
|
||||
<p>
|
||||
<Link href='/'>
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
Divlo
|
||||
</a>
|
||||
</Link>{' '}
|
||||
| {t('common:all-rights-reserved')}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
Version{' '}
|
||||
<a
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href={versionLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
19
components/Footer/FooterText.tsx
Normal file
19
components/Footer/FooterText.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export const FooterText = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p>
|
||||
<Link
|
||||
href="/"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
Théo LUDWIG
|
||||
</Link>{" "}
|
||||
| {i18n.translate("common.all-rights-reserved")}
|
||||
</p>
|
||||
)
|
||||
}
|
28
components/Footer/FooterVersion.tsx
Normal file
28
components/Footer/FooterVersion.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
interface FooterVersionProps {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
|
||||
}, [version])
|
||||
|
||||
return (
|
||||
<p className="mt-1">
|
||||
Version{" "}
|
||||
<a
|
||||
data-cy="version-link"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
href={versionLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
15
components/Footer/index.tsx
Normal file
15
components/Footer/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { getVersion } from "@/utils/getVersion"
|
||||
|
||||
import { FooterText } from "./FooterText"
|
||||
import { FooterVersion } from "./FooterVersion"
|
||||
|
||||
export const Footer = async (): Promise<JSX.Element> => {
|
||||
const version = await getVersion()
|
||||
|
||||
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">
|
||||
<FooterText />
|
||||
<FooterVersion version={version} />
|
||||
</footer>
|
||||
)
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import NextHead from 'next/head'
|
||||
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const Head: React.FC<HeadProps> = (props) => {
|
||||
const {
|
||||
title = 'Divlo',
|
||||
image = '/images/icons/icon-96x96.png',
|
||||
description,
|
||||
url = 'https://divlo.fr/'
|
||||
} = props
|
||||
|
||||
return (
|
||||
<NextHead>
|
||||
<title>{title}</title>
|
||||
<link rel='icon' type='image/png' href={image} />
|
||||
|
||||
{/* Meta Tag */}
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<meta name='description' content={description} />
|
||||
<meta name='Language' content='fr, en' />
|
||||
<meta name='theme-color' content='#ffd800' />
|
||||
|
||||
{/* Open Graph Metadata */}
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:image' content={image} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:locale' content='fr_FR, en_US' />
|
||||
<meta property='og:site_name' content={title} />
|
||||
|
||||
{/* Twitter card Metadata */}
|
||||
<meta name='twitter:card' content='summary' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<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>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export const Arrow: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
width='12'
|
||||
height='8'
|
||||
viewBox='0 0 12 8'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
className='fill-current text-black dark:text-white'
|
||||
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface LanguageFlagProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { language } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/languages/${language}.svg`}
|
||||
alt={language}
|
||||
/>
|
||||
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||
{language.toUpperCase()}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import i18n from 'i18n.json'
|
||||
|
||||
import { Arrow } from './Arrow'
|
||||
import { LanguageFlag } from './LanguageFlag'
|
||||
|
||||
export const Language: React.FC = () => {
|
||||
const { lang: currentLanguage } = useTranslation()
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
|
||||
const handleHiddenMenu = useCallback(() => {
|
||||
setHiddenMenu(!hiddenMenu)
|
||||
}, [hiddenMenu])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hiddenMenu) {
|
||||
window.document.addEventListener('click', handleHiddenMenu)
|
||||
} else {
|
||||
window.document.removeEventListener('click', handleHiddenMenu)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener('click', handleHiddenMenu)
|
||||
}
|
||||
}, [hiddenMenu, handleHiddenMenu])
|
||||
|
||||
const handleLanguage = async (language: string): Promise<void> => {
|
||||
await setLanguage(language)
|
||||
handleHiddenMenu()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||
<div
|
||||
data-cy='language-click'
|
||||
className='mr-5 flex items-center'
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LanguageFlag language={currentLanguage} />
|
||||
<Arrow />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
data-cy='languages-list'
|
||||
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',
|
||||
{ hidden: hiddenMenu }
|
||||
)}
|
||||
>
|
||||
{i18n.locales.map((language, index) => {
|
||||
if (language === currentLanguage) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||
onClick={async () => await handleLanguage(language)}
|
||||
>
|
||||
<LanguageFlag language={language} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
16
components/Header/Locales/Arrow.tsx
Normal file
16
components/Header/Locales/Arrow.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export const Arrow = (): JSX.Element => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="8"
|
||||
viewBox="0 0 12 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="fill-current text-black dark:text-white"
|
||||
d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
30
components/Header/Locales/LocaleFlag.tsx
Normal file
30
components/Header/Locales/LocaleFlag.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
import { useI18n } from "@/i18n/i18n.client"
|
||||
|
||||
export interface LocaleFlagProps {
|
||||
locale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => {
|
||||
const { locale, cookiesStore } = props
|
||||
|
||||
const i18n = useI18n(cookiesStore)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/locales/${locale}.svg`}
|
||||
alt={locale}
|
||||
/>
|
||||
<p data-cy="locale-flag-text" className="mx-2 text-base">
|
||||
{i18n.translate(`common.${locale}`)}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
100
components/Header/Locales/index.tsx
Normal file
100
components/Header/Locales/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useCallback, useEffect, useState, useRef } from "react"
|
||||
import classNames from "clsx"
|
||||
|
||||
import type { Locale as LocaleType, CookiesStore } from "@/utils/constants"
|
||||
import { LOCALES } from "@/utils/constants"
|
||||
|
||||
import { Arrow } from "./Arrow"
|
||||
import { LocaleFlag } from "./LocaleFlag"
|
||||
|
||||
export interface LocalesProps {
|
||||
currentLocale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const Locales = (props: LocalesProps): JSX.Element => {
|
||||
const { currentLocale, cookiesStore } = props
|
||||
const pathname = usePathname()
|
||||
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleHiddenMenu = useCallback(() => {
|
||||
setHiddenMenu((oldHiddenMenu) => {
|
||||
return !oldHiddenMenu
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickEvent = (event: MouseEvent): void => {
|
||||
if (languageClickRef.current == null || event.target == null) {
|
||||
return
|
||||
}
|
||||
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||
setHiddenMenu(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener("click", handleClickEvent)
|
||||
|
||||
return () => {
|
||||
return window.removeEventListener("click", handleClickEvent)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
||||
const { setLocale } = await import("@/i18n/i18n.server")
|
||||
setLocale(locale)
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/blog")) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer flex-col items-center justify-center">
|
||||
<div
|
||||
ref={languageClickRef}
|
||||
data-cy="locale-click"
|
||||
className="mr-5 flex items-center"
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LocaleFlag
|
||||
locale={currentLocale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
<Arrow />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
data-cy="locales-list"
|
||||
className={classNames(
|
||||
"absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
|
||||
{ hidden: hiddenMenu },
|
||||
)}
|
||||
>
|
||||
{LOCALES.filter((locale) => {
|
||||
return locale !== currentLocale
|
||||
}).map((locale) => {
|
||||
return (
|
||||
<li
|
||||
key={locale}
|
||||
className="flex h-12 w-full items-center justify-center hover:bg-[#4f545c]/20"
|
||||
onClick={async () => {
|
||||
return await handleLocale(locale)
|
||||
}}
|
||||
>
|
||||
<LocaleFlag
|
||||
locale={locale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,126 +1,79 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
"use client"
|
||||
|
||||
export const SwitchTheme: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme } = useTheme()
|
||||
import classNames from "clsx"
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
import { useTheme } from "@/theme/theme.client"
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
export interface SwitchThemeProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
const handleClick = (): void => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
const theme = useTheme(cookiesStore)
|
||||
|
||||
const handleClick = async (): Promise<void> => {
|
||||
const { setTheme } = await import("@/theme/theme.server")
|
||||
const newTheme = theme === "dark" ? "light" : "dark"
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='flex items-center'
|
||||
data-cy='switch-theme-click'
|
||||
className="flex items-center"
|
||||
data-cy="switch-theme-click"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
||||
<div className='toggle-track'>
|
||||
<div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
|
||||
<div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
|
||||
<div
|
||||
data-cy='switch-theme-dark'
|
||||
className='toggle-track-check absolute'
|
||||
data-cy="switch-theme-dark"
|
||||
className={classNames(
|
||||
"absolute inset-y-0 left-[8px] my-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 size-[10px] items-center justify-center">
|
||||
🌜
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
data-cy='switch-theme-light'
|
||||
className='toggle-track-x absolute'
|
||||
data-cy="switch-theme-light"
|
||||
className={classNames(
|
||||
"absolute inset-y-0 right-[10px] my-auto size-[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 size-[10px] items-center justify-center">
|
||||
🌞
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='toggle-thumb absolute' />
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute top-[1px] box-border size-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
|
||||
{
|
||||
"left-[27px]": theme === "dark",
|
||||
"left-0": theme === "light",
|
||||
},
|
||||
)}
|
||||
style={{ border: "1px solid #4d4d4d" }}
|
||||
/>
|
||||
<input
|
||||
data-cy='switch-theme-input'
|
||||
type='checkbox'
|
||||
aria-label='Dark mode toggle'
|
||||
className='toggle-screenreader-only absolute overflow-hidden'
|
||||
data-cy="switch-theme-input"
|
||||
type="checkbox"
|
||||
aria-label="Dark mode toggle"
|
||||
className="absolute m-[-1px] hidden size-[1px] overflow-hidden border-0 p-0"
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.toggle-theme-button {
|
||||
touch-action: pan-x;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle-track {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
background-color: #4d4d4d;
|
||||
transition: all 0.2s ease;
|
||||
color: #fff;
|
||||
}
|
||||
.toggle-track-check {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
left: 8px;
|
||||
opacity: ${theme === 'dark' ? 1 : 0};
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.toggle-track-x {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
right: 10px;
|
||||
opacity: ${theme === 'dark' ? 0 : 1};
|
||||
}
|
||||
.toggle_Dark,
|
||||
.toggle_Light {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
.toggle-thumb {
|
||||
left: ${theme === 'dark' ? '27px' : '0px'};
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #4d4d4d;
|
||||
border-radius: 50%;
|
||||
background-color: #fafafa;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s ease;
|
||||
top: 1px;
|
||||
color: #fff;
|
||||
}
|
||||
.toggle-screenreader-only {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
width: 1px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { Header } from '..'
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('should render', () => {
|
||||
const { getByText } = render(<Header />)
|
||||
expect(getByText('Divlo')).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,47 +1,51 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { cookies } from "next/headers"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Language } from './Language'
|
||||
import { SwitchTheme } from './SwitchTheme'
|
||||
import Logo from "@/public/images/logo.png"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export interface HeaderProps {
|
||||
showLanguage?: boolean
|
||||
}
|
||||
import { Locales } from "./Locales"
|
||||
import { SwitchTheme } from "./SwitchTheme"
|
||||
|
||||
export const Header: React.FC<HeaderProps> = (props) => {
|
||||
const { showLanguage = false } = props
|
||||
export const Header = (): JSX.Element => {
|
||||
const cookiesStore = cookies()
|
||||
const i18n = getI18n()
|
||||
|
||||
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'>
|
||||
<Link href='/'>
|
||||
<a>
|
||||
<div className='flex items-center justify-center'>
|
||||
<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">
|
||||
<h1>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-center transition-all duration-300 ease-in-out hover:scale-[1.03]"
|
||||
>
|
||||
<Image
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt='Divlo'
|
||||
className="size-16"
|
||||
src={Logo}
|
||||
alt="Théo LUDWIG"
|
||||
priority
|
||||
/>
|
||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||
Divlo
|
||||
<strong className="ml-1 hidden font-headline font-semibold text-primary dark:text-primary-dark sm:block sm:text-xl">
|
||||
Théo LUDWIG
|
||||
</strong>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex flex-col items-center justify-center px-6'>
|
||||
<Link href='/blog'>
|
||||
<a
|
||||
data-cy='header-blog-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
</h1>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col items-center justify-center px-6">
|
||||
<Link
|
||||
href="/blog"
|
||||
data-cy="header-blog-link"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{showLanguage && <Language />}
|
||||
<SwitchTheme />
|
||||
<Locales
|
||||
currentLocale={i18n.locale}
|
||||
cookiesStore={cookiesStore.toString()}
|
||||
/>
|
||||
<SwitchTheme cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
@ -1,22 +1,30 @@
|
||||
import htmlParser from 'html-react-parser'
|
||||
import htmlParser from "html-react-parser"
|
||||
import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGit } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
export const InterestsIcons = {
|
||||
code: faCode,
|
||||
"open-source": faGit,
|
||||
"high-tech": faMicrochip,
|
||||
} as const
|
||||
|
||||
export interface InterestParagraphProps {
|
||||
title: string
|
||||
description: string
|
||||
id: keyof typeof InterestsIcons
|
||||
}
|
||||
|
||||
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
export const InterestParagraph = (
|
||||
props: InterestParagraphProps,
|
||||
): JSX.Element => {
|
||||
const { title, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='my-6 text-center text-gray dark:text-gray-dark'>
|
||||
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
|
||||
<div className="my-6 text-center text-gray dark:text-gray-dark">
|
||||
<h3 className="text-lg font-semibold text-primary dark:text-primary-dark">
|
||||
{title}
|
||||
</strong>
|
||||
<br />
|
||||
<span>{htmlParser(description)}</span>
|
||||
</p>
|
||||
</>
|
||||
</h3>
|
||||
<p className="my-2">{htmlParser(description)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"
|
||||
|
||||
interface InterestItemProps {
|
||||
title: string
|
||||
fontAwesomeIcon: IconDefinition
|
||||
}
|
||||
|
||||
export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
||||
export const InterestItem = (props: InterestItemProps): JSX.Element => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
|
||||
<li className="m-2 size-8" title={title}>
|
||||
<FontAwesomeIcon
|
||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||
className="block size-full text-primary dark:text-primary-dark"
|
||||
icon={fontAwesomeIcon}
|
||||
/>
|
||||
</li>
|
||||
|
@ -1,21 +1,28 @@
|
||||
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGit } from '@fortawesome/free-brands-svg-icons'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
import { InterestItem } from './InterestItem'
|
||||
import {
|
||||
InterestsIcons,
|
||||
type InterestParagraphProps,
|
||||
} from "../InterestParagraph"
|
||||
import { InterestItem } from "./InterestItem"
|
||||
|
||||
export const InterestsList = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
let paragraphs = i18n.translate<InterestParagraphProps[]>(
|
||||
"home.interests.paragraphs",
|
||||
)
|
||||
if (!Array.isArray(paragraphs)) {
|
||||
paragraphs = []
|
||||
}
|
||||
|
||||
export const InterestsList: React.FC = () => {
|
||||
return (
|
||||
<div className='my-4 flex justify-center'>
|
||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||
<InterestItem
|
||||
title='Developer Full Stack Junior'
|
||||
fontAwesomeIcon={faCode}
|
||||
/>
|
||||
<InterestItem
|
||||
title='Passionate about High-Tech'
|
||||
fontAwesomeIcon={faMicrochip}
|
||||
/>
|
||||
<InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
|
||||
<div className="my-4 flex justify-center">
|
||||
<ul className="m-0 flex w-96 list-none justify-around p-0">
|
||||
{paragraphs.map(({ title, id }) => {
|
||||
const icon = InterestsIcons[id]
|
||||
return <InterestItem key={id} title={title} fontAwesomeIcon={icon} />
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,23 +1,23 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
||||
import { InterestsList } from './InterestsList'
|
||||
import type { InterestParagraphProps } from "./InterestParagraph"
|
||||
import { InterestParagraph } from "./InterestParagraph"
|
||||
import { InterestsList } from "./InterestsList"
|
||||
|
||||
export const Interests: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const Interests = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
const paragraphs: InterestParagraphProps[] = t(
|
||||
'home:interests.paragraphs',
|
||||
{},
|
||||
{
|
||||
returnObjects: true
|
||||
}
|
||||
let paragraphs = i18n.translate<InterestParagraphProps[]>(
|
||||
"home.interests.paragraphs",
|
||||
)
|
||||
if (!Array.isArray(paragraphs)) {
|
||||
paragraphs = []
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='max-w-full'>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
return <InterestParagraph key={index} {...paragraph} />
|
||||
<div className="max-w-full">
|
||||
{paragraphs.map((paragraph) => {
|
||||
return <InterestParagraph key={paragraph.id} {...paragraph} />
|
||||
})}
|
||||
<InterestsList />
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { GitHubIcon } from 'components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
import { GitHubIcon } from "@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon"
|
||||
|
||||
export interface RepositoryProps {
|
||||
name: string
|
||||
@ -7,18 +7,22 @@ export interface RepositoryProps {
|
||||
href: string
|
||||
}
|
||||
|
||||
export const Repository: React.FC<RepositoryProps> = (props) => {
|
||||
export const Repository = (props: RepositoryProps): JSX.Element => {
|
||||
const { name, description, href } = props
|
||||
|
||||
return (
|
||||
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||
<div className='flex'>
|
||||
<GitHubIcon className='mr-2 h-6' />
|
||||
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
||||
</div>
|
||||
<p className='my-4'>{description}</p>
|
||||
</a>
|
||||
<li>
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
<ShadowContainer className="relative !mb-4 max-h-32 cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.03]">
|
||||
<h3 className="flex">
|
||||
<GitHubIcon className="mr-2 h-6" />
|
||||
<span className="font-semibold text-primary dark:text-primary-dark">
|
||||
{name}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="my-4">{description}</p>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -1,35 +1,37 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
import { Repository } from './Repository'
|
||||
import { Repository } from "./Repository"
|
||||
|
||||
export const OpenSource: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const OpenSource = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<div className='mt-0 flex max-w-full flex-col items-center'>
|
||||
<p className='text-center'>{t('home:open-source.description')}</p>
|
||||
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
||||
<div className="mt-0 flex max-w-full flex-col items-center">
|
||||
<p className="text-center">
|
||||
{i18n.translate("home.open-source.description")}
|
||||
</p>
|
||||
<ul className="my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2">
|
||||
<Repository
|
||||
name='nodejs/node'
|
||||
description='Node.js JavaScript runtime 🐢🚀'
|
||||
href='https://github.com/nodejs/node/commits?author=Divlo'
|
||||
name="nodejs/node"
|
||||
description="Node.js JavaScript runtime ✨🐢🚀✨"
|
||||
href="https://github.com/nodejs/node/commits?author=theoludwig"
|
||||
/>
|
||||
<Repository
|
||||
name='standard/standard'
|
||||
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
||||
href='https://github.com/standard/standard/commits?author=Divlo'
|
||||
name="standard/standard"
|
||||
description="🌟 JavaScript Style Guide, with linter & automatic code fixer"
|
||||
href="https://github.com/standard/standard/commits?author=theoludwig"
|
||||
/>
|
||||
<Repository
|
||||
name='nrwl/nx'
|
||||
description='Smart, Extensible Build Framework'
|
||||
href='https://github.com/nrwl/nx/commits?author=Divlo'
|
||||
name="DefinitelyTyped/DefinitelyTyped"
|
||||
description="High quality TypeScript type definitions."
|
||||
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits?author=theoludwig"
|
||||
/>
|
||||
<Repository
|
||||
name='vercel/next.js'
|
||||
description='The React Framework for Production'
|
||||
href='https://github.com/vercel/next.js/commits?author=Divlo'
|
||||
name="vercel/next.js"
|
||||
description="The React Framework"
|
||||
href="https://github.com/vercel/next.js/commits?author=theoludwig"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import Image from "next/image"
|
||||
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
@ -9,35 +9,37 @@ export interface PortfolioItemProps {
|
||||
image: string
|
||||
}
|
||||
|
||||
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'>
|
||||
<li>
|
||||
<a
|
||||
className='group inline-flex justify-center'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="group inline-flex justify-center"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={link}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className='flex justify-center'>
|
||||
<ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
quality={100}
|
||||
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||
className="size-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
|
||||
width={300}
|
||||
height={300}
|
||||
src={image}
|
||||
alt={title}
|
||||
/>
|
||||
</div>
|
||||
<div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'>
|
||||
<h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||
<div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100">
|
||||
<h3 className="my-6 text-2xl font-semibold text-primary dark:text-primary-dark">
|
||||
{title}
|
||||
</h3>
|
||||
<p className='my-6'>{description}</p>
|
||||
<p className="mx-4 my-6 font-semibold">{description}</p>
|
||||
</div>
|
||||
</a>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,21 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
||||
import type { PortfolioItemProps } from "./PortfolioItem"
|
||||
import { PortfolioItem } from "./PortfolioItem"
|
||||
|
||||
export const Portfolio: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
export const Portfolio = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
const items: PortfolioItemProps[] = t(
|
||||
'home:portfolio.items',
|
||||
{},
|
||||
{
|
||||
returnObjects: true
|
||||
let items = i18n.translate<PortfolioItemProps[]>("home.portfolio.items")
|
||||
if (!Array.isArray(items)) {
|
||||
items = []
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex w-full flex-wrap justify-center px-3'>
|
||||
{items.map((item, index) => {
|
||||
return <PortfolioItem key={index} {...item} />
|
||||
<ul className="flex w-full flex-wrap justify-center px-3">
|
||||
{items.map((item) => {
|
||||
return <PortfolioItem key={item.title} {...item} />
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,21 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import htmlParser from "html-react-parser"
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t, lang } = useTranslation()
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export const ProfileDescriptionBottom = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||
{t('home:about.description-bottom')}
|
||||
{lang === 'fr' && (
|
||||
<>
|
||||
<br />
|
||||
<div className="my-6 max-w-md text-center text-base text-gray dark:text-gray-dark">
|
||||
<p>{htmlParser(i18n.translate("home.about.description-bottom"))}</p>
|
||||
|
||||
<br />
|
||||
<a
|
||||
href='/curriculum-vitae.html'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href="/curriculum-vitae/index.html"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
Curriculum vitæ
|
||||
Curriculum vitæ ({i18n.translate("common.fr-FR")})
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export const ProfileInformation: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const ProfileInformation = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
||||
<h1 className='mb-2 text-4xl'>
|
||||
{t('home:about.i-am')}{' '}
|
||||
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
|
||||
Divlo
|
||||
</strong>
|
||||
<div className="mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400">
|
||||
<h1 className="mb-2 text-4xl font-semibold text-primary dark:text-primary-dark">
|
||||
Théo LUDWIG
|
||||
</h1>
|
||||
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||
<h2 className="mb-3 text-base">
|
||||
{i18n.translate("home.about.description")}
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -4,18 +4,18 @@ interface ProfileItemProps {
|
||||
link?: string
|
||||
}
|
||||
|
||||
export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
<li className='mb-3 before:table after:clear-both after:table'>
|
||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||
<li className="mb-3 before:table after:clear-both after:table">
|
||||
<strong className="float-left block w-28 text-sm font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</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 ? (
|
||||
<a
|
||||
className='text-gray hover:underline dark:text-gray-dark'
|
||||
className="text-gray hover:underline dark:text-gray-dark"
|
||||
href={link}
|
||||
>
|
||||
{value}
|
||||
|
@ -1,19 +1,54 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
"use client"
|
||||
|
||||
import { ProfileItem } from './ProfileItem'
|
||||
import { useMemo } from "react"
|
||||
|
||||
export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
import { useI18n } from "@/i18n/i18n.client"
|
||||
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from "@/utils/getAge"
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
import { useIsMounted } from "@/hooks/useIsMounted"
|
||||
|
||||
import { ProfileItem } from "./ProfileItem"
|
||||
|
||||
export interface ProfileListProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const ProfileList = (props: ProfileListProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
|
||||
const i18n = useI18n(cookiesStore)
|
||||
|
||||
const age = useMemo(() => {
|
||||
return getAge(BIRTH_DATE)
|
||||
}, [])
|
||||
|
||||
const { isMounted } = useIsMounted()
|
||||
|
||||
return (
|
||||
<ul className='m-0 list-none p-0'>
|
||||
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
||||
<ProfileItem title={t('home:about.birth-date')} value='31/03/2003' />
|
||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||
<ul className="m-0 list-none p-0">
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
value='contact@divlo.fr'
|
||||
link='mailto:contact@divlo.fr'
|
||||
title={i18n.translate("home.about.pronouns")}
|
||||
value={i18n.translate("home.about.pronouns-value")}
|
||||
/>
|
||||
<ProfileItem
|
||||
title={i18n.translate("home.about.birth-date")}
|
||||
value={
|
||||
isMounted
|
||||
? `${BIRTH_DATE_STRING} (${age} ${i18n.translate(
|
||||
"home.about.years-old",
|
||||
)})`
|
||||
: BIRTH_DATE_STRING
|
||||
}
|
||||
/>
|
||||
|
||||
<ProfileItem
|
||||
title={i18n.translate("home.about.nationality")}
|
||||
value="Alsace, France"
|
||||
/>
|
||||
<ProfileItem
|
||||
title="Email"
|
||||
value="contact@theoludwig.fr"
|
||||
link="mailto:contact@theoludwig.fr"
|
||||
/>
|
||||
</ul>
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Image from 'next/image'
|
||||
import Image from "next/image"
|
||||
|
||||
import DivloLogo from 'public/images/divlo_logo.png'
|
||||
import Logo from "@/public/images/logo.png"
|
||||
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
export const ProfileLogo = (): JSX.Element => {
|
||||
return (
|
||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
||||
<div className="max-h-[370px] max-w-[370px] px-2 py-6">
|
||||
<Image quality={100} src={Logo} alt="Théo LUDWIG" priority />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const EmailIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Email</title>
|
||||
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' />
|
||||
<path d="M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitHubIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitHub</title>
|
||||
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitLabIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitLab</title>
|
||||
<path d='M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z' />
|
||||
<path d="M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import classNames from 'classnames'
|
||||
import classNames from "clsx"
|
||||
|
||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className={classNames(
|
||||
'h-8 w-8 fill-current text-black dark:text-white',
|
||||
className
|
||||
"size-8 fill-current text-black dark:text-white",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>npm</title>
|
||||
<path d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z' />
|
||||
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitchIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitch</title>
|
||||
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' />
|
||||
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitterIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitter</title>
|
||||
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' />
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const YouTubeIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>YouTube</title>
|
||||
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' />
|
||||
<path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
interface SocialMediaItemProps {
|
||||
interface SocialMediaItemProps extends React.PropsWithChildren {
|
||||
link: string
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
||||
export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => {
|
||||
const { link, ariaLabel, children } = props
|
||||
|
||||
return (
|
||||
<li className='mx-4 my-1 inline-block'>
|
||||
<li className="mx-4 my-1 inline-block">
|
||||
<a
|
||||
href={link}
|
||||
aria-label={ariaLabel}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='relative inline-block bg-transparent'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative inline-block bg-transparent transition-all duration-300 ease-in-out hover:scale-110"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
@ -1,37 +1,43 @@
|
||||
import { SocialMediaItem } from './SocialMediaItem'
|
||||
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon'
|
||||
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon'
|
||||
import { GitLabIcon } from './SocialMediaIcons/GitLabIcon'
|
||||
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon'
|
||||
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
|
||||
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
|
||||
import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
||||
import { SocialMediaItem } from "./SocialMediaItem"
|
||||
import { TwitterIcon } from "./SocialMediaIcons/TwitterIcon"
|
||||
import { GitHubIcon } from "./SocialMediaIcons/GitHubIcon"
|
||||
import { GitLabIcon } from "./SocialMediaIcons/GitLabIcon"
|
||||
import { YouTubeIcon } from "./SocialMediaIcons/YouTubeIcon"
|
||||
import { TwitchIcon } from "./SocialMediaIcons/TwitchIcon"
|
||||
import { EmailIcon } from "./SocialMediaIcons/EmailIcon"
|
||||
import { NPMIcon } from "./SocialMediaIcons/NPMIcon"
|
||||
|
||||
export const SocialMediaList: React.FC = () => {
|
||||
export const SocialMediaList = (): JSX.Element => {
|
||||
return (
|
||||
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
||||
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
|
||||
<ul className="m-0 mt-2 list-none py-4 text-center">
|
||||
<SocialMediaItem link="https://github.com/theoludwig" ariaLabel="GitHub">
|
||||
<GitHubIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://gitlab.com/Divlo' ariaLabel='GitLab'>
|
||||
<SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab">
|
||||
<GitLabIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://www.npmjs.com/~divlo' ariaLabel='NPM'>
|
||||
<SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm">
|
||||
<NPMIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
|
||||
<SocialMediaItem
|
||||
link="https://twitter.com/theoludwig_"
|
||||
ariaLabel="Twitter"
|
||||
>
|
||||
<TwitterIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem
|
||||
link='https://www.youtube.com/c/Divlo'
|
||||
ariaLabel='YouTube'
|
||||
link="https://www.youtube.com/@theo_ludwig"
|
||||
ariaLabel="YouTube"
|
||||
>
|
||||
<YouTubeIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
|
||||
<SocialMediaItem
|
||||
link="https://www.twitch.tv/theoludwig"
|
||||
ariaLabel="Twitch"
|
||||
>
|
||||
<TwitchIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
|
||||
<SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email">
|
||||
<EmailIcon />
|
||||
</SocialMediaItem>
|
||||
</ul>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user