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

Compare commits

...

101 Commits

Author SHA1 Message Date
9f79b88202 chore(release): 3.2.4 [skip ci] 2024-04-13 17:17:11 +00:00
23d9caf578 style: fix eslint 2024-04-13 19:13:48 +02:00
7febe6d1f9 fix(blog): typos in posts 2024-04-13 19:03:18 +02:00
c4650c34d9 build(deps): update latest 2024-04-13 18:54:36 +02:00
0eb780485c fix(footer): show 0.0.0-development version in Footer in development 2024-04-06 20:40:25 +02:00
cd5e92b64a fix: hydratation error with age calculation 2024-04-06 20:32:09 +02:00
982b148329 Revert "fix(portfolio): update link to Carolo (carolo.org)"
This reverts commit c2c9b59c7a.
2024-04-06 20:27:04 +02:00
0febee5b51 refactor: rename to primary color 2024-04-06 20:25:02 +02:00
3502f51735 chore(release): 3.2.3 [skip ci] 2024-02-15 08:41:01 +00:00
493df4e2f2 style: fix prettier 2024-02-15 09:35:58 +01:00
c2c9b59c7a fix(portfolio): update link to Carolo (carolo.org) 2024-02-15 09:34:02 +01:00
f6e3008ab9 fix(blog): add command to commit in the past in Git Ultimate Guide 2024-02-15 09:30:34 +01:00
15e94cec64 fix: update dependencies to latest to address security issues Node.js v20.11.1
Ref: https://nodejs.org/en/blog/vulnerability/february-2024-security-releases
2024-02-15 09:27:03 +01:00
5185c6758b chore(release): 3.2.2 [skip ci] 2024-02-02 16:31:35 +00:00
b633eef833 fix: remove npm vulnerability by updating html-w3c-validator 2024-02-02 17:30:25 +01:00
d2e627ff13 chore: cleaner configs 2024-01-29 21:26:59 +01:00
1e0567b538 chore(release): 3.2.1 [skip ci] 2024-01-28 15:15:48 +00:00
c8d32c6acc test: correct selector for Main Title of page 2024-01-28 16:13:02 +01:00
05503cda26 chore: only report errors for html validation 2024-01-28 16:04:00 +01:00
303b6f3011 fix: correct responsive for Header Title 2024-01-28 15:45:45 +01:00
0272cf7080 docs(license): add email address 2024-01-28 15:42:23 +01:00
e8ea42a260 fix: wrong font weight on hover link 2024-01-28 15:42:01 +01:00
f337e14260 chore(release): 3.2.0 [skip ci] 2024-01-28 12:16:11 +00:00
f5020cad19 chore: usage of eslint-plugin-tailwindcss 2024-01-28 03:21:11 +01:00
b8ceefb2f6 feat: new logo v1 2024-01-28 01:56:47 +01:00
1523c8cac0 fix: wording typos 2024-01-25 14:46:03 +01:00
548ddc8425 style: format JSONC files with Prettier correctly 2024-01-24 21:49:30 +01:00
bac65ad61a fix: improve wording 2024-01-23 23:59:10 +01:00
b91f3165b7 fix(blog): add depreciation notice on Thream post 2024-01-23 22:29:21 +01:00
5478e202a7 fix(open source): replace nrwl/nx by DefinitelyTyped 2024-01-23 22:13:39 +01:00
a89b5932c2 fix: update dependencies to latest 2024-01-23 22:01:50 +01:00
339e42acfa chore(release): 3.1.2 [skip ci] 2023-12-28 05:24:14 +00:00
c123815a86 fix(portfolio): remove Thream project as it is now deprecated 2023-12-28 06:21:32 +01:00
dd26a277a2 fix: update dependencies to latest 2023-12-28 06:21:24 +01:00
62222dbb0c chore(release): 3.1.1 [skip ci] 2023-11-07 20:34:11 +00:00
ee0a02bc8b chore: downgrade commitlint temporarily to release new versions 2023-11-07 21:33:05 +01:00
2e04053ec3 fix: update CV with latest education courses 2023-11-07 21:14:43 +01:00
45a9a69122 fix: update dependencies to latest 2023-11-07 20:57:52 +01:00
e566ef6c38 chore: better Prettier config for easier reviews 2023-10-23 23:11:59 +02:00
c7ad15a465 chore(release): 3.1.0 [skip ci] 2023-09-18 20:09:29 +00:00
f4a842efb5 build: downgrade semantic-release to v21.1.2
Ref: https://github.com/semantic-release/semantic-release/issues/2968
2023-09-18 22:08:04 +02:00
424c97019b fix: update dependencies to latest 2023-09-18 22:00:11 +02:00
c0508dc0b9 build: ignore ESLint errors for Production build
It improves performance when extra checking is not necessary.
2023-09-14 12:26:19 +02:00
f04d8a0c11 Revert "build: ignore ESLint and TypeScript errors for Production build"
This reverts commit fdab2a7ea8.
2023-09-14 12:19:26 +02:00
d29064745c feat: add IRCAD as work experience 2023-09-14 12:11:18 +02:00
95febe2a99 feat: add 3rd year of BUT Informatique as education 2023-09-14 12:01:20 +02:00
fdab2a7ea8 build: ignore ESLint and TypeScript errors for Production build
It improves performance when extra checking is not necessary.
2023-09-14 11:49:52 +02:00
35211fa279 fix: try/catch inside middleware when checking locale 2023-08-24 23:09:33 +02:00
137cceffa1 build(deps): update latest 2023-08-24 22:38:09 +02:00
f6bfc466de chore(release): 3.0.0 [skip ci] 2023-08-01 17:41:12 +00:00
e4cf714d95 test: fix styles import for unit tests 2023-08-01 19:39:09 +02:00
d3c86b2a26 chore: update Dockerfile 2023-08-01 19:34:58 +02:00
d2578abeec fix: loader improvements 2023-08-01 18:59:45 +02:00
e51e3bdc19 test: fix e2e tests + 500 error page 2023-08-01 18:18:16 +02:00
56520830e9 refactor: blog directory 2023-08-01 17:44:08 +02:00
2e0138194c refactor: avoid usage of React.FC to use JSX.Element (to stay consistent) 2023-08-01 17:22:09 +02:00
4b2e7bae90 feat: rewrite blog to Next.js v13 app directory
Improvement: Support light theme in code block
2023-08-01 17:07:19 +02:00
caa6a90418 refactor: implement light/dark themes using cookies 2023-08-01 14:11:46 +02:00
e82db952db docs: update interests 2023-08-01 13:15:03 +02:00
6b29ce9b15 feat: rewrite to Next.js v13 app directory
Improvements:
- Hide switch theme input (ugly little white square)
- i18n without subpath (e.g: /fr or /en), same url whatever the locale used
2023-07-31 19:06:46 +02:00
5640f1b434 build(deps): bump Node.js to 20.0.0 and npm to 9.0.0
BREAKING CHANGE: minimum supported Node.js >= 20.0.0 and npm >= 9.0.0
2023-07-30 19:03:36 +02:00
6d0dcb50a7 refactor: 'use client' when appropriate 2023-07-30 18:50:14 +02:00
70603f1444 chore: remove build error with Docker copy wrong node_modules 2023-07-30 18:27:15 +02:00
f42fdbfd0c chore: rename jsonresume-theme-custom to curriculum-vitae 2023-07-28 11:53:04 +02:00
6a3f335f9f fix(posts): update git blog post 2023-07-28 11:40:19 +02:00
f1509d0af1 chore: rename docker-compose.yml to compose.yaml
Ref: https://docs.docker.com/compose/compose-file/03-compose-file/
2023-07-28 11:38:34 +02:00
49599d25ed chore(release): 2.13.0 [skip ci] 2023-07-22 17:47:02 +00:00
65e0f4f8b6 fix: avoid scrolling when changing language 2023-07-22 19:40:28 +02:00
8d60c2d53a feat: add Carolo project in Portfolio 2023-07-22 19:39:57 +02:00
0bbebeab99 build(deps): update latest
Some checks failed
Analyze / analyze (javascript) (push) Failing after 1m24s
Build / build (push) Successful in 3m0s
Lint / lint (push) Successful in 2m6s
Test / test-unit (push) Successful in 1m56s
Test / test-e2e (push) Successful in 3m17s
2023-07-19 00:09:28 +02:00
643e0e5821 chore(release): 2.12.1 [skip ci] 2023-07-14 22:03:03 +00:00
872b018673 style: fix prettier 2023-07-14 23:58:50 +02:00
2644cb0fb5 fix: update /curriculum-vitae to /curriculum-vitae/index.html 2023-07-14 23:54:29 +02:00
bc719578d2 fix: remove vercel cli + update dependencies to latest 2023-07-14 23:50:20 +02:00
117c41b1c3 chore(release): 2.12.0 [skip ci] 2023-07-02 14:59:12 +00:00
b92704b77d feat: increase duration work experience Numerize 2023-07-02 16:50:32 +02:00
bab7581283 fix: update dependencies to latest 2023-07-02 16:42:39 +02:00
988fceb2aa chore(release): 2.11.0 [skip ci] 2023-06-18 10:23:59 +00:00
5211ba1489 feat(skills): add Arch Linux 2023-06-18 12:18:24 +02:00
6886480cef chore(release): 2.10.0 [skip ci] 2023-06-16 21:30:51 +00:00
d78e50638e test: fix with new anchor link behavior in blog posts 2023-06-16 23:26:09 +02:00
3b76195d71 feat(blog): add anchor links for titles/headers 2023-06-16 23:14:25 +02:00
2dc63ba933 chore: maintenance 2023-06-16 22:56:53 +02:00
336f067c52 fix(posts): add explanations for Git (cherry-pick + merge squash) 2023-06-16 22:36:20 +02:00
5fd7f77b6d fix: justify align text in blog posts 2023-06-16 21:48:47 +02:00
db0c708c04 chore(release): 2.9.0 [skip ci] 2023-05-31 18:39:59 +00:00
9d44671fed feat: continue migrating to full name instead of nickname 2023-05-31 20:09:08 +02:00
7bcc5f972c chore(release): 2.8.0 [skip ci] 2023-05-30 19:57:02 +00:00
61172d59e3 feat: migrate progressively to full name instead of nickname 2023-05-30 21:51:27 +02:00
7c0f11ab7d chore(release): 2.7.3 [skip ci] 2023-05-29 15:45:42 +00:00
670897fa78 fix: improve spelling consistency 2023-05-29 17:44:26 +02:00
b88246b668 chore: usage of next start 2023-05-29 17:33:45 +02:00
87fbfe4940 chore(release): 2.7.2 [skip ci] 2023-05-29 15:29:07 +00:00
271aa60247 test: update with new changes 2023-05-29 17:26:06 +02:00
ba34e314c9 fix: update name with full name and nickname 2023-05-29 17:10:14 +02:00
f41bc644b1 fix(deps): remove next-pwa dependency 2023-05-29 16:24:49 +02:00
a18cec4826 chore(release): 2.7.1 [skip ci] 2023-05-21 16:27:24 +00:00
61e589f0f4 fix: responsive on blog post with code blocks and katex 2023-05-21 18:21:46 +02:00
dc5c3cee41 chore(release): 2.7.0 [skip ci] 2023-05-21 12:49:17 +00:00
20cb0c21d5 feat(posts): add programming-challenges 2023-05-21 14:42:53 +02:00
e5232c1394 build(deps): update latest 2023-05-21 12:15:08 +02:00
192 changed files with 11165 additions and 15605 deletions

