mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
243 Commits
Author | SHA1 | Date | |
---|---|---|---|
6abc881e94 | |||
a67d6665ea | |||
1152039663 | |||
919ebd5f3e | |||
94212f9b5c | |||
bf9347f685 | |||
896b6051e8 | |||
b5f3552c07 | |||
5fbae8601f | |||
48d35776a9 | |||
8b9e58c47c | |||
33078ece66 | |||
a2da9618af | |||
a467ea7aff | |||
0e0036b737 | |||
729e540d04 | |||
e5f4615f7f | |||
0bf89f4df5 | |||
bcb184e49c | |||
1505b81233 | |||
a30355582e | |||
a4effb52f9 | |||
52bba0ef9c | |||
8ecfeca50d | |||
fd0740d12a | |||
bd2dc9c9af | |||
a53888ab42 | |||
624e79eecd | |||
049ec367fc | |||
56f22d0c9b | |||
9adb67662e | |||
010087088f | |||
35d4396e80 | |||
934118737a | |||
b692dac926 | |||
dd582652ab | |||
337352de0c | |||
c513268cbb | |||
4fdcb2b667 | |||
377b8e91a6 | |||
fce29c9d4a | |||
c198f47aa9 | |||
8e051332cd | |||
9f3436e1df | |||
2f2373e62f | |||
c6b455dd10 | |||
4e089b41f2 | |||
6c102b1b35 | |||
52b10944b7 | |||
db36eb3e7a | |||
c739ad951d | |||
2802ff029f | |||
1a7457b44b | |||
ff210f879d | |||
607454b360 | |||
d1522fbf44 | |||
b82eae7499 | |||
73527ce8fe | |||
0cd885ee70 | |||
2cb2df975f | |||
37f5843adb | |||
d794d38f14 | |||
fc5ba28b8a | |||
b5945150b8 | |||
aa12d626d2 | |||
6ac4782b7d | |||
0aa998d593 | |||
56f975e53c | |||
5a16d24ea1 | |||
52267005ec | |||
99b9b12ac9 | |||
2cae77481f | |||
e98b47a459 | |||
4cc87758c1 | |||
1bb0f31223 | |||
af2dd0bd60 | |||
63d7485c8d | |||
74fde0ea40 | |||
0d2b318818 | |||
266b3f8589 | |||
f7d304ca80 | |||
63017953d7 | |||
20600eb976 | |||
7f920b77aa | |||
4f5dfc63ea | |||
712805df93 | |||
cd68f597c9 | |||
7ec3fe8ced | |||
90d22b2c7f | |||
4b06fd0522 | |||
b4427f36c2 | |||
b758c64e02 | |||
04469b83ea | |||
36d54666a0 | |||
a34cefec6e | |||
5c343395df | |||
028815a7b6 | |||
a2ad591d6d | |||
7087911756 | |||
35b1c4169f | |||
4c351b8179 | |||
701dccc018 | |||
5133765f94 | |||
3b208c6614 | |||
52870fd6a4 | |||
3a278fec10 | |||
669f592a9f | |||
9c0a3ea1af | |||
fa8d70bf82 | |||
3293fd488e | |||
426bee09da | |||
dbc6c84895 | |||
fab539c9d7 | |||
176ab64a37 | |||
1b56bbc694 | |||
0f9a968081 | |||
6b9ff4100d | |||
870bc3d26b | |||
41e4b93427 | |||
72ae4ef01d | |||
748259b57c | |||
fafd606c18 | |||
b8c3022532 | |||
46adaee53f | |||
508114152c | |||
b2852d172c | |||
16e3b1e465 | |||
ae610ff816 | |||
7c001f3c30 | |||
7eada755e1 | |||
6909304f15 | |||
25b2f05170 | |||
0cc83a811c | |||
78b14c2620 | |||
eebdf0edd2 | |||
62e8005081 | |||
6473e9da7d | |||
1805997f59 | |||
fb25c12883 | |||
849b758fab | |||
ccf5d42c19 | |||
2d68ce59ca | |||
4e6531e341 | |||
8f2d0817ce | |||
7674401e7c | |||
61983dfc4a | |||
ed47407b7d | |||
0a79754978 | |||
725afecbf3 | |||
1bf79e55e1 | |||
3a369c49fa | |||
e78ccf3db4 | |||
acafe71f31 | |||
3ef876b737 | |||
b30bbc99e9 | |||
235c072c21 | |||
f5bdd85b73 | |||
b81ae5a9a6 | |||
1ea5e3f323 | |||
f6eaef54b9 | |||
5b14361d74 | |||
d1f9c0eb2f | |||
95b27abec1 | |||
228e987d8b | |||
7c44102afd | |||
b8410e5628 | |||
d6f0b12b17 | |||
b02e31c373 | |||
e012d41929 | |||
4bd77b45e4 | |||
e43f572588 | |||
9aecb3cab9 | |||
f1256ab23f | |||
892bf0e87a | |||
61ef6c5525 | |||
38405d658e | |||
f3b7c315f0 | |||
6950286eec | |||
60f966c493 | |||
7af4d3c512 | |||
d9b53480be | |||
a574a8ffd1 | |||
b0a34c6162 | |||
ea04f0f189 | |||
1403cdf80c | |||
40e676cfc7 | |||
5f654020d5 | |||
a3ec87bf52 | |||
88588355fd | |||
c329c56094 | |||
08a5454cf4 | |||
8faf47c06e | |||
d7f778de28 | |||
cd3cc50e00 | |||
755f2da03a | |||
7ef9f79b97 | |||
2c53a1409c | |||
1e2d5c0f3e | |||
6db7ed2f5e | |||
9b8102cbdc | |||
e0bc1fed49 | |||
c230f5bb51 | |||
6f4819b689 | |||
fd67737754 | |||
6f94865917 | |||
1e4167e209 | |||
655ed6f6f6 | |||
8fe73be90b | |||
e925b73606 | |||
b902b9a122 | |||
1044302118 | |||
df15232312 | |||
f5d273688d | |||
993dd1e30e | |||
83f90e24c7 | |||
c3fd177ff5 | |||
c5f8b4fb13 | |||
a4e48de57e | |||
e3aa2a4d50 | |||
88c44ed31f | |||
d3d1ca7beb | |||
8d758bc1d7 | |||
34b5f123b4 | |||
809f4612b5 | |||
1f48f7a296 | |||
56258dc06b | |||
3fea7d48f6 | |||
f49ca1f4f2 | |||
e9d9139263 | |||
98e7987b04 | |||
4dc145fe75 | |||
c8b12cd618 | |||
97cf63f643 | |||
cd20e25082 | |||
c62e66a86a | |||
26f24329c7 | |||
bb86fb500a | |||
b78e8b1e02 | |||
2e87d6b51f | |||
4439e73986 | |||
af065afe67 | |||
088510a62b | |||
e0150361b1 |
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"next/babel",
|
|
||||||
{
|
|
||||||
"styled-jsx": {
|
|
||||||
"plugins": ["@styled-jsx/plugin-sass"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
2
.devcontainer/Dockerfile
Normal file
2
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ARG VARIANT="16"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "divlo",
|
||||||
|
"dockerComposeFile": "./docker-compose.yml",
|
||||||
|
"service": "workspace",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"settings": {
|
||||||
|
"remote.autoForwardPorts": false
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"divlo.vscode-styled-jsx-syntax",
|
||||||
|
"divlo.vscode-styled-jsx-languageserver",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
],
|
||||||
|
"forwardPorts": [3000],
|
||||||
|
"postAttachCommand": ["npm", "install"],
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
10
.devcontainer/docker-compose.yml
Normal file
10
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: '3.0'
|
||||||
|
|
||||||
|
services:
|
||||||
|
workspace:
|
||||||
|
build:
|
||||||
|
context: './'
|
||||||
|
dockerfile: './Dockerfile'
|
||||||
|
volumes:
|
||||||
|
- '..:/workspace:cached'
|
||||||
|
command: 'sleep infinity'
|
@ -1,11 +1,12 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.git
|
.git
|
||||||
.next
|
.env
|
||||||
build
|
build
|
||||||
|
.next
|
||||||
coverage
|
coverage
|
||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
out
|
tmp
|
||||||
**/workbox-*.js
|
temp
|
||||||
**/sw.js
|
.DS_Store
|
||||||
**/__test__/**
|
.lighthouseci
|
||||||
|
.vercel
|
||||||
|
@ -1,6 +1,2 @@
|
|||||||
COMPOSE_PROJECT_NAME=divlo.fr-website
|
COMPOSE_PROJECT_NAME=divlo.fr
|
||||||
PORT=3000
|
PORT=3000
|
||||||
EMAIL_HOST=divlo.fr-maildev
|
|
||||||
EMAIL_USER=reply@divlo-website.fr
|
|
||||||
EMAIL_PASSWORD=password
|
|
||||||
EMAIL_PORT=25
|
|
||||||
|
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.next
|
||||||
|
.lighthouseci
|
||||||
|
storybook-static
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
next-env.d.ts
|
||||||
|
**/workbox-*.js
|
||||||
|
**/sw.js
|
17
.eslintrc.json
Normal file
17
.eslintrc.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||||
|
"plugins": ["prettier", "unicorn"],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"unicorn/prefer-node-protocol": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off"
|
||||||
|
}
|
||||||
|
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,11 +1,4 @@
|
|||||||
<!--
|
<!-- 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.
|
|
||||||
|
|
||||||
Before submitting your contribution, please take a moment to review this document:
|
|
||||||
https://github.com/Divlo/Divlo/blob/master/.github/CONTRIBUTING.md
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
## What changes this PR introduce?
|
## What changes this PR introduce?
|
||||||
|
|
||||||
|
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@ -1,16 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: 'github-actions'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'daily'
|
|
||||||
|
|
||||||
- package-ecosystem: 'docker'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'daily'
|
|
||||||
|
|
||||||
- package-ecosystem: 'npm'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'daily'
|
|
38
.github/workflows/Divlo.yml
vendored
38
.github/workflows/Divlo.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
name: 'Divlo'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
runs-on: 'ubuntu-latest'
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
|
||||||
- uses: 'actions/checkout@v2'
|
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
|
|
||||||
- name: 'Cache dependencies'
|
|
||||||
uses: 'actions/cache@v2.1.5'
|
|
||||||
with:
|
|
||||||
path: '.npm'
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
|
|
||||||
- run: 'npm install --global npm@7'
|
|
||||||
- run: 'npm ci --cache .npm --prefer-offline'
|
|
||||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
|
||||||
- run: 'npm run lint:docker'
|
|
||||||
- run: 'npm run lint:editorconfig'
|
|
||||||
- run: 'npm run lint:markdown'
|
|
||||||
- run: 'npm run lint:typescript'
|
|
||||||
- run: 'npm run build'
|
|
||||||
- run: 'npm run lighthouse'
|
|
||||||
- run: 'npm run test'
|
|
@ -1,14 +1,13 @@
|
|||||||
name: 'CodeQL'
|
name: 'Analyze'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master, develop]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: 'Analyze'
|
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@ -17,7 +16,7 @@ jobs:
|
|||||||
language: ['javascript']
|
language: ['javascript']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v2'
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
- name: 'Initialize CodeQL'
|
- name: 'Initialize CodeQL'
|
||||||
uses: 'github/codeql-action/init@v1'
|
uses: 'github/codeql-action/init@v1'
|
25
.github/workflows/build.yml
vendored
Normal file
25
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: 'Build'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@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'
|
47
.github/workflows/lint.yml
vendored
Normal file
47
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: 'Lint'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
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: 'lint:commit'
|
||||||
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
|
|
||||||
|
- name: 'lint:editorconfig'
|
||||||
|
run: 'npm run lint:editorconfig'
|
||||||
|
|
||||||
|
- name: 'lint:markdown'
|
||||||
|
run: 'npm run lint:markdown'
|
||||||
|
|
||||||
|
- name: 'lint:typescript'
|
||||||
|
run: 'npm run lint:typescript'
|
||||||
|
|
||||||
|
- name: 'lint:prettier'
|
||||||
|
run: 'npm run lint:prettier'
|
||||||
|
|
||||||
|
- name: 'lint:dotenv'
|
||||||
|
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.github_token }}
|
||||||
|
|
||||||
|
- name: 'lint:docker'
|
||||||
|
uses: 'hadolint/hadolint-action@v1.6.0'
|
||||||
|
with:
|
||||||
|
dockerfile: './Dockerfile'
|
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@ -1,34 +1,44 @@
|
|||||||
name: 'Release'
|
name: 'Release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
push:
|
||||||
workflows: [Divlo]
|
|
||||||
branches: [master]
|
branches: [master]
|
||||||
types:
|
|
||||||
- 'completed'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v2'
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 'Cache dependencies'
|
- name: 'Import GPG key'
|
||||||
uses: 'actions/cache@v2.1.5'
|
uses: 'crazy-max/ghaction-import-gpg@v4'
|
||||||
with:
|
with:
|
||||||
path: '.npm'
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
git_user_signingkey: true
|
||||||
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- run: 'npm install --global npm@7'
|
- name: 'Use Node.js'
|
||||||
- run: 'npm ci --cache .npm --prefer-offline'
|
uses: 'actions/setup-node@v3.0.0'
|
||||||
- run: 'npm run release'
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Release'
|
||||||
|
run: 'npm run release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||||
|
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
|
||||||
|
|
||||||
|
- name: 'Deploy to Vercel'
|
||||||
|
run: 'npm run deploy -- --token="${VERCEL_TOKEN}" --prod'
|
||||||
|
env:
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
|
70
.github/workflows/test.yml
vendored
Normal file
70
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
name: 'Test'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-unit:
|
||||||
|
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: 'Unit Test'
|
||||||
|
run: 'npm run test:unit'
|
||||||
|
|
||||||
|
test-lighthouse:
|
||||||
|
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: 'html-w3c-validator'
|
||||||
|
run: 'npm run test:html-w3c-validator'
|
||||||
|
|
||||||
|
- name: 'Lighthouse'
|
||||||
|
run: 'npm run test:lighthouse'
|
||||||
|
env:
|
||||||
|
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v3.0.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Build'
|
||||||
|
run: 'npm run build'
|
||||||
|
|
||||||
|
- name: 'End To End (e2e) Test'
|
||||||
|
run: 'npm run test:e2e'
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -11,13 +11,16 @@ out
|
|||||||
# production
|
# production
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
public/*.html
|
||||||
|
# PWA
|
||||||
|
public/workbox-*.js
|
||||||
|
public/sw.js
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage
|
coverage
|
||||||
|
cypress/screenshots
|
||||||
# PWA
|
cypress/videos
|
||||||
**/workbox-*.js
|
cypress/downloads
|
||||||
**/sw.js
|
|
||||||
|
|
||||||
# envs
|
# envs
|
||||||
.env
|
.env
|
||||||
@ -45,3 +48,4 @@ npm-debug.log*
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.lighthouseci
|
.lighthouseci
|
||||||
|
.vercel
|
||||||
|
13
.gitpod.yml
13
.gitpod.yml
@ -1,21 +1,14 @@
|
|||||||
image: 'gitpod/workspace-full'
|
image: 'gitpod/workspace-full'
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: 'docker-daemon'
|
- before: 'cp .env.example .env'
|
||||||
init: 'cp .env.example .env && npm install --global npm@7 && npm ci'
|
init: 'npm install'
|
||||||
command: 'sudo docker-up'
|
command: 'npm run dev'
|
||||||
- name: 'docker-container'
|
|
||||||
init: 'echo "Waiting for docker daemon to start" &&
|
|
||||||
until docker info &> /dev/null; do sleep 1; done;'
|
|
||||||
command: 'docker-compose up'
|
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- port: 3000
|
- port: 3000
|
||||||
onOpen: 'open-preview'
|
onOpen: 'open-preview'
|
||||||
|
|
||||||
- port: 1080
|
|
||||||
onOpen: 'notify'
|
|
||||||
|
|
||||||
github:
|
github:
|
||||||
prebuilds:
|
prebuilds:
|
||||||
master: true
|
master: true
|
||||||
|
8
.html-w3c-validatorrc.json
Normal file
8
.html-w3c-validatorrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
"http://localhost:3000/",
|
||||||
|
"http://localhost:3000/blog",
|
||||||
|
"http://localhost:3000/blog/hello-world"
|
||||||
|
],
|
||||||
|
"files": ["./public/curriculum-vitae.html"]
|
||||||
|
}
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
_
|
|
@ -1,7 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
npm run lint:docker
|
npm run lint:staged
|
||||||
npm run lint:editorconfig
|
|
||||||
npm run lint:markdown
|
|
||||||
npm run lint:typescript
|
|
||||||
|
@ -4,21 +4,22 @@
|
|||||||
"startServerCommand": "npm run start",
|
"startServerCommand": "npm run start",
|
||||||
"startServerReadyPattern": "ready on",
|
"startServerReadyPattern": "ready on",
|
||||||
"startServerReadyTimeout": 20000,
|
"startServerReadyTimeout": 20000,
|
||||||
"url": ["http://localhost:3000/"],
|
"url": [
|
||||||
"numberOfRuns": 3
|
"http://localhost:3000/",
|
||||||
|
"http://localhost:3000/blog",
|
||||||
|
"http://localhost:3000/blog/hello-world"
|
||||||
|
],
|
||||||
|
"numberOfRuns": 1
|
||||||
},
|
},
|
||||||
"assert": {
|
"assert": {
|
||||||
"preset": "lighthouse:recommended",
|
"preset": "lighthouse:recommended",
|
||||||
"assertions": {
|
"assertions": {
|
||||||
"legacy-javascript": "off",
|
"csp-xss": "warning",
|
||||||
"unused-javascript": "off",
|
"non-composited-animations": "warning",
|
||||||
"uses-rel-preload": "off",
|
"unused-javascript": "warning",
|
||||||
"canonical": "off",
|
"image-size-responsive": "warning",
|
||||||
"unsized-images": "off",
|
"unsized-images": "warning",
|
||||||
"uses-responsive-images": "off",
|
"color-contrast": "warning"
|
||||||
"bypass": "warning",
|
|
||||||
"color-contrast": "warning",
|
|
||||||
"preload-lcp-image": "warning"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
|
11
.lintstagedrc.json
Normal file
11
.lintstagedrc.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"*": ["editorconfig-checker"],
|
||||||
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"jest --findRelatedTests"
|
||||||
|
],
|
||||||
|
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||||
|
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
||||||
|
"resume.json": ["resume validate"]
|
||||||
|
}
|
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.next
|
||||||
|
.lighthouseci
|
||||||
|
storybook-static
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
next-env.d.ts
|
||||||
|
**/workbox-*.js
|
||||||
|
**/sw.js
|
||||||
|
*.hbs
|
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"branches": ["master"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
[
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
@ -6,7 +7,32 @@
|
|||||||
"preset": "conventionalcommits"
|
"preset": "conventionalcommits"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/github"
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/npm",
|
||||||
|
{
|
||||||
|
"npmPublish": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": ["package.json", "package-lock.json"],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github",
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"branches": [{ "from": "master", "to": "develop" }],
|
||||||
|
"backmergeStrategy": "merge"
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
9
.vscode/extensions.json
vendored
9
.vscode/extensions.json
vendored
@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
"divlo.vscode-styled-jsx-syntax",
|
"divlo.vscode-styled-jsx-syntax",
|
||||||
"divlo.vscode-styled-jsx-languageserver",
|
"divlo.vscode-styled-jsx-languageserver",
|
||||||
"standard.vscode-standard",
|
"bradlc.vscode-tailwindcss",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"editorconfig.editorconfig",
|
|
||||||
"coenraads.bracket-pair-colorizer",
|
|
||||||
"davidanson.vscode-markdownlint",
|
"davidanson.vscode-markdownlint",
|
||||||
"syler.sass-indented"
|
"ms-azuretools.vscode-docker"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"standard.enable": true,
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"standard.engine": "ts-standard",
|
"editor.bracketPairColorization.enabled": true,
|
||||||
"standard.treatErrorsAsWarnings": true,
|
"prettier.configPath": ".prettierrc.json",
|
||||||
"standard.usePackageJson": true,
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"standard.autoFixOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
|||||||
|
|
||||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
||||||
|
|
||||||
- Ensure your code respect [Typescript Standard Style](https://www.npmjs.com/package/ts-standard).
|
- Ensure your code respect linting.
|
||||||
|
|
||||||
- Make sure your **code passes the tests**.
|
- Make sure your **code passes the tests**.
|
||||||
|
|
||||||
@ -49,6 +49,11 @@ Scopes define what part of the code changed.
|
|||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||||
|
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -60,9 +65,19 @@ cd Divlo
|
|||||||
|
|
||||||
# Configure environment variables
|
# Configure environment variables
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Install
|
||||||
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development environment with [Docker](https://www.docker.com/)
|
### Local Development environment
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run website
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production environment with [Docker](https://www.docker.com/)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Setup and run all the services for you
|
# Setup and run all the services for you
|
||||||
@ -72,4 +87,3 @@ docker-compose up --build
|
|||||||
### Services started
|
### Services started
|
||||||
|
|
||||||
- website : `http://localhost:3000`
|
- website : `http://localhost:3000`
|
||||||
- [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`
|
|
||||||
|
27
Dockerfile
27
Dockerfile
@ -1,10 +1,23 @@
|
|||||||
FROM node:14.16.1
|
FROM node:16.14.0 AS dependencies
|
||||||
RUN npm install --global npm@7
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY ./ ./
|
|
||||||
|
|
||||||
CMD ["npm", "run", "dev", "--", "--port", "${PORT}"]
|
FROM node:16.14.0 AS builder
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY ./ ./
|
||||||
|
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:16.14.0 AS runner
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||||
|
COPY --from=builder /usr/src/app/public ./public
|
||||||
|
COPY --from=builder /usr/src/app/.next ./.next
|
||||||
|
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
|
||||||
|
COPY --from=builder /usr/src/app/locales ./locales
|
||||||
|
COPY --from=builder /usr/src/app/pages ./pages
|
||||||
|
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||||
|
RUN npx next telemetry disable
|
||||||
|
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
|
||||||
|
36
README.md
36
README.md
@ -5,7 +5,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Divlo/Divlo/actions?query=workflow%3A%22Divlo%22"><img src="https://github.com/Divlo/Divlo/actions/workflows/Divlo.yml/badge.svg?branch=master" alt="Divlo's CI" /></a>
|
|
||||||
<a href="https://github.com/Divlo"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
|
<a href="https://github.com/Divlo"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
|
||||||
<a href="https://gitlab.com/Divlo"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
|
<a href="https://gitlab.com/Divlo"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
|
||||||
<a href="https://www.npmjs.com/~divlo"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
|
<a href="https://www.npmjs.com/~divlo"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
|
||||||
@ -20,30 +19,31 @@
|
|||||||
|
|
||||||
## 📜 About
|
## 📜 About
|
||||||
|
|
||||||
```typescript
|
```json
|
||||||
export interface Divlo {
|
{
|
||||||
pronouns: 'He' | 'Him'
|
"name": "Divlo",
|
||||||
birthDate: '31/03/2003'
|
"pronouns": "He/Him",
|
||||||
nationality: 'Alsace, France'
|
"birthDate": "31/03/2003",
|
||||||
interests: [
|
"nationality": "Alsace, France",
|
||||||
'Developer Full Stack Junior',
|
"interests": [
|
||||||
'Passionate about High-Tech',
|
"Developer Full Stack Junior",
|
||||||
'Open-Source enthusiast'
|
"Passionate about High-Tech",
|
||||||
]
|
"Open-Source enthusiast"
|
||||||
skills: {
|
],
|
||||||
languages: ['JavaScript', 'TypeScript', 'Python', 'Dart']
|
"skills": {
|
||||||
frontEnd: ['HTML', 'CSS', 'SASS', 'React.js (+ Next.js)', 'Flutter']
|
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
||||||
backEnd: ['Node.js', 'Strapi', 'MySQL']
|
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
||||||
tools: ['Ubuntu', 'Hyper Terminal', 'VSCode', 'Git', 'Docker']
|
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
||||||
|
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
## 📈 Stats
|
## 📈 Statistics
|
||||||
|
|
||||||
<p align=center>
|
<p align=center>
|
||||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
|
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
|
||||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=Divlo&hide=html,css&langs_count=8&layout=compact&theme=dark" />
|
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=Divlo&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import Error404 from 'pages/404'
|
|
||||||
|
|
||||||
describe('GET /404', () => {
|
|
||||||
it('should render', async () => {
|
|
||||||
const { getByText } = render(<Error404 />)
|
|
||||||
expect(getByText('404')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import Error500 from 'pages/500'
|
|
||||||
|
|
||||||
describe('GET /500', () => {
|
|
||||||
it('should render', async () => {
|
|
||||||
const { getByText } = render(<Error500 />)
|
|
||||||
expect(getByText('500')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,70 +0,0 @@
|
|||||||
import { createMocks } from 'node-mocks-http'
|
|
||||||
|
|
||||||
import handleSendEmail from 'pages/api/send-email'
|
|
||||||
|
|
||||||
jest.mock('nodemailer', () => ({
|
|
||||||
createTransport: () => {
|
|
||||||
return {
|
|
||||||
sendMail: jest.fn(async () => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('POST /api/send-email', () => {
|
|
||||||
it('succeeds and send the email', async () => {
|
|
||||||
const { req, res } = createMocks({
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
name: 'Divlo',
|
|
||||||
email: 'contact@divlo.fr',
|
|
||||||
subject: 'Subject',
|
|
||||||
message: 'Hello world!'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await handleSendEmail(req, res)
|
|
||||||
expect(res._getStatusCode()).toBe(201)
|
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fails with empty values', async () => {
|
|
||||||
const { req, res } = createMocks({
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
subject: '',
|
|
||||||
message: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await handleSendEmail(req, res)
|
|
||||||
expect(res._getStatusCode()).toBe(400)
|
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'requiredFields'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fails with invalid email', async () => {
|
|
||||||
const { req, res } = createMocks({
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
name: 'Name',
|
|
||||||
email: 'random wrong email',
|
|
||||||
subject: 'Subject',
|
|
||||||
message: 'Message'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await handleSendEmail(req, res)
|
|
||||||
expect(res._getStatusCode()).toBe(400)
|
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'invalidEmail'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,31 +0,0 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
|
||||||
|
|
||||||
import { FormState } from './FormState'
|
|
||||||
import { ResultState } from './index'
|
|
||||||
|
|
||||||
export interface FormResultProps {
|
|
||||||
state: ResultState
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormResult: React.FC<FormResultProps> = (props) => {
|
|
||||||
const { state } = props
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (state === 'idle') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === 'loading' || state === 'success') {
|
|
||||||
return (
|
|
||||||
<FormState state={state}>
|
|
||||||
{t(`home:contact.result.${state}`)}
|
|
||||||
</FormState>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormState state='error'>
|
|
||||||
{t(`home:contact.result.${state}`)}
|
|
||||||
</FormState>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
|
||||||
|
|
||||||
export interface FormStateProps extends React.ComponentPropsWithRef<'p'> {
|
|
||||||
state: 'success' | 'error' | 'loading'
|
|
||||||
children: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormState: React.FC<FormStateProps> = props => {
|
|
||||||
const { state, children, ...rest } = props
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='form-result text-center'>
|
|
||||||
<p className={state} {...rest}>
|
|
||||||
{['error', 'success'].includes(state) && (
|
|
||||||
<b>
|
|
||||||
{state === 'error' ? t('home:contact.error') : t('home:contact.success')}:
|
|
||||||
</b>
|
|
||||||
)}{' '}
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.form-result {
|
|
||||||
margin: 30px;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
color: #90ee90;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #ff7f7f;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import Form, { HandleForm } from 'react-component-form'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { Input } from 'components/design/Input'
|
|
||||||
import { Button } from 'components/design/Button'
|
|
||||||
import { Textarea } from 'components/design/Textarea'
|
|
||||||
import { FormResult } from './FormResult'
|
|
||||||
|
|
||||||
export const resultState = [
|
|
||||||
'idle',
|
|
||||||
'success',
|
|
||||||
'loading',
|
|
||||||
'requiredFields',
|
|
||||||
'invalidEmail',
|
|
||||||
'serverError'
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type ResultState = typeof resultState[number]
|
|
||||||
|
|
||||||
export const Contact: React.FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [state, setState] = useState<ResultState>('idle')
|
|
||||||
|
|
||||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
|
||||||
setState('loading')
|
|
||||||
try {
|
|
||||||
const { data } = await axios.post<{ type: ResultState }>(
|
|
||||||
'/api/send-email',
|
|
||||||
formData
|
|
||||||
)
|
|
||||||
if (data.type === 'success') {
|
|
||||||
setState('success')
|
|
||||||
return formElement.reset()
|
|
||||||
}
|
|
||||||
return setState('serverError')
|
|
||||||
} catch (error) {
|
|
||||||
const type = error.response.data.type
|
|
||||||
if (resultState.includes(type)) {
|
|
||||||
return setState(type)
|
|
||||||
}
|
|
||||||
return setState('serverError')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='col-24'>
|
|
||||||
<Form onSubmit={handleSubmit}>
|
|
||||||
<Input
|
|
||||||
label={`${t('home:contact.nameField')} :`}
|
|
||||||
type='text'
|
|
||||||
name='name'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label='Email :'
|
|
||||||
type='email'
|
|
||||||
name='email'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={`${t('home:contact.subjectField')} :`}
|
|
||||||
type='text'
|
|
||||||
name='subject'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Textarea
|
|
||||||
label='Message :'
|
|
||||||
name='message'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='text-center' style={{ marginBottom: 20 }}>
|
|
||||||
<Button type='submit'>{t('home:contact.sendEmail')}</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<FormResult state={state} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -6,30 +6,45 @@ export interface ErrorPageProps {
|
|||||||
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 } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>
|
<h1 className='my-6 text-4xl font-semibold'>
|
||||||
{t('errors:error')} <span className='important'>{statusCode}</span>
|
{t('errors:error')}{' '}
|
||||||
|
<span
|
||||||
|
className='text-yellow dark:text-yellow-dark'
|
||||||
|
data-cy='status-code'
|
||||||
|
>
|
||||||
|
{statusCode}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-center'>
|
<p className='text-center text-lg'>
|
||||||
{message} <Link href='/'>{t('errors:returnToHomePage')}</Link>
|
{message}{' '}
|
||||||
|
<Link href='/'>
|
||||||
|
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||||
|
{t('errors:return-to-home-page')}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<style jsx global>{`
|
<style jsx global>
|
||||||
.content {
|
{`
|
||||||
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
min-height: 100%;
|
flex: 1;
|
||||||
}
|
}
|
||||||
#__next {
|
#__next {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,28 +1,40 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export interface FooterProps {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footer: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
|
const versionLink = useMemo(() => {
|
||||||
|
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||||
|
}, [version])
|
||||||
|
|
||||||
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='Footer text-center'>
|
|
||||||
<p>
|
<p>
|
||||||
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
|
<Link href='/'>
|
||||||
|
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||||
|
Divlo
|
||||||
|
</a>
|
||||||
|
</Link>{' '}
|
||||||
|
| {t('common:all-rights-reserved')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1'>
|
||||||
|
Version{' '}
|
||||||
|
<a
|
||||||
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
|
href={versionLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{version}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.Footer {
|
|
||||||
border-top: var(--border-header-footer);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,16 @@ import NextHead from 'next/head'
|
|||||||
interface HeadProps {
|
interface HeadProps {
|
||||||
title?: string
|
title?: string
|
||||||
image?: string
|
image?: string
|
||||||
description?: string
|
description: string
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Head: React.FC<HeadProps> = props => {
|
export const Head: React.FC<HeadProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
title = 'Divlo',
|
title = 'Divlo',
|
||||||
image = '/images/icons/icon-96x96.png',
|
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
||||||
description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
|
description,
|
||||||
url = 'https://divlo.divlo.fr/'
|
url = 'https://divlo.fr/'
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -21,7 +21,7 @@ export const Head: React.FC<HeadProps> = props => {
|
|||||||
<link rel='icon' type='image/png' href={image} />
|
<link rel='icon' type='image/png' href={image} />
|
||||||
|
|
||||||
{/* Meta Tag */}
|
{/* Meta Tag */}
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
<meta name='description' content={description} />
|
<meta name='description' content={description} />
|
||||||
<meta name='Language' content='fr, en' />
|
<meta name='Language' content='fr, en' />
|
||||||
<meta name='theme-color' content='#ffd800' />
|
<meta name='theme-color' content='#ffd800' />
|
||||||
@ -39,7 +39,7 @@ export const Head: React.FC<HeadProps> = props => {
|
|||||||
<meta name='twitter:card' content='summary' />
|
<meta name='twitter:card' content='summary' />
|
||||||
<meta name='twitter:description' content={description} />
|
<meta name='twitter:description' content={description} />
|
||||||
<meta name='twitter:title' content={title} />
|
<meta name='twitter:title' content={title} />
|
||||||
<meta name='twitter:image:src' content={image} />
|
<meta name='twitter:image' content={image} />
|
||||||
|
|
||||||
{/* Google Verification */}
|
{/* Google Verification */}
|
||||||
<meta
|
<meta
|
||||||
|
@ -8,8 +8,8 @@ export const Arrow: React.FC = () => {
|
|||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
className='fill-current text-black dark:text-white'
|
||||||
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
|
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
|
||||||
fill='#fff'
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -10,22 +10,15 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
|
quality={100}
|
||||||
width={35}
|
width={35}
|
||||||
height={35}
|
height={35}
|
||||||
src={`/images/languages/${language}.svg`}
|
src={`/images/languages/${language}.svg`}
|
||||||
alt={language}
|
alt={language}
|
||||||
/>
|
/>
|
||||||
<p className='language-title'>{language.toUpperCase()}</p>
|
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||||
|
{language.toUpperCase()}
|
||||||
<style jsx>
|
</p>
|
||||||
{`
|
|
||||||
.language-title {
|
|
||||||
margin: 0 8px 0 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: 'Arial', 'sans-serif';
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import setLanguage from 'next-translate/setLanguage'
|
import setLanguage from 'next-translate/setLanguage'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import i18n from 'i18n.json'
|
||||||
|
|
||||||
import { Arrow } from './Arrow'
|
import { Arrow } from './Arrow'
|
||||||
import { LanguageFlag } from './LanguageFlag'
|
import { LanguageFlag } from './LanguageFlag'
|
||||||
import { locales } from 'i18n.json'
|
|
||||||
|
|
||||||
export const Language: React.FC = () => {
|
export const Language: React.FC = () => {
|
||||||
const { lang: currentLanguage } = useTranslation()
|
const { lang: currentLanguage } = useTranslation()
|
||||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||||
|
|
||||||
|
const handleHiddenMenu = useCallback(() => {
|
||||||
|
setHiddenMenu(!hiddenMenu)
|
||||||
|
}, [hiddenMenu])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hiddenMenu) {
|
if (!hiddenMenu) {
|
||||||
window.document.addEventListener('click', handleHiddenMenu)
|
window.document.addEventListener('click', handleHiddenMenu)
|
||||||
@ -20,33 +26,39 @@ export const Language: React.FC = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
window.document.removeEventListener('click', handleHiddenMenu)
|
window.document.removeEventListener('click', handleHiddenMenu)
|
||||||
}
|
}
|
||||||
}, [hiddenMenu])
|
}, [hiddenMenu, handleHiddenMenu])
|
||||||
|
|
||||||
const handleLanguage = async (language: string): Promise<void> => {
|
const handleLanguage = async (language: string): Promise<void> => {
|
||||||
await setLanguage(language)
|
await setLanguage(language)
|
||||||
handleHiddenMenu()
|
handleHiddenMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHiddenMenu = (): void => {
|
|
||||||
setHiddenMenu(!hiddenMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||||
<div className='language-menu'>
|
<div
|
||||||
<div className='selected-language' onClick={handleHiddenMenu}>
|
data-cy='language-click'
|
||||||
|
className='mr-5 flex items-center'
|
||||||
|
onClick={handleHiddenMenu}
|
||||||
|
>
|
||||||
<LanguageFlag language={currentLanguage} />
|
<LanguageFlag language={currentLanguage} />
|
||||||
<Arrow />
|
<Arrow />
|
||||||
</div>
|
</div>
|
||||||
{!hiddenMenu && (
|
|
||||||
<ul>
|
<ul
|
||||||
{locales.map((language, index) => {
|
data-cy='languages-list'
|
||||||
|
className={classNames(
|
||||||
|
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||||
|
{ hidden: hiddenMenu }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n.locales.map((language, index) => {
|
||||||
if (language === currentLanguage) {
|
if (language === currentLanguage) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<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'
|
||||||
onClick={async () => await handleLanguage(language)}
|
onClick={async () => await handleLanguage(language)}
|
||||||
>
|
>
|
||||||
<LanguageFlag language={language} />
|
<LanguageFlag language={language} />
|
||||||
@ -54,52 +66,6 @@ export const Language: React.FC = () => {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.language-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.selected-language {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 60px;
|
|
||||||
width: 100px;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 15px 0 0px;
|
|
||||||
border-radius: 15%;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow: 0px 1px 10px var(--color-shadow);
|
|
||||||
background-color: var(--color-background-primary);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
ul > li {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 50px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
ul > li:hover {
|
|
||||||
background-color: rgba(79, 84, 92, 0.16);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
126
components/Header/SwitchTheme.tsx
Normal file
126
components/Header/SwitchTheme.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
export const SwitchTheme: React.FC = () => {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (): void => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='flex items-center'
|
||||||
|
data-cy='switch-theme-click'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
||||||
|
<div className='toggle-track'>
|
||||||
|
<div
|
||||||
|
data-cy='switch-theme-dark'
|
||||||
|
className='toggle-track-check absolute'
|
||||||
|
>
|
||||||
|
<span className='toggle_Dark relative flex items-center justify-center'>
|
||||||
|
🌜
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-cy='switch-theme-light'
|
||||||
|
className='toggle-track-x absolute'
|
||||||
|
>
|
||||||
|
<span className='toggle_Light relative flex items-center justify-center'>
|
||||||
|
🌞
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
|
|||||||
import { Header } from '..'
|
import { Header } from '..'
|
||||||
|
|
||||||
describe('<Header />', () => {
|
describe('<Header />', () => {
|
||||||
it('should render', async () => {
|
it('should render', () => {
|
||||||
const { getByText } = render(<Header />)
|
const { getByText } = render(<Header />)
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
expect(getByText('Divlo')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
@ -2,87 +2,47 @@ import Link from 'next/link'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import { Language } from './Language'
|
import { Language } from './Language'
|
||||||
|
import { SwitchTheme } from './SwitchTheme'
|
||||||
|
|
||||||
|
export interface HeaderProps {
|
||||||
|
showLanguage?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = (props) => {
|
||||||
|
const { showLanguage = false } = props
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
|
||||||
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='header'>
|
|
||||||
<div className='container'>
|
|
||||||
<nav className='navbar navbar-fixed-top'>
|
|
||||||
<Link href='/'>
|
<Link href='/'>
|
||||||
<a className='navbar__brand-link'>
|
<a>
|
||||||
<div className='navbar__brand'>
|
<div className='flex items-center justify-center'>
|
||||||
<Image
|
<Image
|
||||||
|
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'
|
||||||
/>
|
/>
|
||||||
<strong className='navbar__brand-title'>Divlo</strong>
|
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||||
|
Divlo
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className='navbar__buttons'>
|
<div className='flex justify-between'>
|
||||||
<Language />
|
<div className='flex flex-col items-center justify-center px-6'>
|
||||||
|
<Link href='/blog'>
|
||||||
|
<a
|
||||||
|
data-cy='header-blog-link'
|
||||||
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
{showLanguage && <Language />}
|
||||||
|
<SwitchTheme />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.header {
|
|
||||||
background-color: var(--color-background);
|
|
||||||
border-bottom: var(--border-header-footer);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
height: var(--header-height);
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 1280px;
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.navbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.navbar-fixed-top {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
.navbar__brand-link {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.navbar__brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.navbar__brand-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.navbar__buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
@media (max-width: 320px) {
|
|
||||||
.navbar__brand-title {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,12 @@ export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className='text-center'>
|
<p className='my-6 text-center text-gray dark:text-gray-dark'>
|
||||||
<strong className='important'>{title}</strong>
|
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
|
||||||
|
{title}
|
||||||
|
</strong>
|
||||||
<br />
|
<br />
|
||||||
<span className='paragraph-color'>{htmlParser(description)}</span>
|
<span>{htmlParser(description)}</span>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,41 +1,20 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Tooltip } from 'components/design/Tooltip'
|
|
||||||
|
|
||||||
interface InterestItemProps {
|
interface InterestItemProps {
|
||||||
title: string
|
title: string
|
||||||
fontAwesomeIcon: IconDefinition
|
fontAwesomeIcon: IconDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InterestItem: React.FC<InterestItemProps> = props => {
|
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'>
|
|
||||||
<Tooltip title={title}>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className='color-primary'
|
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
display: 'block'
|
|
||||||
}}
|
|
||||||
icon={fontAwesomeIcon}
|
icon={fontAwesomeIcon}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.interest-item {
|
|
||||||
margin: 7px 5px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,8 @@ import { InterestItem } from './InterestItem'
|
|||||||
|
|
||||||
export const InterestsList: React.FC = () => {
|
export const InterestsList: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='my-4 flex justify-center'>
|
||||||
<div className='container-list'>
|
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||||
<ul className='interests-list'>
|
|
||||||
<InterestItem
|
<InterestItem
|
||||||
title='Developer Full Stack Junior'
|
title='Developer Full Stack Junior'
|
||||||
fontAwesomeIcon={faCode}
|
fontAwesomeIcon={faCode}
|
||||||
@ -16,30 +15,8 @@ export const InterestsList: React.FC = () => {
|
|||||||
title='Passionate about High-Tech'
|
title='Passionate about High-Tech'
|
||||||
fontAwesomeIcon={faMicrochip}
|
fontAwesomeIcon={faMicrochip}
|
||||||
/>
|
/>
|
||||||
<InterestItem
|
<InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
|
||||||
title='Open-Source enthusiast'
|
|
||||||
fontAwesomeIcon={faGit}
|
|
||||||
/>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.container-list {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 15px 0 15px 0;
|
|
||||||
}
|
|
||||||
.interests-list {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 60%;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,20 @@ import { InterestsList } from './InterestsList'
|
|||||||
export const Interests: React.FC = () => {
|
export const Interests: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const paragraphs: InterestParagraphProps[] = t('home:interests.paragraphs', {}, {
|
const paragraphs: InterestParagraphProps[] = t(
|
||||||
|
'home:interests.paragraphs',
|
||||||
|
{},
|
||||||
|
{
|
||||||
returnObjects: true
|
returnObjects: true
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='max-w-full'>
|
||||||
<div className='col-24'>
|
|
||||||
{paragraphs.map((paragraph, index) => {
|
{paragraphs.map((paragraph, index) => {
|
||||||
return <InterestParagraph key={index} {...paragraph} />
|
return <InterestParagraph key={index} {...paragraph} />
|
||||||
})}
|
})}
|
||||||
<InterestsList />
|
<InterestsList />
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
24
components/OpenSource/Repository.tsx
Normal file
24
components/OpenSource/Repository.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||||
|
import { GitHubIcon } from 'components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
||||||
|
|
||||||
|
export interface RepositoryProps {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Repository: React.FC<RepositoryProps> = (props) => {
|
||||||
|
const { name, description, href } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
||||||
|
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='flex'>
|
||||||
|
<GitHubIcon className='mr-2 h-6' />
|
||||||
|
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
||||||
|
</div>
|
||||||
|
<p className='my-4'>{description}</p>
|
||||||
|
</a>
|
||||||
|
</ShadowContainer>
|
||||||
|
)
|
||||||
|
}
|
35
components/OpenSource/index.tsx
Normal file
35
components/OpenSource/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
import { Repository } from './Repository'
|
||||||
|
|
||||||
|
export const OpenSource: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-0 flex max-w-full flex-col items-center'>
|
||||||
|
<p className='text-center'>{t('home:open-source.description')}</p>
|
||||||
|
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
||||||
|
<Repository
|
||||||
|
name='nodejs/node'
|
||||||
|
description='Node.js JavaScript runtime 🐢🚀'
|
||||||
|
href='https://github.com/nodejs/node/commits?author=Divlo'
|
||||||
|
/>
|
||||||
|
<Repository
|
||||||
|
name='standard/standard'
|
||||||
|
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
||||||
|
href='https://github.com/standard/standard/commits?author=Divlo'
|
||||||
|
/>
|
||||||
|
<Repository
|
||||||
|
name='nrwl/nx'
|
||||||
|
description='Smart, Extensible Build Framework'
|
||||||
|
href='https://github.com/nrwl/nx/commits?author=Divlo'
|
||||||
|
/>
|
||||||
|
<Repository
|
||||||
|
name='vercel/next.js'
|
||||||
|
description='The React Framework for Production'
|
||||||
|
href='https://github.com/vercel/next.js/commits?author=Divlo'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||||
|
|
||||||
export interface PortfolioItemProps {
|
export interface PortfolioItemProps {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
@ -7,96 +9,35 @@ export interface PortfolioItemProps {
|
|||||||
image: string
|
image: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PortfolioItem: React.FC<PortfolioItemProps> = props => {
|
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||||
const { title, description, link, image } = props
|
const { title, description, link, image } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'>
|
||||||
<div className='col-sm-24 col-md-10 col-xl-7 portfolio-grid'>
|
|
||||||
<a
|
<a
|
||||||
className='portfolio-link'
|
className='group inline-flex justify-center'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
href={link}
|
href={link}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
<div className='portfolio-figure'>
|
<div className='flex justify-center'>
|
||||||
<Image width={300} height={300} src={image} alt={title} />
|
<Image
|
||||||
|
quality={100}
|
||||||
|
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='portfolio-caption'>
|
<div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'>
|
||||||
<h3 className='portfolio-title important'>{title}</h3>
|
<h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||||
<p className='portfolio-description'>{description}</p>
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className='my-6'>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</ShadowContainer>
|
||||||
|
|
||||||
<style jsx global>
|
|
||||||
{`
|
|
||||||
.portfolio-figure img[alt='${title}'] {
|
|
||||||
max-height: 300px;
|
|
||||||
max-width: 300px;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
}
|
|
||||||
.portfolio-grid:hover img[alt='${title}'] {
|
|
||||||
opacity: 0.05;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.portfolio-grid {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
flex-direction: column;
|
|
||||||
word-wrap: break-word;
|
|
||||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 1rem;
|
|
||||||
margin: 0 0 50px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
/* col-md */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.portfolio-grid {
|
|
||||||
margin: 0 30px 50px 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* col-xl */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.portfolio-grid {
|
|
||||||
margin: 0 20px 50px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.portfolio-figure {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.portfolio-caption {
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
opacity: 0;
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.portfolio-description {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.portfolio-grid:hover .portfolio-caption {
|
|
||||||
opacity: 1;
|
|
||||||
height: auto;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
text-align: center;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
.portfolio-grid:hover .portfolio-link {
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,19 @@ import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
|||||||
export const Portfolio: React.FC = () => {
|
export const Portfolio: React.FC = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
|
||||||
const items: PortfolioItemProps[] = t('home:portfolio.items', {}, {
|
const items: PortfolioItemProps[] = t(
|
||||||
|
'home:portfolio.items',
|
||||||
|
{},
|
||||||
|
{
|
||||||
returnObjects: true
|
returnObjects: true
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='flex w-full flex-wrap justify-center px-3'>
|
||||||
<div className='container-fluid'>
|
|
||||||
<div className='row justify-content-center'>
|
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
return <PortfolioItem key={index} {...item} />
|
return <PortfolioItem key={index} {...item} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
import Translation from 'next-translate/Trans'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
export const ProfileDescriptionBottom: React.FC = () => {
|
export const ProfileDescriptionBottom: React.FC = () => {
|
||||||
return (
|
const { t, lang } = useTranslation()
|
||||||
<>
|
|
||||||
<p className='profile-description-bottom'>
|
|
||||||
<Translation
|
|
||||||
i18nKey='home:about.descriptionBottom'
|
|
||||||
components={[<br key='break' />]}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<style jsx>
|
return (
|
||||||
{`
|
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||||
.profile-description-bottom {
|
{t('home:about.description-bottom')}
|
||||||
font-size: 16px;
|
{lang === 'fr' && (
|
||||||
display: block;
|
<>
|
||||||
font-weight: 400;
|
<br />
|
||||||
line-height: 25px;
|
<br />
|
||||||
color: #b2bac2;
|
<a
|
||||||
margin-top: 30px;
|
href='/curriculum-vitae'
|
||||||
margin-bottom: 0;
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
}
|
>
|
||||||
`}
|
Curriculum vitæ
|
||||||
</style>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,17 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
export const ProfileInfo: React.FC = () => {
|
export const ProfileInformation: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
||||||
<div className='profile-info'>
|
<h1 className='mb-2 text-4xl'>
|
||||||
<h1 className='profile-title'>
|
{t('home:about.i-am')}{' '}
|
||||||
{t('home:about.IAm')} <strong className='important'>Divlo</strong>
|
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
|
||||||
|
Divlo
|
||||||
|
</strong>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className='profile-description'>{t('home:about.description')}</h2>
|
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.profile-info {
|
|
||||||
padding-bottom: 25px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
border-bottom: 1px solid #dedede;
|
|
||||||
}
|
|
||||||
.profile-title {
|
|
||||||
font-size: 36px;
|
|
||||||
line-height: 1.1;
|
|
||||||
font-weight: 300;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.profile-title > strong {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.profile-description {
|
|
||||||
font-size: 17.4px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,20 @@ interface ProfileItemProps {
|
|||||||
link?: string
|
link?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||||
const { title, value, link } = props
|
const { title, value, link } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<li className='mb-3 before:table after:clear-both after:table'>
|
||||||
<li className='profile-list__item'>
|
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||||
<strong className='profile-list__item-title'>{title}</strong>
|
{title}
|
||||||
<span className='profile-list__item-info'>
|
</strong>
|
||||||
|
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||||
{link != null ? (
|
{link != null ? (
|
||||||
<a className='profile-list__link' href={link}>
|
<a
|
||||||
|
className='text-gray hover:underline dark:text-gray-dark'
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
@ -21,59 +25,5 @@ export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.profile-list__item {
|
|
||||||
margin-bottom: 13px;
|
|
||||||
}
|
|
||||||
.profile-list__item::after,
|
|
||||||
.profile-list__item::before {
|
|
||||||
content: ' ';
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
.profile-list__item::after {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.profile-list__item-title {
|
|
||||||
display: block;
|
|
||||||
width: 120px;
|
|
||||||
float: left;
|
|
||||||
color: #d4d4d5;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 20px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.profile-list__item-info {
|
|
||||||
display: block;
|
|
||||||
margin-left: 125px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #84898e;
|
|
||||||
}
|
|
||||||
.profile-list__link {
|
|
||||||
color: #84898e;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.profile-list__item-title {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
.profile-list__item-info {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.profile-list__item-info,
|
|
||||||
.profile-list__item-title {
|
|
||||||
width: 100%;
|
|
||||||
float: none;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DIVLO_BIRTHDAY_DAY,
|
||||||
|
DIVLO_BIRTHDAY_MONTH,
|
||||||
|
DIVLO_BIRTHDAY_YEAR
|
||||||
|
} from 'utils/getAge'
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from './ProfileItem'
|
||||||
|
|
||||||
export const ProfileList: React.FC = () => {
|
export interface ProfileListProps {
|
||||||
|
age: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileList: React.FC<ProfileListProps> = (props) => {
|
||||||
|
const { age } = props
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className='m-0 list-none p-0'>
|
||||||
<ul className='profile-list'>
|
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title={t('home:about.birthDate')}
|
title={t('home:about.birth-date')}
|
||||||
value='31/03/2003'
|
value={`${DIVLO_BIRTHDAY_DAY}/${DIVLO_BIRTHDAY_MONTH}/${DIVLO_BIRTHDAY_YEAR} (${age} ${t(
|
||||||
/>
|
'home:about.years-old'
|
||||||
<ProfileItem
|
)})`}
|
||||||
title={t('home:about.nationality')}
|
|
||||||
value='Alsace, France'
|
|
||||||
/>
|
/>
|
||||||
|
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title='Email'
|
title='Email'
|
||||||
value='contact@divlo.fr'
|
value='contact@divlo.fr'
|
||||||
link='mailto:contact@divlo.fr'
|
link='mailto:contact@divlo.fr'
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.profile-list {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,11 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
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='col-sm-24 col-md-10'>
|
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
||||||
<div className='profile-logo'>
|
|
||||||
<Image
|
|
||||||
width={800}
|
|
||||||
height={800}
|
|
||||||
src='/images/divlo_logo.png'
|
|
||||||
alt='Divlo'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.profile-logo {
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>Email</title>
|
||||||
|
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>GitLab</title>
|
||||||
|
<path d='M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
19
components/Profile/SocialMediaList/SocialMediaIcons/Icon.tsx
Normal file
19
components/Profile/SocialMediaList/SocialMediaIcons/Icon.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
const { children, className, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
className={classNames(
|
||||||
|
'h-8 w-8 fill-current text-black dark:text-white',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>npm</title>
|
||||||
|
<path d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>Twitch</title>
|
||||||
|
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>Twitter</title>
|
||||||
|
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
|
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<title>YouTube</title>
|
||||||
|
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
@ -1,50 +1,22 @@
|
|||||||
import { Tooltip } from 'components/design/Tooltip'
|
|
||||||
import Image from 'next/image'
|
|
||||||
|
|
||||||
interface SocialMediaItemProps {
|
interface SocialMediaItemProps {
|
||||||
link: string
|
link: string
|
||||||
socialMedia: 'Email' | 'GitHub' | 'Twitch' | 'Twitter' | 'YouTube'
|
ariaLabel: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = props => {
|
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
||||||
const { link, socialMedia } = props
|
const { link, ariaLabel, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<li className='mx-4 my-1 inline-block'>
|
||||||
<li className='social-media-list__item'>
|
|
||||||
<a
|
<a
|
||||||
href={link}
|
href={link}
|
||||||
aria-label={socialMedia}
|
aria-label={ariaLabel}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='social-media-list__link'
|
className='relative inline-block bg-transparent'
|
||||||
>
|
>
|
||||||
<Tooltip title={socialMedia}>
|
{children}
|
||||||
<Image
|
|
||||||
width={45}
|
|
||||||
height={45}
|
|
||||||
alt={socialMedia}
|
|
||||||
src={`/images/web/${socialMedia}.png`}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.social-media-list__item {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 5px 15px;
|
|
||||||
}
|
|
||||||
.social-media-list__link {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,39 @@
|
|||||||
import { SocialMediaItem } from './SocialMediaItem'
|
import { SocialMediaItem } from './SocialMediaItem'
|
||||||
|
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon'
|
||||||
|
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon'
|
||||||
|
import { GitLabIcon } from './SocialMediaIcons/GitLabIcon'
|
||||||
|
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon'
|
||||||
|
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
|
||||||
|
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
|
||||||
|
import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
||||||
|
|
||||||
export const SocialMediaList: React.FC = () => {
|
export const SocialMediaList: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
||||||
<div className='row justify-content-center'>
|
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
|
||||||
<ul className='social-media-list'>
|
<GitHubIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
|
<SocialMediaItem link='https://gitlab.com/Divlo' ariaLabel='GitLab'>
|
||||||
|
<GitLabIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
|
<SocialMediaItem link='https://www.npmjs.com/~divlo' ariaLabel='NPM'>
|
||||||
|
<NPMIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
|
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
|
||||||
|
<TwitterIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
<SocialMediaItem
|
<SocialMediaItem
|
||||||
socialMedia='Twitter'
|
|
||||||
link='https://twitter.com/Divlo_FR'
|
|
||||||
/>
|
|
||||||
<SocialMediaItem
|
|
||||||
socialMedia='GitHub'
|
|
||||||
link='https://github.com/Divlo'
|
|
||||||
/>
|
|
||||||
<SocialMediaItem
|
|
||||||
socialMedia='YouTube'
|
|
||||||
link='https://www.youtube.com/c/Divlo'
|
link='https://www.youtube.com/c/Divlo'
|
||||||
/>
|
ariaLabel='YouTube'
|
||||||
<SocialMediaItem
|
>
|
||||||
socialMedia='Twitch'
|
<YouTubeIcon />
|
||||||
link='https://www.twitch.tv/divlo'
|
</SocialMediaItem>
|
||||||
/>
|
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
|
||||||
<SocialMediaItem socialMedia='Email' link='mailto:contact@divlo.fr' />
|
<TwitchIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
|
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
|
||||||
|
<EmailIcon />
|
||||||
|
</SocialMediaItem>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.social-media-list {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px 0;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,23 @@
|
|||||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
||||||
import { ProfileInfo } from './ProfileInfo'
|
import { ProfileInformation } from './ProfileInfo'
|
||||||
import { ProfileList } from './ProfileList'
|
import { ProfileList } from './ProfileList'
|
||||||
import { ProfileLogo } from './ProfileLogo'
|
import { ProfileLogo } from './ProfileLogo'
|
||||||
|
|
||||||
export const Profile: React.FC = () => {
|
export interface ProfileProps {
|
||||||
|
age: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Profile: React.FC<ProfileProps> = (props) => {
|
||||||
|
const { age } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
|
||||||
<div className='row profile'>
|
|
||||||
<ProfileLogo />
|
<ProfileLogo />
|
||||||
<div className='col-sm-24 col-md-14'>
|
<div>
|
||||||
<ProfileInfo />
|
<ProfileInformation />
|
||||||
<ProfileList />
|
<ProfileList age={age} />
|
||||||
<ProfileDescriptionBottom />
|
<ProfileDescriptionBottom />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.profile {
|
|
||||||
padding: 40px 50px 15px 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.profile {
|
|
||||||
padding: 40px 10px 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,36 @@
|
|||||||
|
import { useTheme } from 'next-themes'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { skills } from './skills'
|
import { skills } from './skills'
|
||||||
|
|
||||||
export interface SkillProps {
|
export interface SkillComponentProps {
|
||||||
skill: keyof typeof skills
|
skill: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Skill: React.FC<SkillProps> = props => {
|
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||||
const { skill } = props
|
const { skill } = props
|
||||||
const skillProperties = skills[skill]
|
const skillProperties = skills[skill]
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
const image = useMemo(() => {
|
||||||
|
if (typeof skillProperties.image !== 'string') {
|
||||||
|
return skillProperties.image[theme ?? 'light']
|
||||||
|
}
|
||||||
|
return skillProperties.image
|
||||||
|
}, [skillProperties, theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<a
|
<a
|
||||||
href={skillProperties.link}
|
href={skillProperties.link}
|
||||||
className='skills-link'
|
className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<div className='skills-content text-center'>
|
<div className='text-center'>
|
||||||
<Image
|
<Image quality={100} width={60} height={60} alt={skill} src={image} />
|
||||||
width={60}
|
<p className='mt-1'>{skill}</p>
|
||||||
height={60}
|
|
||||||
alt={skill}
|
|
||||||
src={skillProperties.image}
|
|
||||||
/>
|
|
||||||
<p className='skills-text'>{skill}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.skills-link {
|
|
||||||
max-width: 120px;
|
|
||||||
margin: 0px 10px 0 10px;
|
|
||||||
}
|
|
||||||
.skills-text {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,40 +5,23 @@ export interface SkillsSectionProps {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SkillsSection: React.FC<SkillsSectionProps> = props => {
|
export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
|
||||||
const { title, children } = props
|
const { title, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='container-fluid'>
|
<div className='mx-auto w-full px-4'>
|
||||||
<div className='row row-padding'>
|
<div className='flex flex-wrap px-4 py-6'>
|
||||||
<div className='col-24'>
|
<div className='flex-1'>
|
||||||
<div className='skills-header'>
|
<div className='mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10'>
|
||||||
<h3 className='important'>{title}</h3>
|
<h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className='skills-body'>{children}</div>
|
<div className='flex flex-wrap justify-around'>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.skills-header {
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.skills-header > h3 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.skills-body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { Skill } from './Skill'
|
import { SkillComponent } from './Skill'
|
||||||
import { SkillsSection } from './SkillsSection'
|
import { SkillsSection } from './SkillsSection'
|
||||||
|
|
||||||
export const Skills: React.FC = () => {
|
export const Skills: React.FC = () => {
|
||||||
@ -9,32 +9,33 @@ export const Skills: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SkillsSection title={t('home:skills.languages')}>
|
<SkillsSection title={t('home:skills.languages')}>
|
||||||
<Skill skill='JavaScript' />
|
<SkillComponent skill='JavaScript' />
|
||||||
<Skill skill='TypeScript' />
|
<SkillComponent skill='TypeScript' />
|
||||||
<Skill skill='Python' />
|
<SkillComponent skill='Python' />
|
||||||
<Skill skill='Dart' />
|
<SkillComponent skill='C/C++' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Front-end'>
|
<SkillsSection title='Front-end'>
|
||||||
<Skill skill='HTML' />
|
<SkillComponent skill='HTML' />
|
||||||
<Skill skill='CSS' />
|
<SkillComponent skill='CSS' />
|
||||||
<Skill skill='SASS' />
|
<SkillComponent skill='Tailwind CSS' />
|
||||||
<Skill skill='React.js (+ Next.js)' />
|
<SkillComponent skill='React.js (+ Next.js)' />
|
||||||
<Skill skill='Flutter' />
|
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Back-end'>
|
<SkillsSection title='Back-end'>
|
||||||
<Skill skill='Node.js' />
|
<SkillComponent skill='Node.js' />
|
||||||
<Skill skill='Strapi' />
|
<SkillComponent skill='Fastify' />
|
||||||
<Skill skill='MySQL' />
|
<SkillComponent skill='Prisma' />
|
||||||
|
<SkillComponent skill='PostgreSQL' />
|
||||||
|
<SkillComponent skill='MySQL' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title={t('home:skills.softwareTools')}>
|
<SkillsSection title={t('home:skills.software-tools')}>
|
||||||
<Skill skill='Ubuntu' />
|
<SkillComponent skill='GNU/Linux' />
|
||||||
<Skill skill='Hyper' />
|
<SkillComponent skill='Ubuntu' />
|
||||||
<Skill skill='Visual Studio Code' />
|
<SkillComponent skill='Visual Studio Code' />
|
||||||
<Skill skill='Git' />
|
<SkillComponent skill='Git' />
|
||||||
<Skill skill='Docker' />
|
<SkillComponent skill='Docker' />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
export const skills = {
|
export interface Skill {
|
||||||
|
link: string
|
||||||
|
image: string | { [key: string]: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface 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'
|
||||||
@ -11,6 +20,10 @@ export const skills = {
|
|||||||
link: 'https://www.python.org/',
|
link: 'https://www.python.org/',
|
||||||
image: '/images/skills/Python.png'
|
image: '/images/skills/Python.png'
|
||||||
},
|
},
|
||||||
|
'C/C++': {
|
||||||
|
link: 'https://isocpp.org/',
|
||||||
|
image: '/images/skills/C-Cpp.png'
|
||||||
|
},
|
||||||
Dart: {
|
Dart: {
|
||||||
link: 'https://dart.dev/',
|
link: 'https://dart.dev/',
|
||||||
image: '/images/skills/Dart.png'
|
image: '/images/skills/Dart.png'
|
||||||
@ -27,6 +40,10 @@ export const skills = {
|
|||||||
link: 'https://developer.mozilla.org/docs/Web/CSS',
|
link: 'https://developer.mozilla.org/docs/Web/CSS',
|
||||||
image: '/images/skills/CSS.png'
|
image: '/images/skills/CSS.png'
|
||||||
},
|
},
|
||||||
|
'Tailwind CSS': {
|
||||||
|
link: 'https://tailwindcss.com/',
|
||||||
|
image: '/images/skills/TailwindCSS.png'
|
||||||
|
},
|
||||||
SASS: {
|
SASS: {
|
||||||
link: 'https://sass-lang.com/',
|
link: 'https://sass-lang.com/',
|
||||||
image: '/images/skills/SASS.svg'
|
image: '/images/skills/SASS.svg'
|
||||||
@ -39,6 +56,24 @@ export const skills = {
|
|||||||
link: 'https://nodejs.org/',
|
link: 'https://nodejs.org/',
|
||||||
image: '/images/skills/NodeJS.png'
|
image: '/images/skills/NodeJS.png'
|
||||||
},
|
},
|
||||||
|
Fastify: {
|
||||||
|
link: 'https://www.fastify.io/',
|
||||||
|
image: {
|
||||||
|
light: '/images/skills/Fastify-light.png',
|
||||||
|
dark: '/images/skills/Fastify-dark.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Prisma: {
|
||||||
|
link: 'https://www.prisma.io/',
|
||||||
|
image: {
|
||||||
|
light: '/images/skills/Prisma-light.png',
|
||||||
|
dark: '/images/skills/Prisma-dark.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PostgreSQL: {
|
||||||
|
link: 'https://www.postgresql.org/',
|
||||||
|
image: '/images/skills/PostgreSQL.png'
|
||||||
|
},
|
||||||
MySQL: {
|
MySQL: {
|
||||||
link: 'https://www.mysql.com/',
|
link: 'https://www.mysql.com/',
|
||||||
image: '/images/skills/MySQL.png'
|
image: '/images/skills/MySQL.png'
|
||||||
@ -63,6 +98,10 @@ export const skills = {
|
|||||||
link: 'https://ubuntu.com/',
|
link: 'https://ubuntu.com/',
|
||||||
image: '/images/skills/Ubuntu.png'
|
image: '/images/skills/Ubuntu.png'
|
||||||
},
|
},
|
||||||
|
'GNU/Linux': {
|
||||||
|
link: 'https://www.gnu.org/',
|
||||||
|
image: '/images/skills/GNU-Linux.png'
|
||||||
|
},
|
||||||
Docker: {
|
Docker: {
|
||||||
link: 'https://www.docker.com/',
|
link: 'https://www.docker.com/',
|
||||||
image: '/images/skills/Docker.png'
|
image: '/images/skills/Docker.png'
|
||||||
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
|
|||||||
import { ErrorPage } from '../ErrorPage'
|
import { ErrorPage } from '../ErrorPage'
|
||||||
|
|
||||||
describe('<ErrorPage />', () => {
|
describe('<ErrorPage />', () => {
|
||||||
it('should render the message and statusCode', async () => {
|
it('should render the message and statusCode', () => {
|
||||||
const messageContent = 'message content'
|
const messageContent = 'message content'
|
||||||
const statusCode = 404
|
const statusCode = 404
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
|
@ -3,8 +3,14 @@ import { render } from '@testing-library/react'
|
|||||||
import { Footer } from '../Footer'
|
import { Footer } from '../Footer'
|
||||||
|
|
||||||
describe('<Footer />', () => {
|
describe('<Footer />', () => {
|
||||||
it('should render', async () => {
|
it('should render with appropriate link tag version', () => {
|
||||||
const { getByText } = render(<Footer />)
|
const version = '1.0.0'
|
||||||
|
const { getByText } = render(<Footer version={version} />)
|
||||||
|
const versionLink = getByText(version) as HTMLAnchorElement
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
expect(getByText('Divlo')).toBeInTheDocument()
|
||||||
|
expect(versionLink).toBeInTheDocument()
|
||||||
|
expect(versionLink.href).toEqual(
|
||||||
|
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
|
|
||||||
type ButtonProps = React.ComponentPropsWithRef<'button'>
|
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
(props, ref) => {
|
|
||||||
const { children, ...rest } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button ref={ref} {...rest} className='btn btn-dark'>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.btn {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: color 0.15s ease-in-out,
|
|
||||||
background-color 0.15s ease-in-out,
|
|
||||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
.btn-dark {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #343a40;
|
|
||||||
border-color: #343a40;
|
|
||||||
}
|
|
||||||
.btn-dark:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #23272b;
|
|
||||||
border-color: #1d2124;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,75 +0,0 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
|
|
||||||
interface InputProps extends React.HTMLProps<HTMLInputElement> {
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
|
||||||
const { label, name, ...rest } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='form-group-animation'>
|
|
||||||
<input ref={ref} {...rest} id={name} name={name} />
|
|
||||||
<label htmlFor={name} className='label'>
|
|
||||||
<span className='label-content'>{label}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.form-group-animation {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.form-group-animation input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-top: 35px;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.form-group-animation label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
}
|
|
||||||
.form-group-animation label::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: -1px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 3px solid var(--color-primary);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
.label-content {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 5px;
|
|
||||||
left: 0px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.form-group-animation input:focus + .label .label-content,
|
|
||||||
.form-group-animation input:valid + .label .label-content {
|
|
||||||
transform: translateY(-150%);
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
.form-group-animation input:focus + .label::after,
|
|
||||||
.form-group-animation input:valid + .label::after {
|
|
||||||
transform: translateX(0%);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export const RevealFade: React.FC = props => {
|
export const RevealFade: React.FC = (props) => {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
const htmlElement = useRef<HTMLDivElement>(null)
|
const htmlElement = useRef<HTMLDivElement>(null)
|
||||||
@ -8,9 +8,10 @@ export const RevealFade: React.FC = props => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new window.IntersectionObserver(
|
const observer = new window.IntersectionObserver(
|
||||||
(entries, observer) => {
|
(entries, observer) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('reveal-visible')
|
entry.target.className =
|
||||||
|
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
|
||||||
observer.unobserve(entry.target)
|
observer.unobserve(entry.target)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -25,25 +26,8 @@ export const RevealFade: React.FC = props => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div ref={htmlElement} className='invisible -translate-y-7 opacity-0'>
|
||||||
<div ref={htmlElement} className='reveal'>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.reveal {
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transform: translateY(-30px);
|
|
||||||
}
|
|
||||||
.reveal-visible {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transform: translateY(0);
|
|
||||||
transition: all 500ms ease-out 100ms;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,11 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
|
|
||||||
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
||||||
|
|
||||||
export const SectionHeading = forwardRef<
|
export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
||||||
HTMLHeadingElement,
|
|
||||||
SectionHeadingProps
|
|
||||||
>((props, ref) => {
|
|
||||||
const { children, ...rest } = props
|
const { children, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
|
||||||
<h2 ref={ref} {...rest} className='Section__title'>
|
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.Section__title {
|
|
||||||
font-size: 34px;
|
|
||||||
margin-top: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
|
|
||||||
import { ShadowContainer } from '../ShadowContainer'
|
import { ShadowContainer } from '../ShadowContainer'
|
||||||
import { SectionHeading } from './SectionHeading'
|
import { SectionHeading } from './SectionHeading'
|
||||||
|
|
||||||
@ -10,7 +8,7 @@ type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
|||||||
withoutShadowContainer?: boolean
|
withoutShadowContainer?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
|
export const Section: React.FC<SectionProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
heading,
|
heading,
|
||||||
@ -22,26 +20,28 @@ export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
|
|||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
return (
|
return (
|
||||||
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer style={{ marginTop: 50 }}>
|
<ShadowContainer style={{ marginTop: 50 }}>
|
||||||
<section ref={ref} {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||||
<div className='container-fluid'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withoutShadowContainer) {
|
if (withoutShadowContainer) {
|
||||||
return (
|
return (
|
||||||
<section ref={ref} {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||||
<div className='container-fluid'>{children}</div>
|
<div className='w-full px-3'>{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section ref={ref} {...rest}>
|
<section {...rest}>
|
||||||
{heading != null && (
|
{heading != null && (
|
||||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
||||||
{heading}
|
{heading}
|
||||||
@ -52,11 +52,11 @@ export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
|
|||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<div className='w-full px-3'>
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='container-fluid'>
|
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||||
<div className='row row-padding'>{children}</div>
|
|
||||||
</div>
|
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
@ -1,32 +1,19 @@
|
|||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||||
|
|
||||||
export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
|
export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
className={`shadow-container ${className != null ? className : ''}`}
|
className={classNames(
|
||||||
|
'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.shadow-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
word-wrap: break-word;
|
|
||||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
|
|
||||||
interface TextareaProps extends React.HTMLProps<HTMLTextAreaElement> {
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
||||||
(props, ref) => {
|
|
||||||
const { label, name, ...rest } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
<br />
|
|
||||||
<textarea id={name} name={name} ref={ref} {...rest} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.form-group {
|
|
||||||
padding-top: 15px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.form-group textarea {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-text);
|
|
||||||
outline: none;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
padding: 10px;
|
|
||||||
resize: vertical;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,49 +0,0 @@
|
|||||||
interface TooltipProps extends React.ComponentPropsWithRef<'div'> {
|
|
||||||
title: string
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tooltip: React.FC<TooltipProps> = props => {
|
|
||||||
const { title, children, ...rest } = props
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className='tooltip' {...rest}>
|
|
||||||
{children}
|
|
||||||
<span className='title'>{title}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #222222;
|
|
||||||
padding: 5px 8px;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: all 0.15s ease-in;
|
|
||||||
transform: translate3d(0, -15px, 0);
|
|
||||||
backface-visibility: hidden;
|
|
||||||
}
|
|
||||||
.tooltip ~ .tooltip:hover .title,
|
|
||||||
.tooltip:first-child:hover .title {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transition: all 0.35s ease-out;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
margin: 0;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Button } from '../Button'
|
|
||||||
|
|
||||||
describe('<Button />', () => {
|
|
||||||
it('should render', async () => {
|
|
||||||
const { getByText } = render(<Button>Submit</Button>)
|
|
||||||
expect(getByText('Submit')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,11 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Input } from '../Input'
|
|
||||||
|
|
||||||
describe('<Input />', () => {
|
|
||||||
it('should render the label', async () => {
|
|
||||||
const labelContent = 'label content'
|
|
||||||
const { getByText } = render(<Input label={labelContent} />)
|
|
||||||
expect(getByText(labelContent)).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
8
cypress.json
Normal file
8
cypress.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:3000",
|
||||||
|
"pluginsFile": false,
|
||||||
|
"supportFile": false,
|
||||||
|
"fixturesFolder": false,
|
||||||
|
"video": false,
|
||||||
|
"screenshotOnRunFailure": false
|
||||||
|
}
|
58
cypress/integration/common/Header.spec.ts
Normal file
58
cypress/integration/common/Header.spec.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
describe('Common > Header', () => {
|
||||||
|
beforeEach(() => cy.visit('/'))
|
||||||
|
|
||||||
|
it('should redirect to /blog on click of the blog link', () => {
|
||||||
|
cy.get('[data-cy=header-blog-link]')
|
||||||
|
.click()
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/blog')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should always be visible (sticky header)', () => {
|
||||||
|
cy.scrollTo('bottom').get('header').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Switch theme color (dark/light)', () => {
|
||||||
|
it('should switch theme from `dark` (default) to `light`', () => {
|
||||||
|
cy.get('[data-cy=switch-theme-dark]').should('be.visible')
|
||||||
|
cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
|
||||||
|
cy.get('body').should(
|
||||||
|
'not.have.css',
|
||||||
|
'background-color',
|
||||||
|
'rgb(255, 255, 255)'
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.get('[data-cy=switch-theme-click]').click()
|
||||||
|
|
||||||
|
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
|
||||||
|
cy.get('[data-cy=switch-theme-light]').should('be.visible')
|
||||||
|
cy.get('body').should(
|
||||||
|
'have.css',
|
||||||
|
'background-color',
|
||||||
|
'rgb(255, 255, 255)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Switch Language', () => {
|
||||||
|
it('should switch language from EN (default) to FR', () => {
|
||||||
|
cy.get('h1').contains('I am Divlo')
|
||||||
|
cy.get('[data-cy=language-flag-text]').contains('EN')
|
||||||
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
|
cy.get('[data-cy=language-click]').click()
|
||||||
|
cy.get('[data-cy=languages-list]').should('be.visible')
|
||||||
|
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
|
||||||
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
|
cy.get('[data-cy=language-flag-text]').contains('FR')
|
||||||
|
cy.get('h1').contains('Je suis Divlo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close the language list menu when clicking outside', () => {
|
||||||
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
|
cy.get('[data-cy=language-click]').click()
|
||||||
|
cy.get('[data-cy=languages-list]').should('be.visible')
|
||||||
|
cy.get('h1').click()
|
||||||
|
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
7
cypress/integration/pages/404.spec.ts
Normal file
7
cypress/integration/pages/404.spec.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
describe('Page /404', () => {
|
||||||
|
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
|
||||||
|
|
||||||
|
it('should display the statusCode of 404', () => {
|
||||||
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
|
})
|
||||||
|
})
|
7
cypress/integration/pages/500.spec.ts
Normal file
7
cypress/integration/pages/500.spec.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
describe('Page /500', () => {
|
||||||
|
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
|
||||||
|
|
||||||
|
it('should display the statusCode of 500', () => {
|
||||||
|
cy.get('[data-cy=status-code]').contains('500')
|
||||||
|
})
|
||||||
|
})
|
13
cypress/integration/pages/blog/[slug].spec.ts
Normal file
13
cypress/integration/pages/blog/[slug].spec.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
describe('Page /blog/[slug]', () => {
|
||||||
|
it('should displays the first blog post (`hello-world`)', () => {
|
||||||
|
cy.visit('/blog/hello-world')
|
||||||
|
cy.get('[data-cy=language-flag-text]').should('not.exist')
|
||||||
|
cy.get('h1').should('have.text', '👋 Hello, world!')
|
||||||
|
cy.get('.prose a').should('have.attr', 'target', '_blank')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should redirect to /404 if the blog post doesn't exist", () => {
|
||||||
|
cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false })
|
||||||
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
|
})
|
||||||
|
})
|
22
cypress/integration/pages/blog/index.spec.ts
Normal file
22
cypress/integration/pages/blog/index.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
describe('Page /blog', () => {
|
||||||
|
it('should displays the blog posts sorted from newest to oldest', () => {
|
||||||
|
cy.visit('/blog')
|
||||||
|
cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]')
|
||||||
|
.last()
|
||||||
|
.should('have.text', '👋 Hello, world!')
|
||||||
|
cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]')
|
||||||
|
.last()
|
||||||
|
.should(
|
||||||
|
'have.text',
|
||||||
|
'First post of the blog, introduction and explanation of how this blog is made.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect the user to the right blog post', () => {
|
||||||
|
cy.visit('/blog')
|
||||||
|
cy.get('[data-cy=hello-world]')
|
||||||
|
.click()
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/blog/hello-world')
|
||||||
|
})
|
||||||
|
})
|
19
cypress/integration/pages/index.spec.ts
Normal file
19
cypress/integration/pages/index.spec.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
describe('Page /', () => {
|
||||||
|
beforeEach(() => cy.visit('/'))
|
||||||
|
|
||||||
|
it('should reveals the sections while scrolling except the about section', () => {
|
||||||
|
const sectionsReveals = [
|
||||||
|
'#interests',
|
||||||
|
'#skills',
|
||||||
|
'#portfolio',
|
||||||
|
'#open-source'
|
||||||
|
]
|
||||||
|
cy.get('#about').should('be.visible')
|
||||||
|
for (const section of sectionsReveals) {
|
||||||
|
cy.get(section)
|
||||||
|
.should('not.be.visible')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('be.visible')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
9
cypress/tsconfig.json
Normal file
9
cypress/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["cypress"],
|
||||||
|
"isolatedModules": false
|
||||||
|
},
|
||||||
|
"include": ["../node_modules/cypress", "./**/*.ts"]
|
||||||
|
}
|
@ -1,18 +1,12 @@
|
|||||||
version: '3.0'
|
version: '3.0'
|
||||||
services:
|
services:
|
||||||
divlo.fr-website:
|
divlo.fr:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
|
image: 'divlo.fr'
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: './'
|
||||||
ports:
|
ports:
|
||||||
- '${PORT}:${PORT}'
|
- '${PORT}:${PORT}'
|
||||||
environment:
|
environment:
|
||||||
PORT: ${PORT}
|
PORT: ${PORT}
|
||||||
volumes:
|
env_file: './.env'
|
||||||
- './:/app'
|
|
||||||
|
|
||||||
divlo.fr-maildev:
|
|
||||||
image: 'maildev/maildev:1.1.0'
|
|
||||||
ports:
|
|
||||||
- '1080:80'
|
|
||||||
container_name: 'divlo.fr-maildev'
|
|
||||||
|
@ -1,26 +1,14 @@
|
|||||||
module.exports = {
|
const nextJest = require('next/jest')
|
||||||
roots: ['<rootDir>'],
|
|
||||||
transform: {
|
const createJestConfig = nextJest()
|
||||||
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
|
const customJestConfig = {
|
||||||
},
|
|
||||||
moduleDirectories: ['node_modules', './'],
|
moduleDirectories: ['node_modules', './'],
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
modulePathIgnorePatterns: ['<rootDir>/cypress'],
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: [
|
||||||
'@testing-library/jest-dom/extend-expect',
|
'@testing-library/jest-dom/extend-expect',
|
||||||
'@testing-library/react'
|
'@testing-library/react'
|
||||||
],
|
]
|
||||||
collectCoverage: true,
|
|
||||||
collectCoverageFrom: [
|
|
||||||
'**/*.{js,jsx,ts,tsx}',
|
|
||||||
'!**/*.d.ts',
|
|
||||||
'!**/.next/**',
|
|
||||||
'!**/node_modules/**',
|
|
||||||
'!**/next.config.js',
|
|
||||||
'!**/postcss.config.js',
|
|
||||||
'!**/workbox-*.js',
|
|
||||||
'!**/sw.js',
|
|
||||||
'!**/jest.config.js'
|
|
||||||
],
|
|
||||||
coverageDirectory: './coverage',
|
|
||||||
coverageReporters: ['text', 'cobertura']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = createJestConfig(customJestConfig)
|
||||||
|
4
jsonresume-theme-custom/.gitignore
vendored
Normal file
4
jsonresume-theme-custom/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
theme/index.html
|
||||||
|
dist
|
||||||
|
.parcel-cache
|
28
jsonresume-theme-custom/index.js
Normal file
28
jsonresume-theme-custom/index.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import ejs from 'ejs'
|
||||||
|
import date from 'date-and-time'
|
||||||
|
import { Parcel } from '@parcel/core'
|
||||||
|
|
||||||
|
export const render = async (resume) => {
|
||||||
|
const themeIndexURL = new URL('./theme/index.ejs', import.meta.url)
|
||||||
|
const themeBuildURL = new URL('./theme/index.html', import.meta.url)
|
||||||
|
const indexHTMLURL = new URL('./dist/index.html', import.meta.url)
|
||||||
|
const themeBuildPath = fileURLToPath(themeBuildURL)
|
||||||
|
const html = await ejs.renderFile(fileURLToPath(themeIndexURL), {
|
||||||
|
date,
|
||||||
|
locals: {
|
||||||
|
...resume
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await fs.promises.writeFile(themeBuildURL, html, { encoding: 'utf-8' })
|
||||||
|
const bundler = new Parcel({
|
||||||
|
entries: themeBuildPath,
|
||||||
|
source: themeBuildPath,
|
||||||
|
mode: 'production',
|
||||||
|
defaultConfig: '@parcel/config-default'
|
||||||
|
})
|
||||||
|
await bundler.run()
|
||||||
|
return await fs.promises.readFile(indexHTMLURL, { encoding: 'utf-8' })
|
||||||
|
}
|
4100
jsonresume-theme-custom/package-lock.json
generated
Normal file
4100
jsonresume-theme-custom/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user