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

feat: add light mode + rewrite in Tailwind CSS (#15)

This commit is contained in:
Divlo 2021-05-08 19:52:04 +02:00 committed by GitHub
parent 26f24329c7
commit c62e66a86a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 5803 additions and 7623 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

@ -10,7 +10,7 @@ updates:
schedule: schedule:
interval: 'daily' interval: 'daily'
# - package-ecosystem: 'npm' - package-ecosystem: 'npm'
# directory: '/' directory: '/'
# schedule: schedule:
# interval: 'daily' interval: 'daily'

View File

@ -1,21 +1,14 @@
image: 'gitpod/workspace-full' image: 'gitpod/workspace-full'
tasks: tasks:
- name: 'docker-daemon' - before: 'cp .env.example .env && npm install --global npm@7'
init: 'cp .env.example .env && npm install --global npm@7 && npm ci' init: 'npm clean-install'
command: 'sudo docker-up' command: 'npm run dev'
- name: 'docker-container'
init: 'echo "Waiting for docker daemon to start" &&
until docker info &> /dev/null; do sleep 1; done;'
command: 'docker-compose up'
ports: ports:
- port: 3000 - port: 3000
onOpen: 'open-preview' onOpen: 'open-preview'
- port: 1080
onOpen: 'notify'
github: github:
prebuilds: prebuilds:
master: true master: 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,12 +1,13 @@
{ {
"recommendations": [ "recommendations": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax", "divlo.vscode-styled-jsx-syntax",
"divlo.vscode-styled-jsx-languageserver", "divlo.vscode-styled-jsx-languageserver",
"standard.vscode-standard", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"editorconfig.editorconfig",
"coenraads.bracket-pair-colorizer", "coenraads.bracket-pair-colorizer",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint"
"syler.sass-indented"
] ]
} }

13
.vscode/settings.json vendored
View File

@ -1,8 +1,9 @@
{ {
"standard.enable": true, "typescript.tsdk": "node_modules/typescript/lib",
"standard.engine": "ts-standard", "editor.defaultFormatter": "esbenp.prettier-vscode",
"standard.treatErrorsAsWarnings": true, "prettier.configPath": ".prettierrc.json",
"standard.usePackageJson": true, "editor.formatOnSave": true,
"standard.autoFixOnSave": true, "editor.codeActionsOnSave": {
"typescript.tsdk": "node_modules/typescript/lib" "source.fixAll": true
}
} }

View File

@ -72,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

@ -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,32 +6,42 @@ 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('errors: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 { {`
display: flex; main {
flex-direction: column; display: flex;
justify-content: center; flex-direction: column;
align-items: center; justify-content: center;
min-width: 100vw; align-items: center;
min-height: 100%; min-width: 100vw;
} flex: 1;
#__next { }
padding-top: 0; #__next {
} display: flex;
`} flex-direction: column;
padding-top: 0;
height: 100vh;
}
`}
</style> </style>
</> </>
) )

View File

@ -4,25 +4,11 @@ export const Footer: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( 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'>
<footer className='Footer text-center'> <p>
<p> <span className='text-yellow dark:text-yellow-dark'>Divlo</span> |{' '}
<span className='important'>Divlo</span> | {t('common:allRightsReserved')} {t('common:allRightsReserved')}
</p> </p>
</footer> </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,74 +32,29 @@ 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>
{!hiddenMenu && (
<ul>
{locales.map((language, index) => {
if (language === currentLanguage) {
return null
}
return (
<li
key={index}
onClick={async () => await handleLanguage(language)}
>
<LanguageFlag language={language} />
</li>
)
})}
</ul>
)}
</div> </div>
{!hiddenMenu && (
<style jsx> <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) => {
.language-menu { if (language === currentLanguage) {
display: flex; return null
flex-direction: column; }
justify-content: center; return (
align-items: center; <li
cursor: pointer; key={index}
} className='flex items-center justify-center w-full h-12 hover:bg-[#4f545c] hover:bg-opacity-20 pl-2'
.selected-language { onClick={async () => await handleLanguage(language)}
display: flex; >
align-items: center; <LanguageFlag language={language} />
margin-right: 15px; </li>
} )
ul { })}
display: flex; </ul>
flex-direction: column; )}
justify-content: center; </div>
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

