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

Compare commits

...

76 Commits

Author SHA1 Message Date
117c41b1c3 chore(release): 2.12.0 [skip ci] 2023-07-02 14:59:12 +00:00
b92704b77d feat: increase duration work experience Numerize 2023-07-02 16:50:32 +02:00
bab7581283 fix: update dependencies to latest 2023-07-02 16:42:39 +02:00
988fceb2aa chore(release): 2.11.0 [skip ci] 2023-06-18 10:23:59 +00:00
5211ba1489 feat(skills): add Arch Linux 2023-06-18 12:18:24 +02:00
6886480cef chore(release): 2.10.0 [skip ci] 2023-06-16 21:30:51 +00:00
d78e50638e test: fix with new anchor link behavior in blog posts 2023-06-16 23:26:09 +02:00
3b76195d71 feat(blog): add anchor links for titles/headers 2023-06-16 23:14:25 +02:00
2dc63ba933 chore: maintenance 2023-06-16 22:56:53 +02:00
336f067c52 fix(posts): add explanations for Git (cherry-pick + merge squash) 2023-06-16 22:36:20 +02:00
5fd7f77b6d fix: justify align text in blog posts 2023-06-16 21:48:47 +02:00
db0c708c04 chore(release): 2.9.0 [skip ci] 2023-05-31 18:39:59 +00:00
9d44671fed feat: continue migrating to full name instead of nickname 2023-05-31 20:09:08 +02:00
7bcc5f972c chore(release): 2.8.0 [skip ci] 2023-05-30 19:57:02 +00:00
61172d59e3 feat: migrate progressively to full name instead of nickname 2023-05-30 21:51:27 +02:00
7c0f11ab7d chore(release): 2.7.3 [skip ci] 2023-05-29 15:45:42 +00:00
670897fa78 fix: improve spelling consistency 2023-05-29 17:44:26 +02:00
b88246b668 chore: usage of next start 2023-05-29 17:33:45 +02:00
87fbfe4940 chore(release): 2.7.2 [skip ci] 2023-05-29 15:29:07 +00:00
271aa60247 test: update with new changes 2023-05-29 17:26:06 +02:00
ba34e314c9 fix: update name with full name and nickname 2023-05-29 17:10:14 +02:00
f41bc644b1 fix(deps): remove next-pwa dependency 2023-05-29 16:24:49 +02:00
a18cec4826 chore(release): 2.7.1 [skip ci] 2023-05-21 16:27:24 +00:00
61e589f0f4 fix: responsive on blog post with code blocks and katex 2023-05-21 18:21:46 +02:00
dc5c3cee41 chore(release): 2.7.0 [skip ci] 2023-05-21 12:49:17 +00:00
20cb0c21d5 feat(posts): add programming-challenges 2023-05-21 14:42:53 +02:00
e5232c1394 build(deps): update latest 2023-05-21 12:15:08 +02:00
fd51609713 chore(release): 2.6.1 [skip ci] 2023-05-13 17:15:54 +00:00
edf16c2562 fix(deps): update latest 2023-05-13 19:09:54 +02:00
94e0d190ae chore(release): 2.6.0 [skip ci] 2023-05-10 18:12:22 +00:00
b1cf7f8517 chore: remove unneeded Lighthouse checking 2023-05-09 23:22:33 +02:00
a1a715d3b9 feat: add Numerize as work experience 2023-05-09 23:06:10 +02:00
eede46fb41 build(deps): update latest 2023-05-09 22:56:42 +02:00
e32c53caa1 chore(release): 2.5.6 [skip ci] 2023-04-02 21:18:14 +00:00
361ea37deb chore: fix CI issues 2023-04-02 23:16:51 +02:00
d49a8a7470 fix: update dependencies to latest 2023-04-02 22:44:09 +02:00
a4996c8251 chore: remove useless runner-dependencies in Dockerfile 2023-01-11 17:42:29 +01:00
b25451e631 chore(release): 2.5.5 [skip ci] 2023-01-10 23:05:24 +00:00
042a861f58 fix: update dependencies to latest 2023-01-10 23:56:46 +01:00
d76db36dbc chore(release): 2.5.4 [skip ci] 2022-12-08 08:54:03 +00:00
99d9dcf334 fix: improve Resume 2022-12-08 09:52:39 +01:00
ece5ded1b4 chore(release): 2.5.3 [skip ci] 2022-11-29 09:33:10 +00:00
1514600998 fix: improve Resume 2022-11-29 10:29:02 +01:00
5f5b328895 chore(release): 2.5.2 [skip ci] 2022-11-19 19:43:26 +00:00
c88887a322 fix: better resume 2022-11-19 20:24:13 +01:00
014044573a chore(release): 2.5.1 [skip ci] 2022-11-10 11:35:11 +00:00
df009c3f7b fix(posts): update broken link in thream-v1.0.0.md 2022-11-10 12:31:48 +01:00
5c85ca2ef1 chore: fix cypress unit tests 2022-11-08 11:00:31 +01:00
07f7942496 chore(release): 2.5.0 [skip ci] 2022-10-27 17:24:30 +00:00
213a3fa182 build(deps): bump Next.js to v13 2022-10-27 19:13:29 +02:00
28d9211583 fix(posts): update git-ultimate-guide 2022-10-23 20:15:07 +02:00
4d085cb148 fix: update biography description 2022-10-23 18:38:37 +02:00
e6c583f2cd ci: fix timeout 2022-10-20 23:57:53 +02:00
232b54588a feat(skills): add PHP and Laravel 2022-10-20 22:44:40 +02:00
c419fb3bb4 chore: remove usage of styled-jsx 2022-10-20 22:44:32 +02:00
03e7e22d74 chore: reduce docker image size 2022-10-20 22:44:32 +02:00
e85c241ed1 feat(posts): add git-ultimate-guide 2022-10-20 22:43:25 +02:00
c1877297f8 refactor: minor changes 2022-08-27 02:30:55 +02:00
83231197dd chore(release): 2.4.1 [skip ci] 2022-08-23 11:33:38 +00:00
a2fe2205bc fix(resume): wrong base path for assets 2022-08-23 13:31:17 +02:00
e1f3dceb07 chore(release): 2.4.0 [skip ci] 2022-08-23 10:33:09 +00:00
0f89fee52f feat: add giscus comments system for blog posts 2022-08-23 12:23:31 +02:00
2fcc7ac384 chore(release): 2.3.2 [skip ci] 2022-07-28 21:06:12 +00:00
9351edf626 chore: use the right resume.json 2022-07-28 23:01:19 +02:00
1f4aa54211 chore: remove jest -> cypress for unit tests 2022-07-28 22:51:12 +02:00
8bc1471cbb chore: easier development for jsonresume-theme-custom thanks to vite 2022-07-28 21:20:41 +02:00
1ebdab18a5 fix: update about, now second year of university 2022-07-23 23:00:58 +02:00
b9b76e839a build(deps): update latest 2022-07-01 23:12:47 +02:00
bc065a2e19 chore(release): 2.3.1 [skip ci] 2022-05-03 08:12:15 +00:00
5d3a287b27 fix(resume): wrong dates 2022-05-03 10:05:11 +02:00
fb689c9bc1 chore(release): 2.3.0 [skip ci] 2022-04-11 10:35:55 +00:00
2c3a70df2a feat(posts): add thream-v1-0-0 2022-04-11 12:31:19 +02:00
bce254a355 chore(release): 2.2.1 [skip ci] 2022-03-24 18:00:10 +00:00
f67d331416 fix: calculate age client side so it updates "automatically" (not only on rebuild) 2022-03-24 18:57:27 +01:00
6abc881e94 chore(release): 2.2.0 [skip ci] 2022-03-24 10:49:45 +00:00
a67d6665ea feat: display age nearby the birth date 2022-03-24 11:45:19 +01:00
141 changed files with 15379 additions and 36909 deletions

View File

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

View File

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

View File

@ -1,5 +1,3 @@
version: '3.0'
services:
workspace:
build:

View File

@ -1,12 +1,5 @@
.vscode
.git
.env
.*
!.npmrc
build
.next
coverage
node_modules
tmp
temp
.DS_Store
.lighthouseci
.vercel

View File

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

View File

@ -1,8 +0,0 @@
.next
.lighthouseci
storybook-static
coverage
node_modules
next-env.d.ts
**/workbox-*.js
**/sw.js

View File

@ -4,14 +4,7 @@
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"node": true,
"browser": true,
"jest": true
},
"rules": {
"prettier/prettier": "error",
"unicorn/prefer-node-protocol": "off",
"@typescript-eslint/no-misused-promises": "off"
"prettier/prettier": "error"
}
}

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
## What changes this PR introduce?
# What changes this PR introduce?
## List any relevant issue numbers

