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

feat: add divlo.fr

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

View File

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

View File

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

View File

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

38
components/ErrorPage.tsx Normal file
View File

@ -0,0 +1,38 @@
import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link'
export interface ErrorPageProps {
statusCode: number
message: string
}
export const ErrorPage: React.FC<ErrorPageProps> = props => {
const { message, statusCode } = props
const { t } = useTranslation()
return (
<>
<h1>
{t('errors:error')} <span className='important'>{statusCode}</span>
</h1>
<p className='text-center'>
{message} <Link href='/'>{t('returnToHomePage')}</Link>
</p>
<style jsx global>{`
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-width: 100vw;
min-height: 100%;
}
#__next {
padding-top: 0;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,29 @@
import setLanguage from 'next-translate/setLanguage'
interface LanguageButtonProps {
lang: string
}
export const LanguageButton: React.FC<LanguageButtonProps> = (props) => {
return (
<>
<span
onClick={async () => await setLanguage(props.lang)}
className='important'
>
{props.children}
</span>
<style jsx>
{`
span {
cursor: pointer;
}
span:hover {
text-decoration: underline;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,39 @@
import Image from 'next/image'
import { Tooltip } from 'components/design/Tooltip'
import { LanguageButton } from './LanguageButton'
interface LanguageFlagProps {
imageLink: string
title: string
lang: string
}
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
const { lang, title, imageLink } = props
return (
<>
<div className='LanguageFlag'>
<LanguageButton lang={lang}>
<Tooltip title={title}>
<Image alt={title} src={imageLink} width={31} height={31} />
</Tooltip>
</LanguageButton>
</div>
<style jsx>
{`
.LanguageFlag {
margin-right: 7px;
}
@media (max-width: 700px) {
.LanguageFlag {
display: none;
}
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,60 @@
import useTranslation from 'next-translate/useTranslation'
import { LanguageButton } from './LanguageButton'
import { LanguageFlag } from './LanguageFlag'
export const Footer: React.FC = () => {
const { t } = useTranslation()
return (
<>
<footer className='Footer text-center'>
<p className='Footer__text'>
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
</p>
<p className='Footer__lang'>
<LanguageButton lang='en'>{t('common:english')}</LanguageButton> |{' '}
<LanguageButton lang='fr'>{t('common:french')}</LanguageButton>
</p>
</footer>
<div className='Footer__flags'>
<LanguageFlag
lang='en'
imageLink='/images/flags/english_flag.png'
title={t('common:english')}
/>
<LanguageFlag
lang='fr'
imageLink='/images/flags/french_flag.png'
title={t('common:french')}
/>
</div>
<style jsx>
{`
.Footer {
border-top: var(--border-header-footer);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.Footer__text {
margin: 20px 0 10px 0;
}
.Footer__lang {
margin: 0 0 20px 0;
}
.Footer__flags {
display: flex;
position: fixed;
bottom: 28px;
left: 32px;
z-index: 10;
}
`}
</style>
</>
)
}

59
components/Head.tsx Normal file
View File

@ -0,0 +1,59 @@
import HeadTag from 'next/head'
interface HeadProps {
title?: string
image?: string
description?: string
url?: string
}
const Head: React.FC<HeadProps> = props => {
const {
title = 'Divlo',
image = '/images/icons/icon-96x96.png',
description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
url = 'https://divlo.divlo.fr/'
} = props
return (
<HeadTag>
<title>{title}</title>
<link rel='icon' type='image/png' href={image} />
{/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='description' content={description} />
<meta name='Language' content='fr, en' />
<meta name='theme-color' content='#ffd800' />
{/* Open Graph Metadata */}
<meta property='og:title' content={title} />
<meta property='og:type' content='website' />
<meta property='og:url' content={url} />
<meta property='og:image' content={image} />
<meta property='og:description' content={description} />
<meta property='og:locale' content='fr_FR, en_US' />
<meta property='og:site_name' content={title} />
{/* Twitter card Metadata */}
<meta name='twitter:card' content='summary' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
<meta name='twitter:image:src' content={image} />
{/* Google Verification */}
<meta
name='google-site-verification'
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
/>
{/* PWA Data */}
<link rel='manifest' href='/manifest.json' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='mobile-web-app-capable' content='yes' />
<link rel='apple-touch-icon' href={image} />
</HeadTag>
)
}
export default Head

View File

@ -0,0 +1,38 @@
import Link from 'next/link'
import Image from 'next/image'
export const BrandLogo: React.FC = () => {
return (
<>
<Link href='/'>
<a className='Header__brand-link'>
<Image
width={65}
height={65}
src='/images/divlo_icon_small.png'
alt="Divlo's Logo"
/>
</a>
</Link>
<style jsx>
{`
.Header__brand-link {
display: inline-block;
padding-top: 0.3125rem;
padding-bottom: 0.3125rem;
margin-right: 1rem;
font-size: 1.25rem;
line-height: inherit;
white-space: nowrap;
}
@media (min-width: 993px) {
.Header__brand-link {
width: 40%;
}
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,76 @@
import classNames from 'classnames'
type HamburgerIconComponent = React.FC<{
isActive: boolean
handleToggleNavbar: () => void
}>
export const HamburgerIcon: HamburgerIconComponent = props => {
return (
<>
<div
onClick={props.handleToggleNavbar}
className={classNames('Header__hamburger', {
'Header__hamburger-active': props.isActive
})}
>
<span />
</div>
<style jsx>
{`
.Header__hamburger {
display: none;
width: 56px;
height: 40px;
cursor: pointer;
background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
position: relative;
}
.Header__hamburger > span,
.Header__hamburger > span::before,
.Header__hamburger > span::after {
position: absolute;
width: 22px;
height: 1.3px;
background-color: rgba(255, 255, 255);
}
.Header__hamburger > span {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: background-color 0.3s ease-in-out;
}
.Header__hamburger > span::before,
.Header__hamburger > span::after {
content: '';
transition: transform 0.3s ease-in-out;
}
.Header__hamburger > span::before {
transform: translateY(-8px);
}
.Header__hamburger > span::after {
transform: translateY(8px);
}
.Header__hamburger-active span {
background-color: transparent;
}
.Header__hamburger-active > span::before {
transform: translateY(0px) rotateZ(45deg);
}
.Header__hamburger-active > span::after {
transform: translateY(0px) rotateZ(-45deg);
}
/* Hamburger icon on Mobile */
@media (max-width: 992px) {
.Header__hamburger {
display: flex;
}
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,54 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import classNames from 'classnames'
type NavigationLinkComponent = React.FC<{ path: string }>
export const NavigationLink: NavigationLinkComponent = props => {
const { pathname } = useRouter()
const isCurrentPage = pathname === props.path
return (
<>
<li className='navbar-item'>
<Link href={props.path}>
<a
className={classNames('navbar-link', {
'navbar-link-active': isCurrentPage
})}
>
{props.children}
</a>
</Link>
</li>
<style jsx>
{`
.navbar-link {
display: block;
padding: 0.5rem 1rem;
}
.navbar-link:hover {
text-decoration: none;
color: rgba(255, 255, 255, 0.75);
}
.navbar-link,
.navbar-link-active {
color: rgba(255, 255, 255, 0.5);
}
.navbar-link-active,
.navbar-link-active:hover {
color: var(--text-color);
}
.navbar-item {
list-style: none;
}
.navbar-link {
font-size: 16px;
padding: 0.5rem;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,59 @@
import classNames from 'classnames'
import useTranslation from 'next-translate/useTranslation'
import { NavigationLink } from './NavigationLink'
type NavigationComponent = React.FC<{ isActive: boolean }>
export const Navigation: NavigationComponent = props => {
const { t } = useTranslation()
return (
<>
<nav className='Header__navbar'>
<ul
className={classNames('navbar__list', {
'navbar__list-active': props.isActive
})}
>
<NavigationLink path='/'>{t('common:home')}</NavigationLink>
<NavigationLink path='/setup'>Setup</NavigationLink>
</ul>
</nav>
<style jsx>
{`
@media (min-width: 992px) {
.Header__navbar {
display: flex;
flex-basis: auto;
}
}
.Header__navbar {
flex-basis: 100%;
flex-grow: 1;
align-items: center;
}
.navbar__list {
display: flex;
flex-direction: row;
margin-left: auto;
}
.navbar__list.navbar__list-active {
margin: 0 !important;
display: flex;
}
@media (max-width: 992px) {
.navbar__list {
display: none;
flex-direction: column;
align-items: center;
padding-left: 0;
list-style: none;
}
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,69 @@
import { useState } from 'react'
import { HamburgerIcon } from './HamburgerIcon'
import { BrandLogo } from './BrandLogo'
import { Navigation } from './Navigation'
export const Header: React.FC = () => {
const [isActive, setIsActive] = useState(false)
const handleToggleNavbar = (): void => {
setIsActive(!isActive)
}
return (
<>
<header className='Header'>
<div className='container'>
<BrandLogo />
<HamburgerIcon
isActive={isActive}
handleToggleNavbar={handleToggleNavbar}
/>
<Navigation isActive={isActive} />
</div>
</header>
<style jsx>
{`
.Header {
position: fixed;
width: 100%;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
border-bottom: var(--border-header-footer);
background-color: var(--color-background);
}
@media (min-width: 992px) {
.Header {
display: flex;
flex-basis: auto;
flex-flow: row nowrap;
justify-content: flex-start;
}
}
.Header > .container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
@media (min-width: 992px) {
.Header > .container {
flex-wrap: nowrap;
}
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,20 @@
import htmlParser from 'html-react-parser'
export interface InterestParagraphProps {
title: string
description: string
}
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
const { title, description } = props
return (
<>
<p className='text-center'>
<strong className='important'>{title}</strong>
<br />
<span className='paragraph-color'>{htmlParser(description)}</span>
</p>
</>
)
}

View File

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

View File

@ -0,0 +1,45 @@
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
import { faGit } from '@fortawesome/free-brands-svg-icons'
import { InterestItem } from './InterestItem'
export const InterestsList: React.FC = () => {
return (
<>
<div className='container-list'>
<ul className='interests-list'>
<InterestItem
title='Developer Full Stack Junior'
fontAwesomeIcon={faCode}
/>
<InterestItem
title='Passionate about High-Tech'
fontAwesomeIcon={faMicrochip}
/>
<InterestItem
title='Open-Source enthusiast'
fontAwesomeIcon={faGit}
/>
</ul>
</div>
<style jsx>
{`
.container-list {
display: flex;
justify-content: center;
margin: 15px 0 15px 0;
}
.interests-list {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
width: 60%;
list-style: none;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,23 @@
import useTranslation from 'next-translate/useTranslation'
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
import { InterestsList } from './InterestsList'
export const Interests: React.FC = () => {
const { t } = useTranslation()
const paragraphs: InterestParagraphProps[] = t('home:interests.paragraphs', {}, {
returnObjects: true
})
return (
<>
<div className='col-24'>
{paragraphs.map((paragraph, index) => {
return <InterestParagraph key={index} {...paragraph} />
})}
<InterestsList />
</div>
</>
)
}

View File

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

View File

@ -0,0 +1,23 @@
import useTranslation from 'next-translate/useTranslation'
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
export const Portfolio: React.FC = () => {
const { t } = useTranslation('home')
const items: PortfolioItemProps[] = t('home:portfolio.items', {}, {
returnObjects: true
})
return (
<>
<div className='container-fluid'>
<div className='row justify-content-center'>
{items.map((item, index) => {
return <PortfolioItem key={index} {...item} />
})}
</div>
</div>
</>
)
}

View File

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

View File

@ -0,0 +1,41 @@
import useTranslation from 'next-translate/useTranslation'
export const ProfileInfo: React.FC = () => {
const { t } = useTranslation()
return (
<>
<div className='profile-info'>
<h1 className='profile-title'>
{t('home:about.IAm')} <strong className='important'>Divlo</strong>
</h1>
<h2 className='profile-description'>{t('home:about.description')}</h2>
</div>
<style jsx>
{`
.profile-info {
padding-bottom: 25px;
margin-bottom: 25px;
border-bottom: 1px solid #dedede;
}
.profile-title {
font-size: 36px;
line-height: 1.1;
font-weight: 300;
margin-bottom: 10px;
}
.profile-title > strong {
font-weight: 600;
}
.profile-description {
font-size: 17.4px;
font-weight: 400;
line-height: 1.1;
margin: 0;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,79 @@
interface ProfileItemProps {
title: string
value: string
link?: string
}
export const ProfileItem: React.FC<ProfileItemProps> = props => {
const { title, value, link } = props
return (
<>
<li className='profile-list__item'>
<strong className='profile-list__item-title'>{title}</strong>
<span className='profile-list__item-info'>
{link != null ? (
<a className='profile-list__link' href={link}>
{value}
</a>
) : (
value
)}
</span>
</li>
<style jsx>
{`
.profile-list__item {
margin-bottom: 13px;
}
.profile-list__item::after,
.profile-list__item::before {
content: ' ';
display: table;
}
.profile-list__item::after {
clear: both;
}
.profile-list__item-title {
display: block;
width: 120px;
float: left;
color: #d4d4d5;
font-size: 12px;
font-weight: 700;
line-height: 20px;
text-transform: uppercase;
}
.profile-list__item-info {
display: block;
margin-left: 125px;
font-size: 15px;
font-weight: 400;
line-height: 20px;
color: #84898e;
}
.profile-list__link {
color: #84898e;
}
@media (max-width: 576px) {
.profile-list__item-title {
margin-bottom: 3px;
}
.profile-list__item-info {
margin-left: 0;
margin-bottom: 15px;
}
.profile-list__item-info,
.profile-list__item-title {
width: 100%;
float: none;
line-height: 1.2;
}
}
`}
</style>
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
export interface TableRow {
title: string
value: string
}
export interface TableProps {
rows: TableRow[]
}
export const Table: React.FC<TableProps> = props => {
const { rows } = props
return (
<>
<div className='col-24 table-column text-center'>
<table>
<tbody>
{rows.map((row, index) => {
return (
<tr key={index}>
<th className='table-row'>{row.title}</th>
<td className='table-row'>{row.value}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<style jsx>{`
.table-column {
display: grid;
}
.table,
th,
td {
border: 1px solid var(--color-text-1);
border-collapse: collapse;
}
.table-row {
padding: 15px;
}
.image-setup {
width: 85%;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,21 @@
export const TableTitle: React.FC = props => {
const { children } = props
return (
<>
<div className='col-24'>
<p className='text-center title-table'>
<strong className='important'>{children}</strong>
</p>
</div>
<style jsx>{`
.title-table {
font-size: 24px;
margin: 40px 0 20px 0;
}
`}
</style>
</>
)
}

137
components/Setup/index.tsx Normal file
View File

@ -0,0 +1,137 @@
import useTranslation from 'next-translate/useTranslation'
import Image from 'next/image'
import { Table, TableRow } from './Table'
import { TableTitle } from './TableTitle'
export const Setup: React.FC = () => {
const { t } = useTranslation()
const rowsConfigPC: TableRow[] = [
{
title: t('setup:configPC.motherboard'),
value: 'MSI Z87-G45 GAMING'
},
{
title: t('setup:configPC.processor'),
value: 'Intel Core i5-4690k'
},
{
title: t('setup:configPC.graphicCard'),
value: 'Zotac GeForce GTX 970'
},
{
title: t('setup:configPC.ramMemory'),
value: '16 GB (2 x 8Go) Kingston HyperX'
},
{
title: t('setup:configPC.hardDrive'),
value: '256 GB SSD Crucial & 2 TB Seagate'
}
]
const rowsPeripherals: TableRow[] = [
{
title: t('setup:peripheral.keyboard'),
value: 'Corsair K95 RGB'
},
{
title: t('setup:peripheral.mouse'),
value: 'SteelSeries Rival 310'
},
{
title: t('setup:peripheral.headset'),
value: 'SteelSeries ARCTIS PRO + GAMEDAC'
},
{
title: t('setup:peripheral.mainScreen'),
value: 'IIyama PL2480H'
},
{
title: t('setup:peripheral.secondScreen'),
value: 'Samsung SyncMaster 2220LM'
}
]
const rowsOffice: TableRow[] = [
{
title: t('setup:officeOther.mousepad'),
value: 'SteelSeries QCK Heavy (Grand) as string'
},
{
title: 'Mouse Bungee',
value: 'BenQ ZOWIE Camade'
},
{
title: t('setup:officeOther.usb'),
value: 'Kingston 128GB'
},
{
title: 'Smartphone',
value: 'Samsung Galaxy A5 (2017)'
}
]
return (
<>
<TableTitle>{t('setup:configPC.title')}</TableTitle>
<Table rows={rowsConfigPC} />
<TableTitle>{t('setup:peripheral.title')}</TableTitle>
<Table rows={rowsPeripherals} />
<TableTitle>{t('setup:officeOther.title')}</TableTitle>
<Table rows={rowsOffice} />
<div
className='row row-padding justify-content-center'
style={{ marginTop: 50 }}
>
<Image
src='/images/setup/setup2019.png'
alt='Setup Divlo'
width={856.8}
height={672.58}
className='Setup__image'
/>
</div>
<div className='row row-padding justify-content-center'>
<Image
src='/images/setup/setup2019-lights.jpg'
alt='Setup Divlo'
width={856.8}
height={672.58}
className='Setup__image'
/>
</div>
<div className='row row-padding'>
<TableTitle>{t('setup:connexion')}</TableTitle>
<div style={{ marginBottom: 25 }} className='col-24 text-center'>
<a
href='https://www.speedtest.net/result/8533865940'
target='_blank'
rel='noopener noreferrer'
aria-label='Speedtest link'
>
<Image
src='/images/setup/speedtest-result.png'
alt='Speedtest Result'
width={308}
height={165}
/>
</a>
</div>
</div>
<style jsx global>
{`
.Setup__image {
width: 85% !important;
}
`}
</style>
</>
)
}

View File

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

View File

@ -0,0 +1,44 @@
import { ShadowContainer } from 'components/design/ShadowContainer'
export interface SkillsSectionProps {
title: string
children: React.ReactNode
}
export const SkillsSection: React.FC<SkillsSectionProps> = props => {
const { title, children } = props
return (
<>
<ShadowContainer>
<div className='container-fluid'>
<div className='row row-padding'>
<div className='col-24'>
<div className='skills-header'>
<h3 className='important'>{title}</h3>
</div>
<div className='skills-body'>{children}</div>
</div>
</div>
</div>
</ShadowContainer>
<style jsx>{`
.skills-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 15px;
}
.skills-header > h3 {
margin-bottom: 15px;
}
.skills-body {
display: flex;
justify-content: space-around;
flex-flow: row wrap;
padding-top: 1.5rem;
}
`}
</style>
</>
)
}

View File

@ -0,0 +1,41 @@
import useTranslation from 'next-translate/useTranslation'
import { Skill } from './Skill'
import { SkillsSection } from './SkillsSection'
export const Skills: React.FC = () => {
const { t } = useTranslation()
return (
<>
<SkillsSection title={t('home:skills.languages')}>
<Skill skill='JavaScript' />
<Skill skill='TypeScript' />
<Skill skill='Python' />
<Skill skill='Dart' />
</SkillsSection>
<SkillsSection title='Front-end'>
<Skill skill='HTML' />
<Skill skill='CSS' />
<Skill skill='SASS' />
<Skill skill='React.js (+ Next.js)' />
<Skill skill='Flutter' />
</SkillsSection>
<SkillsSection title='Back-end'>
<Skill skill='Node.js' />
<Skill skill='Strapi' />
<Skill skill='MySQL' />
</SkillsSection>
<SkillsSection title={t('home:skills.softwareTools')}>
<Skill skill='Ubuntu' />
<Skill skill='Hyper' />
<Skill skill='Visual Studio Code' />
<Skill skill='Git' />
<Skill skill='Docker' />
</SkillsSection>
</>
)
}

View File

@ -0,0 +1,70 @@
export const skills = {
JavaScript: {
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
image: '/images/skills/JavaScript.png'
},
TypeScript: {
link: 'https://www.typescriptlang.org/',
image: '/images/skills/TypeScript.png'
},
Python: {
link: 'https://www.python.org/',
image: '/images/skills/Python.png'
},
Dart: {
link: 'https://dart.dev/',
image: '/images/skills/Dart.png'
},
Flutter: {
link: 'https://flutter.dev/',
image: '/images/skills/Flutter.webp'
},
HTML: {
link: 'https://developer.mozilla.org/docs/Web/HTML',
image: '/images/skills/HTML.png'
},
CSS: {
link: 'https://developer.mozilla.org/docs/Web/CSS',
image: '/images/skills/CSS.png'
},
SASS: {
link: 'https://sass-lang.com/',
image: '/images/skills/SASS.svg'
},
'React.js (+ Next.js)': {
link: 'https://reactjs.org/',
image: '/images/skills/ReactJS.png'
},
'Node.js': {
link: 'https://nodejs.org/',
image: '/images/skills/NodeJS.png'
},
MySQL: {
link: 'https://www.mysql.com/',
image: '/images/skills/MySQL.png'
},
Strapi: {
link: 'https://strapi.io/',
image: '/images/skills/Strapi.png'
},
'Visual Studio Code': {
link: 'https://code.visualstudio.com/',
image: '/images/skills/Visual_Studio_Code.png'
},
Git: {
link: 'https://git-scm.com/',
image: '/images/skills/Git.png'
},
Hyper: {
link: 'https://hyper.is/',
image: '/images/skills/Hyper.svg'
},
Ubuntu: {
link: 'https://ubuntu.com/',
image: '/images/skills/Ubuntu.png'
},
Docker: {
link: 'https://www.docker.com/',
image: '/images/skills/Docker.png'
}
} as const

View File

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

View File

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

View File

@ -0,0 +1,49 @@
import { useEffect, useRef } from 'react'
export const RevealFade: React.FC = props => {
const { children } = props
const htmlElement = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new window.IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('reveal-visible')
observer.unobserve(entry.target)
}
})
},
{
root: null,
rootMargin: '0px',
threshold: 0.28
}
)
observer.observe(htmlElement.current as HTMLDivElement)
}, [])
return (
<>
<div ref={htmlElement} className='reveal'>
{children}
</div>
<style jsx>{`
.reveal {
opacity: 0;
visibility: hidden;
transform: translateY(-30px);
}
.reveal-visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
transition: all 500ms ease-out 100ms;
}
`}
</style>
</>
)
}

View File

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

View File

@ -0,0 +1,62 @@
import { forwardRef } from 'react'
import { ShadowContainer } from '../ShadowContainer'
import { SectionHeading } from './SectionHeading'
type SectionProps = React.ComponentPropsWithRef<'section'> & {
heading?: string
description?: string
isMain?: boolean
withoutShadowContainer?: boolean
}
export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
const {
children,
heading,
description,
isMain = false,
withoutShadowContainer = false,
...rest
} = props
if (isMain) {
return (
<ShadowContainer style={{ marginTop: 50 }}>
<section ref={ref} {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>}
<div className='container-fluid'>{children}</div>
</section>
</ShadowContainer>
)
}
if (withoutShadowContainer) {
return (
<section ref={ref} {...rest}>
{heading != null && <SectionHeading>{heading}</SectionHeading>}
<div className='container-fluid'>{children}</div>
</section>
)
}
return (
<section ref={ref} {...rest}>
{heading != null && (
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
{heading}
</SectionHeading>
)}
{description != null && (
<p style={{ marginTop: 7 }} className='text-center'>
{description}
</p>
)}
<ShadowContainer>
<div className='container-fluid'>
<div className='row row-padding'>{children}</div>
</div>
</ShadowContainer>
</section>
)
})

View File

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

View File

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

View File

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