@ -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'> <Link href='/'>
<div className='container'> <a>
<nav className='navbar navbar-fixed-top'> <div className='flex items-center justify-center'>
<Link href='/'> <Image
<a className='navbar__brand-link'> width={60}
<div className='navbar__brand'> height={60}
<Image src='/images/divlo_icon_small.png'
width={60} alt='Divlo'
height={60} />
src='/images/divlo_icon_small.png' <strong className='ml-1 font-headline font-semibold hidden xs:block'>
alt='Divlo' Divlo
/> </strong>
<strong className='navbar__brand-title'>Divlo</strong> </div>
</div> </a>
</a> </Link>
</Link> <div className='flex justify-between'>
<div className='navbar__buttons'> <Language />
<Language /> <SwitchTheme />
</div> </div>
</nav> </header>
</div>
</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'> <FontAwesomeIcon
<Tooltip title={title}> className='text-yellow cursor-pointer h-full w-full block dark:text-yellow-dark'
<FontAwesomeIcon icon={fontAwesomeIcon}
className='color-primary' />
style={{ </li>
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

@ -5,41 +5,18 @@ 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} />
/> <InterestItem
<InterestItem title='Passionate about High-Tech'
title='Passionate about High-Tech' fontAwesomeIcon={faMicrochip}
fontAwesomeIcon={faMicrochip} />
/> <InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
<InterestItem </ul>
title='Open-Source enthusiast' </div>
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

@ -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(
returnObjects: true 'home:interests.paragraphs',
}) {},
{
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='group inline-flex justify-center'
className='portfolio-link' target='_blank'
target='_blank' rel='noopener noreferrer'
rel='noopener noreferrer' href={link}
href={link} aria-label={title}
aria-label={title} >
> <div className='flex justify-center'>
<div className='portfolio-figure'> <Image
<Image width={300} height={300} src={image} alt={title} /> className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
</div> width={300}
<div className='portfolio-caption'> height={300}
<h3 className='portfolio-title important'>{title}</h3> src={image}
<p className='portfolio-description'>{description}</p> alt={title}
</div> />
</a> </div>
</div> <div className='opacity-0 transition-opacity duration-500 h-auto absolute text-center overflow-hidden bottom-0 group-hover:opacity-100'>
<h3 className='text-yellow text-xl font-semibold my-6 dark:text-yellow-dark'>
<style jsx global> {title}
{` </h3>
.portfolio-figure img[alt='${title}'] { <p className='my-6'>{description}</p>
max-height: 300px; </div>
max-width: 300px; </a>
transition: opacity 0.5s ease; </ShadowContainer>
}
.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(
returnObjects: true 'home:portfolio.items',
}) {},
{
returnObjects: true
}
)
return ( return (
<> <div className='flex flex-wrap justify-center px-3 w-full'>
<div className='container-fluid'> {items.map((item, index) => {
<div className='row justify-content-center'> return <PortfolioItem key={index} {...item} />
{items.map((item, index) => { })}
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='block mt-8 mb-0 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')} <ProfileItem
value='31/03/2003' title='Email'
/> value='contact@divlo.fr'
<ProfileItem link='mailto:contact@divlo.fr'
title={t('home:about.nationality')} />
value='Alsace, France' </ul>
/>
<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

@ -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'> <Image
<div className='profile-logo'> width={370}
<Image height={370}
width={800} src='/images/divlo_logo.png'
height={800} alt='Divlo'
src='/images/divlo_logo.png' />
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={ariaLabel}
aria-label={socialMedia} target='_blank'
target='_blank' rel='noopener noreferrer'
rel='noopener noreferrer' className='relative inline-block bg-transparent'
className='social-media-list__link' >
> {children}
<Tooltip title={socialMedia}> </a>
<Image </li>
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

@ -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 p-0 list-none text-center mt-2 px-0 py-4'>
<div className='row justify-content-center'> <SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
<ul className='social-media-list'> <TwitterIcon />
<SocialMediaItem </SocialMediaItem>
socialMedia='Twitter' <SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
link='https://twitter.com/Divlo_FR' <GitHubIcon />
/> </SocialMediaItem>
<SocialMediaItem <SocialMediaItem
socialMedia='GitHub' link='https://www.youtube.com/c/Divlo'
link='https://github.com/Divlo' ariaLabel='YouTube'
/> >
<SocialMediaItem <YouTubeIcon />
socialMedia='YouTube' </SocialMediaItem>
link='https://www.youtube.com/c/Divlo' <SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
/> <TwitchIcon />
<SocialMediaItem </SocialMediaItem>
socialMedia='Twitch' <SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
link='https://www.twitch.tv/divlo' <EmailIcon />
/> </SocialMediaItem>
<SocialMediaItem socialMedia='Email' link='mailto:contact@divlo.fr' /> </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 pb-6 md:pt-10 xl:pt-0 md:flex-row'>
<div className='row profile'> <ProfileLogo />
<ProfileLogo /> <div className='col-sm-24 col-md-14'>
<div className='col-sm-24 col-md-14'> <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='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
className='skills-link' target='_blank'
target='_blank' rel='noopener noreferrer'
rel='noopener noreferrer' >
> <div className='text-center'>
<div className='skills-content text-center'> <Image width={60} height={60} alt={skill} src={skillProperties.image} />
<Image <p className='mt-1'>{skill}</p>
width={60} </div>
height={60} </a>
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

@ -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='w-full px-4 mx-auto'>
<div className='container-fluid'> <div className='flex flex-wrap px-4 py-6'>
<div className='row row-padding'> <div className='flex-1'>
<div className='col-24'> <div className='mb-8 border-b border-gray-600 dark:border-opacity-10 dark:border-white'>
<div className='skills-header'> <h3 className='text-yellow font-semibold text-xl my-3 dark:text-yellow-dark'>
<h3 className='important'>{title}</h3> {title}
</div> </h3>
<div className='skills-body'>{children}</div>
</div> </div>
<div className='flex justify-around flex-wrap'>{children}</div>
</div> </div>
</div> </div>
</ShadowContainer> </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

@ -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,19 +30,20 @@ export const RevealFade: React.FC = props => {
{children} {children}
</div> </div>
<style jsx>{` <style jsx>
.reveal { {`
opacity: 0; .reveal {
visibility: hidden; opacity: 0;
transform: translateY(-30px); visibility: hidden;
} transform: translateY(-30px);
.reveal-visible { }
opacity: 1; .reveal-visible {
visibility: visible; opacity: 1;
transform: translateY(0); visibility: visible;
transition: all 500ms ease-out 100ms; transform: translateY(0);
} transition: all 500ms ease-out 100ms;
`} }
`}
</style> </style>
</> </>
) )

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}
{children} {...rest}
</h2> className='text-4xl font-semibold text-center mt-1 mb-7'
>
<style jsx> {children}
{` </h2>
.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 (
<ShadowContainer style={{ marginTop: 50 }}> <div className='px-3 w-full'>
<section ref={ref} {...rest}> <ShadowContainer style={{ marginTop: 50 }}>
{heading != null && <SectionHeading>{heading}</SectionHeading>} <section ref={ref} {...rest}>
<div className='container-fluid'>{children}</div> {heading != null && <SectionHeading>{heading}</SectionHeading>}
</section> <div className='px-3 w-full'>{children}</div>
</ShadowContainer> </section>
</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>
)} )}
<ShadowContainer> <div className='px-3 w-full'>
<div className='container-fluid'> <ShadowContainer>
<div className='row row-padding'>{children}</div> <div className='px-16 py-4 leading-8 w-full'>{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 flex flex-col 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,11 +0,0 @@
import { render } from '@testing-library/react'
import { Input } from '../Input'
describe('<Input />', () => {
it('should render the label', async () => {
const labelContent = 'label content'
const { getByText } = render(<Input label={labelContent} />)
expect(getByText(labelContent)).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,6 +17,7 @@ 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' '!**/jest.config.js'

View File

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

@ -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"
} }
} }

10508
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.15", "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.6.0"
}, },
"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",
"@styled-jsx/plugin-sass": "3.0.0",
"@testing-library/jest-dom": "5.12.0", "@testing-library/jest-dom": "5.12.0",
"@testing-library/react": "11.2.6", "@testing-library/react": "11.2.6",
"@types/jest": "26.0.23", "@types/jest": "26.0.23",
"@types/node": "15.0.0", "@types/node": "15.0.2",
"@types/nodemailer": "6.4.1", "@types/react": "17.0.5",
"@types/nprogress": "0.2.0",
"@types/react": "17.0.4",
"@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.13", "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,61 +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
}
})
const handler = async (
request: NextApiRequest,
response: NextApiResponse
): Promise<any> => {
if (request.method !== 'POST') {
return response.redirect('/404')
}
const { 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' })
}
try {
await emailTransporter.sendMail({
from: '"Divlo" <contact@divlo.fr>',
to: email,
subject: `Contact - ${validator.escape(subject)}`,
html: `
<b>Name:</b> ${validator.escape(name)} <br/>
<b>Email:</b> ${validator.escape(email)} <br/>
<b>Message:</b> ${validator.escape(message)}
`
})
return response.status(201).json({ type: 'success' })
} catch {
return response.status(500).json({ type: 'serverError' })
}
}
export default handler

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: []
}