Compare commits
157 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
4b7d184c91 | |||
3e18536c2e | |||
8027e2deed | |||
c21abdcc81 | |||
3382177e27 | |||
73352d4414 | |||
f11425b2c2 | |||
039f4d172b | |||
b0a608c1fa |
@ -1,12 +1,3 @@
|
|||||||
{
|
{
|
||||||
"presets": [
|
"presets": ["next/babel"]
|
||||||
[
|
|
||||||
"next/babel",
|
|
||||||
{
|
|
||||||
"styled-jsx": {
|
|
||||||
"plugins": ["@styled-jsx/plugin-sass"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
7
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.163.1/containers/javascript-node/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
|
ARG VARIANT="14-buster"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||||
|
|
||||||
|
ARG EXTRA_NODE_VERSION=16
|
||||||
|
RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
24
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"coenraads.bracket-pair-colorizer",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
],
|
||||||
|
"forwardPorts": [3000],
|
||||||
|
"postAttachCommand": ["npm", "clean-install"],
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
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
|
|
||||||
|
7
.eslintignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.next
|
||||||
|
.lighthouseci
|
||||||
|
node_modules
|
||||||
|
next-env.d.ts
|
||||||
|
**/workbox-*.js
|
||||||
|
**/sw.js
|
||||||
|
.vercel
|
20
.eslintrc.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"standard-with-typescript",
|
||||||
|
"next",
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error"
|
||||||
|
}
|
||||||
|
}
|
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?
|
||||||
|
|
||||||
|
125
.github/workflows/Divlo.yml
vendored
@ -2,37 +2,128 @@ name: 'Divlo'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, development]
|
branches: [master, develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, development]
|
branches: [master, develop]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
analyze:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
language: ['javascript']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v2'
|
- uses: 'actions/checkout@v2.3.4'
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: 'Initialize CodeQL'
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
uses: 'github/codeql-action/init@v1'
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: 'Cache dependencies'
|
- name: 'Perform CodeQL Analysis'
|
||||||
uses: 'actions/cache@v2.1.5'
|
uses: 'github/codeql-action/analyze@v1'
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v2.3.4'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v2.3.0'
|
||||||
with:
|
with:
|
||||||
path: '.npm'
|
node-version: '16.x'
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
- 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:commit -- --to "${{ github.sha }}"'
|
||||||
- run: 'npm run lint:docker'
|
- run: 'npm run lint:docker'
|
||||||
- run: 'npm run lint:editorconfig'
|
- run: 'npm run lint:editorconfig'
|
||||||
- run: 'npm run lint:markdown'
|
- run: 'npm run lint:markdown'
|
||||||
- run: 'npm run lint:typescript'
|
- run: 'npm run lint:typescript'
|
||||||
- run: 'npm run build'
|
|
||||||
- run: 'npm run lighthouse'
|
build:
|
||||||
- run: 'npm run test'
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v2.3.4'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v2.3.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Build'
|
||||||
|
run: 'npm run build'
|
||||||
|
|
||||||
|
- name: 'Lighthouse'
|
||||||
|
run: 'npm run lighthouse'
|
||||||
|
env:
|
||||||
|
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v2.3.4'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v2.3.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Test'
|
||||||
|
run: 'npm run test'
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||||
|
needs: [analyze, lint, build, test]
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v2.3.4'
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: 'Import GPG key'
|
||||||
|
uses: 'crazy-max/ghaction-import-gpg@v3.1.0'
|
||||||
|
with:
|
||||||
|
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
git-user-signingkey: true
|
||||||
|
git-commit-gpgsign: true
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v2.3.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Release'
|
||||||
|
run: 'npm run release'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
GIT_AUTHOR_NAME: ${{ secrets.GIT_NAME }}
|
||||||
|
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_EMAIL }}
|
||||||
|
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 }}
|
||||||
|
34
.github/workflows/release.yml
vendored
@ -1,34 +0,0 @@
|
|||||||
name: 'release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [Divlo]
|
|
||||||
branches: [master]
|
|
||||||
types:
|
|
||||||
- 'completed'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
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 release'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
20
.gitignore
vendored
@ -26,11 +26,23 @@ coverage
|
|||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
||||||
# editors
|
# IDEs and editors
|
||||||
.vscode
|
/.idea
|
||||||
.theia
|
.project
|
||||||
.idea
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.lighthouseci
|
.lighthouseci
|
||||||
|
.vercel
|
||||||
|
20
.gitpod.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
image: 'gitpod/workspace-full'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- before: 'cp .env.example .env && npm install --global npm@7'
|
||||||
|
init: 'npm clean-install'
|
||||||
|
command: 'npm run dev'
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
onOpen: 'open-preview'
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
master: true
|
||||||
|
branches: true
|
||||||
|
pullRequests: true
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
addComment: true
|
||||||
|
addBadge: true
|
||||||
|
addLabel: true
|
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
|
|
||||||
|
@ -10,15 +10,9 @@
|
|||||||
"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",
|
"uses-responsive-images": "warning"
|
||||||
"canonical": "off",
|
|
||||||
"unsized-images": "off",
|
|
||||||
"uses-responsive-images": "off",
|
|
||||||
"bypass": "warning",
|
|
||||||
"color-contrast": "warning",
|
|
||||||
"preload-lcp-image": "warning"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
|
11
.lintstagedrc.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"*": ["editorconfig-checker"],
|
||||||
|
"*.{js,ts,jsx,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"jest --findRelatedTests"
|
||||||
|
],
|
||||||
|
"*.{css,yml,json}": ["prettier --write"],
|
||||||
|
"*.{md}": ["prettier --write", "markdownlint --dot --fix"],
|
||||||
|
"./Dockerfile": ["dockerfilelint"]
|
||||||
|
}
|
9
.prettierignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.next
|
||||||
|
.lighthouseci
|
||||||
|
node_modules
|
||||||
|
next-env.d.ts
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
**/workbox-*.js
|
||||||
|
**/sw.js
|
||||||
|
.vercel
|
6
.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
@ -1,12 +1,32 @@
|
|||||||
{
|
{
|
||||||
"release": {
|
"branches": ["master"],
|
||||||
"branches": ["master"]
|
|
||||||
},
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
["@semantic-release/commit-analyzer", {
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
"preset": "conventionalcommits"
|
"preset": "conventionalcommits"
|
||||||
}],
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/github"
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/npm",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": ["package.json", "package-lock.json"],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github",
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeStrategy": "merge"
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
14
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"divlo.vscode-styled-jsx-syntax",
|
||||||
|
"divlo.vscode-styled-jsx-languageserver",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"coenraads.bracket-pair-colorizer",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"prettier.configPath": ".prettierrc.json",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": true
|
||||||
|
}
|
||||||
|
}
|
@ -47,6 +47,13 @@ Scopes define what part of the code changed.
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 14.0.0
|
||||||
|
- [npm](https://www.npmjs.com/) >= 7.0.0
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -58,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
|
||||||
@ -70,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`
|
|
||||||
|
29
Dockerfile
@ -1,10 +1,23 @@
|
|||||||
FROM node:14.16.1
|
FROM node:16.5.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 clean-install
|
||||||
COPY ./ ./
|
|
||||||
|
|
||||||
CMD ["npm", "run", "dev", "--", "--port", "${PORT}"]
|
FROM node:16.5.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.5.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}"]
|
||||||
|
35
README.md
@ -5,7 +5,7 @@
|
|||||||
</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/Divlo/actions/workflows/Divlo.yml"><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,21 +20,22 @@
|
|||||||
|
|
||||||
## 📜 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"],
|
||||||
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": ["Ubuntu", "Hyper Terminal", "VSCode", "Git", "Docker"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -45,5 +46,5 @@ export interface Divlo {
|
|||||||
|
|
||||||
<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>
|
||||||
|
10
__test__/pages/404.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
10
__test__/pages/500.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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,40 @@ 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 font-semibold text-4xl'>
|
||||||
{t('errors:error')} <span className='important'>{statusCode}</span>
|
{t('errors:error')}{' '}
|
||||||
|
<span className='text-yellow dark:text-yellow-dark'>{statusCode}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-center'>
|
<p className='text-center text-lg'>
|
||||||
{message} <Link href='/'>{t('returnToHomePage')}</Link>
|
{message}{' '}
|
||||||
|
<Link href='/'>
|
||||||
|
<a className='text-yellow dark:text-yellow-dark hover:underline'>
|
||||||
|
{t('errors:returnToHomePage')}
|
||||||
|
</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>
|
||||||
|
40
components/Footer.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
export interface FooterProps {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footer: React.FC<FooterProps> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
|
const versionLink = useMemo(() => {
|
||||||
|
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||||
|
}, [version])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className='bg-white flex flex-col items-center justify-center py-6 text-lg border-t-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
|
||||||
|
<p>
|
||||||
|
<Link href='/'>
|
||||||
|
<a className='hover:underline text-yellow dark:text-yellow-dark'>
|
||||||
|
Divlo
|
||||||
|
</a>
|
||||||
|
</Link>{' '}
|
||||||
|
| {t('common:allRightsReserved')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1'>
|
||||||
|
Version{' '}
|
||||||
|
<a
|
||||||
|
className='hover:underline text-yellow dark:text-yellow-dark'
|
||||||
|
href={versionLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{version}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<footer className='Footer text-center'>
|
|
||||||
<p>
|
|
||||||
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.Footer {
|
|
||||||
border-top: var(--border-header-footer);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -7,12 +7,12 @@ interface HeadProps {
|
|||||||
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 = '/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 = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
|
||||||
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' />
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -15,17 +15,7 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
|||||||
src={`/images/languages/${language}.svg`}
|
src={`/images/languages/${language}.svg`}
|
||||||
alt={language}
|
alt={language}
|
||||||
/>
|
/>
|
||||||
<p className='language-title'>{language.toUpperCase()}</p>
|
<p className='mx-2 text-base'>{language.toUpperCase()}</p>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.language-title {
|
|
||||||
margin: 0 8px 0 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: 'Arial', 'sans-serif';
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
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 { Arrow } from './Arrow'
|
import { Arrow } from './Arrow'
|
||||||
import { LanguageFlag } from './LanguageFlag'
|
import { LanguageFlag } from './LanguageFlag'
|
||||||
import { locales } from 'i18n.json'
|
import i18n 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 +24,29 @@ 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 flex-col justify-center items-center cursor-pointer'>
|
||||||
<div className='language-menu'>
|
<div className='flex items-center mr-5' onClick={handleHiddenMenu}>
|
||||||
<div className='selected-language' onClick={handleHiddenMenu}>
|
|
||||||
<LanguageFlag language={currentLanguage} />
|
<LanguageFlag language={currentLanguage} />
|
||||||
<Arrow />
|
<Arrow />
|
||||||
</div>
|
</div>
|
||||||
{!hiddenMenu && (
|
{!hiddenMenu && (
|
||||||
<ul>
|
<ul className='flex flex-col justify-center items-center absolute p-0 top-14 z-10 w-24 mt-3 mr-4 rounded-lg list-none shadow-light dark:shadow-dark bg-white dark:bg-black'>
|
||||||
{locales.map((language, index) => {
|
{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 items-center justify-center w-full h-12 hover:bg-[#4f545c] hover:bg-opacity-20 pl-2'
|
||||||
onClick={async () => await handleLanguage(language)}
|
onClick={async () => await handleLanguage(language)}
|
||||||
>
|
>
|
||||||
<LanguageFlag language={language} />
|
<LanguageFlag language={language} />
|
||||||
@ -56,50 +56,5 @@ 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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
127
components/Header/SwitchTheme.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='toggle-button'
|
||||||
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||||
|
>
|
||||||
|
<div className='toggle-theme-button'>
|
||||||
|
<div className='toggle-track'>
|
||||||
|
<div className='toggle-track-check'>
|
||||||
|
<span className='toggle_Dark'>🌜</span>
|
||||||
|
</div>
|
||||||
|
<div className='toggle-track-x'>
|
||||||
|
<span className='toggle_Light'>🌞</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='toggle-thumb' />
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
aria-label='Dark mode toggle'
|
||||||
|
className='toggle-screenreader-only'
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx>
|
||||||
|
{`
|
||||||
|
.toggle-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.toggle-theme-button {
|
||||||
|
touch-action: pan-x;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
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 {
|
||||||
|
position: absolute;
|
||||||
|
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 {
|
||||||
|
position: absolute;
|
||||||
|
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 {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
.toggle-thumb {
|
||||||
|
position: absolute;
|
||||||
|
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;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
10
components/Header/__test__/Header.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
|
||||||
|
import { Header } from '..'
|
||||||
|
|
||||||
|
describe('<Header />', () => {
|
||||||
|
it('should render', async () => {
|
||||||
|
const { getByText } = render(<Header />)
|
||||||
|
expect(getByText('Divlo')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -2,87 +2,30 @@ 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 const Header: React.FC = () => {
|
export const Header: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<header className='bg-white sticky top-0 z-50 flex w-full justify-between px-6 py-2 border-b-2 border-gray-600 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
|
||||||
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 font-headline font-semibold hidden xs:block text-yellow dark:text-yellow-dark'>
|
||||||
|
Divlo
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className='navbar__buttons'>
|
<div className='flex justify-between'>
|
||||||
<Language />
|
<Language />
|
||||||
</div>
|
<SwitchTheme />
|
||||||
</nav>
|
|
||||||
</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='text-center my-6 text-gray dark:text-gray-dark'>
|
||||||
<strong className='important'>{title}</strong>
|
<strong className='text-yellow font-medium text-lg 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 w-8 h-8' title={title}>
|
||||||
<li className='interest-item'>
|
|
||||||
<Tooltip title={title}>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className='color-primary'
|
className='text-yellow cursor-pointer h-full w-full block 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='flex justify-center my-4'>
|
||||||
<div className='container-list'>
|
<ul className='flex justify-around p-0 m-0 list-none w-96'>
|
||||||
<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,13 +6,17 @@ 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='col-24'>
|
<div className='max-w-full'>
|
||||||
{paragraphs.map((paragraph, index) => {
|
{paragraphs.map((paragraph, index) => {
|
||||||
return <InterestParagraph key={index} {...paragraph} />
|
return <InterestParagraph key={index} {...paragraph} />
|
||||||
})}
|
})}
|
||||||
|
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='cursor-pointer relative p-6 !mb-4 max-h-32 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
||||||
|
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='flex'>
|
||||||
|
<GitHubIcon className='h-6 mr-2' />
|
||||||
|
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
||||||
|
</div>
|
||||||
|
<p className='my-4'>{description}</p>
|
||||||
|
</a>
|
||||||
|
</ShadowContainer>
|
||||||
|
)
|
||||||
|
}
|
47
components/OpenSource/index.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
import { Repository } from './Repository'
|
||||||
|
|
||||||
|
export const OpenSource: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='max-w-full mt-0 flex flex-col items-center'>
|
||||||
|
<p className='text-center'>{t('home:open-source.description')}</p>
|
||||||
|
<div className='grid grid-cols-1 md:w-10/12 md:grid-cols-2 gap-6 my-6'>
|
||||||
|
<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/styled-jsx'
|
||||||
|
description='Full CSS support for JSX without compromises'
|
||||||
|
href='https://github.com/vercel/styled-jsx/commits?author=Divlo'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx global>{`
|
||||||
|
.animation-custom {
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease 0s;
|
||||||
|
}
|
||||||
|
.animation-custom:hover {
|
||||||
|
transform: translateY(-7px);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
export interface PortfolioItemProps {
|
export interface PortfolioItemProps {
|
||||||
@ -7,96 +8,34 @@ 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='cursor-pointer relative 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
|
||||||
|
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='opacity-0 transition-opacity duration-500 h-auto absolute text-center overflow-hidden bottom-0 group-hover:opacity-100'>
|
||||||
<h3 className='portfolio-title important'>{title}</h3>
|
<h3 className='text-yellow text-xl font-semibold my-6 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 flex-wrap justify-center px-3 w-full'>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,11 @@ import Translation from 'next-translate/Trans'
|
|||||||
|
|
||||||
export const ProfileDescriptionBottom: React.FC = () => {
|
export const ProfileDescriptionBottom: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<p className='mt-8 mb-8 font-normal text-base text-gray dark:text-gray-dark'>
|
||||||
<p className='profile-description-bottom'>
|
|
||||||
<Translation
|
<Translation
|
||||||
i18nKey='home:about.descriptionBottom'
|
i18nKey='home:about.descriptionBottom'
|
||||||
components={[<br key='break' />]}
|
components={[<br key='break' />]}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<style jsx>
|
|
||||||
{`
|
|
||||||
.profile-description-bottom {
|
|
||||||
font-size: 16px;
|
|
||||||
display: block;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 25px;
|
|
||||||
color: #b2bac2;
|
|
||||||
margin-top: 30px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,14 @@ export const ProfileInfo: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='profile-info'>
|
<div className='pb-2 mb-6 border-b-2 font-headline border-gray-600 dark:border-gray-400'>
|
||||||
<h1 className='profile-title'>
|
<h1 className='text-4xl mb-2'>
|
||||||
{t('home:about.IAm')} <strong className='important'>Divlo</strong>
|
{t('home:about.IAm')}{' '}
|
||||||
|
<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='text-base mb-3'>{t('home:about.description')}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>
|
<style jsx>
|
||||||
|
@ -4,16 +4,21 @@ 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='profile-list__item'>
|
<li className='profile-list__item'>
|
||||||
<strong className='profile-list__item-title'>{title}</strong>
|
<strong className='profile-list__item-title text-black dark:text-white'>
|
||||||
<span className='profile-list__item-info'>
|
{title}
|
||||||
|
</strong>
|
||||||
|
<span className='profile-list__item-info text-gray dark:text-gray-dark'>
|
||||||
{link != null ? (
|
{link != null ? (
|
||||||
<a className='profile-list__link' href={link}>
|
<a
|
||||||
|
className='text-gray dark:text-gray-dark hover:underline'
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
@ -39,7 +44,6 @@ export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
float: left;
|
float: left;
|
||||||
color: #d4d4d5;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@ -51,10 +55,6 @@ export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: #84898e;
|
|
||||||
}
|
|
||||||
.profile-list__link {
|
|
||||||
color: #84898e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
|
@ -6,32 +6,14 @@ export const ProfileList: React.FC = () => {
|
|||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className='m-0 p-0 list-none'>
|
||||||
<ul className='profile-list'>
|
<ProfileItem title={t('home:about.birthDate')} value='31/03/2003' />
|
||||||
<ProfileItem
|
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||||
title={t('home:about.birthDate')}
|
|
||||||
value='31/03/2003'
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,13 @@ import Image from 'next/image'
|
|||||||
|
|
||||||
export const ProfileLogo: React.FC = () => {
|
export const ProfileLogo: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='px-2 py-6'>
|
||||||
<div className='col-sm-24 col-md-10'>
|
|
||||||
<div className='profile-logo'>
|
|
||||||
<Image
|
<Image
|
||||||
width={800}
|
width={370}
|
||||||
height={800}
|
height={370}
|
||||||
src='/images/divlo_logo.png'
|
src='/images/divlo_logo.png'
|
||||||
alt='Divlo'
|
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
@ -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(
|
||||||
|
'dark:text-white text-black w-8 h-8 fill-current',
|
||||||
|
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='inline-block mx-4 my-1'>
|
||||||
<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 py-4 list-none 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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,29 +5,13 @@ import { ProfileLogo } from './ProfileLogo'
|
|||||||
|
|
||||||
export const Profile: React.FC = () => {
|
export const Profile: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='flex flex-col justify-center items-center px-10 pt-2 md:pt-10 xl:pt-0 md:flex-row'>
|
||||||
<div className='row profile'>
|
|
||||||
<ProfileLogo />
|
<ProfileLogo />
|
||||||
<div className='col-sm-24 col-md-14'>
|
<div>
|
||||||
<ProfileInfo />
|
<ProfileInfo />
|
||||||
<ProfileList />
|
<ProfileList />
|
||||||
<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 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='w-full px-4 mx-auto'>
|
||||||
<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-opacity-10 dark:border-white'>
|
||||||
<h3 className='important'>{title}</h3>
|
<h3 className='text-yellow font-semibold text-xl my-3 dark:text-yellow-dark'>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className='skills-body'>{children}</div>
|
<div className='flex justify-around flex-wrap'>{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,32 @@ 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' />
|
|
||||||
</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.softwareTools')}>
|
||||||
<Skill skill='Ubuntu' />
|
<SkillComponent skill='Ubuntu' />
|
||||||
<Skill skill='Hyper' />
|
<SkillComponent skill='Hyper' />
|
||||||
<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'
|
||||||
|
15
components/__test__/ErrorPage.test.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
|
||||||
|
import { ErrorPage } from '../ErrorPage'
|
||||||
|
|
||||||
|
describe('<ErrorPage />', () => {
|
||||||
|
it('should render the message and statusCode', async () => {
|
||||||
|
const messageContent = 'message content'
|
||||||
|
const statusCode = 404
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorPage statusCode={statusCode} message={messageContent} />
|
||||||
|
)
|
||||||
|
expect(getByText(messageContent)).toBeInTheDocument()
|
||||||
|
expect(getByText(statusCode)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
12
components/__test__/Footer.test.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
|
||||||
|
import { Footer } from '../Footer'
|
||||||
|
|
||||||
|
describe('<Footer />', () => {
|
||||||
|
it('should render', async () => {
|
||||||
|
const version = '1.0.0'
|
||||||
|
const { getByText } = render(<Footer version={version} />)
|
||||||
|
expect(getByText('Divlo')).toBeInTheDocument()
|
||||||
|
expect(getByText(version)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -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,7 +8,7 @@ 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.classList.add('reveal-visible')
|
||||||
observer.unobserve(entry.target)
|
observer.unobserve(entry.target)
|
||||||
@ -30,7 +30,8 @@ export const RevealFade: React.FC = props => {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>
|
||||||
|
{`
|
||||||
.reveal {
|
.reveal {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -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='text-4xl font-semibold text-center mt-1 mb-3'>
|
||||||
<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='px-3 w-full'>
|
||||||
<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='px-3 w-full'>{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='px-3 w-full'>{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='px-3 w-full'>
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='container-fluid'>
|
<div className='px-16 py-4 leading-8 w-full'>{children}</div>
|
||||||
<div className='row row-padding'>{children}</div>
|
|
||||||
</div>
|
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
|
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(
|
||||||
|
'shadow-container h-full max-w-full break-words',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -15,14 +20,9 @@ export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
|
|||||||
<style jsx>
|
<style jsx>
|
||||||
{`
|
{`
|
||||||
.shadow-container {
|
.shadow-container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
word-wrap: break-word;
|
|
||||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
@ -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,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'
|
|
||||||
|
@ -5,21 +5,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleDirectories: ['node_modules', './'],
|
moduleDirectories: ['node_modules', './'],
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: [
|
||||||
'@testing-library/jest-dom/extend-expect',
|
'@testing-library/jest-dom/extend-expect',
|
||||||
'@testing-library/react'
|
'@testing-library/react'
|
||||||
],
|
],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
collectCoverageFrom: [
|
|
||||||
'**/*.{js,jsx,ts,tsx}',
|
|
||||||
'!**/*.d.ts',
|
|
||||||
'!**/.next/**',
|
|
||||||
'!**/node_modules/**',
|
|
||||||
'!**/next.config.js',
|
|
||||||
'!**/postcss.config.js',
|
|
||||||
'!**/workbox-*.js',
|
|
||||||
'!**/sw.js'
|
|
||||||
],
|
|
||||||
coverageDirectory: './coverage',
|
coverageDirectory: './coverage',
|
||||||
coverageReporters: ['text', 'cobertura']
|
coverageReporters: ['text', 'cobertura']
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,32 @@
|
|||||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
||||||
"birthDate": "Birth date",
|
"birthDate": "Birth date",
|
||||||
"nationality": "Nationality",
|
"nationality": "Nationality",
|
||||||
"descriptionBottom": "I'm learning online programming languages to improve my skills in my passion. <0/> <0/> I designed my graphic chart and my website."
|
"descriptionBottom": "I am self-taught in Computer Science by following online trainings. <0/> <0/> I put into practice everything I learn and make many projects."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "My Interests",
|
"title": "Interests",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Developer Full Stack Junior :",
|
"title": "Developer Full Stack Junior :",
|
||||||
"description": "Computer programming is my main passion, I love it! <br/> Mostly web development for the moment but I'm programming some Python and others programming language too."
|
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming some Python and others programming language too."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionate about High-Tech :",
|
"title": "Passionate about High-Tech :",
|
||||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and even better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Open-Source enthusiast :",
|
"title": "Open-Source enthusiast :",
|
||||||
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "My skills",
|
"title": "Skills",
|
||||||
"languages": "Programming languages",
|
"languages": "Programming languages",
|
||||||
"softwareTools": "Software and tools"
|
"softwareTools": "Software and tools"
|
||||||
},
|
},
|
||||||
"portfolio": {
|
"portfolio": {
|
||||||
"title": "My Portfolio",
|
"title": "Portfolio",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "function.divlo.fr",
|
"title": "function.divlo.fr",
|
||||||
@ -42,22 +42,16 @@
|
|||||||
"description": "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.",
|
"description": "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.",
|
||||||
"link": "https://thream.divlo.fr/",
|
"link": "https://thream.divlo.fr/",
|
||||||
"image": "/images/portfolio/threamdivlofr.png"
|
"image": "/images/portfolio/threamdivlofr.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Leon",
|
||||||
|
"description": "Leon is your open-source personal assistant.",
|
||||||
|
"link": "https://getleon.ai/",
|
||||||
|
"image": "/images/portfolio/leon.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"contact": {
|
"open-source": {
|
||||||
"title": "Contact-Me",
|
"description": "List of most famous open source projects I contributed to."
|
||||||
"nameField": "Name",
|
|
||||||
"subjectField": "Subject",
|
|
||||||
"sendEmail": "Send email",
|
|
||||||
"result": {
|
|
||||||
"loading": "Loading...",
|
|
||||||
"success": "Your email has been sent!",
|
|
||||||
"requiredFields": "You must fill all the fields...",
|
|
||||||
"invalidEmail": "Please enter a valid email address...",
|
|
||||||
"serverError": "The server could not process your request..."
|
|
||||||
},
|
|
||||||
"error": "Error",
|
|
||||||
"success": "Success"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,32 @@
|
|||||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||||
"birthDate": "Date de naissance",
|
"birthDate": "Date de naissance",
|
||||||
"nationality": "Nationalité",
|
"nationality": "Nationalité",
|
||||||
"descriptionBottom": "J'apprends en ligne l'informatique et les langages de programmation pour m'améliorer dans ma passion. <0/> <0/> J'ai conçu ma charte graphique et mon site internet."
|
"descriptionBottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne. <0/> <0/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Mes intérêts",
|
"title": "Intérêts",
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
{
|
{
|
||||||
"title": "Développeur Full Stack Junior :",
|
"title": "Développeur Full Stack Junior :",
|
||||||
"description": "La programmation informatique est ma principale passion, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi du Python et d'autres langages de programmation."
|
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi du Python et d'autres langages de programmation."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passionné de High-Tech :",
|
"title": "Passionné de High-Tech :",
|
||||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et même meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enthousiaste de l'Open-Source :",
|
"title": "Enthousiaste de l'Open-Source :",
|
||||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "Mes compétences",
|
"title": "Compétences",
|
||||||
"languages": "Langages de programmation",
|
"languages": "Langages de programmation",
|
||||||
"softwareTools": "Logiciels et outils"
|
"softwareTools": "Logiciels et outils"
|
||||||
},
|
},
|
||||||
"portfolio": {
|
"portfolio": {
|
||||||
"title": "Mon Portfolio",
|
"title": "Portfolio",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "function.divlo.fr",
|
"title": "function.divlo.fr",
|
||||||
@ -42,22 +42,16 @@
|
|||||||
"description": "Votre plateforme open source pour rester proche de vos amis et communautés, parler, discuter, collaborer, partager et vous amuser.",
|
"description": "Votre plateforme open source pour rester proche de vos amis et communautés, parler, discuter, collaborer, partager et vous amuser.",
|
||||||
"link": "https://thream.divlo.fr/",
|
"link": "https://thream.divlo.fr/",
|
||||||
"image": "/images/portfolio/threamdivlofr.png"
|
"image": "/images/portfolio/threamdivlofr.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Leon",
|
||||||
|
"description": "Leon est votre assistant personnel open source.",
|
||||||
|
"link": "https://getleon.ai/",
|
||||||
|
"image": "/images/portfolio/leon.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"contact": {
|
"open-source": {
|
||||||
"title": "Contactez-Moi",
|
"description": "Liste des projets open source les plus célèbres auxquels j'ai contribué."
|
||||||
"nameField": "Nom",
|
|
||||||
"subjectField": "Objet",
|
|
||||||
"sendEmail": "Envoyer l'email",
|
|
||||||
"result": {
|
|
||||||
"loading": "Chargement...",
|
|
||||||
"success": "Votre email a été envoyé!",
|
|
||||||
"requiredFields": "Vous devez remplir tous les champs...",
|
|
||||||
"invalidEmail": "Veuillez entrer une adresse mail valide...",
|
|
||||||
"serverError": "Le serveur n'a pas pu traiter votre requête..."
|
|
||||||
},
|
|
||||||
"error": "Erreur",
|
|
||||||
"success": "Succès"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
next-env.d.ts
vendored
@ -1,2 +1,3 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/types/global" />
|
/// <reference types="next/types/global" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
34261
package-lock.json
generated
100
package.json
@ -1,26 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "divlo",
|
||||||
"version": "0.0.0-development",
|
"version": "1.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Divlo/Divlo"
|
"url": "https://github.com/Divlo/Divlo"
|
||||||
},
|
},
|
||||||
"ts-standard": {
|
"engines": {
|
||||||
"ignore": [
|
"node": ">=14.0.0",
|
||||||
".next",
|
"npm": ">=7.0.0"
|
||||||
".lighthouseci",
|
|
||||||
"node_modules",
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/workbox-*.js",
|
|
||||||
"**/sw.js"
|
|
||||||
],
|
|
||||||
"envs": [
|
|
||||||
"node",
|
|
||||||
"browser",
|
|
||||||
"jest"
|
|
||||||
],
|
|
||||||
"report": "stylish"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@ -31,59 +19,65 @@
|
|||||||
"lint:docker": "dockerfilelint './Dockerfile'",
|
"lint:docker": "dockerfilelint './Dockerfile'",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
||||||
"lint:typescript": "ts-standard",
|
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
||||||
|
"lint:staged": "lint-staged",
|
||||||
"lighthouse": "lhci autorun",
|
"lighthouse": "lhci autorun",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
|
"deploy": "vercel",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/montserrat": "4.2.2",
|
"@fontsource/montserrat": "4.5.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "0.1.14",
|
"@fortawesome/react-fontawesome": "0.1.14",
|
||||||
"axios": "0.21.1",
|
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"html-react-parser": "1.2.6",
|
"html-react-parser": "1.2.7",
|
||||||
"next": "10.1.3",
|
"next": "11.0.1",
|
||||||
"next-pwa": "5.2.14",
|
"next-pwa": "5.2.24",
|
||||||
"next-translate": "1.0.6",
|
"next-themes": "0.0.15",
|
||||||
"nodemailer": "6.5.0",
|
"next-translate": "1.0.7",
|
||||||
"normalize.css": "8.0.1",
|
|
||||||
"nprogress": "0.2.0",
|
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-component-form": "1.3.0",
|
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"universal-cookie": "4.0.4",
|
"read-pkg": "5.2.0",
|
||||||
"validator": "13.5.2"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "12.1.1",
|
"@commitlint/cli": "13.1.0",
|
||||||
"@commitlint/config-conventional": "12.1.1",
|
"@commitlint/config-conventional": "13.1.0",
|
||||||
"@fullhuman/postcss-purgecss": "4.0.3",
|
"@lhci/cli": "0.8.0",
|
||||||
"@lhci/cli": "0.7.1",
|
"@saithodev/semantic-release-backmerge": "1.5.3",
|
||||||
"@styled-jsx/plugin-sass": "3.0.0",
|
"@semantic-release/git": "9.0.0",
|
||||||
"@testing-library/jest-dom": "5.11.10",
|
"@testing-library/jest-dom": "5.14.1",
|
||||||
"@testing-library/react": "11.2.6",
|
"@testing-library/react": "12.0.0",
|
||||||
"@types/jest": "26.0.22",
|
"@types/jest": "26.0.24",
|
||||||
"@types/node": "14.14.41",
|
"@types/node": "16.4.3",
|
||||||
"@types/nodemailer": "6.4.1",
|
"@types/react": "17.0.15",
|
||||||
"@types/nprogress": "0.2.0",
|
"@types/styled-jsx": "2.2.9",
|
||||||
"@types/react": "17.0.3",
|
"@typescript-eslint/eslint-plugin": "4.28.5",
|
||||||
"@types/styled-jsx": "2.2.8",
|
"autoprefixer": "10.3.1",
|
||||||
"@types/validator": "13.1.3",
|
"babel-jest": "27.0.6",
|
||||||
"babel-jest": "26.6.3",
|
|
||||||
"dockerfilelint": "1.8.0",
|
"dockerfilelint": "1.8.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"husky": "6.0.0",
|
"eslint": "7.31.0",
|
||||||
"jest": "26.6.3",
|
"eslint-config-next": "11.0.1",
|
||||||
"markdownlint-cli": "0.27.1",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"node-mocks-http": "1.10.1",
|
"eslint-config-standard-with-typescript": "20.0.0",
|
||||||
"postcss": "8.2.10",
|
"eslint-plugin-import": "2.23.4",
|
||||||
"sass": "1.32.11",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"semantic-release": "17.4.2",
|
"eslint-plugin-prettier": "3.4.0",
|
||||||
"ts-standard": "10.0.0",
|
"eslint-plugin-promise": "5.1.0",
|
||||||
"typescript": "4.2.4"
|
"husky": "7.0.1",
|
||||||
|
"jest": "27.0.6",
|
||||||
|
"lint-staged": "11.1.1",
|
||||||
|
"markdownlint-cli": "0.28.1",
|
||||||
|
"postcss": "8.3.6",
|
||||||
|
"prettier": "2.3.2",
|
||||||
|
"semantic-release": "17.4.4",
|
||||||
|
"tailwindcss": "2.2.7",
|
||||||
|
"typescript": "4.3.5",
|
||||||
|
"vercel": "23.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
import { GetStaticProps } from 'next'
|
import { GetStaticProps } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import readPackageJSON from 'read-pkg'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
|
import { Header } from 'components/Header'
|
||||||
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
|
|
||||||
const Error404: React.FC = () => {
|
const Error404: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='Divlo - 404' />
|
<Head title='Divlo - 404' />
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
<ErrorPage statusCode={404} message={t('errors:notFound')} />
|
<ErrorPage statusCode={404} message={t('errors:notFound')} />
|
||||||
|
</main>
|
||||||
|
<Footer version={version} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||||
return { props: {} }
|
const { version } = await readPackageJSON()
|
||||||
|
return { props: { version } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Error404
|
export default Error404
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
import { GetStaticProps } from 'next'
|
import { GetStaticProps } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import readPackageJSON from 'read-pkg'
|
||||||
|
|
||||||
import { ErrorPage } from 'components/ErrorPage'
|
import { ErrorPage } from 'components/ErrorPage'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
|
import { Header } from 'components/Header'
|
||||||
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
|
|
||||||
const Error500: React.FC = () => {
|
const Error500: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='Divlo - 500' />
|
<Head title='Divlo - 500' />
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
<ErrorPage statusCode={500} message={t('errors:serverError')} />
|
<ErrorPage statusCode={500} message={t('errors:serverError')} />
|
||||||
|
</main>
|
||||||
|
<Footer version={version} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||||
return { props: {} }
|
const { version } = await readPackageJSON()
|
||||||
|
return { props: { version } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Error500
|
export default Error500
|
||||||
|
@ -1,32 +1,20 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
import { AppProps } from 'next/app'
|
import { AppProps } from 'next/app'
|
||||||
import Router from 'next/router'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import NProgress from 'nprogress'
|
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import UniversalCookie from 'universal-cookie'
|
import UniversalCookie from 'universal-cookie'
|
||||||
|
|
||||||
import 'normalize.css/normalize.css'
|
import 'tailwindcss/tailwind.css'
|
||||||
import '@fontsource/montserrat/400.css'
|
import '@fontsource/montserrat/400.css'
|
||||||
import '@fontsource/montserrat/500.css'
|
import '@fontsource/montserrat/500.css'
|
||||||
import '@fontsource/montserrat/600.css'
|
import '@fontsource/montserrat/600.css'
|
||||||
import '@fontsource/montserrat/700.css'
|
import '@fontsource/montserrat/700.css'
|
||||||
|
|
||||||
import 'styles/grid.scss'
|
|
||||||
import 'styles/general.scss'
|
|
||||||
import 'styles/nprogress.scss'
|
|
||||||
|
|
||||||
import { Header } from 'components/Header'
|
|
||||||
import { Footer } from 'components/Footer'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
const universalCookie = new UniversalCookie()
|
const universalCookie = new UniversalCookie()
|
||||||
|
|
||||||
/** how long in seconds, until the cookie expires (10 years) */
|
/** how long in seconds, until the cookie expires (10 years) */
|
||||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||||
|
|
||||||
Router.events.on('routeChangeStart', () => NProgress.start())
|
|
||||||
Router.events.on('routeChangeComplete', () => NProgress.done())
|
|
||||||
Router.events.on('routeChangeError', () => NProgress.done())
|
|
||||||
|
|
||||||
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||||
const { lang } = useTranslation()
|
const { lang } = useTranslation()
|
||||||
|
|
||||||
@ -38,13 +26,9 @@ const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
|||||||
}, [lang])
|
}, [lang])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ThemeProvider attribute='class' defaultTheme='dark'>
|
||||||
<Header />
|
|
||||||
<main className='content container'>
|
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</main>
|
</ThemeProvider>
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
pages/_document.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Document, {
|
||||||
|
Html,
|
||||||
|
Head,
|
||||||
|
Main,
|
||||||
|
NextScript,
|
||||||
|
DocumentContext,
|
||||||
|
DocumentInitialProps
|
||||||
|
} from 'next/document'
|
||||||
|
|
||||||
|
class MyDocument extends Document {
|
||||||
|
static async getInitialProps(
|
||||||
|
ctx: DocumentContext
|
||||||
|
): Promise<DocumentInitialProps> {
|
||||||
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
return initialProps
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<body className='bg-white dark:bg-black text-black dark:text-white font-headline'>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyDocument
|
@ -1,69 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import nodemailer from 'nodemailer'
|
|
||||||
import validator from 'validator'
|
|
||||||
|
|
||||||
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
|
|
||||||
|
|
||||||
const emailTransporter = nodemailer.createTransport({
|
|
||||||
host: process.env.EMAIL_HOST,
|
|
||||||
port: EMAIL_PORT,
|
|
||||||
secure: EMAIL_PORT === 465,
|
|
||||||
auth: {
|
|
||||||
user: process.env.EMAIL_USER,
|
|
||||||
pass: process.env.EMAIL_PASSWORD
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default async (
|
|
||||||
request: NextApiRequest,
|
|
||||||
response: NextApiResponse
|
|
||||||
): Promise<any> => {
|
|
||||||
if (request.method !== 'POST') {
|
|
||||||
return response.redirect('/404')
|
|
||||||
}
|
|
||||||
|
|
||||||
let { name, email, subject, message } = request.body as {
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
subject: string
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
validator.isEmpty(name) ||
|
|
||||||
validator.isEmpty(email) ||
|
|
||||||
validator.isEmpty(subject) ||
|
|
||||||
validator.isEmpty(message)
|
|
||||||
) {
|
|
||||||
return response.status(400).json({ type: 'requiredFields' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validator.isEmail(email)) {
|
|
||||||
return response.status(400).json({ type: 'invalidEmail' })
|
|
||||||
}
|
|
||||||
|
|
||||||
email = validator.normalizeEmail(email) as string
|
|
||||||
message = validator.trim(message)
|
|
||||||
message = validator.escape(message)
|
|
||||||
subject = validator.trim(subject)
|
|
||||||
subject = validator.escape(subject)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await emailTransporter.sendMail({
|
|
||||||
from: '"Divlo" <contact@divlo.fr>',
|
|
||||||
to: email,
|
|
||||||
subject: `Contact - ${subject}`,
|
|
||||||
html: `
|
|
||||||
<b>Name:</b> ${name} <br/>
|
|
||||||
<b>Email:</b> ${email} <br/>
|
|
||||||
<b>Message:</b> ${message}
|
|
||||||
`
|
|
||||||
})
|
|
||||||
return response.status(201).json({ type: 'success' })
|
|
||||||
} catch {
|
|
||||||
return response.status(500).json({ type: 'serverError' })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { GetStaticProps } from 'next'
|
import { GetStaticProps } from 'next'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import readPackageJSON from 'read-pkg'
|
||||||
|
|
||||||
import { Contact } from 'components/Contact'
|
|
||||||
import { RevealFade } from 'components/design/RevealFade'
|
import { RevealFade } from 'components/design/RevealFade'
|
||||||
import { Section } from 'components/design/Section'
|
import { Section } from 'components/design/Section'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
@ -10,14 +10,20 @@ import { Portfolio } from 'components/Portfolio'
|
|||||||
import { Profile } from 'components/Profile'
|
import { Profile } from 'components/Profile'
|
||||||
import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
||||||
import { Skills } from 'components/Skills'
|
import { Skills } from 'components/Skills'
|
||||||
|
import { OpenSource } from 'components/OpenSource'
|
||||||
|
import { Header } from 'components/Header'
|
||||||
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head />
|
<Head />
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
<Section isMain id='about'>
|
<Section isMain id='about'>
|
||||||
<Profile />
|
<Profile />
|
||||||
<SocialMediaList />
|
<SocialMediaList />
|
||||||
@ -30,7 +36,11 @@ const Home: React.FC = () => {
|
|||||||
</RevealFade>
|
</RevealFade>
|
||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section id='skills' heading={t('home:skills.title')} withoutShadowContainer>
|
<Section
|
||||||
|
id='skills'
|
||||||
|
heading={t('home:skills.title')}
|
||||||
|
withoutShadowContainer
|
||||||
|
>
|
||||||
<Skills />
|
<Skills />
|
||||||
</Section>
|
</Section>
|
||||||
</RevealFade>
|
</RevealFade>
|
||||||
@ -46,16 +56,23 @@ const Home: React.FC = () => {
|
|||||||
</RevealFade>
|
</RevealFade>
|
||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section id='contact' heading={t('home:contact.title')}>
|
<Section
|
||||||
<Contact />
|
id='open-source'
|
||||||
|
heading='Open source'
|
||||||
|
withoutShadowContainer
|
||||||
|
>
|
||||||
|
<OpenSource />
|
||||||
</Section>
|
</Section>
|
||||||
</RevealFade>
|
</RevealFade>
|
||||||
|
</main>
|
||||||
|
<Footer version={version} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||||
return { props: {} }
|
const { version } = await readPackageJSON()
|
||||||
|
return { props: { version } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: {
|
||||||
[
|
tailwindcss: {},
|
||||||
'@fullhuman/postcss-purgecss',
|
autoprefixer: {}
|
||||||
{
|
|
||||||
content: [
|
|
||||||
'./pages/**/*.{js,jsx,ts,tsx}',
|
|
||||||
'./components/**/*.{js,jsx,ts,tsx}'
|
|
||||||
],
|
|
||||||
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
|
|
||||||
safelist: ['html', 'body']
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
BIN
public/images/portfolio/leon.png
Normal file
After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 66 KiB |
BIN
public/images/skills/C-Cpp.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/images/skills/Fastify-dark.png
Normal file
After Width: | Height: | Size: 11 KiB |