1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

Compare commits

...

19 Commits

Author SHA1 Message Date
cd20e25082 fix: update content 2021-05-08 21:09:03 +02:00
c62e66a86a feat: add light mode + rewrite in Tailwind CSS (#15) 2021-05-08 19:52:04 +02:00
26f24329c7 ci(dependabot): temporary disabled npm ecosystem 2021-04-29 16:21:07 +02:00
bb86fb500a build(deps-dev): bump @types/node from 14.14.41 to 15.0.0 (#9) 2021-04-27 10:44:27 +02:00
b78e8b1e02 build(deps-dev): bump @types/jest from 26.0.22 to 26.0.23 (#7) 2021-04-27 10:32:59 +02:00
2e87d6b51f build(deps-dev): bump @types/react from 17.0.3 to 17.0.4 (#8) 2021-04-27 10:32:48 +02:00
4439e73986 build(deps-dev): bump postcss from 8.2.12 to 8.2.13 (#6) 2021-04-27 10:32:37 +02:00
af065afe67 build(deps): bump next-pwa to 5.2.15 2021-04-24 11:29:44 +02:00
088510a62b build(deps-dev): bump postcss from 8.2.10 to 8.2.12 (#5) 2021-04-23 14:19:05 +02:00
e0150361b1 build(deps-dev): bump @testing-library/jest-dom from 5.11.10 to 5.12.0 (#4) 2021-04-23 14:18:52 +02:00
4b7d184c91 fix(pages/api): escape html in send-email 2021-04-20 19:56:02 +02:00
3e18536c2e chore: usage of GitHub flow instead of Git flow 2021-04-20 19:47:57 +02:00
8027e2deed test: improve coverage 2021-04-20 18:23:05 +02:00
c21abdcc81 ci: add codeql-analysis 2021-04-20 17:32:53 +02:00
3382177e27 fix(components): returnToHomePage inside ErrorPage 2021-04-20 15:47:52 +02:00
73352d4414 chore: add vercel-deploy.sh 2021-04-20 15:45:47 +02:00
f11425b2c2 chore: prebuilds Gitpod with everything to true 2021-04-20 03:44:54 +00:00
039f4d172b chore: add prebuilds for Gitpod 2021-04-20 03:31:43 +00:00
b0a608c1fa chore: add Gitpod 2021-04-20 05:18:03 +02:00
92 changed files with 15746 additions and 10385 deletions

View File

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

View File

@ -1,6 +1,2 @@
COMPOSE_PROJECT_NAME=divlo.fr-website COMPOSE_PROJECT_NAME=divlo.fr
PORT=3000 PORT=3000
EMAIL_HOST=divlo.fr-maildev
EMAIL_USER=reply@divlo-website.fr
EMAIL_PASSWORD=password
EMAIL_PORT=25

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
.next
.lighthouseci
node_modules
next-env.d.ts
**/workbox-*.js
**/sw.js

15
.eslintrc.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": ["standard-with-typescript", "eslint-config-prettier"],
"plugins": ["eslint-plugin-prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"node": true,
"browser": true,
"jest": true
},
"rules": {
"prettier/prettier": "error"
}
}

View File

@ -2,9 +2,9 @@ name: 'Divlo'
on: on:
push: push:
branches: [master, development] branches: [master]
pull_request: pull_request:
branches: [master, development] branches: [master]
jobs: jobs:
ci: ci:

28
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: 'CodeQL'
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
analyze:
name: 'Analyze'
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
matrix:
language: ['javascript']
steps:
- uses: 'actions/checkout@v2'
- name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@v1'
with:
languages: ${{ matrix.language }}
- name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@v1'

View File

@ -1,4 +1,4 @@
name: 'release' name: 'Release'
on: on:
workflow_run: workflow_run:

19
.gitignore vendored
View File

@ -26,10 +26,21 @@ coverage
# debug # debug
npm-debug.log* npm-debug.log*
# editors # IDEs and editors
.vscode /.idea
.theia .project
.idea .classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc # misc
.DS_Store .DS_Store

20
.gitpod.yml Normal file
View File

@ -0,0 +1,20 @@
image: 'gitpod/workspace-full'
tasks:
- before: 'cp .env.example .env && npm install --global npm@7'
init: 'npm clean-install'
command: 'npm run dev'
ports:
- port: 3000
onOpen: 'open-preview'
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addComment: true
addBadge: true
addLabel: true

View File

@ -1,7 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
npm run lint:docker npm run lint:staged
npm run lint:editorconfig
npm run lint:markdown
npm run lint:typescript

11
.lintstagedrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"*": ["editorconfig-checker"],
"*.{js,ts,jsx,tsx}": [
"prettier --write",
"eslint --fix",
"jest --findRelatedTests"
],
"*.{css,yml,json}": ["prettier --write"],
"*.{md}": ["prettier --write", "markdownlint --dot --fix"],
"./Dockerfile": ["dockerfilelint"]
}

8
.prettierignore Normal file
View File

@ -0,0 +1,8 @@
.next
.lighthouseci
node_modules
next-env.d.ts
package.json
package-lock.json
**/workbox-*.js
**/sw.js

6
.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
}

View File

@ -1,11 +1,11 @@
{ {
"release": {
"branches": ["master"]
},
"plugins": [ "plugins": [
["@semantic-release/commit-analyzer", { [
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits" "preset": "conventionalcommits"
}], }
],
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@semantic-release/github" "@semantic-release/github"
] ]

13
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"recommendations": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax",
"divlo.vscode-styled-jsx-languageserver",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"coenraads.bracket-pair-colorizer",
"davidanson.vscode-markdownlint"
]
}

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.configPath": ".prettierrc.json",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}

View File

@ -47,6 +47,8 @@ Scopes define what part of the code changed.
## Getting Started ## Getting Started
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Divlo/Divlo)
### Installation ### Installation
```sh ```sh
@ -70,4 +72,3 @@ docker-compose up --build
### Services started ### Services started
- website : `http://localhost:3000` - website : `http://localhost:3000`
- [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`

View File

@ -1,10 +1,11 @@
FROM node:14.16.1 FROM node:16.1.0
RUN npm install --global npm@7
WORKDIR /app WORKDIR /usr/src/app
RUN chown --recursive node:node /usr/src/app
COPY ./package*.json ./ COPY --chown=node:node ./package*.json ./
RUN npm install RUN npm install
COPY ./ ./ COPY --chown=node:node ./ ./
CMD ["npm", "run", "dev", "--", "--port", "${PORT}"] USER node
RUN npm run build
CMD ["npm", "run", "start", "--", "--port", "${PORT}"]

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import Error404 from 'pages/404'
describe('GET /404', () => {
it('should render', async () => {
const { getByText } = render(<Error404 />)
expect(getByText('404')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import Error500 from 'pages/500'
describe('GET /500', () => {
it('should render', async () => {
const { getByText } = render(<Error500 />)
expect(getByText('500')).toBeInTheDocument()
})
})

View File

@ -1,70 +0,0 @@
import { createMocks } from 'node-mocks-http'
import handleSendEmail from 'pages/api/send-email'
jest.mock('nodemailer', () => ({
createTransport: () => {
return {
sendMail: jest.fn(async () => {})
}
}
}))
describe('POST /api/send-email', () => {
it('succeeds and send the email', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Divlo',
email: 'contact@divlo.fr',
subject: 'Subject',
message: 'Hello world!'
}
})
await handleSendEmail(req, res)
expect(res._getStatusCode()).toBe(201)
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
type: 'success'
})
)
})
it('fails with empty values', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: '',
email: '',
subject: '',
message: ''
}
})
await handleSendEmail(req, res)
expect(res._getStatusCode()).toBe(400)
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
type: 'requiredFields'
})
)
})
it('fails with invalid email', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Name',
email: 'random wrong email',
subject: 'Subject',
message: 'Message'
}
})
await handleSendEmail(req, res)
expect(res._getStatusCode()).toBe(400)
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
type: 'invalidEmail'
})
)
})
})

View File

@ -1,31 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
import { FormState } from './FormState'
import { ResultState } from './index'
export interface FormResultProps {
state: ResultState
}
export const FormResult: React.FC<FormResultProps> = (props) => {
const { state } = props
const { t } = useTranslation()
if (state === 'idle') {
return null
}
if (state === 'loading' || state === 'success') {
return (
<FormState state={state}>
{t(`home:contact.result.${state}`)}
</FormState>
)
}
return (
<FormState state='error'>
{t(`home:contact.result.${state}`)}
</FormState>
)
}

View File

@ -1,39 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
export interface FormStateProps extends React.ComponentPropsWithRef<'p'> {
state: 'success' | 'error' | 'loading'
children: string
}
export const FormState: React.FC<FormStateProps> = props => {
const { state, children, ...rest } = props
const { t } = useTranslation()
return (
<>
<div className='form-result text-center'>
<p className={state} {...rest}>
{['error', 'success'].includes(state) && (
<b>
{state === 'error' ? t('home:contact.error') : t('home:contact.success')}:
</b>
)}{' '}
{children}
</p>
</div>
<style jsx>{`
.form-result {
margin: 30px;
}
.success {
color: #90ee90;
}
.error {
color: #ff7f7f;
}
`}
</style>
</>
)
}

View File

@ -1,89 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
import { useState } from 'react'
import Form, { HandleForm } from 'react-component-form'
import axios from 'axios'
import { Input } from 'components/design/Input'
import { Button } from 'components/design/Button'
import { Textarea } from 'components/design/Textarea'
import { FormResult } from './FormResult'
export const resultState = [
'idle',
'success',
'loading',
'requiredFields',
'invalidEmail',
'serverError'
] as const
export type ResultState = typeof resultState[number]
export const Contact: React.FC = () => {
const { t } = useTranslation()
const [state, setState] = useState<ResultState>('idle')
const handleSubmit: HandleForm = async (formData, formElement) => {
setState('loading')
try {
const { data } = await axios.post<{ type: ResultState }>(
'/api/send-email',
formData
)
if (data.type === 'success') {
setState('success')
return formElement.reset()
}
return setState('serverError')
} catch (error) {
const type = error.response.data.type
if (resultState.includes(type)) {
return setState(type)
}
return setState('serverError')
}
}
return (
<>
<div className='col-24'>
<Form onSubmit={handleSubmit}>
<Input
label={`${t('home:contact.nameField')} :`}
type='text'
name='name'
autoComplete='off'
required
/>
<Input
label='Email :'
type='email'
name='email'
autoComplete='off'
required
/>
<Input
label={`${t('home:contact.subjectField')} :`}
type='text'
name='subject'
autoComplete='off'
required
/>
<Textarea
label='Message :'
name='message'
autoComplete='off'
required
/>
<div className='text-center' style={{ marginBottom: 20 }}>
<Button type='submit'>{t('home:contact.sendEmail')}</Button>
</div>
</Form>
<FormResult state={state} />
</div>
</>
)
}

View File

@ -6,30 +6,40 @@ export interface ErrorPageProps {
message: string message: string
} }
export const ErrorPage: React.FC<ErrorPageProps> = props => { export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
const { message, statusCode } = props const { message, statusCode } = props
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<> <>
<h1> <h1 className='my-6 font-semibold text-4xl'>
{t('errors:error')} <span className='important'>{statusCode}</span> {t('errors:error')}{' '}
<span className='text-yellow dark:text-yellow-dark'>{statusCode}</span>
</h1> </h1>
<p className='text-center'> <p className='text-center text-lg'>
{message} <Link href='/'>{t('returnToHomePage')}</Link> {message}{' '}
<Link href='/'>
<a className='text-yellow dark:text-yellow-dark hover:underline'>
{t('errors:returnToHomePage')}
</a>
</Link>
</p> </p>
<style jsx global>{` <style jsx global>
.content { {`
main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-width: 100vw; min-width: 100vw;
min-height: 100%; flex: 1;
} }
#__next { #__next {
display: flex;
flex-direction: column;
padding-top: 0; padding-top: 0;
height: 100vh;
} }
`} `}
</style> </style>

14
components/Footer.tsx Normal file
View File

@ -0,0 +1,14 @@
import useTranslation from 'next-translate/useTranslation'
export const Footer: React.FC = () => {
const { t } = useTranslation()
return (
<footer className='bg-white flex justify-center py-6 text-lg border-t-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
<p>
<span className='text-yellow dark:text-yellow-dark'>Divlo</span> |{' '}
{t('common:allRightsReserved')}
</p>
</footer>
)
}

View File

@ -1,28 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
export const Footer: React.FC = () => {
const { t } = useTranslation()
return (
<>
<footer className='Footer text-center'>
<p>
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
</p>
</footer>
<style jsx>
{`
.Footer {
border-top: var(--border-header-footer);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px;
}
`}
</style>
</>
)
}

View File

@ -7,12 +7,12 @@ interface HeadProps {
url?: string url?: string
} }
export const Head: React.FC<HeadProps> = props => { export const Head: React.FC<HeadProps> = (props) => {
const { const {
title = 'Divlo', title = 'Divlo',
image = '/images/icons/icon-96x96.png', image = '/images/icons/icon-96x96.png',
description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech", description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
url = 'https://divlo.divlo.fr/' url = 'https://divlo.fr/'
} = props } = props
return ( return (
@ -21,7 +21,7 @@ export const Head: React.FC<HeadProps> = props => {
<link rel='icon' type='image/png' href={image} /> <link rel='icon' type='image/png' href={image} />
{/* Meta Tag */} {/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1' /> <meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content={description} /> <meta name='description' content={description} />
<meta name='Language' content='fr, en' /> <meta name='Language' content='fr, en' />
<meta name='theme-color' content='#ffd800' /> <meta name='theme-color' content='#ffd800' />

View File

@ -8,8 +8,8 @@ export const Arrow: React.FC = () => {
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
> >
<path <path
className='fill-current text-black dark:text-white'
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z' d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
fill='#fff'
/> />
</svg> </svg>
) )

View File

@ -15,17 +15,7 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
src={`/images/languages/${language}.svg`} src={`/images/languages/${language}.svg`}
alt={language} alt={language}
/> />
<p className='language-title'>{language.toUpperCase()}</p> <p className='mx-2 text-base'>{language.toUpperCase()}</p>
<style jsx>
{`
.language-title {
margin: 0 8px 0 10px;
font-size: 16px;
font-family: 'Arial', 'sans-serif';
}
`}
</style>
</> </>
) )
} }

View File

@ -32,14 +32,13 @@ export const Language: React.FC = () => {
} }
return ( return (
<> <div className='flex flex-col justify-center items-center cursor-pointer'>
<div className='language-menu'> <div className='flex items-center mr-5' onClick={handleHiddenMenu}>
<div className='selected-language' onClick={handleHiddenMenu}>
<LanguageFlag language={currentLanguage} /> <LanguageFlag language={currentLanguage} />
<Arrow /> <Arrow />
</div> </div>
{!hiddenMenu && ( {!hiddenMenu && (
<ul> <ul className='flex flex-col justify-center items-center absolute p-0 top-14 z-10 w-24 mt-3 mr-4 rounded-lg list-none shadow-light dark:shadow-dark bg-white dark:bg-black'>
{locales.map((language, index) => { {locales.map((language, index) => {
if (language === currentLanguage) { if (language === currentLanguage) {
return null return null
@ -47,6 +46,7 @@ export const Language: React.FC = () => {
return ( return (
<li <li
key={index} key={index}
className='flex items-center justify-center w-full h-12 hover:bg-[#4f545c] hover:bg-opacity-20 pl-2'
onClick={async () => await handleLanguage(language)} onClick={async () => await handleLanguage(language)}
> >
<LanguageFlag language={language} /> <LanguageFlag language={language} />
@ -56,50 +56,5 @@ export const Language: React.FC = () => {
</ul> </ul>
)} )}
</div> </div>
<style jsx>
{`
.language-menu {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
.selected-language {
display: flex;
align-items: center;
margin-right: 15px;
}
ul {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
top: 60px;
width: 100px;
padding: 10px;
margin: 10px 15px 0 0px;
border-radius: 15%;
padding: 0;
box-shadow: 0px 1px 10px var(--color-shadow);
background-color: var(--color-background-primary);
z-index: 10;
}
ul > li {
list-style: none;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
width: 100%;
}
ul > li:hover {
background-color: rgba(79, 84, 92, 0.16);
}
`}
</style>
</>
) )
} }

View File

@ -0,0 +1,127 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<>
<div
className='toggle-button'
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<div className='toggle-theme-button'>
<div className='toggle-track'>
<div className='toggle-track-check'>
<span className='toggle_Dark'>🌜</span>
</div>
<div className='toggle-track-x'>
<span className='toggle_Light'>🌞</span>
</div>
</div>
<div className='toggle-thumb' />
<input
type='checkbox'
aria-label='Dark mode toggle'
className='toggle-screenreader-only'
defaultChecked
/>
</div>
</div>
<style jsx>
{`
.toggle-button {
display: flex;
align-items: center;
}
.toggle-theme-button {
touch-action: pan-x;
display: inline-block;
position: relative;
cursor: pointer;
background-color: transparent;
border: 0;
padding: 0;
user-select: none;
}
.toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: #4d4d4d;
transition: all 0.2s ease;
color: #fff;
}
.toggle-track-check {
position: absolute;
width: 14px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
left: 8px;
opacity: ${theme === 'dark' ? 1 : 0};
transition: opacity 0.25s ease;
}
.toggle-track-x {
position: absolute;
width: 10px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
right: 10px;
opacity: ${theme === 'dark' ? 0 : 1};
}
.toggle_Dark,
.toggle_Light {
align-items: center;
display: flex;
height: 10px;
justify-content: center;
position: relative;
width: 10px;
}
.toggle-thumb {
position: absolute;
left: ${theme === 'dark' ? '27px' : '0px'};
width: 22px;
height: 22px;
border: 1px solid #4d4d4d;
border-radius: 50%;
background-color: #fafafa;
box-sizing: border-box;
transition: all 0.25s ease;
top: 1px;
color: #fff;
}
.toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import { Header } from '..'
describe('<Header />', () => {
it('should render', async () => {
const { getByText } = render(<Header />)
expect(getByText('Divlo')).toBeInTheDocument()
})
})

View File

@ -2,87 +2,30 @@ import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import { Language } from './Language' import { Language } from './Language'
import { SwitchTheme } from './SwitchTheme'
export const Header: React.FC = () => { export const Header: React.FC = () => {
return ( return (
<> <header className='bg-white sticky top-0 z-50 flex w-full justify-between px-6 py-2 border-b-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
<header className='header'>
<div className='container'>
<nav className='navbar navbar-fixed-top'>
<Link href='/'> <Link href='/'>
<a className='navbar__brand-link'> <a>
<div className='navbar__brand'> <div className='flex items-center justify-center'>
<Image <Image
width={60} width={60}
height={60} height={60}
src='/images/divlo_icon_small.png' src='/images/divlo_icon_small.png'
alt='Divlo' alt='Divlo'
/> />
<strong className='navbar__brand-title'>Divlo</strong> <strong className='ml-1 font-headline font-semibold hidden xs:block'>
Divlo
</strong>
</div> </div>
</a> </a>
</Link> </Link>
<div className='navbar__buttons'> <div className='flex justify-between'>
<Language /> <Language />
</div> <SwitchTheme />
</nav>
</div> </div>
</header> </header>
<style jsx>
{`
.header {
background-color: var(--color-background);
border-bottom: var(--border-header-footer);
padding: 0.5rem 1rem;
position: fixed;
width: 100%;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: var(--header-height);
}
.container {
max-width: 1280px;
width: 100%;
margin: auto;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-fixed-top {
position: sticky;
top: 0;
z-index: 200;
}
.navbar__brand-link {
color: var(--color-text-1);
text-decoration: none;
font-size: 16px;
}
.navbar__brand {
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar__brand-title {
font-weight: 600;
margin-left: 10px;
}
.navbar__buttons {
display: flex;
justify-content: space-between;
}
@media (max-width: 320px) {
.navbar__brand-title {
display: none;
}
}
`}
</style>
</>
) )
} }

View File

@ -10,8 +10,10 @@ export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
return ( return (
<> <>
<p className='text-center'> <p className='text-center my-6 text-gray dark:text-gray-dark'>
<strong className='important'>{title}</strong> <strong className='text-yellow font-medium text-lg dark:text-yellow-dark'>
{title}
</strong>
<br /> <br />
<span className='paragraph-color'>{htmlParser(description)}</span> <span className='paragraph-color'>{htmlParser(description)}</span>
</p> </p>

View File

@ -1,41 +1,20 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { Tooltip } from 'components/design/Tooltip'
interface InterestItemProps { interface InterestItemProps {
title: string title: string
fontAwesomeIcon: IconDefinition fontAwesomeIcon: IconDefinition
} }
export const InterestItem: React.FC<InterestItemProps> = props => { export const InterestItem: React.FC<InterestItemProps> = (props) => {
const { fontAwesomeIcon, title } = props const { fontAwesomeIcon, title } = props
return ( return (
<> <li className='interest-item my-2 mx-2 w-8 h-8' title={title}>
<li className='interest-item'>
<Tooltip title={title}>
<FontAwesomeIcon <FontAwesomeIcon
className='color-primary' className='text-yellow cursor-pointer h-full w-full block dark:text-yellow-dark'
style={{
cursor: 'pointer',
height: '100%',
width: '100%',
display: 'block'
}}
icon={fontAwesomeIcon} icon={fontAwesomeIcon}
/> />
</Tooltip>
</li> </li>
<style jsx>
{`
.interest-item {
margin: 7px 5px;
width: 34px;
height: 34px;
}
`}
</style>
</>
) )
} }

View File

@ -5,9 +5,8 @@ import { InterestItem } from './InterestItem'
export const InterestsList: React.FC = () => { export const InterestsList: React.FC = () => {
return ( return (
<> <div className='flex justify-center my-4'>
<div className='container-list'> <ul className='flex justify-around p-0 m-0 list-none w-96'>
<ul className='interests-list'>
<InterestItem <InterestItem
title='Developer Full Stack Junior' title='Developer Full Stack Junior'
fontAwesomeIcon={faCode} fontAwesomeIcon={faCode}
@ -16,30 +15,8 @@ export const InterestsList: React.FC = () => {
title='Passionate about High-Tech' title='Passionate about High-Tech'
fontAwesomeIcon={faMicrochip} fontAwesomeIcon={faMicrochip}
/> />
<InterestItem <InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
title='Open-Source enthusiast'
fontAwesomeIcon={faGit}
/>
</ul> </ul>
</div> </div>
<style jsx>
{`
.container-list {
display: flex;
justify-content: center;
margin: 15px 0 15px 0;
}
.interests-list {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
width: 60%;
list-style: none;
}
`}
</style>
</>
) )
} }

View File

@ -6,13 +6,17 @@ import { InterestsList } from './InterestsList'
export const Interests: React.FC = () => { export const Interests: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const paragraphs: InterestParagraphProps[] = t('home:interests.paragraphs', {}, { const paragraphs: InterestParagraphProps[] = t(
'home:interests.paragraphs',
{},
{
returnObjects: true returnObjects: true
}) }
)
return ( return (
<> <>
<div className='col-24'> <div className='max-w-full'>
{paragraphs.map((paragraph, index) => { {paragraphs.map((paragraph, index) => {
return <InterestParagraph key={index} {...paragraph} /> return <InterestParagraph key={index} {...paragraph} />
})} })}

View File

@ -1,3 +1,4 @@
import { ShadowContainer } from 'components/design/ShadowContainer'
import Image from 'next/image' import Image from 'next/image'
export interface PortfolioItemProps { export interface PortfolioItemProps {
@ -7,96 +8,34 @@ export interface PortfolioItemProps {
image: string image: string
} }
export const PortfolioItem: React.FC<PortfolioItemProps> = props => { export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
const { title, description, link, image } = props const { title, description, link, image } = props
return ( return (
<> <ShadowContainer className='cursor-pointer relative items-center sm:ml-10'>
<div className='col-sm-24 col-md-10 col-xl-7 portfolio-grid'>
<a <a
className='portfolio-link' className='group inline-flex justify-center'
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
href={link} href={link}
aria-label={title} aria-label={title}
> >
<div className='portfolio-figure'> <div className='flex justify-center'>
<Image width={300} height={300} src={image} alt={title} /> <Image
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
width={300}
height={300}
src={image}
alt={title}
/>
</div> </div>
<div className='portfolio-caption'> <div className='opacity-0 transition-opacity duration-500 h-auto absolute text-center overflow-hidden bottom-0 group-hover:opacity-100'>
<h3 className='portfolio-title important'>{title}</h3> <h3 className='text-yellow text-xl font-semibold my-6 dark:text-yellow-dark'>
<p className='portfolio-description'>{description}</p> {title}
</h3>
<p className='my-6'>{description}</p>
</div> </div>
</a> </a>
</div> </ShadowContainer>
<style jsx global>
{`
.portfolio-figure img[alt='${title}'] {
max-height: 300px;
max-width: 300px;
transition: opacity 0.5s ease;
}
.portfolio-grid:hover img[alt='${title}'] {
opacity: 0.05;
}
`}
</style>
<style jsx>
{`
.portfolio-grid {
display: flex;
align-items: center;
position: relative;
flex-direction: column;
word-wrap: break-word;
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
border: 1px solid black;
border-radius: 1rem;
margin: 0 0 50px 0;
cursor: pointer;
}
/* col-md */
@media (min-width: 768px) {
.portfolio-grid {
margin: 0 30px 50px 30px;
}
}
/* col-xl */
@media (min-width: 1200px) {
.portfolio-grid {
margin: 0 20px 50px 20px;
}
}
.portfolio-figure {
display: flex;
justify-content: center;
}
.portfolio-caption {
transition: opacity 0.5s ease;
opacity: 0;
height: 0;
overflow: hidden;
}
.portfolio-description {
font-size: 16px;
}
.portfolio-grid:hover .portfolio-caption {
opacity: 1;
height: auto;
position: absolute;
bottom: 0;
text-align: center;
width: 80%;
}
.portfolio-grid:hover .portfolio-link {
color: var(--text-color);
display: flex;
justify-content: center;
}
`}
</style>
</>
) )
} }

View File

@ -5,19 +5,19 @@ import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
export const Portfolio: React.FC = () => { export const Portfolio: React.FC = () => {
const { t } = useTranslation('home') const { t } = useTranslation('home')
const items: PortfolioItemProps[] = t('home:portfolio.items', {}, { const items: PortfolioItemProps[] = t(
'home:portfolio.items',
{},
{
returnObjects: true returnObjects: true
}) }
)
return ( return (
<> <div className='flex flex-wrap justify-center px-3 w-full'>
<div className='container-fluid'>
<div className='row justify-content-center'>
{items.map((item, index) => { {items.map((item, index) => {
return <PortfolioItem key={index} {...item} /> return <PortfolioItem key={index} {...item} />
})} })}
</div> </div>
</div>
</>
) )
} }

View File

@ -2,27 +2,11 @@ import Translation from 'next-translate/Trans'
export const ProfileDescriptionBottom: React.FC = () => { export const ProfileDescriptionBottom: React.FC = () => {
return ( return (
<> <p className='mt-8 mb-8 font-normal text-base text-gray dark:text-gray-dark'>
<p className='profile-description-bottom'>
<Translation <Translation
i18nKey='home:about.descriptionBottom' i18nKey='home:about.descriptionBottom'
components={[<br key='break' />]} components={[<br key='break' />]}
/> />
</p> </p>
<style jsx>
{`
.profile-description-bottom {
font-size: 16px;
display: block;
font-weight: 400;
line-height: 25px;
color: #b2bac2;
margin-top: 30px;
margin-bottom: 0;
}
`}
</style>
</>
) )
} }

View File

@ -5,11 +5,14 @@ export const ProfileInfo: React.FC = () => {
return ( return (
<> <>
<div className='profile-info'> <div className='pb-2 mb-6 border-b-2 font-headline border-gray-600 dark:border-gray-400'>
<h1 className='profile-title'> <h1 className='text-4xl mb-2'>
{t('home:about.IAm')} <strong className='important'>Divlo</strong> {t('home:about.IAm')}{' '}
<strong className='font-semibold text-yellow dark:text-yellow-dark'>
Divlo
</strong>
</h1> </h1>
<h2 className='profile-description'>{t('home:about.description')}</h2> <h2 className='text-base mb-3'>{t('home:about.description')}</h2>
</div> </div>
<style jsx> <style jsx>

View File

@ -4,16 +4,18 @@ interface ProfileItemProps {
link?: string link?: string
} }
export const ProfileItem: React.FC<ProfileItemProps> = props => { export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
const { title, value, link } = props const { title, value, link } = props
return ( return (
<> <>
<li className='profile-list__item'> <li className='profile-list__item'>
<strong className='profile-list__item-title'>{title}</strong> <strong className='profile-list__item-title text-black dark:text-white'>
<span className='profile-list__item-info'> {title}
</strong>
<span className='profile-list__item-info text-gray dark:text-gray-dark'>
{link != null ? ( {link != null ? (
<a className='profile-list__link' href={link}> <a className='text-gray dark:text-gray-dark' href={link}>
{value} {value}
</a> </a>
) : ( ) : (
@ -39,7 +41,6 @@ export const ProfileItem: React.FC<ProfileItemProps> = props => {
display: block; display: block;
width: 120px; width: 120px;
float: left; float: left;
color: #d4d4d5;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
line-height: 20px; line-height: 20px;
@ -51,10 +52,6 @@ export const ProfileItem: React.FC<ProfileItemProps> = props => {
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
color: #84898e;
}
.profile-list__link {
color: #84898e;
} }
@media (max-width: 576px) { @media (max-width: 576px) {

View File

@ -6,32 +6,14 @@ export const ProfileList: React.FC = () => {
const { t } = useTranslation('home') const { t } = useTranslation('home')
return ( return (
<> <ul className='m-0 p-0 list-none'>
<ul className='profile-list'> <ProfileItem title={t('home:about.birthDate')} value='31/03/2003' />
<ProfileItem <ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
title={t('home:about.birthDate')}
value='31/03/2003'
/>
<ProfileItem
title={t('home:about.nationality')}
value='Alsace, France'
/>
<ProfileItem <ProfileItem
title='Email' title='Email'
value='contact@divlo.fr' value='contact@divlo.fr'
link='mailto:contact@divlo.fr' link='mailto:contact@divlo.fr'
/> />
</ul> </ul>
<style jsx>
{`
.profile-list {
margin: 0;
padding: 0;
list-style: none;
}
`}
</style>
</>
) )
} }

View File

@ -2,25 +2,13 @@ import Image from 'next/image'
export const ProfileLogo: React.FC = () => { export const ProfileLogo: React.FC = () => {
return ( return (
<> <div className='px-2 py-6'>
<div className='col-sm-24 col-md-10'>
<div className='profile-logo'>
<Image <Image
width={800} width={370}
height={800} height={370}
src='/images/divlo_logo.png' src='/images/divlo_logo.png'
alt='Divlo' alt='Divlo'
/> />
</div> </div>
</div>
<style jsx>{`
.profile-logo {
margin-right: 10px;
margin-left: 10px;
}
`}
</style>
</>
) )
} }

View File

@ -0,0 +1,10 @@
import { Icon } from './Icon'
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Email</title>
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' />
</Icon>
)
}

View File

@ -0,0 +1,10 @@
import { Icon } from './Icon'
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>GitHub</title>
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
</Icon>
)
}

View File

@ -0,0 +1,14 @@
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
const { children, ...rest } = props
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
className='dark:text-white text-black w-8 h-8 fill-current'
{...rest}
>
{children}
</svg>
)
}

View File

@ -0,0 +1,10 @@
import { Icon } from './Icon'
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Twitch</title>
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' />
</Icon>
)
}

View File

@ -0,0 +1,10 @@
import { Icon } from './Icon'
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Twitter</title>
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' />
</Icon>
)
}

View File

@ -0,0 +1,10 @@
import { Icon } from './Icon'
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>YouTube</title>
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' />
</Icon>
)
}

View File

@ -1,50 +1,22 @@
import { Tooltip } from 'components/design/Tooltip'
import Image from 'next/image'
interface SocialMediaItemProps { interface SocialMediaItemProps {
link: string link: string
socialMedia: 'Email' | 'GitHub' | 'Twitch' | 'Twitter' | 'YouTube' ariaLabel: string
} }
export const SocialMediaItem: React.FC<SocialMediaItemProps> = props => { export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
const { link, socialMedia } = props const { link, ariaLabel, children } = props
return ( return (
<> <li className='inline-block mx-4 my-1'>
<li className='social-media-list__item'>
<a <a
href={link} href={link}
aria-label={socialMedia} aria-label={ariaLabel}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
className='social-media-list__link' className='relative inline-block bg-transparent'
> >
<Tooltip title={socialMedia}> {children}
<Image
width={45}
height={45}
alt={socialMedia}
src={`/images/web/${socialMedia}.png`}
/>
</Tooltip>
</a> </a>
</li> </li>
<style jsx>
{`
.social-media-list__item {
display: inline-block;
margin: 5px 15px;
}
.social-media-list__link {
width: 45px;
height: 45px;
position: relative;
display: inline-block;
background-color: transparent;
}
`}
</style>
</>
) )
} }

View File

@ -1,41 +1,31 @@
import { SocialMediaItem } from './SocialMediaItem' import { SocialMediaItem } from './SocialMediaItem'
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon'
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon'
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon'
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
export const SocialMediaList: React.FC = () => { export const SocialMediaList: React.FC = () => {
return ( return (
<> <ul className='social-media-list m-0 mt-2 py-4 list-none text-center'>
<div className='row justify-content-center'> <SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
<ul className='social-media-list'> <TwitterIcon />
</SocialMediaItem>
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
<GitHubIcon />
</SocialMediaItem>
<SocialMediaItem <SocialMediaItem
socialMedia='Twitter'
link='https://twitter.com/Divlo_FR'
/>
<SocialMediaItem
socialMedia='GitHub'
link='https://github.com/Divlo'
/>
<SocialMediaItem
socialMedia='YouTube'
link='https://www.youtube.com/c/Divlo' link='https://www.youtube.com/c/Divlo'
/> ariaLabel='YouTube'
<SocialMediaItem >
socialMedia='Twitch' <YouTubeIcon />
link='https://www.twitch.tv/divlo' </SocialMediaItem>
/> <SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
<SocialMediaItem socialMedia='Email' link='mailto:contact@divlo.fr' /> <TwitchIcon />
</SocialMediaItem>
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
<EmailIcon />
</SocialMediaItem>
</ul> </ul>
</div>
<style jsx>{`
.social-media-list {
margin: 0;
padding: 0;
list-style: none;
text-align: center;
padding: 15px 0;
margin-top: 10px;
}
`}
</style>
</>
) )
} }

View File

@ -5,29 +5,13 @@ import { ProfileLogo } from './ProfileLogo'
export const Profile: React.FC = () => { export const Profile: React.FC = () => {
return ( return (
<> <div className='flex flex-col justify-center items-center px-10 pt-2 md:pt-10 xl:pt-0 md:flex-row'>
<div className='row profile'>
<ProfileLogo /> <ProfileLogo />
<div className='col-sm-24 col-md-14'> <div>
<ProfileInfo /> <ProfileInfo />
<ProfileList /> <ProfileList />
<ProfileDescriptionBottom /> <ProfileDescriptionBottom />
</div> </div>
</div> </div>
<style jsx>
{`
.profile {
padding: 40px 50px 15px 50px;
}
@media (max-width: 576px) {
.profile {
padding: 40px 10px 0 10px;
}
}
`}
</style>
</>
) )
} }

View File

@ -6,39 +6,21 @@ export interface SkillProps {
skill: keyof typeof skills skill: keyof typeof skills
} }
export const Skill: React.FC<SkillProps> = props => { export const Skill: React.FC<SkillProps> = (props) => {
const { skill } = props const { skill } = props
const skillProperties = skills[skill] const skillProperties = skills[skill]
return ( return (
<>
<a <a
href={skillProperties.link} href={skillProperties.link}
className='skills-link' className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
<div className='skills-content text-center'> <div className='text-center'>
<Image <Image width={60} height={60} alt={skill} src={skillProperties.image} />
width={60} <p className='mt-1'>{skill}</p>
height={60}
alt={skill}
src={skillProperties.image}
/>
<p className='skills-text'>{skill}</p>
</div> </div>
</a> </a>
<style jsx>{`
.skills-link {
max-width: 120px;
margin: 0px 10px 0 10px;
}
.skills-text {
margin-top: 5px;
}
`}
</style>
</>
) )
} }

View File

@ -5,40 +5,23 @@ export interface SkillsSectionProps {
children: React.ReactNode children: React.ReactNode
} }
export const SkillsSection: React.FC<SkillsSectionProps> = props => { export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
const { title, children } = props const { title, children } = props
return ( return (
<>
<ShadowContainer> <ShadowContainer>
<div className='container-fluid'> <div className='w-full px-4 mx-auto'>
<div className='row row-padding'> <div className='flex flex-wrap px-4 py-6'>
<div className='col-24'> <div className='flex-1'>
<div className='skills-header'> <div className='mb-8 border-b border-gray-600 dark:border-opacity-10 dark:border-white'>
<h3 className='important'>{title}</h3> <h3 className='text-yellow font-semibold text-xl my-3 dark:text-yellow-dark'>
{title}
</h3>
</div> </div>
<div className='skills-body'>{children}</div> <div className='flex justify-around flex-wrap'>{children}</div>
</div> </div>
</div> </div>
</div> </div>
</ShadowContainer> </ShadowContainer>
<style jsx>{`
.skills-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 15px;
}
.skills-header > h3 {
margin-bottom: 15px;
}
.skills-body {
display: flex;
justify-content: space-around;
flex-flow: row wrap;
padding-top: 1.5rem;
}
`}
</style>
</>
) )
} }

View File

@ -0,0 +1,15 @@
import { render } from '@testing-library/react'
import { ErrorPage } from '../ErrorPage'
describe('<ErrorPage />', () => {
it('should render the message and statusCode', async () => {
const messageContent = 'message content'
const statusCode = 404
const { getByText } = render(
<ErrorPage statusCode={statusCode} message={messageContent} />
)
expect(getByText(messageContent)).toBeInTheDocument()
expect(getByText(statusCode)).toBeInTheDocument()
})
})

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import { Footer } from '../Footer'
describe('<Footer />', () => {
it('should render', async () => {
const { getByText } = render(<Footer />)
expect(getByText('Divlo')).toBeInTheDocument()
})
})

View File

@ -1,43 +0,0 @@
import { forwardRef } from 'react'
type ButtonProps = React.ComponentPropsWithRef<'button'>
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { children, ...rest } = props
return (
<>
<button ref={ref} {...rest} className='btn btn-dark'>
{children}
</button>
<style jsx>
{`
.btn {
cursor: pointer;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btn-dark {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn-dark:hover {
color: #fff;
background-color: #23272b;
border-color: #1d2124;
}
`}
</style>
</>
)
}
)

View File

@ -1,75 +0,0 @@
import { forwardRef } from 'react'
interface InputProps extends React.HTMLProps<HTMLInputElement> {
label: string
}
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { label, name, ...rest } = props
return (
<>
<div className='form-group-animation'>
<input ref={ref} {...rest} id={name} name={name} />
<label htmlFor={name} className='label'>
<span className='label-content'>{label}</span>
</label>
</div>
<style jsx>{`
.form-group-animation {
position: relative;
margin-top: 10px;
margin-bottom: 30px;
overflow: hidden;
}
.form-group-animation input {
width: 100%;
height: 100%;
padding-top: 35px;
color: var(--color-text-1);
border: none;
background: transparent;
outline: none;
}
.form-group-animation label {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-bottom: 1px solid #fff;
}
.form-group-animation label::after {
content: '';
position: absolute;
left: 0;
bottom: -1px;
height: 100%;
width: 100%;
border-bottom: 3px solid var(--color-primary);
transform: translateX(-100%);
transition: transform 0.2s ease;
}
.label-content {
position: absolute;
bottom: 5px;
left: 0px;
transition: all 0.3s ease;
}
.form-group-animation input:focus + .label .label-content,
.form-group-animation input:valid + .label .label-content {
transform: translateY(-150%);
font-size: 14px;
color: var(--color-primary);
}
.form-group-animation input:focus + .label::after,
.form-group-animation input:valid + .label::after {
transform: translateX(0%);
}
`}
</style>
</>
)
})

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
export const RevealFade: React.FC = props => { export const RevealFade: React.FC = (props) => {
const { children } = props const { children } = props
const htmlElement = useRef<HTMLDivElement>(null) const htmlElement = useRef<HTMLDivElement>(null)
@ -8,7 +8,7 @@ export const RevealFade: React.FC = props => {
useEffect(() => { useEffect(() => {
const observer = new window.IntersectionObserver( const observer = new window.IntersectionObserver(
(entries, observer) => { (entries, observer) => {
entries.forEach(entry => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add('reveal-visible') entry.target.classList.add('reveal-visible')
observer.unobserve(entry.target) observer.unobserve(entry.target)
@ -30,7 +30,8 @@ export const RevealFade: React.FC = props => {
{children} {children}
</div> </div>
<style jsx>{` <style jsx>
{`
.reveal { .reveal {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;

View File

@ -3,26 +3,18 @@ import { forwardRef } from 'react'
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'> type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
export const SectionHeading = forwardRef< export const SectionHeading = forwardRef<
HTMLHeadingElement, HTMLHeadingElement,
SectionHeadingProps SectionHeadingProps
>((props, ref) => { >((props, ref) => {
const { children, ...rest } = props const { children, ...rest } = props
return ( return (
<> <h2
<h2 ref={ref} {...rest} className='Section__title'> ref={ref}
{...rest}
className='text-4xl font-semibold text-center mt-1 mb-7'
>
{children} {children}
</h2> </h2>
<style jsx>
{`
.Section__title {
font-size: 34px;
margin-top: 10px;
text-align: center;
}
`}
</style>
</>
) )
}) })

View File

@ -22,12 +22,14 @@ export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
if (isMain) { if (isMain) {
return ( return (
<div className='px-3 w-full'>
<ShadowContainer style={{ marginTop: 50 }}> <ShadowContainer style={{ marginTop: 50 }}>
<section ref={ref} {...rest}> <section ref={ref} {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>} {heading != null && <SectionHeading>{heading}</SectionHeading>}
<div className='container-fluid'>{children}</div> <div className='px-3 w-full'>{children}</div>
</section> </section>
</ShadowContainer> </ShadowContainer>
</div>
) )
} }
@ -35,7 +37,7 @@ export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
return ( return (
<section ref={ref} {...rest}> <section ref={ref} {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>} {heading != null && <SectionHeading>{heading}</SectionHeading>}
<div className='container-fluid'>{children}</div> <div className='px-3 w-full'>{children}</div>
</section> </section>
) )
} }
@ -52,11 +54,11 @@ export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
{description} {description}
</p> </p>
)} )}
<div className='px-3 w-full'>
<ShadowContainer> <ShadowContainer>
<div className='container-fluid'> <div className='px-16 py-4 leading-8 w-full'>{children}</div>
<div className='row row-padding'>{children}</div>
</div>
</ShadowContainer> </ShadowContainer>
</div>
</section> </section>
) )
}) })

View File

@ -1,12 +1,17 @@
import classNames from 'classnames'
type ShadowContainerProps = React.ComponentPropsWithRef<'div'> type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
export const ShadowContainer: React.FC<ShadowContainerProps> = props => { export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
return ( return (
<> <>
<div <div
className={`shadow-container ${className != null ? className : ''}`} className={classNames(
'shadow-container h-full max-w-full break-words',
className
)}
{...rest} {...rest}
> >
{children} {children}
@ -15,14 +20,9 @@ export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
<style jsx> <style jsx>
{` {`
.shadow-container { .shadow-container {
display: flex;
flex-direction: column;
word-wrap: break-word;
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25); box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
border: 1px solid black; border: 1px solid black;
border-radius: 1rem; border-radius: 1rem;
height: 100%;
max-width: 100%;
margin-bottom: 50px; margin-bottom: 50px;
} }
`} `}

View File

@ -1,39 +0,0 @@
import { forwardRef } from 'react'
interface TextareaProps extends React.HTMLProps<HTMLTextAreaElement> {
label: string
}
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
(props, ref) => {
const { label, name, ...rest } = props
return (
<>
<div className='form-group'>
<label htmlFor={name}>{label}</label>
<br />
<textarea id={name} name={name} ref={ref} {...rest} />
</div>
<style jsx>{`
.form-group {
padding-top: 15px;
margin-bottom: 30px;
}
.form-group textarea {
background: transparent;
color: var(--color-text);
outline: none;
width: 100%;
height: auto;
padding: 10px;
resize: vertical;
margin-top: 8px;
}
`}
</style>
</>
)
}
)

View File

@ -1,49 +0,0 @@
interface TooltipProps extends React.ComponentPropsWithRef<'div'> {
title: string
children: React.ReactNode
}
export const Tooltip: React.FC<TooltipProps> = props => {
const { title, children, ...rest } = props
return (
<>
<span className='tooltip' {...rest}>
{children}
<span className='title'>{title}</span>
</span>
<style jsx>{`
.title {
color: #fff;
font-size: 11px;
font-weight: 400;
line-height: 1;
display: inline-block;
background-color: #222222;
padding: 5px 8px;
white-space: nowrap;
position: absolute;
top: 100%;
margin-top: 10px;
z-index: 1;
opacity: 0;
visibility: hidden;
border-radius: 3px;
transition: all 0.15s ease-in;
transform: translate3d(0, -15px, 0);
backface-visibility: hidden;
}
.tooltip ~ .tooltip:hover .title,
.tooltip:first-child:hover .title {
opacity: 1;
visibility: visible;
transition: all 0.35s ease-out;
transform: translate3d(0, 0, 0);
margin: 0;
backface-visibility: hidden;
}
`}
</style>
</>
)
}

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react'
import { Button } from '../Button'
describe('<Button />', () => {
it('should render', async () => {
const { getByText } = render(<Button>Submit</Button>)
expect(getByText('Submit')).toBeInTheDocument()
})
})

View File

@ -1,18 +1,11 @@
version: '3.0' version: '3.0'
services: services:
divlo.fr-website: divlo.fr:
container_name: ${COMPOSE_PROJECT_NAME} container_name: ${COMPOSE_PROJECT_NAME}
image: 'divlo.fr'
build: build:
context: './' context: './'
ports: ports:
- '${PORT}:${PORT}' - '${PORT}:${PORT}'
environment: environment:
PORT: ${PORT} PORT: ${PORT}
volumes:
- './:/app'
divlo.fr-maildev:
image: 'maildev/maildev:1.1.0'
ports:
- '1080:80'
container_name: 'divlo.fr-maildev'

View File

@ -17,8 +17,10 @@ module.exports = {
'!**/node_modules/**', '!**/node_modules/**',
'!**/next.config.js', '!**/next.config.js',
'!**/postcss.config.js', '!**/postcss.config.js',
'!**/tailwind.config.js',
'!**/workbox-*.js', '!**/workbox-*.js',
'!**/sw.js' '!**/sw.js',
'!**/jest.config.js'
], ],
coverageDirectory: './coverage', coverageDirectory: './coverage',
coverageReporters: ['text', 'cobertura'] coverageReporters: ['text', 'cobertura']

View File

@ -4,18 +4,18 @@
"description": "Developer Full Stack Junior • Passionate about High-Tech", "description": "Developer Full Stack Junior • Passionate about High-Tech",
"birthDate": "Birth date", "birthDate": "Birth date",
"nationality": "Nationality", "nationality": "Nationality",
"descriptionBottom": "I'm learning online programming languages to improve my skills in my passion. <0/> <0/> I designed my graphic chart and my website." "descriptionBottom": "I am self-taught in Computer Science by following online trainings. <0/> <0/> I put into practice everything I learn and make many projects."
}, },
"interests": { "interests": {
"title": "My Interests", "title": "My Interests",
"paragraphs": [ "paragraphs": [
{ {
"title": "Developer Full Stack Junior :", "title": "Developer Full Stack Junior :",
"description": "Computer programming is my main passion, I love it! <br/> Mostly web development for the moment but I'm programming some Python and others programming language too." "description": "Computer programming is my main hobby, I love it! <br/> Mostly web development for the moment but I'm programming some Python and others programming language too."
}, },
{ {
"title": "Passionate about High-Tech :", "title": "Passionate about High-Tech :",
"description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and even better than the past. Technologies improve gradually over time, which is very useful in many areas." "description": "I always wondered how the future would be. Every day I want to wake up and think that the future will be great and better than the past. Technologies improve gradually over time, which is very useful in many areas."
}, },
{ {
"title": "Open-Source enthusiast :", "title": "Open-Source enthusiast :",
@ -44,20 +44,5 @@
"image": "/images/portfolio/threamdivlofr.png" "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"
} }
} }

View File

@ -4,18 +4,18 @@
"description": "Développeur Full Stack Junior • Passionné de High-Tech", "description": "Développeur Full Stack Junior • Passionné de High-Tech",
"birthDate": "Date de naissance", "birthDate": "Date de naissance",
"nationality": "Nationalité", "nationality": "Nationalité",
"descriptionBottom": "J'apprends en ligne l'informatique et les langages de programmation pour m'améliorer dans ma passion. <0/> <0/> J'ai conçu ma charte graphique et mon site internet." "descriptionBottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne. <0/> <0/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets."
}, },
"interests": { "interests": {
"title": "Mes intérêts", "title": "Mes intérêts",
"paragraphs": [ "paragraphs": [
{ {
"title": "Développeur Full Stack Junior :", "title": "Développeur Full Stack Junior :",
"description": "La programmation informatique est ma principale passion, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi du Python et d'autres langages de programmation." "description": "La programmation informatique est mon loisir principal, j'adore! <br/> Principalement du développement Web pour le moment, mais je programme aussi du Python et d'autres langages de programmation."
}, },
{ {
"title": "Passionné de High-Tech :", "title": "Passionné de High-Tech :",
"description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et même meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines." "description": "Je me suis toujours demandé comment l'avenir serait. Chaque jour, je veux me réveiller et penser que l'avenir sera formidable et meilleur que le passé. Les technolgies s'améliorent progressivement avec le temps, ce qui est très utile dans de nombreux domaines."
}, },
{ {
"title": "Enthousiaste de l'Open-Source :", "title": "Enthousiaste de l'Open-Source :",
@ -44,20 +44,5 @@
"image": "/images/portfolio/threamdivlofr.png" "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"
} }
} }

23070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,22 +6,6 @@
"type": "git", "type": "git",
"url": "https://github.com/Divlo/Divlo" "url": "https://github.com/Divlo/Divlo"
}, },
"ts-standard": {
"ignore": [
".next",
".lighthouseci",
"node_modules",
"next-env.d.ts",
"**/workbox-*.js",
"**/sw.js"
],
"envs": [
"node",
"browser",
"jest"
],
"report": "stylish"
},
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"start": "next start", "start": "next start",
@ -30,60 +14,60 @@
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:docker": "dockerfilelint './Dockerfile'", "lint:docker": "dockerfilelint './Dockerfile'",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules", "lint:markdown": "markdownlint '*.md' --dot --ignore node_modules",
"lint:typescript": "ts-standard", "lint:typescript": "eslint '*.{js,ts,jsx,tsx}'",
"lint:staged": "lint-staged",
"lighthouse": "lhci autorun", "lighthouse": "lhci autorun",
"test": "jest", "test": "jest",
"release": "semantic-release", "release": "semantic-release",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fontsource/montserrat": "4.2.2", "@fontsource/montserrat": "4.3.0",
"@fortawesome/fontawesome-svg-core": "1.2.35", "@fortawesome/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-brands-svg-icons": "5.15.3", "@fortawesome/free-brands-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14", "@fortawesome/react-fontawesome": "0.1.14",
"axios": "0.21.1",
"classnames": "2.3.1", "classnames": "2.3.1",
"html-react-parser": "1.2.6", "html-react-parser": "1.2.6",
"next": "10.1.3", "next": "10.2.0",
"next-pwa": "5.2.14", "next-pwa": "5.2.21",
"next-themes": "0.0.14",
"next-translate": "1.0.6", "next-translate": "1.0.6",
"nodemailer": "6.5.0",
"normalize.css": "8.0.1",
"nprogress": "0.2.0",
"react": "17.0.2", "react": "17.0.2",
"react-component-form": "1.3.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"universal-cookie": "4.0.4", "universal-cookie": "4.0.4"
"validator": "13.5.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "12.1.1", "@commitlint/cli": "12.1.1",
"@commitlint/config-conventional": "12.1.1", "@commitlint/config-conventional": "12.1.1",
"@fullhuman/postcss-purgecss": "4.0.3", "@lhci/cli": "0.7.2",
"@lhci/cli": "0.7.1", "@testing-library/jest-dom": "5.12.0",
"@styled-jsx/plugin-sass": "3.0.0",
"@testing-library/jest-dom": "5.11.10",
"@testing-library/react": "11.2.6", "@testing-library/react": "11.2.6",
"@types/jest": "26.0.22", "@types/jest": "26.0.23",
"@types/node": "14.14.41", "@types/node": "15.0.2",
"@types/nodemailer": "6.4.1", "@types/react": "17.0.5",
"@types/nprogress": "0.2.0",
"@types/react": "17.0.3",
"@types/styled-jsx": "2.2.8", "@types/styled-jsx": "2.2.8",
"@types/validator": "13.1.3", "@typescript-eslint/eslint-plugin": "4.22.1",
"autoprefixer": "10.2.5",
"babel-jest": "26.6.3", "babel-jest": "26.6.3",
"dockerfilelint": "1.8.0", "dockerfilelint": "1.8.0",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "4.0.2",
"eslint": "7.26.0",
"eslint-config-prettier": "8.3.0",
"eslint-config-standard-with-typescript": "20.0.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-promise": "4.3.1",
"husky": "6.0.0", "husky": "6.0.0",
"jest": "26.6.3", "jest": "26.6.3",
"lint-staged": "11.0.0",
"markdownlint-cli": "0.27.1", "markdownlint-cli": "0.27.1",
"node-mocks-http": "1.10.1", "postcss": "8.2.14",
"postcss": "8.2.10", "prettier": "2.2.1",
"sass": "1.32.11",
"semantic-release": "17.4.2", "semantic-release": "17.4.2",
"ts-standard": "10.0.0", "tailwindcss": "2.1.2",
"typescript": "4.2.4" "typescript": "4.2.4"
} }
} }

