mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
89 Commits
Author | SHA1 | Date | |
---|---|---|---|
3074945c54
|
|||
fc0dfdda5f
|
|||
f62964c62a
|
|||
8ec113c9cb
|
|||
8a59e9034f
|
|||
d7121ea833
|
|||
c10f690622
|
|||
6915072ab9
|
|||
dd803bcc51
|
|||
efa33f26ec
|
|||
5f3dfad988
|
|||
b231381cb3
|
|||
bbb2e56512
|
|||
66cf6d7438
|
|||
2a635bf3ba
|
|||
9f79b88202
|
|||
23d9caf578
|
|||
7febe6d1f9
|
|||
c4650c34d9
|
|||
0eb780485c
|
|||
cd5e92b64a
|
|||
982b148329
|
|||
0febee5b51
|
|||
3502f51735
|
|||
493df4e2f2
|
|||
c2c9b59c7a
|
|||
f6e3008ab9
|
|||
15e94cec64
|
|||
5185c6758b
|
|||
b633eef833
|
|||
d2e627ff13
|
|||
1e0567b538
|
|||
c8d32c6acc
|
|||
05503cda26
|
|||
303b6f3011
|
|||
0272cf7080
|
|||
e8ea42a260
|
|||
f337e14260
|
|||
f5020cad19
|
|||
b8ceefb2f6
|
|||
1523c8cac0
|
|||
548ddc8425
|
|||
bac65ad61a
|
|||
b91f3165b7
|
|||
5478e202a7
|
|||
a89b5932c2
|
|||
339e42acfa
|
|||
c123815a86
|
|||
dd26a277a2
|
|||
62222dbb0c
|
|||
ee0a02bc8b
|
|||
2e04053ec3
|
|||
45a9a69122
|
|||
e566ef6c38
|
|||
c7ad15a465
|
|||
f4a842efb5
|
|||
424c97019b
|
|||
c0508dc0b9
|
|||
f04d8a0c11
|
|||
d29064745c
|
|||
95febe2a99
|
|||
fdab2a7ea8
|
|||
35211fa279
|
|||
137cceffa1
|
|||
f6bfc466de
|
|||
e4cf714d95
|
|||
d3c86b2a26
|
|||
d2578abeec
|
|||
e51e3bdc19
|
|||
56520830e9
|
|||
2e0138194c
|
|||
4b2e7bae90
|
|||
caa6a90418
|
|||
e82db952db
|
|||
6b29ce9b15
|
|||
5640f1b434
|
|||
6d0dcb50a7
|
|||
70603f1444
|
|||
f42fdbfd0c
|
|||
6a3f335f9f
|
|||
f1509d0af1
|
|||
49599d25ed
|
|||
65e0f4f8b6
|
|||
8d60c2d53a
|
|||
0bbebeab99
|
|||
643e0e5821
|
|||
872b018673
|
|||
2644cb0fb5
|
|||
bc719578d2
|
@ -1 +0,0 @@
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:18
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "theoludwig",
|
||||
"dockerComposeFile": "./docker-compose.yml",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postAttachCommand": ["npm", "install"],
|
||||
"remoteUser": "node"
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
services:
|
||||
workspace:
|
||||
build:
|
||||
context: './'
|
||||
dockerfile: './Dockerfile'
|
||||
volumes:
|
||||
- '..:/workspace:cached'
|
||||
command: 'sleep infinity'
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
# For more information see: https://editorconfig.org/
|
||||
# https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
|
@ -1,2 +1,4 @@
|
||||
COMPOSE_PROJECT_NAME=theoludwig
|
||||
HOSTNAME=0.0.0.0
|
||||
PORT=3000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
@ -1,10 +1,38 @@
|
||||
{
|
||||
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier", "unicorn"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
"root": true,
|
||||
"extends": [
|
||||
"conventions",
|
||||
"next/core-web-vitals",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"plugins": ["import", "promise", "unicorn"],
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["classNames"]
|
||||
},
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
"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",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🐛 Bug Report'
|
||||
about: 'Report an unexpected problem or unintended behavior.'
|
||||
title: '[Bug]'
|
||||
labels: 'bug'
|
||||
name: "🐛 Bug Report"
|
||||
about: "Report an unexpected problem or unintended behavior."
|
||||
title: "[Bug]"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
<!--
|
||||
|
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '📜 Documentation'
|
||||
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
||||
title: '[Documentation]'
|
||||
labels: 'documentation'
|
||||
name: "📜 Documentation"
|
||||
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
|
||||
title: "[Documentation]"
|
||||
labels: "documentation"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '✨ Feature Request'
|
||||
about: 'Suggest a new feature idea.'
|
||||
title: '[Feature]'
|
||||
labels: 'feature request'
|
||||
name: "✨ Feature Request"
|
||||
about: "Suggest a new feature idea."
|
||||
title: "[Feature]"
|
||||
labels: "feature request"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🔧 Improvement'
|
||||
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
||||
title: '[Improvement]'
|
||||
labels: 'improvement'
|
||||
name: "🔧 Improvement"
|
||||
about: "Improve structure/format/performance/refactor/tests of the code."
|
||||
title: "[Improvement]"
|
||||
labels: "improvement"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@ -1,8 +1,8 @@
|
||||
---
|
||||
name: '🙋 Question'
|
||||
about: 'Further information is requested.'
|
||||
title: '[Question]'
|
||||
labels: 'question'
|
||||
name: "🙋 Question"
|
||||
about: "Further information is requested."
|
||||
title: "[Question]"
|
||||
labels: "question"
|
||||
---
|
||||
|
||||
### Question
|
||||
|
27
.github/workflows/analyze.yml
vendored
27
.github/workflows/analyze.yml
vendored
@ -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.3'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@v2'
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@v2'
|
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@ -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.3'
|
||||
- uses: "actions/checkout@v4.1.6"
|
||||
|
||||
- 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"
|
||||
|
40
.github/workflows/lint.yml
vendored
40
.github/workflows/lint.yml
vendored
@ -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.3'
|
||||
- uses: "actions/checkout@v4.1.6"
|
||||
|
||||
- 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.21.0"
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@ -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.3'
|
||||
- uses: "actions/checkout@v4.1.6"
|
||||
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 }}
|
||||
|
50
.github/workflows/test.yml
vendored
50
.github/workflows/test.yml
vendored
@ -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.3'
|
||||
- uses: "actions/checkout@v4.1.6"
|
||||
|
||||
- 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.3'
|
||||
- uses: "actions/checkout@v4.1.6"
|
||||
|
||||
- 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
4
.gitignore
vendored
@ -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
|
||||
|
20
.gitpod.yml
20
.gitpod.yml
@ -1,20 +0,0 @@
|
||||
image: 'gitpod/workspace-full'
|
||||
|
||||
tasks:
|
||||
- before: 'cp .env.example .env'
|
||||
init: 'npm 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
|
@ -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"]
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npm run lint:commit -- --edit
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npm run lint:staged
|
||||
|
@ -1,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"
|
||||
]
|
||||
}
|
||||
|
@ -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"],
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -5,7 +5,7 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.options": {
|
||||
"ignorePath": ".gitignore"
|
||||
|
@ -29,12 +29,10 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
|
||||
|
||||
## Getting Started
|
||||
|
||||
[](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
|
||||
|
||||
@ -49,7 +47,7 @@ cd theoludwig
|
||||
cp .env.example .env
|
||||
|
||||
# Install
|
||||
npm install
|
||||
npm clean-install
|
||||
```
|
||||
|
||||
### Local Development environment
|
||||
@ -66,6 +64,6 @@ npm run dev
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
### Services started
|
||||
### Service started
|
||||
|
||||
- `website`: <http://127.0.0.1:3000>
|
||||
`website`: <http://127.0.0.1:3000>
|
||||
|
25
Dockerfile
25
Dockerfile
@ -1,21 +1,28 @@
|
||||
FROM node:18.16.1 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.1 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"]
|
||||
|
4
LICENSE
4
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
# MIT License
|
||||
|
||||
Copyright (c) Théo LUDWIG
|
||||
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
|
||||
|
10
README.md
10
README.md
@ -1,7 +1,7 @@
|
||||
<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">
|
||||
@ -25,10 +25,10 @@
|
||||
"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"],
|
||||
"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=theoludwig&show_icons=true&theme=dark" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" alt="Théo LUDWIG's Programming Languages" />
|
||||
</p>
|
||||
|
11
app/blog/[slug]/loading.tsx
Normal file
11
app/blog/[slug]/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
44
app/blog/[slug]/page.tsx
Normal file
44
app/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { getBlogPostBySlug } from "@/blog/blog"
|
||||
import { BlogPost } from "@/blog/BlogPost"
|
||||
|
||||
interface BlogPostPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = async (
|
||||
props: BlogPostPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const blogPost = await getBlogPostBySlug(props.params.slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
|
||||
const description = blogPost.frontmatter.description
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
|
||||
const { params } = props
|
||||
|
||||
return <BlogPost slug={params.slug} />
|
||||
}
|
||||
|
||||
export default BlogPostPage
|
11
app/blog/loading.tsx
Normal file
11
app/blog/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
42
app/blog/page.tsx
Normal file
42
app/blog/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Suspense } from "react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { BlogPosts } from "@/blog/BlogPosts"
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const title = "Blog | Théo LUDWIG"
|
||||
const description =
|
||||
"The latest news about my journey of learning computer science."
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
|
||||
const BlogPage = async (): Promise<JSX.Element> => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col flex-wrap items-center">
|
||||
<div className="mt-10 flex flex-col items-center">
|
||||
<h1 className="text-4xl font-semibold text-primary dark:text-primary-dark">
|
||||
Blog
|
||||
</h1>
|
||||
<p className="mt-6 text-center" data-cy="blog-post-date">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<Suspense fallback={<Loader className="mt-8" />}>
|
||||
<BlogPosts />
|
||||
</Suspense>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPage
|
32
app/error.tsx
Normal file
32
app/error.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
export interface ErrorHandlingProps {
|
||||
error: Error
|
||||
}
|
||||
|
||||
const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
|
||||
const { error } = props
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 className="my-6 text-4xl font-semibold">
|
||||
Error{" "}
|
||||
<span
|
||||
className="text-primary dark:text-primary-dark"
|
||||
data-cy="status-code"
|
||||
>
|
||||
500
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-center text-lg">Server error</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandling
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
@ -7,6 +7,10 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.prose {
|
||||
@apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300;
|
||||
}
|
||||
@ -16,7 +20,7 @@
|
||||
}
|
||||
|
||||
.prose [id]::before {
|
||||
content: '';
|
||||
content: "";
|
||||
display: block;
|
||||
height: 90px;
|
||||
margin-top: -90px;
|
||||
@ -25,7 +29,12 @@
|
||||
|
||||
.prose a,
|
||||
.prose strong {
|
||||
@apply text-yellow dark:text-yellow-dark;
|
||||
@apply !font-semibold text-primary dark:text-primary-dark;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.prose h2,
|
||||
@ -39,9 +48,9 @@
|
||||
.prose code {
|
||||
color: #ce9178;
|
||||
}
|
||||
.prose :where(code):not(:where([class~='not-prose'] *))::before,
|
||||
.prose :where(code):not(:where([class~='not-prose'] *))::after {
|
||||
content: '';
|
||||
.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;
|
||||
@ -53,15 +62,17 @@ code {
|
||||
code .line::before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 1rem;
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
text-align: right;
|
||||
color: rgba(133, 133, 133, 0.8);
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code .line:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.katex .base {
|
||||
display: inline !important;
|
||||
white-space: normal !important;
|
80
app/layout.tsx
Normal file
80
app/layout.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import type { Metadata } from "next"
|
||||
import classNames from "clsx"
|
||||
|
||||
import "@fontsource/montserrat/400.css"
|
||||
import "@fontsource/montserrat/600.css"
|
||||
import "./globals.css"
|
||||
|
||||
import { Header } from "@/components/Header"
|
||||
import { Footer } from "@/components/Footer"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
import { getTheme } from "@/theme/theme.server"
|
||||
|
||||
const title = "Théo LUDWIG"
|
||||
const description =
|
||||
"Théo LUDWIG - Developer Full Stack • Open-Source Enthusiast"
|
||||
const image = "/images/logo.png"
|
||||
const url = new URL("https://theoludwig.fr")
|
||||
const locale = "fr-FR, en-US"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
metadataBase: url,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
siteName: title,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
width: 96,
|
||||
height: 96,
|
||||
},
|
||||
],
|
||||
locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title,
|
||||
description,
|
||||
images: [image],
|
||||
},
|
||||
}
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const RootLayout = (props: RootLayoutProps): JSX.Element => {
|
||||
const { children } = props
|
||||
|
||||
const i18n = getI18n()
|
||||
const theme = getTheme()
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={i18n.locale}
|
||||
className={classNames(
|
||||
{
|
||||
dark: theme === "dark",
|
||||
light: theme === "light",
|
||||
},
|
||||
"scroll-smooth",
|
||||
)}
|
||||
style={{
|
||||
colorScheme: theme,
|
||||
}}
|
||||
>
|
||||
<body className="flex min-h-screen flex-col bg-white font-headline text-black dark:bg-black dark:text-white">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootLayout
|
11
app/loading.tsx
Normal file
11
app/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from "@/components/design/Loader"
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
32
app/not-found.tsx
Normal file
32
app/not-found.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
const NotFound = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 className="my-6 text-4xl font-semibold">
|
||||
{i18n.translate("errors.error")}{" "}
|
||||
<span
|
||||
className="text-primary dark:text-primary-dark"
|
||||
data-cy="status-code"
|
||||
>
|
||||
404
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-center text-lg">
|
||||
{i18n.translate("errors.not-found")}{" "}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
{i18n.translate("errors.return-to-home-page")}
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
59
app/page.tsx
Normal file
59
app/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { RevealFade } from "@/components/design/RevealFade"
|
||||
import { Section } from "@/components/design/Section"
|
||||
import { Interests } from "@/components/Interests"
|
||||
import { Portfolio } from "@/components/Portfolio"
|
||||
import { Profile } from "@/components/Profile"
|
||||
import { SocialMediaList } from "@/components/Profile/SocialMediaList"
|
||||
import { Skills } from "@/components/Skills"
|
||||
import { OpenSource } from "@/components/OpenSource"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
const HomePage = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl">
|
||||
<Section isMain id="about">
|
||||
<Profile />
|
||||
<SocialMediaList />
|
||||
</Section>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="interests"
|
||||
heading={i18n.translate("home.interests.title")}
|
||||
>
|
||||
<Interests />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="skills"
|
||||
heading={i18n.translate("home.skills.title")}
|
||||
withoutShadowContainer
|
||||
>
|
||||
<Skills />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id="portfolio"
|
||||
heading={i18n.translate("home.portfolio.title")}
|
||||
withoutShadowContainer
|
||||
>
|
||||
<Portfolio />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section id="open-source" heading="Open source" withoutShadowContainer>
|
||||
<OpenSource />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
37
blog/BlogPost.tsx
Normal file
37
blog/BlogPost.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import date from "date-and-time"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { getBlogPostBySlug } from "@/blog/blog"
|
||||
import { BlogPostContent } from "@/blog/BlogPostContent"
|
||||
|
||||
export interface BlogPostProps {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
|
||||
const { slug } = props
|
||||
|
||||
const blogPost = await getBlogPostBySlug(slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center">
|
||||
<div className="my-10 flex flex-col items-center text-center">
|
||||
<h1 className="text-3xl font-semibold text-primary dark:text-primary-dark">
|
||||
{blogPost.frontmatter.title}
|
||||
</h1>
|
||||
<p className="mt-2" data-cy="blog-post-date">
|
||||
{date.format(
|
||||
new Date(blogPost.frontmatter.publishedOn),
|
||||
"DD/MM/YYYY",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<BlogPostContent content={blogPost.content} />
|
||||
</main>
|
||||
)
|
||||
}
|
33
blog/BlogPostComments.tsx
Normal file
33
blog/BlogPostComments.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import Giscus from "@giscus/react"
|
||||
|
||||
import { useTheme } from "@/theme/theme.client"
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
|
||||
interface BlogPostCommentsProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
|
||||
const theme = useTheme(cookiesStore)
|
||||
|
||||
return (
|
||||
<Giscus
|
||||
id="comments"
|
||||
repo="theoludwig/theoludwig"
|
||||
repoId="MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ="
|
||||
category="General"
|
||||
categoryId="DIC_kwDOFWUewM4CQ_WK"
|
||||
mapping="pathname"
|
||||
reactionsEnabled="1"
|
||||
emitMetadata="0"
|
||||
inputPosition="top"
|
||||
theme={theme}
|
||||
lang="en"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}
|
135
blog/BlogPostContent.tsx
Normal file
135
blog/BlogPostContent.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { nodeTypes } from "@mdx-js/mdx"
|
||||
import rehypeShikiFromHighlighter from "@shikijs/rehype/core"
|
||||
import { MDXRemote } from "next-mdx-remote/rsc"
|
||||
import { cookies } from "next/headers"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import rehypeSlug from "rehype-slug"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import remarkMath from "remark-math"
|
||||
import { getHighlighterCore } from "shiki/core"
|
||||
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { BlogPostComments } from "@/blog/BlogPostComments"
|
||||
import { getTheme } from "@/theme/theme.server"
|
||||
|
||||
const Heading = (
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLHeadingElement>,
|
||||
HTMLHeadingElement
|
||||
>,
|
||||
): JSX.Element => {
|
||||
const { children, id = "" } = props
|
||||
return (
|
||||
<h2 {...props}>
|
||||
<Link href={`#${id}`} className="group relative hover:no-underline">
|
||||
<FontAwesomeIcon
|
||||
className="absolute bottom-2 left-[-26px] mr-2 hidden size-4 !text-black group-hover:inline dark:!text-white"
|
||||
icon={faLink}
|
||||
/>
|
||||
{children}
|
||||
</Link>
|
||||
</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 getHighlighterCore({
|
||||
themes: [
|
||||
import("shiki/themes/light-plus.mjs"),
|
||||
import("shiki/themes/dark-plus.mjs"),
|
||||
],
|
||||
langs: [
|
||||
import("shiki/langs/markdown.mjs"),
|
||||
import("shiki/langs/shell.mjs"),
|
||||
import("shiki/langs/javascript.mjs"),
|
||||
import("shiki/langs/typescript.mjs"),
|
||||
import("shiki/langs/python.mjs"),
|
||||
],
|
||||
loadWasm: import("shiki/wasm"),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="prose mb-10">
|
||||
<div className="px-8">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm, remarkMath],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeRaw, { passThrough: nodeTypes }],
|
||||
rehypeKatex,
|
||||
[
|
||||
rehypeShikiFromHighlighter,
|
||||
highlighter,
|
||||
{
|
||||
theme: `${theme}-plus`,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}}
|
||||
components={{
|
||||
h1: Heading,
|
||||
h2: Heading,
|
||||
h3: Heading,
|
||||
h4: Heading,
|
||||
h5: Heading,
|
||||
h6: Heading,
|
||||
img: (properties) => {
|
||||
const { src = "", alt = "Blog Image" } = properties
|
||||
const source = src.replace("../../public/", "/")
|
||||
return (
|
||||
<span className="flex flex-col items-center justify-center">
|
||||
<Image
|
||||
src={source}
|
||||
alt={alt}
|
||||
width={1000}
|
||||
height={1000}
|
||||
className="size-auto"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
a: (props) => {
|
||||
const { href = "", ...rest } = props
|
||||
if (href.startsWith("#")) {
|
||||
return <a {...props} />
|
||||
}
|
||||
if (href.startsWith("../posts/")) {
|
||||
return (
|
||||
<a
|
||||
href={href
|
||||
.replace("../posts/", "/blog/")
|
||||
.replace(".md", "")}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <a target="_blank" rel="noopener noreferrer" {...props} />
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
42
blog/BlogPosts.tsx
Normal file
42
blog/BlogPosts.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import Link from "next/link"
|
||||
import date from "date-and-time"
|
||||
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
import { getBlogPosts } from "@/blog/blog"
|
||||
|
||||
export const BlogPosts = async (): Promise<JSX.Element> => {
|
||||
const posts = await getBlogPosts()
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center p-8">
|
||||
<ul className="w-[1600px]" data-cy="blog-posts">
|
||||
{posts.map((post) => {
|
||||
const postPublishedOn = date.format(
|
||||
new Date(post.frontmatter.publishedOn),
|
||||
"DD/MM/YYYY",
|
||||
)
|
||||
return (
|
||||
<li key={post.slug}>
|
||||
<Link href={`/blog/${post.slug}`} locale="en" data-cy={post.slug}>
|
||||
<ShadowContainer className="cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.02]">
|
||||
<h2
|
||||
data-cy="blog-post-title"
|
||||
className="text-xl font-semibold text-primary dark:text-primary-dark"
|
||||
>
|
||||
{post.frontmatter.title}
|
||||
</h2>
|
||||
<p data-cy="blog-post-date" className="mt-2">
|
||||
{postPublishedOn}
|
||||
</p>
|
||||
<p data-cy="blog-post-description" className="mt-3">
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
</ShadowContainer>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
65
blog/blog.ts
Normal file
65
blog/blog.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
import { cache } from "react"
|
||||
import matter from "gray-matter"
|
||||
|
||||
export const BLOG_POSTS_PATH = path.join(process.cwd(), "blog", "posts")
|
||||
|
||||
export interface FrontMatter {
|
||||
title: string
|
||||
description: string
|
||||
isPublished: boolean
|
||||
publishedOn: string
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
frontmatter: FrontMatter
|
||||
slug: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
||||
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||
const blogPostsWithTime = await Promise.all(
|
||||
blogPosts.map(async (blogPostFilename) => {
|
||||
const [slug, extension] = blogPostFilename.split(".")
|
||||
if (slug == null || extension == null) {
|
||||
throw new Error("Invalid blog post filename.")
|
||||
}
|
||||
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||
encoding: "utf8",
|
||||
})
|
||||
const { data, content } = matter(blogPostContent) as unknown as {
|
||||
data: FrontMatter
|
||||
content: string
|
||||
}
|
||||
const date = new Date(data.publishedOn)
|
||||
return {
|
||||
slug,
|
||||
content,
|
||||
frontmatter: data,
|
||||
time: date.getTime(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||
.filter((post) => {
|
||||
return post.frontmatter.isPublished
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.time - a.time
|
||||
})
|
||||
return blogPostsSortedByPublicationDate
|
||||
})
|
||||
|
||||
export const getBlogPostBySlug = cache(
|
||||
async (slug: string): Promise<BlogPost | undefined> => {
|
||||
const blogPosts = await getBlogPosts()
|
||||
const blogPost = blogPosts.find((blogPost) => {
|
||||
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
||||
})
|
||||
return blogPost
|
||||
},
|
||||
)
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
title: '🧼 Clean Code'
|
||||
title: "🧼 Clean Code"
|
||||
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
|
||||
isPublished: true
|
||||
publishedOn: '2022-02-23T08:00:18.758Z'
|
||||
publishedOn: "2022-02-23T08:00:18.758Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
@ -19,13 +19,13 @@ 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.
|
||||
|
||||
@ -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})`)
|
||||
@ -170,17 +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
|
||||
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, "")
|
||||
}
|
||||
```
|
||||
|
||||
@ -189,15 +189,15 @@ const createFile = async (
|
||||
### 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): Promise<void> => {
|
||||
await fs.promises.writeFile(name, '')
|
||||
await fs.promises.writeFile(name, "")
|
||||
}
|
||||
|
||||
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||
await createFile(path.join('temporary', name))
|
||||
await createFile(path.join("temporary", name))
|
||||
}
|
||||
```
|
||||
|
@ -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! 👋
|
||||
@ -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,11 @@ 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>
|
||||
|
||||
@ -145,6 +153,24 @@ 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 avoid creating duplicated commits with cherry-pick, we can use rebase after cherry-pick.
|
||||
# <target-branch> being the commit where you want to apply the commit to cherry-pick.
|
||||
# <from-branch> being the branch where the commit to cherry-pick is.
|
||||
git rebase <target-branch> <from-branch>
|
||||
|
||||
# If, by mistake, you have started a branch from the wrong base branch, you can rebase the branch on the correct base branch.
|
||||
# For example, if you have started a branch `feature-2` from `feature` instead of `develop`, you can rebase the branch on `develop`.
|
||||
git rebase --onto <new-base-branch> <old-base-branch> <branch>
|
||||
# For example:
|
||||
git rebase --onto develop feature feature-2
|
||||
|
||||
# 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
|
||||
@ -194,7 +220,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).
|
||||
|
||||
@ -230,6 +256,32 @@ There are many ways to organize the work, but the most popular ones are:
|
||||
|
||||
They are called **Git workflows**, or **Git branching strategies**.
|
||||
|
||||
## Tips and tricks
|
||||
|
||||
### `diff-commits` alias
|
||||
|
||||
The `git diff` command allows you to compare the changes between two commits, branches, etc.
|
||||
|
||||
Sometimes, you want to compare what commits have been made between two branches, without looking at the changes in the files, to do so, we can create an `alias` in `.gitconfig`:
|
||||
|
||||
```sh
|
||||
[alias]
|
||||
diff-commits = !sh -c 'echo -n "Commits in $2 not in $1 \\(" && printf "%d" $(git cherry -v $1 $2 | wc -l) && echo "\\)" && git cherry -v $1 $2 && echo "" && echo -n "Commits in $1 not in $2 \\(" && printf "%d" $(git cherry -v $2 $1 | wc -l) && echo "\\)" && git cherry -v $2 $1' -
|
||||
```
|
||||
|
||||
With this alias, we can compare the commits between `main` and `develop` branches for example:
|
||||
|
||||
```sh
|
||||
$ git diff-commits main develop
|
||||
|
||||
Commits in develop not in main (2)
|
||||
+ 9b80e0724df8454b43bc3935a1bffb67615572d7 feat: new feature
|
||||
+ 50721f8ecb60ff023bdccc1873ec1e20ee0b21a0 feat: new feature 2
|
||||
|
||||
Commits in main not in develop (1)
|
||||
- f7bb9d2af7763e0a311099e880e8bf7d6b51bf4d fix: urgent hotfix
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
`git` is the tool that every programmer should know to do collaborative work (not only, `git` is also very powerful even when working alone) and keep track of changes across a set of files.
|
@ -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! 👋
|
@ -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**.
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
title: '🧠 Programming Challenges'
|
||||
description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.'
|
||||
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'
|
||||
publishedOn: "2023-05-21T10:20:18.837Z"
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
@ -15,7 +15,7 @@ We don't want to "reinvent the wheel" and rewrite everything from scratch for ea
|
||||
|
||||
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 is easier and allows a better maintenance in the long term.
|
||||
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.
|
||||
|
||||
@ -216,7 +216,7 @@ $$
|
||||
|
||||
#### Complexity Classes (from fastest to slowest)
|
||||
|
||||

|
||||

|
||||
|
||||
Here is a list of classes of functions that are commonly encountered when analyzing the running time of an algorithm.
|
||||
|
||||
@ -240,7 +240,7 @@ Here is a list of classes of functions that are commonly encountered when analyz
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
@ -286,7 +286,7 @@ Contiguous subarray is any sub series of elements in a given array that are cont
|
||||
|
||||
**Explanation:** The subarray with the largest sum is `[2, 4, -3, 5, 2]` which has a sum of `10`.
|
||||
|
||||
### Worst solution: Brute force
|
||||
### Worst solution: Brute force ($O(n^3)$)
|
||||
|
||||
```python
|
||||
def maximum_subarray_sum_cubic(array: list[int]) -> int:
|
||||
@ -309,7 +309,7 @@ def maximum_subarray_sum_cubic(array: list[int]) -> int:
|
||||
return best_sum
|
||||
```
|
||||
|
||||
### Better solution: Linear time
|
||||
### Better solution: Linear time ($O(n)$)
|
||||
|
||||
```python
|
||||
def maximum_subarray_sum_linear(array: list[int]) -> int:
|
@ -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://thream.divlo.fr/) has been released! 🎉
|
||||
After months of hard work, [Thream v1.0.0](https://github.com/Thream) has been released! 🎉
|
||||
|
||||
[**Thream**](https://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://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,9 +31,9 @@ The source code is available on [GitHub](https://github.com/Thream).
|
||||
|
||||
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
|
||||
|
||||

|
||||

|
||||
|
||||
[**Thream**](https://thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://thream.divlo.fr/).
|
||||
[**Thream**](https://github.com/Thream) is a website that works on any recent browser.
|
||||
|
||||
## History
|
||||
|
||||
@ -31,9 +41,9 @@ The idea for the project has existed since May 13, 2020, symbolized by a [public
|
||||
|
||||
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.
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
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://thream.divlo.fr/).
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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/theoludwig/theoludwig/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'
|
||||
>
|
||||
Théo LUDWIG
|
||||
</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>
|
||||
)
|
||||
}
|
19
components/Footer/FooterText.tsx
Normal file
19
components/Footer/FooterText.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export const FooterText = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p>
|
||||
<Link
|
||||
href="/"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
>
|
||||
Théo LUDWIG
|
||||
</Link>{" "}
|
||||
| {i18n.translate("common.all-rights-reserved")}
|
||||
</p>
|
||||
)
|
||||
}
|
28
components/Footer/FooterVersion.tsx
Normal file
28
components/Footer/FooterVersion.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
interface FooterVersionProps {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
|
||||
}, [version])
|
||||
|
||||
return (
|
||||
<p className="mt-1">
|
||||
Version{" "}
|
||||
<a
|
||||
data-cy="version-link"
|
||||
className="font-semibold text-primary hover:underline dark:text-primary-dark"
|
||||
href={versionLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
15
components/Footer/index.tsx
Normal file
15
components/Footer/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { getVersion } from "@/utils/getVersion"
|
||||
|
||||
import { FooterText } from "./FooterText"
|
||||
import { FooterVersion } from "./FooterVersion"
|
||||
|
||||
export const Footer = async (): Promise<JSX.Element> => {
|
||||
const version = await getVersion()
|
||||
|
||||
return (
|
||||
<footer className="flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black">
|
||||
<FooterText />
|
||||
<FooterVersion version={version} />
|
||||
</footer>
|
||||
)
|
||||
}
|
@ -1,45 +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 = 'Théo LUDWIG',
|
||||
image = 'https://theoludwig.fr/images/icon-96x96.png',
|
||||
description = 'Théo LUDWIG - Developer Full Stack • Passionate about High-Tech',
|
||||
url = 'https://theoludwig.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-FR, en-US' />
|
||||
<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} />
|
||||
</NextHead>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export const Arrow: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
width='12'
|
||||
height='8'
|
||||
viewBox='0 0 12 8'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
className='fill-current text-black dark:text-white'
|
||||
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface LanguageFlagProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { language } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/languages/${language}.svg`}
|
||||
alt={language}
|
||||
/>
|
||||
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||
{language.toUpperCase()}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,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>
|
||||
)
|
||||
}
|
16
components/Header/Locales/Arrow.tsx
Normal file
16
components/Header/Locales/Arrow.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export const Arrow = (): JSX.Element => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="8"
|
||||
viewBox="0 0 12 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="fill-current text-black dark:text-white"
|
||||
d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
30
components/Header/Locales/LocaleFlag.tsx
Normal file
30
components/Header/Locales/LocaleFlag.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import type { CookiesStore } from "@/utils/constants"
|
||||
import { useI18n } from "@/i18n/i18n.client"
|
||||
|
||||
export interface LocaleFlagProps {
|
||||
locale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => {
|
||||
const { locale, cookiesStore } = props
|
||||
|
||||
const i18n = useI18n(cookiesStore)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/locales/${locale}.svg`}
|
||||
alt={locale}
|
||||
/>
|
||||
<p data-cy="locale-flag-text" className="mx-2 text-base">
|
||||
{i18n.translate(`common.${locale}`)}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
100
components/Header/Locales/index.tsx
Normal file
100
components/Header/Locales/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useCallback, useEffect, useState, useRef } from "react"
|
||||
import classNames from "clsx"
|
||||
|
||||
import type { Locale as LocaleType, CookiesStore } from "@/utils/constants"
|
||||
import { LOCALES } from "@/utils/constants"
|
||||
|
||||
import { Arrow } from "./Arrow"
|
||||
import { LocaleFlag } from "./LocaleFlag"
|
||||
|
||||
export interface LocalesProps {
|
||||
currentLocale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const Locales = (props: LocalesProps): JSX.Element => {
|
||||
const { currentLocale, cookiesStore } = props
|
||||
const pathname = usePathname()
|
||||
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleHiddenMenu = useCallback(() => {
|
||||
setHiddenMenu((oldHiddenMenu) => {
|
||||
return !oldHiddenMenu
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickEvent = (event: MouseEvent): void => {
|
||||
if (languageClickRef.current == null || event.target == null) {
|
||||
return
|
||||
}
|
||||
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||
setHiddenMenu(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener("click", handleClickEvent)
|
||||
|
||||
return () => {
|
||||
return window.removeEventListener("click", handleClickEvent)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
||||
const { setLocale } = await import("@/i18n/i18n.server")
|
||||
setLocale(locale)
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/blog")) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer flex-col items-center justify-center">
|
||||
<div
|
||||
ref={languageClickRef}
|
||||
data-cy="locale-click"
|
||||
className="mr-5 flex items-center"
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LocaleFlag
|
||||
locale={currentLocale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
<Arrow />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
data-cy="locales-list"
|
||||
className={classNames(
|
||||
"absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
|
||||
{ hidden: hiddenMenu },
|
||||
)}
|
||||
>
|
||||
{LOCALES.filter((locale) => {
|
||||
return locale !== currentLocale
|
||||
}).map((locale) => {
|
||||
return (
|
||||
<li
|
||||
key={locale}
|
||||
className="flex h-12 w-full items-center justify-center hover:bg-[#4f545c]/20"
|
||||
onClick={async () => {
|
||||
return await handleLocale(locale)
|
||||
}}
|
||||
>
|
||||
<LocaleFlag
|
||||
locale={locale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,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-px 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-px hidden size-px overflow-hidden border-0 p-0"
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,45 +1,51 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { cookies } from "next/headers"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Language } from './Language'
|
||||
import { SwitchTheme } from './SwitchTheme'
|
||||
import Logo from "@/public/images/logo.png"
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export interface HeaderProps {
|
||||
showLanguage?: boolean
|
||||
}
|
||||
import { Locales } from "./Locales"
|
||||
import { SwitchTheme } from "./SwitchTheme"
|
||||
|
||||
export const Header: React.FC<HeaderProps> = (props) => {
|
||||
const { showLanguage = false } = props
|
||||
export const Header = (): JSX.Element => {
|
||||
const cookiesStore = cookies()
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||
<Link href='/'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<header className="sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black">
|
||||
<h1>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-center transition-all duration-300 ease-in-out hover:scale-105"
|
||||
>
|
||||
<Image
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
src='/images/icon_small.png'
|
||||
alt='Théo LUDWIG'
|
||||
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'>
|
||||
<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>
|
||||
</Link>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex flex-col items-center justify-center px-6'>
|
||||
</Link>
|
||||
</h1>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col items-center justify-center px-6">
|
||||
<Link
|
||||
href='/blog'
|
||||
data-cy='header-blog-link'
|
||||
className='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 /> : null}
|
||||
<SwitchTheme />
|
||||
<Locales
|
||||
currentLocale={i18n.locale}
|
||||
cookiesStore={cookiesStore.toString()}
|
||||
/>
|
||||
<SwitchTheme cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
@ -1,22 +1,30 @@
|
||||
import htmlParser from 'html-react-parser'
|
||||
import htmlParser from "html-react-parser"
|
||||
import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGit } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
export const InterestsIcons = {
|
||||
code: faCode,
|
||||
"open-source": faGit,
|
||||
"high-tech": faMicrochip,
|
||||
} as const
|
||||
|
||||
export interface InterestParagraphProps {
|
||||
title: string
|
||||
description: string
|
||||
id: keyof typeof InterestsIcons
|
||||
}
|
||||
|
||||
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
export const InterestParagraph = (
|
||||
props: InterestParagraphProps,
|
||||
): JSX.Element => {
|
||||
const { title, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='my-6 text-center text-gray dark:text-gray-dark'>
|
||||
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
|
||||
{title}
|
||||
</strong>
|
||||
<br />
|
||||
<span>{htmlParser(description)}</span>
|
||||
</p>
|
||||
</>
|
||||
<div className="my-6 text-center text-gray dark:text-gray-dark">
|
||||
<h3 className="text-lg font-semibold text-primary dark:text-primary-dark">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="my-2">{htmlParser(description)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -1,24 +1,23 @@
|
||||
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'>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
return <InterestParagraph key={index} {...paragraph} />
|
||||
<div className="max-w-full">
|
||||
{paragraphs.map((paragraph) => {
|
||||
return <InterestParagraph key={paragraph.id} {...paragraph} />
|
||||
})}
|
||||
<InterestsList />
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { GitHubIcon } from 'components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
import { GitHubIcon } from "@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon"
|
||||
|
||||
export interface RepositoryProps {
|
||||
name: string
|
||||
@ -7,18 +7,22 @@ export interface RepositoryProps {
|
||||
href: string
|
||||
}
|
||||
|
||||
export const Repository: React.FC<RepositoryProps> = (props) => {
|
||||
export const Repository = (props: RepositoryProps): JSX.Element => {
|
||||
const { name, description, href } = props
|
||||
|
||||
return (
|
||||
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||
<div className='flex'>
|
||||
<GitHubIcon className='mr-2 h-6' />
|
||||
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
||||
</div>
|
||||
<p className='my-4'>{description}</p>
|
||||
<li>
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
<ShadowContainer className="relative !mb-4 max-h-32 cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.03]">
|
||||
<h3 className="flex">
|
||||
<GitHubIcon className="mr-2 h-6" />
|
||||
<span className="font-semibold text-primary dark:text-primary-dark">
|
||||
{name}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="my-4">{description}</p>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</ShadowContainer>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -1,35 +1,37 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
import { Repository } from './Repository'
|
||||
import { Repository } from "./Repository"
|
||||
|
||||
export const OpenSource: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const OpenSource = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<div className='mt-0 flex max-w-full flex-col items-center'>
|
||||
<p className='text-center'>{t('home:open-source.description')}</p>
|
||||
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
||||
<div className="mt-0 flex max-w-full flex-col items-center">
|
||||
<p className="text-center">
|
||||
{i18n.translate("home.open-source.description")}
|
||||
</p>
|
||||
<ul className="my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2">
|
||||
<Repository
|
||||
name='nodejs/node'
|
||||
description='Node.js JavaScript runtime 🐢🚀'
|
||||
href='https://github.com/nodejs/node/commits?author=theoludwig'
|
||||
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=theoludwig'
|
||||
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=theoludwig'
|
||||
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=theoludwig'
|
||||
name="vercel/next.js"
|
||||
description="The React Framework"
|
||||
href="https://github.com/vercel/next.js/commits?author=theoludwig"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import Image from "next/image"
|
||||
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
@ -9,35 +9,37 @@ export interface PortfolioItemProps {
|
||||
image: string
|
||||
}
|
||||
|
||||
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'>
|
||||
<li>
|
||||
<a
|
||||
className='group inline-flex justify-center'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="group inline-flex justify-center"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={link}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className='flex justify-center'>
|
||||
<Image
|
||||
quality={100}
|
||||
className='h-auto w-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'>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='my-6'>{description}</p>
|
||||
</div>
|
||||
<ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
quality={100}
|
||||
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-2xl font-semibold text-primary dark:text-primary-dark">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mx-4 my-6 font-semibold">{description}</p>
|
||||
</div>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</ShadowContainer>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -1,24 +1,21 @@
|
||||
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'>
|
||||
{items.map((item, index) => {
|
||||
return <PortfolioItem key={index} {...item} />
|
||||
<ul className="flex w-full flex-wrap justify-center px-3">
|
||||
{items.map((item) => {
|
||||
return <PortfolioItem key={item.title} {...item} />
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,21 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import htmlParser from "html-react-parser"
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t, lang } = useTranslation()
|
||||
import { getI18n } from "@/i18n/i18n.server"
|
||||
|
||||
export const ProfileDescriptionBottom = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p className='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>
|
||||
</>
|
||||
) : null}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +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 font-semibold text-yellow dark:text-yellow-dark'>
|
||||
<div className="mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400">
|
||||
<h1 className="mb-2 text-4xl font-semibold text-primary dark:text-primary-dark">
|
||||
Théo LUDWIG
|
||||
</h1>
|
||||
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||
<h2 className="mb-3 text-base">
|
||||
{i18n.translate("home.about.description")}
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -4,18 +4,18 @@ interface ProfileItemProps {
|
||||
link?: string
|
||||
}
|
||||
|
||||
export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
<li className='mb-3 before:table after:clear-both after:table'>
|
||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||
<li className="mb-3 before:table after:clear-both after:table">
|
||||
<strong className="float-left block w-28 text-sm font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</strong>
|
||||
<span className='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}
|
||||
|
@ -1,32 +1,54 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useMemo } from 'react'
|
||||
"use client"
|
||||
|
||||
import { BIRTH_DATE, BIRTH_DATE_STRING, 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(BIRTH_DATE)
|
||||
}, [])
|
||||
|
||||
const { isMounted } = useIsMounted()
|
||||
|
||||
return (
|
||||
<ul className='m-0 list-none p-0'>
|
||||
<ul className="m-0 list-none p-0">
|
||||
<ProfileItem
|
||||
title={t('home:about.pronouns')}
|
||||
value={t('home:about.pronouns-value')}
|
||||
title={i18n.translate("home.about.pronouns")}
|
||||
value={i18n.translate("home.about.pronouns-value")}
|
||||
/>
|
||||
<ProfileItem
|
||||
title={t('home:about.birth-date')}
|
||||
value={`${BIRTH_DATE_STRING} (${age} ${t('home:about.years-old')})`}
|
||||
title={i18n.translate("home.about.birth-date")}
|
||||
value={
|
||||
isMounted
|
||||
? `${BIRTH_DATE_STRING} (${age} ${i18n.translate(
|
||||
"home.about.years-old",
|
||||
)})`
|
||||
: BIRTH_DATE_STRING
|
||||
}
|
||||
/>
|
||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
value='contact@theoludwig.fr'
|
||||
link='mailto:contact@theoludwig.fr'
|
||||
title={i18n.translate("home.about.nationality")}
|
||||
value="Alsace, France"
|
||||
/>
|
||||
<ProfileItem
|
||||
title="Email"
|
||||
value="contact@theoludwig.fr"
|
||||
link="mailto:contact@theoludwig.fr"
|
||||
/>
|
||||
</ul>
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Image from 'next/image'
|
||||
import Image from "next/image"
|
||||
|
||||
import Logo from 'public/images/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={Logo} alt='Théo LUDWIG' priority />
|
||||
<div className="max-h-[370px] max-w-[370px] px-2 py-6">
|
||||
<Image quality={100} src={Logo} alt="Théo LUDWIG" priority />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const EmailIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Email</title>
|
||||
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' />
|
||||
<path d="M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitHubIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitHub</title>
|
||||
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitLabIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitLab</title>
|
||||
<path d='M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z' />
|
||||
<path d="M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import classNames from '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}
|
||||
>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>npm</title>
|
||||
<path d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z' />
|
||||
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitchIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitch</title>
|
||||
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' />
|
||||
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitterIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitter</title>
|
||||
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' />
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const YouTubeIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>YouTube</title>
|
||||
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' />
|
||||
<path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
@ -1,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 transition-all duration-300 ease-in-out hover:scale-110"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
@ -1,43 +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/theoludwig' 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/theoludwig' ariaLabel='GitLab'>
|
||||
<SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab">
|
||||
<GitLabIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='NPM'>
|
||||
<SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm">
|
||||
<NPMIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem
|
||||
link='https://twitter.com/theoludwig_'
|
||||
ariaLabel='Twitter'
|
||||
link="https://twitter.com/theoludwig_"
|
||||
ariaLabel="Twitter"
|
||||
>
|
||||
<TwitterIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem
|
||||
link='https://www.youtube.com/@theo_ludwig'
|
||||
ariaLabel='YouTube'
|
||||
link="https://www.youtube.com/@theo_ludwig"
|
||||
ariaLabel="YouTube"
|
||||
>
|
||||
<YouTubeIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem
|
||||
link='https://www.twitch.tv/theoludwig'
|
||||
ariaLabel='Twitch'
|
||||
link="https://www.twitch.tv/theoludwig"
|
||||
ariaLabel="Twitch"
|
||||
>
|
||||
<TwitchIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'>
|
||||
<SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email">
|
||||
<EmailIcon />
|
||||
</SocialMediaItem>
|
||||
</ul>
|
||||
|
@ -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>
|
||||
|
@ -1,47 +1,49 @@
|
||||
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'
|
||||
>
|
||||
<div className='text-center'>
|
||||
<li>
|
||||
<a
|
||||
href={skillProperties.link}
|
||||
className="mx-2 flex max-w-xl flex-col items-center justify-center text-center text-primary hover:underline dark:text-primary-dark"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className='inline h-16 w-16'
|
||||
className="inline size-16"
|
||||
quality={100}
|
||||
width={64}
|
||||
height={64}
|
||||
alt={skill}
|
||||
src={image}
|
||||
src={getImage()}
|
||||
/>
|
||||
<p className='mt-1'>{skill}</p>
|
||||
</div>
|
||||
</a>
|
||||
<p className="mt-1 font-semibold">{skill}</p>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
<ul className="flex flex-wrap justify-around">{children}</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,40 +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='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='Frontend'>
|
||||
<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='Backend'>
|
||||
<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='Arch Linux' />
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
|
@ -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/VisualStudioCode.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'
|
||||
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",
|
||||
},
|
||||
'Arch Linux': {
|
||||
link: 'https://archlinux.org/',
|
||||
image: '/images/skills/ArchLinux.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'
|
||||
"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
|
||||
|
28
components/design/Loader.tsx
Normal file
28
components/design/Loader.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ShadowContainer } from '../ShadowContainer'
|
||||
import { SectionHeading } from './SectionHeading'
|
||||
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||
import { SectionHeading } from "@/components/design/Section/SectionHeading"
|
||||
|
||||
type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
||||
type SectionProps = React.ComponentPropsWithRef<"section"> & {
|
||||
heading?: string
|
||||
description?: string
|
||||
isMain?: boolean
|
||||
withoutShadowContainer?: boolean
|
||||
}
|
||||
|
||||
export const Section: React.FC<SectionProps> = (props) => {
|
||||
export const Section = (props: SectionProps): JSX.Element => {
|
||||
const {
|
||||
children,
|
||||
heading,
|
||||
@ -20,13 +20,13 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
|
||||
if (isMain) {
|
||||
return (
|
||||
<div className='w-full px-3'>
|
||||
<div className="w-full px-3">
|
||||
<ShadowContainer style={{ marginTop: 50 }}>
|
||||
<section {...rest}>
|
||||
{heading != null ? (
|
||||
<SectionHeading>{heading}</SectionHeading>
|
||||
) : null}
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
<div className="w-full px-3">{children}</div>
|
||||
</section>
|
||||
</ShadowContainer>
|
||||
</div>
|
||||
@ -37,7 +37,7 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
return (
|
||||
<section {...rest}>
|
||||
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
<div className="w-full px-3">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -52,13 +52,13 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
</SectionHeading>
|
||||
) : null}
|
||||
{description != null ? (
|
||||
<p style={{ marginTop: 7 }} className='text-center'>
|
||||
<p style={{ marginTop: 7 }} className="text-center">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
<div className='w-full px-3'>
|
||||
<ShadowContainer>
|
||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||
<div className="w-full px-3">
|
||||
<ShadowContainer className="w-full px-2 py-4 leading-8 sm:px-16">
|
||||
{children}
|
||||
</ShadowContainer>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import classNames from 'clsx'
|
||||
import classNames from "clsx"
|
||||
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<"div">
|
||||
|
||||
export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
|
||||
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ',
|
||||
className
|
||||
"mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
|
@ -1,11 +1,12 @@
|
||||
services:
|
||||
theoludwig:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
image: 'theoludwig'
|
||||
image: "theoludwig"
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: './'
|
||||
context: "./"
|
||||
ports:
|
||||
- '${PORT-3000}:${PORT-3000}'
|
||||
- "${PORT-3000}:${PORT-3000}"
|
||||
environment:
|
||||
PORT: ${PORT-3000}
|
||||
env_file: '.env'
|
||||
env_file: ".env"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user