Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
b5f3552c07 | |||
5fbae8601f | |||
48d35776a9 | |||
8b9e58c47c | |||
33078ece66 | |||
a2da9618af | |||
a467ea7aff | |||
0e0036b737 | |||
729e540d04 | |||
e5f4615f7f | |||
0bf89f4df5 | |||
bcb184e49c | |||
1505b81233 | |||
a30355582e | |||
a4effb52f9 | |||
52bba0ef9c | |||
8ecfeca50d | |||
fd0740d12a | |||
bd2dc9c9af | |||
a53888ab42 | |||
624e79eecd | |||
049ec367fc | |||
56f22d0c9b | |||
9adb67662e | |||
010087088f | |||
35d4396e80 | |||
934118737a | |||
b692dac926 | |||
dd582652ab | |||
337352de0c | |||
c513268cbb | |||
4fdcb2b667 | |||
377b8e91a6 | |||
fce29c9d4a | |||
c198f47aa9 | |||
8e051332cd | |||
9f3436e1df | |||
2f2373e62f | |||
c6b455dd10 | |||
4e089b41f2 | |||
6c102b1b35 | |||
52b10944b7 | |||
db36eb3e7a | |||
c739ad951d | |||
2802ff029f | |||
1a7457b44b | |||
ff210f879d | |||
607454b360 | |||
d1522fbf44 | |||
b82eae7499 | |||
73527ce8fe | |||
0cd885ee70 | |||
2cb2df975f | |||
37f5843adb | |||
d794d38f14 | |||
fc5ba28b8a | |||
b5945150b8 | |||
aa12d626d2 | |||
6ac4782b7d | |||
0aa998d593 | |||
56f975e53c | |||
5a16d24ea1 | |||
52267005ec | |||
99b9b12ac9 | |||
2cae77481f | |||
e98b47a459 | |||
4cc87758c1 | |||
1bb0f31223 | |||
af2dd0bd60 | |||
63d7485c8d | |||
74fde0ea40 | |||
0d2b318818 | |||
266b3f8589 | |||
f7d304ca80 | |||
63017953d7 | |||
20600eb976 | |||
7f920b77aa | |||
4f5dfc63ea | |||
712805df93 | |||
cd68f597c9 | |||
7ec3fe8ced | |||
90d22b2c7f | |||
4b06fd0522 | |||
b4427f36c2 | |||
b758c64e02 | |||
04469b83ea | |||
36d54666a0 | |||
a34cefec6e | |||
5c343395df | |||
028815a7b6 |
@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
@ -1,7 +1,2 @@
|
||||
# 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"
|
||||
ARG VARIANT="16"
|
||||
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}"
|
||||
|
@ -14,11 +14,10 @@
|
||||
"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"],
|
||||
"postAttachCommand": ["npm", "install"],
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
.vercel
|
||||
|
@ -1,11 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"standard-with-typescript",
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier", "unicorn"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
@ -15,6 +10,7 @@
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
"prettier/prettier": "error",
|
||||
"unicorn/prefer-node-protocol": "off"
|
||||
}
|
||||
}
|
||||
|
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
16
.github/dependabot.yml
vendored
@ -1,16 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
144
.github/workflows/Divlo.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
language: ['javascript']
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.3.4'
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@v1'
|
||||
@ -26,33 +26,13 @@ jobs:
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
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:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
- run: 'npm run lint:docker'
|
||||
- run: 'npm run lint:editorconfig'
|
||||
- run: 'npm run lint:markdown'
|
||||
- run: 'npm run lint:typescript'
|
||||
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.3.4'
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.3.0'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
@ -63,18 +43,13 @@ jobs:
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Lighthouse'
|
||||
run: 'npm run lighthouse'
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
|
||||
test:
|
||||
lint:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.3.4'
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.3.0'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
@ -82,21 +57,115 @@ jobs:
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Test'
|
||||
run: 'npm run test'
|
||||
- name: 'lint:commit'
|
||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
|
||||
- name: 'lint:editorconfig'
|
||||
run: 'npm run lint:editorconfig'
|
||||
|
||||
- name: 'lint:markdown'
|
||||
run: 'npm run lint:markdown'
|
||||
|
||||
- name: 'lint:typescript'
|
||||
run: 'npm run lint:typescript'
|
||||
|
||||
- name: 'lint:prettier'
|
||||
run: 'npm run lint:prettier'
|
||||
|
||||
- name: 'resume:validate'
|
||||
run: 'npm run resume:validate'
|
||||
|
||||
- name: 'lint:dotenv'
|
||||
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
- name: 'lint:docker'
|
||||
uses: 'hadolint/hadolint-action@v1.6.0'
|
||||
with:
|
||||
dockerfile: './Dockerfile'
|
||||
|
||||
test-unit:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Unit Test'
|
||||
run: 'npm run test:unit'
|
||||
|
||||
test-lighthouse:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'html-w3c-validator'
|
||||
run: 'npm run test:html-w3c-validator'
|
||||
|
||||
- name: 'Lighthouse'
|
||||
run: 'npm run test:lighthouse'
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
|
||||
test-e2e:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'End To End (e2e) Test'
|
||||
run: 'npm run test:e2e'
|
||||
|
||||
release:
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
needs: [analyze, lint, build, test]
|
||||
needs: [analyze, build, lint, test-unit, test-lighthouse, test-e2e]
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.3.4'
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Import GPG key'
|
||||
uses: 'crazy-max/ghaction-import-gpg@v4'
|
||||
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'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: 'npm'
|
||||
@ -107,10 +176,7 @@ jobs:
|
||||
- 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 }}
|
||||
|
||||
|
11
.gitignore
vendored
@ -11,13 +11,16 @@ out
|
||||
# production
|
||||
build
|
||||
dist
|
||||
public/*.html
|
||||
# PWA
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# PWA
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/downloads
|
||||
|
||||
# envs
|
||||
.env
|
||||
|
@ -1,8 +1,8 @@
|
||||
image: 'gitpod/workspace-full'
|
||||
|
||||
tasks:
|
||||
- before: 'cp .env.example .env && npm install --global npm@7'
|
||||
init: 'npm clean-install'
|
||||
- before: 'cp .env.example .env'
|
||||
init: 'npm install'
|
||||
command: 'npm run dev'
|
||||
|
||||
ports:
|
||||
|
7
.html-w3c-validatorrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"urls": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
]
|
||||
}
|
@ -4,15 +4,22 @@
|
||||
"startServerCommand": "npm run start",
|
||||
"startServerReadyPattern": "ready on",
|
||||
"startServerReadyTimeout": 20000,
|
||||
"url": ["http://localhost:3000/"],
|
||||
"numberOfRuns": 3
|
||||
"url": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
],
|
||||
"numberOfRuns": 1
|
||||
},
|
||||
"assert": {
|
||||
"preset": "lighthouse:recommended",
|
||||
"assertions": {
|
||||
"csp-xss": "warning",
|
||||
"non-composited-animations": "warning",
|
||||
"uses-responsive-images": "warning"
|
||||
"unused-javascript": "warning",
|
||||
"image-size-responsive": "warning",
|
||||
"unsized-images": "warning",
|
||||
"color-contrast": "warning"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"*": ["editorconfig-checker"],
|
||||
"*.{js,ts,jsx,tsx}": [
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests"
|
||||
],
|
||||
"*.{css,yml,json}": ["prettier --write"],
|
||||
"*.{md}": ["prettier --write", "markdownlint --dot --fix"],
|
||||
"./Dockerfile": ["dockerfilelint"]
|
||||
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
||||
"resume.json": ["resume validate"]
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
package.json
|
||||
package-lock.json
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
.vercel
|
||||
*.hbs
|
||||
|
@ -13,7 +13,12 @@
|
||||
"preset": "conventionalcommits"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
@ -21,6 +26,12 @@
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
"@semantic-release/github",
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeStrategy": "merge"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
1
.vscode/extensions.json
vendored
@ -7,7 +7,6 @@
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"coenraads.bracket-pair-colorizer",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
|
3
.vscode/settings.json
vendored
@ -1,7 +1,8 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"prettier.configPath": ".prettierrc.json",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
|
@ -13,7 +13,7 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
|
||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
||||
|
||||
- Ensure your code respect [Typescript Standard Style](https://www.npmjs.com/package/ts-standard).
|
||||
- Ensure your code respect linting.
|
||||
|
||||
- Make sure your **code passes the tests**.
|
||||
|
||||
@ -51,8 +51,8 @@ Scopes define what part of the code changed.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 14.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 7.0.0
|
||||
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||
|
||||
### Installation
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
FROM node:16.5.0 AS dependencies
|
||||
FROM node:16.14.0 AS dependencies
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./package*.json ./
|
||||
RUN npm clean-install
|
||||
RUN npm install
|
||||
|
||||
FROM node:16.5.0 AS builder
|
||||
FROM node:16.14.0 AS builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./ ./
|
||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
|
||||
FROM node:16.5.0 AS runner
|
||||
FROM node:16.14.0 AS runner
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||
|
@ -23,7 +23,7 @@
|
||||
```json
|
||||
{
|
||||
"name": "Divlo",
|
||||
"pronouns": "He' | 'Him",
|
||||
"pronouns": "He/Him",
|
||||
"birthDate": "31/03/2003",
|
||||
"nationality": "Alsace, France",
|
||||
"interests": [
|
||||
@ -32,17 +32,17 @@
|
||||
"Open-Source enthusiast"
|
||||
],
|
||||
"skills": {
|
||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python"],
|
||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
||||
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
||||
"tools": ["Ubuntu", "Hyper Terminal", "VSCode", "Git", "Docker"]
|
||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<hr />
|
||||
|
||||
## 📈 Stats
|
||||
## 📈 Statistics
|
||||
|
||||
<p align=center>
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import Error404 from 'pages/404'
|
||||
|
||||
describe('GET /404', () => {
|
||||
it('should render', async () => {
|
||||
const { getByText } = render(<Error404 />)
|
||||
expect(getByText('404')).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,10 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import Error500 from 'pages/500'
|
||||
|
||||
describe('GET /500', () => {
|
||||
it('should render', async () => {
|
||||
const { getByText } = render(<Error500 />)
|
||||
expect(getByText('500')).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -12,15 +12,20 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className='my-6 font-semibold text-4xl'>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
{t('errors:error')}{' '}
|
||||
<span className='text-yellow dark:text-yellow-dark'>{statusCode}</span>
|
||||
<span
|
||||
className='text-yellow dark:text-yellow-dark'
|
||||
data-cy='status-code'
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
</h1>
|
||||
<p className='text-center text-lg'>
|
||||
{message}{' '}
|
||||
<Link href='/'>
|
||||
<a className='text-yellow dark:text-yellow-dark hover:underline'>
|
||||
{t('errors:returnToHomePage')}
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
{t('errors:return-to-home-page')}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
@ -11,26 +11,23 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
if (version !== '0.0.0-development') {
|
||||
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
}
|
||||
return 'https://github.com/Divlo/Divlo/tree/develop'
|
||||
}, [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'>
|
||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||
<p>
|
||||
<Link href='/'>
|
||||
<a className='hover:underline text-yellow dark:text-yellow-dark'>
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
Divlo
|
||||
</a>
|
||||
</Link>{' '}
|
||||
| {t('common:allRightsReserved')}
|
||||
| {t('common:all-rights-reserved')}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
Version{' '}
|
||||
<a
|
||||
className='hover:underline text-yellow dark:text-yellow-dark'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href={versionLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
|
@ -3,7 +3,7 @@ import NextHead from 'next/head'
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description?: string
|
||||
description: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
||||
const {
|
||||
title = 'Divlo',
|
||||
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,
|
||||
url = 'https://divlo.fr/'
|
||||
} = props
|
||||
|
||||
@ -39,7 +39,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
||||
<meta name='twitter:card' content='summary' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<meta name='twitter:image:src' content={image} />
|
||||
<meta name='twitter:image' content={image} />
|
||||
|
||||
{/* Google Verification */}
|
||||
<meta
|
||||
|
@ -10,12 +10,15 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/languages/${language}.svg`}
|
||||
alt={language}
|
||||
/>
|
||||
<p className='mx-2 text-base'>{language.toUpperCase()}</p>
|
||||
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||
{language.toUpperCase()}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import i18n from 'i18n.json'
|
||||
|
||||
import { Arrow } from './Arrow'
|
||||
import { LanguageFlag } from './LanguageFlag'
|
||||
import i18n from 'i18n.json'
|
||||
|
||||
export const Language: React.FC = () => {
|
||||
const { lang: currentLanguage } = useTranslation()
|
||||
@ -32,13 +34,23 @@ export const Language: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col justify-center items-center cursor-pointer'>
|
||||
<div className='flex items-center mr-5' onClick={handleHiddenMenu}>
|
||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||
<div
|
||||
data-cy='language-click'
|
||||
className='mr-5 flex items-center'
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LanguageFlag language={currentLanguage} />
|
||||
<Arrow />
|
||||
</div>
|
||||
{!hiddenMenu && (
|
||||
<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'>
|
||||
|
||||
<ul
|
||||
data-cy='languages-list'
|
||||
className={classNames(
|
||||
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||
{ hidden: hiddenMenu }
|
||||
)}
|
||||
>
|
||||
{i18n.locales.map((language, index) => {
|
||||
if (language === currentLanguage) {
|
||||
return null
|
||||
@ -46,7 +58,7 @@ export const Language: React.FC = () => {
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className='flex items-center justify-center w-full h-12 hover:bg-[#4f545c] hover:bg-opacity-20 pl-2'
|
||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||
onClick={async () => await handleLanguage(language)}
|
||||
>
|
||||
<LanguageFlag language={language} />
|
||||
@ -54,7 +66,6 @@ export const Language: React.FC = () => {
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -13,26 +13,42 @@ export const SwitchTheme: React.FC = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleClick = (): void => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='toggle-button'
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className='flex items-center'
|
||||
data-cy='switch-theme-click'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className='toggle-theme-button'>
|
||||
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
||||
<div className='toggle-track'>
|
||||
<div className='toggle-track-check'>
|
||||
<span className='toggle_Dark'>🌜</span>
|
||||
<div
|
||||
data-cy='switch-theme-dark'
|
||||
className='toggle-track-check absolute'
|
||||
>
|
||||
<span className='toggle_Dark relative flex items-center justify-center'>
|
||||
🌜
|
||||
</span>
|
||||
</div>
|
||||
<div className='toggle-track-x'>
|
||||
<span className='toggle_Light'>🌞</span>
|
||||
<div
|
||||
data-cy='switch-theme-light'
|
||||
className='toggle-track-x absolute'
|
||||
>
|
||||
<span className='toggle_Light relative flex items-center justify-center'>
|
||||
🌞
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='toggle-thumb' />
|
||||
<div className='toggle-thumb absolute' />
|
||||
<input
|
||||
data-cy='switch-theme-input'
|
||||
type='checkbox'
|
||||
aria-label='Dark mode toggle'
|
||||
className='toggle-screenreader-only'
|
||||
className='toggle-screenreader-only absolute overflow-hidden'
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
@ -40,16 +56,8 @@ export const SwitchTheme: React.FC = () => {
|
||||
|
||||
<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;
|
||||
@ -64,7 +72,6 @@ export const SwitchTheme: React.FC = () => {
|
||||
color: #fff;
|
||||
}
|
||||
.toggle-track-check {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
@ -77,7 +84,6 @@ export const SwitchTheme: React.FC = () => {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.toggle-track-x {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
@ -90,15 +96,10 @@ export const SwitchTheme: React.FC = () => {
|
||||
}
|
||||
.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;
|
||||
@ -115,9 +116,7 @@ export const SwitchTheme: React.FC = () => {
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
`}
|
||||
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
|
||||
import { Header } from '..'
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('should render', async () => {
|
||||
it('should render', () => {
|
||||
const { getByText } = render(<Header />)
|
||||
expect(getByText('Divlo')).toBeInTheDocument()
|
||||
})
|
||||
|
@ -4,26 +4,43 @@ import Image from 'next/image'
|
||||
import { Language } from './Language'
|
||||
import { SwitchTheme } from './SwitchTheme'
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
export interface HeaderProps {
|
||||
showLanguage?: boolean
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = (props) => {
|
||||
const { showLanguage = false } = props
|
||||
|
||||
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='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||
<Link href='/'>
|
||||
<a>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Image
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt='Divlo'
|
||||
/>
|
||||
<strong className='ml-1 font-headline font-semibold hidden xs:block text-yellow dark:text-yellow-dark'>
|
||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||
Divlo
|
||||
</strong>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className='flex justify-between'>
|
||||
<Language />
|
||||
<div className='flex flex-col items-center justify-center px-6'>
|
||||
<Link href='/blog'>
|
||||
<a
|
||||
data-cy='header-blog-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{showLanguage && <Language />}
|
||||
<SwitchTheme />
|
||||
</div>
|
||||
</header>
|
||||
|
@ -10,8 +10,8 @@ export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-center my-6 text-gray dark:text-gray-dark'>
|
||||
<strong className='text-yellow font-medium text-lg dark:text-yellow-dark'>
|
||||
<p className='my-6 text-center text-gray dark:text-gray-dark'>
|
||||
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
|
||||
{title}
|
||||
</strong>
|
||||
<br />
|
||||
|
@ -10,9 +10,9 @@ export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
<li className='interest-item my-2 mx-2 w-8 h-8' title={title}>
|
||||
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
|
||||
<FontAwesomeIcon
|
||||
className='text-yellow cursor-pointer h-full w-full block dark:text-yellow-dark'
|
||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||
icon={fontAwesomeIcon}
|
||||
/>
|
||||
</li>
|
||||
|
@ -5,8 +5,8 @@ import { InterestItem } from './InterestItem'
|
||||
|
||||
export const InterestsList: React.FC = () => {
|
||||
return (
|
||||
<div className='flex justify-center my-4'>
|
||||
<ul className='flex justify-around p-0 m-0 list-none w-96'>
|
||||
<div className='my-4 flex justify-center'>
|
||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||
<InterestItem
|
||||
title='Developer Full Stack Junior'
|
||||
fontAwesomeIcon={faCode}
|
||||
|
@ -15,13 +15,11 @@ export const Interests: React.FC = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='max-w-full'>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
return <InterestParagraph key={index} {...paragraph} />
|
||||
})}
|
||||
<InterestsList />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ 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'>
|
||||
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||
<div className='flex'>
|
||||
<GitHubIcon className='h-6 mr-2' />
|
||||
<GitHubIcon className='mr-2 h-6' />
|
||||
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
||||
</div>
|
||||
<p className='my-4'>{description}</p>
|
||||
|
@ -6,13 +6,12 @@ export const OpenSource: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='max-w-full mt-0 flex flex-col items-center'>
|
||||
<div className='mt-0 flex max-w-full 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'>
|
||||
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
||||
<Repository
|
||||
name='nodejs/node'
|
||||
description='Node.js JavaScript runtime ✨️🐢🚀✨️'
|
||||
description='Node.js JavaScript runtime 🐢🚀'
|
||||
href='https://github.com/nodejs/node/commits?author=Divlo'
|
||||
/>
|
||||
<Repository
|
||||
@ -26,22 +25,11 @@ export const OpenSource: React.FC = () => {
|
||||
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'
|
||||
name='vercel/next.js'
|
||||
description='The React Framework for Production'
|
||||
href='https://github.com/vercel/next.js/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,6 +1,7 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
description: string
|
||||
@ -12,7 +13,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
<ShadowContainer className='cursor-pointer relative items-center sm:ml-10'>
|
||||
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'>
|
||||
<a
|
||||
className='group inline-flex justify-center'
|
||||
target='_blank'
|
||||
@ -22,6 +23,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
>
|
||||
<div className='flex justify-center'>
|
||||
<Image
|
||||
quality={100}
|
||||
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||
width={300}
|
||||
height={300}
|
||||
@ -29,8 +31,8 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
alt={title}
|
||||
/>
|
||||
</div>
|
||||
<div className='opacity-0 transition-opacity duration-500 h-auto absolute text-center overflow-hidden bottom-0 group-hover:opacity-100'>
|
||||
<h3 className='text-yellow text-xl font-semibold my-6 dark:text-yellow-dark'>
|
||||
<div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'>
|
||||
<h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='my-6'>{description}</p>
|
||||
|
@ -14,7 +14,7 @@ export const Portfolio: React.FC = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap justify-center px-3 w-full'>
|
||||
<div className='flex w-full flex-wrap justify-center px-3'>
|
||||
{items.map((item, index) => {
|
||||
return <PortfolioItem key={index} {...item} />
|
||||
})}
|
||||
|
@ -1,12 +1,23 @@
|
||||
import Translation from 'next-translate/Trans'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t, lang } = useTranslation()
|
||||
|
||||
return (
|
||||
<p className='mt-8 mb-8 font-normal text-base text-gray dark:text-gray-dark'>
|
||||
<Translation
|
||||
i18nKey='home:about.descriptionBottom'
|
||||
components={[<br key='break' />]}
|
||||
/>
|
||||
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||
{t('home:about.description-bottom')}
|
||||
{lang === 'fr' && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
href='/curriculum-vitae.html'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Curriculum vitæ
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -1,44 +1,17 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export const ProfileInfo: React.FC = () => {
|
||||
export const ProfileInformation: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='pb-2 mb-6 border-b-2 font-headline border-gray-600 dark:border-gray-400'>
|
||||
<h1 className='text-4xl mb-2'>
|
||||
{t('home:about.IAm')}{' '}
|
||||
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
||||
<h1 className='mb-2 text-4xl'>
|
||||
{t('home:about.i-am')}{' '}
|
||||
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
|
||||
Divlo
|
||||
</strong>
|
||||
</h1>
|
||||
<h2 className='text-base mb-3'>{t('home:about.description')}</h2>
|
||||
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-info {
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
.profile-title {
|
||||
font-size: 36px;
|
||||
line-height: 1.1;
|
||||
font-weight: 300;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.profile-title > strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.profile-description {
|
||||
font-size: 17.4px;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -8,15 +8,14 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='profile-list__item'>
|
||||
<strong className='profile-list__item-title text-black dark:text-white'>
|
||||
<li className='mb-3 before:table after:clear-both after:table'>
|
||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||
{title}
|
||||
</strong>
|
||||
<span className='profile-list__item-info text-gray dark:text-gray-dark'>
|
||||
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||
{link != null ? (
|
||||
<a
|
||||
className='text-gray dark:text-gray-dark hover:underline'
|
||||
className='text-gray hover:underline dark:text-gray-dark'
|
||||
href={link}
|
||||
>
|
||||
{value}
|
||||
@ -26,54 +25,5 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-list__item {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
.profile-list__item::after,
|
||||
.profile-list__item::before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.profile-list__item::after {
|
||||
clear: both;
|
||||
}
|
||||
.profile-list__item-title {
|
||||
display: block;
|
||||
width: 120px;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
display: block;
|
||||
margin-left: 125px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.profile-list__item-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
margin-left: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.profile-list__item-info,
|
||||
.profile-list__item-title {
|
||||
width: 100%;
|
||||
float: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
return (
|
||||
<ul className='m-0 p-0 list-none'>
|
||||
<ProfileItem title={t('home:about.birthDate')} value='31/03/2003' />
|
||||
<ul className='m-0 list-none p-0'>
|
||||
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
||||
<ProfileItem title={t('home:about.birth-date')} value='31/03/2003' />
|
||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
|
@ -1,14 +1,11 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import DivloLogo from 'public/images/divlo_logo.png'
|
||||
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
return (
|
||||
<div className='px-2 py-6'>
|
||||
<Image
|
||||
width={370}
|
||||
height={370}
|
||||
src='/images/divlo_logo.png'
|
||||
alt='Divlo'
|
||||
/>
|
||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
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',
|
||||
'h-8 w-8 fill-current text-black dark:text-white',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
|
@ -7,7 +7,7 @@ export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
||||
const { link, ariaLabel, children } = props
|
||||
|
||||
return (
|
||||
<li className='inline-block mx-4 my-1'>
|
||||
<li className='mx-4 my-1 inline-block'>
|
||||
<a
|
||||
href={link}
|
||||
aria-label={ariaLabel}
|
||||
|
@ -9,7 +9,7 @@ import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
||||
|
||||
export const SocialMediaList: React.FC = () => {
|
||||
return (
|
||||
<ul className='social-media-list m-0 mt-2 py-4 list-none text-center'>
|
||||
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
||||
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
|
||||
<GitHubIcon />
|
||||
</SocialMediaItem>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
||||
import { ProfileInfo } from './ProfileInfo'
|
||||
import { ProfileInformation } from './ProfileInfo'
|
||||
import { ProfileList } from './ProfileList'
|
||||
import { ProfileLogo } from './ProfileLogo'
|
||||
|
||||
export const Profile: React.FC = () => {
|
||||
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='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
|
||||
<ProfileLogo />
|
||||
<div>
|
||||
<ProfileInfo />
|
||||
<ProfileInformation />
|
||||
<ProfileList />
|
||||
<ProfileDescriptionBottom />
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='text-center'>
|
||||
<Image width={60} height={60} alt={skill} src={image} />
|
||||
<Image quality={100} width={60} height={60} alt={skill} src={image} />
|
||||
<p className='mt-1'>{skill}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -10,15 +10,15 @@ export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
|
||||
|
||||
return (
|
||||
<ShadowContainer>
|
||||
<div className='w-full px-4 mx-auto'>
|
||||
<div className='mx-auto w-full px-4'>
|
||||
<div className='flex flex-wrap px-4 py-6'>
|
||||
<div className='flex-1'>
|
||||
<div className='mb-8 border-b border-gray-600 dark:border-opacity-10 dark:border-white'>
|
||||
<h3 className='text-yellow font-semibold text-xl my-3 dark:text-yellow-dark'>
|
||||
<div className='mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10'>
|
||||
<h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className='flex justify-around flex-wrap'>{children}</div>
|
||||
<div className='flex flex-wrap justify-around'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ export const Skills: React.FC = () => {
|
||||
<SkillComponent skill='JavaScript' />
|
||||
<SkillComponent skill='TypeScript' />
|
||||
<SkillComponent skill='Python' />
|
||||
<SkillComponent skill='C/C++' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Front-end'>
|
||||
@ -29,9 +30,9 @@ export const Skills: React.FC = () => {
|
||||
<SkillComponent skill='MySQL' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title={t('home:skills.softwareTools')}>
|
||||
<SkillsSection title={t('home:skills.software-tools')}>
|
||||
<SkillComponent skill='GNU/Linux' />
|
||||
<SkillComponent skill='Ubuntu' />
|
||||
<SkillComponent skill='Hyper' />
|
||||
<SkillComponent skill='Visual Studio Code' />
|
||||
<SkillComponent skill='Git' />
|
||||
<SkillComponent skill='Docker' />
|
||||
|
@ -98,6 +98,10 @@ export const skills: Skills = {
|
||||
link: 'https://ubuntu.com/',
|
||||
image: '/images/skills/Ubuntu.png'
|
||||
},
|
||||
'GNU/Linux': {
|
||||
link: 'https://www.gnu.org/',
|
||||
image: '/images/skills/GNU-Linux.png'
|
||||
},
|
||||
Docker: {
|
||||
link: 'https://www.docker.com/',
|
||||
image: '/images/skills/Docker.png'
|
||||
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
|
||||
import { ErrorPage } from '../ErrorPage'
|
||||
|
||||
describe('<ErrorPage />', () => {
|
||||
it('should render the message and statusCode', async () => {
|
||||
it('should render the message and statusCode', () => {
|
||||
const messageContent = 'message content'
|
||||
const statusCode = 404
|
||||
const { getByText } = render(
|
||||
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
|
||||
import { Footer } from '../Footer'
|
||||
|
||||
describe('<Footer />', () => {
|
||||
it('should render the version link pointing to the GitHub release', async () => {
|
||||
it('should render with appropriate link tag version', () => {
|
||||
const version = '1.0.0'
|
||||
const { getByText } = render(<Footer version={version} />)
|
||||
const versionLink = getByText(version) as HTMLAnchorElement
|
||||
@ -13,15 +13,4 @@ describe('<Footer />', () => {
|
||||
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the version link pointing to the `develop` branch', async () => {
|
||||
const version = '0.0.0-development'
|
||||
const { getByText } = render(<Footer version={version} />)
|
||||
const versionLink = getByText(version) as HTMLAnchorElement
|
||||
expect(getByText('Divlo')).toBeInTheDocument()
|
||||
expect(versionLink).toBeInTheDocument()
|
||||
expect(versionLink.href).toEqual(
|
||||
'https://github.com/Divlo/Divlo/tree/develop'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -10,7 +10,8 @@ export const RevealFade: React.FC = (props) => {
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('reveal-visible')
|
||||
entry.target.className =
|
||||
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
@ -25,26 +26,8 @@ export const RevealFade: React.FC = (props) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={htmlElement} className='reveal'>
|
||||
<div ref={htmlElement} className='invisible -translate-y-7 opacity-0'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
.reveal-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transition: all 500ms ease-out 100ms;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<h2 {...rest} className='text-4xl font-semibold text-center mt-1 mb-3'>
|
||||
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
|
@ -20,11 +20,11 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
|
||||
if (isMain) {
|
||||
return (
|
||||
<div className='px-3 w-full'>
|
||||
<div className='w-full px-3'>
|
||||
<ShadowContainer style={{ marginTop: 50 }}>
|
||||
<section {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='px-3 w-full'>{children}</div>
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
</section>
|
||||
</ShadowContainer>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
return (
|
||||
<section {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='px-3 w-full'>{children}</div>
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -52,9 +52,9 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className='px-3 w-full'>
|
||||
<div className='w-full px-3'>
|
||||
<ShadowContainer>
|
||||
<div className='px-16 py-4 leading-8 w-full'>{children}</div>
|
||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||
</ShadowContainer>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -6,27 +6,14 @@ export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'shadow-container h-full max-w-full break-words',
|
||||
'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.shadow-container {
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
8
cypress.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"pluginsFile": false,
|
||||
"supportFile": false,
|
||||
"fixturesFolder": false,
|
||||
"video": false,
|
||||
"screenshotOnRunFailure": false
|
||||
}
|
58
cypress/integration/common/Header.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
describe('Common > Header', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
|
||||
it('should redirect to /blog on click of the blog link', () => {
|
||||
cy.get('[data-cy=header-blog-link]')
|
||||
.click()
|
||||
.location('pathname')
|
||||
.should('eq', '/blog')
|
||||
})
|
||||
|
||||
it('should always be visible (sticky header)', () => {
|
||||
cy.scrollTo('bottom').get('header').should('be.visible')
|
||||
})
|
||||
|
||||
describe('Switch theme color (dark/light)', () => {
|
||||
it('should switch theme from `dark` (default) to `light`', () => {
|
||||
cy.get('[data-cy=switch-theme-dark]').should('be.visible')
|
||||
cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
|
||||
cy.get('body').should(
|
||||
'not.have.css',
|
||||
'background-color',
|
||||
'rgb(255, 255, 255)'
|
||||
)
|
||||
|
||||
cy.get('[data-cy=switch-theme-click]').click()
|
||||
|
||||
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
|
||||
cy.get('[data-cy=switch-theme-light]').should('be.visible')
|
||||
cy.get('body').should(
|
||||
'have.css',
|
||||
'background-color',
|
||||
'rgb(255, 255, 255)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Switch Language', () => {
|
||||
it('should switch language from EN (default) to FR', () => {
|
||||
cy.get('h1').contains('I am Divlo')
|
||||
cy.get('[data-cy=language-flag-text]').contains('EN')
|
||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||
cy.get('[data-cy=language-click]').click()
|
||||
cy.get('[data-cy=languages-list]').should('be.visible')
|
||||
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
|
||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||
cy.get('[data-cy=language-flag-text]').contains('FR')
|
||||
cy.get('h1').contains('Je suis Divlo')
|
||||
})
|
||||
|
||||
it('should close the language list menu when clicking outside', () => {
|
||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||
cy.get('[data-cy=language-click]').click()
|
||||
cy.get('[data-cy=languages-list]').should('be.visible')
|
||||
cy.get('h1').click()
|
||||
cy.get('[data-cy=languages-list]').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
7
cypress/integration/pages/404.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
describe('Page /404', () => {
|
||||
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
|
||||
|
||||
it('should display the statusCode of 404', () => {
|
||||
cy.get('[data-cy=status-code]').contains('404')
|
||||
})
|
||||
})
|
7
cypress/integration/pages/500.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
describe('Page /500', () => {
|
||||
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
|
||||
|
||||
it('should display the statusCode of 500', () => {
|
||||
cy.get('[data-cy=status-code]').contains('500')
|
||||
})
|
||||
})
|
13
cypress/integration/pages/blog/[slug].spec.ts
Normal file
@ -0,0 +1,13 @@
|
||||
describe('Page /blog/[slug]', () => {
|
||||
it('should displays the first blog post (`hello-world`)', () => {
|
||||
cy.visit('/blog/hello-world')
|
||||
cy.get('[data-cy=language-flag-text]').should('not.exist')
|
||||
cy.get('h1').should('have.text', '👋 Hello, world!')
|
||||
cy.get('.prose a').should('have.attr', 'target', '_blank')
|
||||
})
|
||||
|
||||
it("should redirect to /404 if the blog post doesn't exist", () => {
|
||||
cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false })
|
||||
cy.get('[data-cy=status-code]').contains('404')
|
||||
})
|
||||
})
|
22
cypress/integration/pages/blog/index.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
describe('Page /blog', () => {
|
||||
it('should displays the blog posts sorted from newest to oldest', () => {
|
||||
cy.visit('/blog')
|
||||
cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]')
|
||||
.last()
|
||||
.should('have.text', '👋 Hello, world!')
|
||||
cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]')
|
||||
.last()
|
||||
.should(
|
||||
'have.text',
|
||||
'First post of the blog, introduction and explanation of how this blog is made.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect the user to the right blog post', () => {
|
||||
cy.visit('/blog')
|
||||
cy.get('[data-cy=hello-world]')
|
||||
.click()
|
||||
.location('pathname')
|
||||
.should('eq', '/blog/hello-world')
|
||||
})
|
||||
})
|
19
cypress/integration/pages/index.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
describe('Page /', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
|
||||
it('should reveals the sections while scrolling except the about section', () => {
|
||||
const sectionsReveals = [
|
||||
'#interests',
|
||||
'#skills',
|
||||
'#portfolio',
|
||||
'#open-source'
|
||||
]
|
||||
cy.get('#about').should('be.visible')
|
||||
for (const section of sectionsReveals) {
|
||||
cy.get(section)
|
||||
.should('not.be.visible')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
}
|
||||
})
|
||||
})
|
9
cypress/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["cypress"],
|
||||
"isolatedModules": false
|
||||
},
|
||||
"include": ["../node_modules/cypress", "./**/*.ts"]
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
module.exports = {
|
||||
roots: ['<rootDir>'],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
|
||||
},
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest()
|
||||
const customJestConfig = {
|
||||
moduleDirectories: ['node_modules', './'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/cypress'],
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: [
|
||||
'@testing-library/jest-dom/extend-expect',
|
||||
'@testing-library/react'
|
||||
],
|
||||
collectCoverage: true,
|
||||
coverageDirectory: './coverage',
|
||||
coverageReporters: ['text', 'cobertura']
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = createJestConfig(customJestConfig)
|
||||
|
4
jsonresume-theme-custom/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
theme/index.html
|
||||
dist
|
||||
.parcel-cache
|
32
jsonresume-theme-custom/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const ejs = require('ejs')
|
||||
const date = require('date-and-time')
|
||||
const { Parcel } = require('@parcel/core')
|
||||
|
||||
const render = async (resume) => {
|
||||
const themeIndexPath = path.join(__dirname, 'theme', 'index.ejs')
|
||||
const themeBuildPath = path.join(__dirname, 'theme', 'index.html')
|
||||
const indexHTMLPath = path.join(__dirname, 'dist', 'index.html')
|
||||
const html = await ejs.renderFile(themeIndexPath, {
|
||||
date,
|
||||
locals: {
|
||||
...resume
|
||||
}
|
||||
})
|
||||
|
||||
await fs.promises.writeFile(themeBuildPath, html, { encoding: 'utf-8' })
|
||||
const bundler = new Parcel({
|
||||
entries: themeBuildPath,
|
||||
source: themeBuildPath,
|
||||
mode: 'production',
|
||||
defaultConfig: '@parcel/config-default'
|
||||
})
|
||||
await bundler.run()
|
||||
return await fs.promises.readFile(indexHTMLPath, { encoding: 'utf-8' })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
render
|
||||
}
|
4953
jsonresume-theme-custom/package-lock.json
generated
Normal file
18
jsonresume-theme-custom/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "jsonresume-theme-custom",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"date-and-time": "2.1.2",
|
||||
"ejs": "3.1.6",
|
||||
"modern-normalize": "1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/config-default": "2.3.2",
|
||||
"@parcel/core": "2.3.2",
|
||||
"@parcel/optimizer-data-url": "^2.3.2",
|
||||
"@parcel/transformer-inline-string": "^2.3.2",
|
||||
"parcel": "2.3.2"
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
<!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M243.4 2.587C251.4-.8625 260.6-.8625 268.6 2.587L492.6 98.59C506.6 104.6 514.4 119.6 511.3 134.4C508.3 149.3 495.2 159.1 479.1 160V168C479.1 181.3 469.3 192 455.1 192H55.1C42.74 192 31.1 181.3 31.1 168V160C16.81 159.1 3.708 149.3 .6528 134.4C-2.402 119.6 5.429 104.6 19.39 98.59L243.4 2.587zM256 128C273.7 128 288 113.7 288 96C288 78.33 273.7 64 256 64C238.3 64 224 78.33 224 96C224 113.7 238.3 128 256 128zM127.1 416H167.1V224H231.1V416H280V224H344V416H384V224H448V420.3C448.6 420.6 449.2 420.1 449.8 421.4L497.8 453.4C509.5 461.2 514.7 475.8 510.6 489.3C506.5 502.8 494.1 512 480 512H31.1C17.9 512 5.458 502.8 1.372 489.3C-2.715 475.8 2.515 461.2 14.25 453.4L62.25 421.4C62.82 420.1 63.41 420.6 63.1 420.3V224H127.1V416z"/></svg>
|
After Width: | Height: | Size: 1015 B |
2
jsonresume-theme-custom/theme/images/graduation-cap.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M623.1 136.9l-282.7-101.2c-13.73-4.91-28.7-4.91-42.43 0L16.05 136.9C6.438 140.4 0 149.6 0 160s6.438 19.65 16.05 23.09L76.07 204.6c-11.89 15.8-20.26 34.16-24.55 53.95C40.05 263.4 32 274.8 32 288c0 9.953 4.814 18.49 11.94 24.36l-24.83 149C17.48 471.1 25 480 34.89 480H93.11c9.887 0 17.41-8.879 15.78-18.63l-24.83-149C91.19 306.5 96 297.1 96 288c0-10.29-5.174-19.03-12.72-24.89c4.252-17.76 12.88-33.82 24.94-47.03l190.6 68.23c13.73 4.91 28.7 4.91 42.43 0l282.7-101.2C633.6 179.6 640 170.4 640 160S633.6 140.4 623.1 136.9zM351.1 314.4C341.7 318.1 330.9 320 320 320c-10.92 0-21.69-1.867-32-5.555L142.8 262.5L128 405.3C128 446.6 213.1 480 320 480c105.1 0 192-33.4 192-74.67l-14.78-142.9L351.1 314.4z"/></svg>
|
After Width: | Height: | Size: 986 B |
2
jsonresume-theme-custom/theme/images/heart.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z"/></svg>
|
After Width: | Height: | Size: 629 B |
2
jsonresume-theme-custom/theme/images/toolbox.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M502.6 182.6l-45.25-45.25C451.4 131.4 443.3 128 434.8 128H384V80C384 53.5 362.5 32 336 32h-160C149.5 32 128 53.5 128 80V128H77.25c-8.5 0-16.62 3.375-22.62 9.375L9.375 182.6C3.375 188.6 0 196.8 0 205.3V304h128v-32C128 263.1 135.1 256 144 256h32C184.9 256 192 263.1 192 272v32h128v-32C320 263.1 327.1 256 336 256h32C376.9 256 384 263.1 384 272v32h128V205.3C512 196.8 508.6 188.6 502.6 182.6zM336 128h-160V80h160V128zM384 368c0 8.875-7.125 16-16 16h-32c-8.875 0-16-7.125-16-16v-32H192v32C192 376.9 184.9 384 176 384h-32C135.1 384 128 376.9 128 368v-32H0V448c0 17.62 14.38 32 32 32h448c17.62 0 32-14.38 32-32v-112h-128V368z"/></svg>
|
After Width: | Height: | Size: 912 B |
2
jsonresume-theme-custom/theme/images/user.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 224 256zM274.7 304H173.3C77.61 304 0 381.6 0 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304z"/></svg>
|
After Width: | Height: | Size: 528 B |
206
jsonresume-theme-custom/theme/index.ejs
Normal file
@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= locals.basics.name %></title>
|
||||
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
||||
|
||||
<style>
|
||||
@import './styles/global.css';
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row main clearfix">
|
||||
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
|
||||
<div class="card profile-card">
|
||||
<div class="profile-pic-container">
|
||||
<div class="profile-pic">
|
||||
<img
|
||||
class="media-object img-circle center-block"
|
||||
data-src="holder.js/100x100"
|
||||
alt="<%= locals.basics.name %>"
|
||||
src="<%= locals.basics.image %>"
|
||||
/>
|
||||
</div>
|
||||
<div class="name-and-profession text-center">
|
||||
<h3>
|
||||
<strong><%= locals.basics.name %></strong>
|
||||
</h3>
|
||||
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-details clearfix">
|
||||
<div class="detail">
|
||||
<span class="info"><%= locals.basics.phone %></span>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a
|
||||
class="link-disguise"
|
||||
href="mailto:<%= locals.basics.email %>"
|
||||
>
|
||||
<%= locals.basics.email %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a class="link-disguise" href="<%= locals.basics.url %>">
|
||||
<%= locals.basics.url %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="card background-card">
|
||||
<div class="background-details">
|
||||
<div class="detail" id="about">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/user.svg" alt="user" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">À propos</h4>
|
||||
<div class="card card-nested">
|
||||
<div class="content mop-wrapper">
|
||||
<p><%- locals.basics.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="work-experience">
|
||||
<div class="icon">
|
||||
<img
|
||||
src="data-url:./images/building-columns.svg"
|
||||
alt="work"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Expériences</h4>
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.work.forEach((experience) => { %>
|
||||
<li class="card card-nested clearfix">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<a href="<%= experience.website %>">
|
||||
<strong><%= experience.name %></strong>
|
||||
</a>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%- experience.position %></strong>
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<span class="space-right">
|
||||
<%= date.format(new Date(experience.startDate),
|
||||
'DD/MM/YYYY') %> - <%= date.format(new
|
||||
Date(experience.endDate), 'DD/MM/YYYY') %>
|
||||
</span>
|
||||
</small>
|
||||
</p>
|
||||
<div class="experience-description">
|
||||
<p><%- experience.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="skills">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/toolbox.svg" alt="toolbox" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Compétences</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.skills.forEach((skill) => { %>
|
||||
<li class="card card-nested card-skills">
|
||||
<div class="skill-info">
|
||||
<strong><%= skill.name %></strong>
|
||||
<div class="space-top labels">
|
||||
<% skill.keywords.forEach((keyword) => { %>
|
||||
<p class="label label-keyword"><%= keyword %></p>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="education">
|
||||
<div class="icon">
|
||||
<img
|
||||
src="data-url:./images/graduation-cap.svg"
|
||||
alt="graduation"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Éducation</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.education.forEach((degree) => { %>
|
||||
<li class="card card-nested">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.studyType %></strong>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.score %></strong>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<%= degree.institution %>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<small>
|
||||
<%= degree.startDate %> - <%= degree.endDate %>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="interests">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/heart.svg" alt="heart" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Intérets</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.interests.forEach((interest) => { %>
|
||||
<li class="card card-nested">
|
||||
<p><strong><%= interest.name %></strong></p>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
229
jsonresume-theme-custom/theme/styles/global.css
Normal file
@ -0,0 +1,229 @@
|
||||
@import 'npm:modern-normalize/modern-normalize.css';
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
line-height: 1.42857143;
|
||||
font-size: 14px;
|
||||
}
|
||||
hr {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
a {
|
||||
color: #337ab7;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #23527c;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.link-disguise {
|
||||
color: inherit;
|
||||
}
|
||||
.link-disguise:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.h4,
|
||||
.h5,
|
||||
.h6,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: inherit;
|
||||
}
|
||||
.h3,
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.h4,
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.h5,
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.container-fluid {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.row {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
.clear-margin {
|
||||
margin: 0;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.center-block {
|
||||
display: block;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-muted {
|
||||
color: #777;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.list-unstyled {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-card-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
float: none !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.profile-card-wrapper .profile-card {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.profile-pic {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.profile-pic img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
}
|
||||
.contact-details {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.contact-details .detail {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding: 10px;
|
||||
}
|
||||
.social-links {
|
||||
line-height: 2.5;
|
||||
}
|
||||
.experience-description {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.background-details .detail {
|
||||
display: table;
|
||||
}
|
||||
.background-details .detail .icon,
|
||||
.background-details .detail .info {
|
||||
display: table-cell;
|
||||
}
|
||||
.background-details .detail .icon {
|
||||
color: #707070;
|
||||
}
|
||||
.background-details .detail .icon {
|
||||
min-width: 45px;
|
||||
max-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
.icon img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.background-details .detail .mobile-title {
|
||||
display: none;
|
||||
}
|
||||
.card-nested {
|
||||
min-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
|
||||
.card-skills {
|
||||
position: relative;
|
||||
}
|
||||
.labels {
|
||||
line-height: 2;
|
||||
}
|
||||
.space-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.label {
|
||||
display: inline;
|
||||
padding: 0.2em 0.6em 0.3em;
|
||||
font-size: 75%;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
.label-keyword {
|
||||
display: inline-block;
|
||||
background: #7eb0db;
|
||||
color: white;
|
||||
font-size: 0.9em;
|
||||
padding: 5px;
|
||||
border: 1px solid #357ebd;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.label-keyword p {
|
||||
margin: 0;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"english": "English",
|
||||
"french": "French",
|
||||
"allRightsReserved": "All rights reserved",
|
||||
"all-rights-reserved": "All rights reserved",
|
||||
"home": "Home"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returnToHomePage": "Return to the home page?",
|
||||
"return-to-home-page": "Return to the home page?",
|
||||
"error": "Error",
|
||||
"serverError": "Internal Server Error!",
|
||||
"notFound": "This page doesn't exist!"
|
||||
"server-error": "Internal Server Error!",
|
||||
"not-found": "This page doesn't exist!"
|
||||
}
|
||||
|
@ -1,24 +1,25 @@
|
||||
{
|
||||
"about": {
|
||||
"IAm": "I am",
|
||||
"i-am": "I am",
|
||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
||||
"birthDate": "Birth date",
|
||||
"full-name": "Full name",
|
||||
"birth-date": "Birth date",
|
||||
"nationality": "Nationality",
|
||||
"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."
|
||||
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (first year)."
|
||||
},
|
||||
"interests": {
|
||||
"title": "Interests",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Developer Full Stack Junior :",
|
||||
"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": "Developer Full Stack Junior",
|
||||
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
||||
},
|
||||
{
|
||||
"title": "Passionate about High-Tech :",
|
||||
"title": "Passionate about High-Tech",
|
||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||
},
|
||||
{
|
||||
"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 class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
@ -26,7 +27,7 @@
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"languages": "Programming languages",
|
||||
"softwareTools": "Software and tools"
|
||||
"software-tools": "Software and tools"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Portfolio",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"english": "Anglais",
|
||||
"french": "Français",
|
||||
"allRightsReserved": "Tous droits réservés",
|
||||
"all-rights-reserved": "Tous droits réservés",
|
||||
"home": "Accueil"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returnToHomePage": "Revenir à la page d'accueil ?",
|
||||
"return-to-home-page": "Revenir à la page d'accueil ?",
|
||||
"error": "Erreur",
|
||||
"serverError": "Erreur Interne du Serveur !",
|
||||
"notFound": "Cette page n'existe pas!"
|
||||
"server-error": "Erreur Interne du Serveur !",
|
||||
"not-found": "Cette page n'existe pas!"
|
||||
}
|
||||
|
@ -1,24 +1,25 @@
|
||||
{
|
||||
"about": {
|
||||
"IAm": "Je suis",
|
||||
"i-am": "Je suis",
|
||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||
"birthDate": "Date de naissance",
|
||||
"full-name": "Prénom NOM",
|
||||
"birth-date": "Date de naissance",
|
||||
"nationality": "Nationalité",
|
||||
"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."
|
||||
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année)."
|
||||
},
|
||||
"interests": {
|
||||
"title": "Intérêts",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Développeur Full Stack Junior :",
|
||||
"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": "Développeur Full Stack Junior",
|
||||
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
||||
},
|
||||
{
|
||||
"title": "Passionné de High-Tech :",
|
||||
"title": "Passionné de High-Tech",
|
||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||
},
|
||||
{
|
||||
"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 class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
@ -26,7 +27,7 @@
|
||||
"skills": {
|
||||
"title": "Compétences",
|
||||
"languages": "Langages de programmation",
|
||||
"softwareTools": "Logiciels et outils"
|
||||
"software-tools": "Logiciels et outils"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Portfolio",
|
||||
@ -39,7 +40,7 @@
|
||||
},
|
||||
{
|
||||
"title": "thream.divlo.fr",
|
||||
"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 amusez-vous.",
|
||||
"link": "https://thream.divlo.fr/",
|
||||
"image": "/images/portfolio/threamdivlofr.png"
|
||||
},
|
||||
|
4
next-env.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
@ -1,11 +1,41 @@
|
||||
const nextPWA = require('next-pwa')
|
||||
const nextTranslate = require('next-translate')
|
||||
const { createSecureHeaders } = require('next-secure-headers')
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
module.exports = nextTranslate(
|
||||
nextPWA({
|
||||
reactStrictMode: true,
|
||||
pwa: {
|
||||
disable: process.env.NODE_ENV !== 'production',
|
||||
dest: 'public'
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: createSecureHeaders({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
'data:',
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ['*', 'data:', 'blob:'],
|
||||
mediaSrc: "'none'",
|
||||
connectSrc: '*',
|
||||
objectSrc: "'none'",
|
||||
fontSrc: "'self'",
|
||||
baseURI: "'none'"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
45198
package-lock.json
generated
124
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "divlo",
|
||||
"version": "1.3.2",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -13,70 +13,94 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"start": "next start",
|
||||
"build": "next build",
|
||||
"build": "npm run resume:export && next build",
|
||||
"export": "next export",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:docker": "dockerfilelint './Dockerfile'",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
||||
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
||||
"lint:markdown": "markdownlint \"**/*.{md,mdx}\" --dot --ignore-path \".gitignore\"",
|
||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:prettier": "prettier \".\" --check",
|
||||
"lint:staged": "lint-staged",
|
||||
"lighthouse": "lhci autorun",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
||||
"test:lighthouse": "lhci autorun",
|
||||
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
||||
"test:e2e:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
||||
"resume:validate": "resume validate",
|
||||
"resume:serve": "resume serve --theme \"custom\"",
|
||||
"resume:export": "resume export \"./public/curriculum-vitae.html\" --format \"html\" --theme \"custom\"",
|
||||
"release": "semantic-release",
|
||||
"deploy": "vercel",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/montserrat": "4.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@fontsource/montserrat": "4.5.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.0.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.17",
|
||||
"classnames": "2.3.1",
|
||||
"html-react-parser": "1.2.7",
|
||||
"next": "11.0.1",
|
||||
"next-pwa": "5.2.24",
|
||||
"next-themes": "0.0.15",
|
||||
"next-translate": "1.0.7",
|
||||
"date-and-time": "2.1.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"html-react-parser": "1.4.8",
|
||||
"next": "12.1.0",
|
||||
"next-mdx-remote": "4.0.0",
|
||||
"next-pwa": "5.4.4",
|
||||
"next-themes": "0.1.1",
|
||||
"next-translate": "1.3.4",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"read-pkg": "5.2.0",
|
||||
"read-pkg": "7.1.0",
|
||||
"rehype-raw": "6.1.1",
|
||||
"rehype-slug": "5.0.1",
|
||||
"remark-gfm": "3.0.1",
|
||||
"sharp": "0.30.1",
|
||||
"shiki": "0.10.1",
|
||||
"unified": "10.1.1",
|
||||
"unist-util-visit": "4.1.0",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "13.1.0",
|
||||
"@commitlint/config-conventional": "13.1.0",
|
||||
"@lhci/cli": "0.8.0",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "12.0.0",
|
||||
"@types/jest": "26.0.24",
|
||||
"@types/node": "16.4.3",
|
||||
"@types/react": "17.0.15",
|
||||
"@types/styled-jsx": "2.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "4.28.5",
|
||||
"autoprefixer": "10.3.1",
|
||||
"babel-jest": "27.0.6",
|
||||
"dockerfilelint": "1.8.0",
|
||||
"@commitlint/cli": "16.2.1",
|
||||
"@commitlint/config-conventional": "16.2.1",
|
||||
"@lhci/cli": "0.9.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@tailwindcss/typography": "0.5.2",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.3",
|
||||
"@types/date-and-time": "0.13.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "17.0.19",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/unist": "2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.12.1",
|
||||
"autoprefixer": "10.4.2",
|
||||
"cypress": "9.5.0",
|
||||
"editorconfig-checker": "4.0.2",
|
||||
"eslint": "7.31.0",
|
||||
"eslint-config-next": "11.0.1",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-config-standard-with-typescript": "20.0.0",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "3.4.0",
|
||||
"eslint-plugin-promise": "5.1.0",
|
||||
"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"
|
||||
"eslint": "8.9.0",
|
||||
"eslint-config-conventions": "1.1.0",
|
||||
"eslint-config-next": "12.1.0",
|
||||
"eslint-config-prettier": "8.4.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-unicorn": "41.0.0",
|
||||
"html-w3c-validator": "1.0.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
||||
"lint-staged": "12.3.4",
|
||||
"markdownlint-cli": "0.31.1",
|
||||
"next-secure-headers": "2.2.0",
|
||||
"postcss": "8.4.6",
|
||||
"prettier": "2.5.1",
|
||||
"prettier-plugin-tailwindcss": "0.1.7",
|
||||
"resume-cli": "3.0.6",
|
||||
"semantic-release": "19.0.2",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"tailwindcss": "3.0.23",
|
||||
"typescript": "4.4.4",
|
||||
"vercel": "24.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,27 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import readPackageJSON from 'read-pkg'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
||||
|
||||
const Error404: React.FC<FooterProps> = (props) => {
|
||||
interface Error404Props extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
|
||||
const Error404: NextPage<Error404Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
const { version, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Divlo - 404' />
|
||||
<Head title='404 | Divlo' description={description} />
|
||||
|
||||
<Header />
|
||||
<Header showLanguage />
|
||||
<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:not-found')} />
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</>
|
||||
@ -25,8 +29,10 @@ const Error404: React.FC<FooterProps> = (props) => {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
const { version } = await readPackageJSON()
|
||||
return { props: { version } }
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
}
|
||||
|
||||
export default Error404
|
||||
|
@ -1,23 +1,27 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import readPackageJSON from 'read-pkg'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
||||
|
||||
const Error500: React.FC<FooterProps> = (props) => {
|
||||
interface Error500Props extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
|
||||
const Error500: NextPage<Error500Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
const { version, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Divlo - 500' />
|
||||
<Head title='500 | Divlo' description={description} />
|
||||
|
||||
<Header />
|
||||
<Header showLanguage />
|
||||
<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:server-error')} />
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</>
|
||||
@ -25,8 +29,10 @@ const Error500: React.FC<FooterProps> = (props) => {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
const { version } = await readPackageJSON()
|
||||
return { props: { version } }
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
}
|
||||
|
||||
export default Error500
|
||||
|
@ -4,18 +4,16 @@ import { ThemeProvider } from 'next-themes'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import UniversalCookie from 'universal-cookie'
|
||||
|
||||
import 'tailwindcss/tailwind.css'
|
||||
import 'styles/global.css'
|
||||
import '@fontsource/montserrat/400.css'
|
||||
import '@fontsource/montserrat/500.css'
|
||||
import '@fontsource/montserrat/600.css'
|
||||
import '@fontsource/montserrat/700.css'
|
||||
|
||||
const universalCookie = new UniversalCookie()
|
||||
|
||||
/** how long in seconds, until the cookie expires (10 years) */
|
||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||
|
||||
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const { lang } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
@ -32,4 +30,4 @@ const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
export default Application
|
||||
|
@ -1,31 +1,15 @@
|
||||
import Document, {
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
DocumentInitialProps
|
||||
} from 'next/document'
|
||||
import { Html, Head, Main, NextScript } 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 {
|
||||
const Document: React.FC = () => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body className='bg-white dark:bg-black text-black dark:text-white font-headline'>
|
||||
<body className='bg-white font-headline text-black dark:bg-black dark:text-white'>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
export default Document
|
||||
|
83
pages/blog/[slug].tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import date from 'date-and-time'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import type { Post } from 'utils/blog'
|
||||
|
||||
interface BlogPostPageProps extends FooterProps {
|
||||
post: Post
|
||||
}
|
||||
|
||||
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||
const { version, post } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head
|
||||
title={`${post.frontmatter.title} | Divlo`}
|
||||
description={post.frontmatter.description}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
||||
<div className='my-10 flex flex-col items-center'>
|
||||
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
||||
<p className='mt-2' data-cy='blog-post-date'>
|
||||
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='prose mb-10 px-8'>
|
||||
<MDXRemote
|
||||
{...post.source}
|
||||
components={{
|
||||
a: (props: React.ComponentPropsWithoutRef<'a'>) => {
|
||||
if (props.href?.startsWith('#') ?? false) {
|
||||
return <a {...props} />
|
||||
}
|
||||
return (
|
||||
<a target='_blank' rel='noopener noreferrer' {...props} />
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
||||
context
|
||||
) => {
|
||||
const slug = context?.params?.slug
|
||||
const { getPostBySlug } = await import('utils/blog')
|
||||
const post = await getPostBySlug(slug)
|
||||
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/404',
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
}
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
return { props: { version, post } }
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const { getPosts } = await import('utils/blog')
|
||||
const posts = await getPosts()
|
||||
return {
|
||||
paths: posts.map((post) => {
|
||||
return { params: { slug: post.slug } }
|
||||
}),
|
||||
fallback: false
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPostPage
|
77
pages/blog/index.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import Link from 'next/link'
|
||||
import date from 'date-and-time'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import type { PostMetadata } from 'utils/blog'
|
||||
|
||||
const blogDescription =
|
||||
'The latest news about my journey of learning computer science.'
|
||||
|
||||
interface BlogPageProps extends FooterProps {
|
||||
posts: PostMetadata[]
|
||||
}
|
||||
|
||||
const BlogPage: NextPage<BlogPageProps> = (props) => {
|
||||
const { version, posts } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Blog | Divlo' description={blogDescription} />
|
||||
|
||||
<Header />
|
||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
||||
<div className='mt-10 flex flex-col items-center'>
|
||||
<h1 className='text-4xl font-semibold'>Blog</h1>
|
||||
<p className='mt-6 text-center' data-cy='blog-post-date'>
|
||||
{blogDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-center p-8'>
|
||||
<div className='w-[1600px]' data-cy='blog-posts'>
|
||||
{posts.map((post, index) => {
|
||||
const postPublishedOn = date.format(
|
||||
new Date(post.frontmatter.publishedOn),
|
||||
'DD/MM/YYYY'
|
||||
)
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} key={index} locale='en'>
|
||||
<a data-cy={post.slug}>
|
||||
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
||||
<h2
|
||||
data-cy='blog-post-title'
|
||||
className='text-xl font-semibold'
|
||||
>
|
||||
{post.frontmatter.title}
|
||||
</h2>
|
||||
<p data-cy='blog-post-date' className='mt-2'>
|
||||
{postPublishedOn}
|
||||
</p>
|
||||
<p data-cy='blog-post-description' className='mt-3'>
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<BlogPageProps> = async () => {
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { getPosts } = await import('utils/blog')
|
||||
const posts = await getPosts()
|
||||
const { version } = await readPackage()
|
||||
return { props: { version, posts } }
|
||||
}
|
||||
|
||||
export default BlogPage
|
@ -1,6 +1,5 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import readPackageJSON from 'read-pkg'
|
||||
|
||||
import { RevealFade } from 'components/design/RevealFade'
|
||||
import { Section } from 'components/design/Section'
|
||||
@ -13,16 +12,21 @@ import { Skills } from 'components/Skills'
|
||||
import { OpenSource } from 'components/OpenSource'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
||||
|
||||
const Home: React.FC<FooterProps> = (props) => {
|
||||
interface HomeProps extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
|
||||
const Home: NextPage<HomeProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
const { version, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head />
|
||||
<Head description={description} />
|
||||
|
||||
<Header />
|
||||
<Header showLanguage />
|
||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||
<Section isMain id='about'>
|
||||
<Profile />
|
||||
@ -71,8 +75,10 @@ const Home: React.FC<FooterProps> = (props) => {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
const { version } = await readPackageJSON()
|
||||
return { props: { version } }
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
254
posts/clean-code.mdx
Normal file
@ -0,0 +1,254 @@
|
||||
---
|
||||
title: '🧼 Clean Code'
|
||||
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
|
||||
isPublished: true
|
||||
publishedOn: '2022-02-23T08:00:18.758Z'
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
Have you already heard of "**Clean Code**" or "**Design Patterns**" ?
|
||||
|
||||
Even if you know what it is about, this blog post will probably still be useful to you, I will share some tips and tricks to make your code more readable and maintainable in the long term.
|
||||
|
||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||
|
||||
## Definition : Clean Code
|
||||
|
||||
A clean code is a code that is **easy** to **read** and easy to **understand**.
|
||||
|
||||
But I promise it is not a code that is easy to write, in fact it is really **hard to write Clean Code**.
|
||||
|
||||
We could ask ourselves, what is **easy** to **read** and easy to **understand** ?
|
||||
|
||||
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
|
||||
|
||||
## Why is it so important ?
|
||||
|
||||
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
|
||||
|
||||
For example the [Linux kernel](https://www.kernel.org/), is one of the biggest open source project with many contributors worldwide. Last data shows that it is about **20 millions** lines of code.
|
||||
|
||||
With a project of this magnitude, we can't let everyone do what they want and however they want, **we must set rules and conventions** to get everyone to agree, this allows to add features faster and will reduce possible bugs as **developers** will not struggle as much to understand the code.
|
||||
|
||||
## Definition : Design Patterns
|
||||
|
||||
These **rules** and **conventions** are so called **Design Patterns**.
|
||||
|
||||
A software design pattern is a general way of **solving a problem** by applying a **well-known solution**.
|
||||
|
||||
Design patterns are formalized **best practices** that the programmer can use to solve common problems when designing an application or system.
|
||||
|
||||
## How to write Clean Code and famous Design Patterns
|
||||
|
||||
To show you the rules and conventions, I will write the examples in the [TypeScript](https://www.typescriptlang.org/) programming language but it is relevant to any programming language.
|
||||
|
||||
### Naming variables
|
||||
|
||||
We all know that **variables** are used everywhere in **programming**, good variable names allow us to better understand the intention of the code.
|
||||
|
||||
#### Same vocabulary for the same type of variable
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
```typescript
|
||||
function getUserInfo(): User
|
||||
function getUserDetails(): User
|
||||
function getUserData(): User
|
||||
```
|
||||
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
function getUser(): User
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Avoid "Magic Numbers"
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
```typescript
|
||||
// What does 86400000 mean?
|
||||
setTimeout(restart, 86400000)
|
||||
```
|
||||
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
|
||||
setTimeout(restart, MILLISECONDS_IN_A_DAY)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Explicit is better than implicit (no abbreviations or acronyms)
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
```typescript
|
||||
const u = getUser()
|
||||
const s = getSubscription()
|
||||
const t = charge(u, s)
|
||||
```
|
||||
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
const user = getUser()
|
||||
const subscription = getSubscription()
|
||||
const transaction = charge(user, subscription)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### As short as possible, as long as necessary
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
```typescript
|
||||
interface Car {
|
||||
carModel: string
|
||||
carColor: 'red' | 'blue' | 'yellow'
|
||||
}
|
||||
const printCar = (car: Car): void => {
|
||||
console.log(`${car.carModel} (${car.carColor})`)
|
||||
}
|
||||
```
|
||||
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
interface Car {
|
||||
model: string
|
||||
color: 'red' | 'blue' | 'yellow'
|
||||
}
|
||||
const printCar = (car: Car): void => {
|
||||
console.log(`${car.model} (${car.color})`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Boolean names (Prefix: is, has, can)
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
```typescript
|
||||
let person = true
|
||||
let age = true
|
||||
let dance = true
|
||||
function isEmailNotUsed(email: string): boolean
|
||||
```
|
||||
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
let isPerson = true
|
||||
let hasAge = true
|
||||
let canDance = true
|
||||
function isEmailUsed(email: string): boolean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DRY (Don't Repeat Yourself)
|
||||
|
||||
When we copy/paste the same lines of code, we should better abstract it in a function, that we can reuse later without having to copy/paste the lines of code, that makes the code more maintainable afterwards, because if we need to change the behavior of this piece of code, we won't need to change it in several places, but only when declaring the function.
|
||||
|
||||
---
|
||||
|
||||
### KISS (Keep It Simple Stupid)
|
||||
|
||||
As we have just said, we will prefer to abstract the code in multiple functions, rather than leaving everything in the same place, but a function should not do "too much", and we should rather separate it into several distinct functions.
|
||||
|
||||
We have to keep it as simple as possible, not to implement features that are not requested, and to divide the functions as much as possible into small functions.
|
||||
|
||||
### Example (bad way)
|
||||
|
||||
```typescript
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const createFile = async (name: string, isTemporary: boolean = false) => {
|
||||
if (isTemporary) {
|
||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
||||
}
|
||||
return await fs.promises.writeFile(name, '')
|
||||
}
|
||||
```
|
||||
|
||||
`createFile` is a function that does 2 things so it is better to split it in 2 separated functions.
|
||||
|
||||
### Example (good way)
|
||||
|
||||
```typescript
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const createFile = async (name: string) => {
|
||||
await fs.promises.writeFile(name, '')
|
||||
}
|
||||
|
||||
const createTemporaryFile = async (name: string) => {
|
||||
await createFile(path.join('temporary', name))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TDD (Test Driven Development)
|
||||
|
||||
Test-driven development (TDD) is a software development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases. This is as opposed to software being developed first and test cases created later.
|
||||
|
||||
We first write tests that should fails because there are no implementation, and then we write the code implementation to make the tests succeeds.
|
||||
|
||||
The End To End (e2e) and Unit tests should document what is the behavior intended for the code.
|
||||
|
||||
---
|
||||
|
||||
### Avoid comments
|
||||
|
||||
One of the most important rule of "Clean Code" : If you need to add **comments**, it's because your code is **not clean**.
|
||||
|
||||
I know that might be counter intuitive at first, as most developers will advice you to add comments to your code, to document what it does.
|
||||
|
||||
The thing is that you should choose good variable names, break down features in multiple functions, so that others developers can read your code and understand it just by reading the functions names etc.
|
||||
|
||||
You can write comments, but that should only be used documenting how to use a function but not for the implementation itself and in places where you can't be more explicit.
|
||||
|
||||
In fact, as we saw in the [TDD section](#tdd-test-driven-development), automated tests can document what a function should returns, and how the code should behave, so that should already improve code maintainability.
|
||||
|
||||
Having a good comment explaining a difficult code is better than nothing with a bad written code, difficult to understand.
|
||||
|
||||
#### Example (bad way)
|
||||
|
||||
```typescript
|
||||
// Check if subscription is active
|
||||
if (subscription.endDate > Date.now()) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example (good way)
|
||||
|
||||
```typescript
|
||||
const isSubscriptionActive = subscription.endDate > Date.now()
|
||||
if (isSubscriptionActive) {
|
||||
}
|
||||
```
|
||||
|
||||
Here we are creating a new variable `isSubscriptionActive` that allows us to avoid the need of a comment to understand what the code does.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
We can't write the perfect clean code understandable by everyone but we can **write code that is as perfect as possible to ease maintaibility** for yourself and others developers.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin](https://books.google.fr/books/about/Clean_Code.html?id=hjEFCAAAQBAJ)
|
||||
- [Software Design Pattern (Wikipedia)](https://en.wikipedia.org/wiki/Software_design_pattern)
|
||||
- [TDD - Test-driven development (Wikipedia)](https://en.wikipedia.org/wiki/Test-driven_development)
|
||||
- [github.com/labs42io/clean-code-typescript](https://github.com/labs42io/clean-code-typescript)
|
71
posts/hello-world.mdx
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
title: '👋 Hello, world!'
|
||||
description: 'First post of the blog, introduction and explanation of how this blog is made.'
|
||||
isPublished: true
|
||||
publishedOn: '2022-02-20T08:00:18.758Z'
|
||||
---
|
||||
|
||||
Hello, world! 👋
|
||||
|
||||
## Introduction
|
||||
|
||||
This blog is here to document my journey of learning computer science, explaining technical difficulties and problems I encountered, and how I solved them.
|
||||
|
||||
The idea is that I will share my knowledge with you (readers), and hopefully help you to learn too.
|
||||
|
||||
Keep in mind that I will not translate the posts in French, all the posts will be written in English, as I'm not a native English speaker, I will probably make mistakes, feel free to open pull requests on [GitHub](https://github.com/Divlo/Divlo) to correct them. 😊
|
||||
|
||||
I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned!
|
||||
|
||||
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@Divlo_FR](https://twitter.com/Divlo_FR).
|
||||
|
||||
## Project based learning
|
||||
|
||||
The blog posts subjects will be often related to the problems I encountered in the projects I am currently working on.
|
||||
|
||||
Most of the time, when I am learning something new, I **learn it because I actually need it for a project**, I don't learn [React.js](https://reactjs.org) because it is trending, and everyone talks about it.
|
||||
|
||||
I learn something new, because it solved a "real life" problem I had encountered. For example, [React.js](https://reactjs.org) allows to easily update the DOM (Document Object Model) in the browser, so we can add interactivity to our web pages, not only that, it allows to reuse multiple HTML (JSX) elements with components.
|
||||
|
||||
[React.js](https://reactjs.org) is only an example, but hopefully you understood my point: I often don't like too much theoretical thing, and enjoy much more practical things.
|
||||
|
||||
## How this blog is made
|
||||
|
||||
In this section, I will explain what technologies I used to make this blog, and what are the technical choices I had to do.
|
||||
|
||||
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
||||
|
||||
I decided to keep things simple, here are the 2 main features missing on my blog:
|
||||
|
||||
- Comments (you can interact with me on my Twitter account)
|
||||
- Views counter
|
||||
|
||||
That not mean that these features will never be implemented, but to avoid the need of a database now, I dropped out these features.
|
||||
|
||||
### Technologies
|
||||
|
||||
- [Next.js](https://nextjs.org/)
|
||||
|
||||
It allows to have a server-side rendered website, that means that it is faster and easier to have a good SEO (Search Engine Optimization) than a SPA (Single Page Application).
|
||||
|
||||
- [MDX](https://mdxjs.com/)
|
||||
|
||||
MDX is an extension of Markdown that allows you to use custom React components.
|
||||
|
||||
Here's what Markdown looks like:
|
||||
|
||||
```md
|
||||
A simple paragraph, with some **bold** text and some `inline code`.
|
||||
```
|
||||
|
||||
When using Markdown in a web application, there's a "compile" step; the Markdown needs to be transformed into HTML, so that it can be understood by the browser. Those asterisks get turned into a `<strong>` tag, and each paragraph gets a `<p>` tag etc.
|
||||
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
|
||||
Tailwind is a CSS framework to rapidly build modern websites without ever leaving HTML.
|
||||
|
||||
## Conclusion
|
||||
|
||||
I hope you will enjoy my blog, and will find it useful.
|
||||
|
||||
See you in the next posts! 😊
|
44
posts/mistakes-as-junior-developer.mdx
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
title: '❌ Mistakes I made as a junior developer'
|
||||
description: 'Here are mistakes I made when I started, to prevent you from making the same mistakes.'
|
||||
isPublished: false
|
||||
publishedOn: '2021-12-06T22:06:33.818Z'
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
I will explain some of my mistakes I made as a junior developer, so you can avoid doing them.
|
||||
|
||||
## 1. Skipped learning how to do automated tests
|
||||
|
||||
Probably one of the most common error junior developers do.
|
||||
|
||||
When you begin in programming, you learn a programming language, so you learn variables, conditions, loops, functions, etc.
|
||||
|
||||
With these concepts, you might start a new project, thinking that you will be able to do everything.
|
||||
|
||||
But as the project grows, you will end up using functions at multiple places in code, so if you change the behavior of a function, it will affect the whole project.
|
||||
|
||||
And because the code grows, you might do some refactoring, but because we are humans, we make mistakes, you could accidentally break the whole project even with a tiny change you thought was safe to do.
|
||||
|
||||
If you would have automated tests, you would have a way to know if you made a mistake even before deploying to production.
|
||||
|
||||
Depending on the programming language you are using, and what is the project you are working on, writing tests will be different.
|
||||
|
||||
Be aware that there are 3 main testing strategy:
|
||||
|
||||
- [Unit testing](https://en.wikipedia.org/wiki/Unit_testing)
|
||||
- [Integration testing](https://en.wikipedia.org/wiki/Integration_testing)
|
||||
- [End-to-end testing](https://en.wikipedia.org/wiki/End-to-end_testing)
|
||||
|
||||
After you learnt the basic of programming, learn how to write automated tests, it will save you a lot of time and debugging.
|
||||
|
||||
## 2. Thinking too big, with too much abstraction
|
||||
|
||||
Abstraction is great, but it can be harder to understand what is going on if actally don't need this abstraction.
|
||||
|
||||
Find the right balance, between abstraction and implementation, start simple, and then gradually improve and add more features.
|
||||
|
||||
When you start a new project, you should focus on the core of the project, not on the details, to release as soon as possible, a working usable version of your project also called a [**Minimum Viable Product** (MVP)](https://en.wikipedia.org/wiki/Minimum_viable_product).
|
||||
|
||||
## 3. Focusing on the thing that don't add value to a project
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 92 KiB |
BIN
public/images/skills/GNU-Linux.png
Normal file
After Width: | Height: | Size: 290 KiB |
119
resume.json
Normal file
@ -0,0 +1,119 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
|
||||
"meta": {
|
||||
"theme": "custom"
|
||||
},
|
||||
"basics": {
|
||||
"name": "Théo LUDWIG",
|
||||
"label": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||
"image": "https://s.gravatar.com/avatar/ebd6e0bf679562c20e28b5ffd02bf3e5?s=100&r=pg&d=mm",
|
||||
"email": "contact@divlo.fr",
|
||||
"location": {},
|
||||
"url": "https://divlo.fr",
|
||||
"summary": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année). <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets."
|
||||
},
|
||||
"education": [
|
||||
{
|
||||
"startDate": "2022",
|
||||
"endDate": "2024",
|
||||
"studyType": "Diplôme du Bachelor Universitaire de Technologie (BUT) Informatique",
|
||||
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
||||
"score": "En cours"
|
||||
},
|
||||
{
|
||||
"startDate": "2019",
|
||||
"endDate": "2021",
|
||||
"studyType": "Diplôme du Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
|
||||
"institution": "Lycée Heinrich Nessel à Haguenau",
|
||||
"score": "Mention Assez Bien"
|
||||
},
|
||||
{
|
||||
"startDate": "2014",
|
||||
"endDate": "2018",
|
||||
"studyType": "Diplôme national du brevet",
|
||||
"institution": "Collège Gustave Doré à Hochfelden",
|
||||
"score": "Mention Bien"
|
||||
}
|
||||
],
|
||||
"work": [
|
||||
{
|
||||
"summary": "Développement site web en React.js et Strapi afin de répondre <a href=\"https://www.nuitdelinfo.com/nuitinfo/_media/infos:la_nuit_de_l_info_2021_-_sujet.pdf\">au sujet de la Nuit de l'Info 2021</a>.<br /> TOP 1 France: Défi de l'entreprise <a href=\"https://www.nuitdelinfo.com/inscription/defis/300\">ToolPad</a>.",
|
||||
"website": "https://www.nuitdelinfo.com/",
|
||||
"name": "La Nuit de l'info 2021",
|
||||
"position": "Participation avec l'équipe <a href=\"https://www.nuitdelinfo.com/inscription/equipes/46\">Who are We</a>",
|
||||
"startDate": "2021-07-07",
|
||||
"endDate": "2021-07-30"
|
||||
},
|
||||
{
|
||||
"summary": "Agent administratif en vue de faire face au sucroît temporaire d'activités liés à la numérisation des plans des postes sources <br /> actuellement sous format papier calque suite à la libération des locaux des archives.",
|
||||
"website": "https://www.es.fr/",
|
||||
"name": "ÉS (Électricité de Strasbourg)",
|
||||
"location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
|
||||
"position": "Emploi d'été en qualité d'agent administratif",
|
||||
"startDate": "2021-07-07",
|
||||
"endDate": "2021-07-30"
|
||||
},
|
||||
{
|
||||
"summary": "Hackathon développement d'une landing page et web scraping.",
|
||||
"website": "https://www.wildcodeschool.fr/",
|
||||
"name": "Wild Code School",
|
||||
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
||||
"position": "Stage initiation métier développeur web",
|
||||
"startDate": "2019-06-24",
|
||||
"endDate": "2019-06-28"
|
||||
},
|
||||
{
|
||||
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
|
||||
"website": "https://www.itpartners.fr/",
|
||||
"name": "Tribe | IT Partners",
|
||||
"location": "16 Rue du Parc, 67205 Oberhausbergen",
|
||||
"position": "Stage initiation métier développeur web",
|
||||
"startDate": "2019-06-17",
|
||||
"endDate": "2019-06-21"
|
||||
},
|
||||
{
|
||||
"summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
|
||||
"website": "https://www.es.fr/",
|
||||
"name": "ÉS (Électricité de Strasbourg)",
|
||||
"location": "26 Bd du Président-Wilson, 67000 Strasbourg",
|
||||
"position": "Stage de découverte (3ème)",
|
||||
"startDate": "2018-02-19",
|
||||
"endDate": "2018-02-23"
|
||||
}
|
||||
],
|
||||
"interests": [
|
||||
{
|
||||
"name": "Développeur Full Stack Junior"
|
||||
},
|
||||
{
|
||||
"name": "Passionné de High-Tech"
|
||||
},
|
||||
{
|
||||
"name": "Enthousiaste de l'Open-Source"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"keywords": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
||||
"name": "Langages de programmation"
|
||||
},
|
||||
{
|
||||
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
||||
"name": "Front-end"
|
||||
},
|
||||
{
|
||||
"keywords": ["Node.js", "Fastify", "PostgreSQL", "MySQL"],
|
||||
"name": "Back-end"
|
||||
},
|
||||
{
|
||||
"keywords": [
|
||||
"GNU/Linux",
|
||||
"Ubuntu",
|
||||
"Visual Studio Code",
|
||||
"git",
|
||||
"Docker"
|
||||
],
|
||||
"name": "Logiciels et outils"
|
||||
}
|
||||
]
|
||||
}
|