View File

@ -10,7 +10,6 @@ const Error404: React.FC = () => {
return ( return (
<> <>
<Head title='Divlo - 404' /> <Head title='Divlo - 404' />
<ErrorPage statusCode={404} message={t('errors:notFound')} /> <ErrorPage statusCode={404} message={t('errors:notFound')} />
</> </>
) )

View File

@ -10,7 +10,6 @@ const Error500: React.FC = () => {
return ( return (
<> <>
<Head title='Divlo - 500' /> <Head title='Divlo - 500' />
<ErrorPage statusCode={500} message={t('errors:serverError')} /> <ErrorPage statusCode={500} message={t('errors:serverError')} />
</> </>
) )

View File

@ -1,32 +1,23 @@
import { useEffect } from 'react'
import { AppProps } from 'next/app' import { AppProps } from 'next/app'
import Router from 'next/router' import { ThemeProvider } from 'next-themes'
import NProgress from 'nprogress'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import UniversalCookie from 'universal-cookie' import UniversalCookie from 'universal-cookie'
import 'normalize.css/normalize.css' import 'tailwindcss/tailwind.css'
import '@fontsource/montserrat/400.css' import '@fontsource/montserrat/400.css'
import '@fontsource/montserrat/500.css' import '@fontsource/montserrat/500.css'
import '@fontsource/montserrat/600.css' import '@fontsource/montserrat/600.css'
import '@fontsource/montserrat/700.css' import '@fontsource/montserrat/700.css'
import 'styles/grid.scss'
import 'styles/general.scss'
import 'styles/nprogress.scss'
import { Header } from 'components/Header' import { Header } from 'components/Header'
import { Footer } from 'components/Footer' import { Footer } from 'components/Footer'
import { useEffect } from 'react'
const universalCookie = new UniversalCookie() const universalCookie = new UniversalCookie()
/** how long in seconds, until the cookie expires (10 years) */ /** how long in seconds, until the cookie expires (10 years) */
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60 const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
Router.events.on('routeChangeStart', () => NProgress.start())
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
const { lang } = useTranslation() const { lang } = useTranslation()
@ -38,13 +29,13 @@ const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
}, [lang]) }, [lang])
return ( return (
<> <ThemeProvider attribute='class' defaultTheme='dark'>
<Header /> <Header />
<main className='content container'> <main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
<Component {...pageProps} /> <Component {...pageProps} />
</main> </main>
<Footer /> <Footer />
</> </ThemeProvider>
) )
} }