View File

@ -1 +1 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:18
FROM mcr.microsoft.com/devcontainers/javascript-node:20

View File

@ -0,0 +1,9 @@
services:
workspace:
build:
context: "./"
dockerfile: "./Dockerfile"
volumes:
- "..:/workspace:cached"
command: "sleep infinity"
network_mode: "host"

View File

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

View File

@ -1,8 +0,0 @@
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'

View File

@ -1,5 +1,21 @@
.*
!.npmrc
build
coverage
node_modules
**/.turbo
**/.next
**/out
**/build
**/coverage
**/node_modules
# envs
.env
.env.production
.env.development
secrets
# misc
.DS_Store
*.pem
Dockerfile
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,4 +1,4 @@
# For more information see: https://editorconfig.org/
# https://editorconfig.org/
root = true

View File

@ -1,2 +1,4 @@
COMPOSE_PROJECT_NAME=divlo
COMPOSE_PROJECT_NAME=theoludwig
HOSTNAME=0.0.0.0
PORT=3000
NEXT_TELEMETRY_DISABLED=1

View File

@ -1,15 +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
"settings": {
"tailwindcss": {
"callees": ["classNames"]
},
"react": {
"version": "detect"
}
},
"rules": {
"prettier/prettier": "error",
"@next/next/no-img-element": "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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
name: 'Build'
name: "Build"
on:
push:
@ -8,18 +8,18 @@ on:
jobs:
build:
runs-on: 'ubuntu-latest'
runs-on: "ubuntu-latest"
steps:
- uses: 'actions/checkout@v3.5.2'
- uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: '18.x'
cache: 'npm'
node-version: "20.x"
cache: "npm"
- name: 'Install dependencies'
run: 'npm clean-install'
- name: "Install dependencies"
run: "npm clean-install"
- name: 'Build'
run: 'npm run build'
- name: "Build"
run: "npm run build"

View File

@ -1,4 +1,4 @@
name: 'Lint'
name: "Lint"
on:
push:
@ -8,35 +8,35 @@ on:
jobs:
lint:
runs-on: 'ubuntu-latest'
runs-on: "ubuntu-latest"
steps:
- uses: 'actions/checkout@v3.5.2'
- uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: '18.x'
cache: 'npm'
node-version: "20.x"
cache: "npm"
- name: 'Install dependencies'
run: 'npm clean-install'
- name: "Install dependencies"
run: "npm clean-install"
- name: 'lint:commit'
- name: "lint:commit"
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: 'lint:editorconfig'
run: 'npm run lint:editorconfig'
- name: "lint:editorconfig"
run: "npm run lint:editorconfig"
- name: 'lint:markdown'
run: 'npm run lint:markdown'
- name: "lint:markdown"
run: "npm run lint:markdown"
- name: 'lint:eslint'
run: 'npm run lint:eslint'
- name: "lint:eslint"
run: "npm run lint:eslint"
- name: 'lint:prettier'
run: 'npm run lint:prettier'
- name: "lint:prettier"
run: "npm run lint:prettier"
- name: 'lint:dotenv'
uses: 'dotenv-linter/action-dotenv-linter@v2'
- name: "lint:dotenv"
uses: "dotenv-linter/action-dotenv-linter@v2.18.0"
with:
github_token: ${{ secrets.github_token }}

View File

@ -1,4 +1,4 @@
name: 'Release'
name: "Release"
on:
push:
@ -6,39 +6,32 @@ on:
jobs:
release:
runs-on: 'ubuntu-latest'
runs-on: "ubuntu-latest"
steps:
- uses: 'actions/checkout@v3.5.2'
- uses: "actions/checkout@v4.1.1"
with:
fetch-depth: 0
persist-credentials: false
- name: 'Import GPG key'
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
- 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@v3.6.0'
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: '18.x'
cache: 'npm'
node-version: "20.x"
cache: "npm"
- name: 'Install dependencies'
run: 'npm clean-install'
- name: "Install dependencies"
run: "npm clean-install"
- name: 'Release'
run: 'npm run release'
- 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 }}

