feat: add divlo.fr
12
.babelrc.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"next/babel",
|
||||
{
|
||||
"styled-jsx": {
|
||||
"plugins": ["@styled-jsx/plugin-sass"]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
1
.commitlintrc.json
Normal file
@ -0,0 +1 @@
|
||||
{ "extends": ["@commitlint/config-conventional"] }
|
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
out
|
||||
.next
|
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
||||
# For more information see: https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
||||
COMPOSE_PROJECT_NAME=divlo.fr-website
|
||||
PORT=3000
|
||||
EMAIL_HOST=divlo.fr-maildev
|
||||
EMAIL_USER=reply@divlo-website.fr
|
||||
EMAIL_PASSWORD=password
|
||||
EMAIL_PORT=25
|
20
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: '🐛 Bug Report'
|
||||
about: 'Report an unexpected problem or unintended behavior.'
|
||||
title: '[Bug]'
|
||||
labels: 'bug'
|
||||
---
|
||||
|
||||
<!--
|
||||
Please provide a clear and concise description of what the bug is. Include
|
||||
screenshots if needed. Please make sure your issue has not already been fixed.
|
||||
-->
|
||||
|
||||
## Steps To Reproduce
|
||||
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
|
||||
## The current behavior
|
||||
|
||||
## The expected behavior
|
18
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: '📜 Documentation'
|
||||
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
||||
title: '[Documentation]'
|
||||
labels: 'documentation'
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
## Documentation
|
||||
|
||||
<!-- Please uncomment the type of documentation problem this issue address -->
|
||||
|
||||
<!-- Documentation is Missing -->
|
||||
<!-- Documentation is Confusing -->
|
||||
<!-- Documentation has Typo errors -->
|
||||
|
||||
## Proposal
|
20
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: '✨ Feature Request'
|
||||
about: 'Suggest a new feature idea.'
|
||||
title: '[Feature]'
|
||||
labels: 'feature request'
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
## Description
|
||||
|
||||
<!-- A clear and concise description of the problem or missing capability... -->
|
||||
|
||||
## Describe the solution you'd like
|
||||
|
||||
<!-- If you have a solution in mind, please describe it. -->
|
||||
|
||||
## Describe alternatives you've considered
|
||||
|
||||
<!-- Have you considered any alternative solutions or workarounds? -->
|
20
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: '🔧 Improvement'
|
||||
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
||||
title: '[Improvement]'
|
||||
labels: 'improvement'
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
## Type of Improvement
|
||||
|
||||
<!-- Please uncomment the type of improvements this issue address -->
|
||||
|
||||
<!-- Files and Folders Structure -->
|
||||
<!-- Performance -->
|
||||
<!-- Refactoring code -->
|
||||
<!-- Tests -->
|
||||
<!-- Not Sure? -->
|
||||
|
||||
## Proposal
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: '🙋 Question'
|
||||
about: 'Further information is requested.'
|
||||
title: '[Question]'
|
||||
labels: 'question'
|
||||
---
|
||||
|
||||
### Question
|
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<!--
|
||||
|
||||
Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time.
|
||||
|
||||
Before submitting your contribution, please take a moment to review this document:
|
||||
https://github.com/Divlo/Divlo/blob/master/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
||||
## What changes this PR introduce?
|
||||
|
||||
## List any relevant issue numbers
|
||||
|
||||
## Is there anything you'd like reviewers to focus on?
|
18
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# For more information see: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
38
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: 'ci'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: 'actions/setup-node@v2.1.5'
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: 'Cache dependencies'
|
||||
uses: 'actions/cache@v2.1.5'
|
||||
with:
|
||||
path: '.npm'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- run: 'npm ci --cache .npm --prefer-offline'
|
||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
- run: 'npm run lint:docker'
|
||||
- run: 'npm run lint:editorconfig'
|
||||
- run: 'npm run lint:markdown'
|
||||
- run: 'npm run lint:typescript'
|
||||
- run: 'npm run build'
|
||||
- run: 'npm run lighthouse'
|
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.npm
|
||||
|
||||
# next.js
|
||||
.next
|
||||
out
|
||||
|
||||
# production
|
||||
build
|
||||
dist
|
||||
|
||||
# PWA
|
||||
**/workbox-*.js
|
||||
**/sw.js
|
||||
|
||||
# envs
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# editors
|
||||
.vscode
|
||||
.theia
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.lighthouseci
|
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
4
.husky/commit-msg
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:commit -- --edit
|
7
.husky/pre-commit
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:docker
|
||||
npm run lint:editorconfig
|
||||
npm run lint:markdown
|
||||
npm run lint:typescript
|
29
.lighthouserc.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"startServerCommand": "npm run start",
|
||||
"startServerReadyPattern": "ready on",
|
||||
"startServerReadyTimeout": 20000,
|
||||
"url": ["http://localhost:3000/", "http://localhost:3000/setup"],
|
||||
"numberOfRuns": 3
|
||||
},
|
||||
"assert": {
|
||||
"preset": "lighthouse:recommended",
|
||||
"assertions": {
|
||||
"legacy-javascript": "off",
|
||||
"unused-javascript": "off",
|
||||
"uses-rel-preload": "off",
|
||||
"canonical": "off",
|
||||
"unsized-images": "off",
|
||||
"uses-responsive-images": "off",
|
||||
"bypass": "warning",
|
||||
"color-contrast": "warning",
|
||||
"preload-lcp-image": "warning"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
},
|
||||
"server": {}
|
||||
}
|
||||
}
|
7
.markdownlint.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
"MD024": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
}
|
132
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
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
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][mozilla coc].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][faq]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[mozilla coc]: https://github.com/mozilla/diversity
|
||||
[faq]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
46
CONTRIBUTING.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 💡 Contributing
|
||||
|
||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
|
||||
## Types of contributions
|
||||
|
||||
- Reporting a bug.
|
||||
- Suggest a new feature idea.
|
||||
- Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).
|
||||
- Improve structure/format/performance/refactor/tests of the code.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
||||
|
||||
- Ensure your code respect [Typescript Standard Style](https://www.npmjs.com/package/ts-standard).
|
||||
|
||||
- Make sure your **code passes the tests**.
|
||||
|
||||
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.
|
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM node:14.16.1
|
||||
RUN npm install --global npm@7
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package*.json ./
|
||||
RUN npm install
|
||||
COPY ./ ./
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--port", "${PORT}"]
|
@ -1,6 +1,4 @@
|
||||
# Welcome to my GitHub Account! 👋
|
||||
|
||||
![Divlo](./images/capture.png)
|
||||
# Divlo
|
||||
|
||||
## Social Media
|
||||
|
||||
|
31
components/Contact/FormResult.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { FormState } from './FormState'
|
||||
import { ResultState } from './index'
|
||||
|
||||
export interface FormResultProps {
|
||||
state: ResultState
|
||||
}
|
||||
|
||||
export const FormResult: React.FC<FormResultProps> = (props) => {
|
||||
const { state } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (state === 'idle') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (state === 'loading' || state === 'success') {
|
||||
return (
|
||||
<FormState state={state}>
|
||||
{t(`home:contact.result.${state}`)}
|
||||
</FormState>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormState state='error'>
|
||||
{t(`home:contact.result.${state}`)}
|
||||
</FormState>
|
||||
)
|
||||
}
|
39
components/Contact/FormState.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export interface FormStateProps extends React.ComponentPropsWithRef<'p'> {
|
||||
state: 'success' | 'error' | 'loading'
|
||||
children: string
|
||||
}
|
||||
|
||||
export const FormState: React.FC<FormStateProps> = props => {
|
||||
const { state, children, ...rest } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-result text-center'>
|
||||
<p className={state} {...rest}>
|
||||
{['error', 'success'].includes(state) && (
|
||||
<b>
|
||||
{state === 'error' ? t('home:contact.error') : t('home:contact.success')}:
|
||||
</b>
|
||||
)}{' '}
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-result {
|
||||
margin: 30px;
|
||||
}
|
||||
.success {
|
||||
color: #90ee90;
|
||||
}
|
||||
.error {
|
||||
color: #ff7f7f;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
89
components/Contact/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useState } from 'react'
|
||||
import Form, { HandleForm } from 'react-component-form'
|
||||
import axios from 'axios'
|
||||
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { Textarea } from 'components/design/Textarea'
|
||||
import { FormResult } from './FormResult'
|
||||
|
||||
export const resultState = [
|
||||
'idle',
|
||||
'success',
|
||||
'loading',
|
||||
'requiredFields',
|
||||
'invalidEmail',
|
||||
'serverError'
|
||||
] as const
|
||||
|
||||
export type ResultState = typeof resultState[number]
|
||||
|
||||
export const Contact: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [state, setState] = useState<ResultState>('idle')
|
||||
|
||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
||||
setState('loading')
|
||||
try {
|
||||
const { data } = await axios.post<{ type: ResultState }>(
|
||||
'/api/send-email',
|
||||
formData
|
||||
)
|
||||
if (data.type === 'success') {
|
||||
setState('success')
|
||||
return formElement.reset()
|
||||
}
|
||||
return setState('serverError')
|
||||
} catch (error) {
|
||||
const type = error.response.data.type
|
||||
if (resultState.includes(type)) {
|
||||
return setState(type)
|
||||
}
|
||||
return setState('serverError')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
label={`${t('home:contact.nameField')} :`}
|
||||
type='text'
|
||||
name='name'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label='Email :'
|
||||
type='email'
|
||||
name='email'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label={`${t('home:contact.subjectField')} :`}
|
||||
type='text'
|
||||
name='subject'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='Message :'
|
||||
name='message'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button type='submit'>{t('home:contact.sendEmail')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<FormResult state={state} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
38
components/ErrorPage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface ErrorPageProps {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export const ErrorPage: React.FC<ErrorPageProps> = props => {
|
||||
const { message, statusCode } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
{t('errors:error')} <span className='important'>{statusCode}</span>
|
||||
</h1>
|
||||
<p className='text-center'>
|
||||
{message} <Link href='/'>{t('returnToHomePage')}</Link>
|
||||
</p>
|
||||
|
||||
<style jsx global>{`
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 100vw;
|
||||
min-height: 100%;
|
||||
}
|
||||
#__next {
|
||||
padding-top: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
29
components/Footer/LanguageButton.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
|
||||
interface LanguageButtonProps {
|
||||
lang: string
|
||||
}
|
||||
|
||||
export const LanguageButton: React.FC<LanguageButtonProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
onClick={async () => await setLanguage(props.lang)}
|
||||
className='important'
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
39
components/Footer/LanguageFlag.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
import { LanguageButton } from './LanguageButton'
|
||||
|
||||
interface LanguageFlagProps {
|
||||
imageLink: string
|
||||
title: string
|
||||
lang: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { lang, title, imageLink } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='LanguageFlag'>
|
||||
<LanguageButton lang={lang}>
|
||||
<Tooltip title={title}>
|
||||
<Image alt={title} src={imageLink} width={31} height={31} />
|
||||
</Tooltip>
|
||||
</LanguageButton>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.LanguageFlag {
|
||||
margin-right: 7px;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.LanguageFlag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
60
components/Footer/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { LanguageButton } from './LanguageButton'
|
||||
import { LanguageFlag } from './LanguageFlag'
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className='Footer text-center'>
|
||||
<p className='Footer__text'>
|
||||
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
|
||||
</p>
|
||||
<p className='Footer__lang'>
|
||||
<LanguageButton lang='en'>{t('common:english')}</LanguageButton> |{' '}
|
||||
<LanguageButton lang='fr'>{t('common:french')}</LanguageButton>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<div className='Footer__flags'>
|
||||
<LanguageFlag
|
||||
lang='en'
|
||||
imageLink='/images/flags/english_flag.png'
|
||||
title={t('common:english')}
|
||||
/>
|
||||
<LanguageFlag
|
||||
lang='fr'
|
||||
imageLink='/images/flags/french_flag.png'
|
||||
title={t('common:french')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Footer {
|
||||
border-top: var(--border-header-footer);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.Footer__text {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
.Footer__lang {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.Footer__flags {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
left: 32px;
|
||||
z-index: 10;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
59
components/Head.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import HeadTag from 'next/head'
|
||||
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const Head: React.FC<HeadProps> = props => {
|
||||
const {
|
||||
title = 'Divlo',
|
||||
image = '/images/icons/icon-96x96.png',
|
||||
description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
|
||||
url = 'https://divlo.divlo.fr/'
|
||||
} = props
|
||||
|
||||
return (
|
||||
<HeadTag>
|
||||
<title>{title}</title>
|
||||
<link rel='icon' type='image/png' href={image} />
|
||||
|
||||
{/* Meta Tag */}
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||
<meta name='description' content={description} />
|
||||
<meta name='Language' content='fr, en' />
|
||||
<meta name='theme-color' content='#ffd800' />
|
||||
|
||||
{/* Open Graph Metadata */}
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:image' content={image} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:locale' content='fr_FR, en_US' />
|
||||
<meta property='og:site_name' content={title} />
|
||||
|
||||
{/* Twitter card Metadata */}
|
||||
<meta name='twitter:card' content='summary' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<meta name='twitter:image:src' content={image} />
|
||||
|
||||
{/* Google Verification */}
|
||||
<meta
|
||||
name='google-site-verification'
|
||||
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
|
||||
/>
|
||||
|
||||
{/* PWA Data */}
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<link rel='apple-touch-icon' href={image} />
|
||||
</HeadTag>
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
38
components/Header/BrandLogo.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
export const BrandLogo: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Link href='/'>
|
||||
<a className='Header__brand-link'>
|
||||
<Image
|
||||
width={65}
|
||||
height={65}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt="Divlo's Logo"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header__brand-link {
|
||||
display: inline-block;
|
||||
padding-top: 0.3125rem;
|
||||
padding-bottom: 0.3125rem;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (min-width: 993px) {
|
||||
.Header__brand-link {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
76
components/Header/HamburgerIcon.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
type HamburgerIconComponent = React.FC<{
|
||||
isActive: boolean
|
||||
handleToggleNavbar: () => void
|
||||
}>
|
||||
|
||||
export const HamburgerIcon: HamburgerIconComponent = props => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={props.handleToggleNavbar}
|
||||
className={classNames('Header__hamburger', {
|
||||
'Header__hamburger-active': props.isActive
|
||||
})}
|
||||
>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header__hamburger {
|
||||
display: none;
|
||||
width: 56px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
}
|
||||
.Header__hamburger > span,
|
||||
.Header__hamburger > span::before,
|
||||
.Header__hamburger > span::after {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 1.3px;
|
||||
background-color: rgba(255, 255, 255);
|
||||
}
|
||||
.Header__hamburger > span {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
.Header__hamburger > span::before,
|
||||
.Header__hamburger > span::after {
|
||||
content: '';
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
.Header__hamburger > span::before {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.Header__hamburger > span::after {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
.Header__hamburger-active span {
|
||||
background-color: transparent;
|
||||
}
|
||||
.Header__hamburger-active > span::before {
|
||||
transform: translateY(0px) rotateZ(45deg);
|
||||
}
|
||||
.Header__hamburger-active > span::after {
|
||||
transform: translateY(0px) rotateZ(-45deg);
|
||||
}
|
||||
/* Hamburger icon on Mobile */
|
||||
@media (max-width: 992px) {
|
||||
.Header__hamburger {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
54
components/Header/Navigation/NavigationLink.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type NavigationLinkComponent = React.FC<{ path: string }>
|
||||
|
||||
export const NavigationLink: NavigationLinkComponent = props => {
|
||||
const { pathname } = useRouter()
|
||||
const isCurrentPage = pathname === props.path
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='navbar-item'>
|
||||
<Link href={props.path}>
|
||||
<a
|
||||
className={classNames('navbar-link', {
|
||||
'navbar-link-active': isCurrentPage
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.navbar-link {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.navbar-link:hover {
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.navbar-link,
|
||||
.navbar-link-active {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.navbar-link-active,
|
||||
.navbar-link-active:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.navbar-item {
|
||||
list-style: none;
|
||||
}
|
||||
.navbar-link {
|
||||
font-size: 16px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
59
components/Header/Navigation/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import classNames from 'classnames'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { NavigationLink } from './NavigationLink'
|
||||
|
||||
type NavigationComponent = React.FC<{ isActive: boolean }>
|
||||
|
||||
export const Navigation: NavigationComponent = props => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className='Header__navbar'>
|
||||
<ul
|
||||
className={classNames('navbar__list', {
|
||||
'navbar__list-active': props.isActive
|
||||
})}
|
||||
>
|
||||
<NavigationLink path='/'>{t('common:home')}</NavigationLink>
|
||||
<NavigationLink path='/setup'>Setup</NavigationLink>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
@media (min-width: 992px) {
|
||||
.Header__navbar {
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
.Header__navbar {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
.navbar__list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
.navbar__list.navbar__list-active {
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.navbar__list {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
69
components/Header/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { HamburgerIcon } from './HamburgerIcon'
|
||||
import { BrandLogo } from './BrandLogo'
|
||||
import { Navigation } from './Navigation'
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
|
||||
const handleToggleNavbar = (): void => {
|
||||
setIsActive(!isActive)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='Header'>
|
||||
<div className='container'>
|
||||
<BrandLogo />
|
||||
<HamburgerIcon
|
||||
isActive={isActive}
|
||||
handleToggleNavbar={handleToggleNavbar}
|
||||
/>
|
||||
<Navigation isActive={isActive} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
border-bottom: var(--border-header-footer);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.Header {
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.Header > .container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.Header > .container {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
20
components/Interests/InterestParagraph.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import htmlParser from 'html-react-parser'
|
||||
|
||||
export interface InterestParagraphProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
const { title, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-center'>
|
||||
<strong className='important'>{title}</strong>
|
||||
<br />
|
||||
<span className='paragraph-color'>{htmlParser(description)}</span>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Interests/InterestsList/InterestItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
|
||||
interface InterestItemProps {
|
||||
title: string
|
||||
fontAwesomeIcon: IconDefinition
|
||||
}
|
||||
|
||||
export const InterestItem: React.FC<InterestItemProps> = props => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='interest-item'>
|
||||
<Tooltip title={title}>
|
||||
<FontAwesomeIcon
|
||||
className='color-primary'
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'block'
|
||||
}}
|
||||
icon={fontAwesomeIcon}
|
||||
/>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.interest-item {
|
||||
margin: 7px 5px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
45
components/Interests/InterestsList/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGit } from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
import { InterestItem } from './InterestItem'
|
||||
|
||||
export const InterestsList: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='container-list'>
|
||||
<ul className='interests-list'>
|
||||
<InterestItem
|
||||
title='Developer Full Stack Junior'
|
||||
fontAwesomeIcon={faCode}
|
||||
/>
|
||||
<InterestItem
|
||||
title='Passionate about High-Tech'
|
||||
fontAwesomeIcon={faMicrochip}
|
||||
/>
|
||||
<InterestItem
|
||||
title='Open-Source enthusiast'
|
||||
fontAwesomeIcon={faGit}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.container-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
.interests-list {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 60%;
|
||||
list-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
23
components/Interests/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
||||
import { InterestsList } from './InterestsList'
|
||||
|
||||
export const Interests: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const paragraphs: InterestParagraphProps[] = t('home:interests.paragraphs', {}, {
|
||||
returnObjects: true
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
return <InterestParagraph key={index} {...paragraph} />
|
||||
})}
|
||||
<InterestsList />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
102
components/Portfolio/PortfolioItem.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
description: string
|
||||
link: string
|
||||
image: string
|
||||
}
|
||||
|
||||
export const PortfolioItem: React.FC<PortfolioItemProps> = props => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-sm-24 col-md-10 col-xl-7 portfolio-grid'>
|
||||
<a
|
||||
className='portfolio-link'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href={link}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className='portfolio-figure'>
|
||||
<Image width={300} height={300} src={image} alt={title} />
|
||||
</div>
|
||||
<div className='portfolio-caption'>
|
||||
<h3 className='portfolio-title important'>{title}</h3>
|
||||
<p className='portfolio-description'>{description}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
.portfolio-figure img[alt='${title}'] {
|
||||
max-height: 300px;
|
||||
max-width: 300px;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.portfolio-grid:hover img[alt='${title}'] {
|
||||
opacity: 0.05;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.portfolio-grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
margin: 0 0 50px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* col-md */
|
||||
@media (min-width: 768px) {
|
||||
.portfolio-grid {
|
||||
margin: 0 30px 50px 30px;
|
||||
}
|
||||
}
|
||||
/* col-xl */
|
||||
@media (min-width: 1200px) {
|
||||
.portfolio-grid {
|
||||
margin: 0 20px 50px 20px;
|
||||
}
|
||||
}
|
||||
.portfolio-figure {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.portfolio-caption {
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.portfolio-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
.portfolio-grid:hover .portfolio-caption {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
}
|
||||
.portfolio-grid:hover .portfolio-link {
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
23
components/Portfolio/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
||||
|
||||
export const Portfolio: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
const items: PortfolioItemProps[] = t('home:portfolio.items', {}, {
|
||||
returnObjects: true
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='container-fluid'>
|
||||
<div className='row justify-content-center'>
|
||||
{items.map((item, index) => {
|
||||
return <PortfolioItem key={index} {...item} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
31
components/Profile/ProfileDescriptionBottom.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Translation from 'next-translate/Trans'
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='profile-description-bottom'>
|
||||
<Translation
|
||||
i18nKey={t('home:about.descriptionBottom')}
|
||||
components={[<br key='break' />]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-description-bottom {
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
line-height: 25px;
|
||||
color: #b2bac2;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Profile/ProfileInfo.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export const ProfileInfo: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='profile-info'>
|
||||
<h1 className='profile-title'>
|
||||
{t('home:about.IAm')} <strong className='important'>Divlo</strong>
|
||||
</h1>
|
||||
<h2 className='profile-description'>{t('home:about.description')}</h2>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-info {
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
.profile-title {
|
||||
font-size: 36px;
|
||||
line-height: 1.1;
|
||||
font-weight: 300;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.profile-title > strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.profile-description {
|
||||
font-size: 17.4px;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
79
components/Profile/ProfileList/ProfileItem.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
interface ProfileItemProps {
|
||||
title: string
|
||||
value: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='profile-list__item'>
|
||||
<strong className='profile-list__item-title'>{title}</strong>
|
||||
<span className='profile-list__item-info'>
|
||||
{link != null ? (
|
||||
<a className='profile-list__link' href={link}>
|
||||
{value}
|
||||
</a>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-list__item {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
.profile-list__item::after,
|
||||
.profile-list__item::before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.profile-list__item::after {
|
||||
clear: both;
|
||||
}
|
||||
.profile-list__item-title {
|
||||
display: block;
|
||||
width: 120px;
|
||||
float: left;
|
||||
color: #d4d4d5;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
display: block;
|
||||
margin-left: 125px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #84898e;
|
||||
}
|
||||
.profile-list__link {
|
||||
color: #84898e;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.profile-list__item-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
margin-left: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.profile-list__item-info,
|
||||
.profile-list__item-title {
|
||||
width: 100%;
|
||||
float: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
37
components/Profile/ProfileList/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ProfileItem } from './ProfileItem'
|
||||
|
||||
export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className='profile-list'>
|
||||
<ProfileItem
|
||||
title={t('home:about.birthDate')}
|
||||
value='31/03/2003'
|
||||
/>
|
||||
<ProfileItem
|
||||
title={t('home:about.nationality')}
|
||||
value='Alsace, France'
|
||||
/>
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
value='contact@divlo.fr'
|
||||
link='mailto:contact@divlo.fr'
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
26
components/Profile/ProfileLogo.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='col-sm-24 col-md-10'>
|
||||
<div className='profile-logo'>
|
||||
<Image
|
||||
width={800}
|
||||
height={800}
|
||||
src='/images/divlo_logo.png'
|
||||
alt='Divlo'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.profile-logo {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
50
components/Profile/SocialMediaList/SocialMediaItem.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface SocialMediaItemProps {
|
||||
link: string
|
||||
socialMedia: 'Email' | 'GitHub' | 'Twitch' | 'Twitter' | 'YouTube'
|
||||
}
|
||||
|
||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = props => {
|
||||
const { link, socialMedia } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='social-media-list__item'>
|
||||
<a
|
||||
href={link}
|
||||
aria-label={socialMedia}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='social-media-list__link'
|
||||
>
|
||||
<Tooltip title={socialMedia}>
|
||||
<Image
|
||||
width={45}
|
||||
height={45}
|
||||
alt={socialMedia}
|
||||
src={`/images/web/${socialMedia}.png`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.social-media-list__item {
|
||||
display: inline-block;
|
||||
margin: 5px 15px;
|
||||
}
|
||||
.social-media-list__link {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Profile/SocialMediaList/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { SocialMediaItem } from './SocialMediaItem'
|
||||
|
||||
export const SocialMediaList: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='row justify-content-center'>
|
||||
<ul className='social-media-list'>
|
||||
<SocialMediaItem
|
||||
socialMedia='Twitter'
|
||||
link='https://twitter.com/Divlo_FR'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='GitHub'
|
||||
link='https://github.com/Divlo'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='YouTube'
|
||||
link='https://www.youtube.com/c/Divlo'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='Twitch'
|
||||
link='https://www.twitch.tv/divlo'
|
||||
/>
|
||||
<SocialMediaItem socialMedia='Email' link='mailto:contact@divlo.fr' />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.social-media-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
33
components/Profile/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
||||
import { ProfileInfo } from './ProfileInfo'
|
||||
import { ProfileList } from './ProfileList'
|
||||
import { ProfileLogo } from './ProfileLogo'
|
||||
|
||||
export const Profile: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='row profile'>
|
||||
<ProfileLogo />
|
||||
<div className='col-sm-24 col-md-14'>
|
||||
<ProfileInfo />
|
||||
<ProfileList />
|
||||
<ProfileDescriptionBottom />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile {
|
||||
padding: 40px 50px 15px 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.profile {
|
||||
padding: 40px 10px 0 10px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
50
components/Setup/Table.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
export interface TableRow {
|
||||
title: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface TableProps {
|
||||
rows: TableRow[]
|
||||
}
|
||||
|
||||
export const Table: React.FC<TableProps> = props => {
|
||||
const { rows } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24 table-column text-center'>
|
||||
<table>
|
||||
<tbody>
|
||||
{rows.map((row, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<th className='table-row'>{row.title}</th>
|
||||
<td className='table-row'>{row.value}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.table-column {
|
||||
display: grid;
|
||||
}
|
||||
.table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--color-text-1);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.table-row {
|
||||
padding: 15px;
|
||||
}
|
||||
.image-setup {
|
||||
width: 85%;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
21
components/Setup/TableTitle.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export const TableTitle: React.FC = props => {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
<p className='text-center title-table'>
|
||||
<strong className='important'>{children}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.title-table {
|
||||
font-size: 24px;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
137
components/Setup/index.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Table, TableRow } from './Table'
|
||||
import { TableTitle } from './TableTitle'
|
||||
|
||||
export const Setup: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const rowsConfigPC: TableRow[] = [
|
||||
{
|
||||
title: t('setup:configPC.motherboard'),
|
||||
value: 'MSI Z87-G45 GAMING'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.processor'),
|
||||
value: 'Intel Core i5-4690k'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.graphicCard'),
|
||||
value: 'Zotac GeForce GTX 970'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.ramMemory'),
|
||||
value: '16 GB (2 x 8Go) Kingston HyperX'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.hardDrive'),
|
||||
value: '256 GB SSD Crucial & 2 TB Seagate'
|
||||
}
|
||||
]
|
||||
|
||||
const rowsPeripherals: TableRow[] = [
|
||||
{
|
||||
title: t('setup:peripheral.keyboard'),
|
||||
value: 'Corsair K95 RGB'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.mouse'),
|
||||
value: 'SteelSeries Rival 310'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.headset'),
|
||||
value: 'SteelSeries ARCTIS PRO + GAMEDAC'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.mainScreen'),
|
||||
value: 'IIyama PL2480H'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.secondScreen'),
|
||||
value: 'Samsung SyncMaster 2220LM'
|
||||
}
|
||||
]
|
||||
|
||||
const rowsOffice: TableRow[] = [
|
||||
{
|
||||
title: t('setup:officeOther.mousepad'),
|
||||
value: 'SteelSeries QCK Heavy (Grand) as string'
|
||||
},
|
||||
{
|
||||
title: 'Mouse Bungee',
|
||||
value: 'BenQ ZOWIE Camade'
|
||||
},
|
||||
{
|
||||
title: t('setup:officeOther.usb'),
|
||||
value: 'Kingston 128GB'
|
||||
},
|
||||
{
|
||||
title: 'Smartphone',
|
||||
value: 'Samsung Galaxy A5 (2017)'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableTitle>{t('setup:configPC.title')}</TableTitle>
|
||||
<Table rows={rowsConfigPC} />
|
||||
|
||||
<TableTitle>{t('setup:peripheral.title')}</TableTitle>
|
||||
<Table rows={rowsPeripherals} />
|
||||
|
||||
<TableTitle>{t('setup:officeOther.title')}</TableTitle>
|
||||
<Table rows={rowsOffice} />
|
||||
|
||||
<div
|
||||
className='row row-padding justify-content-center'
|
||||
style={{ marginTop: 50 }}
|
||||
>
|
||||
<Image
|
||||
src='/images/setup/setup2019.png'
|
||||
alt='Setup Divlo'
|
||||
width={856.8}
|
||||
height={672.58}
|
||||
className='Setup__image'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row row-padding justify-content-center'>
|
||||
<Image
|
||||
src='/images/setup/setup2019-lights.jpg'
|
||||
alt='Setup Divlo'
|
||||
width={856.8}
|
||||
height={672.58}
|
||||
className='Setup__image'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row row-padding'>
|
||||
<TableTitle>{t('setup:connexion')}</TableTitle>
|
||||
<div style={{ marginBottom: 25 }} className='col-24 text-center'>
|
||||
<a
|
||||
href='https://www.speedtest.net/result/8533865940'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
aria-label='Speedtest link'
|
||||
>
|
||||
<Image
|
||||
src='/images/setup/speedtest-result.png'
|
||||
alt='Speedtest Result'
|
||||
width={308}
|
||||
height={165}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
.Setup__image {
|
||||
width: 85% !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
44
components/Skills/Skill.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { skills } from './skills'
|
||||
|
||||
export interface SkillProps {
|
||||
skill: keyof typeof skills
|
||||
}
|
||||
|
||||
export const Skill: React.FC<SkillProps> = props => {
|
||||
const { skill } = props
|
||||
const skillProperties = skills[skill]
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={skillProperties.link}
|
||||
className='skills-link'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='skills-content text-center'>
|
||||
<Image
|
||||
width={60}
|
||||
height={60}
|
||||
alt={skill}
|
||||
src={skillProperties.image}
|
||||
/>
|
||||
<p className='skills-text'>{skill}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style jsx>{`
|
||||
.skills-link {
|
||||
max-width: 120px;
|
||||
margin: 0px 10px 0 10px;
|
||||
}
|
||||
.skills-text {
|
||||
margin-top: 5px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
44
components/Skills/SkillsSection.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
|
||||
export interface SkillsSectionProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const SkillsSection: React.FC<SkillsSectionProps> = props => {
|
||||
const { title, children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShadowContainer>
|
||||
<div className='container-fluid'>
|
||||
<div className='row row-padding'>
|
||||
<div className='col-24'>
|
||||
<div className='skills-header'>
|
||||
<h3 className='important'>{title}</h3>
|
||||
</div>
|
||||
<div className='skills-body'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShadowContainer>
|
||||
|
||||
<style jsx>{`
|
||||
.skills-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.skills-header > h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.skills-body {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-flow: row wrap;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Skills/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Skill } from './Skill'
|
||||
import { SkillsSection } from './SkillsSection'
|
||||
|
||||
export const Skills: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkillsSection title={t('home:skills.languages')}>
|
||||
<Skill skill='JavaScript' />
|
||||
<Skill skill='TypeScript' />
|
||||
<Skill skill='Python' />
|
||||
<Skill skill='Dart' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Front-end'>
|
||||
<Skill skill='HTML' />
|
||||
<Skill skill='CSS' />
|
||||
<Skill skill='SASS' />
|
||||
<Skill skill='React.js (+ Next.js)' />
|
||||
<Skill skill='Flutter' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Back-end'>
|
||||
<Skill skill='Node.js' />
|
||||
<Skill skill='Strapi' />
|
||||
<Skill skill='MySQL' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title={t('home:skills.softwareTools')}>
|
||||
<Skill skill='Ubuntu' />
|
||||
<Skill skill='Hyper' />
|
||||
<Skill skill='Visual Studio Code' />
|
||||
<Skill skill='Git' />
|
||||
<Skill skill='Docker' />
|
||||
</SkillsSection>
|
||||
</>
|
||||
)
|
||||
}
|
70
components/Skills/skills.ts
Normal file
@ -0,0 +1,70 @@
|
||||
export const skills = {
|
||||
JavaScript: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
||||
image: '/images/skills/JavaScript.png'
|
||||
},
|
||||
TypeScript: {
|
||||
link: 'https://www.typescriptlang.org/',
|
||||
image: '/images/skills/TypeScript.png'
|
||||
},
|
||||
Python: {
|
||||
link: 'https://www.python.org/',
|
||||
image: '/images/skills/Python.png'
|
||||
},
|
||||
Dart: {
|
||||
link: 'https://dart.dev/',
|
||||
image: '/images/skills/Dart.png'
|
||||
},
|
||||
Flutter: {
|
||||
link: 'https://flutter.dev/',
|
||||
image: '/images/skills/Flutter.webp'
|
||||
},
|
||||
HTML: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/HTML',
|
||||
image: '/images/skills/HTML.png'
|
||||
},
|
||||
CSS: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/CSS',
|
||||
image: '/images/skills/CSS.png'
|
||||
},
|
||||
SASS: {
|
||||
link: 'https://sass-lang.com/',
|
||||
image: '/images/skills/SASS.svg'
|
||||
},
|
||||
'React.js (+ Next.js)': {
|
||||
link: 'https://reactjs.org/',
|
||||
image: '/images/skills/ReactJS.png'
|
||||
},
|
||||
'Node.js': {
|
||||
link: 'https://nodejs.org/',
|
||||
image: '/images/skills/NodeJS.png'
|
||||
},
|
||||
MySQL: {
|
||||
link: 'https://www.mysql.com/',
|
||||
image: '/images/skills/MySQL.png'
|
||||
},
|
||||
Strapi: {
|
||||
link: 'https://strapi.io/',
|
||||
image: '/images/skills/Strapi.png'
|
||||
},
|
||||
'Visual Studio Code': {
|
||||
link: 'https://code.visualstudio.com/',
|
||||
image: '/images/skills/Visual_Studio_Code.png'
|
||||
},
|
||||
Git: {
|
||||
link: 'https://git-scm.com/',
|
||||
image: '/images/skills/Git.png'
|
||||
},
|
||||
Hyper: {
|
||||
link: 'https://hyper.is/',
|
||||
image: '/images/skills/Hyper.svg'
|
||||
},
|
||||
Ubuntu: {
|
||||
link: 'https://ubuntu.com/',
|
||||
image: '/images/skills/Ubuntu.png'
|
||||
},
|
||||
Docker: {
|
||||
link: 'https://www.docker.com/',
|
||||
image: '/images/skills/Docker.png'
|
||||
}
|
||||
} as const
|
43
components/design/Button.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type ButtonProps = React.ComponentPropsWithRef<'button'>
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={ref} {...rest} className='btn btn-dark'>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn-dark {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40;
|
||||
}
|
||||
.btn-dark:hover {
|
||||
color: #fff;
|
||||
background-color: #23272b;
|
||||
border-color: #1d2124;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
75
components/design/Input.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface InputProps extends React.HTMLProps<HTMLInputElement> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const { label, name, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-group-animation'>
|
||||
<input ref={ref} {...rest} id={name} name={name} />
|
||||
<label htmlFor={name} className='label'>
|
||||
<span className='label-content'>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-group-animation {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.form-group-animation input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 35px;
|
||||
color: var(--color-text-1);
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
.form-group-animation label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
.form-group-animation label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -1px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-bottom: 3px solid var(--color-primary);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.label-content {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.form-group-animation input:focus + .label .label-content,
|
||||
.form-group-animation input:valid + .label .label-content {
|
||||
transform: translateY(-150%);
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.form-group-animation input:focus + .label::after,
|
||||
.form-group-animation input:valid + .label::after {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
})
|
49
components/design/RevealFade.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const RevealFade: React.FC = props => {
|
||||
const { children } = props
|
||||
|
||||
const htmlElement = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('reveal-visible')
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0.28
|
||||
}
|
||||
)
|
||||
observer.observe(htmlElement.current as HTMLDivElement)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={htmlElement} className='reveal'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
.reveal-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transition: all 500ms ease-out 100ms;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
28
components/design/Section/SectionHeading.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
||||
|
||||
export const SectionHeading = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
SectionHeadingProps
|
||||
>((props, ref) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 ref={ref} {...rest} className='Section__title'>
|
||||
{children}
|
||||
</h2>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Section__title {
|
||||
font-size: 34px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
})
|
62
components/design/Section/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { ShadowContainer } from '../ShadowContainer'
|
||||
import { SectionHeading } from './SectionHeading'
|
||||
|
||||
type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
||||
heading?: string
|
||||
description?: string
|
||||
isMain?: boolean
|
||||
withoutShadowContainer?: boolean
|
||||
}
|
||||
|
||||
export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
heading,
|
||||
description,
|
||||
isMain = false,
|
||||
withoutShadowContainer = false,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (isMain) {
|
||||
return (
|
||||
<ShadowContainer style={{ marginTop: 50 }}>
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='container-fluid'>{children}</div>
|
||||
</section>
|
||||
</ShadowContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (withoutShadowContainer) {
|
||||
return (
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='container-fluid'>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && (
|
||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
||||
{heading}
|
||||
</SectionHeading>
|
||||
)}
|
||||
{description != null && (
|
||||
<p style={{ marginTop: 7 }} className='text-center'>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<ShadowContainer>
|
||||
<div className='container-fluid'>
|
||||
<div className='row row-padding'>{children}</div>
|
||||
</div>
|
||||
</ShadowContainer>
|
||||
</section>
|
||||
)
|
||||
})
|
32
components/design/ShadowContainer.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||
|
||||
export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`shadow-container ${className != null ? className : ''}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.shadow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
39
components/design/Textarea.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface TextareaProps extends React.HTMLProps<HTMLTextAreaElement> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
(props, ref) => {
|
||||
const { label, name, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-group'>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<br />
|
||||
<textarea id={name} name={name} ref={ref} {...rest} />
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-group {
|
||||
padding-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-group textarea {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
49
components/design/Tooltip.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
interface TooltipProps extends React.ComponentPropsWithRef<'div'> {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = props => {
|
||||
const { title, children, ...rest } = props
|
||||
return (
|
||||
<>
|
||||
<span className='tooltip' {...rest}>
|
||||
{children}
|
||||
<span className='title'>{title}</span>
|
||||
</span>
|
||||
|
||||
<style jsx>{`
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
background-color: #222222;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease-in;
|
||||
transform: translate3d(0, -15px, 0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.tooltip ~ .tooltip:hover .title,
|
||||
.tooltip:first-child:hover .title {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: all 0.35s ease-out;
|
||||
transform: translate3d(0, 0, 0);
|
||||
margin: 0;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
18
docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
||||
version: '3.0'
|
||||
services:
|
||||
divlo.fr-website:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
build:
|
||||
context: './'
|
||||
ports:
|
||||
- '${PORT}:${PORT}'
|
||||
environment:
|
||||
PORT: ${PORT}
|
||||
volumes:
|
||||
- './:/app'
|
||||
|
||||
divlo.fr-maildev:
|
||||
image: 'maildev/maildev:1.1.0'
|
||||
ports:
|
||||
- '1080:80'
|
||||
container_name: 'divlo.fr-maildev'
|
11
i18n.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"locales": ["en", "fr"],
|
||||
"defaultLocale": "en",
|
||||
"pages": {
|
||||
"*": ["common"],
|
||||
"/": ["home"],
|
||||
"/setup": ["setup"],
|
||||
"/404": ["errors"],
|
||||
"/500": ["errors"]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 74 KiB |
6
locales/en/common.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"english": "English",
|
||||
"french": "French",
|
||||
"allRightsReserved": "All rights reserved",
|
||||
"home": "Home"
|
||||
}
|
6
locales/en/errors.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"returnToHomePage": "Return to the home page?",
|
||||
"error": "Error",
|
||||
"serverError": "Internal Server Error!",
|
||||
"notFound": "This page doesn't exist!"
|
||||
}
|
63
locales/en/home.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"about": {
|
||||
"IAm": "I am",
|
||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
||||
"birthDate": "Birth date",
|
||||
"nationality": "Nationality",
|
||||
"descriptionBottom": "I'm learning online programming languages to improve my skills in my passion. <0/> <0/> I designed my graphic chart and my website."
|
||||
},
|
||||
"interests": {
|
||||
"title": "My Interests",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Developer Full Stack Junior :",
|
||||
"description": "Computer programming is my main passion, I love it! <br/> Mostly web development for the moment but I'm programming some Python and others programming language too."
|
||||
},
|
||||
{
|
||||
"title": "Passionate about High-Tech :",
|
||||
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and even better than the past. Technologies improve gradually over time, which is very useful in many areas."
|
||||
},
|
||||
{
|
||||
"title": "Open-Source enthusiast :",
|
||||
"description": "For me, everyone should work, solve problems, build things and think together. Long live open source, whenever you can share your work, do it! <br/> The website is open-source on <a href='https://github.com/Divlo/divlo.fr' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"title": "My skills",
|
||||
"languages": "Programming languages",
|
||||
"softwareTools": "Software and tools"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "My Portfolio",
|
||||
"items": [
|
||||
{
|
||||
"title": "function.divlo.fr",
|
||||
"description": "Learn programming through project-based alias function.",
|
||||
"link": "https://function.divlo.fr/",
|
||||
"image": "/images/portfolio/functiondivlofr.png"
|
||||
},
|
||||
{
|
||||
"title": "thream.divlo.fr",
|
||||
"description": "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.",
|
||||
"link": "https://thream.divlo.fr/",
|
||||
"image": "/images/portfolio/threamdivlofr.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact-Me",
|
||||
"nameField": "Name",
|
||||
"subjectField": "Subject",
|
||||
"sendEmail": "Send email",
|
||||
"result": {
|
||||
"loading": "Loading...",
|
||||
"success": "Your email has been sent!",
|
||||
"requiredFields": "You must fill all the fields...",
|
||||
"invalidEmail": "Please enter a valid email address...",
|
||||
"serverError": "The server could not process your request..."
|
||||
},
|
||||
"error": "Error",
|
||||
"success": "Success"
|
||||
}
|
||||
}
|
26
locales/en/setup.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "Setup of Divlo",
|
||||
"description": "The list of all the computer equipment that Divlo has.",
|
||||
"configPC": {
|
||||
"title": "Hardware PC Configuration",
|
||||
"motherboard": "Motherboard",
|
||||
"processor": "Processor",
|
||||
"graphicCard": "Graphic card",
|
||||
"ramMemory": "Ram Memory",
|
||||
"hardDrive": "Hard Drive"
|
||||
},
|
||||
"peripheral": {
|
||||
"title": "Computer Peripheral ",
|
||||
"keyboard": "Keyboard",
|
||||
"mouse": "Mouse",
|
||||
"headset": "Micro Headset",
|
||||
"mainScreen": "Main Screen",
|
||||
"secondScreen": "2nd screen"
|
||||
},
|
||||
"officeOther": {
|
||||
"title": "Office / Other",
|
||||
"mousepad": "Mousepad",
|
||||
"usb": "USB Key"
|
||||
},
|
||||
"connexion": "My internet connection"
|
||||
}
|
6
locales/fr/common.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"english": "Anglais",
|
||||
"french": "Français",
|
||||
"allRightsReserved": "Tous droits réservés",
|
||||
"home": "Accueil"
|
||||
}
|
6
locales/fr/errors.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"returnToHomePage": "Revenir à la page d'accueil ?",
|
||||
"error": "Erreur",
|
||||
"serverError": "Erreur Interne du Serveur !",
|
||||
"notFound": "Cette page n'existe pas!"
|
||||
}
|
63
locales/fr/home.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"about": {
|
||||
"IAm": "Je suis",
|
||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||
"birthDate": "Date de naissance",
|
||||
"nationality": "Nationalité",
|
||||
"descriptionBottom": "J'apprends en ligne l'informatique et les langages de programmation pour m'améliorer dans ma passion. <br/> <br/> J'ai conçu ma charte graphique et mon site internet."
|
||||
},
|
||||
"interests": {
|
||||
"title": "Mes intérêts",
|
||||
"paragraphs": [
|
||||
{
|
||||
"title": "Développeur Full Stack Junior :",
|
||||
"description": "La programmation informatique est ma principale passion, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi du Python et d'autres langages de programmation."
|
||||
},
|
||||
{
|
||||
"title": "Passionné de High-Tech :",
|
||||
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et même meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
|
||||
},
|
||||
{
|
||||
"title": "Enthousiaste de l'Open-Source :",
|
||||
"description": "Pour moi, tout le monde devrait travailler, résoudre des problèmes, construire des choses et réfléchir ensemble. Longue vie à l'open-source, chaque fois que vous pouvez partagez votre travail, faites-le! <br/> Le site est open-source sur <a href='https://github.com/Divlo/divlo.fr' target='_blank' rel='noopener noreferrer'>github</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"title": "Mes compétences",
|
||||
"languages": "Langages de programmation",
|
||||
"softwareTools": "Logiciels et outils"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Mon Portfolio",
|
||||
"items": [
|
||||
{
|
||||
"title": "function.divlo.fr",
|
||||
"description": "Apprenez la programmation grâce à l'apprentissage par projet alias fonction.",
|
||||
"link": "https://function.divlo.fr/",
|
||||
"image": "/images/portfolio/functiondivlofr.png"
|
||||
},
|
||||
{
|
||||
"title": "thream.divlo.fr",
|
||||
"description": "Votre plateforme open source pour rester proche de vos amis et communautés, parler, discuter, collaborer, partager et vous amuser.",
|
||||
"link": "https://thream.divlo.fr/",
|
||||
"image": "/images/portfolio/threamdivlofr.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contactez-Moi",
|
||||
"nameField": "Nom",
|
||||
"subjectField": "Objet",
|
||||
"sendEmail": "Envoyer l'email",
|
||||
"result": {
|
||||
"loading": "Chargement...",
|
||||
"success": "Votre email a été envoyé!",
|
||||
"requiredFields": "Vous devez remplir tous les champs...",
|
||||
"invalidEmail": "Veuillez entrer une adresse mail valide...",
|
||||
"serverError": "Le serveur n'a pas pu traiter votre requête..."
|
||||
},
|
||||
"error": "Erreur",
|
||||
"success": "Succès"
|
||||
}
|
||||
}
|
26
locales/fr/setup.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "Setup de Divlo",
|
||||
"description": "La liste de tout le matériel informatique dont dispose Divlo.",
|
||||
"configPC": {
|
||||
"title": "Configuration matérielle du PC",
|
||||
"motherboard": "Carte mère",
|
||||
"processor": "Processeur",
|
||||
"graphicCard": "Carte graphique",
|
||||
"ramMemory": "Mémoires Ram",
|
||||
"hardDrive": "Disques Dur"
|
||||
},
|
||||
"peripheral": {
|
||||
"title": "Périphériques",
|
||||
"keyboard": "Clavier",
|
||||
"mouse": "Souris",
|
||||
"headset": "Casque Micro",
|
||||
"mainScreen": "Écran Principal",
|
||||
"secondScreen": "2ème écran"
|
||||
},
|
||||
"officeOther": {
|
||||
"title": "Bureautique / Autre",
|
||||
"mousepad": "Tapis de souris",
|
||||
"usb": "Clé USB"
|
||||
},
|
||||
"connexion": "Ma connection internet"
|
||||
}
|
2
next-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
11
next.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
const nextPWA = require('next-pwa')
|
||||
const nextTranslate = require('next-translate')
|
||||
|
||||
module.exports = nextTranslate(
|
||||
nextPWA({
|
||||
pwa: {
|
||||
disable: process.env.NODE_ENV !== 'production',
|
||||
dest: 'public'
|
||||
}
|
||||
})
|
||||
)
|
26928
package-lock.json
generated
Normal file
75
package.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "divlo",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"ts-standard": {
|
||||
"ignore": [
|
||||
".next",
|
||||
".lighthouseci",
|
||||
"node_modules",
|
||||
"next-env.d.ts",
|
||||
"**/workbox-*.js",
|
||||
"**/sw.js"
|
||||
],
|
||||
"envs": [
|
||||
"node",
|
||||
"browser"
|
||||
],
|
||||
"report": "stylish"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"start": "next start",
|
||||
"build": "next build",
|
||||
"export": "next export",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:docker": "dockerfilelint './Dockerfile'",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
||||
"lint:typescript": "ts-standard",
|
||||
"lighthouse": "lhci autorun",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/montserrat": "4.2.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"axios": "0.21.1",
|
||||
"classnames": "2.3.1",
|
||||
"html-react-parser": "1.2.5",
|
||||
"next": "10.1.3",
|
||||
"next-pwa": "5.2.10",
|
||||
"next-translate": "1.0.6",
|
||||
"nodemailer": "6.5.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"react": "17.0.2",
|
||||
"react-component-form": "1.3.0",
|
||||
"react-dom": "17.0.2",
|
||||
"universal-cookie": "4.0.4",
|
||||
"validator": "13.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "12.1.1",
|
||||
"@commitlint/config-conventional": "12.1.1",
|
||||
"@fullhuman/postcss-purgecss": "4.0.3",
|
||||
"@lhci/cli": "0.7.1",
|
||||
"@styled-jsx/plugin-sass": "3.0.0",
|
||||
"@types/node": "14.14.41",
|
||||
"@types/nodemailer": "6.4.1",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/styled-jsx": "2.2.8",
|
||||
"@types/validator": "13.1.3",
|
||||
"dockerfilelint": "1.8.0",
|
||||
"editorconfig-checker": "4.0.2",
|
||||
"husky": "6.0.0",
|
||||
"markdownlint-cli": "0.27.1",
|
||||
"postcss": "8.2.10",
|
||||
"sass": "1.32.10",
|
||||
"ts-standard": "10.0.0",
|
||||
"typescript": "4.2.4"
|
||||
}
|
||||
}
|
23
pages/404.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import Head from 'components/Head'
|
||||
|
||||
const Error404: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Divlo - 404' />
|
||||
|
||||
<ErrorPage statusCode={404} message={t('errors:notFound')} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Error404
|
23
pages/500.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
import Head from 'components/Head'
|
||||
|
||||
const Error500: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Divlo - 500' />
|
||||
|
||||
<ErrorPage statusCode={500} message={t('errors:serverError')} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Error500
|
51
pages/_app.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { AppProps } from 'next/app'
|
||||
import Router from 'next/router'
|
||||
import NProgress from 'nprogress'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import UniversalCookie from 'universal-cookie'
|
||||
|
||||
import 'normalize.css/normalize.css'
|
||||
import '@fontsource/montserrat/400.css'
|
||||
import '@fontsource/montserrat/500.css'
|
||||
import '@fontsource/montserrat/600.css'
|
||||
import '@fontsource/montserrat/700.css'
|
||||
|
||||
import 'styles/grid.scss'
|
||||
import 'styles/general.scss'
|
||||
import 'styles/nprogress.scss'
|
||||
|
||||
import { Header } from 'components/Header'
|
||||
import { Footer } from 'components/Footer'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const universalCookie = new UniversalCookie()
|
||||
|
||||
/** how long in seconds, until the cookie expires (10 years) */
|
||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||
|
||||
Router.events.on('routeChangeStart', () => NProgress.start())
|
||||
Router.events.on('routeChangeComplete', () => NProgress.done())
|
||||
Router.events.on('routeChangeError', () => NProgress.done())
|
||||
|
||||
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const { lang } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
universalCookie.set('NEXT_LOCALE', lang, {
|
||||
path: '/',
|
||||
maxAge: COOKIE_MAX_AGE
|
||||
})
|
||||
}, [lang])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className='content container'>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
69
pages/api/send-email.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import nodemailer from 'nodemailer'
|
||||
import validator from 'validator'
|
||||
|
||||
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
|
||||
|
||||
const emailTransporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: EMAIL_PORT,
|
||||
secure: EMAIL_PORT === 465,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
})
|
||||
|
||||
export default async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<any> => {
|
||||
if (req.method !== 'POST') {
|
||||
return res.redirect('/404')
|
||||
}
|
||||
|
||||
let { name, email, subject, message } = req.body as {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}
|
||||
|
||||
if (
|
||||
validator.isEmpty(name) ||
|
||||
validator.isEmpty(email) ||
|
||||
validator.isEmpty(subject) ||
|
||||
validator.isEmpty(message)
|
||||
) {
|
||||
return res.status(400).json({ type: 'requiredFields' })
|
||||
}
|
||||
|
||||
if (!validator.isEmail(email)) {
|
||||
return res.status(400).json({ type: 'invalidEmail' })
|
||||
}
|
||||
|
||||
email = validator.normalizeEmail(email) as string
|
||||
message = validator.trim(message)
|
||||
message = validator.escape(message)
|
||||
subject = validator.trim(subject)
|
||||
subject = validator.escape(subject)
|
||||
|
||||
try {
|
||||
await emailTransporter.sendMail({
|
||||
from: '"Divlo" <contact@divlo.fr>',
|
||||
to: email,
|
||||
subject: `Contact - ${subject}`,
|
||||
html: `
|
||||
<b>Name:</b> ${name} <br/>
|
||||
<b>Email:</b> ${email} <br/>
|
||||
<b>Message:</b> ${message}
|
||||
`
|
||||
})
|
||||
return res.status(201).json({ type: 'success' })
|
||||
} catch {
|
||||
return res.status(500).json({ type: 'serverError' })
|
||||
}
|
||||
}
|
61
pages/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Contact } from 'components/Contact'
|
||||
import { RevealFade } from 'components/design/RevealFade'
|
||||
import { Section } from 'components/design/Section'
|
||||
import Head from 'components/Head'
|
||||
import { Interests } from 'components/Interests'
|
||||
import { Portfolio } from 'components/Portfolio'
|
||||
import { Profile } from 'components/Profile'
|
||||
import { SocialMediaList } from 'components/Profile/SocialMediaList'
|
||||
import { Skills } from 'components/Skills'
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head />
|
||||
|
||||
<Section isMain id='about'>
|
||||
<Profile />
|
||||
<SocialMediaList />
|
||||
</Section>
|
||||
|
||||
<RevealFade>
|
||||
<Section id='interests' heading={t('home:interests.title')}>
|
||||
<Interests />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section id='skills' heading={t('home:skills.title')} withoutShadowContainer>
|
||||
<Skills />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section
|
||||
id='portfolio'
|
||||
heading={t('home:portfolio.title')}
|
||||
withoutShadowContainer
|
||||
>
|
||||
<Portfolio />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
|
||||
<RevealFade>
|
||||
<Section id='contact' heading={t('home:contact.title')}>
|
||||
<Contact />
|
||||
</Section>
|
||||
</RevealFade>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Home
|
31
pages/setup.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Section } from 'components/design/Section'
|
||||
import Head from 'components/Head'
|
||||
import { Setup } from 'components/Setup'
|
||||
|
||||
const SetupPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={t('setup:title')} description={t('setup:description')} />
|
||||
|
||||
<Section
|
||||
id='setup'
|
||||
style={{ marginTop: 60 }}
|
||||
description={t('setup:description')}
|
||||
heading={t('setup:title')}
|
||||
>
|
||||
<Setup />
|
||||
</Section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default SetupPage
|
15
postcss.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@fullhuman/postcss-purgecss',
|
||||
{
|
||||
content: [
|
||||
'./pages/**/*.{js,jsx,ts,tsx}',
|
||||
'./components/**/*.{js,jsx,ts,tsx}'
|
||||
],
|
||||
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
|
||||
safelist: ['html', 'body']
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
BIN
public/images/divlo_brand-logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/divlo_icon_small.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/divlo_logo.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
public/images/error.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
public/images/flags/english_flag.png
Normal file
After Width: | Height: | Size: 525 B |
BIN
public/images/flags/french_flag.png
Normal file
After Width: | Height: | Size: 127 B |
BIN
public/images/icons/icon-128x128.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/images/icons/icon-144x144.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/icons/icon-152x152.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/icons/icon-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/images/icons/icon-384x384.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/images/icons/icon-512x512.png
Normal file
After Width: | Height: | Size: 49 KiB |