31
pages/_document.tsx Normal file
View File

@ -0,0 +1,31 @@
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps
} from 'next/document'
class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
const initialProps = await Document.getInitialProps(ctx)
return initialProps
}
render(): JSX.Element {
return (
<Html>
<Head />
<body className='bg-white dark:bg-black text-black dark:text-white font-headline'>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

View File

@ -1,69 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import nodemailer from 'nodemailer'
import validator from 'validator'
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
const emailTransporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: EMAIL_PORT,
secure: EMAIL_PORT === 465,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
tls: {
rejectUnauthorized: false
}
})
export default async (
request: NextApiRequest,
response: NextApiResponse
): Promise<any> => {
if (request.method !== 'POST') {
return response.redirect('/404')
}
let { name, email, subject, message } = request.body as {
name: string
email: string
subject: string
message: string
}
if (
validator.isEmpty(name) ||
validator.isEmpty(email) ||
validator.isEmpty(subject) ||
validator.isEmpty(message)
) {
return response.status(400).json({ type: 'requiredFields' })
}
if (!validator.isEmail(email)) {
return response.status(400).json({ type: 'invalidEmail' })
}
email = validator.normalizeEmail(email) as string
message = validator.trim(message)
message = validator.escape(message)
subject = validator.trim(subject)
subject = validator.escape(subject)
try {
await emailTransporter.sendMail({
from: '"Divlo" <contact@divlo.fr>',
to: email,
subject: `Contact - ${subject}`,
html: `
<b>Name:</b> ${name} <br/>
<b>Email:</b> ${email} <br/>
<b>Message:</b> ${message}
`
})
return response.status(201).json({ type: 'success' })
} catch {
return response.status(500).json({ type: 'serverError' })
}
}