View File

@ -16,12 +16,12 @@ jobs:
language: ['javascript']
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
- name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@v1'
uses: 'github/codeql-action/init@v2'
with:
languages: ${{ matrix.language }}
- name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@v1'
uses: 'github/codeql-action/analyze@v2'

View File

@ -10,16 +10,16 @@ jobs:
build:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
with:
node-version: '16.x'
node-version: '18.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Install dependencies'
run: 'npm clean-install'
- name: 'Build'
run: 'npm run build'

View File

@ -10,16 +10,16 @@ jobs:
lint:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
with:
node-version: '16.x'
node-version: '18.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Install dependencies'
run: 'npm clean-install'
- name: 'lint:commit'
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
@ -30,8 +30,8 @@ jobs:
- name: 'lint:markdown'
run: 'npm run lint:markdown'
- name: 'lint:typescript'
run: 'npm run lint:typescript'
- name: 'lint:eslint'
run: 'npm run lint:eslint'
- name: 'lint:prettier'
run: 'npm run lint:prettier'
@ -40,8 +40,3 @@ jobs:
uses: 'dotenv-linter/action-dotenv-linter@v2'
with:
github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -8,26 +8,26 @@ jobs:
release:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
with:
fetch-depth: 0
persist-credentials: false
- name: 'Import GPG key'
uses: 'crazy-max/ghaction-import-gpg@v4'
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
with:
node-version: '16.x'
node-version: '18.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Install dependencies'
run: 'npm clean-install'
- name: 'Release'
run: 'npm run release'

View File

@ -10,33 +10,33 @@ jobs:
test-unit:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
with:
node-version: '16.x'
node-version: '18.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Install dependencies'
run: 'npm clean-install'
- name: 'Unit Test'
run: 'npm run test:unit'
test-lighthouse:
test-e2e:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
with:
node-version: '16.x'
node-version: '18.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Install dependencies'
run: 'npm clean-install'
- name: 'Build'
run: 'npm run build'
@ -44,27 +44,5 @@ jobs:
- name: 'html-w3c-validator'
run: 'npm run test:html-w3c-validator'
- name: 'Lighthouse'
run: 'npm run test:lighthouse'
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
test-e2e:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
with:
node-version: '16.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Build'
run: 'npm run build'
- name: 'End To End (e2e) Test'
run: 'npm run test:e2e'

6
.gitignore vendored
View File

@ -11,7 +11,7 @@ out
# production
build
dist
public/*.html
public/curriculum-vitae
# PWA
public/workbox-*.js
public/sw.js
@ -49,3 +49,7 @@ npm-debug.log*
.DS_Store
.lighthouseci
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

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

View File

@ -1,30 +0,0 @@
{
"ci": {
"collect": {
"startServerCommand": "npm run start",
"startServerReadyPattern": "ready on",
"startServerReadyTimeout": 20000,
"url": [
"http://localhost:3000/",
"http://localhost:3000/blog",
"http://localhost:3000/blog/hello-world"
],
"numberOfRuns": 1
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"csp-xss": "warning",
"non-composited-animations": "warning",
"unused-javascript": "warning",
"image-size-responsive": "warning",
"unsized-images": "warning",
"color-contrast": "warning"
}
},
"upload": {
"target": "temporary-public-storage"
},
"server": {}
}
}

View File

@ -1,11 +1,6 @@
{
"*": ["editorconfig-checker"],
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix",
"jest --findRelatedTests"
],
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
"resume.json": ["resume validate"]
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
}

12
.markdownlint-cli2.jsonc Normal file
View File

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

View File

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

View File

@ -1,9 +0,0 @@
.next
.lighthouseci
storybook-static
coverage
node_modules
next-env.d.ts
**/workbox-*.js
**/sw.js
*.hbs

View File

@ -3,8 +3,6 @@
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax",
"divlo.vscode-styled-jsx-languageserver",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"davidanson.vscode-markdownlint",

View File

@ -6,5 +6,9 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
},
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
}

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@divlo.fr.
<contact@theoludwig.fr>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@ -1,6 +1,10 @@
# 💡 Contributing
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
Thanks a lot for your interest in contributing to **theoludwig.fr**! 🎉
## Code of Conduct
**theoludwig.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
## Types of contributions
@ -11,43 +15,21 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
## Pull Requests
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
- **Please first discuss** the change you wish to make via [issue](https://github.com/theoludwig/theoludwig/issues) before making a change. It might avoid a waste of your time.
- Ensure your code respect linting.
- Make sure your **code passes the tests**.
If you're adding new features to **divlo.fr**, please include tests.
If you're adding new features to **theoludwig.fr**, please include tests.
## Commits
The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
## Getting Started
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Divlo/Divlo)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
### Prerequisites
@ -58,10 +40,10 @@ Scopes define what part of the code changed.
```sh
# Clone the repository
git clone https://github.com/Divlo/Divlo.git
git clone git@github.com:theoludwig/theoludwig.git
# Go to the project root
cd Divlo
cd theoludwig
# Configure environment variables
cp .env.example .env
@ -81,9 +63,9 @@ npm run dev
```sh
# Setup and run all the services for you
docker-compose up --build
docker compose up --build
```
### Services started
- website : `http://localhost:3000`
- `website`: <http://127.0.0.1:3000>

View File

@ -1,23 +1,21 @@
FROM node:16.14.0 AS dependencies
WORKDIR /usr/src/app
FROM node:18.16.1 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm install
RUN npm clean-install
FROM node:16.14.0 AS builder
WORKDIR /usr/src/app
FROM node:18.16.1 AS builder
WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
RUN npm run build
FROM node:16.14.0 AS runner
WORKDIR /usr/src/app
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
WORKDIR /usr/src/application
ENV NODE_ENV=production
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder /usr/src/app/.next ./.next
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
COPY --from=builder /usr/src/app/locales ./locales
COPY --from=builder /usr/src/app/pages ./pages
COPY --from=builder /usr/src/app/node_modules ./node_modules
RUN npx next telemetry disable
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
ENV 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
CMD ["./server.js"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) Divlo
Copyright (c) Théo LUDWIG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,18 +1,18 @@
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center">
<strong>Developer Full Stack Junior • Passionate about High-Tech</strong>
<strong>Developer Full Stack • Open-Source enthusiast</strong>
</p>
<p align="center">
<a href="https://github.com/Divlo"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/Divlo"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~divlo"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/Divlo_FR"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
<a href="https://www.youtube.com/channel/UCfEKQzI3c8vmZOrsTOi5spA"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/divlo"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://www.divlo.fr"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
<a href="mailto:contact@divlo.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@divlo.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
<a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
</p>
<hr />
@ -21,20 +21,16 @@
```json
{
"name": "Divlo",
"name": "Théo LUDWIG",
"pronouns": "He/Him",
"birthDate": "31/03/2003",
"nationality": "Alsace, France",
"interests": [
"Developer Full Stack Junior",
"Passionate about High-Tech",
"Open-Source enthusiast"
],
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
"skills": {
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
"frontend": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"]
}
}
```
@ -44,6 +40,6 @@
## 📈 Statistics
<p align=center>
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=Divlo&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
</p>

View File

@ -1,53 +1,45 @@
import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link'
export interface ErrorPageProps {
import type { FooterProps } from './Footer'
import { Footer } from './Footer'
import { Header } from './Header'
export interface ErrorPageProps extends FooterProps {
statusCode: number
message: string
}
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
const { message, statusCode } = props
const { message, statusCode, version } = props
const { t } = useTranslation()
return (
<>
<h1 className='my-6 text-4xl font-semibold'>
{t('errors:error')}{' '}
<span
className='text-yellow dark:text-yellow-dark'
data-cy='status-code'
>
{statusCode}
</span>
</h1>
<p className='text-center text-lg'>
{message}{' '}
<Link href='/'>
<a className='text-yellow hover:underline dark:text-yellow-dark'>
{t('errors:return-to-home-page')}
</a>
</Link>
</p>
<style jsx global>
{`
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-width: 100vw;
flex: 1;
}
#__next {
display: flex;
flex-direction: column;
padding-top: 0;
height: 100vh;
}
`}
</style>
<div className='flex h-screen flex-col pt-0'>
<Header showLanguage />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
<h1 className='my-6 text-4xl font-semibold'>
{t('errors:error')}{' '}
<span
className='text-yellow dark:text-yellow-dark'
data-cy='status-code'
>
{statusCode}
</span>
</h1>
<p className='text-center text-lg'>
{message}{' '}
<Link
href='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
{t('errors:return-to-home-page')}
</Link>
</p>
</main>
<Footer version={version} />
</div>
</>
)
}

