mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
a18cec4826 | |||
61e589f0f4 | |||
dc5c3cee41 | |||
20cb0c21d5 | |||
e5232c1394 | |||
fd51609713 | |||
edf16c2562 | |||
94e0d190ae | |||
b1cf7f8517 | |||
a1a715d3b9 | |||
eede46fb41 | |||
e32c53caa1 | |||
361ea37deb | |||
d49a8a7470 | |||
a4996c8251 | |||
b25451e631 | |||
042a861f58 | |||
d76db36dbc | |||
99d9dcf334 | |||
ece5ded1b4 | |||
1514600998 | |||
5f5b328895 | |||
c88887a322 | |||
014044573a | |||
df009c3f7b | |||
5c85ca2ef1 | |||
07f7942496 | |||
213a3fa182 | |||
28d9211583 | |||
4d085cb148 | |||
e6c583f2cd | |||
232b54588a | |||
c419fb3bb4 | |||
03e7e22d74 | |||
e85c241ed1 | |||
c1877297f8 |
@ -1,2 +1 @@
|
|||||||
ARG VARIANT="16"
|
FROM mcr.microsoft.com/devcontainers/javascript-node:18
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "Divlo",
|
||||||
"dockerComposeFile": "./docker-compose.yml",
|
"dockerComposeFile": "./docker-compose.yml",
|
||||||
"service": "workspace",
|
"service": "workspace",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"settings": {
|
"customizations": {
|
||||||
"remote.autoForwardPorts": false
|
"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],
|
"forwardPorts": [3000],
|
||||||
"postAttachCommand": ["npm", "install"],
|
"postAttachCommand": ["npm", "install"],
|
||||||
"remoteUser": "node"
|
"remoteUser": "node"
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
.vscode
|
.*
|
||||||
.git
|
!.npmrc
|
||||||
.env
|
|
||||||
build
|
build
|
||||||
.next
|
|
||||||
coverage
|
coverage
|
||||||
node_modules
|
node_modules
|
||||||
tmp
|
|
||||||
temp
|
|
||||||
.DS_Store
|
|
||||||
.lighthouseci
|
|
||||||
.vercel
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
COMPOSE_PROJECT_NAME=divlo.fr
|
COMPOSE_PROJECT_NAME=divlo
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
@ -4,12 +4,7 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error"
|
||||||
"unicorn/prefer-node-protocol": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
|||||||
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||||
|
|
||||||
## What changes this PR introduce?
|
# What changes this PR introduce?
|
||||||
|
|
||||||
## List any relevant issue numbers
|
## List any relevant issue numbers
|
||||||
|
|
||||||
|
6
.github/workflows/analyze.yml
vendored
6
.github/workflows/analyze.yml
vendored
@ -16,12 +16,12 @@ jobs:
|
|||||||
language: ['javascript']
|
language: ['javascript']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Initialize CodeQL'
|
- name: 'Initialize CodeQL'
|
||||||
uses: 'github/codeql-action/init@v1'
|
uses: 'github/codeql-action/init@v2'
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: 'Perform CodeQL Analysis'
|
- name: 'Perform CodeQL Analysis'
|
||||||
uses: 'github/codeql-action/analyze@v1'
|
uses: 'github/codeql-action/analyze@v2'
|
||||||
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
|
21
.github/workflows/lint.yml
vendored
21
.github/workflows/lint.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'lint:commit'
|
- name: 'lint:commit'
|
||||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
@ -30,8 +30,8 @@ jobs:
|
|||||||
- name: 'lint:markdown'
|
- name: 'lint:markdown'
|
||||||
run: 'npm run lint:markdown'
|
run: 'npm run lint:markdown'
|
||||||
|
|
||||||
- name: 'lint:typescript'
|
- name: 'lint:eslint'
|
||||||
run: 'npm run lint:typescript'
|
run: 'npm run lint:eslint'
|
||||||
|
|
||||||
- name: 'lint:prettier'
|
- name: 'lint:prettier'
|
||||||
run: 'npm run lint:prettier'
|
run: 'npm run lint:prettier'
|
||||||
@ -40,8 +40,3 @@ jobs:
|
|||||||
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
github_token: ${{ secrets.github_token }}
|
||||||
|
|
||||||
- name: 'lint:docker'
|
|
||||||
uses: 'hadolint/hadolint-action@v1.6.0'
|
|
||||||
with:
|
|
||||||
dockerfile: './Dockerfile'
|
|
||||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -8,26 +8,26 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 'Import GPG key'
|
- name: 'Import GPG key'
|
||||||
uses: 'crazy-max/ghaction-import-gpg@v4'
|
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Release'
|
- name: 'Release'
|
||||||
run: 'npm run release'
|
run: 'npm run release'
|
||||||
|
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@ -10,33 +10,33 @@ jobs:
|
|||||||
test-unit:
|
test-unit:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Unit Test'
|
- name: 'Unit Test'
|
||||||
run: 'npm run test:unit'
|
run: 'npm run test:unit'
|
||||||
|
|
||||||
test-lighthouse:
|
test-e2e:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
- uses: 'actions/checkout@v3.5.0'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Setup Node.js'
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
uses: 'actions/setup-node@v3.6.0'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '18.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install dependencies'
|
||||||
run: 'npm install'
|
run: 'npm clean-install'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
@ -44,27 +44,5 @@ jobs:
|
|||||||
- name: 'html-w3c-validator'
|
- name: 'html-w3c-validator'
|
||||||
run: 'npm run test:html-w3c-validator'
|
run: 'npm run test:html-w3c-validator'
|
||||||
|
|
||||||
- name: 'Lighthouse'
|
|
||||||
run: 'npm run test:lighthouse'
|
|
||||||
env:
|
|
||||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
||||||
|
|
||||||
test-e2e:
|
|
||||||
runs-on: 'ubuntu-latest'
|
|
||||||
steps:
|
|
||||||
- uses: 'actions/checkout@v3.0.0'
|
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
|
||||||
uses: 'actions/setup-node@v3.0.0'
|
|
||||||
with:
|
|
||||||
node-version: '16.x'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: 'Install'
|
|
||||||
run: 'npm install'
|
|
||||||
|
|
||||||
- name: 'Build'
|
|
||||||
run: 'npm run build'
|
|
||||||
|
|
||||||
- name: 'End To End (e2e) Test'
|
- name: 'End To End (e2e) Test'
|
||||||
run: 'npm run test:e2e'
|
run: 'npm run test:e2e'
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -49,3 +49,7 @@ npm-debug.log*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.lighthouseci
|
.lighthouseci
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"urls": [
|
"urls": [
|
||||||
"http://localhost:3000/",
|
"http://127.0.0.1:3000/",
|
||||||
"http://localhost:3000/blog",
|
"http://127.0.0.1:3000/blog",
|
||||||
"http://localhost:3000/blog/hello-world"
|
"http://127.0.0.1:3000/blog/hello-world"
|
||||||
],
|
],
|
||||||
"files": ["./public/curriculum-vitae/index.html"]
|
"files": ["./public/curriculum-vitae/index.html"]
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"ci": {
|
|
||||||
"collect": {
|
|
||||||
"startServerCommand": "npm run start",
|
|
||||||
"startServerReadyPattern": "ready on",
|
|
||||||
"startServerReadyTimeout": 20000,
|
|
||||||
"url": [
|
|
||||||
"http://localhost:3000/",
|
|
||||||
"http://localhost:3000/blog",
|
|
||||||
"http://localhost:3000/blog/hello-world"
|
|
||||||
],
|
|
||||||
"numberOfRuns": 1
|
|
||||||
},
|
|
||||||
"assert": {
|
|
||||||
"preset": "lighthouse:recommended",
|
|
||||||
"assertions": {
|
|
||||||
"csp-xss": "warning",
|
|
||||||
"non-composited-animations": "warning",
|
|
||||||
"unused-javascript": "warning",
|
|
||||||
"image-size-responsive": "warning",
|
|
||||||
"unsized-images": "warning",
|
|
||||||
"color-contrast": "warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload": {
|
|
||||||
"target": "temporary-public-storage"
|
|
||||||
},
|
|
||||||
"server": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"default": true,
|
"default": true,
|
||||||
"MD013": false,
|
"relative-links": true,
|
||||||
|
"extends": "markdownlint/style/prettier",
|
||||||
"MD024": false,
|
"MD024": false,
|
||||||
"MD033": false,
|
"MD033": false
|
||||||
"MD041": false
|
|
||||||
},
|
},
|
||||||
"globs": ["**/*.{md,mdx}"],
|
"globs": ["**/*.{md,mdx}"],
|
||||||
"ignores": ["**/node_modules"]
|
"ignores": ["**/node_modules"],
|
||||||
|
"customRules": ["markdownlint-rule-relative-links"]
|
||||||
}
|
}
|
||||||
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -3,8 +3,6 @@
|
|||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"divlo.vscode-styled-jsx-syntax",
|
|
||||||
"divlo.vscode-styled-jsx-languageserver",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
"bradlc.vscode-tailwindcss",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"davidanson.vscode-markdownlint",
|
"davidanson.vscode-markdownlint",
|
||||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -6,5 +6,9 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": true
|
||||||
}
|
},
|
||||||
|
"eslint.options": {
|
||||||
|
"ignorePath": ".gitignore"
|
||||||
|
},
|
||||||
|
"prettier.ignorePath": ".gitignore"
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
contact@divlo.fr.
|
<contact@divlo.fr>.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
**divlo.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||||
|
|
||||||
## Types of contributions
|
## Types of contributions
|
||||||
|
|
||||||
- Reporting a bug.
|
- Reporting a bug.
|
||||||
@ -21,29 +25,7 @@ If you're adding new features to **divlo.fr**, please include tests.
|
|||||||
|
|
||||||
## Commits
|
## Commits
|
||||||
|
|
||||||
The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases.
|
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
|
||||||
|
|
||||||
### Types
|
|
||||||
|
|
||||||
Types define which kind of changes you made to the project.
|
|
||||||
|
|
||||||
| Types | Description |
|
|
||||||
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| feat | A new feature. |
|
|
||||||
| fix | A bug fix. |
|
|
||||||
| docs | Documentation only changes. |
|
|
||||||
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
|
|
||||||
| refactor | A code change that neither fixes a bug nor adds a feature. |
|
|
||||||
| perf | A code change that improves performance. |
|
|
||||||
| test | Adding missing tests or correcting existing tests. |
|
|
||||||
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
|
|
||||||
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
|
|
||||||
| chore | Other changes that don't modify src or test files. |
|
|
||||||
| revert | Reverts a previous commit. |
|
|
||||||
|
|
||||||
### Scopes
|
|
||||||
|
|
||||||
Scopes define what part of the code changed.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@ -86,4 +68,4 @@ docker compose up --build
|
|||||||
|
|
||||||
### Services started
|
### Services started
|
||||||
|
|
||||||
- website : `http://localhost:3000`
|
- website: `http://127.0.0.1:3000`
|
||||||
|
32
Dockerfile
32
Dockerfile
@ -1,23 +1,21 @@
|
|||||||
FROM node:16.16.0 AS dependencies
|
FROM node:18.16.0 AS builder-dependencies
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm install
|
RUN npm clean-install
|
||||||
|
|
||||||
FROM node:16.16.0 AS builder
|
FROM node:18.16.0 AS builder
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
|
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:16.16.0 AS runner
|
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/application
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
COPY --from=builder /usr/src/app/public ./public
|
COPY --from=builder /usr/src/application/.next/standalone ./
|
||||||
COPY --from=builder /usr/src/app/.next ./.next
|
COPY --from=builder /usr/src/application/.next/static ./.next/static
|
||||||
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
|
COPY --from=builder /usr/src/application/public ./public
|
||||||
COPY --from=builder /usr/src/app/locales ./locales
|
COPY --from=builder /usr/src/application/locales ./locales
|
||||||
COPY --from=builder /usr/src/app/pages ./pages
|
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
|
||||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
CMD ["./server.js"]
|
||||||
RUN npx next telemetry disable
|
|
||||||
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
|
|
||||||
|
14
README.md
14
README.md
@ -1,7 +1,7 @@
|
|||||||
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
|
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Developer Full Stack Junior • Passionate about High-Tech</strong>
|
<strong>Developer Full Stack • Open-Source enthusiast</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -25,15 +25,11 @@
|
|||||||
"pronouns": "He/Him",
|
"pronouns": "He/Him",
|
||||||
"birthDate": "31/03/2003",
|
"birthDate": "31/03/2003",
|
||||||
"nationality": "Alsace, France",
|
"nationality": "Alsace, France",
|
||||||
"interests": [
|
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
|
||||||
"Developer Full Stack Junior",
|
|
||||||
"Passionate about High-Tech",
|
|
||||||
"Open-Source enthusiast"
|
|
||||||
],
|
|
||||||
"skills": {
|
"skills": {
|
||||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||||
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
"backEnd": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,45 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export interface ErrorPageProps {
|
import type { FooterProps } from './Footer'
|
||||||
|
import { Footer } from './Footer'
|
||||||
|
import { Header } from './Header'
|
||||||
|
|
||||||
|
export interface ErrorPageProps extends FooterProps {
|
||||||
statusCode: number
|
statusCode: number
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||||
const { message, statusCode } = props
|
const { message, statusCode, version } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className='my-6 text-4xl font-semibold'>
|
<div className='flex h-screen flex-col pt-0'>
|
||||||
{t('errors:error')}{' '}
|
<Header showLanguage />
|
||||||
<span
|
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
|
||||||
className='text-yellow dark:text-yellow-dark'
|
<h1 className='my-6 text-4xl font-semibold'>
|
||||||
data-cy='status-code'
|
{t('errors:error')}{' '}
|
||||||
>
|
<span
|
||||||
{statusCode}
|
className='text-yellow dark:text-yellow-dark'
|
||||||
</span>
|
data-cy='status-code'
|
||||||
</h1>
|
>
|
||||||
<p className='text-center text-lg'>
|
{statusCode}
|
||||||
{message}{' '}
|
</span>
|
||||||
<Link href='/'>
|
</h1>
|
||||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
<p className='text-center text-lg'>
|
||||||
{t('errors:return-to-home-page')}
|
{message}{' '}
|
||||||
</a>
|
<Link
|
||||||
</Link>
|
href='/'
|
||||||
</p>
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
|
>
|
||||||
<style jsx global>
|
{t('errors:return-to-home-page')}
|
||||||
{`
|
</Link>
|
||||||
main {
|
</p>
|
||||||
display: flex;
|
</main>
|
||||||
flex-direction: column;
|
<Footer version={version} />
|
||||||
justify-content: center;
|
</div>
|
||||||
align-items: center;
|
|
||||||
min-width: 100vw;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
#__next {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,11 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||||
<p>
|
<p>
|
||||||
<Link href='/'>
|
<Link
|
||||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
href='/'
|
||||||
Divlo
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
</a>
|
>
|
||||||
|
Divlo
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
| {t('common:all-rights-reserved')}
|
| {t('common:all-rights-reserved')}
|
||||||
</p>
|
</p>
|
||||||
|
@ -11,7 +11,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
const {
|
const {
|
||||||
title = 'Divlo',
|
title = 'Divlo',
|
||||||
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
||||||
description = 'Divlo - Developer Full Stack Junior • Passionate about High-Tech',
|
description = 'Divlo - Developer Full Stack • Passionate about High-Tech',
|
||||||
url = 'https://divlo.fr/'
|
url = 'https://divlo.fr/'
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ export const Language: React.FC = () => {
|
|||||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const handleHiddenMenu = useCallback(() => {
|
const handleHiddenMenu = useCallback(() => {
|
||||||
setHiddenMenu((oldHiddenMenu) => !oldHiddenMenu)
|
setHiddenMenu((oldHiddenMenu) => {
|
||||||
|
return !oldHiddenMenu
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,7 +55,7 @@ export const Language: React.FC = () => {
|
|||||||
<ul
|
<ul
|
||||||
data-cy='languages-list'
|
data-cy='languages-list'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
'absolute top-14 z-10 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||||
{ hidden: hiddenMenu }
|
{ hidden: hiddenMenu }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -65,7 +67,9 @@ export const Language: React.FC = () => {
|
|||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||||
onClick={async () => await handleLanguage(language)}
|
onClick={async () => {
|
||||||
|
return await handleLanguage(language)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LanguageFlag language={language} />
|
<LanguageFlag language={language} />
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import classNames from 'clsx'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
export const SwitchTheme: React.FC = () => {
|
export const SwitchTheme: React.FC = () => {
|
||||||
@ -18,109 +19,60 @@ export const SwitchTheme: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className='flex items-center'
|
||||||
className='flex items-center'
|
data-cy='switch-theme-click'
|
||||||
data-cy='switch-theme-click'
|
onClick={handleClick}
|
||||||
onClick={handleClick}
|
>
|
||||||
>
|
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
|
||||||
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
|
||||||
<div className='toggle-track'>
|
<div
|
||||||
<div
|
data-cy='switch-theme-dark'
|
||||||
data-cy='switch-theme-dark'
|
className={classNames(
|
||||||
className='toggle-track-check absolute'
|
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
|
||||||
>
|
{
|
||||||
<span className='toggle_Dark relative flex items-center justify-center'>
|
'opacity-100': theme === 'dark',
|
||||||
🌜
|
'opacity-0': theme === 'light'
|
||||||
</span>
|
}
|
||||||
</div>
|
)}
|
||||||
<div
|
>
|
||||||
data-cy='switch-theme-light'
|
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
||||||
className='toggle-track-x absolute'
|
🌜
|
||||||
>
|
</span>
|
||||||
<span className='toggle_Light relative flex items-center justify-center'>
|
</div>
|
||||||
🌞
|
<div
|
||||||
</span>
|
data-cy='switch-theme-light'
|
||||||
</div>
|
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>
|
||||||
<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>
|
||||||
|
<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>
|
</div>
|
||||||
|
</div>
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.toggle-theme-button {
|
|
||||||
touch-action: pan-x;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.toggle-track {
|
|
||||||
width: 50px;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 30px;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.toggle-track-check {
|
|
||||||
width: 14px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
left: 8px;
|
|
||||||
opacity: ${theme === 'dark' ? 1 : 0};
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
.toggle-track-x {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
right: 10px;
|
|
||||||
opacity: ${theme === 'dark' ? 0 : 1};
|
|
||||||
}
|
|
||||||
.toggle_Dark,
|
|
||||||
.toggle_Light {
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
.toggle-thumb {
|
|
||||||
left: ${theme === 'dark' ? '27px' : '0px'};
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: 1px solid #4d4d4d;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fafafa;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
top: 1px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.toggle-screenreader-only {
|
|
||||||
border: 0;
|
|
||||||
clip: rect(0 0 0 0);
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,33 +14,31 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||||
<Link href='/'>
|
<Link href='/'>
|
||||||
<a>
|
<div className='flex items-center justify-center'>
|
||||||
<div className='flex items-center justify-center'>
|
<Image
|
||||||
<Image
|
quality={100}
|
||||||
quality={100}
|
width={60}
|
||||||
width={60}
|
height={60}
|
||||||
height={60}
|
src='/images/divlo_icon_small.png'
|
||||||
src='/images/divlo_icon_small.png'
|
alt='Divlo'
|
||||||
alt='Divlo'
|
priority
|
||||||
/>
|
/>
|
||||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||||
Divlo
|
Divlo
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<div className='flex flex-col items-center justify-center px-6'>
|
<div className='flex flex-col items-center justify-center px-6'>
|
||||||
<Link href='/blog'>
|
<Link
|
||||||
<a
|
href='/blog'
|
||||||
data-cy='header-blog-link'
|
data-cy='header-blog-link'
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{showLanguage && <Language />}
|
{showLanguage ? <Language /> : null}
|
||||||
<SwitchTheme />
|
<SwitchTheme />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
interface InterestItemProps {
|
interface InterestItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -10,7 +10,7 @@ export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
|||||||
const { fontAwesomeIcon, title } = props
|
const { fontAwesomeIcon, title } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
|
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||||
icon={fontAwesomeIcon}
|
icon={fontAwesomeIcon}
|
||||||
|
@ -7,10 +7,7 @@ export const InterestsList: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='my-4 flex justify-center'>
|
<div className='my-4 flex justify-center'>
|
||||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||||
<InterestItem
|
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
|
||||||
title='Developer Full Stack Junior'
|
|
||||||
fontAwesomeIcon={faCode}
|
|
||||||
/>
|
|
||||||
<InterestItem
|
<InterestItem
|
||||||
title='Passionate about High-Tech'
|
title='Passionate about High-Tech'
|
||||||
fontAwesomeIcon={faMicrochip}
|
fontAwesomeIcon={faMicrochip}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
import type { InterestParagraphProps } from './InterestParagraph'
|
||||||
|
import { InterestParagraph } from './InterestParagraph'
|
||||||
import { InterestsList } from './InterestsList'
|
import { InterestsList } from './InterestsList'
|
||||||
|
|
||||||
export const Interests: React.FC = () => {
|
export const Interests: React.FC = () => {
|
||||||
|
@ -24,7 +24,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
|||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<Image
|
<Image
|
||||||
quality={100}
|
quality={100}
|
||||||
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||||
width={300}
|
width={300}
|
||||||
height={300}
|
height={300}
|
||||||
src={image}
|
src={image}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
import type { PortfolioItemProps } from './PortfolioItem'
|
||||||
|
import { PortfolioItem } from './PortfolioItem'
|
||||||
|
|
||||||
export const Portfolio: React.FC = () => {
|
export const Portfolio: React.FC = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
@ -4,9 +4,9 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
|||||||
const { t, lang } = useTranslation()
|
const { t, lang } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||||
{t('home:about.description-bottom')}
|
{t('home:about.description-bottom')}
|
||||||
{lang === 'fr' && (
|
{lang === 'fr' ? (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@ -17,7 +17,7 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
|||||||
Curriculum vitæ
|
Curriculum vitæ
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
|||||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||||
{title}
|
{title}
|
||||||
</strong>
|
</strong>
|
||||||
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||||
{link != null ? (
|
{link != null ? (
|
||||||
<a
|
<a
|
||||||
className='text-gray hover:underline dark:text-gray-dark'
|
className='text-gray hover:underline dark:text-gray-dark'
|
||||||
|
@ -5,7 +5,7 @@ import DivloLogo from 'public/images/divlo_logo.png'
|
|||||||
export const ProfileLogo: React.FC = () => {
|
export const ProfileLogo: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||||
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
<Image quality={100} src={DivloLogo} alt='Divlo' priority />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ import { useTheme } from 'next-themes'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import type { SkillName } from './skills'
|
||||||
import { skills } from './skills'
|
import { skills } from './skills'
|
||||||
|
|
||||||
export interface SkillComponentProps {
|
export interface SkillComponentProps {
|
||||||
skill: string
|
skill: SkillName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||||
@ -14,10 +15,13 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
if (typeof skillProperties.image !== 'string') {
|
if (typeof skillProperties.image === 'string') {
|
||||||
return skillProperties.image[theme ?? 'light']
|
return skillProperties.image
|
||||||
}
|
}
|
||||||
return skillProperties.image
|
if (theme === 'light') {
|
||||||
|
return skillProperties.image.light
|
||||||
|
}
|
||||||
|
return skillProperties.image.dark
|
||||||
}, [skillProperties, theme])
|
}, [skillProperties, theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +32,14 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
|||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<Image quality={100} width={60} height={60} alt={skill} src={image} />
|
<Image
|
||||||
|
className='inline h-auto w-auto'
|
||||||
|
quality={100}
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
alt={skill}
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
<p className='mt-1'>{skill}</p>
|
<p className='mt-1'>{skill}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -13,6 +13,7 @@ export const Skills: React.FC = () => {
|
|||||||
<SkillComponent skill='TypeScript' />
|
<SkillComponent skill='TypeScript' />
|
||||||
<SkillComponent skill='Python' />
|
<SkillComponent skill='Python' />
|
||||||
<SkillComponent skill='C/C++' />
|
<SkillComponent skill='C/C++' />
|
||||||
|
<SkillComponent skill='PHP' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Front-end'>
|
<SkillsSection title='Front-end'>
|
||||||
@ -23,11 +24,10 @@ export const Skills: React.FC = () => {
|
|||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Back-end'>
|
<SkillsSection title='Back-end'>
|
||||||
|
<SkillComponent skill='Laravel' />
|
||||||
<SkillComponent skill='Node.js' />
|
<SkillComponent skill='Node.js' />
|
||||||
<SkillComponent skill='Fastify' />
|
<SkillComponent skill='Fastify' />
|
||||||
<SkillComponent skill='Prisma' />
|
|
||||||
<SkillComponent skill='PostgreSQL' />
|
<SkillComponent skill='PostgreSQL' />
|
||||||
<SkillComponent skill='MySQL' />
|
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title={t('home:skills.software-tools')}>
|
<SkillsSection title={t('home:skills.software-tools')}>
|
||||||
|
@ -3,11 +3,7 @@ export interface Skill {
|
|||||||
image: string | { [key: string]: string }
|
image: string | { [key: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Skills {
|
export const skills = {
|
||||||
[key: string]: Skill
|
|
||||||
}
|
|
||||||
|
|
||||||
export const skills: Skills = {
|
|
||||||
JavaScript: {
|
JavaScript: {
|
||||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
||||||
image: '/images/skills/JavaScript.png'
|
image: '/images/skills/JavaScript.png'
|
||||||
@ -24,6 +20,14 @@ export const skills: Skills = {
|
|||||||
link: 'https://isocpp.org/',
|
link: 'https://isocpp.org/',
|
||||||
image: '/images/skills/C-Cpp.png'
|
image: '/images/skills/C-Cpp.png'
|
||||||
},
|
},
|
||||||
|
PHP: {
|
||||||
|
link: 'https://www.php.net/',
|
||||||
|
image: '/images/skills/PHP.png'
|
||||||
|
},
|
||||||
|
Laravel: {
|
||||||
|
link: 'https://laravel.com/',
|
||||||
|
image: '/images/skills/Laravel.png'
|
||||||
|
},
|
||||||
Dart: {
|
Dart: {
|
||||||
link: 'https://dart.dev/',
|
link: 'https://dart.dev/',
|
||||||
image: '/images/skills/Dart.png'
|
image: '/images/skills/Dart.png'
|
||||||
@ -107,3 +111,5 @@ export const skills: Skills = {
|
|||||||
image: '/images/skills/Docker.png'
|
image: '/images/skills/Docker.png'
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export type SkillName = keyof typeof skills
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export const RevealFade: React.FC<React.PropsWithChildren<{}>> = (props) => {
|
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
const htmlElement = useRef<HTMLDivElement>(null)
|
const htmlElement = useRef<HTMLDivElement>(null)
|
||||||
|
@ -4,7 +4,7 @@ export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
|||||||
const { children, ...rest } = props
|
const { children, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
|
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,9 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
<div className='w-full px-3'>
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer style={{ marginTop: 50 }}>
|
<ShadowContainer style={{ marginTop: 50 }}>
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null ? (
|
||||||
|
<SectionHeading>{heading}</SectionHeading>
|
||||||
|
) : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
@ -34,7 +36,7 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
if (withoutShadowContainer) {
|
if (withoutShadowContainer) {
|
||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -42,16 +44,18 @@ export const Section: React.FC<SectionProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && (
|
{heading != null ? (
|
||||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
<SectionHeading
|
||||||
|
style={{ ...(description != null ? { margin: 0 } : {}) }}
|
||||||
|
>
|
||||||
{heading}
|
{heading}
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
)}
|
) : null}
|
||||||
{description != null && (
|
{description != null ? (
|
||||||
<p style={{ marginTop: 7 }} className='text-center'>
|
<p style={{ marginTop: 7 }} className='text-center'>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
<div className='w-full px-3'>
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||||
|
@ -3,14 +3,11 @@ import { defineConfig } from 'cypress'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: false,
|
fixturesFolder: false,
|
||||||
video: false,
|
video: false,
|
||||||
downloadsFolder: undefined,
|
|
||||||
screenshotOnRunFailure: false,
|
screenshotOnRunFailure: false,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://127.0.0.1:3000',
|
||||||
supportFile: false
|
supportFile: false
|
||||||
},
|
},
|
||||||
|
|
||||||
component: {
|
component: {
|
||||||
devServer: {
|
devServer: {
|
||||||
framework: 'next',
|
framework: 'next',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Footer } from 'components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
|
|
||||||
describe('<Footer />', () => {
|
describe('<Footer />', () => {
|
||||||
it('should render with appropriate link tag version', () => {
|
it('should render with appropriate link tag version', () => {
|
||||||
@ -14,5 +14,3 @@ describe('<Footer />', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export {}
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
describe('Common > Header', () => {
|
describe('Common > Header', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
it('should redirect to /blog on click of the blog link', () => {
|
it('should redirect to /blog on click of the blog link', () => {
|
||||||
cy.get('[data-cy=header-blog-link]')
|
cy.get('[data-cy=header-blog-link]')
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
describe('Page /404', () => {
|
describe('Page /404', () => {
|
||||||
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/404', { failOnStatusCode: false })
|
||||||
|
})
|
||||||
|
|
||||||
it('should display the statusCode of 404', () => {
|
it('should display the statusCode of 404', () => {
|
||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
describe('Page /500', () => {
|
describe('Page /500', () => {
|
||||||
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/500', { failOnStatusCode: false })
|
||||||
|
})
|
||||||
|
|
||||||
it('should display the statusCode of 500', () => {
|
it('should display the statusCode of 500', () => {
|
||||||
cy.get('[data-cy=status-code]').contains('500')
|
cy.get('[data-cy=status-code]').contains('500')
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
describe('Page /', () => {
|
describe('Page /', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
beforeEach(() => {
|
||||||
|
return cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
it('should reveals the sections while scrolling except the about section', () => {
|
it('should reveals the sections while scrolling except the about section', () => {
|
||||||
const sectionsReveals = [
|
const sectionsReveals = ['#interests', '#skills', '#portfolio']
|
||||||
'#interests',
|
|
||||||
'#skills',
|
|
||||||
'#portfolio',
|
|
||||||
'#open-source'
|
|
||||||
]
|
|
||||||
cy.get('#about').should('be.visible')
|
cy.get('#about').should('be.visible')
|
||||||
for (const section of sectionsReveals) {
|
for (const section of sectionsReveals) {
|
||||||
cy.get(section)
|
cy.get(section)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { mount } from 'cypress/react'
|
import { mount } from 'cypress/react'
|
||||||
|
|
||||||
import './commands'
|
import './commands'
|
||||||
|
import '../../styles/global.css'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
divlo.fr:
|
divlo:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
image: 'divlo.fr'
|
image: 'divlo'
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: './'
|
||||||
ports:
|
ports:
|
||||||
- '${PORT}:${PORT}'
|
- '${PORT-3000}:${PORT-3000}'
|
||||||
environment:
|
environment:
|
||||||
PORT: ${PORT}
|
PORT: ${PORT-3000}
|
||||||
env_file: './.env'
|
env_file: '.env'
|
||||||
|
@ -3,10 +3,10 @@ import fs from 'node:fs'
|
|||||||
|
|
||||||
import { build } from 'vite'
|
import { build } from 'vite'
|
||||||
|
|
||||||
const jsonResumeThemeCustom = new URL('../', import.meta.url)
|
const jsonResumeThemeCustom = new URL('./', import.meta.url)
|
||||||
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
|
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
|
||||||
const publicResumeOutputURL = new URL(
|
const publicResumeOutputURL = new URL(
|
||||||
'../../public/curriculum-vitae',
|
'../public/curriculum-vitae',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
)
|
)
|
||||||
|
|
@ -6,6 +6,7 @@
|
|||||||
<title><%= locals.basics.name %></title>
|
<title><%= locals.basics.name %></title>
|
||||||
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
||||||
<link rel="stylesheet" href="./styles/global.css" />
|
<link rel="stylesheet" href="./styles/global.css" />
|
||||||
|
<script defer type="module" src="./scripts/main.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -26,12 +27,15 @@
|
|||||||
<strong><%= locals.basics.name %></strong>
|
<strong><%= locals.basics.name %></strong>
|
||||||
</h3>
|
</h3>
|
||||||
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
<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>
|
</div>
|
||||||
<div class="contact-details clearfix">
|
<div class="contact-details clearfix">
|
||||||
<div class="detail">
|
|
||||||
<span class="info"><%= locals.basics.phone %></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
<span class="info">
|
<span class="info">
|
||||||
<a
|
<a
|
||||||
@ -70,61 +74,109 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="detail" id="work-experience">
|
<section class="section-separated">
|
||||||
<div class="icon">
|
<div class="detail" id="education">
|
||||||
<img src="./images/building-columns.svg" alt="work" />
|
<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>
|
||||||
<div class="info">
|
|
||||||
<h4 class="title text-uppercase">Expériences</h4>
|
<div class="detail" id="skills">
|
||||||
<ul class="list-unstyled clear-margin">
|
<div class="icon">
|
||||||
<% locals.work.forEach((experience) => { %>
|
<img src="./images/toolbox.svg" alt="toolbox" />
|
||||||
<li class="card card-nested clearfix">
|
</div>
|
||||||
<div class="content">
|
<div class="info">
|
||||||
<p class="clear-margin relative">
|
<h4 class="title text-uppercase">Compétences</h4>
|
||||||
<a href="<%= experience.website %>">
|
<div class="content">
|
||||||
<strong><%= experience.name %></strong>
|
<ul class="list-unstyled clear-margin">
|
||||||
</a>
|
<% locals.skills.forEach((skill) => { %>
|
||||||
</p>
|
<li class="card card-nested card-skills">
|
||||||
<p class="clear-margin relative">
|
<div class="skill-info">
|
||||||
<strong><%- experience.position %></strong>
|
<strong><%= skill.name %></strong>
|
||||||
</p>
|
<div class="space-top labels">
|
||||||
<p class="text-muted">
|
<% skill.keywords.forEach((keyword) => { %>
|
||||||
<small>
|
<p class="label label-keyword"><%= keyword %></p>
|
||||||
<span class="space-right">
|
<% }) %>
|
||||||
<%= date.format(new Date(experience.startDate),
|
</div>
|
||||||
'DD/MM/YYYY') %> - <%= date.format(new
|
</div>
|
||||||
Date(experience.endDate), 'DD/MM/YYYY') %>
|
</li>
|
||||||
</span>
|
<% }) %>
|
||||||
</small>
|
</ul>
|
||||||
</p>
|
</div>
|
||||||
<div class="experience-description">
|
</div>
|
||||||
<p><%- experience.summary %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="detail" id="skills">
|
<section class="section-separated">
|
||||||
<div class="icon">
|
<div class="detail" id="work-experience">
|
||||||
<img src="./images/toolbox.svg" alt="toolbox" />
|
<div class="icon">
|
||||||
</div>
|
<img src="./images/building-columns.svg" alt="work" />
|
||||||
<div class="info">
|
</div>
|
||||||
<h4 class="title text-uppercase">Compétences</h4>
|
<div class="info">
|
||||||
<div class="content">
|
<h4 class="title text-uppercase">Expériences</h4>
|
||||||
<ul class="list-unstyled clear-margin">
|
<ul class="list-unstyled clear-margin">
|
||||||
<% locals.skills.forEach((skill) => { %>
|
<% locals.work.filter((experience) =>
|
||||||
<li class="card card-nested card-skills">
|
experience.description == null).forEach((experience) => {
|
||||||
<div class="skill-info">
|
%>
|
||||||
<strong><%= skill.name %></strong>
|
<li class="card card-nested clearfix">
|
||||||
<div class="space-top labels">
|
<div class="content">
|
||||||
<% skill.keywords.forEach((keyword) => { %>
|
<p class="clear-margin relative">
|
||||||
<p class="label label-keyword"><%= keyword %></p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -132,63 +184,57 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
<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>
|
||||||
|
|
||||||
<div class="detail" id="education">
|
<ul class="list-unstyled clear-margin">
|
||||||
<div class="icon">
|
<% locals.work.filter((experience) =>
|
||||||
<img src="./images/graduation-cap.svg" alt="graduation" />
|
experience.description != null).forEach((experience) =>
|
||||||
</div>
|
{ %>
|
||||||
<div class="info">
|
<li class="card card-nested clearfix">
|
||||||
<h4 class="title text-uppercase">Éducation</h4>
|
<div class="content">
|
||||||
<div class="content">
|
<p class="clear-margin relative">
|
||||||
<ul class="list-unstyled clear-margin">
|
<a href="<%= experience.website %>">
|
||||||
<% locals.education.forEach((degree) => { %>
|
<strong><%= experience.name %></strong>
|
||||||
<li class="card card-nested">
|
</a>
|
||||||
<div class="content">
|
</p>
|
||||||
<p class="clear-margin relative">
|
<p class="clear-margin relative">
|
||||||
<strong><%= degree.studyType %></strong>
|
<strong><%- experience.position %></strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="clear-margin relative">
|
<p class="text-muted">
|
||||||
<strong><%= degree.score %></strong>
|
<small>
|
||||||
</p>
|
<span class="space-right">
|
||||||
<p class="text-muted clear-margin">
|
<%= date.format(new
|
||||||
<%= degree.institution %>
|
Date(experience.startDate), 'DD/MM/YYYY') %> -
|
||||||
</p>
|
<%= date.format(new Date(experience.endDate),
|
||||||
<p class="text-muted clear-margin">
|
'DD/MM/YYYY') %> (<%= experience.duration %>)
|
||||||
<small>
|
</span>
|
||||||
<%= degree.startDate %> <%= degree.endDate != null
|
</small>
|
||||||
? " - " + degree.endDate : "" %>
|
</p>
|
||||||
</small>
|
<div class="experience-description">
|
||||||
</p>
|
<p><%- experience.summary %></p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<% }) %>
|
</li>
|
||||||
</ul>
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
1964
jsonresume-theme-custom/package-lock.json
generated
1964
jsonresume-theme-custom/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,12 +9,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"modern-normalize": "1.1.0"
|
"jsonc-parser": "3.2.0",
|
||||||
|
"modern-normalize": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.7.11",
|
"@types/node": "20.2.1",
|
||||||
"date-and-time": "2.4.1",
|
"date-and-time": "3.0.0",
|
||||||
"vite": "3.0.9",
|
"vite": "4.3.8",
|
||||||
"vite-plugin-html": "3.2.0"
|
"vite-plugin-html": "3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
jsonresume-theme-custom/scripts/main.js
Normal file
5
jsonresume-theme-custom/scripts/main.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { DIVLO_BIRTHDAY, getAge } from '../../utils/getAge.ts'
|
||||||
|
|
||||||
|
const yearOld = document.getElementById('year-old')
|
||||||
|
|
||||||
|
yearOld.textContent = getAge(DIVLO_BIRTHDAY).toString()
|
@ -209,7 +209,6 @@ h5 {
|
|||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
@ -217,8 +216,6 @@ h5 {
|
|||||||
}
|
}
|
||||||
.label-keyword {
|
.label-keyword {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #7eb0db;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #357ebd;
|
border: 1px solid #357ebd;
|
||||||
@ -227,3 +224,6 @@ h5 {
|
|||||||
.label-keyword p {
|
.label-keyword p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.section-separated {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import { parse as JSONCParser } from 'jsonc-parser'
|
||||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
|
||||||
const jsonResumeURL = new URL('../resume.json', import.meta.url)
|
const jsonResumeURL = new URL('../resume.jsonc', import.meta.url)
|
||||||
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
||||||
encoding: 'utf-8'
|
encoding: 'utf-8'
|
||||||
})
|
})
|
||||||
const resume = JSON.parse(dataResumeStringJSON)
|
const resume = JSONCParser(dataResumeStringJSON)
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
/**
|
||||||
|
* Documentation: <https://vitejs.dev/config/>
|
||||||
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
assetsDir: './'
|
assetsDir: './'
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"i-am": "I am",
|
"i-am": "I am",
|
||||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
"description": "Developer Full Stack • Open-Source enthusiast",
|
||||||
"full-name": "Full name",
|
"full-name": "Full name",
|
||||||
"birth-date": "Birth date",
|
"birth-date": "Birth date",
|
||||||
"years-old": "years old",
|
"years-old": "years old",
|
||||||
"nationality": "Nationality",
|
"nationality": "Nationality",
|
||||||
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (second year)."
|
"description-bottom": "I am a student in computer science following the French training \"BUT Informatique\" and I am also a self-taught."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interests",
|
"title": "Interests",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Developer Full Stack Junior",
|
"title": "Developer Full Stack",
|
||||||
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Open-Source enthusiast",
|
||||||
|
"description": "For me, everyone should work, solve problems, build things and think together.<br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionate about High-Tech",
|
"title": "Passionate about High-Tech",
|
||||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Open-Source enthusiast",
|
|
||||||
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
"return-to-home-page": "Revenir à la page d'accueil ?",
|
"return-to-home-page": "Revenir à la page d'accueil ?",
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"server-error": "Erreur Interne du Serveur !",
|
"server-error": "Erreur Interne du Serveur !",
|
||||||
"not-found": "Cette page n'existe pas!"
|
"not-found": "Cette page n'existe pas !"
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"i-am": "Je suis",
|
"i-am": "Je suis",
|
||||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
|
||||||
"full-name": "Prénom NOM",
|
"full-name": "Prénom NOM",
|
||||||
"birth-date": "Date de naissance",
|
"birth-date": "Date de naissance",
|
||||||
"years-old": "ans",
|
"years-old": "ans",
|
||||||
"nationality": "Nationalité",
|
"nationality": "Nationalité",
|
||||||
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (deuxième année)."
|
"description-bottom": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Intérêts",
|
"title": "Intérêts",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Développeur Full Stack Junior",
|
"title": "Développeur Full Stack",
|
||||||
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Enthousiaste de l'Open-Source",
|
||||||
|
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionné de High-Tech",
|
"title": "Passionné de High-Tech",
|
||||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Enthousiaste de l'Open-Source",
|
|
||||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
5
next-env.d.ts
vendored
5
next-env.d.ts
vendored
@ -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.
|
|
@ -2,11 +2,12 @@ const nextPWA = require('next-pwa')({
|
|||||||
disable: process.env.NODE_ENV !== 'production',
|
disable: process.env.NODE_ENV !== 'production',
|
||||||
dest: 'public'
|
dest: 'public'
|
||||||
})
|
})
|
||||||
const nextTranslate = require('next-translate')
|
const nextTranslate = require('next-translate-plugin')
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
module.exports = nextTranslate(
|
const nextConfig = {
|
||||||
nextPWA({
|
reactStrictMode: true,
|
||||||
reactStrictMode: true
|
output: 'standalone'
|
||||||
})
|
}
|
||||||
)
|
|
||||||
|
module.exports = nextTranslate(nextPWA(nextConfig))
|
||||||
|
32027
package-lock.json
generated
32027
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
111
package.json
111
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "divlo",
|
||||||
"version": "2.4.1",
|
"version": "2.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -14,85 +14,88 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build": "npm run resume:build && next build",
|
"build": "npm run resume:build && next build",
|
||||||
"export": "next export",
|
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint-cli2",
|
"lint:markdown": "markdownlint-cli2",
|
||||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
|
"lint:eslint": "eslint \".\" --ignore-path \".gitignore\"",
|
||||||
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test:unit": "cypress run --component",
|
"test:unit": "cypress run --component",
|
||||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"",
|
||||||
"test:lighthouse": "lhci autorun",
|
"test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
|
||||||
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"",
|
||||||
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
"resume:build": "node ./jsonresume-theme-custom/build.js",
|
||||||
"resume:build": "node ./jsonresume-theme-custom/scripts/build.js",
|
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
"deploy": "vercel",
|
"deploy": "vercel",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/montserrat": "4.5.12",
|
"@fontsource/montserrat": "5.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.1.2",
|
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@giscus/react": "2.2.0",
|
"@giscus/react": "2.2.8",
|
||||||
"clsx": "1.2.1",
|
"clsx": "1.2.1",
|
||||||
"date-and-time": "2.4.1",
|
"date-and-time": "3.0.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"html-react-parser": "3.0.4",
|
"html-react-parser": "3.0.16",
|
||||||
"next": "12.2.5",
|
"katex": "0.16.7",
|
||||||
"next-mdx-remote": "4.1.0",
|
"next": "13.4.3",
|
||||||
|
"next-mdx-remote": "4.4.1",
|
||||||
"next-pwa": "5.6.0",
|
"next-pwa": "5.6.0",
|
||||||
"next-themes": "0.2.0",
|
"next-themes": "0.2.1",
|
||||||
"next-translate": "1.5.0",
|
"next-translate": "2.0.5",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"read-pkg": "7.1.0",
|
"read-pkg": "8.0.0",
|
||||||
|
"rehype-katex": "6.0.3",
|
||||||
"rehype-raw": "6.1.1",
|
"rehype-raw": "6.1.1",
|
||||||
"rehype-slug": "5.0.1",
|
"rehype-slug": "5.1.0",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"sharp": "0.30.7",
|
"remark-math": "5.1.1",
|
||||||
"shiki": "0.11.1",
|
"sharp": "0.32.1",
|
||||||
|
"shiki": "0.14.2",
|
||||||
"unified": "10.1.2",
|
"unified": "10.1.2",
|
||||||
"unist-util-visit": "4.1.1",
|
"unist-util-visit": "4.1.2",
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "17.0.3",
|
"@commitlint/cli": "17.6.3",
|
||||||
"@commitlint/config-conventional": "17.0.3",
|
"@commitlint/config-conventional": "17.6.3",
|
||||||
"@lhci/cli": "0.9.0",
|
"@saithodev/semantic-release-backmerge": "3.2.0",
|
||||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
|
||||||
"@semantic-release/git": "10.0.1",
|
"@semantic-release/git": "10.0.1",
|
||||||
"@tailwindcss/typography": "0.5.4",
|
"@tailwindcss/typography": "0.5.9",
|
||||||
"@types/node": "18.7.11",
|
"@tsconfig/strictest": "2.0.1",
|
||||||
"@types/react": "18.0.17",
|
"@types/node": "20.2.1",
|
||||||
|
"@types/react": "18.2.6",
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.34.0",
|
"@typescript-eslint/eslint-plugin": "5.59.6",
|
||||||
"autoprefixer": "10.4.8",
|
"autoprefixer": "10.4.14",
|
||||||
"cypress": "10.6.0",
|
"cypress": "12.12.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "5.0.1",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.41.0",
|
||||||
"eslint-config-conventions": "3.0.0",
|
"eslint-config-conventions": "9.0.0",
|
||||||
"eslint-config-next": "12.2.5",
|
"eslint-config-next": "13.4.3",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.1.1",
|
||||||
"eslint-plugin-unicorn": "43.0.2",
|
"eslint-plugin-unicorn": "47.0.0",
|
||||||
"html-w3c-validator": "1.2.0",
|
"html-w3c-validator": "1.3.0",
|
||||||
"husky": "8.0.1",
|
"husky": "8.0.3",
|
||||||
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
||||||
"lint-staged": "13.0.3",
|
"lint-staged": "13.2.2",
|
||||||
"markdownlint-cli2": "0.5.1",
|
"markdownlint-cli2": "0.7.1",
|
||||||
"postcss": "8.4.16",
|
"markdownlint-rule-relative-links": "1.2.0",
|
||||||
"prettier": "2.7.1",
|
"next-translate-plugin": "2.0.5",
|
||||||
"prettier-plugin-tailwindcss": "0.1.13",
|
"postcss": "8.4.23",
|
||||||
"semantic-release": "19.0.4",
|
"prettier": "2.8.8",
|
||||||
"start-server-and-test": "1.14.0",
|
"prettier-plugin-tailwindcss": "0.3.0",
|
||||||
"tailwindcss": "3.1.8",
|
"semantic-release": "21.0.2",
|
||||||
"typescript": "4.7.4",
|
"start-server-and-test": "2.0.0",
|
||||||
"vercel": "28.1.1"
|
"tailwindcss": "3.3.2",
|
||||||
|
"typescript": "5.0.4",
|
||||||
|
"vercel": "29.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import type { FooterProps } from 'components/Footer'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
|
||||||
|
|
||||||
interface Error404Props extends FooterProps {}
|
interface Error404Props extends FooterProps {}
|
||||||
|
|
||||||
@ -15,12 +14,11 @@ const Error404: NextPage<Error404Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='404 | Divlo' />
|
<Head title='404 | Divlo' />
|
||||||
|
<ErrorPage
|
||||||
<Header showLanguage />
|
statusCode={404}
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
message={t('errors:not-found')}
|
||||||
<ErrorPage statusCode={404} message={t('errors:not-found')} />
|
version={version}
|
||||||
</main>
|
/>
|
||||||
<Footer version={version} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import type { FooterProps } from 'components/Footer'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
|
||||||
|
|
||||||
interface Error500Props extends FooterProps {}
|
interface Error500Props extends FooterProps {}
|
||||||
|
|
||||||
@ -15,12 +14,11 @@ const Error500: NextPage<Error500Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='500 | Divlo' />
|
<Head title='500 | Divlo' />
|
||||||
|
<ErrorPage
|
||||||
<Header showLanguage />
|
statusCode={500}
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
message={t('errors:server-error')}
|
||||||
<ErrorPage statusCode={500} message={t('errors:server-error')} />
|
version={version}
|
||||||
</main>
|
/>
|
||||||
<Footer version={version} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AppProps } from 'next/app'
|
import type { AppType } from 'next/app'
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import UniversalCookie from 'universal-cookie'
|
import UniversalCookie from 'universal-cookie'
|
||||||
@ -13,7 +13,7 @@ const universalCookie = new UniversalCookie()
|
|||||||
/** how long in seconds, until the cookie expires (10 years) */
|
/** how long in seconds, until the cookie expires (10 years) */
|
||||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||||
|
|
||||||
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
|
const Application: AppType = ({ Component, pageProps }) => {
|
||||||
const { lang } = useTranslation()
|
const { lang } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||||
|
import Image from 'next/image'
|
||||||
import { MDXRemote } from 'next-mdx-remote'
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
import Giscus from '@giscus/react'
|
import Giscus from '@giscus/react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
import type { Post } from 'utils/blog'
|
import type { Post } from 'utils/blog'
|
||||||
|
|
||||||
interface BlogPostPageProps extends FooterProps {
|
interface BlogPostPageProps extends FooterProps {
|
||||||
@ -26,41 +30,58 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
|
||||||
<div className='my-10 flex flex-col items-center'>
|
<div className='my-10 flex flex-col items-center text-center'>
|
||||||
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
||||||
<p className='mt-2' data-cy='blog-post-date'>
|
<p className='mt-2' data-cy='blog-post-date'>
|
||||||
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='prose mb-10 px-8'>
|
<div className='prose mb-10'>
|
||||||
<MDXRemote
|
<div className='px-8'>
|
||||||
{...post.source}
|
<MDXRemote
|
||||||
components={{
|
{...post.source}
|
||||||
a: (props: React.ComponentPropsWithoutRef<'a'>) => {
|
components={{
|
||||||
if (props.href?.startsWith('#') ?? false) {
|
img: (properties) => {
|
||||||
return <a {...props} />
|
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='Divlo/Divlo'
|
||||||
/>
|
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
||||||
<Giscus
|
category='General'
|
||||||
id='comments'
|
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
||||||
repo='Divlo/Divlo'
|
mapping='pathname'
|
||||||
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
reactionsEnabled='1'
|
||||||
category='General'
|
emitMetadata='0'
|
||||||
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
inputPosition='top'
|
||||||
mapping='pathname'
|
theme={theme}
|
||||||
reactionsEnabled='1'
|
lang='en'
|
||||||
emitMetadata='0'
|
loading='lazy'
|
||||||
inputPosition='top'
|
/>
|
||||||
theme={theme}
|
</div>
|
||||||
lang='en'
|
|
||||||
loading='lazy'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer version={version} />
|
<Footer version={version} />
|
||||||
@ -71,7 +92,7 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
|||||||
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const slug = context?.params?.slug
|
const slug = context?.params?.['slug']
|
||||||
const { getPostBySlug } = await import('utils/blog')
|
const { getPostBySlug } = await import('utils/blog')
|
||||||
const post = await getPostBySlug(slug)
|
const post = await getPostBySlug(slug)
|
||||||
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||||
import type { PostMetadata } from 'utils/blog'
|
import type { PostMetadata } from 'utils/blog'
|
||||||
|
|
||||||
@ -38,23 +39,26 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
|
|||||||
'DD/MM/YYYY'
|
'DD/MM/YYYY'
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Link href={`/blog/${post.slug}`} key={index} locale='en'>
|
<Link
|
||||||
<a data-cy={post.slug}>
|
href={`/blog/${post.slug}`}
|
||||||
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
key={index}
|
||||||
<h2
|
locale='en'
|
||||||
data-cy='blog-post-title'
|
data-cy={post.slug}
|
||||||
className='text-xl font-semibold'
|
>
|
||||||
>
|
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
||||||
{post.frontmatter.title}
|
<h2
|
||||||
</h2>
|
data-cy='blog-post-title'
|
||||||
<p data-cy='blog-post-date' className='mt-2'>
|
className='text-xl font-semibold'
|
||||||
{postPublishedOn}
|
>
|
||||||
</p>
|
{post.frontmatter.title}
|
||||||
<p data-cy='blog-post-description' className='mt-3'>
|
</h2>
|
||||||
{post.frontmatter.description}
|
<p data-cy='blog-post-date' className='mt-2'>
|
||||||
</p>
|
{postPublishedOn}
|
||||||
</ShadowContainer>
|
</p>
|
||||||
</a>
|
<p data-cy='blog-post-description' className='mt-3'>
|
||||||
|
{post.frontmatter.description}
|
||||||
|
</p>
|
||||||
|
</ShadowContainer>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import type { GetStaticProps, NextPage } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { RevealFade } from 'components/design/RevealFade'
|
import { RevealFade } from 'components/design/RevealFade'
|
||||||
@ -11,7 +11,8 @@ import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
|||||||
import { Skills } from 'components/Skills'
|
import { Skills } from 'components/Skills'
|
||||||
import { OpenSource } from 'components/OpenSource'
|
import { OpenSource } from 'components/OpenSource'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import type { FooterProps } from 'components/Footer'
|
||||||
|
import { Footer } from 'components/Footer'
|
||||||
|
|
||||||
interface HomeProps extends FooterProps {}
|
interface HomeProps extends FooterProps {}
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@ publishedOn: '2022-02-23T08:00:18.758Z'
|
|||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
|
||||||
Have you already heard of "**Clean Code**" or "**Design Patterns**" ?
|
Have you already heard of "**Clean Code**" or "**Design Patterns**"?
|
||||||
|
|
||||||
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
|
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
|
||||||
|
|
||||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||||
|
|
||||||
## Definition : Clean Code
|
## Definition: Clean Code
|
||||||
|
|
||||||
A clean code is a code that is **easy** to **read** and easy to **understand**.
|
A clean code is a code that is **easy** to **read** and easy to **understand**.
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ We could ask ourselves, what is **easy** to **read** and easy to **understand**
|
|||||||
|
|
||||||
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
|
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
|
||||||
|
|
||||||
## Why is it so important ?
|
## Why is it so important?
|
||||||
|
|
||||||
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ For example the [Linux kernel](https://www.kernel.org/), is one of the biggest o
|
|||||||
|
|
||||||
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
|
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
|
||||||
|
|
||||||
## Definition : Design Patterns
|
## Definition: Design Patterns
|
||||||
|
|
||||||
These **rules** and **conventions** are so called **Design Patterns**.
|
These **rules** and **conventions** are so called **Design Patterns**.
|
||||||
|
|
||||||
@ -77,8 +77,8 @@ setTimeout(restart, 86400000)
|
|||||||
##### Example (good way)
|
##### Example (good way)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
|
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000
|
||||||
setTimeout(restart, MILLISECONDS_IN_A_DAY)
|
setTimeout(restart, MILLISECONDS_IN_ONE_DAY)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -131,7 +131,9 @@ const printCar = (car: Car): void => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Boolean names (Prefix: is, has, can)
|
#### Boolean names
|
||||||
|
|
||||||
|
The name of a boolean variable should be a question, and the answer should be true or false. We can use prefixes like `is`, `has`, `can` to make it more explicit and we should avoid negation.
|
||||||
|
|
||||||
##### Example (bad way)
|
##### Example (bad way)
|
||||||
|
|
||||||
@ -171,7 +173,10 @@ We have to keep it as simple as possible, not to implement features that are not
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const createFile = async (name: string, isTemporary: boolean = false) => {
|
const createFile = async (
|
||||||
|
name: string,
|
||||||
|
isTemporary: boolean = false
|
||||||
|
): Promise<void> => {
|
||||||
if (isTemporary) {
|
if (isTemporary) {
|
||||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
return await fs.promises.writeFile(path.join('temporary', name), '')
|
||||||
}
|
}
|
||||||
@ -187,11 +192,11 @@ const createFile = async (name: string, isTemporary: boolean = false) => {
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const createFile = async (name: string) => {
|
const createFile = async (name: string): Promise<void> => {
|
||||||
await fs.promises.writeFile(name, '')
|
await fs.promises.writeFile(name, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTemporaryFile = async (name: string) => {
|
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||||
await createFile(path.join('temporary', name))
|
await createFile(path.join('temporary', name))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -210,7 +215,7 @@ The End To End (e2e) and Unit tests should document what is the behavior intende
|
|||||||
|
|
||||||
### Avoid comments
|
### Avoid comments
|
||||||
|
|
||||||
One of the most important rule of "Clean Code" : If you need to add **comments**, it's because your code is **not clean**.
|
One of the most important rule of "Clean Code": If you need to add **comments**, it's because your code is **not clean**.
|
||||||
|
|
||||||
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.
|
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.
|
||||||
|
|
239
posts/git-ultimate-guide.md
Normal file
239
posts/git-ultimate-guide.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
title: '🗓️ Git version control: Ultimate Guide'
|
||||||
|
description: 'What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow.'
|
||||||
|
isPublished: true
|
||||||
|
publishedOn: '2022-10-27T14:33:07.465Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
Hello! 👋
|
||||||
|
|
||||||
|
Welcome to the Ultimate Guide to master `git` in your daily workflow, we will see what are the most used commands, what are the best practices, and tips and tricks.
|
||||||
|
|
||||||
|
This guide is a summary of the most important things to know when working with `git`, and in general, will link to the official documentation of `git` or other resources for more details, it is on purpose to not go in depth in each topic, it allows to summarize `git` and vocabulary about it (you can use it as a `git` cheatsheet).
|
||||||
|
|
||||||
|
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
**Git** is a free and open-source distributed **version control system** for keeping track of changes across a set of files.
|
||||||
|
|
||||||
|
Git was originally authored by [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds) in 2005 for the development of the [Linux kernel](https://kernel.org/).
|
||||||
|
|
||||||
|
Git allows:
|
||||||
|
|
||||||
|
- to be able to work with several people on the same codebase.
|
||||||
|
- track changes to know who did what and when.
|
||||||
|
- revert changes.
|
||||||
|
|
||||||
|
Git is **decentralized**, which means that every developer has a full copy of the repository and the complete history of the project.
|
||||||
|
|
||||||
|
## Get started with `git` and `.gitconfig` config file
|
||||||
|
|
||||||
|
The first thing you should do when you install Git is to set your user name and email address.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git config --global user.name "Username"
|
||||||
|
git config --global user.email "email@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
These configurations are stored in the `.gitconfig` file in your home directory (e.g: `~/.gitconfig`) with this format:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[user]
|
||||||
|
name = Username
|
||||||
|
email = email@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config).
|
||||||
|
|
||||||
|
## How `git` works?
|
||||||
|
|
||||||
|
Each `git` project is called a **repository** (or **repo** for short) and it contains all the files and folders for a project, as well as each file's revision history (**commits**) stored in the `.git` folder.
|
||||||
|
|
||||||
|
The history of a repository is represented by a graph.
|
||||||
|
|
||||||
|
Each node is called commit and contains:
|
||||||
|
|
||||||
|
- an instantaneous view (snapshot) of the state of the repository at a specific moment
|
||||||
|
- metadata: message, author, creation date, etc.
|
||||||
|
|
||||||
|
Commits are **snapshots** (not diffs on each file) of the project at specific moments in time.
|
||||||
|
|
||||||
|
There are several areas where the files in your project will live in Git:
|
||||||
|
|
||||||
|
- **Working directory**: the files that you see in your computer's file system.
|
||||||
|
- **Staging area**: the files that will go into your next commit (files added with `git add <filename>` command).
|
||||||
|
- **Local repository**: the `.git` directory, which contains all of your project's commits, branches, etc. (files added with `git commit -m "message"` command).
|
||||||
|
- **Remote repository**: the `.git` directory in a remote server (files added with `git push` command).
|
||||||
|
|
||||||
|
## Commands cheatsheet
|
||||||
|
|
||||||
|
You can find the official documentation of `git` commands at [git-scm.com/docs](https://git-scm.com/docs).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Initialize a new git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Clone a repository
|
||||||
|
git clone <url>
|
||||||
|
|
||||||
|
# Add all the files to staging area
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Add specific file to staging area
|
||||||
|
git add <file>
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git commit -m "chore: initial commit"
|
||||||
|
|
||||||
|
# Add remote repository
|
||||||
|
git remote add <remote> <url>
|
||||||
|
# The main <remote> is often called `origin`
|
||||||
|
|
||||||
|
# Add forked repository
|
||||||
|
git remote add <remote> <url>
|
||||||
|
# The forked <remote> is often called `upstream`
|
||||||
|
|
||||||
|
# List all the remotes
|
||||||
|
git remote
|
||||||
|
|
||||||
|
# Sync forked repository
|
||||||
|
git fetch <remote>
|
||||||
|
git merge <remote>/<branch>
|
||||||
|
|
||||||
|
# Push changes to remote repository
|
||||||
|
git push <remote>
|
||||||
|
|
||||||
|
# Pull changes from remote repository
|
||||||
|
git pull <remote>
|
||||||
|
|
||||||
|
# Show the status of the working tree
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Show the commit history
|
||||||
|
git log
|
||||||
|
|
||||||
|
# Create a new branch
|
||||||
|
git checkout -b <branch>
|
||||||
|
|
||||||
|
# Switch to a branch (or tag or commit)
|
||||||
|
git checkout <branch>
|
||||||
|
|
||||||
|
# Merge a branch into the current branch
|
||||||
|
git merge <branch>
|
||||||
|
|
||||||
|
# Delete a branch
|
||||||
|
git branch --delete <branch>
|
||||||
|
git push <remote> --delete <branch>
|
||||||
|
|
||||||
|
# Fetch branches from remote repository and prune
|
||||||
|
git fetch --prune
|
||||||
|
|
||||||
|
# Revert a commit
|
||||||
|
git revert <commit>
|
||||||
|
|
||||||
|
# Change several past commits (interactive rebase)
|
||||||
|
# HEAD points to the current consulted commit.
|
||||||
|
git rebase --interactive HEAD~<number-of-commits>
|
||||||
|
|
||||||
|
# Reset the current branch, delete all commits since <branch> (without removing the changes)
|
||||||
|
git reset --soft <branch>
|
||||||
|
|
||||||
|
# Apply the changes introduced by some existing commits
|
||||||
|
git cherry-pick <commit>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.gitignore` file
|
||||||
|
|
||||||
|
The `.gitignore` file is a text file that tells `git` which files (or patterns) it should ignore.
|
||||||
|
|
||||||
|
The `.gitignore` file is usually placed in the root directory of the repository.
|
||||||
|
|
||||||
|
We usually ignore files that are generated by the build process or files that contain sensitive information.
|
||||||
|
|
||||||
|
Example of `.gitignore` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.env
|
||||||
|
build
|
||||||
|
*.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.gitkeep` file
|
||||||
|
|
||||||
|
The `.gitkeep` file is a file that is used to keep an empty directory in a Git repository.
|
||||||
|
|
||||||
|
This is useful when you want to keep an empty directory in your repository but you don't want to commit any file inside it.
|
||||||
|
|
||||||
|
## Git remote repositories (GitHub/GitLab)
|
||||||
|
|
||||||
|
Once you are ready to share your code over the internet, you will need to create a remote repository on a service like [GitHub](https://github.com) or [GitLab](https://gitlab.com).
|
||||||
|
|
||||||
|
There are many other services, you can also self-host your own Git server.
|
||||||
|
|
||||||
|
### SSH vs HTTPS authentication
|
||||||
|
|
||||||
|
Once you have created a remote repository, you will need to authenticate to push and pull changes.
|
||||||
|
|
||||||
|
There are two main ways to authenticate:
|
||||||
|
|
||||||
|
- **SSH**: you will need to generate an SSH key pair and add the public key to your remote repository.
|
||||||
|
- **HTTPS**: you will need to provide your username and password each time you push or pull changes.
|
||||||
|
|
||||||
|
SSH authentication is the recommended way to authenticate to a remote repository.
|
||||||
|
|
||||||
|
You can find more information about SSH authentication in the [official documentation](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key).
|
||||||
|
|
||||||
|
### Sign `git` commits with `gpg`
|
||||||
|
|
||||||
|
As we have seen in the [Get started with `git` and `.gitconfig` config file](#get-started-with-git-and-gitconfig-config-file) section, we can configure `git` with a name and email address with a value of our choice.
|
||||||
|
|
||||||
|
That means that **anyone can create a commit with any name and email address and claim to be whoever they want** when they create a commit.
|
||||||
|
|
||||||
|
To avoid this, you can sign your commits with a <abbr title="GNU Privacy Guard">[GPG](https://gnupg.org/)</abbr> key.
|
||||||
|
|
||||||
|
You can find more information about signing commits in the [official documentation](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work).
|
||||||
|
|
||||||
|
### Continous Integration/Continuous Delivery (CI/CD)
|
||||||
|
|
||||||
|
Once you have your code in a remote repository, everyone (with access) can potentially start contributing to the project. This is great, but it also means that you need to have a way to ensure that your code is working as expected for each change in the project.
|
||||||
|
|
||||||
|
You could do it manually, depending on the size and the complexity of the project, but it could be a tedious task.
|
||||||
|
|
||||||
|
Instead, you can use a **Continuous Integration** (CI) service to automate the process of testing your code, running linting, unit tests, e2e tests, etc.
|
||||||
|
|
||||||
|
There are many CI services, but the most popular ones are [GitHub Actions](https://github.com/features/actions), [GitLab CI](https://docs.gitlab.com/ee/ci/), [CircleCI](https://circleci.com/), [Travis CI](https://travis-ci.org/), and many others...
|
||||||
|
|
||||||
|
Then, once your code is ready, tested and working as expected, you can use a **Continuous Delivery** (CD) service to automate the process of **deploying your code**.
|
||||||
|
|
||||||
|
CI/CD services are usually integrated with remote repositories, so you can configure them to run automatically when you push changes to the remote repository.
|
||||||
|
|
||||||
|
## Best practices and `git` workflows
|
||||||
|
|
||||||
|
Commit messages are very important, they are a way to easily know what has changed in the project.
|
||||||
|
|
||||||
|
There are many conventions for commit messages, but the most popular one is the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||||
|
|
||||||
|
Then, we can use the commit messages to automatically determine a [semantic version](https://semver.org/) for the next release of the project.
|
||||||
|
|
||||||
|
When multiple developers are working on the same project, it is important to organize the work in a way that everyone can work on different features without conflicts (changes in the same files).
|
||||||
|
|
||||||
|
There are many ways to organize the work, but the most popular ones are:
|
||||||
|
|
||||||
|
- [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)
|
||||||
|
- [GitHub Flow](https://guides.github.com/introduction/flow/)
|
||||||
|
- [Trunk-based development](https://trunkbaseddevelopment.com/)
|
||||||
|
|
||||||
|
They are called **Git workflows**, or **Git branching strategies**.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
`git` is the tool that every programmer should know to do collaborative work (not only, `git` is also very powerful even when working alone) and keep track of changes across a set of files.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [Git official website and documentation](https://git-scm.com/)
|
||||||
|
- [Git Explained in 100 Seconds](https://www.youtube.com/watch?v=hwP7WQkmECE)
|
||||||
|
- [Understand Git in 7 minutes](https://www.jesuisundev.com/en/understand-git-in-7-minutes/)
|
||||||
|
- [How (and why) to sign Git commits | With Blue Ink](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html?utm_source=tiktok&utm_campaign=codetok-sign)
|
||||||
|
- [What Are the Best Git Branching Strategies](https://www.flagship.io/git-branching-strategies/)
|
@ -35,18 +35,11 @@ In this section, I will explain what technologies I used to make this blog, and
|
|||||||
|
|
||||||
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
||||||
|
|
||||||
I decided to keep things simple, here are the 2 main features missing on my blog:
|
|
||||||
|
|
||||||
- Comments (you can interact with me on my Twitter account)
|
|
||||||
- Views counter
|
|
||||||
|
|
||||||
That not mean that these features will never be implemented, but to avoid the need of a database now, I dropped out these features.
|
|
||||||
|
|
||||||
### Technologies
|
### Technologies
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/)
|
- [Next.js](https://nextjs.org/)
|
||||||
|
|
||||||
It allows to have a server-side rendered website, that means that it is faster and easier to have a good SEO (Search Engine Optimization) than a SPA (Single Page Application).
|
It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>.
|
||||||
|
|
||||||
- [MDX](https://mdxjs.com/)
|
- [MDX](https://mdxjs.com/)
|
||||||
|
|
343
posts/programming-challenges.md
Normal file
343
posts/programming-challenges.md
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
---
|
||||||
|
title: '🧠 Programming Challenges'
|
||||||
|
description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.'
|
||||||
|
isPublished: true
|
||||||
|
publishedOn: '2023-05-21T10:20:18.837Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
Hello! 👋
|
||||||
|
|
||||||
|
As **performance** and **reliability** is more and more important in software development, it is important to know how to write **efficient code**, and also learn to **not rely on every possible dependency of the world**, when it is not worth it.
|
||||||
|
|
||||||
|
The more dependencies we add to our projects, the greater the complexity and maintenance overhead becomes. Each additional dependency requires understanding its functionality, <abbr title="Application Programming Interface">API</abbr>, and potential conflicts with other dependencies. This complexity makes the codebase harder to maintain, and it also poses significant security risks.
|
||||||
|
|
||||||
|
We don't want to "reinvent the wheel" and rewrite everything from scratch for each project. In fact, you are **always depending on something** when you are writing your software. At the very least, you are dependent on the programming language you are using. Even if you are doing very low-level stuff, you are still depending on something: hardware.
|
||||||
|
|
||||||
|
However, it is important to draw a line between what dependencies are worth the cost and which are not.
|
||||||
|
|
||||||
|
Most likely adding a [JavaScript npm package `is-odd`](https://www.npmjs.com/package/is-odd) to check if a number is odd or even for example, is not worth it. Writing it ourselves is easier and allows a better maintenance in the long term.
|
||||||
|
|
||||||
|
Learning **how to solve problems** and how to write efficient code is very important and also a very broad and complicated topic, so this blog post will only be an **introduction to the subject**, and will not go in depth.
|
||||||
|
|
||||||
|
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||||
|
|
||||||
|
## What is Competitive Programming?
|
||||||
|
|
||||||
|
**Competitive programming** consists of solving correctly and efficiently **well-defined problems** by writing **computer programs** under specified **constraints**. Typically a solution to a problem is a combination of well-known techniques and new insights.
|
||||||
|
|
||||||
|
There are many famous competitions: [Google Code Jam](https://codingcompetitions.withgoogle.com/codejam), [Facebook Hacker Cup](https://www.facebook.com/codingcompetitions/hacker-cup), [International Olympiad in Informatics](https://ioinformatics.org/), [International Collegiate Programming Contest](https://icpc.global/), [LeetCode](https://leetcode.com/), [CodinGame](https://www.codingame.com/), etc.
|
||||||
|
|
||||||
|
The most common programming languages used for Competitive Programming are: **C++**, **Python** and **Java**. However the design of the algorithms and data structures are applicable to **any programming language**.
|
||||||
|
|
||||||
|
All examples solutions on this blog post will be done in **Python**.
|
||||||
|
|
||||||
|
## Topics to explore/learn with Competitive Programming
|
||||||
|
|
||||||
|
- Time/Space complexity and Big O Notation
|
||||||
|
|
||||||
|
- Sorting: Sorting algorithms and Binary search
|
||||||
|
|
||||||
|
- Data structures: Arrays (1D, 2D: Matrix, 3D, Multidimensional), Dictionaries, Linked lists, Stack, Queue, Trees, Graphs, Heaps, etc.
|
||||||
|
|
||||||
|
- Complete search: Generating Subsets, Permutations, Combinations, etc.
|
||||||
|
|
||||||
|
- Greedy algorithms: Coin problem, Scheduling, Minimizing sums, etc.
|
||||||
|
|
||||||
|
- Dynamic programming: Fibonacci, Coin problem, Knapsack, etc.
|
||||||
|
|
||||||
|
- Bit manipulation: Bit representation, Bit operations, etc.
|
||||||
|
|
||||||
|
- Shortest path: Dijkstra, Bellman-Ford, Floyd-Warshall, etc.
|
||||||
|
|
||||||
|
- String: Trie structure, String hashing, Z-algorithm, etc.
|
||||||
|
|
||||||
|
You can see there are lot of concepts to learn and explore, and it is not an exhaustive list. On this blog post, we will only see the first topic: **Time/Space complexity and Big O Notation**.
|
||||||
|
|
||||||
|
## Time/Space complexity and Big O Notation
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
An Algorithm is a finite sequence of well-defined instructions, that have to be given to the computer to perform a specific task. In this context, the variation can occur the way how the instructions are defined. There can be **any number of ways**, a specific set of instructions can be defined **to perform the same task**. Also, with options available to choose any one of the available programming languages, the instructions can take any form of syntax along with the performance boundaries of the chosen programming language. We also indicated the algorithm to be performed in a computer, which leads to the next variation, in terms of the operating system, processor, hardware, etc. that are used, which can also influence the way an algorithm can be performed.
|
||||||
|
|
||||||
|
Different factors can influence the outcome of an algorithm being executed, it is wise to understand how efficiently such programs are used to perform a task. To gauge this, we require to evaluate:
|
||||||
|
|
||||||
|
- The **Space complexity** of an algorithm **quantifies** the amount of **space or memory taken** by an algorithm to run based on the size of the input.
|
||||||
|
- The **Time complexity** of an algorithm **quantifies** the amount of **time taken** by an algorithm to run based on the size of the input.
|
||||||
|
|
||||||
|
We more often talk about the **time complexity** than space complexity of an algorithm, because we can reuse memory unlike time and memory is cheap nowadays.
|
||||||
|
|
||||||
|
**Big O Notation** describes the complexity of an algorithm in terms of **how quickly it grows relative to the input size $n$ (e.g: length of the string, size of the array etc.)** by defining the $N$ number of operations that are done on it.
|
||||||
|
Example of Big O notation: $O(n^2)$.
|
||||||
|
|
||||||
|
### Time complexity
|
||||||
|
|
||||||
|
Time complexity **measures** the **time taken** **to execute each statement** of code in an algorithm. It is not going to examine the total execution time of an algorithm. Rather, it is going to give information about the variation (increase or decrease) in execution time when the number of operations (increase or decrease) in an algorithm.
|
||||||
|
|
||||||
|
There are many rules to calculate the time complexity of an algorithm.
|
||||||
|
|
||||||
|
#### Loops
|
||||||
|
|
||||||
|
A common reason why an algorithm is slow is that it contains many loops that go through the input. The more nested loops the algorithm contains, the slower it will run.
|
||||||
|
|
||||||
|
If there are $k$ nested loops, the time complexity of the algorithm will be $O(n^k)$.
|
||||||
|
|
||||||
|
##### Example $O(n)$
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(n):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# or with a while loop
|
||||||
|
iteration = 0
|
||||||
|
while iteration < n:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Example $O(n^2)$
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(n):
|
||||||
|
for iteration2 in range(n):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Example $O(n^3)$
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(n):
|
||||||
|
for iteration2 in range(n):
|
||||||
|
for iteration3 in range(n):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
etc.
|
||||||
|
|
||||||
|
#### Order of magnitude
|
||||||
|
|
||||||
|
A time complexity does not tell us the exact number of times the code inside a loop is executed, but it only shows the **order of magnitude**.
|
||||||
|
|
||||||
|
In the following examples, the time complexity of the algorithms is $O(n)$ but the number of operations is different.
|
||||||
|
|
||||||
|
##### Example 1
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(0, n * 3, 1):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Number of operations: $3n$
|
||||||
|
|
||||||
|
##### Example 2
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(0, n + 5, 1):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Number of operations: $n + 5$
|
||||||
|
|
||||||
|
##### Example 3
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(0, n, 2):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Number of operations: ${n \over 2}$
|
||||||
|
|
||||||
|
#### Phases
|
||||||
|
|
||||||
|
If the algorithms consists of consecutive phases, the total time complexity is the largest time complexity of a single phase because it is usually the bottleneck of the code.
|
||||||
|
|
||||||
|
The following code consists of 3 phases, with time complexities $O(n)$, $O(n^2)$ and $O(n)$. Thus the total time complexity is $O(n^2)$.
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(n):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for iteration in range(n):
|
||||||
|
for iteration2 in range(n):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for iteration in range(n):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Several variables
|
||||||
|
|
||||||
|
Sometimes the time complexity depends on several factors. In this case, the time complexity formula contains several variables: $O(nm)$.
|
||||||
|
|
||||||
|
```python
|
||||||
|
for iteration in range(n):
|
||||||
|
for iteration2 in range(m):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Recursion
|
||||||
|
|
||||||
|
The time complexity of a recursive function depends on the number of times it is called and the time complexity of a single call. The total time complexity is the product of these values.
|
||||||
|
|
||||||
|
##### Example 1
|
||||||
|
|
||||||
|
The call `recursive(n)` causes $n$ calls and the time complexity of each call is $O(1)$. Thus the total time complexity is $O(n)$.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def recursive(n: int):
|
||||||
|
if n != 1:
|
||||||
|
recursive(n - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Example 2
|
||||||
|
|
||||||
|
```python
|
||||||
|
def recursive(n: int):
|
||||||
|
if n != 1:
|
||||||
|
recursive(n - 1)
|
||||||
|
recursive(n - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, `recursive(n)` causes 2 other calls except for $n = 1$.
|
||||||
|
|
||||||
|
The following table shows the function calls produced by this single call:
|
||||||
|
|
||||||
|
| function call | number of calls |
|
||||||
|
| ------------- | --------------- |
|
||||||
|
| $g(n)$ | $1$ |
|
||||||
|
| $g(n - 1)$ | $2$ |
|
||||||
|
| $g(n - 2)$ | $4$ |
|
||||||
|
| ... | ... |
|
||||||
|
| $g(1)$ | $2^{n - 1}$ |
|
||||||
|
|
||||||
|
Based on this, the time complexity is:
|
||||||
|
|
||||||
|
$$
|
||||||
|
1 + 2 + 4 + ... + 2^{n - 1} = 2^n - 1 = O(2^n)
|
||||||
|
$$
|
||||||
|
|
||||||
|
#### Complexity Classes (from fastest to slowest)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here is a list of classes of functions that are commonly encountered when analyzing the running time of an algorithm.
|
||||||
|
|
||||||
|
- $O(1)$: **Constant** (does not depend on the input size). A typical constant-time algorithm is a direct formula that calculates the answer.
|
||||||
|
|
||||||
|
- $O(\log_2(n))$: **Logarithmic** algorithm often halves the input size at each step. $\log_2(n)$ equals the number of times $n$ must be divided by 2 to get 1.
|
||||||
|
|
||||||
|
- $O(\sqrt{n})$: **Square root** algorithm is slower than $O(\log_2(n))$ but faster than $O(n)$.
|
||||||
|
|
||||||
|
- $O(n)$: **Linear** algorithm goes through the input a constant number of times. This is often the best possible time complexity, because it is usually necessary to access each input element at least once before reporting the answer.
|
||||||
|
|
||||||
|
- $O(n \log_2(n))$: **Log linear** often indicates that the algorithm sorts the input, because the time complexity of efficient sorting algorithms is $O(n \log_2(n))$. Another possibility is that the algorithm uses a data structure where each operation takes $O(\log_2(n))$ time.
|
||||||
|
|
||||||
|
- $O(n^2)$: **Quadratic** algorithm often contains 2 nested loops. It is possible to go trough all pairs of the input elements in $O(n^2)$ time.
|
||||||
|
|
||||||
|
- $O(n^3)$: **Cubic** algorithm often contains 3 nested loops. It is possible to go trough all triplets of the input elements in $O(n^3)$ time.
|
||||||
|
|
||||||
|
- $O(2^n)$: **Exponential** often indicates that the algorithm iterates through all subsets of the input elements. For example, the subsets of $\{1, 2, 3\}$ are $S = \{\{\empty\}, \{1\}, \{2\}, \{3\}, \{1, 2\}, \{1, 3\}, \{2, 3\}, \{1, 2, 3\} \}$.
|
||||||
|
|
||||||
|
- $O(n!)$: **Factorial** often indicates that the algorithm iterates through all permutations of the input elements. For example, the permutations of $\{1, 2, 3\}$ are $P = \{\{1, 2, 3\}, \{1, 3, 2\}, \{2, 1, 3\}, \{2, 3, 1\}, \{3, 1, 2\}, \{3, 2, 1\} \}$.
|
||||||
|
|
||||||
|
### Estimating efficiency
|
||||||
|
|
||||||
|
By checking the time complexity of an algorithm, it is possible to check before implementing the algorithm,that it is efficient enough for the problem.
|
||||||
|
|
||||||
|
Example: assume that the time limit for a problem is 1 second and the input size is $n = 10^5$. If the time complexity is $O(n^2)$, the algorithm will perform about $(10^5)^2 = 10^{10}$ operations.
|
||||||
|
|
||||||
|
Given that a modern computer can perform some hundred of millions of operations per second. This should take at least 10 seconds, so the algorithm seems to be too slow for solving the problem.
|
||||||
|
|
||||||
|
## Practical problem: Maximum subarray sum
|
||||||
|
|
||||||
|
There are often several possible algorithms for solving a problem such that their time complexities are different. This section discusses a classic problem that can be solved using several different algorithmic techniques, including brute force, divide and conquer, dynamic programming, and reduction to shortest paths, each technique with different time complexity.
|
||||||
|
|
||||||
|
**Maximum subarray sum**: Given an array of $n$ integers, find the contiguous subarray with the largest sum.
|
||||||
|
|
||||||
|
Contiguous subarray is any sub series of elements in a given array that are contiguous ie their indices are continuous. The problem is interesting when there may be negative values in the array, because if the array only contains positive values, the maximum subarray sum is basically the sum of the array (the subarray being the complete array).
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
```txt
|
||||||
|
[1, 2, 3, 4, 5, 6]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
```txt
|
||||||
|
21
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:** The subarray with the largest sum is the array itself (as there is no negative values) `[1, 2, 3, 4, 5, 6]` which has a sum of `21`.
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
```txt
|
||||||
|
[-1, 2, 4, -3, 5, 2, -5, 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
```txt
|
||||||
|
10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:** The subarray with the largest sum is `[2, 4, -3, 5, 2]` which has a sum of `10`.
|
||||||
|
|
||||||
|
### Worst solution: Brute force
|
||||||
|
|
||||||
|
```python
|
||||||
|
def maximum_subarray_sum_cubic(array: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
Time complexity: O((array_length)^3)
|
||||||
|
|
||||||
|
We go through all possible subarrays, calculate the sum in each subarray and maintain the maximum sum.
|
||||||
|
"""
|
||||||
|
if len(array) == 0:
|
||||||
|
return 0
|
||||||
|
best_sum = array[0]
|
||||||
|
length = len(array)
|
||||||
|
for i in range(length):
|
||||||
|
for j in range(i, length):
|
||||||
|
sum = 0
|
||||||
|
for k in range(i, j + 1):
|
||||||
|
sum += array[k]
|
||||||
|
if sum > best_sum:
|
||||||
|
best_sum = sum
|
||||||
|
return best_sum
|
||||||
|
```
|
||||||
|
|
||||||
|
### Better solution: Linear time
|
||||||
|
|
||||||
|
```python
|
||||||
|
def maximum_subarray_sum_linear(array: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
Time complexity: O(array_length)
|
||||||
|
|
||||||
|
We loop through the array and for each array position, we calculate the maximum sum of a subarray that ends at that position. After this, the answer for the problem is the maximum of those sums.
|
||||||
|
"""
|
||||||
|
if len(array) == 0:
|
||||||
|
return 0
|
||||||
|
best_sum = array[0]
|
||||||
|
length = len(array)
|
||||||
|
sum = 0
|
||||||
|
for i in range(length):
|
||||||
|
sum = max(array[i], sum + array[i])
|
||||||
|
best_sum = max(best_sum, sum)
|
||||||
|
return best_sum
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Problems solving is a very complicated and large topic, and also a very important skill to have as a software developer.
|
||||||
|
|
||||||
|
To improve our problems solving skills, we can regularly practice with [programming challenges](https://github.com/Divlo/programming-challenges).
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [Wikipedia - Competitive programming](https://en.wikipedia.org/wiki/Competitive_programming)
|
||||||
|
- [Frontend Masters - The Last Algorithms Course You'll Need](https://frontendmasters.com/courses/algorithms/)
|
||||||
|
- [Big-O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||||
|
- [programming challenges](https://github.com/Divlo/programming-challenges)
|
@ -21,7 +21,7 @@ The source code is available on [GitHub](https://github.com/Thream).
|
|||||||
|
|
||||||
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
|
The 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://www.thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://www.thream.divlo.fr/).
|
[**Thream**](https://www.thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://www.thream.divlo.fr/).
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ The main goal is to put into **practice knowledge in web development** and compu
|
|||||||
|
|
||||||
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, with colors close to the image of Divlo.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
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,12 +53,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.
|
- 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.
|
||||||
|
|
||||||
<p className='flex flex-col items-center justify-center'>
|

|
||||||
<img
|
|
||||||
alt='HTTP Communication Schema'
|
|
||||||
src='/images/posts/thream-v1-0-0/http-communication.png'
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
public/images/skills/Laravel.png
Normal file
BIN
public/images/skills/Laravel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
public/images/skills/PHP.png
Normal file
BIN
public/images/skills/PHP.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 182 KiB |
118
resume.json
118
resume.json
@ -1,118 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
|
|
||||||
"meta": {
|
|
||||||
"theme": "custom"
|
|
||||||
},
|
|
||||||
"basics": {
|
|
||||||
"name": "Théo LUDWIG",
|
|
||||||
"label": "Développeur Full Stack Junior • Passionné de High-Tech",
|
|
||||||
"image": "https://divlo.fr/images/logo_orange.png",
|
|
||||||
"email": "contact@divlo.fr",
|
|
||||||
"location": {},
|
|
||||||
"url": "https://divlo.fr",
|
|
||||||
"summary": "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). <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets."
|
|
||||||
},
|
|
||||||
"education": [
|
|
||||||
{
|
|
||||||
"startDate": "2022",
|
|
||||||
"studyType": "Diplôme du Bachelor Universitaire de Technologie (BUT) Informatique",
|
|
||||||
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
|
||||||
"score": "En cours"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startDate": "2019",
|
|
||||||
"endDate": "2021",
|
|
||||||
"studyType": "Diplôme du Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
|
|
||||||
"institution": "Lycée Heinrich Nessel à Haguenau",
|
|
||||||
"score": "Mention Assez Bien"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startDate": "2014",
|
|
||||||
"endDate": "2018",
|
|
||||||
"studyType": "Diplôme national du brevet",
|
|
||||||
"institution": "Collège Gustave Doré à Hochfelden",
|
|
||||||
"score": "Mention Bien"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"work": [
|
|
||||||
{
|
|
||||||
"summary": "Développement site web en React.js et Strapi afin de répondre <a href=\"https://www.nuitdelinfo.com/nuitinfo/_media/infos:la_nuit_de_l_info_2021_-_sujet.pdf\">au sujet de la Nuit de l'Info 2021</a>.<br /> TOP 1 France: Défi de l'entreprise <a href=\"https://www.nuitdelinfo.com/inscription/defis/300\">ToolPad</a>.",
|
|
||||||
"website": "https://www.nuitdelinfo.com/",
|
|
||||||
"name": "La Nuit de l'info 2021",
|
|
||||||
"position": "Participation avec l'équipe <a href=\"https://www.nuitdelinfo.com/inscription/equipes/46\">Who are We</a>",
|
|
||||||
"startDate": "2021-12-02",
|
|
||||||
"endDate": "2021-12-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"summary": "Agent administratif en vue de faire face au sucroît temporaire d'activités liés à la numérisation des plans des postes sources <br /> actuellement sous format papier calque suite à la libération des locaux des archives.",
|
|
||||||
"website": "https://www.es.fr/",
|
|
||||||
"name": "ÉS (Électricité de Strasbourg)",
|
|
||||||
"location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
|
|
||||||
"position": "Emploi d'été en qualité d'agent administratif",
|
|
||||||
"startDate": "2021-07-07",
|
|
||||||
"endDate": "2021-07-30"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"summary": "Hackathon développement d'une landing page et web scraping.",
|
|
||||||
"website": "https://www.wildcodeschool.fr/",
|
|
||||||
"name": "Wild Code School",
|
|
||||||
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
|
||||||
"position": "Stage initiation métier développeur web",
|
|
||||||
"startDate": "2019-06-24",
|
|
||||||
"endDate": "2019-06-28"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
|
|
||||||
"website": "https://www.itpartners.fr/",
|
|
||||||
"name": "Tribe | IT Partners",
|
|
||||||
"location": "16 Rue du Parc, 67205 Oberhausbergen",
|
|
||||||
"position": "Stage initiation métier développeur web",
|
|
||||||
"startDate": "2019-06-17",
|
|
||||||
"endDate": "2019-06-21"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
|
|
||||||
"website": "https://www.es.fr/",
|
|
||||||
"name": "ÉS (Électricité de Strasbourg)",
|
|
||||||
"location": "26 Bd du Président-Wilson, 67000 Strasbourg",
|
|
||||||
"position": "Stage de découverte (3ème)",
|
|
||||||
"startDate": "2018-02-19",
|
|
||||||
"endDate": "2018-02-23"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"interests": [
|
|
||||||
{
|
|
||||||
"name": "Développeur Full Stack Junior"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Passionné de High-Tech"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Enthousiaste de l'Open-Source"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"skills": [
|
|
||||||
{
|
|
||||||
"keywords": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
|
||||||
"name": "Langages de programmation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
|
||||||
"name": "Front-end"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"keywords": ["Node.js", "Fastify", "PostgreSQL", "MySQL"],
|
|
||||||
"name": "Back-end"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"keywords": [
|
|
||||||
"GNU/Linux",
|
|
||||||
"Ubuntu",
|
|
||||||
"Visual Studio Code",
|
|
||||||
"git",
|
|
||||||
"Docker"
|
|
||||||
],
|
|
||||||
"name": "Logiciels et outils"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
160
resume.jsonc
Normal file
160
resume.jsonc
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
|
||||||
|
"meta": {
|
||||||
|
"theme": "custom"
|
||||||
|
},
|
||||||
|
"basics": {
|
||||||
|
"name": "Théo LUDWIG",
|
||||||
|
"label": "Développeur Full Stack • Étudiant",
|
||||||
|
"image": "https://divlo.fr/images/logo_orange.png",
|
||||||
|
"email": "contact@divlo.fr",
|
||||||
|
"age": "31/03/2003",
|
||||||
|
"location": {
|
||||||
|
"address": "Alsace, France"
|
||||||
|
},
|
||||||
|
"url": "https://divlo.fr",
|
||||||
|
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://divlo.fr\">divlo.fr</a>)."
|
||||||
|
},
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"startDate": "2022",
|
||||||
|
"endDate": "2023",
|
||||||
|
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
|
||||||
|
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
||||||
|
"score": "2ème année",
|
||||||
|
"courses": [
|
||||||
|
"Développement Web avec le framework Laravel en PHP",
|
||||||
|
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
|
||||||
|
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
|
||||||
|
"Sécurisation des accès à la base de données et PL/SQL",
|
||||||
|
"Projet développement d'une application web en React.js en équipe de 3 personnes pendant 3 mois"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"startDate": "2021",
|
||||||
|
"endDate": "2022",
|
||||||
|
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
|
||||||
|
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
||||||
|
"score": "1ère année",
|
||||||
|
"courses": [
|
||||||
|
"Développement Orientée Objet en Java",
|
||||||
|
"Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)",
|
||||||
|
"Développement d'application Windows Forms (.NET Framework) en C#",
|
||||||
|
"Base de données relationnelles et langage SQL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"startDate": "2019",
|
||||||
|
"endDate": "2021",
|
||||||
|
"studyType": "Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
|
||||||
|
"institution": "Lycée Heinrich Nessel à Haguenau",
|
||||||
|
"score": "Mention Assez Bien"
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// "startDate": "2014",
|
||||||
|
// "endDate": "2018",
|
||||||
|
// "studyType": "Diplôme national du brevet",
|
||||||
|
// "institution": "Collège Gustave Doré à Hochfelden",
|
||||||
|
// "score": "Mention Bien"
|
||||||
|
// }
|
||||||
|
],
|
||||||
|
"work": [
|
||||||
|
{
|
||||||
|
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
|
||||||
|
"website": "https://numerize.com/",
|
||||||
|
"name": "Numerize",
|
||||||
|
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
|
||||||
|
"position": "Stagiaire Développeur Web",
|
||||||
|
"startDate": "2023-04-11",
|
||||||
|
"endDate": "2023-06-23",
|
||||||
|
"duration": "3 mois"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
|
||||||
|
"website": "https://www.es.fr/",
|
||||||
|
"name": "ÉS (Électricité de Strasbourg)",
|
||||||
|
"location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
|
||||||
|
"position": "Emploi d'été en qualité d'agent administratif",
|
||||||
|
"startDate": "2021-07-07",
|
||||||
|
"endDate": "2021-07-30",
|
||||||
|
"duration": "1 mois"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
|
||||||
|
"website": "https://www.itpartners.fr/",
|
||||||
|
"name": "Tribe | IT Partners",
|
||||||
|
"location": "16 Rue du Parc, 67205 Oberhausbergen",
|
||||||
|
"position": "Stage initiation métier développeur web",
|
||||||
|
"startDate": "2019-06-17",
|
||||||
|
"endDate": "2019-06-21",
|
||||||
|
"duration": "1 semaine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "interests",
|
||||||
|
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://www.toolpad.fr/\">ToolPad</a>.",
|
||||||
|
"website": "https://www.nuitdelinfo.com/",
|
||||||
|
"name": "La Nuit de l'info 2021",
|
||||||
|
"position": "Participation en équipe de 5 personnes",
|
||||||
|
"startDate": "2021-12-02",
|
||||||
|
"endDate": "2021-12-03",
|
||||||
|
"duration": "1 semaine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "interests",
|
||||||
|
"summary": "Hackathon développement d'une landing page et web scraping.",
|
||||||
|
"website": "https://www.wildcodeschool.fr/",
|
||||||
|
"name": "Wild Code School",
|
||||||
|
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
||||||
|
"position": "Initiation métier développeur web",
|
||||||
|
"startDate": "2019-06-24",
|
||||||
|
"endDate": "2019-06-28",
|
||||||
|
"duration": "1 semaine"
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
|
||||||
|
// "website": "https://www.es.fr/",
|
||||||
|
// "name": "ÉS (Électricité de Strasbourg)",
|
||||||
|
// "location": "26 Bd du Président-Wilson, 67000 Strasbourg",
|
||||||
|
// "position": "Stage de découverte (3ème)",
|
||||||
|
// "startDate": "2018-02-19",
|
||||||
|
// "endDate": "2018-02-23",
|
||||||
|
// "duration": "1 semaine"
|
||||||
|
// }
|
||||||
|
],
|
||||||
|
"interests": [
|
||||||
|
{
|
||||||
|
"name": "Enthousiaste de l'Open-Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Passionné de High-Tech"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"keywords": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||||
|
"name": "Langages de programmation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||||
|
"name": "Front-end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keywords": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||||
|
"name": "Back-end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keywords": [
|
||||||
|
"GNU/Linux",
|
||||||
|
"Ubuntu",
|
||||||
|
"Visual Studio Code",
|
||||||
|
"Git",
|
||||||
|
"Docker"
|
||||||
|
],
|
||||||
|
"name": "Logiciels et outils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keywords": ["Permis B", "Anglais"],
|
||||||
|
"name": "Autres"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -2,8 +2,13 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.break-wrap-words {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.prose {
|
.prose {
|
||||||
@apply !max-w-4xl text-gray dark:text-gray-300;
|
@apply !max-w-5xl text-gray dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a,
|
.prose a,
|
||||||
@ -28,8 +33,6 @@
|
|||||||
}
|
}
|
||||||
.shiki {
|
.shiki {
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
word-break: break-word !important;
|
|
||||||
word-wrap: normal;
|
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
counter-reset: step;
|
counter-reset: step;
|
||||||
@ -43,4 +46,12 @@ code .line::before {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: rgba(133, 133, 133, 0.8);
|
color: rgba(133, 133, 133, 0.8);
|
||||||
|
word-wrap: normal;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex .base {
|
||||||
|
display: inline !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const tailwindConfig = {
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
'./components/**/*.{js,ts,jsx,tsx}'
|
'./components/**/*.{js,ts,jsx,tsx}'
|
||||||
@ -47,3 +48,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/typography')]
|
plugins: [require('@tailwindcss/typography')]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = tailwindConfig
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"lib": ["dom", "dom.iterable", "ESNext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"sourceMap": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"strict": true,
|
|
||||||
"types": ["cypress"],
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"esModuleInterop": true,
|
"paths": {
|
||||||
"forceConsistentCasingInFileNames": true,
|
"@/*": ["./*"]
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
},
|
||||||
"skipLibCheck": true,
|
"types": ["cypress"],
|
||||||
|
"noEmit": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"exclude": ["dist", ".next", "out", "next.config.js"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import rehypeRaw from 'rehype-raw'
|
|||||||
import { serialize } from 'next-mdx-remote/serialize'
|
import { serialize } from 'next-mdx-remote/serialize'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeSlug from 'rehype-slug'
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
import rehypeKatex from 'rehype-katex'
|
||||||
import matter from 'gray-matter'
|
import matter from 'gray-matter'
|
||||||
import { getHighlighter } from 'shiki'
|
import { getHighlighter } from 'shiki'
|
||||||
|
|
||||||
@ -37,12 +39,18 @@ export const getPosts = async (): Promise<PostMetadata[]> => {
|
|||||||
const posts = await fs.promises.readdir(POSTS_PATH)
|
const posts = await fs.promises.readdir(POSTS_PATH)
|
||||||
const postsWithTime = await Promise.all(
|
const postsWithTime = await Promise.all(
|
||||||
posts.map(async (postFilename) => {
|
posts.map(async (postFilename) => {
|
||||||
const [slug] = postFilename.split('.')
|
const [slug, extension] = postFilename.split('.')
|
||||||
const blogPostPath = path.join(POSTS_PATH, `${slug}.mdx`)
|
if (slug == null || extension == null) {
|
||||||
|
throw new Error('Invalid postFilename.')
|
||||||
|
}
|
||||||
|
const blogPostPath = path.join(POSTS_PATH, `${slug}.${extension}`)
|
||||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
const { data, content } = matter(blogPostContent) as any
|
const { data, content } = matter(blogPostContent) as unknown as {
|
||||||
|
data: FrontMatter
|
||||||
|
content: string
|
||||||
|
}
|
||||||
const date = new Date(data.publishedOn)
|
const date = new Date(data.publishedOn)
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
@ -53,8 +61,12 @@ export const getPosts = async (): Promise<PostMetadata[]> => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
const postsWithTimeSorted = postsWithTime
|
const postsWithTimeSorted = postsWithTime
|
||||||
.filter((post) => post.frontmatter.isPublished)
|
.filter((post) => {
|
||||||
.sort((a, b) => b.time - a.time)
|
return post.frontmatter.isPublished
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.time - a.time
|
||||||
|
})
|
||||||
return postsWithTimeSorted
|
return postsWithTimeSorted
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +74,9 @@ export const getPostBySlug = async (
|
|||||||
slug?: string | string[]
|
slug?: string | string[]
|
||||||
): Promise<Post | undefined> => {
|
): Promise<Post | undefined> => {
|
||||||
const posts = await getPosts()
|
const posts = await getPosts()
|
||||||
const post = posts.find((post) => post.slug === slug)
|
const post = posts.find((post) => {
|
||||||
|
return post.slug === slug
|
||||||
|
})
|
||||||
if (post == null) {
|
if (post == null) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -72,12 +86,14 @@ export const getPostBySlug = async (
|
|||||||
const source = await serialize(post.content, {
|
const source = await serialize(post.content, {
|
||||||
mdxOptions: {
|
mdxOptions: {
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
remarkGfm as any,
|
remarkGfm,
|
||||||
[remarkSyntaxHighlightingPlugin, { highlighter }]
|
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
||||||
|
remarkMath
|
||||||
],
|
],
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
rehypeSlug as any,
|
rehypeSlug,
|
||||||
[rehypeRaw, { passThrough: nodeTypes }]
|
[rehypeRaw, { passThrough: nodeTypes }],
|
||||||
|
rehypeKatex
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Plugin, Transformer } from 'unified'
|
import type { Plugin, Transformer } from 'unified'
|
||||||
import { Literal } from 'unist'
|
import type { Literal } from 'unist'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
import { Highlighter } from 'shiki'
|
import type { Highlighter } from 'shiki'
|
||||||
|
|
||||||
export interface RemarkSyntaxHighlightingPluginOptions {
|
export interface RemarkSyntaxHighlightingPluginOptions {
|
||||||
highlighter: Highlighter
|
highlighter: Highlighter
|
||||||
|
Reference in New Issue
Block a user