View File

@ -1,7 +1,6 @@
import { GetStaticProps } from 'next' import { GetStaticProps } from 'next'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import { Contact } from 'components/Contact'
import { RevealFade } from 'components/design/RevealFade' import { RevealFade } from 'components/design/RevealFade'
import { Section } from 'components/design/Section' import { Section } from 'components/design/Section'
import { Head } from 'components/Head' import { Head } from 'components/Head'
@ -30,7 +29,11 @@ const Home: React.FC = () => {
</RevealFade> </RevealFade>
<RevealFade> <RevealFade>
<Section id='skills' heading={t('home:skills.title')} withoutShadowContainer> <Section
id='skills'
heading={t('home:skills.title')}
withoutShadowContainer
>
<Skills /> <Skills />
</Section> </Section>
</RevealFade> </RevealFade>
@ -44,12 +47,6 @@ const Home: React.FC = () => {
<Portfolio /> <Portfolio />
</Section> </Section>
</RevealFade> </RevealFade>
<RevealFade>
<Section id='contact' heading={t('home:contact.title')}>
<Contact />
</Section>
</RevealFade>
</> </>
) )
} }

View File

@ -1,15 +1,6 @@
module.exports = { module.exports = {
plugins: [ plugins: {
[ tailwindcss: {},
'@fullhuman/postcss-purgecss', autoprefixer: {}
{
content: [
'./pages/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}'
],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ['html', 'body']
} }
]
]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,95 +0,0 @@
:root {
--border-header-footer: 3px rgba(255, 255, 255, 0.7) solid;
--color-primary: #ffd800;
--color-text-1: rgb(222, 222, 222);
--color-text-2: #b2bac2;
--color-background: #181818;
--color-shadow: rgba(255, 255, 255, 0.2);
--header-height: 79px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
#__next {
display: flex;
flex-flow: column wrap;
min-height: 100vh;
padding-top: var(--header-height);
}
html {
line-height: initial;
}
body {
background-color: var(--color-background);
color: var(--color-text-1);
font-family: 'Montserrat', 'Arial', 'sans-serif';
font-weight: 400;
font-size: 18px;
}
.content {
flex-grow: 1;
display: flex;
flex-direction: column;
opacity: 1;
visibility: visible;
position: relative;
overflow: hidden;
transition: opacity 400ms ease-out;
}
p {
font-size: 18px;
line-height: 1.9;
}
.color-primary {
color: var(--color-primary);
}
a,
.important {
color: var(--color-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
border: 0;
height: auto;
max-width: 100%;
vertical-align: middle;
}
.row-padding {
padding: 15px 50px 15px 50px;
}
.text-center {
text-align: center;
word-break: break-word;
}
.justify-content-center {
justify-content: center;
}
.align-items-center {
align-items: center;
}
.d-none {
display: none !important;
}
.paragraph-color {
color: var(--color-text-2);
}
b,
strong {
font-weight: 500;
}
em {
font-style: italic;
}
@media (max-width: 576px) {
.content {
width: 100% !important;
}
.row-padding {
padding: 0 !important;
}
}

View File

@ -1,710 +0,0 @@
/*
* Bootstrap Grid v4.4.1 (https://getbootstrap.com/)
* Edited by Divlo (col, col-sm, col-md, col-lg, col-xl) and 24 columns (no offsets)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (max-width: 576px) {
.container {
width: 90%;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12,
.col-13,
.col-14,
.col-15,
.col-16,
.col-17,
.col-18,
.col-19,
.col-20,
.col-21,
.col-22,
.col-23,
.col-24,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-13,
.col-sm-14,
.col-sm-15,
.col-sm-16,
.col-sm-17,
.col-sm-18,
.col-sm-19,
.col-sm-20,
.col-sm-21,
.col-sm-22,
.col-sm-23,
.col-sm-24,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12,
.col-md-13,
.col-md-14,
.col-md-15,
.col-md-16,
.col-md-17,
.col-md-18,
.col-md-19,
.col-md-20,
.col-md-21,
.col-md-22,
.col-md-23,
.col-md-24,
.col-lg-1,
.col-lg-2,
.col-lg-3,
.col-lg-4,
.col-lg-5,
.col-lg-6,
.col-lg-7,
.col-lg-8,
.col-lg-9,
.col-lg-10,
.col-lg-11,
.col-lg-12,
.col-lg-13,
.col-lg-14,
.col-lg-15,
.col-lg-16,
.col-lg-17,
.col-lg-18,
.col-lg-19,
.col-lg-20,
.col-lg-21,
.col-lg-22,
.col-lg-23,
.col-lg-24,
.col-xl-1,
.col-xl-2,
.col-xl-3,
.col-xl-4,
.col-xl-5,
.col-xl-6,
.col-xl-7,
.col-xl-8,
.col-xl-9,
.col-xl-10,
.col-xl-11,
.col-xl-12,
.col-xl-13,
.col-xl-14,
.col-xl-15,
.col-xl-16,
.col-xl-17,
.col-xl-18,
.col-xl-19,
.col-xl-20,
.col-xl-21,
.col-xl-22,
.col-xl-23,
.col-xl-24 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
/* col- */
.col-1 {
flex: 0 0 4.16667%;
max-width: 4.16667%;
}
.col-2 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-3 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
.col-4 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-5 {
flex: 0 0 20.83333%;
max-width: 20.83333%;
}
.col-6 {
flex: 0 0 25%;
max-width: 25%;
}
.col-7 {
flex: 0 0 29.16667%;
max-width: 29.16667%;
}
.col-8 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-9 {
flex: 0 0 37.5%;
max-width: 37.5%;
}
.col-10 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-11 {
flex: 0 0 45.83333%;
max-width: 45.83333%;
}
.col-12 {
flex: 0 0 50%;
max-width: 50%;
}
.col-13 {
flex: 0 0 54.16667%;
max-width: 54.16667%;
}
.col-14 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-15 {
flex: 0 0 62.5%;
max-width: 62.5%;
}
.col-16 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-17 {
flex: 0 0 70.83333%;
max-width: 70.83333%;
}
.col-18 {
flex: 0 0 75%;
max-width: 75%;
}
.col-19 {
flex: 0 0 79.16667%;
max-width: 79.16667%;
}
.col-20 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-21 {
flex: 0 0 87.5%;
max-width: 87.5%;
}
.col-22 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-23 {
flex: 0 0 95.83333%;
max-width: 95.83333%;
}
.col-24 {
flex: 0 0 100%;
max-width: 100%;
}
/* col-sm */
@media (min-width: 576px) {
.col-sm-1 {
flex: 0 0 4.16667%;
max-width: 4.16667%;
}
.col-sm-2 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-sm-3 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
.col-sm-4 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-sm-5 {
flex: 0 0 20.83333%;
max-width: 20.83333%;
}
.col-sm-6 {
flex: 0 0 25%;
max-width: 25%;
}
.col-sm-7 {
flex: 0 0 29.16667%;
max-width: 29.16667%;
}
.col-sm-8 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-sm-9 {
flex: 0 0 37.5%;
max-width: 37.5%;
}
.col-sm-10 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-sm-11 {
flex: 0 0 45.83333%;
max-width: 45.83333%;
}
.col-sm-12 {
flex: 0 0 50%;
max-width: 50%;
}
.col-sm-13 {
flex: 0 0 54.16667%;
max-width: 54.16667%;
}
.col-sm-14 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-sm-15 {
flex: 0 0 62.5%;
max-width: 62.5%;
}
.col-sm-16 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-sm-17 {
flex: 0 0 70.83333%;
max-width: 70.83333%;
}
.col-sm-18 {
flex: 0 0 75%;
max-width: 75%;
}
.col-sm-19 {
flex: 0 0 79.16667%;
max-width: 79.16667%;
}
.col-sm-20 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-sm-21 {
flex: 0 0 87.5%;
max-width: 87.5%;
}
.col-sm-22 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-sm-23 {
flex: 0 0 95.83333%;
max-width: 95.83333%;
}
.col-sm-24 {
flex: 0 0 100%;
max-width: 100%;
}
}
/* col-md */
@media (min-width: 768px) {
.col-md-1 {
flex: 0 0 4.16667%;
max-width: 4.16667%;
}
.col-md-2 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-md-3 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
.col-md-4 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-md-5 {
flex: 0 0 20.83333%;
max-width: 20.83333%;
}
.col-md-6 {
flex: 0 0 25%;
max-width: 25%;
}
.col-md-7 {
flex: 0 0 29.16667%;
max-width: 29.16667%;
}
.col-md-8 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-md-9 {
flex: 0 0 37.5%;
max-width: 37.5%;
}
.col-md-10 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-md-11 {
flex: 0 0 45.83333%;
max-width: 45.83333%;
}
.col-md-12 {
flex: 0 0 50%;
max-width: 50%;
}
.col-md-13 {
flex: 0 0 54.16667%;
max-width: 54.16667%;
}
.col-md-14 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-md-15 {
flex: 0 0 62.5%;
max-width: 62.5%;
}
.col-md-16 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-md-17 {
flex: 0 0 70.83333%;
max-width: 70.83333%;
}
.col-md-18 {
flex: 0 0 75%;
max-width: 75%;
}
.col-md-19 {
flex: 0 0 79.16667%;
max-width: 79.16667%;
}
.col-md-20 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-md-21 {
flex: 0 0 87.5%;
max-width: 87.5%;
}
.col-md-22 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-md-23 {
flex: 0 0 95.83333%;
max-width: 95.83333%;
}
.col-md-24 {
flex: 0 0 100%;
max-width: 100%;
}
}
/* col-lg */
@media (min-width: 992px) {
.col-lg-1 {
flex: 0 0 4.16667%;
max-width: 4.16667%;
}
.col-lg-2 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-lg-3 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
.col-lg-4 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-lg-5 {
flex: 0 0 20.83333%;
max-width: 20.83333%;
}
.col-lg-6 {
flex: 0 0 25%;
max-width: 25%;
}
.col-lg-7 {
flex: 0 0 29.16667%;
max-width: 29.16667%;
}
.col-lg-8 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-lg-9 {
flex: 0 0 37.5%;
max-width: 37.5%;
}
.col-lg-10 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-lg-11 {
flex: 0 0 45.83333%;
max-width: 45.83333%;
}
.col-lg-12 {
flex: 0 0 50%;
max-width: 50%;
}
.col-lg-13 {
flex: 0 0 54.16667%;
max-width: 54.16667%;
}
.col-lg-14 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-lg-15 {
flex: 0 0 62.5%;
max-width: 62.5%;
}
.col-lg-16 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-lg-17 {
flex: 0 0 70.83333%;
max-width: 70.83333%;
}
.col-lg-18 {
flex: 0 0 75%;
max-width: 75%;
}
.col-lg-19 {
flex: 0 0 79.16667%;
max-width: 79.16667%;
}
.col-lg-20 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-lg-21 {
flex: 0 0 87.5%;
max-width: 87.5%;
}
.col-lg-22 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-lg-23 {
flex: 0 0 95.83333%;
max-width: 95.83333%;
}
.col-lg-24 {
flex: 0 0 100%;
max-width: 100%;
}
}
/* col-xl */
@media (min-width: 1200px) {
.col-xl-1 {
flex: 0 0 4.16667%;
max-width: 4.16667%;
}
.col-xl-2 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-xl-3 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
.col-xl-4 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-xl-5 {
flex: 0 0 20.83333%;
max-width: 20.83333%;
}
.col-xl-6 {
flex: 0 0 25%;
max-width: 25%;
}
.col-xl-7 {
flex: 0 0 29.16667%;
max-width: 29.16667%;
}
.col-xl-8 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-xl-9 {
flex: 0 0 37.5%;
max-width: 37.5%;
}
.col-xl-10 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-xl-11 {
flex: 0 0 45.83333%;
max-width: 45.83333%;
}
.col-xl-12 {
flex: 0 0 50%;
max-width: 50%;
}
.col-xl-13 {
flex: 0 0 54.16667%;
max-width: 54.16667%;
}
.col-xl-14 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-xl-15 {
flex: 0 0 62.5%;
max-width: 62.5%;
}
.col-xl-16 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-xl-17 {
flex: 0 0 70.83333%;
max-width: 70.83333%;
}
.col-xl-18 {
flex: 0 0 75%;
max-width: 75%;
}
.col-xl-19 {
flex: 0 0 79.16667%;
max-width: 79.16667%;
}
.col-xl-20 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-xl-21 {
flex: 0 0 87.5%;
max-width: 87.5%;
}
.col-xl-22 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-xl-23 {
flex: 0 0 95.83333%;
max-width: 95.83333%;
}
.col-xl-24 {
flex: 0 0 100%;
max-width: 100%;
}
}

View File

@ -1,85 +0,0 @@
/*! purgecss start ignore */
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: var(--color-primary);
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px var(--color-primary), 0 0 5px var(--color-primary);
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: var(--color-primary);
border-left-color: var(--color-primary);
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/*! purgecss end ignore */

34
tailwind.config.js Normal file
View File

@ -0,0 +1,34 @@
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {
extend: {
screens: {
xs: '380px'
},
colors: {
black: '#181818',
gray: {
DEFAULT: '#333333',
dark: '#b2bac2'
},
yellow: {
DEFAULT: '#ff8000',
dark: '#ffd800'
}
},
boxShadow: {
dark: '0px 1px 10px hsla(0, 0%, 100%, 0.2)',
light: '0px 1px 10px rgba(0, 0, 0, 0.25)'
},
fontFamily: {
headline: ['Montserrat', 'Arial', 'sans-serif']
}
}
},
variants: {
extend: {}
},
plugins: []
}