Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
a18cec4826 | |||
61e589f0f4 | |||
dc5c3cee41 | |||
20cb0c21d5 | |||
e5232c1394 | |||
fd51609713 | |||
edf16c2562 | |||
94e0d190ae | |||
b1cf7f8517 | |||
a1a715d3b9 | |||
eede46fb41 | |||
e32c53caa1 | |||
361ea37deb | |||
d49a8a7470 | |||
a4996c8251 | |||
b25451e631 | |||
042a861f58 | |||
d76db36dbc | |||
99d9dcf334 | |||
ece5ded1b4 | |||
1514600998 | |||
5f5b328895 | |||
c88887a322 | |||
014044573a | |||
df009c3f7b | |||
5c85ca2ef1 | |||
07f7942496 | |||
213a3fa182 | |||
28d9211583 | |||
4d085cb148 | |||
e6c583f2cd | |||
232b54588a | |||
c419fb3bb4 | |||
03e7e22d74 | |||
e85c241ed1 | |||
c1877297f8 | |||
83231197dd | |||
a2fe2205bc | |||
e1f3dceb07 | |||
0f89fee52f | |||
2fcc7ac384 | |||
9351edf626 | |||
1f4aa54211 | |||
8bc1471cbb | |||
1ebdab18a5 | |||
b9b76e839a | |||
bc065a2e19 | |||
5d3a287b27 | |||
fb689c9bc1 | |||
2c3a70df2a | |||
bce254a355 | |||
f67d331416 | |||
6abc881e94 | |||
a67d6665ea | |||
1152039663 | |||
919ebd5f3e |
@ -1,2 +1 @@
|
||||
ARG VARIANT="16"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:18
|
||||
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"name": "divlo",
|
||||
"name": "Divlo",
|
||||
"dockerComposeFile": "./docker-compose.yml",
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"remote.autoForwardPorts": false
|
||||
},
|
||||
@ -10,13 +12,13 @@
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker"
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postAttachCommand": ["npm", "install"],
|
||||
"remoteUser": "node"
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
workspace:
|
||||
build:
|
||||
|
@ -1,12 +1,5 @@
|
||||
.vscode
|
||||
.git
|
||||
.env
|
||||
.*
|
||||
!.npmrc
|
||||
build
|
||||
.next
|
||||
coverage
|
||||
node_modules
|
||||
tmp
|
||||
temp
|
||||
.DS_Store
|
||||
.lighthouseci
|
||||
.vercel
|
||||
|
@ -1,2 +1,2 @@
|
||||
COMPOSE_PROJECT_NAME=divlo.fr
|
||||
COMPOSE_PROJECT_NAME=divlo
|
||||
PORT=3000
|
||||
|
@ -1,8 +0,0 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
@ -4,13 +4,7 @@
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"unicorn/prefer-node-protocol": "off"
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
||||
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||
|
||||
## What changes this PR introduce?
|
||||
# What changes this PR introduce?
|
||||
|
||||
## List any relevant issue numbers
|
||||
|
||||
|
6
.github/workflows/analyze.yml
vendored
@ -16,12 +16,12 @@ jobs:
|
||||
language: ['javascript']
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@v1'
|
||||
uses: 'github/codeql-action/init@v2'
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@v1'
|
||||
uses: 'github/codeql-action/analyze@v2'
|
||||
|
12
.github/workflows/build.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm clean-install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
|
24
.github/workflows/lint.yml
vendored
@ -10,16 +10,16 @@ jobs:
|
||||
lint:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm clean-install'
|
||||
|
||||
- name: 'lint:commit'
|
||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
@ -30,21 +30,13 @@ jobs:
|
||||
- name: 'lint:markdown'
|
||||
run: 'npm run lint:markdown'
|
||||
|
||||
- name: 'lint:typescript'
|
||||
run: 'npm run lint:typescript'
|
||||
- name: 'lint:eslint'
|
||||
run: 'npm run lint:eslint'
|
||||
|
||||
- name: 'lint:prettier'
|
||||
run: 'npm run lint:prettier'
|
||||
|
||||
- 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'
|
||||
|
14
.github/workflows/release.yml
vendored
@ -8,26 +8,26 @@ jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Import GPG key'
|
||||
uses: 'crazy-max/ghaction-import-gpg@v4'
|
||||
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.4.0'
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm clean-install'
|
||||
|
||||
- name: 'Release'
|
||||
run: 'npm run release'
|
||||
|
48
.github/workflows/test.yml
vendored
@ -10,33 +10,33 @@ jobs:
|
||||
test-unit:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm clean-install'
|
||||
|
||||
- name: 'Unit Test'
|
||||
run: 'npm run test:unit'
|
||||
|
||||
test-lighthouse:
|
||||
test-e2e:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2.4.0'
|
||||
- uses: 'actions/checkout@v3.5.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.5.1'
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm install'
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm clean-install'
|
||||
|
||||
- name: 'Build'
|
||||
run: 'npm run build'
|
||||
@ -44,27 +44,5 @@ jobs:
|
||||
- name: 'html-w3c-validator'
|
||||
run: 'npm run test:html-w3c-validator'
|
||||
|
||||
- name: 'Lighthouse'
|
||||
run: 'npm run test:lighthouse'
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
|
||||
test-e2e:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@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'
|
||||
|
6
.gitignore
vendored
@ -11,7 +11,7 @@ out
|
||||
# production
|
||||
build
|
||||
dist
|
||||
public/*.html
|
||||
public/curriculum-vitae
|
||||
# PWA
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
@ -49,3 +49,7 @@ npm-debug.log*
|
||||
.DS_Store
|
||||
.lighthouseci
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"urls": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
]
|
||||
"http://127.0.0.1:3000/",
|
||||
"http://127.0.0.1:3000/blog",
|
||||
"http://127.0.0.1:3000/blog/hello-world"
|
||||
],
|
||||
"files": ["./public/curriculum-vitae/index.html"]
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"startServerCommand": "npm run start",
|
||||
"startServerReadyPattern": "ready on",
|
||||
"startServerReadyTimeout": 20000,
|
||||
"url": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/blog",
|
||||
"http://localhost:3000/blog/hello-world"
|
||||
],
|
||||
"numberOfRuns": 1
|
||||
},
|
||||
"assert": {
|
||||
"preset": "lighthouse:recommended",
|
||||
"assertions": {
|
||||
"csp-xss": "warning",
|
||||
"non-composited-animations": "warning",
|
||||
"unused-javascript": "warning",
|
||||
"image-size-responsive": "warning",
|
||||
"unsized-images": "warning",
|
||||
"color-contrast": "warning"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
},
|
||||
"server": {}
|
||||
}
|
||||
}
|
@ -1,11 +1,6 @@
|
||||
{
|
||||
"*": ["editorconfig-checker"],
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests"
|
||||
],
|
||||
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
|
||||
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
||||
"resume.json": ["resume validate"]
|
||||
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
|
||||
}
|
||||
|
12
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"relative-links": true,
|
||||
"extends": "markdownlint/style/prettier",
|
||||
"MD024": false,
|
||||
"MD033": false
|
||||
},
|
||||
"globs": ["**/*.{md,mdx}"],
|
||||
"ignores": ["**/node_modules"],
|
||||
"customRules": ["markdownlint-rule-relative-links"]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
"MD024": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
.next
|
||||
.lighthouseci
|
||||
storybook-static
|
||||
coverage
|
||||
node_modules
|
||||
next-env.d.ts
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
*.hbs
|
2
.vscode/extensions.json
vendored
@ -3,8 +3,6 @@
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"davidanson.vscode-markdownlint",
|
||||
|
6
.vscode/settings.json
vendored
@ -6,5 +6,9 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
},
|
||||
"eslint.options": {
|
||||
"ignorePath": ".gitignore"
|
||||
},
|
||||
"prettier.ignorePath": ".gitignore"
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
contact@divlo.fr.
|
||||
<contact@divlo.fr>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
**divlo.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||
|
||||
## Types of contributions
|
||||
|
||||
- Reporting a bug.
|
||||
@ -21,29 +25,7 @@ If you're adding new features to **divlo.fr**, please include tests.
|
||||
|
||||
## Commits
|
||||
|
||||
The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases.
|
||||
|
||||
### Types
|
||||
|
||||
Types define which kind of changes you made to the project.
|
||||
|
||||
| Types | Description |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| feat | A new feature. |
|
||||
| fix | A bug fix. |
|
||||
| docs | Documentation only changes. |
|
||||
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
|
||||
| refactor | A code change that neither fixes a bug nor adds a feature. |
|
||||
| perf | A code change that improves performance. |
|
||||
| test | Adding missing tests or correcting existing tests. |
|
||||
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
|
||||
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
|
||||
| chore | Other changes that don't modify src or test files. |
|
||||
| revert | Reverts a previous commit. |
|
||||
|
||||
### Scopes
|
||||
|
||||
Scopes define what part of the code changed.
|
||||
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@ -81,9 +63,9 @@ npm run dev
|
||||
|
||||
```sh
|
||||
# Setup and run all the services for you
|
||||
docker-compose up --build
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
### Services started
|
||||
|
||||
- website : `http://localhost:3000`
|
||||
- website: `http://127.0.0.1:3000`
|
||||
|
32
Dockerfile
@ -1,23 +1,21 @@
|
||||
FROM node:16.14.0 AS dependencies
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:18.16.0 AS builder-dependencies
|
||||
WORKDIR /usr/src/application
|
||||
COPY ./package*.json ./
|
||||
RUN npm install
|
||||
RUN npm clean-install
|
||||
|
||||
FROM node:16.14.0 AS builder
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:18.16.0 AS builder
|
||||
WORKDIR /usr/src/application
|
||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||
COPY ./ ./
|
||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
|
||||
FROM node:16.14.0 AS runner
|
||||
WORKDIR /usr/src/app
|
||||
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
|
||||
WORKDIR /usr/src/application
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||
COPY --from=builder /usr/src/app/public ./public
|
||||
COPY --from=builder /usr/src/app/.next ./.next
|
||||
COPY --from=builder /usr/src/app/i18n.json ./i18n.json
|
||||
COPY --from=builder /usr/src/app/locales ./locales
|
||||
COPY --from=builder /usr/src/app/pages ./pages
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
RUN npx next telemetry disable
|
||||
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
COPY --from=builder /usr/src/application/.next/standalone ./
|
||||
COPY --from=builder /usr/src/application/.next/static ./.next/static
|
||||
COPY --from=builder /usr/src/application/public ./public
|
||||
COPY --from=builder /usr/src/application/locales ./locales
|
||||
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
|
||||
CMD ["./server.js"]
|
||||
|
14
README.md
@ -1,7 +1,7 @@
|
||||
<h1 align="center"><a href="https://divlo.fr/">Divlo</a></h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Developer Full Stack Junior • Passionate about High-Tech</strong>
|
||||
<strong>Developer Full Stack • Open-Source enthusiast</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@ -25,15 +25,11 @@
|
||||
"pronouns": "He/Him",
|
||||
"birthDate": "31/03/2003",
|
||||
"nationality": "Alsace, France",
|
||||
"interests": [
|
||||
"Developer Full Stack Junior",
|
||||
"Passionate about High-Tech",
|
||||
"Open-Source enthusiast"
|
||||
],
|
||||
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
|
||||
"skills": {
|
||||
"programmingLanguages": ["JavaScript", "TypeScript", "Python", "C/C++"],
|
||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
|
||||
"backEnd": ["Node.js", "Fastify", "Prisma", "PostgreSQL", "MySQL"],
|
||||
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||
"frontEnd": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||
"backEnd": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,24 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface ErrorPageProps {
|
||||
import type { FooterProps } from './Footer'
|
||||
import { Footer } from './Footer'
|
||||
import { Header } from './Header'
|
||||
|
||||
export interface ErrorPageProps extends FooterProps {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||
const { message, statusCode } = props
|
||||
const { message, statusCode, version } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex h-screen flex-col pt-0'>
|
||||
<Header showLanguage />
|
||||
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
{t('errors:error')}{' '}
|
||||
<span
|
||||
@ -23,31 +30,16 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||
</h1>
|
||||
<p className='text-center text-lg'>
|
||||
{message}{' '}
|
||||
<Link href='/'>
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
{t('errors:return-to-home-page')}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 100vw;
|
||||
flex: 1;
|
||||
}
|
||||
#__next {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -17,16 +17,18 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
||||
return (
|
||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||
<p>
|
||||
<Link href='/'>
|
||||
<a className='text-yellow hover:underline dark:text-yellow-dark'>
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Divlo
|
||||
</a>
|
||||
</Link>{' '}
|
||||
| {t('common:all-rights-reserved')}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
Version{' '}
|
||||
<a
|
||||
data-cy='version-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href={versionLink}
|
||||
target='_blank'
|
||||
|
@ -3,15 +3,15 @@ import NextHead from 'next/head'
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description: string
|
||||
description?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const Head: React.FC<HeadProps> = (props) => {
|
||||
const {
|
||||
title = 'Divlo',
|
||||
image = '/images/icons/icon-96x96.png',
|
||||
description,
|
||||
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
||||
description = 'Divlo - Developer Full Stack • Passionate about High-Tech',
|
||||
url = 'https://divlo.fr/'
|
||||
} = props
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
import classNames from 'classnames'
|
||||
import classNames from 'clsx'
|
||||
|
||||
import i18n from 'i18n.json'
|
||||
|
||||
@ -11,31 +11,39 @@ import { LanguageFlag } from './LanguageFlag'
|
||||
export const Language: React.FC = () => {
|
||||
const { lang: currentLanguage } = useTranslation()
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleHiddenMenu = useCallback(() => {
|
||||
setHiddenMenu(!hiddenMenu)
|
||||
}, [hiddenMenu])
|
||||
setHiddenMenu((oldHiddenMenu) => {
|
||||
return !oldHiddenMenu
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hiddenMenu) {
|
||||
window.document.addEventListener('click', handleHiddenMenu)
|
||||
} else {
|
||||
window.document.removeEventListener('click', handleHiddenMenu)
|
||||
const handleClickEvent = (event: MouseEvent): void => {
|
||||
if (languageClickRef.current == null || event.target == null) {
|
||||
return
|
||||
}
|
||||
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||
setHiddenMenu(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener('click', handleClickEvent)
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener('click', handleHiddenMenu)
|
||||
return window.removeEventListener('click', handleClickEvent)
|
||||
}
|
||||
}, [hiddenMenu, handleHiddenMenu])
|
||||
}, [])
|
||||
|
||||
const handleLanguage = async (language: string): Promise<void> => {
|
||||
await setLanguage(language)
|
||||
handleHiddenMenu()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||
<div
|
||||
ref={languageClickRef}
|
||||
data-cy='language-click'
|
||||
className='mr-5 flex items-center'
|
||||
onClick={handleHiddenMenu}
|
||||
@ -47,7 +55,7 @@ export const Language: React.FC = () => {
|
||||
<ul
|
||||
data-cy='languages-list'
|
||||
className={classNames(
|
||||
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||
'absolute top-14 z-10 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||
{ hidden: hiddenMenu }
|
||||
)}
|
||||
>
|
||||
@ -59,7 +67,9 @@ export const Language: React.FC = () => {
|
||||
<li
|
||||
key={index}
|
||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||
onClick={async () => await handleLanguage(language)}
|
||||
onClick={async () => {
|
||||
return await handleLanguage(language)
|
||||
}}
|
||||
>
|
||||
<LanguageFlag language={language} />
|
||||
</li>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import classNames from 'clsx'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
export const SwitchTheme: React.FC = () => {
|
||||
@ -18,109 +19,60 @@ export const SwitchTheme: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='flex items-center'
|
||||
data-cy='switch-theme-click'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
||||
<div className='toggle-track'>
|
||||
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
|
||||
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
|
||||
<div
|
||||
data-cy='switch-theme-dark'
|
||||
className='toggle-track-check absolute'
|
||||
className={classNames(
|
||||
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
|
||||
{
|
||||
'opacity-100': theme === 'dark',
|
||||
'opacity-0': theme === 'light'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className='toggle_Dark relative flex items-center justify-center'>
|
||||
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
||||
🌜
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
data-cy='switch-theme-light'
|
||||
className='toggle-track-x absolute'
|
||||
className={classNames(
|
||||
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
|
||||
{
|
||||
'opacity-100': theme === 'light',
|
||||
'opacity-0': theme === 'dark'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className='toggle_Light relative flex items-center justify-center'>
|
||||
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
||||
🌞
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='toggle-thumb absolute' />
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
|
||||
{
|
||||
'left-[27px]': theme === 'dark',
|
||||
'left-0': theme === 'light'
|
||||
}
|
||||
)}
|
||||
style={{ border: '1px solid #4d4d4d' }}
|
||||
/>
|
||||
<input
|
||||
data-cy='switch-theme-input'
|
||||
type='checkbox'
|
||||
aria-label='Dark mode toggle'
|
||||
className='toggle-screenreader-only absolute overflow-hidden'
|
||||
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.toggle-theme-button {
|
||||
touch-action: pan-x;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle-track {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
background-color: #4d4d4d;
|
||||
transition: all 0.2s ease;
|
||||
color: #fff;
|
||||
}
|
||||
.toggle-track-check {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
left: 8px;
|
||||
opacity: ${theme === 'dark' ? 1 : 0};
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.toggle-track-x {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
right: 10px;
|
||||
opacity: ${theme === 'dark' ? 0 : 1};
|
||||
}
|
||||
.toggle_Dark,
|
||||
.toggle_Light {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
.toggle-thumb {
|
||||
left: ${theme === 'dark' ? '27px' : '0px'};
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #4d4d4d;
|
||||
border-radius: 50%;
|
||||
background-color: #fafafa;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s ease;
|
||||
top: 1px;
|
||||
color: #fff;
|
||||
}
|
||||
.toggle-screenreader-only {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
width: 1px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { Header } from '..'
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('should render', () => {
|
||||
const { getByText } = render(<Header />)
|
||||
expect(getByText('Divlo')).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -14,7 +14,6 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
||||
return (
|
||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||
<Link href='/'>
|
||||
<a>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Image
|
||||
quality={100}
|
||||
@ -22,25 +21,24 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
||||
height={60}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt='Divlo'
|
||||
priority
|
||||
/>
|
||||
<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'>
|
||||
<div className='flex flex-col items-center justify-center px-6'>
|
||||
<Link href='/blog'>
|
||||
<a
|
||||
<Link
|
||||
href='/blog'
|
||||
data-cy='header-blog-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{showLanguage && <Language />}
|
||||
{showLanguage ? <Language /> : null}
|
||||
<SwitchTheme />
|
||||
</div>
|
||||
</header>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
interface InterestItemProps {
|
||||
title: string
|
||||
@ -10,7 +10,7 @@ export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
<li className='interest-item my-2 mx-2 h-8 w-8' title={title}>
|
||||
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}>
|
||||
<FontAwesomeIcon
|
||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
||||
icon={fontAwesomeIcon}
|
||||
|
@ -7,10 +7,7 @@ export const InterestsList: React.FC = () => {
|
||||
return (
|
||||
<div className='my-4 flex justify-center'>
|
||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||
<InterestItem
|
||||
title='Developer Full Stack Junior'
|
||||
fontAwesomeIcon={faCode}
|
||||
/>
|
||||
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
|
||||
<InterestItem
|
||||
title='Passionate about High-Tech'
|
||||
fontAwesomeIcon={faMicrochip}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
||||
import type { InterestParagraphProps } from './InterestParagraph'
|
||||
import { InterestParagraph } from './InterestParagraph'
|
||||
import { InterestsList } from './InterestsList'
|
||||
|
||||
export const Interests: React.FC = () => {
|
||||
|
@ -24,7 +24,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
<div className='flex justify-center'>
|
||||
<Image
|
||||
quality={100}
|
||||
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
||||
width={300}
|
||||
height={300}
|
||||
src={image}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
||||
import type { PortfolioItemProps } from './PortfolioItem'
|
||||
import { PortfolioItem } from './PortfolioItem'
|
||||
|
||||
export const Portfolio: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
@ -4,9 +4,9 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t, lang } = useTranslation()
|
||||
|
||||
return (
|
||||
<p className='mt-8 mb-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||
{t('home:about.description-bottom')}
|
||||
{lang === 'fr' && (
|
||||
{lang === 'fr' ? (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
@ -17,7 +17,7 @@ export const ProfileDescriptionBottom: React.FC = () => {
|
||||
Curriculum vitæ
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
||||
{title}
|
||||
</strong>
|
||||
<span className='ml-0 mb-4 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
||||
{link != null ? (
|
||||
<a
|
||||
className='text-gray hover:underline dark:text-gray-dark'
|
||||
|
@ -1,14 +1,24 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { DIVLO_BIRTHDAY, DIVLO_BIRTHDAY_DATE, getAge } from 'utils/getAge'
|
||||
|
||||
import { ProfileItem } from './ProfileItem'
|
||||
|
||||
export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
const age = useMemo(() => {
|
||||
return getAge(DIVLO_BIRTHDAY)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ul className='m-0 list-none p-0'>
|
||||
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
||||
<ProfileItem title={t('home:about.birth-date')} value='31/03/2003' />
|
||||
<ProfileItem
|
||||
title={t('home:about.birth-date')}
|
||||
value={`${DIVLO_BIRTHDAY_DATE} (${age} ${t('home:about.years-old')})`}
|
||||
/>
|
||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
|
@ -5,7 +5,7 @@ import DivloLogo from 'public/images/divlo_logo.png'
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
return (
|
||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||
<Image quality={100} src={DivloLogo} alt='Divlo' />
|
||||
<Image quality={100} src={DivloLogo} alt='Divlo' priority />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import classNames from 'classnames'
|
||||
import classNames from 'clsx'
|
||||
|
||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
|
@ -3,7 +3,9 @@ interface SocialMediaItemProps {
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
||||
export const SocialMediaItem: React.FC<
|
||||
React.PropsWithChildren<SocialMediaItemProps>
|
||||
> = (props) => {
|
||||
const { link, ariaLabel, children } = props
|
||||
|
||||
return (
|
||||
|
@ -2,10 +2,11 @@ import { useTheme } from 'next-themes'
|
||||
import Image from 'next/image'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type { SkillName } from './skills'
|
||||
import { skills } from './skills'
|
||||
|
||||
export interface SkillComponentProps {
|
||||
skill: string
|
||||
skill: SkillName
|
||||
}
|
||||
|
||||
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
@ -14,10 +15,13 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const image = useMemo(() => {
|
||||
if (typeof skillProperties.image !== 'string') {
|
||||
return skillProperties.image[theme ?? 'light']
|
||||
}
|
||||
if (typeof skillProperties.image === 'string') {
|
||||
return skillProperties.image
|
||||
}
|
||||
if (theme === 'light') {
|
||||
return skillProperties.image.light
|
||||
}
|
||||
return skillProperties.image.dark
|
||||
}, [skillProperties, theme])
|
||||
|
||||
return (
|
||||
@ -28,7 +32,14 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='text-center'>
|
||||
<Image quality={100} width={60} height={60} alt={skill} src={image} />
|
||||
<Image
|
||||
className='inline h-auto w-auto'
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
alt={skill}
|
||||
src={image}
|
||||
/>
|
||||
<p className='mt-1'>{skill}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -13,6 +13,7 @@ export const Skills: React.FC = () => {
|
||||
<SkillComponent skill='TypeScript' />
|
||||
<SkillComponent skill='Python' />
|
||||
<SkillComponent skill='C/C++' />
|
||||
<SkillComponent skill='PHP' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Front-end'>
|
||||
@ -23,11 +24,10 @@ export const Skills: React.FC = () => {
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Back-end'>
|
||||
<SkillComponent skill='Laravel' />
|
||||
<SkillComponent skill='Node.js' />
|
||||
<SkillComponent skill='Fastify' />
|
||||
<SkillComponent skill='Prisma' />
|
||||
<SkillComponent skill='PostgreSQL' />
|
||||
<SkillComponent skill='MySQL' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title={t('home:skills.software-tools')}>
|
||||
|
@ -3,11 +3,7 @@ export interface Skill {
|
||||
image: string | { [key: string]: string }
|
||||
}
|
||||
|
||||
export interface Skills {
|
||||
[key: string]: Skill
|
||||
}
|
||||
|
||||
export const skills: Skills = {
|
||||
export const skills = {
|
||||
JavaScript: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
||||
image: '/images/skills/JavaScript.png'
|
||||
@ -24,6 +20,14 @@ export const skills: Skills = {
|
||||
link: 'https://isocpp.org/',
|
||||
image: '/images/skills/C-Cpp.png'
|
||||
},
|
||||
PHP: {
|
||||
link: 'https://www.php.net/',
|
||||
image: '/images/skills/PHP.png'
|
||||
},
|
||||
Laravel: {
|
||||
link: 'https://laravel.com/',
|
||||
image: '/images/skills/Laravel.png'
|
||||
},
|
||||
Dart: {
|
||||
link: 'https://dart.dev/',
|
||||
image: '/images/skills/Dart.png'
|
||||
@ -107,3 +111,5 @@ export const skills: Skills = {
|
||||
image: '/images/skills/Docker.png'
|
||||
}
|
||||
} as const
|
||||
|
||||
export type SkillName = keyof typeof skills
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { ErrorPage } from '../ErrorPage'
|
||||
|
||||
describe('<ErrorPage />', () => {
|
||||
it('should render the message and statusCode', () => {
|
||||
const messageContent = 'message content'
|
||||
const statusCode = 404
|
||||
const { getByText } = render(
|
||||
<ErrorPage statusCode={statusCode} message={messageContent} />
|
||||
)
|
||||
expect(getByText(messageContent)).toBeInTheDocument()
|
||||
expect(getByText(statusCode)).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,16 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { Footer } from '../Footer'
|
||||
|
||||
describe('<Footer />', () => {
|
||||
it('should render with appropriate link tag version', () => {
|
||||
const version = '1.0.0'
|
||||
const { getByText } = render(<Footer version={version} />)
|
||||
const versionLink = getByText(version) as HTMLAnchorElement
|
||||
expect(getByText('Divlo')).toBeInTheDocument()
|
||||
expect(versionLink).toBeInTheDocument()
|
||||
expect(versionLink.href).toEqual(
|
||||
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
)
|
||||
})
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const RevealFade: React.FC = (props) => {
|
||||
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
|
||||
const { children } = props
|
||||
|
||||
const htmlElement = useRef<HTMLDivElement>(null)
|
||||
|
@ -4,7 +4,7 @@ export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<h2 {...rest} className='mt-1 mb-3 text-center text-4xl font-semibold'>
|
||||
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
|
@ -23,7 +23,9 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
<div className='w-full px-3'>
|
||||
<ShadowContainer style={{ marginTop: 50 }}>
|
||||
<section {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
{heading != null ? (
|
||||
<SectionHeading>{heading}</SectionHeading>
|
||||
) : null}
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
</section>
|
||||
</ShadowContainer>
|
||||
@ -34,7 +36,7 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
if (withoutShadowContainer) {
|
||||
return (
|
||||
<section {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
||||
<div className='w-full px-3'>{children}</div>
|
||||
</section>
|
||||
)
|
||||
@ -42,16 +44,18 @@ export const Section: React.FC<SectionProps> = (props) => {
|
||||
|
||||
return (
|
||||
<section {...rest}>
|
||||
{heading != null && (
|
||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
||||
{heading != null ? (
|
||||
<SectionHeading
|
||||
style={{ ...(description != null ? { margin: 0 } : {}) }}
|
||||
>
|
||||
{heading}
|
||||
</SectionHeading>
|
||||
)}
|
||||
{description != null && (
|
||||
) : null}
|
||||
{description != null ? (
|
||||
<p style={{ marginTop: 7 }} className='text-center'>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
<div className='w-full px-3'>
|
||||
<ShadowContainer>
|
||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import classNames from 'classnames'
|
||||
import classNames from 'clsx'
|
||||
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||
|
||||
|
17
cypress.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
video: false,
|
||||
screenshotOnRunFailure: false,
|
||||
e2e: {
|
||||
baseUrl: 'http://127.0.0.1:3000',
|
||||
supportFile: false
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'next',
|
||||
bundler: 'webpack'
|
||||
}
|
||||
}
|
||||
})
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"pluginsFile": false,
|
||||
"supportFile": false,
|
||||
"fixturesFolder": false,
|
||||
"video": false,
|
||||
"screenshotOnRunFailure": false
|
||||
}
|
16
cypress/component/Footer.cy.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Footer } from '@/components/Footer'
|
||||
|
||||
describe('<Footer />', () => {
|
||||
it('should render with appropriate link tag version', () => {
|
||||
const version = '1.0.0'
|
||||
cy.mount(<Footer version={version} />)
|
||||
cy.contains('Divlo')
|
||||
.get('[data-cy=version-link]')
|
||||
.should('have.text', version)
|
||||
.should(
|
||||
'have.attr',
|
||||
'href',
|
||||
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
)
|
||||
})
|
||||
})
|
17
cypress/component/utils/getAge.cy.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { getAge } from '../../../utils/getAge'
|
||||
|
||||
describe('utils/getAge', () => {
|
||||
it('should calculate the right age of a person', () => {
|
||||
cy.clock(new Date('2018-03-20')).then(() => {
|
||||
const birthDate = new Date('1980-02-20')
|
||||
expect(getAge(birthDate)).equal(38)
|
||||
})
|
||||
})
|
||||
|
||||
it('should calculate the right age of a person (taking into account the months)', () => {
|
||||
cy.clock(new Date('2018-03-20')).then(() => {
|
||||
const birthDate = new Date('1980-07-20')
|
||||
expect(getAge(birthDate)).equal(37)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,5 +1,7 @@
|
||||
describe('Common > Header', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
beforeEach(() => {
|
||||
return cy.visit('/')
|
||||
})
|
||||
|
||||
it('should redirect to /blog on click of the blog link', () => {
|
||||
cy.get('[data-cy=header-blog-link]')
|
||||
@ -56,3 +58,5 @@ describe('Common > Header', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
@ -1,7 +1,11 @@
|
||||
describe('Page /404', () => {
|
||||
beforeEach(() => cy.visit('/404', { failOnStatusCode: false }))
|
||||
beforeEach(() => {
|
||||
return cy.visit('/404', { failOnStatusCode: false })
|
||||
})
|
||||
|
||||
it('should display the statusCode of 404', () => {
|
||||
cy.get('[data-cy=status-code]').contains('404')
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
@ -1,7 +1,11 @@
|
||||
describe('Page /500', () => {
|
||||
beforeEach(() => cy.visit('/500', { failOnStatusCode: false }))
|
||||
beforeEach(() => {
|
||||
return cy.visit('/500', { failOnStatusCode: false })
|
||||
})
|
||||
|
||||
it('should display the statusCode of 500', () => {
|
||||
cy.get('[data-cy=status-code]').contains('500')
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
@ -11,3 +11,5 @@ describe('Page /blog/[slug]', () => {
|
||||
cy.get('[data-cy=status-code]').contains('404')
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
@ -20,3 +20,5 @@ describe('Page /blog', () => {
|
||||
.should('eq', '/blog/hello-world')
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
@ -1,13 +1,10 @@
|
||||
describe('Page /', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
beforeEach(() => {
|
||||
return cy.visit('/')
|
||||
})
|
||||
|
||||
it('should reveals the sections while scrolling except the about section', () => {
|
||||
const sectionsReveals = [
|
||||
'#interests',
|
||||
'#skills',
|
||||
'#portfolio',
|
||||
'#open-source'
|
||||
]
|
||||
const sectionsReveals = ['#interests', '#skills', '#portfolio']
|
||||
cy.get('#about').should('be.visible')
|
||||
for (const section of sectionsReveals) {
|
||||
cy.get(section)
|
||||
@ -17,3 +14,5 @@ describe('Page /', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
3
cypress/support/commands.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export {}
|
14
cypress/support/component-index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Components App</title>
|
||||
<!-- Used by Next.js to inject CSS. -->
|
||||
<div id="__next_css__DO_NOT_USE__"></div>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
14
cypress/support/component.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { mount } from 'cypress/react'
|
||||
|
||||
import './commands'
|
||||
import '../../styles/global.css'
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["cypress"],
|
||||
"isolatedModules": false
|
||||
},
|
||||
"include": ["../node_modules/cypress", "./**/*.ts"]
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
version: '3.0'
|
||||
services:
|
||||
divlo.fr:
|
||||
divlo:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
image: 'divlo.fr'
|
||||
image: 'divlo'
|
||||
build:
|
||||
context: './'
|
||||
ports:
|
||||
- '${PORT}:${PORT}'
|
||||
- '${PORT-3000}:${PORT-3000}'
|
||||
environment:
|
||||
PORT: ${PORT}
|
||||
env_file: './.env'
|
||||
PORT: ${PORT-3000}
|
||||
env_file: '.env'
|
||||
|
@ -1,14 +0,0 @@
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest()
|
||||
const customJestConfig = {
|
||||
moduleDirectories: ['node_modules', './'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/cypress'],
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: [
|
||||
'@testing-library/jest-dom/extend-expect',
|
||||
'@testing-library/react'
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = createJestConfig(customJestConfig)
|
22
jsonresume-theme-custom/.gitignore
vendored
@ -1,4 +1,22 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
theme/index.html
|
||||
dist
|
||||
.parcel-cache
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
20
jsonresume-theme-custom/build.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { build } from 'vite'
|
||||
|
||||
const jsonResumeThemeCustom = new URL('./', import.meta.url)
|
||||
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
|
||||
const publicResumeOutputURL = new URL(
|
||||
'../public/curriculum-vitae',
|
||||
import.meta.url
|
||||
)
|
||||
|
||||
await build({
|
||||
root: fileURLToPath(jsonResumeThemeCustom),
|
||||
base: '/curriculum-vitae/'
|
||||
})
|
||||
|
||||
await fs.promises.cp(jsonResumeThemeCustomDist, publicResumeOutputURL, {
|
||||
recursive: true
|
||||
})
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |
244
jsonresume-theme-custom/index.html
Normal file
@ -0,0 +1,244 @@
|
||||
<!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 %>" />
|
||||
<link rel="stylesheet" href="./styles/global.css" />
|
||||
<script defer type="module" src="./scripts/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row main clearfix">
|
||||
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
|
||||
<div class="card profile-card">
|
||||
<div class="profile-pic-container">
|
||||
<div class="profile-pic">
|
||||
<img
|
||||
class="media-object img-circle center-block"
|
||||
data-src="holder.js/100x100"
|
||||
alt="<%= locals.basics.name %>"
|
||||
src="<%= locals.basics.image %>"
|
||||
/>
|
||||
</div>
|
||||
<div class="name-and-profession text-center">
|
||||
<h3>
|
||||
<strong><%= locals.basics.name %></strong>
|
||||
</h3>
|
||||
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
||||
<h5 class="text-muted">
|
||||
<%= locals.basics.age %> (<span id="year-old"></span> ans)
|
||||
</h5>
|
||||
<h5 class="text-muted">
|
||||
<%= locals.basics.location.address %>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-details clearfix">
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a
|
||||
class="link-disguise"
|
||||
href="mailto:<%= locals.basics.email %>"
|
||||
>
|
||||
<%= locals.basics.email %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a class="link-disguise" href="<%= locals.basics.url %>">
|
||||
<%= locals.basics.url %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="card background-card">
|
||||
<div class="background-details">
|
||||
<div class="detail" id="about">
|
||||
<div class="icon">
|
||||
<img src="./images/user.svg" alt="user" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">À propos</h4>
|
||||
<div class="card card-nested">
|
||||
<div class="content mop-wrapper">
|
||||
<p><%- locals.basics.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<section class="section-separated">
|
||||
<div class="detail" id="education">
|
||||
<div class="icon">
|
||||
<img src="./images/graduation-cap.svg" alt="graduation" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Formations</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.education.forEach((degree) => { %>
|
||||
<li class="card card-nested">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.studyType %></strong>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.score %></strong>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<%= degree.institution %>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<small>
|
||||
<%= degree.startDate %> <%= degree.endDate !=
|
||||
null ? " - " + degree.endDate : "" %>
|
||||
</small>
|
||||
</p>
|
||||
<% if (degree.courses != null) { %>
|
||||
<ul class="education-courses">
|
||||
<% degree.courses.forEach((course) => { %>
|
||||
<li><%= course %></li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail" id="skills">
|
||||
<div class="icon">
|
||||
<img src="./images/toolbox.svg" alt="toolbox" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Compétences</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.skills.forEach((skill) => { %>
|
||||
<li class="card card-nested card-skills">
|
||||
<div class="skill-info">
|
||||
<strong><%= skill.name %></strong>
|
||||
<div class="space-top labels">
|
||||
<% skill.keywords.forEach((keyword) => { %>
|
||||
<p class="label label-keyword"><%= keyword %></p>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section class="section-separated">
|
||||
<div class="detail" id="work-experience">
|
||||
<div class="icon">
|
||||
<img src="./images/building-columns.svg" alt="work" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Expériences</h4>
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.work.filter((experience) =>
|
||||
experience.description == null).forEach((experience) => {
|
||||
%>
|
||||
<li class="card card-nested clearfix">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<a href="<%= experience.website %>">
|
||||
<strong><%= experience.name %></strong>
|
||||
</a>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%- experience.position %></strong>
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<span class="space-right">
|
||||
<%= date.format(new Date(experience.startDate),
|
||||
'DD/MM/YYYY') %> - <%= date.format(new
|
||||
Date(experience.endDate), 'DD/MM/YYYY') %> (<%=
|
||||
experience.duration %>)
|
||||
</span>
|
||||
</small>
|
||||
</p>
|
||||
<div class="experience-description">
|
||||
<p><%- experience.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail" id="interests">
|
||||
<div class="icon">
|
||||
<img src="./images/heart.svg" alt="heart" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Intérets</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.interests.forEach((interest) => { %>
|
||||
<li class="card card-nested">
|
||||
<p><strong><%= interest.name %></strong></p>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.work.filter((experience) =>
|
||||
experience.description != null).forEach((experience) =>
|
||||
{ %>
|
||||
<li class="card card-nested clearfix">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<a href="<%= experience.website %>">
|
||||
<strong><%= experience.name %></strong>
|
||||
</a>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%- experience.position %></strong>
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<span class="space-right">
|
||||
<%= date.format(new
|
||||
Date(experience.startDate), 'DD/MM/YYYY') %> -
|
||||
<%= date.format(new Date(experience.endDate),
|
||||
'DD/MM/YYYY') %> (<%= experience.duration %>)
|
||||
</span>
|
||||
</small>
|
||||
</p>
|
||||
<div class="experience-description">
|
||||
<p><%- experience.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,32 +0,0 @@
|
||||
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
|
||||
}
|
5447
jsonresume-theme-custom/package-lock.json
generated
@ -2,17 +2,20 @@
|
||||
"name": "jsonresume-theme-custom",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"scripts": {},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-and-time": "2.1.2",
|
||||
"ejs": "3.1.6",
|
||||
"modern-normalize": "1.1.0"
|
||||
"jsonc-parser": "3.2.0",
|
||||
"modern-normalize": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/config-default": "2.3.2",
|
||||
"@parcel/core": "2.3.2",
|
||||
"@parcel/optimizer-data-url": "^2.3.2",
|
||||
"@parcel/transformer-inline-string": "^2.3.2",
|
||||
"parcel": "2.3.2"
|
||||
"@types/node": "20.2.1",
|
||||
"date-and-time": "3.0.0",
|
||||
"vite": "4.3.8",
|
||||
"vite-plugin-html": "3.2.0"
|
||||
}
|
||||
}
|
||||
|
5
jsonresume-theme-custom/scripts/main.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { DIVLO_BIRTHDAY, getAge } from '../../utils/getAge.ts'
|
||||
|
||||
const yearOld = document.getElementById('year-old')
|
||||
|
||||
yearOld.textContent = getAge(DIVLO_BIRTHDAY).toString()
|
@ -1,4 +1,4 @@
|
||||
@import 'npm:modern-normalize/modern-normalize.css';
|
||||
@import 'modern-normalize/modern-normalize.css';
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
||||
@ -209,7 +209,6 @@ h5 {
|
||||
font-size: 75%;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
@ -217,8 +216,6 @@ h5 {
|
||||
}
|
||||
.label-keyword {
|
||||
display: inline-block;
|
||||
background: #7eb0db;
|
||||
color: white;
|
||||
font-size: 0.9em;
|
||||
padding: 5px;
|
||||
border: 1px solid #357ebd;
|
||||
@ -227,3 +224,6 @@ h5 {
|
||||
.label-keyword p {
|
||||
margin: 0;
|
||||
}
|
||||
.section-separated {
|
||||
display: flex;
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= locals.basics.name %></title>
|
||||
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
||||
|
||||
<style>
|
||||
@import './styles/global.css';
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row main clearfix">
|
||||
<section class="col-md-3 card-wrapper profile-card-wrapper affix">
|
||||
<div class="card profile-card">
|
||||
<div class="profile-pic-container">
|
||||
<div class="profile-pic">
|
||||
<img
|
||||
class="media-object img-circle center-block"
|
||||
data-src="holder.js/100x100"
|
||||
alt="<%= locals.basics.name %>"
|
||||
src="<%= locals.basics.image %>"
|
||||
/>
|
||||
</div>
|
||||
<div class="name-and-profession text-center">
|
||||
<h3>
|
||||
<strong><%= locals.basics.name %></strong>
|
||||
</h3>
|
||||
<h5 class="text-muted"><%= locals.basics.label %></h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-details clearfix">
|
||||
<div class="detail">
|
||||
<span class="info"><%= locals.basics.phone %></span>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a
|
||||
class="link-disguise"
|
||||
href="mailto:<%= locals.basics.email %>"
|
||||
>
|
||||
<%= locals.basics.email %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="info">
|
||||
<a class="link-disguise" href="<%= locals.basics.url %>">
|
||||
<%= locals.basics.url %>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="card background-card">
|
||||
<div class="background-details">
|
||||
<div class="detail" id="about">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/user.svg" alt="user" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">À propos</h4>
|
||||
<div class="card card-nested">
|
||||
<div class="content mop-wrapper">
|
||||
<p><%- locals.basics.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="work-experience">
|
||||
<div class="icon">
|
||||
<img
|
||||
src="data-url:./images/building-columns.svg"
|
||||
alt="work"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Expériences</h4>
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.work.forEach((experience) => { %>
|
||||
<li class="card card-nested clearfix">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<a href="<%= experience.website %>">
|
||||
<strong><%= experience.name %></strong>
|
||||
</a>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%- experience.position %></strong>
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<span class="space-right">
|
||||
<%= date.format(new Date(experience.startDate),
|
||||
'DD/MM/YYYY') %> - <%= date.format(new
|
||||
Date(experience.endDate), 'DD/MM/YYYY') %>
|
||||
</span>
|
||||
</small>
|
||||
</p>
|
||||
<div class="experience-description">
|
||||
<p><%- experience.summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="skills">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/toolbox.svg" alt="toolbox" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Compétences</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.skills.forEach((skill) => { %>
|
||||
<li class="card card-nested card-skills">
|
||||
<div class="skill-info">
|
||||
<strong><%= skill.name %></strong>
|
||||
<div class="space-top labels">
|
||||
<% skill.keywords.forEach((keyword) => { %>
|
||||
<p class="label label-keyword"><%= keyword %></p>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="education">
|
||||
<div class="icon">
|
||||
<img
|
||||
src="data-url:./images/graduation-cap.svg"
|
||||
alt="graduation"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Éducation</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.education.forEach((degree) => { %>
|
||||
<li class="card card-nested">
|
||||
<div class="content">
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.studyType %></strong>
|
||||
</p>
|
||||
<p class="clear-margin relative">
|
||||
<strong><%= degree.score %></strong>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<%= degree.institution %>
|
||||
</p>
|
||||
<p class="text-muted clear-margin">
|
||||
<small>
|
||||
<%= degree.startDate %> - <%= degree.endDate %>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="detail" id="interests">
|
||||
<div class="icon">
|
||||
<img src="data-url:./images/heart.svg" alt="heart" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="title text-uppercase">Intérets</h4>
|
||||
<div class="content">
|
||||
<ul class="list-unstyled clear-margin">
|
||||
<% locals.interests.forEach((interest) => { %>
|
||||
<li class="card card-nested">
|
||||
<p><strong><%= interest.name %></strong></p>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
36
jsonresume-theme-custom/vite.config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import { parse as JSONCParser } from 'jsonc-parser'
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import date from 'date-and-time'
|
||||
|
||||
const jsonResumeURL = new URL('../resume.jsonc', import.meta.url)
|
||||
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
const resume = JSONCParser(dataResumeStringJSON)
|
||||
|
||||
/**
|
||||
* Documentation: <https://vitejs.dev/config/>
|
||||
*/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
assetsDir: './'
|
||||
},
|
||||
plugins: [
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
date,
|
||||
locals: {
|
||||
...resume
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
css: {
|
||||
postcss: {}
|
||||
}
|
||||
})
|
@ -1,26 +1,27 @@
|
||||
{
|
||||
"about": {
|
||||
"i-am": "I am",
|
||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
||||
"description": "Developer Full Stack • Open-Source enthusiast",
|
||||
"full-name": "Full name",
|
||||
"birth-date": "Birth date",
|
||||
"years-old": "years old",
|
||||
"nationality": "Nationality",
|
||||
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (first year)."
|
||||
"description-bottom": "I am a student in computer science following the French training \"BUT Informatique\" and I am also a self-taught."
|
||||
},
|
||||
"interests": {
|
||||
"title": "Interests",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Developer Full Stack Junior",
|
||||
"title": "Developer Full Stack",
|
||||
"description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming in others programming language too."
|
||||
},
|
||||
{
|
||||
"title": "Open-Source enthusiast",
|
||||
"description": "For me, everyone should work, solve problems, build things and think together.<br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||
},
|
||||
{
|
||||
"title": "Passionate about High-Tech",
|
||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||
},
|
||||
{
|
||||
"title": "Open-Source enthusiast",
|
||||
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1,26 +1,27 @@
|
||||
{
|
||||
"about": {
|
||||
"i-am": "Je suis",
|
||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
|
||||
"full-name": "Prénom NOM",
|
||||
"birth-date": "Date de naissance",
|
||||
"years-old": "ans",
|
||||
"nationality": "Nationalité",
|
||||
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année)."
|
||||
"description-bottom": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne."
|
||||
},
|
||||
"interests": {
|
||||
"title": "Intérêts",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Développeur Full Stack Junior",
|
||||
"title": "Développeur Full Stack",
|
||||
"description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi dans d'autres langages de programmation."
|
||||
},
|
||||
{
|
||||
"title": "Enthousiaste de l'Open-Source",
|
||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>GitHub</a>."
|
||||
},
|
||||
{
|
||||
"title": "Passionné de High-Tech",
|
||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||
},
|
||||
{
|
||||
"title": "Enthousiaste de l'Open-Source",
|
||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a class='text-yellow dark:text-yellow-dark hover:underline' href='https://github.com/Divlo/Divlo' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -1,41 +1,13 @@
|
||||
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: {
|
||||
const nextPWA = require('next-pwa')({
|
||||
disable: process.env.NODE_ENV !== 'production',
|
||||
dest: 'public'
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: createSecureHeaders({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
'data:',
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ['*', 'data:', 'blob:'],
|
||||
mediaSrc: "'none'",
|
||||
connectSrc: '*',
|
||||
objectSrc: "'none'",
|
||||
fontSrc: "'self'",
|
||||
baseURI: "'none'"
|
||||
}
|
||||
}
|
||||
})
|
||||
const nextTranslate = require('next-translate-plugin')
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
module.exports = nextTranslate(nextPWA(nextConfig))
|
||||
|
49112
package-lock.json
generated
145
package.json
@ -1,106 +1,101 @@
|
||||
{
|
||||
"name": "divlo",
|
||||
"version": "2.0.2",
|
||||
"version": "2.7.1",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Divlo/Divlo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"start": "next start",
|
||||
"build": "npm run resume:export && next build",
|
||||
"export": "next export",
|
||||
"build": "npm run resume:build && next build",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:markdown": "markdownlint \"**/*.{md,mdx}\" --dot --ignore-path \".gitignore\"",
|
||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:prettier": "prettier \".\" --check",
|
||||
"lint:markdown": "markdownlint-cli2",
|
||||
"lint:eslint": "eslint \".\" --ignore-path \".gitignore\"",
|
||||
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||
"lint:staged": "lint-staged",
|
||||
"test:unit": "jest",
|
||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
||||
"test:lighthouse": "lhci autorun",
|
||||
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
||||
"test:e2e:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
||||
"resume:validate": "resume validate",
|
||||
"resume:serve": "resume serve --theme \"custom\"",
|
||||
"resume:export": "resume export \"./public/curriculum-vitae.html\" --format \"html\" --theme \"custom\"",
|
||||
"test:unit": "cypress run --component",
|
||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"",
|
||||
"test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
|
||||
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"",
|
||||
"resume:build": "node ./jsonresume-theme-custom/build.js",
|
||||
"release": "semantic-release",
|
||||
"deploy": "vercel",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/montserrat": "4.5.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.0.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.17",
|
||||
"classnames": "2.3.1",
|
||||
"date-and-time": "2.1.2",
|
||||
"@fontsource/montserrat": "5.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@giscus/react": "2.2.8",
|
||||
"clsx": "1.2.1",
|
||||
"date-and-time": "3.0.0",
|
||||
"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": "7.1.0",
|
||||
"html-react-parser": "3.0.16",
|
||||
"katex": "0.16.7",
|
||||
"next": "13.4.3",
|
||||
"next-mdx-remote": "4.4.1",
|
||||
"next-pwa": "5.6.0",
|
||||
"next-themes": "0.2.1",
|
||||
"next-translate": "2.0.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"read-pkg": "8.0.0",
|
||||
"rehype-katex": "6.0.3",
|
||||
"rehype-raw": "6.1.1",
|
||||
"rehype-slug": "5.0.1",
|
||||
"rehype-slug": "5.1.0",
|
||||
"remark-gfm": "3.0.1",
|
||||
"sharp": "0.30.1",
|
||||
"shiki": "0.10.1",
|
||||
"unified": "10.1.1",
|
||||
"unist-util-visit": "4.1.0",
|
||||
"remark-math": "5.1.1",
|
||||
"sharp": "0.32.1",
|
||||
"shiki": "0.14.2",
|
||||
"unified": "10.1.2",
|
||||
"unist-util-visit": "4.1.2",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "16.2.1",
|
||||
"@commitlint/config-conventional": "16.2.1",
|
||||
"@lhci/cli": "0.9.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.1",
|
||||
"@commitlint/cli": "17.6.3",
|
||||
"@commitlint/config-conventional": "17.6.3",
|
||||
"@saithodev/semantic-release-backmerge": "3.2.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@tailwindcss/typography": "0.5.2",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.3",
|
||||
"@types/date-and-time": "0.13.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "17.0.19",
|
||||
"@types/react": "17.0.39",
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"@tsconfig/strictest": "2.0.1",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/react": "18.2.6",
|
||||
"@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": "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",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.6",
|
||||
"autoprefixer": "10.4.14",
|
||||
"cypress": "12.12.0",
|
||||
"editorconfig-checker": "5.0.1",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-conventions": "9.0.0",
|
||||
"eslint-config-next": "13.4.3",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-unicorn": "47.0.0",
|
||||
"html-w3c-validator": "1.3.0",
|
||||
"husky": "8.0.3",
|
||||
"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"
|
||||
"lint-staged": "13.2.2",
|
||||
"markdownlint-cli2": "0.7.1",
|
||||
"markdownlint-rule-relative-links": "1.2.0",
|
||||
"next-translate-plugin": "2.0.5",
|
||||
"postcss": "8.4.23",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-tailwindcss": "0.3.0",
|
||||
"semantic-release": "21.0.2",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "5.0.4",
|
||||
"vercel": "29.3.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,32 @@
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import type { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
||||
import type { FooterProps } from 'components/Footer'
|
||||
|
||||
interface Error404Props extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
interface Error404Props extends FooterProps {}
|
||||
|
||||
const Error404: NextPage<Error404Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version, description } = props
|
||||
const { version } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='404 | Divlo' description={description} />
|
||||
|
||||
<Header showLanguage />
|
||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||
<ErrorPage statusCode={404} message={t('errors:not-found')} />
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
<Head title='404 | Divlo' />
|
||||
<ErrorPage
|
||||
statusCode={404}
|
||||
message={t('errors:not-found')}
|
||||
version={version}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
export const getStaticProps: GetStaticProps<Error404Props> = async () => {
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
return { props: { version } }
|
||||
}
|
||||
|
||||
export default Error404
|
||||
|
@ -1,38 +1,32 @@
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import type { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
||||
import type { FooterProps } from 'components/Footer'
|
||||
|
||||
interface Error500Props extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
interface Error500Props extends FooterProps {}
|
||||
|
||||
const Error500: NextPage<Error500Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version, description } = props
|
||||
const { version } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='500 | Divlo' description={description} />
|
||||
|
||||
<Header showLanguage />
|
||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||
<ErrorPage statusCode={500} message={t('errors:server-error')} />
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
<Head title='500 | Divlo' />
|
||||
<ErrorPage
|
||||
statusCode={500}
|
||||
message={t('errors:server-error')}
|
||||
version={version}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
export const getStaticProps: GetStaticProps<Error500Props> = async () => {
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
return { props: { version } }
|
||||
}
|
||||
|
||||
export default Error500
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AppProps } from 'next/app'
|
||||
import type { AppType } from 'next/app'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import UniversalCookie from 'universal-cookie'
|
||||
@ -13,7 +13,7 @@ const universalCookie = new UniversalCookie()
|
||||
/** how long in seconds, until the cookie expires (10 years) */
|
||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||
|
||||
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const Application: AppType = ({ Component, pageProps }) => {
|
||||
const { lang } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||
import Image from 'next/image'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import date from 'date-and-time'
|
||||
import Giscus from '@giscus/react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer, FooterProps } from 'components/Footer'
|
||||
import type { FooterProps } from 'components/Footer'
|
||||
import { Footer } from 'components/Footer'
|
||||
import type { Post } from 'utils/blog'
|
||||
|
||||
interface BlogPostPageProps extends FooterProps {
|
||||
@ -14,6 +20,8 @@ interface BlogPostPageProps extends FooterProps {
|
||||
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||
const { version, post } = props
|
||||
|
||||
const { theme = 'dark' } = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head
|
||||
@ -22,18 +30,34 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||
/>
|
||||
|
||||
<Header />
|
||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
||||
<div className='my-10 flex flex-col items-center'>
|
||||
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
|
||||
<div className='my-10 flex flex-col items-center text-center'>
|
||||
<h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
|
||||
<p className='mt-2' data-cy='blog-post-date'>
|
||||
{date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='prose mb-10 px-8'>
|
||||
<div className='prose mb-10'>
|
||||
<div className='px-8'>
|
||||
<MDXRemote
|
||||
{...post.source}
|
||||
components={{
|
||||
a: (props: React.ComponentPropsWithoutRef<'a'>) => {
|
||||
img: (properties) => {
|
||||
const { src = '', alt = 'Blog Image' } = properties
|
||||
const source = src.replace('../public/', '/')
|
||||
return (
|
||||
<span className='flex flex-col items-center justify-center'>
|
||||
<Image
|
||||
src={source}
|
||||
alt={alt}
|
||||
width={1000}
|
||||
height={1000}
|
||||
className='h-auto w-auto'
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
a: (props) => {
|
||||
if (props.href?.startsWith('#') ?? false) {
|
||||
return <a {...props} />
|
||||
}
|
||||
@ -43,6 +67,21 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Giscus
|
||||
id='comments'
|
||||
repo='Divlo/Divlo'
|
||||
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
||||
category='General'
|
||||
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
||||
mapping='pathname'
|
||||
reactionsEnabled='1'
|
||||
emitMetadata='0'
|
||||
inputPosition='top'
|
||||
theme={theme}
|
||||
lang='en'
|
||||
loading='lazy'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
@ -53,7 +92,7 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
|
||||
context
|
||||
) => {
|
||||
const slug = context?.params?.slug
|
||||
const slug = context?.params?.['slug']
|
||||
const { getPostBySlug } = await import('utils/blog')
|
||||
const post = await getPostBySlug(slug)
|
||||
if (post == null || (post != null && !post.frontmatter.isPublished)) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import type { 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 type { FooterProps } from 'components/Footer'
|
||||
import { Footer } from 'components/Footer'
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import type { PostMetadata } from 'utils/blog'
|
||||
|
||||
@ -38,8 +39,12 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
|
||||
'DD/MM/YYYY'
|
||||
)
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} key={index} locale='en'>
|
||||
<a data-cy={post.slug}>
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
key={index}
|
||||
locale='en'
|
||||
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'
|
||||
@ -54,7 +59,6 @@ const BlogPage: NextPage<BlogPageProps> = (props) => {
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
</ShadowContainer>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import type { GetStaticProps, NextPage } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { RevealFade } from 'components/design/RevealFade'
|
||||
@ -11,20 +11,18 @@ import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
||||
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'
|
||||
import type { FooterProps } from 'components/Footer'
|
||||
import { Footer } from 'components/Footer'
|
||||
|
||||
interface HomeProps extends FooterProps {
|
||||
description: string
|
||||
}
|
||||
interface HomeProps extends FooterProps {}
|
||||
|
||||
const Home: NextPage<HomeProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version, description } = props
|
||||
const { version } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head description={description} />
|
||||
<Head />
|
||||
|
||||
<Header showLanguage />
|
||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||
@ -74,11 +72,10 @@ const Home: NextPage<HomeProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
||||
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
const description = getDefaultDescription()
|
||||
return { props: { version, description } }
|
||||
return { props: { version } }
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
@ -77,8 +77,8 @@ setTimeout(restart, 86400000)
|
||||
##### Example (good way)
|
||||
|
||||
```typescript
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
|
||||
setTimeout(restart, MILLISECONDS_IN_A_DAY)
|
||||
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000
|
||||
setTimeout(restart, MILLISECONDS_IN_ONE_DAY)
|
||||
```
|
||||
|
||||
---
|
||||
@ -131,7 +131,9 @@ const printCar = (car: Car): void => {
|
||||
|
||||
---
|
||||
|
||||
#### Boolean names (Prefix: is, has, can)
|
||||
#### Boolean names
|
||||
|
||||
The name of a boolean variable should be a question, and the answer should be true or false. We can use prefixes like `is`, `has`, `can` to make it more explicit and we should avoid negation.
|
||||
|
||||
##### Example (bad way)
|
||||
|
||||
@ -171,7 +173,10 @@ We have to keep it as simple as possible, not to implement features that are not
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const createFile = async (name: string, isTemporary: boolean = false) => {
|
||||
const createFile = async (
|
||||
name: string,
|
||||
isTemporary: boolean = false
|
||||
): Promise<void> => {
|
||||
if (isTemporary) {
|
||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
||||
}
|
||||
@ -187,11 +192,11 @@ const createFile = async (name: string, isTemporary: boolean = false) => {
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const createFile = async (name: string) => {
|
||||
const createFile = async (name: string): Promise<void> => {
|
||||
await fs.promises.writeFile(name, '')
|
||||
}
|
||||
|
||||
const createTemporaryFile = async (name: string) => {
|
||||
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||
await createFile(path.join('temporary', name))
|
||||
}
|
||||
```
|
239
posts/git-ultimate-guide.md
Normal file
@ -0,0 +1,239 @@
|
||||
---
|
||||
title: '🗓️ Git version control: Ultimate Guide'
|
||||
description: 'What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow.'
|
||||
isPublished: true
|
||||
publishedOn: '2022-10-27T14:33:07.465Z'
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
Welcome to the Ultimate Guide to master `git` in your daily workflow, we will see what are the most used commands, what are the best practices, and tips and tricks.
|
||||
|
||||
This guide is a summary of the most important things to know when working with `git`, and in general, will link to the official documentation of `git` or other resources for more details, it is on purpose to not go in depth in each topic, it allows to summarize `git` and vocabulary about it (you can use it as a `git` cheatsheet).
|
||||
|
||||
**Note:** Sources used to write this blog post are available at the [end of this post](#sources).
|
||||
|
||||
## Introduction
|
||||
|
||||
**Git** is a free and open-source distributed **version control system** for keeping track of changes across a set of files.
|
||||
|
||||
Git was originally authored by [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds) in 2005 for the development of the [Linux kernel](https://kernel.org/).
|
||||
|
||||
Git allows:
|
||||
|
||||
- to be able to work with several people on the same codebase.
|
||||
- track changes to know who did what and when.
|
||||
- revert changes.
|
||||
|
||||
Git is **decentralized**, which means that every developer has a full copy of the repository and the complete history of the project.
|
||||
|
||||
## Get started with `git` and `.gitconfig` config file
|
||||
|
||||
The first thing you should do when you install Git is to set your user name and email address.
|
||||
|
||||
```sh
|
||||
git config --global user.name "Username"
|
||||
git config --global user.email "email@example.com"
|
||||
```
|
||||
|
||||
These configurations are stored in the `.gitconfig` file in your home directory (e.g: `~/.gitconfig`) with this format:
|
||||
|
||||
```sh
|
||||
[user]
|
||||
name = Username
|
||||
email = email@example.com
|
||||
```
|
||||
|
||||
You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config).
|
||||
|
||||
## How `git` works?
|
||||
|
||||
Each `git` project is called a **repository** (or **repo** for short) and it contains all the files and folders for a project, as well as each file's revision history (**commits**) stored in the `.git` folder.
|
||||
|
||||
The history of a repository is represented by a graph.
|
||||
|
||||
Each node is called commit and contains:
|
||||
|
||||
- an instantaneous view (snapshot) of the state of the repository at a specific moment
|
||||
- metadata: message, author, creation date, etc.
|
||||
|
||||
Commits are **snapshots** (not diffs on each file) of the project at specific moments in time.
|
||||
|
||||
There are several areas where the files in your project will live in Git:
|
||||
|
||||
- **Working directory**: the files that you see in your computer's file system.
|
||||
- **Staging area**: the files that will go into your next commit (files added with `git add <filename>` command).
|
||||
- **Local repository**: the `.git` directory, which contains all of your project's commits, branches, etc. (files added with `git commit -m "message"` command).
|
||||
- **Remote repository**: the `.git` directory in a remote server (files added with `git push` command).
|
||||
|
||||
## Commands cheatsheet
|
||||
|
||||
You can find the official documentation of `git` commands at [git-scm.com/docs](https://git-scm.com/docs).
|
||||
|
||||
```sh
|
||||
# Initialize a new git repository
|
||||
git init
|
||||
|
||||
# Clone a repository
|
||||
git clone <url>
|
||||
|
||||
# Add all the files to staging area
|
||||
git add .
|
||||
|
||||
# Add specific file to staging area
|
||||
git add <file>
|
||||
|
||||
# Commit changes
|
||||
git commit -m "chore: initial commit"
|
||||
|
||||
# Add remote repository
|
||||
git remote add <remote> <url>
|
||||
# The main <remote> is often called `origin`
|
||||
|
||||
# Add forked repository
|
||||
git remote add <remote> <url>
|
||||
# The forked <remote> is often called `upstream`
|
||||
|
||||
# List all the remotes
|
||||
git remote
|
||||
|
||||
# Sync forked repository
|
||||
git fetch <remote>
|
||||
git merge <remote>/<branch>
|
||||
|
||||
# Push changes to remote repository
|
||||
git push <remote>
|
||||
|
||||
# Pull changes from remote repository
|
||||
git pull <remote>
|
||||
|
||||
# Show the status of the working tree
|
||||
git status
|
||||
|
||||
# Show the commit history
|
||||
git log
|
||||
|
||||
# Create a new branch
|
||||
git checkout -b <branch>
|
||||
|
||||
# Switch to a branch (or tag or commit)
|
||||
git checkout <branch>
|
||||
|
||||
# Merge a branch into the current branch
|
||||
git merge <branch>
|
||||
|
||||
# Delete a branch
|
||||
git branch --delete <branch>
|
||||
git push <remote> --delete <branch>
|
||||
|
||||
# Fetch branches from remote repository and prune
|
||||
git fetch --prune
|
||||
|
||||
# Revert a commit
|
||||
git revert <commit>
|
||||
|
||||
# Change several past commits (interactive rebase)
|
||||
# HEAD points to the current consulted commit.
|
||||
git rebase --interactive HEAD~<number-of-commits>
|
||||
|
||||
# Reset the current branch, delete all commits since <branch> (without removing the changes)
|
||||
git reset --soft <branch>
|
||||
|
||||
# Apply the changes introduced by some existing commits
|
||||
git cherry-pick <commit>
|
||||
```
|
||||
|
||||
## `.gitignore` file
|
||||
|
||||
The `.gitignore` file is a text file that tells `git` which files (or patterns) it should ignore.
|
||||
|
||||
The `.gitignore` file is usually placed in the root directory of the repository.
|
||||
|
||||
We usually ignore files that are generated by the build process or files that contain sensitive information.
|
||||
|
||||
Example of `.gitignore` file:
|
||||
|
||||
```sh
|
||||
.env
|
||||
build
|
||||
*.exe
|
||||
```
|
||||
|
||||
## `.gitkeep` file
|
||||
|
||||
The `.gitkeep` file is a file that is used to keep an empty directory in a Git repository.
|
||||
|
||||
This is useful when you want to keep an empty directory in your repository but you don't want to commit any file inside it.
|
||||
|
||||
## Git remote repositories (GitHub/GitLab)
|
||||
|
||||
Once you are ready to share your code over the internet, you will need to create a remote repository on a service like [GitHub](https://github.com) or [GitLab](https://gitlab.com).
|
||||
|
||||
There are many other services, you can also self-host your own Git server.
|
||||
|
||||
### SSH vs HTTPS authentication
|
||||
|
||||
Once you have created a remote repository, you will need to authenticate to push and pull changes.
|
||||
|
||||
There are two main ways to authenticate:
|
||||
|
||||
- **SSH**: you will need to generate an SSH key pair and add the public key to your remote repository.
|
||||
- **HTTPS**: you will need to provide your username and password each time you push or pull changes.
|
||||
|
||||
SSH authentication is the recommended way to authenticate to a remote repository.
|
||||
|
||||
You can find more information about SSH authentication in the [official documentation](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key).
|
||||
|
||||
### Sign `git` commits with `gpg`
|
||||
|
||||
As we have seen in the [Get started with `git` and `.gitconfig` config file](#get-started-with-git-and-gitconfig-config-file) section, we can configure `git` with a name and email address with a value of our choice.
|
||||
|
||||
That means that **anyone can create a commit with any name and email address and claim to be whoever they want** when they create a commit.
|
||||
|
||||
To avoid this, you can sign your commits with a <abbr title="GNU Privacy Guard">[GPG](https://gnupg.org/)</abbr> key.
|
||||
|
||||
You can find more information about signing commits in the [official documentation](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work).
|
||||
|
||||
### Continous Integration/Continuous Delivery (CI/CD)
|
||||
|
||||
Once you have your code in a remote repository, everyone (with access) can potentially start contributing to the project. This is great, but it also means that you need to have a way to ensure that your code is working as expected for each change in the project.
|
||||
|
||||
You could do it manually, depending on the size and the complexity of the project, but it could be a tedious task.
|
||||
|
||||
Instead, you can use a **Continuous Integration** (CI) service to automate the process of testing your code, running linting, unit tests, e2e tests, etc.
|
||||
|
||||
There are many CI services, but the most popular ones are [GitHub Actions](https://github.com/features/actions), [GitLab CI](https://docs.gitlab.com/ee/ci/), [CircleCI](https://circleci.com/), [Travis CI](https://travis-ci.org/), and many others...
|
||||
|
||||
Then, once your code is ready, tested and working as expected, you can use a **Continuous Delivery** (CD) service to automate the process of **deploying your code**.
|
||||
|
||||
CI/CD services are usually integrated with remote repositories, so you can configure them to run automatically when you push changes to the remote repository.
|
||||
|
||||
## Best practices and `git` workflows
|
||||
|
||||
Commit messages are very important, they are a way to easily know what has changed in the project.
|
||||
|
||||
There are many conventions for commit messages, but the most popular one is the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
Then, we can use the commit messages to automatically determine a [semantic version](https://semver.org/) for the next release of the project.
|
||||
|
||||
When multiple developers are working on the same project, it is important to organize the work in a way that everyone can work on different features without conflicts (changes in the same files).
|
||||
|
||||
There are many ways to organize the work, but the most popular ones are:
|
||||
|
||||
- [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)
|
||||
- [GitHub Flow](https://guides.github.com/introduction/flow/)
|
||||
- [Trunk-based development](https://trunkbaseddevelopment.com/)
|
||||
|
||||
They are called **Git workflows**, or **Git branching strategies**.
|
||||
|
||||
## Conclusion
|
||||
|
||||
`git` is the tool that every programmer should know to do collaborative work (not only, `git` is also very powerful even when working alone) and keep track of changes across a set of files.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Git official website and documentation](https://git-scm.com/)
|
||||
- [Git Explained in 100 Seconds](https://www.youtube.com/watch?v=hwP7WQkmECE)
|
||||
- [Understand Git in 7 minutes](https://www.jesuisundev.com/en/understand-git-in-7-minutes/)
|
||||
- [How (and why) to sign Git commits | With Blue Ink](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html?utm_source=tiktok&utm_campaign=codetok-sign)
|
||||
- [What Are the Best Git Branching Strategies](https://www.flagship.io/git-branching-strategies/)
|
@ -35,18 +35,11 @@ In this section, I will explain what technologies I used to make this blog, and
|
||||
|
||||
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
||||
|
||||
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).
|
||||
It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>.
|
||||
|
||||
- [MDX](https://mdxjs.com/)
|
||||
|
66
posts/mistakes-as-junior-developer.md
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
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: true
|
||||
publishedOn: '2022-03-14T07:42:52.989Z'
|
||||
---
|
||||
|
||||
Hello! 👋
|
||||
|
||||
I will explain some of the 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 errors junior developers does.
|
||||
|
||||
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, 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 to make it more maintainable, but because we are humans, we make mistakes, you could accidentally break the entire project even with a tiny change you thought was safe to do.
|
||||
|
||||
If you had 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 strategies:
|
||||
|
||||
- [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 learned the basics of programming, learn how to write automated tests, it will save you a lot of time and debugging.
|
||||
|
||||
I would even say that you should get used-to writing tests, it should be an automatism, you should not even have to think about it to do it.
|
||||
|
||||
## 2. Thinking too big, with too much abstraction
|
||||
|
||||
Abstraction is great, but it can be harder to understand what is going on if actually don't need this abstraction.
|
||||
|
||||
Find the right balance, between abstraction and simple 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), it is better than a half-functioning, over-engineered project.
|
||||
|
||||
I made this mistake while developing [Thream](https://thream.divlo.fr), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
|
||||
|
||||
Basically, I thought it was cool, to do a "big" v1.0.0 release with a lot of features, but in fact, it was not, because I could not even show what I was developing (to the end-users, not technical people) as I was making multiple features at the same time and also mainly focused on the **REST API** side and not at all the **website (frontend)**.
|
||||
|
||||
What I recommend you to do is to start with a **v1.0.0** release as soon as possible with the minimum required features needed for your project idea, and then gradually add new features and release new versions.
|
||||
|
||||
In my example for [Thream](https://thream.divlo.fr), I could release a v1.0.0 without these features:
|
||||
|
||||
- English/French translation (could be only English)
|
||||
- Light/Dark theme (could be only Dark)
|
||||
- OAuth2 Authentication (could be only simple email/password authentication)
|
||||
- User public profile
|
||||
- Channels (maybe could be only one channel per guild to start with)
|
||||
|
||||
And probably more, what was really required with [Thream](https://thream.divlo.fr), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough.
|
||||
|
||||
And then with this basis, I could release, v1.1.0, v1.2.0 etc. with more features, and release new versions more often to show the progress of the project, it is also more motivating to have users testing our project and to **get feedback sooner**.
|
||||
|
||||
**Start simple, improve later.**
|
||||
|
||||
## Conclusion
|
||||
|
||||
The real key to success is to **be passionate**, **keep learning** on your own, and **look at mistakes as learning experiences**.
|