View File

@ -11,22 +11,24 @@ export const Footer: React.FC<FooterProps> = (props) => {
const { version } = props
const versionLink = useMemo(() => {
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
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='/'>
<a className='text-yellow hover:underline dark:text-yellow-dark'>
Divlo
</a>
<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'

View File

@ -3,16 +3,16 @@ import NextHead from 'next/head'
interface HeadProps {
title?: string
image?: string
description: string
description?: string
url?: string
}
export const Head: React.FC<HeadProps> = (props) => {
const {
title = 'Divlo',
image = 'https://divlo.fr/images/icons/icon-96x96.png',
description,
url = 'https://divlo.fr/'
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 (
@ -23,7 +23,7 @@ export const Head: React.FC<HeadProps> = (props) => {
{/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content={description} />
<meta name='Language' content='fr, en' />
<meta name='Language' content='fr-FR, en-US' />
<meta name='theme-color' content='#ffd800' />
{/* Open Graph Metadata */}
@ -32,7 +32,7 @@ export const Head: React.FC<HeadProps> = (props) => {
<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:locale' content='fr-FR, en-US' />
<meta property='og:site_name' content={title} />
{/* Twitter card Metadata */}
@ -40,18 +40,6 @@ export const Head: React.FC<HeadProps> = (props) => {
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
<meta name='twitter:image' content={image} />
{/* Google Verification */}
<meta
name='google-site-verification'
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
/>
{/* PWA Data */}
<link rel='manifest' href='/manifest.json' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='mobile-web-app-capable' content='yes' />
<link rel='apple-touch-icon' href={image} />
</NextHead>
)
}

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useRef } from 'react'
import useTranslation from 'next-translate/useTranslation'
import setLanguage from 'next-translate/setLanguage'
import classNames from 'classnames'
import classNames from 'clsx'
import i18n from 'i18n.json'
@ -11,31 +11,39 @@ 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(!hiddenMenu)
}, [hiddenMenu])
setHiddenMenu((oldHiddenMenu) => {
return !oldHiddenMenu
})
}, [])
useEffect(() => {
if (!hiddenMenu) {
window.document.addEventListener('click', handleHiddenMenu)
} else {
window.document.removeEventListener('click', handleHiddenMenu)
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 () => {
window.document.removeEventListener('click', handleHiddenMenu)
return window.removeEventListener('click', handleClickEvent)
}
}, [hiddenMenu, handleHiddenMenu])
}, [])
const handleLanguage = async (language: string): Promise<void> => {
await setLanguage(language)
handleHiddenMenu()
}
return (
<div className='flex cursor-pointer flex-col items-center justify-center'>
<div
ref={languageClickRef}
data-cy='language-click'
className='mr-5 flex items-center'
onClick={handleHiddenMenu}
@ -47,7 +55,7 @@ export const Language: React.FC = () => {
<ul
data-cy='languages-list'
className={classNames(
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
'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 }
)}
>
@ -59,7 +67,9 @@ export const Language: React.FC = () => {
<li
key={index}
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
onClick={async () => await handleLanguage(language)}
onClick={async () => {
return await handleLanguage(language)
}}
>
<LanguageFlag language={language} />
</li>

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import classNames from 'clsx'
import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => {
@ -18,109 +19,60 @@ export const SwitchTheme: React.FC = () => {
}
return (
<>
<div
className='flex items-center'
data-cy='switch-theme-click'
onClick={handleClick}
>
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
<div className='toggle-track'>
<div
data-cy='switch-theme-dark'
className='toggle-track-check absolute'
>
<span className='toggle_Dark relative flex items-center justify-center'>
🌜
</span>
</div>
<div
data-cy='switch-theme-light'
className='toggle-track-x absolute'
>
<span className='toggle_Light relative flex items-center justify-center'>
🌞
</span>
</div>
<div
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
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',
{
'opacity-100': theme === 'dark',
'opacity-0': theme === 'light'
}
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌜
</span>
</div>
<div
data-cy='switch-theme-light'
className={classNames(
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
{
'opacity-100': theme === 'light',
'opacity-0': theme === 'dark'
}
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌞
</span>
</div>
<div className='toggle-thumb absolute' />
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='toggle-screenreader-only absolute overflow-hidden'
defaultChecked
/>
</div>
<div
className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
{
'left-[27px]': theme === 'dark',
'left-0': theme === 'light'
}
)}
style={{ border: '1px solid #4d4d4d' }}
/>
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
defaultChecked
/>
</div>
<style jsx>
{`
.toggle-theme-button {
touch-action: pan-x;
border: 0;
padding: 0;
user-select: none;
}
.toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: #4d4d4d;
transition: all 0.2s ease;
color: #fff;
}
.toggle-track-check {
width: 14px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
left: 8px;
opacity: ${theme === 'dark' ? 1 : 0};
transition: opacity 0.25s ease;
}
.toggle-track-x {
width: 10px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
right: 10px;
opacity: ${theme === 'dark' ? 0 : 1};
}
.toggle_Dark,
.toggle_Light {
height: 10px;
width: 10px;
}
.toggle-thumb {
left: ${theme === 'dark' ? '27px' : '0px'};
width: 22px;
height: 22px;
border: 1px solid #4d4d4d;
border-radius: 50%;
background-color: #fafafa;
box-sizing: border-box;
transition: all 0.25s ease;
top: 1px;
color: #fff;
}
.toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
padding: 0;
width: 1px;
}
`}
</style>
</>
</div>
)
}

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react'
import { Header } from '..'
describe('<Header />', () => {
it('should render', () => {
const { getByText } = render(<Header />)
expect(getByText('Divlo')).toBeInTheDocument()
})
})

View File

@ -14,33 +14,31 @@ export const Header: React.FC<HeaderProps> = (props) => {
return (
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
<Link href='/'>
<a>
<div className='flex items-center justify-center'>
<Image
quality={100}
width={60}
height={60}
src='/images/divlo_icon_small.png'
alt='Divlo'
/>
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
Divlo
</strong>
</div>
</a>
<div className='flex items-center justify-center'>
<Image
quality={100}
width={60}
height={60}
src='/images/icon_small.png'
alt='Théo LUDWIG'
priority
/>
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
Théo LUDWIG
</strong>
</div>
</Link>
<div className='flex justify-between'>
<div className='flex flex-col items-center justify-center px-6'>
<Link href='/blog'>
<a
data-cy='header-blog-link'
className='text-yellow hover:underline dark:text-yellow-dark'
>
Blog
</a>
<Link
href='/blog'
data-cy='header-blog-link'
className='text-yellow hover:underline dark:text-yellow-dark'
>
Blog
</Link>
</div>
{showLanguage && <Language />}
{showLanguage ? <Language /> : null}
<SwitchTheme />
</div>
</header>

View File

@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
interface InterestItemProps {
title: string
@ -10,7 +10,7 @@ export const InterestItem: React.FC<InterestItemProps> = (props) => {
const { fontAwesomeIcon, title } = props
return (
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}>
<FontAwesomeIcon
className='block h-full w-full text-yellow dark:text-yellow-dark'
icon={fontAwesomeIcon}

View File

@ -7,10 +7,7 @@ export const InterestsList: React.FC = () => {
return (
<div className='my-4 flex justify-center'>
<ul className='m-0 flex w-96 list-none justify-around p-0'>
<InterestItem
title='Developer Full Stack Junior'
fontAwesomeIcon={faCode}
/>
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
<InterestItem
title='Passionate about High-Tech'
fontAwesomeIcon={faMicrochip}

View File

@ -1,6 +1,7 @@
import useTranslation from 'next-translate/useTranslation'
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
import type { InterestParagraphProps } from './InterestParagraph'
import { InterestParagraph } from './InterestParagraph'
import { InterestsList } from './InterestsList'
export const Interests: React.FC = () => {

View File

@ -12,22 +12,22 @@ export const OpenSource: React.FC = () => {
<Repository
name='nodejs/node'
description='Node.js JavaScript runtime 🐢🚀'
href='https://github.com/nodejs/node/commits?author=Divlo'
href='https://github.com/nodejs/node/commits?author=theoludwig'
/>
<Repository
name='standard/standard'
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
href='https://github.com/standard/standard/commits?author=Divlo'
href='https://github.com/standard/standard/commits?author=theoludwig'
/>
<Repository
name='nrwl/nx'
description='Smart, Extensible Build Framework'
href='https://github.com/nrwl/nx/commits?author=Divlo'
href='https://github.com/nrwl/nx/commits?author=theoludwig'
/>
<Repository
name='vercel/next.js'
description='The React Framework for Production'
href='https://github.com/vercel/next.js/commits?author=Divlo'
href='https://github.com/vercel/next.js/commits?author=theoludwig'
/>
</div>
</div>

View File

@ -24,7 +24,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
<div className='flex justify-center'>
<Image
quality={100}
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
width={300}
height={300}
src={image}

View File

@ -1,6 +1,7 @@
import useTranslation from 'next-translate/useTranslation'
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
import type { PortfolioItemProps } from './PortfolioItem'
import { PortfolioItem } from './PortfolioItem'
export const Portfolio: React.FC = () => {
const { t } = useTranslation('home')

View File

@ -4,9 +4,9 @@ export const ProfileDescriptionBottom: React.FC = () => {
const { t, lang } = useTranslation()
return (
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
{t('home:about.description-bottom')}
{lang === 'fr' && (
{lang === 'fr' ? (
<>
<br />
<br />
@ -17,7 +17,7 @@ export const ProfileDescriptionBottom: React.FC = () => {
Curriculum vitæ
</a>
</>
)}
) : null}
</p>
)
}

View File

@ -5,11 +5,8 @@ export const ProfileInformation: React.FC = () => {
return (
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
<h1 className='mb-2 text-4xl'>
{t('home:about.i-am')}{' '}
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
Divlo
</strong>
<h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
Théo LUDWIG
</h1>
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
</div>

View File

@ -12,7 +12,7 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
{title}
</strong>
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
{link != null ? (
<a
className='text-gray hover:underline dark:text-gray-dark'

View File

@ -1,19 +1,32 @@
import useTranslation from 'next-translate/useTranslation'
import { useMemo } from 'react'
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from 'utils/getAge'
import { ProfileItem } from './ProfileItem'
export const ProfileList: React.FC = () => {
const { t } = useTranslation('home')
const age = useMemo(() => {
return getAge(BIRTH_DATE)
}, [])
return (
<ul className='m-0 list-none p-0'>
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
<ProfileItem title={t('home:about.birth-date')} value='31/03/2003' />
<ProfileItem
title={t('home:about.pronouns')}
value={t('home:about.pronouns-value')}
/>
<ProfileItem
title={t('home:about.birth-date')}
value={`${BIRTH_DATE_STRING} (${age} ${t('home:about.years-old')})`}
/>
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
<ProfileItem
title='Email'
value='contact@divlo.fr'
link='mailto:contact@divlo.fr'
value='contact@theoludwig.fr'
link='mailto:contact@theoludwig.fr'
/>
</ul>
)

View File

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

View File

@ -1,4 +1,4 @@
import classNames from 'classnames'
import classNames from 'clsx'
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
const { children, className, ...rest } = props

View File

@ -3,7 +3,9 @@ interface SocialMediaItemProps {
ariaLabel: string
}
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
export const SocialMediaItem: React.FC<
React.PropsWithChildren<SocialMediaItemProps>
> = (props) => {
const { link, ariaLabel, children } = props
return (

View File

@ -10,28 +10,34 @@ import { NPMIcon } from './SocialMediaIcons/NPMIcon'
export const SocialMediaList: React.FC = () => {
return (
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
<SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'>
<GitHubIcon />
</SocialMediaItem>
<SocialMediaItem link='https://gitlab.com/Divlo' ariaLabel='GitLab'>
<SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'>
<GitLabIcon />
</SocialMediaItem>
<SocialMediaItem link='https://www.npmjs.com/~divlo' ariaLabel='NPM'>
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='NPM'>
<NPMIcon />
</SocialMediaItem>
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
<SocialMediaItem
link='https://twitter.com/theoludwig_'
ariaLabel='Twitter'
>
<TwitterIcon />
</SocialMediaItem>
<SocialMediaItem
link='https://www.youtube.com/c/Divlo'
link='https://www.youtube.com/@theo_ludwig'
ariaLabel='YouTube'
>
<YouTubeIcon />
</SocialMediaItem>
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
<SocialMediaItem
link='https://www.twitch.tv/theoludwig'
ariaLabel='Twitch'
>
<TwitchIcon />
</SocialMediaItem>
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'>
<EmailIcon />
</SocialMediaItem>
</ul>

View File

@ -2,10 +2,11 @@ import { useTheme } from 'next-themes'
import Image from 'next/image'
import { useMemo } from 'react'
import type { SkillName } from './skills'
import { skills } from './skills'
export interface SkillComponentProps {
skill: string
skill: SkillName
}
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
@ -14,10 +15,13 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
const { theme } = useTheme()
const image = useMemo(() => {
if (typeof skillProperties.image !== 'string') {
return skillProperties.image[theme ?? 'light']
if (typeof skillProperties.image === 'string') {
return skillProperties.image
}
return skillProperties.image
if (theme === 'light') {
return skillProperties.image.light
}
return skillProperties.image.dark
}, [skillProperties, theme])
return (
@ -28,7 +32,14 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
rel='noopener noreferrer'
>
<div className='text-center'>
<Image quality={100} width={60} height={60} alt={skill} src={image} />
<Image
className='inline h-16 w-16'
quality={100}
width={64}
height={64}
alt={skill}
src={image}
/>
<p className='mt-1'>{skill}</p>
</div>
</a>

View File

@ -9,30 +9,29 @@ export const Skills: React.FC = () => {
return (
<>
<SkillsSection title={t('home:skills.languages')}>
<SkillComponent skill='JavaScript' />
<SkillComponent skill='TypeScript' />
<SkillComponent skill='Python' />
<SkillComponent skill='C/C++' />
<SkillComponent skill='PHP' />
</SkillsSection>
<SkillsSection title='Front-end'>
<SkillsSection title='Frontend'>
<SkillComponent skill='HTML' />
<SkillComponent skill='CSS' />
<SkillComponent skill='Tailwind CSS' />
<SkillComponent skill='React.js (+ Next.js)' />
</SkillsSection>
<SkillsSection title='Back-end'>
<SkillsSection title='Backend'>
<SkillComponent skill='Laravel' />
<SkillComponent skill='Node.js' />
<SkillComponent skill='Fastify' />
<SkillComponent skill='Prisma' />
<SkillComponent skill='PostgreSQL' />
<SkillComponent skill='MySQL' />
</SkillsSection>
<SkillsSection title={t('home:skills.software-tools')}>
<SkillComponent skill='GNU/Linux' />
<SkillComponent skill='Ubuntu' />
<SkillComponent skill='Arch Linux' />
<SkillComponent skill='Visual Studio Code' />
<SkillComponent skill='Git' />
<SkillComponent skill='Docker' />

View File

@ -3,11 +3,7 @@ export interface Skill {
image: string | { [key: string]: string }
}
export interface Skills {
[key: string]: Skill
}
export const skills: Skills = {
export const skills = {
JavaScript: {
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
image: '/images/skills/JavaScript.png'
@ -24,6 +20,14 @@ export const skills: Skills = {
link: 'https://isocpp.org/',
image: '/images/skills/C-Cpp.png'
},
PHP: {
link: 'https://www.php.net/',
image: '/images/skills/PHP.png'
},
Laravel: {
link: 'https://laravel.com/',
image: '/images/skills/Laravel.png'
},
Dart: {
link: 'https://dart.dev/',
image: '/images/skills/Dart.png'
@ -84,20 +88,20 @@ export const skills: Skills = {
},
'Visual Studio Code': {
link: 'https://code.visualstudio.com/',
image: '/images/skills/Visual_Studio_Code.png'
image: '/images/skills/VisualStudioCode.png'
},
Git: {
link: 'https://git-scm.com/',
image: '/images/skills/Git.png'
},
Hyper: {
link: 'https://hyper.is/',
image: '/images/skills/Hyper.svg'
},
Ubuntu: {
link: 'https://ubuntu.com/',
image: '/images/skills/Ubuntu.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'
@ -107,3 +111,5 @@ export const skills: Skills = {
image: '/images/skills/Docker.png'
}
} as const
export type SkillName = keyof typeof skills

View File

@ -1,15 +0,0 @@
import { render } from '@testing-library/react'
import { ErrorPage } from '../ErrorPage'
describe('<ErrorPage />', () => {
it('should render the message and statusCode', () => {
const messageContent = 'message content'
const statusCode = 404
const { getByText } = render(
<ErrorPage statusCode={statusCode} message={messageContent} />
)
expect(getByText(messageContent)).toBeInTheDocument()
expect(getByText(statusCode)).toBeInTheDocument()
})
})

View File

@ -1,16 +0,0 @@
import { render } from '@testing-library/react'
import { Footer } from '../Footer'
describe('<Footer />', () => {
it('should render with appropriate link tag version', () => {
const version = '1.0.0'
const { getByText } = render(<Footer version={version} />)
const versionLink = getByText(version) as HTMLAnchorElement
expect(getByText('Divlo')).toBeInTheDocument()
expect(versionLink).toBeInTheDocument()
expect(versionLink.href).toEqual(
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
)
})
})

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
export const RevealFade: React.FC = (props) => {
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
const { children } = props
const htmlElement = useRef<HTMLDivElement>(null)

View File

@ -4,7 +4,7 @@ export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
const { children, ...rest } = props
return (
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
{children}
</h2>
)

View File

@ -23,7 +23,9 @@ export const Section: React.FC<SectionProps> = (props) => {
<div className='w-full px-3'>
<ShadowContainer style={{ marginTop: 50 }}>
<section {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>}
{heading != null ? (
<SectionHeading>{heading}</SectionHeading>
) : null}
<div className='w-full px-3'>{children}</div>
</section>
</ShadowContainer>
@ -34,7 +36,7 @@ export const Section: React.FC<SectionProps> = (props) => {
if (withoutShadowContainer) {
return (
<section {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>}
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
<div className='w-full px-3'>{children}</div>
</section>
)
@ -42,16 +44,18 @@ export const Section: React.FC<SectionProps> = (props) => {
return (
<section {...rest}>
{heading != null && (
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
{heading != null ? (
<SectionHeading
style={{ ...(description != null ? { margin: 0 } : {}) }}
>
{heading}
</SectionHeading>
)}
{description != null && (
) : null}
{description != null ? (
<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>

View File

@ -1,4 +1,4 @@
import classNames from 'classnames'
import classNames from 'clsx'
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>

17
cypress.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from 'cypress'
export default defineConfig({
fixturesFolder: false,
video: false,
screenshotOnRunFailure: false,
e2e: {
baseUrl: 'http://127.0.0.1:3000',
supportFile: false
},
component: {
devServer: {
framework: 'next',
bundler: 'webpack'
}
}
})

View File

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

View File

@ -0,0 +1,16 @@
import { Footer } from '@/components/Footer'
describe('<Footer />', () => {
it('should render with appropriate link tag version', () => {
const version = '1.0.0'
cy.mount(<Footer version={version} />)
cy.contains('Théo LUDWIG')
.get('[data-cy=version-link]')
.should('have.text', version)
.should(
'have.attr',
'href',
`https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
)
})
})

View File

@ -0,0 +1,17 @@
import { getAge } from '../../../utils/getAge'
describe('utils/getAge', () => {
it('should calculate the right age of a person', () => {
cy.clock(new Date('2018-03-20')).then(() => {
const birthDate = new Date('1980-02-20')
expect(getAge(birthDate)).equal(38)
})
})
it('should calculate the right age of a person (taking into account the months)', () => {
cy.clock(new Date('2018-03-20')).then(() => {
const birthDate = new Date('1980-07-20')
expect(getAge(birthDate)).equal(37)
})
})
})

View File

@ -1,5 +1,7 @@
describe('Common > Header', () => {
beforeEach(() => cy.visit('/'))
beforeEach(() => {
return cy.visit('/')
})
it('should redirect to /blog on click of the blog link', () => {
cy.get('[data-cy=header-blog-link]')
@ -36,7 +38,7 @@ describe('Common > Header', () => {
describe('Switch Language', () => {
it('should switch language from EN (default) to FR', () => {
cy.get('h1').contains('I am Divlo')
cy.get('h1').contains('Théo LUDWIG')
cy.get('[data-cy=language-flag-text]').contains('EN')
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-click]').click()
@ -44,7 +46,7 @@ describe('Common > Header', () => {
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-flag-text]').contains('FR')
cy.get('h1').contains('Je suis Divlo')
cy.get('h1').contains('Théo LUDWIG')
})
it('should close the language list menu when clicking outside', () => {
@ -56,3 +58,5 @@ describe('Common > Header', () => {
})
})
})
export {}

View File

@ -1,7 +1,11 @@
describe('Page /404', () => {
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
beforeEach(() => {
return cy.visit('/404', { failOnStatusCode: false })
})
it('should display the statusCode of 404', () => {
cy.get('[data-cy=status-code]').contains('404')
})
})
export {}

View File

@ -1,7 +1,11 @@
describe('Page /500', () => {
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
beforeEach(() => {
return cy.visit('/500', { failOnStatusCode: false })
})
it('should display the statusCode of 500', () => {
cy.get('[data-cy=status-code]').contains('500')
})
})
export {}

View File

@ -3,7 +3,7 @@ describe('Page /blog/[slug]', () => {
cy.visit('/blog/hello-world')
cy.get('[data-cy=language-flag-text]').should('not.exist')
cy.get('h1').should('have.text', '👋 Hello, world!')
cy.get('.prose a').should('have.attr', 'target', '_blank')
cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
})
it("should redirect to /404 if the blog post doesn't exist", () => {
@ -11,3 +11,5 @@ describe('Page /blog/[slug]', () => {
cy.get('[data-cy=status-code]').contains('404')
})
})
export {}

View File

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

View File

@ -1,13 +1,10 @@
describe('Page /', () => {
beforeEach(() => cy.visit('/'))
beforeEach(() => {
return cy.visit('/')
})
it('should reveals the sections while scrolling except the about section', () => {
const sectionsReveals = [
'#interests',
'#skills',
'#portfolio',
'#open-source'
]
const sectionsReveals = ['#interests', '#skills', '#portfolio']
cy.get('#about').should('be.visible')
for (const section of sectionsReveals) {
cy.get(section)
@ -17,3 +14,5 @@ describe('Page /', () => {
}
})
})
export {}

View File

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

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
<!-- Used by Next.js to inject CSS. -->
<div id="__next_css__DO_NOT_USE__"></div>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,14 @@
import { mount } from 'cypress/react'
import './commands'
import '../../styles/global.css'
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)

View File

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

View File

@ -1,12 +1,11 @@
version: '3.0'
services:
divlo.fr:
theoludwig:
container_name: ${COMPOSE_PROJECT_NAME}
image: 'divlo.fr'
image: 'theoludwig'
build:
context: './'
ports:
- '${PORT}:${PORT}'
- '${PORT-3000}:${PORT-3000}'
environment:
PORT: ${PORT}
env_file: './.env'
PORT: ${PORT-3000}
env_file: '.env'

View File

@ -1,14 +0,0 @@
const nextJest = require('next/jest')
const createJestConfig = nextJest()
const customJestConfig = {
moduleDirectories: ['node_modules', './'],
modulePathIgnorePatterns: ['<rootDir>/cypress'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: [
'@testing-library/jest-dom/extend-expect',
'@testing-library/react'
]
}
module.exports = createJestConfig(customJestConfig)

View File

@ -1,4 +1,22 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
theme/index.html
dist
.parcel-cache
dist-ssr
*.local
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,20 @@
import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
import { build } from 'vite'
const jsonResumeThemeCustom = new URL('./', import.meta.url)
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
const publicResumeOutputURL = new URL(
'../public/curriculum-vitae',
import.meta.url
)
await build({
root: fileURLToPath(jsonResumeThemeCustom),
base: '/curriculum-vitae/'
})
await fs.promises.cp(jsonResumeThemeCustomDist, publicResumeOutputURL, {
recursive: true
})

View File

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1015 B

View File

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 986 B

View File

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View File

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 912 B

View File

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= locals.basics.name %></title>
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
<link rel="stylesheet" href="./styles/global.css" />
<script defer type="module" src="./scripts/main.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row main clearfix">
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
<div class="card profile-card">
<div class="profile-pic-container">
<div class="profile-pic">
<img
class="media-object img-circle center-block"
data-src="holder.js/100x100"
alt="<%= locals.basics.name %>"
src="<%= locals.basics.image %>"
/>
</div>
<div class="name-and-profession text-center">
<h3>
<strong><%= locals.basics.name %></strong>
</h3>
<h5 class="text-muted"><%= locals.basics.label %></h5>
<h5 class="text-muted">
<%= locals.basics.age %> (<span id="year-old"></span> ans)
</h5>
<h5 class="text-muted">
<%= locals.basics.location.address %>
</h5>
</div>
</div>
<div class="contact-details clearfix">
<div class="detail">
<span class="info">
<a
class="link-disguise"
href="mailto:<%= locals.basics.email %>"
>
<%= locals.basics.email %>
</a>
</span>
</div>
<div class="detail">
<span class="info">
<a class="link-disguise" href="<%= locals.basics.url %>">
<%= locals.basics.url %>
</a>
</span>
</div>
</div>
<hr />
</div>
<div class="card background-card">
<div class="background-details">
<div class="detail" id="about">
<div class="icon">
<img src="./images/user.svg" alt="user" />
</div>
<div class="info">
<h4 class="title text-uppercase">À propos</h4>
<div class="card card-nested">
<div class="content mop-wrapper">
<p><%- locals.basics.summary %></p>
</div>
</div>
</div>
</div>
<hr />
<section class="section-separated">
<div class="detail" id="education">
<div class="icon">
<img src="./images/graduation-cap.svg" alt="graduation" />
</div>
<div class="info">
<h4 class="title text-uppercase">Formations</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.education.forEach((degree) => { %>
<li class="card card-nested">
<div class="content">
<p class="clear-margin relative">
<strong><%= degree.studyType %></strong>
</p>
<p class="clear-margin relative">
<strong><%= degree.score %></strong>
</p>
<p class="text-muted clear-margin">
<%= degree.institution %>
</p>
<p class="text-muted clear-margin">
<small>
<%= degree.startDate %> <%= degree.endDate !=
null ? " - " + degree.endDate : "" %>
</small>
</p>
<% if (degree.courses != null) { %>
<ul class="education-courses">
<% degree.courses.forEach((course) => { %>
<li><%= course %></li>
<% }) %>
</ul>
<% } %>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
<div class="detail" id="skills">
<div class="icon">
<img src="./images/toolbox.svg" alt="toolbox" />
</div>
<div class="info">
<h4 class="title text-uppercase">Compétences</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.skills.forEach((skill) => { %>
<li class="card card-nested card-skills">
<div class="skill-info">
<strong><%= skill.name %></strong>
<div class="space-top labels">
<% skill.keywords.forEach((keyword) => { %>
<p class="label label-keyword"><%= keyword %></p>
<% }) %>
</div>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
</section>
<hr />
<section class="section-separated">
<div class="detail" id="work-experience">
<div class="icon">
<img src="./images/building-columns.svg" alt="work" />
</div>
<div class="info">
<h4 class="title text-uppercase">Expériences</h4>
<ul class="list-unstyled clear-margin">
<% locals.work.filter((experience) =>
experience.description == null).forEach((experience) => {
%>
<li class="card card-nested clearfix">
<div class="content">
<p class="clear-margin relative">
<a href="<%= experience.website %>">
<strong><%= experience.name %></strong>
</a>
</p>
<p class="clear-margin relative">
<strong><%- experience.position %></strong>
</p>
<p class="text-muted">
<small>
<span class="space-right">
<%= date.format(new Date(experience.startDate),
'DD/MM/YYYY') %> - <%= date.format(new
Date(experience.endDate), 'DD/MM/YYYY') %> (<%=
experience.duration %>)
</span>
</small>
</p>
<div class="experience-description">
<p><%- experience.summary %></p>
</div>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
<div class="detail" id="interests">
<div class="icon">
<img src="./images/heart.svg" alt="heart" />
</div>
<div class="info">
<h4 class="title text-uppercase">Intérets</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.interests.forEach((interest) => { %>
<li class="card card-nested">
<p><strong><%= interest.name %></strong></p>
</li>
<% }) %>
</ul>
<ul class="list-unstyled clear-margin">
<% locals.work.filter((experience) =>
experience.description != null).forEach((experience) =>
{ %>
<li class="card card-nested clearfix">
<div class="content">
<p class="clear-margin relative">
<a href="<%= experience.website %>">
<strong><%= experience.name %></strong>
</a>
</p>
<p class="clear-margin relative">
<strong><%- experience.position %></strong>
</p>
<p class="text-muted">
<small>
<span class="space-right">
<%= date.format(new
Date(experience.startDate), 'DD/MM/YYYY') %> -
<%= date.format(new Date(experience.endDate),
'DD/MM/YYYY') %> (<%= experience.duration %>)
</span>
</small>
</p>
<div class="experience-description">
<p><%- experience.summary %></p>
</div>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
</section>
</div>
</div>
</section>
</div>
</div>
</body>
</html>

View File

@ -1,28 +0,0 @@
import fs from 'fs'
import { fileURLToPath } from 'url'
import ejs from 'ejs'
import date from 'date-and-time'
import { Parcel } from '@parcel/core'
export const render = async (resume) => {
const themeIndexURL = new URL('./theme/index.ejs', import.meta.url)
const themeBuildURL = new URL('./theme/index.html', import.meta.url)
const indexHTMLURL = new URL('./dist/index.html', import.meta.url)
const themeBuildPath = fileURLToPath(themeBuildURL)
const html = await ejs.renderFile(fileURLToPath(themeIndexURL), {
date,
locals: {
...resume
}
})
await fs.promises.writeFile(themeBuildURL, html, { encoding: 'utf-8' })
const bundler = new Parcel({
entries: themeBuildPath,
source: themeBuildPath,
mode: 'production',
defaultConfig: '@parcel/config-default'
})
await bundler.run()
return await fs.promises.readFile(indexHTMLURL, { encoding: 'utf-8' })
}

File diff suppressed because it is too large Load Diff

View File

@ -3,17 +3,19 @@
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"date-and-time": "2.3.0",
"ejs": "3.1.6",
"modern-normalize": "1.1.0"
"jsonc-parser": "3.2.0",
"modern-normalize": "2.0.0"
},
"devDependencies": {
"@parcel/config-default": "2.3.2",
"@parcel/core": "2.3.2",
"@parcel/optimizer-data-url": "^2.3.2",
"@parcel/transformer-inline-string": "^2.3.2",
"parcel": "2.3.2"
"@types/node": "20.3.3",
"date-and-time": "3.0.2",
"vite": "4.3.9",
"vite-plugin-html": "3.2.0"
}
}

View File

@ -1,18 +0,0 @@
import fs from 'fs'
import { render } from '../index.js'
const jsonResumeURL = new URL('../../resume.json', import.meta.url)
const publicResumeURL = new URL(
'../../public/curriculum-vitae.html',
import.meta.url
)
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
encoding: 'utf-8'
})
const dataResumeJSON = JSON.parse(dataResumeStringJSON)
const dataResumeIndexHTML = await render(dataResumeJSON)
await fs.promises.writeFile(publicResumeURL, dataResumeIndexHTML, {
encoding: 'utf-8'
})

View File

@ -0,0 +1,5 @@
import { BIRTH_DATE, getAge } from '../../utils/getAge.ts'
const yearOld = document.getElementById('year-old')
yearOld.textContent = getAge(BIRTH_DATE).toString()

View File

@ -1,4 +1,4 @@
@import 'npm:modern-normalize/modern-normalize.css';
@import 'modern-normalize/modern-normalize.css';
body {
font-family: 'Montserrat', 'Arial', 'sans-serif';
@ -209,7 +209,6 @@ h5 {
font-size: 75%;
font-weight: 600;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
@ -217,8 +216,6 @@ h5 {
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
@ -227,3 +224,6 @@ h5 {
.label-keyword p {
margin: 0;
}
.section-separated {
display: flex;
}

View File

@ -1,206 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= locals.basics.name %></title>
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
<style>
@import './styles/global.css';
</style>
</head>
<body>
<div class="container-fluid">
<div class="row main clearfix">
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
<div class="card profile-card">
<div class="profile-pic-container">
<div class="profile-pic">
<img
class="media-object img-circle center-block"
data-src="holder.js/100x100"
alt="<%= locals.basics.name %>"
src="<%= locals.basics.image %>"
/>
</div>
<div class="name-and-profession text-center">
<h3>
<strong><%= locals.basics.name %></strong>
</h3>
<h5 class="text-muted"><%= locals.basics.label %></h5>
</div>
</div>
<div class="contact-details clearfix">
<div class="detail">
<span class="info"><%= locals.basics.phone %></span>
</div>
<div class="detail">
<span class="info">
<a
class="link-disguise"
href="mailto:<%= locals.basics.email %>"
>
<%= locals.basics.email %>
</a>
</span>
</div>
<div class="detail">
<span class="info">
<a class="link-disguise" href="<%= locals.basics.url %>">
<%= locals.basics.url %>
</a>
</span>
</div>
</div>
<hr />
</div>
<div class="card background-card">
<div class="background-details">
<div class="detail" id="about">
<div class="icon">
<img src="data-url:./images/user.svg" alt="user" />
</div>
<div class="info">
<h4 class="title text-uppercase">À propos</h4>
<div class="card card-nested">
<div class="content mop-wrapper">
<p><%- locals.basics.summary %></p>
</div>
</div>
</div>
</div>
<hr />
<div class="detail" id="work-experience">
<div class="icon">
<img
src="data-url:./images/building-columns.svg"
alt="work"
/>
</div>
<div class="info">
<h4 class="title text-uppercase">Expériences</h4>
<ul class="list-unstyled clear-margin">
<% locals.work.forEach((experience) => { %>
<li class="card card-nested clearfix">
<div class="content">
<p class="clear-margin relative">
<a href="<%= experience.website %>">
<strong><%= experience.name %></strong>
</a>
</p>
<p class="clear-margin relative">
<strong><%- experience.position %></strong>
</p>
<p class="text-muted">
<small>
<span class="space-right">
<%= date.format(new Date(experience.startDate),
'DD/MM/YYYY') %> - <%= date.format(new
Date(experience.endDate), 'DD/MM/YYYY') %>
</span>
</small>
</p>
<div class="experience-description">
<p><%- experience.summary %></p>
</div>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
<hr />
<div class="detail" id="skills">
<div class="icon">
<img src="data-url:./images/toolbox.svg" alt="toolbox" />
</div>
<div class="info">
<h4 class="title text-uppercase">Compétences</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.skills.forEach((skill) => { %>
<li class="card card-nested card-skills">
<div class="skill-info">
<strong><%= skill.name %></strong>
<div class="space-top labels">
<% skill.keywords.forEach((keyword) => { %>
<p class="label label-keyword"><%= keyword %></p>
<% }) %>
</div>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
<hr />
<div class="detail" id="education">
<div class="icon">
<img
src="data-url:./images/graduation-cap.svg"
alt="graduation"
/>
</div>
<div class="info">
<h4 class="title text-uppercase">Éducation</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.education.forEach((degree) => { %>
<li class="card card-nested">
<div class="content">
<p class="clear-margin relative">
<strong><%= degree.studyType %></strong>
</p>
<p class="clear-margin relative">
<strong><%= degree.score %></strong>
</p>
<p class="text-muted clear-margin">
<%= degree.institution %>
</p>
<p class="text-muted clear-margin">
<small>
<%= degree.startDate %> - <%= degree.endDate %>
</small>
</p>
</div>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
<hr />
<div class="detail" id="interests">
<div class="icon">
<img src="data-url:./images/heart.svg" alt="heart" />
</div>
<div class="info">
<h4 class="title text-uppercase">Intérets</h4>
<div class="content">
<ul class="list-unstyled clear-margin">
<% locals.interests.forEach((interest) => { %>
<li class="card card-nested">
<p><strong><%= interest.name %></strong></p>
</li>
<% }) %>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
import fs from 'node:fs'
import { defineConfig } from 'vite'
import { parse as JSONCParser } from 'jsonc-parser'
import { createHtmlPlugin } from 'vite-plugin-html'
import date from 'date-and-time'
const jsonResumeURL = new URL('../resume.jsonc', import.meta.url)
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
encoding: 'utf-8'
})
const resume = JSONCParser(dataResumeStringJSON)
/**
* Documentation: <https://vitejs.dev/config/>
*/
export default defineConfig({
build: {
assetsDir: './'
},
plugins: [
createHtmlPlugin({
inject: {
data: {
date,
locals: {
...resume
}
}
}
})
],
css: {
postcss: {}
}
})

View File

@ -1,26 +1,27 @@
{
"about": {
"i-am": "I am",
"description": "Developer Full Stack Junior • Passionate about High-Tech",
"full-name": "Full name",
"description": "Developer Full Stack • Open-Source enthusiast",
"pronouns": "Pronouns",
"pronouns-value": "He/Him",
"birth-date": "Birth date",
"years-old": "years old",
"nationality": "Nationality",
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (first year)."
"description-bottom": "I am a student in computer science following the French training \"BUT Informatique\" and I am also a self-taught."
},
"interests": {
"title": "Interests",
"paragraphs": [
{
"title": "Developer Full Stack Junior",
"title": "Developer Full Stack",
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
},
{
"title": "Open-Source enthusiast",
"description": "For me, everyone should work, solve problems, build things and think together.<br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/theoludwig/theoludwig' target='_blank' rel='noopener noreferrer'>GitHub</a>."
},
{
"title": "Passionate about High-Tech",
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
},
{
"title": "Open-Source enthusiast",
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
}
]
},

View File

@ -2,5 +2,5 @@
"return-to-home-page": "Revenir à la page d'accueil ?",
"error": "Erreur",
"server-error": "Erreur Interne du Serveur !",
"not-found": "Cette page n'existe pas!"
"not-found": "Cette page n'existe pas !"
}

View File

@ -1,26 +1,27 @@
{
"about": {
"i-am": "Je suis",
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
"full-name": "Prénom NOM",
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
"pronouns": "Pronoms",
"pronouns-value": "Il/Lui",
"birth-date": "Date de naissance",
"years-old": "ans",
"nationality": "Nationalité",
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année)."
"description-bottom": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne."
},
"interests": {
"title": "Intérêts",
"paragraphs": [
{
"title": "Développeur Full Stack Junior",
"title": "Développeur Full Stack",
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
},
{
"title": "Enthousiaste de l'Open-Source",
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/theoludwig/theoludwig' target='_blank' rel='noopener noreferrer'>GitHub</a>."
},
{
"title": "Passionné de High-Tech",
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
},
{
"title": "Enthousiaste de l'Open-Source",
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
}
]
},

5
next-env.d.ts vendored
View File

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,41 +1,9 @@
const nextPWA = require('next-pwa')
const nextTranslate = require('next-translate')
const { createSecureHeaders } = require('next-secure-headers')
const nextTranslate = require('next-translate-plugin')
/** @type {import("next").NextConfig} */
module.exports = nextTranslate(
nextPWA({
reactStrictMode: true,
pwa: {
disable: process.env.NODE_ENV !== 'production',
dest: 'public'
},
headers() {
return [
{
source: '/:path*',
headers: createSecureHeaders({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
'data:',
"'unsafe-eval'",
"'unsafe-inline'"
],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ['*', 'data:', 'blob:'],
mediaSrc: "'none'",
connectSrc: '*',
objectSrc: "'none'",
fontSrc: "'self'",
baseURI: "'none'"
}
}
})
}
]
}
})
)
const nextConfig = {
reactStrictMode: true,
output: 'standalone'
}
module.exports = nextTranslate(nextConfig)

43488
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +1,100 @@
{
"name": "divlo",
"version": "2.1.0",
"name": "theoludwig",
"version": "2.12.0",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/Divlo/Divlo"
"url": "https://github.com/theoludwig/theoludwig"
},
"engines": {
"node": ">=14.0.0",
"npm": ">=7.0.0"
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"scripts": {
"dev": "next dev",
"start": "next start",
"build": "npm run resume:build && next build",
"export": "next export",
"lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint \"**/*.{md,mdx}\" --dot --ignore-path \".gitignore\"",
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
"lint:prettier": "prettier \".\" --check",
"lint:markdown": "markdownlint-cli2",
"lint:eslint": "eslint . --ignore-path .gitignore",
"lint:prettier": "prettier . --check --ignore-path .gitignore",
"lint:staged": "lint-staged",
"test:unit": "jest",
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
"test:lighthouse": "lhci autorun",
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
"test:e2e:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
"resume:build": "node ./jsonresume-theme-custom/scripts/build.js",
"test:unit": "cypress run --component",
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"",
"test:e2e": "start-server-and-test \"start\" http://127.0.0.1:3000 \"cypress run\"",
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"",
"resume:build": "node ./jsonresume-theme-custom/build.js",
"release": "semantic-release",
"deploy": "vercel",
"postinstall": "husky install"
},
"dependencies": {
"@fontsource/montserrat": "4.5.5",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "6.0.0",
"@fortawesome/free-solid-svg-icons": "6.0.0",
"@fortawesome/react-fontawesome": "0.1.17",
"classnames": "2.3.1",
"date-and-time": "2.3.0",
"@fontsource/montserrat": "5.0.3",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@giscus/react": "2.3.0",
"clsx": "1.2.1",
"date-and-time": "3.0.2",
"gray-matter": "4.0.3",
"html-react-parser": "1.4.8",
"next": "12.1.0",
"next-mdx-remote": "4.0.0",
"next-pwa": "5.4.6",
"next-themes": "0.1.1",
"next-translate": "1.3.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"read-pkg": "7.1.0",
"html-react-parser": "4.0.0",
"katex": "0.16.8",
"next": "13.4.7",
"next-mdx-remote": "4.4.1",
"next-themes": "0.2.1",
"next-translate": "2.4.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"read-pkg": "8.0.0",
"rehype-katex": "6.0.3",
"rehype-raw": "6.1.1",
"rehype-slug": "5.0.1",
"rehype-slug": "5.1.0",
"remark-gfm": "3.0.1",
"sharp": "0.30.2",
"shiki": "0.10.1",
"remark-math": "5.1.1",
"sharp": "0.32.1",
"shiki": "0.14.3",
"unified": "10.1.2",
"unist-util-visit": "4.1.0",
"unist-util-visit": "4.1.2",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@commitlint/cli": "16.2.1",
"@commitlint/config-conventional": "16.2.1",
"@lhci/cli": "0.9.0",
"@saithodev/semantic-release-backmerge": "2.1.2",
"@commitlint/cli": "17.6.6",
"@commitlint/config-conventional": "17.6.6",
"@saithodev/semantic-release-backmerge": "3.2.0",
"@semantic-release/git": "10.0.1",
"@tailwindcss/typography": "0.5.2",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@types/jest": "27.4.1",
"@types/node": "17.0.21",
"@types/react": "17.0.40",
"@tailwindcss/typography": "0.5.9",
"@tsconfig/strictest": "2.0.1",
"@types/node": "20.3.3",
"@types/react": "18.2.14",
"@types/unist": "2.0.6",
"@typescript-eslint/eslint-plugin": "5.14.0",
"autoprefixer": "10.4.2",
"cypress": "9.5.1",
"editorconfig-checker": "4.0.2",
"eslint": "8.11.0",
"eslint-config-conventions": "1.1.0",
"eslint-config-next": "12.1.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-promise": "6.0.0",
"eslint-plugin-unicorn": "41.0.0",
"html-w3c-validator": "1.1.0",
"husky": "7.0.4",
"jest": "27.5.1",
"@typescript-eslint/eslint-plugin": "5.60.1",
"autoprefixer": "10.4.14",
"cypress": "12.16.0",
"editorconfig-checker": "5.1.1",
"eslint": "8.44.0",
"eslint-config-conventions": "10.0.0",
"eslint-config-next": "13.4.7",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "47.0.0",
"html-w3c-validator": "1.4.0",
"husky": "8.0.3",
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
"lint-staged": "12.3.5",
"markdownlint-cli": "0.31.1",
"next-secure-headers": "2.2.0",
"postcss": "8.4.8",
"prettier": "2.5.1",
"prettier-plugin-tailwindcss": "0.1.8",
"semantic-release": "19.0.2",
"start-server-and-test": "1.14.0",
"tailwindcss": "3.0.23",
"typescript": "4.6.2",
"vercel": "24.0.0"
"lint-staged": "13.2.3",
"markdownlint-cli2": "0.8.1",
"markdownlint-rule-relative-links": "2.1.0",
"next-translate-plugin": "2.4.4",
"postcss": "8.4.24",
"prettier": "2.8.8",
"prettier-plugin-tailwindcss": "0.3.0",
"semantic-release": "21.0.6",
"start-server-and-test": "2.0.0",
"tailwindcss": "3.3.2",
"typescript": "5.0.4",
"vercel": "31.0.1"
}
}

View File

@ -1,38 +1,32 @@
import { GetStaticProps, NextPage } from 'next'
import type { GetStaticProps, NextPage } from 'next'
import useTranslation from 'next-translate/useTranslation'
import { ErrorPage } from 'components/ErrorPage'
import { Head } from 'components/Head'
import { Header } from 'components/Header'
import { Footer, FooterProps } from 'components/Footer'
import { getDefaultDescription } from 'utils/getDefaultDescription'
import type { FooterProps } from 'components/Footer'
interface Error404Props extends FooterProps {
description: string
}
interface Error404Props extends FooterProps {}
const Error404: NextPage<Error404Props> = (props) => {
const { t } = useTranslation()
const { version, description } = props
const { version } = props
return (
<>
<Head title='404 | Divlo' description={description} />
<Header showLanguage />
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
<ErrorPage statusCode={404} message={t('errors:not-found')} />
</main>
<Footer version={version} />
<Head title='404 | Théo LUDWIG' />
<ErrorPage
statusCode={404}
message={t('errors:not-found')}
version={version}
/>
</>
)
}
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
export const getStaticProps: GetStaticProps<Error404Props> = async () => {
const { readPackage } = await import('read-pkg')
const { version } = await readPackage()
const description = getDefaultDescription()
return { props: { version, description } }
return { props: { version } }
}
export default Error404

View File

@ -1,38 +1,32 @@
import { GetStaticProps, NextPage } from 'next'
import type { GetStaticProps, NextPage } from 'next'
import useTranslation from 'next-translate/useTranslation'
import { ErrorPage } from 'components/ErrorPage'
import { Head } from 'components/Head'
import { Header } from 'components/Header'
import { Footer, FooterProps } from 'components/Footer'
import { getDefaultDescription } from 'utils/getDefaultDescription'
import type { FooterProps } from 'components/Footer'
interface Error500Props extends FooterProps {
description: string
}
interface Error500Props extends FooterProps {}
const Error500: NextPage<Error500Props> = (props) => {
const { t } = useTranslation()
const { version, description } = props
const { version } = props
return (
<>
<Head title='500 | Divlo' description={description} />
<Header showLanguage />
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
<ErrorPage statusCode={500} message={t('errors:server-error')} />
</main>
<Footer version={version} />
<Head title='500 | Théo LUDWIG' />
<ErrorPage
statusCode={500}
message={t('errors:server-error')}
version={version}
/>
</>
)
}
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
export const getStaticProps: GetStaticProps<Error500Props> = async () => {
const { readPackage } = await import('read-pkg')
const { version } = await readPackage()
const description = getDefaultDescription()
return { props: { version, description } }
return { props: { version } }
}
export default Error500

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { AppProps } from 'next/app'
import type { AppType } from 'next/app'
import { ThemeProvider } from 'next-themes'
import useTranslation from 'next-translate/useTranslation'
import UniversalCookie from 'universal-cookie'
@ -13,7 +13,7 @@ const universalCookie = new UniversalCookie()
/** how long in seconds, until the cookie expires (10 years) */
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
const Application: AppType = ({ Component, pageProps }) => {
const { lang } = useTranslation()
useEffect(() => {

View File

@ -1,48 +1,116 @@
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
import Link from 'next/link'
import Image from 'next/image'
import { MDXRemote } from 'next-mdx-remote'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLink } from '@fortawesome/free-solid-svg-icons'
import date from 'date-and-time'
import Giscus from '@giscus/react'
import { useTheme } from 'next-themes'
import 'katex/dist/katex.min.css'
import { Head } from 'components/Head'
import { Header } from 'components/Header'
import { Footer, FooterProps } from 'components/Footer'
import type { FooterProps } from 'components/Footer'
import { Footer } from 'components/Footer'
import type { Post } from 'utils/blog'
interface BlogPostPageProps extends FooterProps {
post: Post
}
const Heading = (
props: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLHeadingElement>,
HTMLHeadingElement
>
): JSX.Element => {
const { children, id = '' } = props
return (
<h2 {...props} className='group'>
<Link
href={`#${id}`}
className='invisible !text-black group-hover:visible dark:!text-white'
>
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
</Link>
{children}
</h2>
)
}
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
const { version, post } = props
const { theme = 'dark' } = useTheme()
return (
<>
<Head
title={`${post.frontmatter.title} | Divlo`}
title={`${post.frontmatter.title} | Théo LUDWIG`}
description={post.frontmatter.description}
/>
<Header />
<main className='flex flex-1 flex-col flex-wrap items-center'>
<div className='my-10 flex flex-col items-center'>
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
<div className='my-10 flex flex-col items-center text-center'>
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
<p className='mt-2' data-cy='blog-post-date'>
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
</p>
</div>
<div className='prose mb-10 px-8'>
<MDXRemote
{...post.source}
components={{
a: (props: React.ComponentPropsWithoutRef<'a'>) => {
if (props.href?.startsWith('#') ?? false) {
return <a {...props} />
<div className='prose mb-10'>
<div className='px-8'>
<MDXRemote
{...post.source}
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='h-auto w-auto'
/>
</span>
)
},
a: (props) => {
if (props.href?.startsWith('#') ?? false) {
return <a {...props} />
}
return (
<a target='_blank' rel='noopener noreferrer' {...props} />
)
}
return (
<a target='_blank' rel='noopener noreferrer' {...props} />
)
}
}}
/>
}}
/>
<Giscus
id='comments'
repo='theoludwig/theoludwig'
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
category='General'
categoryId='DIC_kwDOFWUewM4CQ_WK'
mapping='pathname'
reactionsEnabled='1'
emitMetadata='0'
inputPosition='top'
theme={theme}
lang='en'
loading='lazy'
/>
</div>
</div>
</main>
<Footer version={version} />
@ -53,7 +121,7 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
context
) => {
const slug = context?.params?.slug
const slug = context?.params?.['slug']
const { getPostBySlug } = await import('utils/blog')
const post = await getPostBySlug(slug)
if (post == null || (post != null && !post.frontmatter.isPublished)) {

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