View File

@ -1,4 +1,4 @@
name: 'Test'
name: "Test"
on:
push:
@ -8,41 +8,41 @@ on:
jobs:
test-unit:
runs-on: 'ubuntu-latest'
runs-on: "ubuntu-latest"
steps:
- uses: 'actions/checkout@v3.5.2'
- uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: '18.x'
cache: 'npm'
node-version: "20.x"
cache: "npm"
- name: 'Install dependencies'
run: 'npm clean-install'
- name: "Install dependencies"
run: "npm clean-install"
- name: 'Unit Test'
run: 'npm run test:unit'
- name: "Unit Test"
run: "npm run test:unit"
test-e2e:
runs-on: 'ubuntu-latest'
runs-on: "ubuntu-latest"
steps:
- uses: 'actions/checkout@v3.5.0'
- uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: '18.x'
cache: 'npm'
node-version: "20.x"
cache: "npm"
- name: 'Install dependencies'
run: 'npm clean-install'
- name: "Install dependencies"
run: "npm clean-install"
- name: 'Build'
run: 'npm run build'
- name: "Build"
run: "npm run build"
- name: 'html-w3c-validator'
run: 'npm run test:html-w3c-validator'
- name: "html-w3c-validator"
run: "npm run test:html-w3c-validator"
- name: 'End To End (e2e) Test'
run: 'npm run test:e2e'
- name: "End To End (e2e) Test"
run: "npm run test:e2e"

4
.gitignore vendored
View File

@ -12,9 +12,6 @@ out
build
dist
public/curriculum-vitae
# PWA
public/workbox-*.js
public/sw.js
# testing
coverage
@ -48,7 +45,6 @@ npm-debug.log*
# misc
.DS_Store
.lighthouseci
.vercel
# typescript
*.tsbuildinfo

