1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2024-12-08 00:44:30 +01:00

feat: add divlo.fr

This commit is contained in:
divlo 2021-04-18 01:56:23 +02:00
parent 3072daa443
commit c2f762ac68
134 changed files with 31003 additions and 3 deletions

12
.babelrc.json Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"next/babel",
{
"styled-jsx": {
"plugins": ["@styled-jsx/plugin-sass"]
}
}
]
]
}

1
.commitlintrc.json Normal file
View File

@ -0,0 +1 @@
{ "extends": ["@commitlint/config-conventional"] }

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules
build
dist
out
.next

11
.editorconfig Normal file
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
_

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:commit -- --edit

7
.husky/pre-commit Executable file
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false
}

1
.npmrc Normal file
View File

@ -0,0 +1 @@
save-exact=true

132
CODE_OF_CONDUCT.md Normal file
View 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
View 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
View 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}"]

View File

@ -1,6 +1,4 @@
# Welcome to my GitHub Account! 👋
![Divlo](./images/capture.png)
# Divlo
## Social Media

View 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>
)
}

View 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>
</>
)
}

View 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
View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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
View 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

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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
View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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

View 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>
</>
)
}
)

View 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>
</>
)
})

View 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>
</>
)
}

View 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>
</>
)
})

View 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>
)
})

View 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>
</>
)
}

View 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>
</>
)
}
)

View 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
View 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
View File

@ -0,0 +1,11 @@
{
"locales": ["en", "fr"],
"defaultLocale": "en",
"pages": {
"*": ["common"],
"/": ["home"],
"/setup": ["setup"],
"/404": ["errors"],
"/500": ["errors"]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

6
locales/en/common.json Normal file
View File

@ -0,0 +1,6 @@
{
"english": "English",
"french": "French",
"allRightsReserved": "All rights reserved",
"home": "Home"
}

6
locales/en/errors.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

11
next.config.js Normal file
View 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

File diff suppressed because it is too large Load Diff

75
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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']
}
]
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
public/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Some files were not shown because too many files have changed in this diff Show More