View File

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

View File

@ -1,8 +1,6 @@
{
"urls": [
"http://127.0.0.1:3000/",
"http://127.0.0.1:3000/blog",
"http://127.0.0.1:3000/blog/hello-world"
],
"files": ["./public/curriculum-vitae/index.html"]
"$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"]
}

View File

@ -1,4 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#!/usr/bin/env sh
npm run lint:commit -- --edit

View File

@ -1,4 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#!/usr/bin/env sh
npm run lint:staged

View File

@ -1,6 +1,7 @@
{
"*": ["editorconfig-checker"],
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
"**/*": ["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"
]
}

View File

@ -1,12 +1,12 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"default": true,
"relative-links": true,
"extends": "markdownlint/style/prettier",
"MD024": false,
"MD033": false
"no-duplicate-heading": false,
"no-inline-html": false,
},
"globs": ["**/*.{md,mdx}"],
"globs": ["**/*.md"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
"customRules": ["markdownlint-rule-relative-links"],
}

View File

@ -1,6 +1,4 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -5,7 +5,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"eslint.options": {
"ignorePath": ".gitignore"

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
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

View File

@ -1,10 +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
**divlo.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
**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
@ -15,13 +15,13 @@ 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
@ -29,27 +29,27 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
## Getting Started
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Divlo/Divlo)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
### Prerequisites
- [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
@ -68,4 +68,4 @@ docker compose up --build
### Services started
- website: `http://127.0.0.1:3000`
- `website`: <http://127.0.0.1:3000>

View File

@ -1,21 +1,28 @@
FROM node:18.16.0 AS builder-dependencies
FROM node:20.12.2 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm clean-install
FROM node:18.16.0 AS builder
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 ./ ./
RUN npm run build
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
WORKDIR /usr/src/application
FROM node:20.12.2-slim AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /usr/src/application/.next/standalone ./
COPY --from=builder /usr/src/application/.next/static ./.next/static
COPY --from=builder /usr/src/application/public ./public
COPY --from=builder /usr/src/application/locales ./locales
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
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"]

View File

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

View File

@ -1,18 +1,18 @@
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center">
<strong>Developer Full Stack • Open-Source enthusiast</strong>
<strong>Developer Full Stack • Open-Source Enthusiast</strong>
</p>
<p align="center">
<a href="https://github.com/Divlo"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://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 />
@ -21,16 +21,16 @@
```json
{
"name": "Divlo",
"name": "Théo LUDWIG",
"pronouns": "He/Him",
"birthDate": "31/03/2003",
"nationality": "Alsace, France",
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": {
"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", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
"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"]
}
}
```
@ -40,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>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

78
app/globals.css Normal file
View 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;
}

77
app/layout.tsx Normal file
View File

@ -0,0 +1,77 @@
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",
})}
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
View 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
View 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
View 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
View 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
View 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
View 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>
)
}

45
blog/BlogPosts.tsx Normal file
View File

@ -0,0 +1,45 @@
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">
<div className="w-[1600px]" data-cy="blog-posts">
{posts.map((post, index) => {
const postPublishedOn = date.format(
new Date(post.frontmatter.publishedOn),
"DD/MM/YYYY",
)
return (
<Link
href={`/blog/${post.slug}`}
key={index}
locale="en"
data-cy={post.slug}
>
<ShadowContainer className="cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2">
<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>
)
})}
</div>
</div>
)
}

65
blog/blog.ts Normal file
View 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
},
)

View File

@ -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! 👋
@ -19,19 +19,19 @@ A clean code is a code that is **easy** to **read** and easy to **understand**.
But I promise it is not a code that is easy to write, in fact it is really **hard to write Clean Code**.
We could ask ourselves, what is **easy** to **read** and easy to **understand** ?
We could ask ourselves, what is **easy** to **read** and easy to **understand**?
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
## 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.
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
## Definition : Design Patterns
## Definition: Design Patterns
These **rules** and **conventions** are so called **Design Patterns**.
@ -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))
}
```
@ -210,7 +215,7 @@ The End To End (e2e) and Unit tests should document what is the behavior intende
### Avoid comments
One of the most important rule of "Clean Code" : If you need to add **comments**, it's because your code is **not clean**.
One of the most important rule of "Clean Code": If you need to add **comments**, it's because your code is **not clean**.
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.

View File

@ -1,8 +1,8 @@
---
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.'
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'
publishedOn: "2022-10-27T14:33:07.465Z"
---
Hello! 👋
@ -21,7 +21,7 @@ Git was originally authored by [Linus Torvalds](https://en.wikipedia.org/wiki/Li
Git allows:
- to be able to work with several people on the same codebase.
- to work with several people on the same codebase.
- track changes to know who did what and when.
- revert changes.
@ -84,7 +84,10 @@ git add .
git add <file>
# Commit changes
git commit -m "chore: initial commit"
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>
@ -122,6 +125,18 @@ 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>
@ -132,15 +147,19 @@ git fetch --prune
# Revert a commit
git revert <commit>
# Change several past commits (interactive rebase)
# HEAD points to the current consulted commit.
git rebase --interactive HEAD~<number-of-commits>
# 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
@ -190,7 +209,7 @@ As we have seen in the [Get started with `git` and `.gitconfig` config file](#ge
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 <abbr title="GNU Privacy Guard">[GPG](https://gnupg.org/)</abbr> key.
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).

View File

@ -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,7 +33,7 @@ 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.
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

View File

@ -1,8 +1,8 @@
---
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.'
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'
publishedOn: "2022-03-14T07:42:52.989Z"
---
Hello! 👋
@ -41,13 +41,13 @@ Find the right balance, between abstraction and simple implementation, start sim
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](https://thream.divlo.fr), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
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](https://thream.divlo.fr), I could release a v1.0.0 without these features:
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)
@ -55,7 +55,7 @@ In my example for [Thream](https://thream.divlo.fr), I could release a v1.0.0 wi
- User public profile
- Channels (maybe could be only one channel per guild to start with)
And probably more, what was really required with [Thream](https://thream.divlo.fr), 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 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**.

View 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)
![Big O Notation](../../public/images/posts/programming-challenges/big-o-chart-notations.webp)
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)

View File

@ -1,19 +1,29 @@
---
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.'
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'
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://www.thream.divlo.fr/) has been released! 🎉
After months of hard work, [Thream v1.0.0](https://github.com/Thream) has been released! 🎉
[**Thream**](https://www.thream.divlo.fr/) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
[**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://www.thream.divlo.fr/) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
[**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.
@ -21,19 +31,19 @@ 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.
![The Thream app on a community page](../public/images/posts/thream-v1-0-0/thream-ui.png)
![The Thream app on a community page](../../public/images/posts/thream-v1-0-0/thream-ui.png)
[**Thream**](https://www.thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://www.thream.divlo.fr/).
[**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/Divlo_FR/status/1260638175246135296) by the creator: Divlo.
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, with colors close to the image of Divlo.
The development of the project begins under the name of **SocialProject**, on August 20, 2020.
![SocialProject](../public/images/posts/thream-v1-0-0/social-project.jpg)
![SocialProject](../../public/images/posts/thream-v1-0-0/social-project.jpg)
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.
@ -53,7 +63,7 @@ Since the project is mainly developed during free time (mainly on weekends), the
- 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.
![HTTP Communication Schema](../public/images/posts/thream-v1-0-0/http-communication.png)
![HTTP Communication Schema](../../public/images/posts/thream-v1-0-0/http-communication.png)
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.
@ -115,5 +125,3 @@ The other interest of the project is that it is completely **open-source**, and
**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.
**Thream** is available: [**thream.divlo.fr**](https://www.thream.divlo.fr/).

View File

@ -1,28 +1,30 @@
import type { Plugin, Transformer } from 'unified'
import type { Literal } from 'unist'
import { visit } from 'unist-util-visit'
import type { 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,
})
})
}

View File

@ -1,45 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link'
import type { FooterProps } from './Footer'
import { Footer } from './Footer'
import { Header } from './Header'
export interface ErrorPageProps extends FooterProps {
statusCode: number
message: string
}
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
const { message, statusCode, version } = props
const { t } = useTranslation()
return (
<>
<div className='flex h-screen flex-col pt-0'>
<Header showLanguage />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
<h1 className='my-6 text-4xl font-semibold'>
{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='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
{t('errors:return-to-home-page')}
</Link>
</p>
</main>
<Footer version={version} />
</div>
</>
)
}

View File

@ -1,42 +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='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
Divlo
</Link>{' '}
| {t('common:all-rights-reserved')}
</p>
<p className='mt-1'>
Version{' '}
<a
data-cy='version-link'
className='text-yellow hover:underline dark:text-yellow-dark'
href={versionLink}
target='_blank'
rel='noopener noreferrer'
>
{version}
</a>
</p>
</footer>
)
}

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

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

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

View File

@ -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 = 'https://divlo.fr/images/icons/icon-96x96.png',
description = 'Divlo - Developer Full Stack • Passionate about High-Tech',
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>
)
}

View File

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

View File

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

View File

@ -1,81 +0,0 @@
import { useCallback, useEffect, useState, useRef } from 'react'
import useTranslation from 'next-translate/useTranslation'
import setLanguage from 'next-translate/setLanguage'
import classNames from 'clsx'
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 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 handleLanguage = async (language: string): Promise<void> => {
await setLanguage(language)
}
return (
<div className='flex cursor-pointer flex-col items-center justify-center'>
<div
ref={languageClickRef}
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 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
{ hidden: hiddenMenu }
)}
>
{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 () => {
return await handleLanguage(language)
}}
>
<LanguageFlag language={language} />
</li>
)
})}
</ul>
</div>
)
}

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

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

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

View File

@ -1,75 +1,76 @@
import { useEffect, useState } from 'react'
import classNames from 'clsx'
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='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 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'
data-cy="switch-theme-dark"
className={classNames(
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
"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'
}
"opacity-100": theme === "dark",
"opacity-0": theme === "light",
},
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
<span className="relative flex size-[10px] items-center justify-center">
🌜
</span>
</div>
<div
data-cy='switch-theme-light'
data-cy="switch-theme-light"
className={classNames(
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
"absolute inset-y-0 right-[10px] my-auto size-[10px] leading-[0]",
{
'opacity-100': theme === 'light',
'opacity-0': theme === 'dark'
}
"opacity-100": theme === "light",
"opacity-0": theme === "dark",
},
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
<span className="relative flex size-[10px] items-center justify-center">
🌞
</span>
</div>
</div>
<div
className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
"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'
}
"left-[27px]": theme === "dark",
"left-0": theme === "light",
},
)}
style={{ border: '1px solid #4d4d4d' }}
style={{ border: "1px solid #4d4d4d" }}
/>
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
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>

View File

@ -1,44 +1,48 @@
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='/'>
<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">
<Link href="/">
<h1 className="flex items-center justify-center">
<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>
</h1>
</Link>
<div className='flex justify-between'>
<div className='flex flex-col items-center justify-center px-6'>
<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='text-yellow hover:underline dark:text-yellow-dark'
href="/blog"
data-cy="header-blog-link"
className="font-semibold text-primary hover:underline dark:text-primary-dark"
>
Blog
</Link>
</div>
{showLanguage && <Language />}
<SwitchTheme />
<Locales
currentLocale={i18n.locale}
cookiesStore={cookiesStore.toString()}
/>
<SwitchTheme cookiesStore={cookiesStore.toString()} />
</div>
</header>
)

View File

@ -1,17 +1,28 @@
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'>
<p className="my-6 text-center text-gray dark:text-gray-dark">
<strong className="text-lg font-semibold text-primary dark:text-primary-dark">
{title}
</strong>
<br />

View File

@ -1,18 +1,18 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import type { 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 mx-2 my-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>

View File

@ -1,18 +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' 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>
)

View File

@ -1,22 +1,21 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from "@/i18n/i18n.server"
import type { InterestParagraphProps } from './InterestParagraph'
import { InterestParagraph } 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'>
<div className="max-w-full">
{paragraphs.map((paragraph, index) => {
return <InterestParagraph key={index} {...paragraph} />
})}

View File

@ -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,17 +7,19 @@ 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>
<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="font-semibold text-primary dark:text-primary-dark">
{name}
</span>
</div>
<p className='my-4'>{description}</p>
<p className="my-4">{description}</p>
</a>
</ShadowContainer>
)

View File

@ -1,33 +1,35 @@
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>
<div 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>
</div>

View File

@ -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,33 +9,33 @@ 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'>
<ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
<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'>
<div className="flex justify-center">
<Image
quality={100}
className='h-auto w-auto 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>

View File

@ -1,21 +1,18 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from "@/i18n/i18n.server"
import type { PortfolioItemProps } from './PortfolioItem'
import { PortfolioItem } 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'>
<div className="flex w-full flex-wrap justify-center px-3">
{items.map((item, index) => {
return <PortfolioItem key={index} {...item} />
})}

View File

@ -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='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
{t('home:about.description-bottom')}
{lang === 'fr' && (
<>
<br />
<br />
<a
href='/curriculum-vitae'
className='text-yellow hover:underline dark:text-yellow-dark'
>
Curriculum vitæ
</a>
</>
)}
</p>
<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/index.html"
className="font-semibold text-primary hover:underline dark:text-primary-dark"
>
Curriculum vitæ ({i18n.translate("common.fr-FR")})
</a>
</div>
)
}

View File

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

View File

@ -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='mb-4 ml-0 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}

View File

@ -1,29 +1,51 @@
import useTranslation from 'next-translate/useTranslation'
import { useMemo } from 'react'
"use client"
import { DIVLO_BIRTHDAY, DIVLO_BIRTHDAY_DATE, getAge } from 'utils/getAge'
import { useMemo } from "react"
import { ProfileItem } from './ProfileItem'
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"
export const ProfileList: React.FC = () => {
const { t } = useTranslation('home')
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(DIVLO_BIRTHDAY)
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' />
<ul className="m-0 list-none p-0">
<ProfileItem
title={t('home:about.birth-date')}
value={`${DIVLO_BIRTHDAY_DATE} (${age} ${t('home:about.years-old')})`}
title={i18n.translate("home.about.pronouns")}
value={i18n.translate("home.about.pronouns-value")}
/>
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
{isMounted ? (
<ProfileItem
title={i18n.translate("home.about.birth-date")}
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
"home.about.years-old",
)})`}
/>
) : null}
<ProfileItem
title='Email'
value='contact@divlo.fr'
link='mailto:contact@divlo.fr'
title={i18n.translate("home.about.nationality")}
value="Alsace, France"
/>
<ProfileItem
title="Email"
value="contact@theoludwig.fr"
link="mailto:contact@theoludwig.fr"
/>
</ul>
)

View File

@ -1,11 +1,11 @@
import Image from 'next/image'
import Image from "next/image"
import DivloLogo from 'public/images/divlo_logo.png'
import Logo from "@/public/images/logo.png"
export const ProfileLogo: React.FC = () => {
export const ProfileLogo = (): JSX.Element => {
return (
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
<Image quality={100} src={DivloLogo} alt='Divlo' priority />
<div className="max-h-[370px] max-w-[370px] px-2 py-6">
<Image quality={100} src={Logo} alt="Théo LUDWIG" priority />
</div>
)
}

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import classNames from 'clsx'
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}
>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,19 @@
interface SocialMediaItemProps {
interface SocialMediaItemProps extends React.PropsWithChildren {
link: string
ariaLabel: string
}
export const SocialMediaItem: React.FC<
React.PropsWithChildren<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"
>
{children}
</a>

View File

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

View File

@ -1,15 +1,19 @@
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
import { ProfileInformation } from './ProfileInfo'
import { ProfileList } from './ProfileList'
import { ProfileLogo } from './ProfileLogo'
import { cookies } from "next/headers"
import { ProfileDescriptionBottom } from "./ProfileDescriptionBottom"
import { ProfileInformation } from "./ProfileInfo"
import { ProfileList } from "./ProfileList"
import { ProfileLogo } from "./ProfileLogo"
export const Profile = (): JSX.Element => {
const cookiesStore = cookies()
export const Profile: React.FC = () => {
return (
<div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
<div className="flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10">
<ProfileLogo />
<div>
<ProfileInformation />
<ProfileList />
<ProfileList cookiesStore={cookiesStore.toString()} />
<ProfileDescriptionBottom />
</div>
</div>

View File

@ -1,46 +1,48 @@
import { useTheme } from 'next-themes'
import Image from 'next/image'
import { useMemo } from 'react'
import Image from "next/image"
import type { SkillName } from './skills'
import { skills } from './skills'
import { getTheme } from "@/theme/theme.server"
import type { SkillName } from "./skills"
import { skills } from "./skills"
export interface SkillComponentProps {
skill: SkillName
}
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
const { skill } = props
const skillProperties = skills[skill]
const { theme } = useTheme()
const image = useMemo(() => {
if (typeof skillProperties.image === 'string') {
const skillProperties = skills[skill]
const theme = getTheme()
const getImage = (): string => {
if (typeof skillProperties.image === "string") {
return skillProperties.image
}
if (theme === 'light') {
if (theme === "light") {
return skillProperties.image.light
}
return skillProperties.image.dark
}, [skillProperties, theme])
}
return (
<a
href={skillProperties.link}
className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
target='_blank'
rel='noopener noreferrer'
className="mx-2 max-w-xl text-primary hover:underline dark:text-primary-dark"
target="_blank"
rel="noopener noreferrer"
>
<div className='text-center'>
<div className="text-center">
<Image
className='inline h-auto w-auto'
className="inline size-16"
quality={100}
width={60}
height={60}
width={64}
height={64}
alt={skill}
src={image}
src={getImage()}
/>
<p className='mt-1'>{skill}</p>
<p className="mt-1 font-semibold">{skill}</p>
</div>
</a>
)

View File

@ -1,24 +1,24 @@
import { ShadowContainer } from 'components/design/ShadowContainer'
import { ShadowContainer } from "@/components/design/ShadowContainer"
export interface SkillsSectionProps {
title: string
children: React.ReactNode
}
export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
export const SkillsSection = (props: SkillsSectionProps): JSX.Element => {
const { title, children } = props
return (
<ShadowContainer>
<div className='mx-auto w-full px-4'>
<div className='flex flex-wrap px-4 py-6'>
<div className='flex-1'>
<div className='mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10'>
<h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'>
<div className="mx-auto w-full px-4">
<div className="flex flex-wrap px-4 py-6">
<div className="flex-1">
<div className="mb-8 border-b border-gray-600 dark:border-white/10">
<h3 className="my-3 text-xl font-semibold text-primary dark:text-primary-dark">
{title}
</h3>
</div>
<div className='flex flex-wrap justify-around'>{children}</div>
<div className="flex flex-wrap justify-around">{children}</div>
</div>
</div>
</div>

View File

@ -1,41 +1,40 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from "@/i18n/i18n.server"
import { SkillComponent } from './Skill'
import { SkillsSection } from './SkillsSection'
import { SkillComponent } from "./Skill"
import { SkillsSection } from "./SkillsSection"
export const Skills: React.FC = () => {
const { t } = useTranslation()
export const Skills = (): JSX.Element => {
const i18n = getI18n()
return (
<>
<SkillsSection title={t('home:skills.languages')}>
<SkillComponent skill='JavaScript' />
<SkillComponent skill='TypeScript' />
<SkillComponent skill='Python' />
<SkillComponent skill='C/C++' />
<SkillComponent skill='PHP' />
<SkillsSection title={i18n.translate("home.skills.languages")}>
<SkillComponent skill="TypeScript" />
<SkillComponent skill="Python" />
<SkillComponent skill="C/C++" />
<SkillComponent skill="PHP" />
</SkillsSection>
<SkillsSection title='Front-end'>
<SkillComponent skill='HTML' />
<SkillComponent skill='CSS' />
<SkillComponent skill='Tailwind CSS' />
<SkillComponent skill='React.js (+ Next.js)' />
<SkillsSection title="Frontend">
<SkillComponent skill="HTML" />
<SkillComponent skill="CSS" />
<SkillComponent skill="Tailwind CSS" />
<SkillComponent skill="React.js (+ Next.js)" />
</SkillsSection>
<SkillsSection title='Back-end'>
<SkillComponent skill='Laravel' />
<SkillComponent skill='Node.js' />
<SkillComponent skill='Fastify' />
<SkillComponent skill='PostgreSQL' />
<SkillsSection title="Backend">
<SkillComponent skill="Laravel" />
<SkillComponent skill="Node.js" />
<SkillComponent skill="Fastify" />
<SkillComponent skill="PostgreSQL" />
</SkillsSection>
<SkillsSection title={t('home:skills.software-tools')}>
<SkillComponent skill='GNU/Linux' />
<SkillComponent skill='Ubuntu' />
<SkillComponent skill='Visual Studio Code' />
<SkillComponent skill='Git' />
<SkillComponent skill='Docker' />
<SkillsSection title={i18n.translate("home.skills.software-tools")}>
<SkillComponent skill="GNU/Linux" />
<SkillComponent skill="Arch Linux" />
<SkillComponent skill="Visual Studio Code" />
<SkillComponent skill="Git" />
<SkillComponent skill="Docker" />
</SkillsSection>
</>
)

View File

@ -5,111 +5,111 @@ export interface Skill {
export const skills = {
JavaScript: {
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
image: '/images/skills/JavaScript.png'
link: "https://developer.mozilla.org/docs/Web/JavaScript",
image: "/images/skills/JavaScript.png",
},
TypeScript: {
link: 'https://www.typescriptlang.org/',
image: '/images/skills/TypeScript.png'
link: "https://www.typescriptlang.org/",
image: "/images/skills/TypeScript.png",
},
Python: {
link: 'https://www.python.org/',
image: '/images/skills/Python.png'
link: "https://www.python.org/",
image: "/images/skills/Python.png",
},
'C/C++': {
link: 'https://isocpp.org/',
image: '/images/skills/C-Cpp.png'
"C/C++": {
link: "https://isocpp.org/",
image: "/images/skills/C-Cpp.png",
},
PHP: {
link: 'https://www.php.net/',
image: '/images/skills/PHP.png'
link: "https://www.php.net/",
image: "/images/skills/PHP.png",
},
Laravel: {
link: 'https://laravel.com/',
image: '/images/skills/Laravel.png'
link: "https://laravel.com/",
image: "/images/skills/Laravel.png",
},
Dart: {
link: 'https://dart.dev/',
image: '/images/skills/Dart.png'
link: "https://dart.dev/",
image: "/images/skills/Dart.png",
},
Flutter: {
link: 'https://flutter.dev/',
image: '/images/skills/Flutter.webp'
link: "https://flutter.dev/",
image: "/images/skills/Flutter.webp",
},
HTML: {
link: 'https://developer.mozilla.org/docs/Web/HTML',
image: '/images/skills/HTML.png'
link: "https://developer.mozilla.org/docs/Web/HTML",
image: "/images/skills/HTML.png",
},
CSS: {
link: 'https://developer.mozilla.org/docs/Web/CSS',
image: '/images/skills/CSS.png'
link: "https://developer.mozilla.org/docs/Web/CSS",
image: "/images/skills/CSS.png",
},
'Tailwind CSS': {
link: 'https://tailwindcss.com/',
image: '/images/skills/TailwindCSS.png'
"Tailwind CSS": {
link: "https://tailwindcss.com/",
image: "/images/skills/TailwindCSS.png",
},
SASS: {
link: 'https://sass-lang.com/',
image: '/images/skills/SASS.svg'
link: "https://sass-lang.com/",
image: "/images/skills/SASS.svg",
},
'React.js (+ Next.js)': {
link: 'https://reactjs.org/',
image: '/images/skills/ReactJS.png'
"React.js (+ Next.js)": {
link: "https://reactjs.org/",
image: "/images/skills/ReactJS.png",
},
'Node.js': {
link: 'https://nodejs.org/',
image: '/images/skills/NodeJS.png'
"Node.js": {
link: "https://nodejs.org/",
image: "/images/skills/NodeJS.png",
},
Fastify: {
link: 'https://www.fastify.io/',
link: "https://www.fastify.io/",
image: {
light: '/images/skills/Fastify-light.png',
dark: '/images/skills/Fastify-dark.png'
}
light: "/images/skills/Fastify-light.png",
dark: "/images/skills/Fastify-dark.png",
},
},
Prisma: {
link: 'https://www.prisma.io/',
link: "https://www.prisma.io/",
image: {
light: '/images/skills/Prisma-light.png',
dark: '/images/skills/Prisma-dark.png'
}
light: "/images/skills/Prisma-light.png",
dark: "/images/skills/Prisma-dark.png",
},
},
PostgreSQL: {
link: 'https://www.postgresql.org/',
image: '/images/skills/PostgreSQL.png'
link: "https://www.postgresql.org/",
image: "/images/skills/PostgreSQL.png",
},
MySQL: {
link: 'https://www.mysql.com/',
image: '/images/skills/MySQL.png'
link: "https://www.mysql.com/",
image: "/images/skills/MySQL.png",
},
Strapi: {
link: 'https://strapi.io/',
image: '/images/skills/Strapi.png'
link: "https://strapi.io/",
image: "/images/skills/Strapi.png",
},
'Visual Studio Code': {
link: 'https://code.visualstudio.com/',
image: '/images/skills/Visual_Studio_Code.png'
"Visual Studio Code": {
link: "https://code.visualstudio.com/",
image: "/images/skills/VisualStudioCode.png",
},
Git: {
link: 'https://git-scm.com/',
image: '/images/skills/Git.png'
},
Hyper: {
link: 'https://hyper.is/',
image: '/images/skills/Hyper.svg'
link: "https://git-scm.com/",
image: "/images/skills/Git.png",
},
Ubuntu: {
link: 'https://ubuntu.com/',
image: '/images/skills/Ubuntu.png'
link: "https://ubuntu.com/",
image: "/images/skills/Ubuntu.png",
},
'GNU/Linux': {
link: 'https://www.gnu.org/',
image: '/images/skills/GNU-Linux.png'
"Arch Linux": {
link: "https://archlinux.org/",
image: "/images/skills/ArchLinux.png",
},
"GNU/Linux": {
link: "https://www.gnu.org/",
image: "/images/skills/GNU-Linux.png",
},
Docker: {
link: 'https://www.docker.com/',
image: '/images/skills/Docker.png'
}
link: "https://www.docker.com/",
image: "/images/skills/Docker.png",
},
} as const
export type SkillName = keyof typeof skills

View File

@ -0,0 +1,28 @@
import classNames from "clsx"
export interface LoaderProps {
width?: number
height?: number
className?: string
}
export const Loader = (props: LoaderProps): JSX.Element => {
const { width = 50, height = 50, className } = props
return (
<div
style={{
width,
height,
}}
className={classNames(
"inline-block animate-spin rounded-full border-[3px] border-current border-t-transparent text-primary dark:text-primary-dark",
className,
)}
role="status"
aria-label="loading"
>
<span className="sr-only">Loading...</span>
</div>
)
}

View File

@ -1,6 +1,10 @@
import { useEffect, useRef } from 'react'
"use client"
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
import { useEffect, useRef } from "react"
export type RevealFadeProps = React.PropsWithChildren
export const RevealFade = (props: RevealFadeProps): JSX.Element => {
const { children } = props
const htmlElement = useRef<HTMLDivElement>(null)
@ -8,25 +12,25 @@ export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
useEffect(() => {
const observer = new window.IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.className =
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
"opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out"
observer.unobserve(entry.target)
}
})
}
},
{
root: null,
rootMargin: '0px',
threshold: 0.28
}
rootMargin: "0px",
threshold: 0.28,
},
)
observer.observe(htmlElement.current as HTMLDivElement)
}, [])
return (
<div ref={htmlElement} className='invisible -translate-y-7 opacity-0'>
<div ref={htmlElement} className="invisible -translate-y-7 opacity-0">
{children}
</div>
)

View File

@ -1,10 +1,13 @@
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
type SectionHeadingProps = React.ComponentPropsWithRef<"h2">
export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
const { children, ...rest } = props
return (
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
<h2
{...rest}
className="mb-3 mt-1 text-center text-4xl font-semibold text-primary dark:text-primary-dark"
>
{children}
</h2>
)

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