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:
31
components/Contact/FormResult.tsx
Normal file
31
components/Contact/FormResult.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { FormState } from './FormState'
|
||||
import { ResultState } from './index'
|
||||
|
||||
export interface FormResultProps {
|
||||
state: ResultState
|
||||
}
|
||||
|
||||
export const FormResult: React.FC<FormResultProps> = (props) => {
|
||||
const { state } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (state === 'idle') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (state === 'loading' || state === 'success') {
|
||||
return (
|
||||
<FormState state={state}>
|
||||
{t(`home:contact.result.${state}`)}
|
||||
</FormState>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormState state='error'>
|
||||
{t(`home:contact.result.${state}`)}
|
||||
</FormState>
|
||||
)
|
||||
}
|
39
components/Contact/FormState.tsx
Normal file
39
components/Contact/FormState.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export interface FormStateProps extends React.ComponentPropsWithRef<'p'> {
|
||||
state: 'success' | 'error' | 'loading'
|
||||
children: string
|
||||
}
|
||||
|
||||
export const FormState: React.FC<FormStateProps> = props => {
|
||||
const { state, children, ...rest } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-result text-center'>
|
||||
<p className={state} {...rest}>
|
||||
{['error', 'success'].includes(state) && (
|
||||
<b>
|
||||
{state === 'error' ? t('home:contact.error') : t('home:contact.success')}:
|
||||
</b>
|
||||
)}{' '}
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-result {
|
||||
margin: 30px;
|
||||
}
|
||||
.success {
|
||||
color: #90ee90;
|
||||
}
|
||||
.error {
|
||||
color: #ff7f7f;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
89
components/Contact/index.tsx
Normal file
89
components/Contact/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useState } from 'react'
|
||||
import Form, { HandleForm } from 'react-component-form'
|
||||
import axios from 'axios'
|
||||
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { Textarea } from 'components/design/Textarea'
|
||||
import { FormResult } from './FormResult'
|
||||
|
||||
export const resultState = [
|
||||
'idle',
|
||||
'success',
|
||||
'loading',
|
||||
'requiredFields',
|
||||
'invalidEmail',
|
||||
'serverError'
|
||||
] as const
|
||||
|
||||
export type ResultState = typeof resultState[number]
|
||||
|
||||
export const Contact: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [state, setState] = useState<ResultState>('idle')
|
||||
|
||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
||||
setState('loading')
|
||||
try {
|
||||
const { data } = await axios.post<{ type: ResultState }>(
|
||||
'/api/send-email',
|
||||
formData
|
||||
)
|
||||
if (data.type === 'success') {
|
||||
setState('success')
|
||||
return formElement.reset()
|
||||
}
|
||||
return setState('serverError')
|
||||
} catch (error) {
|
||||
const type = error.response.data.type
|
||||
if (resultState.includes(type)) {
|
||||
return setState(type)
|
||||
}
|
||||
return setState('serverError')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
label={`${t('home:contact.nameField')} :`}
|
||||
type='text'
|
||||
name='name'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label='Email :'
|
||||
type='email'
|
||||
name='email'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label={`${t('home:contact.subjectField')} :`}
|
||||
type='text'
|
||||
name='subject'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='Message :'
|
||||
name='message'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button type='submit'>{t('home:contact.sendEmail')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<FormResult state={state} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
38
components/ErrorPage.tsx
Normal file
38
components/ErrorPage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface ErrorPageProps {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export const ErrorPage: React.FC<ErrorPageProps> = props => {
|
||||
const { message, statusCode } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
{t('errors:error')} <span className='important'>{statusCode}</span>
|
||||
</h1>
|
||||
<p className='text-center'>
|
||||
{message} <Link href='/'>{t('returnToHomePage')}</Link>
|
||||
</p>
|
||||
|
||||
<style jsx global>{`
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 100vw;
|
||||
min-height: 100%;
|
||||
}
|
||||
#__next {
|
||||
padding-top: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
29
components/Footer/LanguageButton.tsx
Normal file
29
components/Footer/LanguageButton.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
|
||||
interface LanguageButtonProps {
|
||||
lang: string
|
||||
}
|
||||
|
||||
export const LanguageButton: React.FC<LanguageButtonProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
onClick={async () => await setLanguage(props.lang)}
|
||||
className='important'
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
39
components/Footer/LanguageFlag.tsx
Normal file
39
components/Footer/LanguageFlag.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
import { LanguageButton } from './LanguageButton'
|
||||
|
||||
interface LanguageFlagProps {
|
||||
imageLink: string
|
||||
title: string
|
||||
lang: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { lang, title, imageLink } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='LanguageFlag'>
|
||||
<LanguageButton lang={lang}>
|
||||
<Tooltip title={title}>
|
||||
<Image alt={title} src={imageLink} width={31} height={31} />
|
||||
</Tooltip>
|
||||
</LanguageButton>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.LanguageFlag {
|
||||
margin-right: 7px;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.LanguageFlag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
60
components/Footer/index.tsx
Normal file
60
components/Footer/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { LanguageButton } from './LanguageButton'
|
||||
import { LanguageFlag } from './LanguageFlag'
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className='Footer text-center'>
|
||||
<p className='Footer__text'>
|
||||
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
|
||||
</p>
|
||||
<p className='Footer__lang'>
|
||||
<LanguageButton lang='en'>{t('common:english')}</LanguageButton> |{' '}
|
||||
<LanguageButton lang='fr'>{t('common:french')}</LanguageButton>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<div className='Footer__flags'>
|
||||
<LanguageFlag
|
||||
lang='en'
|
||||
imageLink='/images/flags/english_flag.png'
|
||||
title={t('common:english')}
|
||||
/>
|
||||
<LanguageFlag
|
||||
lang='fr'
|
||||
imageLink='/images/flags/french_flag.png'
|
||||
title={t('common:french')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Footer {
|
||||
border-top: var(--border-header-footer);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.Footer__text {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
.Footer__lang {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.Footer__flags {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
left: 32px;
|
||||
z-index: 10;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
59
components/Head.tsx
Normal file
59
components/Head.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import HeadTag from 'next/head'
|
||||
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const Head: React.FC<HeadProps> = props => {
|
||||
const {
|
||||
title = 'Divlo',
|
||||
image = '/images/icons/icon-96x96.png',
|
||||
description = "I'm Divlo, I'm 18 years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech",
|
||||
url = 'https://divlo.divlo.fr/'
|
||||
} = props
|
||||
|
||||
return (
|
||||
<HeadTag>
|
||||
<title>{title}</title>
|
||||
<link rel='icon' type='image/png' href={image} />
|
||||
|
||||
{/* Meta Tag */}
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||
<meta name='description' content={description} />
|
||||
<meta name='Language' content='fr, en' />
|
||||
<meta name='theme-color' content='#ffd800' />
|
||||
|
||||
{/* Open Graph Metadata */}
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:image' content={image} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:locale' content='fr_FR, en_US' />
|
||||
<meta property='og:site_name' content={title} />
|
||||
|
||||
{/* Twitter card Metadata */}
|
||||
<meta name='twitter:card' content='summary' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<meta name='twitter:image:src' content={image} />
|
||||
|
||||
{/* Google Verification */}
|
||||
<meta
|
||||
name='google-site-verification'
|
||||
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
|
||||
/>
|
||||
|
||||
{/* PWA Data */}
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<link rel='apple-touch-icon' href={image} />
|
||||
</HeadTag>
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
38
components/Header/BrandLogo.tsx
Normal file
38
components/Header/BrandLogo.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
export const BrandLogo: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Link href='/'>
|
||||
<a className='Header__brand-link'>
|
||||
<Image
|
||||
width={65}
|
||||
height={65}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt="Divlo's Logo"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header__brand-link {
|
||||
display: inline-block;
|
||||
padding-top: 0.3125rem;
|
||||
padding-bottom: 0.3125rem;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (min-width: 993px) {
|
||||
.Header__brand-link {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
76
components/Header/HamburgerIcon.tsx
Normal file
76
components/Header/HamburgerIcon.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
type HamburgerIconComponent = React.FC<{
|
||||
isActive: boolean
|
||||
handleToggleNavbar: () => void
|
||||
}>
|
||||
|
||||
export const HamburgerIcon: HamburgerIconComponent = props => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={props.handleToggleNavbar}
|
||||
className={classNames('Header__hamburger', {
|
||||
'Header__hamburger-active': props.isActive
|
||||
})}
|
||||
>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header__hamburger {
|
||||
display: none;
|
||||
width: 56px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
}
|
||||
.Header__hamburger > span,
|
||||
.Header__hamburger > span::before,
|
||||
.Header__hamburger > span::after {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 1.3px;
|
||||
background-color: rgba(255, 255, 255);
|
||||
}
|
||||
.Header__hamburger > span {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
.Header__hamburger > span::before,
|
||||
.Header__hamburger > span::after {
|
||||
content: '';
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
.Header__hamburger > span::before {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.Header__hamburger > span::after {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
.Header__hamburger-active span {
|
||||
background-color: transparent;
|
||||
}
|
||||
.Header__hamburger-active > span::before {
|
||||
transform: translateY(0px) rotateZ(45deg);
|
||||
}
|
||||
.Header__hamburger-active > span::after {
|
||||
transform: translateY(0px) rotateZ(-45deg);
|
||||
}
|
||||
/* Hamburger icon on Mobile */
|
||||
@media (max-width: 992px) {
|
||||
.Header__hamburger {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
54
components/Header/Navigation/NavigationLink.tsx
Normal file
54
components/Header/Navigation/NavigationLink.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type NavigationLinkComponent = React.FC<{ path: string }>
|
||||
|
||||
export const NavigationLink: NavigationLinkComponent = props => {
|
||||
const { pathname } = useRouter()
|
||||
const isCurrentPage = pathname === props.path
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='navbar-item'>
|
||||
<Link href={props.path}>
|
||||
<a
|
||||
className={classNames('navbar-link', {
|
||||
'navbar-link-active': isCurrentPage
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.navbar-link {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.navbar-link:hover {
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.navbar-link,
|
||||
.navbar-link-active {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.navbar-link-active,
|
||||
.navbar-link-active:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.navbar-item {
|
||||
list-style: none;
|
||||
}
|
||||
.navbar-link {
|
||||
font-size: 16px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
59
components/Header/Navigation/index.tsx
Normal file
59
components/Header/Navigation/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import classNames from 'classnames'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { NavigationLink } from './NavigationLink'
|
||||
|
||||
type NavigationComponent = React.FC<{ isActive: boolean }>
|
||||
|
||||
export const Navigation: NavigationComponent = props => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className='Header__navbar'>
|
||||
<ul
|
||||
className={classNames('navbar__list', {
|
||||
'navbar__list-active': props.isActive
|
||||
})}
|
||||
>
|
||||
<NavigationLink path='/'>{t('common:home')}</NavigationLink>
|
||||
<NavigationLink path='/setup'>Setup</NavigationLink>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
@media (min-width: 992px) {
|
||||
.Header__navbar {
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
.Header__navbar {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
.navbar__list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
.navbar__list.navbar__list-active {
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.navbar__list {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
69
components/Header/index.tsx
Normal file
69
components/Header/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { HamburgerIcon } from './HamburgerIcon'
|
||||
import { BrandLogo } from './BrandLogo'
|
||||
import { Navigation } from './Navigation'
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
|
||||
const handleToggleNavbar = (): void => {
|
||||
setIsActive(!isActive)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='Header'>
|
||||
<div className='container'>
|
||||
<BrandLogo />
|
||||
<HamburgerIcon
|
||||
isActive={isActive}
|
||||
handleToggleNavbar={handleToggleNavbar}
|
||||
/>
|
||||
<Navigation isActive={isActive} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
border-bottom: var(--border-header-footer);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.Header {
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.Header > .container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.Header > .container {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
20
components/Interests/InterestParagraph.tsx
Normal file
20
components/Interests/InterestParagraph.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import htmlParser from 'html-react-parser'
|
||||
|
||||
export interface InterestParagraphProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
const { title, description } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-center'>
|
||||
<strong className='important'>{title}</strong>
|
||||
<br />
|
||||
<span className='paragraph-color'>{htmlParser(description)}</span>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Interests/InterestsList/InterestItem.tsx
Normal file
41
components/Interests/InterestsList/InterestItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
|
||||
interface InterestItemProps {
|
||||
title: string
|
||||
fontAwesomeIcon: IconDefinition
|
||||
}
|
||||
|
||||
export const InterestItem: React.FC<InterestItemProps> = props => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='interest-item'>
|
||||
<Tooltip title={title}>
|
||||
<FontAwesomeIcon
|
||||
className='color-primary'
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'block'
|
||||
}}
|
||||
icon={fontAwesomeIcon}
|
||||
/>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.interest-item {
|
||||
margin: 7px 5px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
45
components/Interests/InterestsList/index.tsx
Normal file
45
components/Interests/InterestsList/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGit } from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
import { InterestItem } from './InterestItem'
|
||||
|
||||
export const InterestsList: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='container-list'>
|
||||
<ul className='interests-list'>
|
||||
<InterestItem
|
||||
title='Developer Full Stack Junior'
|
||||
fontAwesomeIcon={faCode}
|
||||
/>
|
||||
<InterestItem
|
||||
title='Passionate about High-Tech'
|
||||
fontAwesomeIcon={faMicrochip}
|
||||
/>
|
||||
<InterestItem
|
||||
title='Open-Source enthusiast'
|
||||
fontAwesomeIcon={faGit}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.container-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
.interests-list {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 60%;
|
||||
list-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
23
components/Interests/index.tsx
Normal file
23
components/Interests/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { InterestParagraph, InterestParagraphProps } from './InterestParagraph'
|
||||
import { InterestsList } from './InterestsList'
|
||||
|
||||
export const Interests: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const paragraphs: InterestParagraphProps[] = t('home:interests.paragraphs', {}, {
|
||||
returnObjects: true
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
return <InterestParagraph key={index} {...paragraph} />
|
||||
})}
|
||||
<InterestsList />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
102
components/Portfolio/PortfolioItem.tsx
Normal file
102
components/Portfolio/PortfolioItem.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
description: string
|
||||
link: string
|
||||
image: string
|
||||
}
|
||||
|
||||
export const PortfolioItem: React.FC<PortfolioItemProps> = props => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-sm-24 col-md-10 col-xl-7 portfolio-grid'>
|
||||
<a
|
||||
className='portfolio-link'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href={link}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className='portfolio-figure'>
|
||||
<Image width={300} height={300} src={image} alt={title} />
|
||||
</div>
|
||||
<div className='portfolio-caption'>
|
||||
<h3 className='portfolio-title important'>{title}</h3>
|
||||
<p className='portfolio-description'>{description}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
.portfolio-figure img[alt='${title}'] {
|
||||
max-height: 300px;
|
||||
max-width: 300px;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.portfolio-grid:hover img[alt='${title}'] {
|
||||
opacity: 0.05;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.portfolio-grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
margin: 0 0 50px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* col-md */
|
||||
@media (min-width: 768px) {
|
||||
.portfolio-grid {
|
||||
margin: 0 30px 50px 30px;
|
||||
}
|
||||
}
|
||||
/* col-xl */
|
||||
@media (min-width: 1200px) {
|
||||
.portfolio-grid {
|
||||
margin: 0 20px 50px 20px;
|
||||
}
|
||||
}
|
||||
.portfolio-figure {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.portfolio-caption {
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.portfolio-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
.portfolio-grid:hover .portfolio-caption {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
}
|
||||
.portfolio-grid:hover .portfolio-link {
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
23
components/Portfolio/index.tsx
Normal file
23
components/Portfolio/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { PortfolioItem, PortfolioItemProps } from './PortfolioItem'
|
||||
|
||||
export const Portfolio: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
const items: PortfolioItemProps[] = t('home:portfolio.items', {}, {
|
||||
returnObjects: true
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='container-fluid'>
|
||||
<div className='row justify-content-center'>
|
||||
{items.map((item, index) => {
|
||||
return <PortfolioItem key={index} {...item} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
31
components/Profile/ProfileDescriptionBottom.tsx
Normal file
31
components/Profile/ProfileDescriptionBottom.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Translation from 'next-translate/Trans'
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='profile-description-bottom'>
|
||||
<Translation
|
||||
i18nKey={t('home:about.descriptionBottom')}
|
||||
components={[<br key='break' />]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-description-bottom {
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
line-height: 25px;
|
||||
color: #b2bac2;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Profile/ProfileInfo.tsx
Normal file
41
components/Profile/ProfileInfo.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export const ProfileInfo: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='profile-info'>
|
||||
<h1 className='profile-title'>
|
||||
{t('home:about.IAm')} <strong className='important'>Divlo</strong>
|
||||
</h1>
|
||||
<h2 className='profile-description'>{t('home:about.description')}</h2>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-info {
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
.profile-title {
|
||||
font-size: 36px;
|
||||
line-height: 1.1;
|
||||
font-weight: 300;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.profile-title > strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.profile-description {
|
||||
font-size: 17.4px;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
79
components/Profile/ProfileList/ProfileItem.tsx
Normal file
79
components/Profile/ProfileList/ProfileItem.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
interface ProfileItemProps {
|
||||
title: string
|
||||
value: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
export const ProfileItem: React.FC<ProfileItemProps> = props => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='profile-list__item'>
|
||||
<strong className='profile-list__item-title'>{title}</strong>
|
||||
<span className='profile-list__item-info'>
|
||||
{link != null ? (
|
||||
<a className='profile-list__link' href={link}>
|
||||
{value}
|
||||
</a>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-list__item {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
.profile-list__item::after,
|
||||
.profile-list__item::before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.profile-list__item::after {
|
||||
clear: both;
|
||||
}
|
||||
.profile-list__item-title {
|
||||
display: block;
|
||||
width: 120px;
|
||||
float: left;
|
||||
color: #d4d4d5;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
display: block;
|
||||
margin-left: 125px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #84898e;
|
||||
}
|
||||
.profile-list__link {
|
||||
color: #84898e;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.profile-list__item-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.profile-list__item-info {
|
||||
margin-left: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.profile-list__item-info,
|
||||
.profile-list__item-title {
|
||||
width: 100%;
|
||||
float: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
37
components/Profile/ProfileList/index.tsx
Normal file
37
components/Profile/ProfileList/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ProfileItem } from './ProfileItem'
|
||||
|
||||
export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className='profile-list'>
|
||||
<ProfileItem
|
||||
title={t('home:about.birthDate')}
|
||||
value='31/03/2003'
|
||||
/>
|
||||
<ProfileItem
|
||||
title={t('home:about.nationality')}
|
||||
value='Alsace, France'
|
||||
/>
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
value='contact@divlo.fr'
|
||||
link='mailto:contact@divlo.fr'
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
26
components/Profile/ProfileLogo.tsx
Normal file
26
components/Profile/ProfileLogo.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='col-sm-24 col-md-10'>
|
||||
<div className='profile-logo'>
|
||||
<Image
|
||||
width={800}
|
||||
height={800}
|
||||
src='/images/divlo_logo.png'
|
||||
alt='Divlo'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.profile-logo {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
50
components/Profile/SocialMediaList/SocialMediaItem.tsx
Normal file
50
components/Profile/SocialMediaList/SocialMediaItem.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Tooltip } from 'components/design/Tooltip'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface SocialMediaItemProps {
|
||||
link: string
|
||||
socialMedia: 'Email' | 'GitHub' | 'Twitch' | 'Twitter' | 'YouTube'
|
||||
}
|
||||
|
||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = props => {
|
||||
const { link, socialMedia } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className='social-media-list__item'>
|
||||
<a
|
||||
href={link}
|
||||
aria-label={socialMedia}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='social-media-list__link'
|
||||
>
|
||||
<Tooltip title={socialMedia}>
|
||||
<Image
|
||||
width={45}
|
||||
height={45}
|
||||
alt={socialMedia}
|
||||
src={`/images/web/${socialMedia}.png`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.social-media-list__item {
|
||||
display: inline-block;
|
||||
margin: 5px 15px;
|
||||
}
|
||||
.social-media-list__link {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Profile/SocialMediaList/index.tsx
Normal file
41
components/Profile/SocialMediaList/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { SocialMediaItem } from './SocialMediaItem'
|
||||
|
||||
export const SocialMediaList: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='row justify-content-center'>
|
||||
<ul className='social-media-list'>
|
||||
<SocialMediaItem
|
||||
socialMedia='Twitter'
|
||||
link='https://twitter.com/Divlo_FR'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='GitHub'
|
||||
link='https://github.com/Divlo'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='YouTube'
|
||||
link='https://www.youtube.com/c/Divlo'
|
||||
/>
|
||||
<SocialMediaItem
|
||||
socialMedia='Twitch'
|
||||
link='https://www.twitch.tv/divlo'
|
||||
/>
|
||||
<SocialMediaItem socialMedia='Email' link='mailto:contact@divlo.fr' />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.social-media-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
33
components/Profile/index.tsx
Normal file
33
components/Profile/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
||||
import { ProfileInfo } from './ProfileInfo'
|
||||
import { ProfileList } from './ProfileList'
|
||||
import { ProfileLogo } from './ProfileLogo'
|
||||
|
||||
export const Profile: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='row profile'>
|
||||
<ProfileLogo />
|
||||
<div className='col-sm-24 col-md-14'>
|
||||
<ProfileInfo />
|
||||
<ProfileList />
|
||||
<ProfileDescriptionBottom />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.profile {
|
||||
padding: 40px 50px 15px 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.profile {
|
||||
padding: 40px 10px 0 10px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
50
components/Setup/Table.tsx
Normal file
50
components/Setup/Table.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
export interface TableRow {
|
||||
title: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface TableProps {
|
||||
rows: TableRow[]
|
||||
}
|
||||
|
||||
export const Table: React.FC<TableProps> = props => {
|
||||
const { rows } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24 table-column text-center'>
|
||||
<table>
|
||||
<tbody>
|
||||
{rows.map((row, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<th className='table-row'>{row.title}</th>
|
||||
<td className='table-row'>{row.value}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.table-column {
|
||||
display: grid;
|
||||
}
|
||||
.table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--color-text-1);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.table-row {
|
||||
padding: 15px;
|
||||
}
|
||||
.image-setup {
|
||||
width: 85%;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
21
components/Setup/TableTitle.tsx
Normal file
21
components/Setup/TableTitle.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export const TableTitle: React.FC = props => {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-24'>
|
||||
<p className='text-center title-table'>
|
||||
<strong className='important'>{children}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.title-table {
|
||||
font-size: 24px;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
137
components/Setup/index.tsx
Normal file
137
components/Setup/index.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Table, TableRow } from './Table'
|
||||
import { TableTitle } from './TableTitle'
|
||||
|
||||
export const Setup: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const rowsConfigPC: TableRow[] = [
|
||||
{
|
||||
title: t('setup:configPC.motherboard'),
|
||||
value: 'MSI Z87-G45 GAMING'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.processor'),
|
||||
value: 'Intel Core i5-4690k'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.graphicCard'),
|
||||
value: 'Zotac GeForce GTX 970'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.ramMemory'),
|
||||
value: '16 GB (2 x 8Go) Kingston HyperX'
|
||||
},
|
||||
{
|
||||
title: t('setup:configPC.hardDrive'),
|
||||
value: '256 GB SSD Crucial & 2 TB Seagate'
|
||||
}
|
||||
]
|
||||
|
||||
const rowsPeripherals: TableRow[] = [
|
||||
{
|
||||
title: t('setup:peripheral.keyboard'),
|
||||
value: 'Corsair K95 RGB'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.mouse'),
|
||||
value: 'SteelSeries Rival 310'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.headset'),
|
||||
value: 'SteelSeries ARCTIS PRO + GAMEDAC'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.mainScreen'),
|
||||
value: 'IIyama PL2480H'
|
||||
},
|
||||
{
|
||||
title: t('setup:peripheral.secondScreen'),
|
||||
value: 'Samsung SyncMaster 2220LM'
|
||||
}
|
||||
]
|
||||
|
||||
const rowsOffice: TableRow[] = [
|
||||
{
|
||||
title: t('setup:officeOther.mousepad'),
|
||||
value: 'SteelSeries QCK Heavy (Grand) as string'
|
||||
},
|
||||
{
|
||||
title: 'Mouse Bungee',
|
||||
value: 'BenQ ZOWIE Camade'
|
||||
},
|
||||
{
|
||||
title: t('setup:officeOther.usb'),
|
||||
value: 'Kingston 128GB'
|
||||
},
|
||||
{
|
||||
title: 'Smartphone',
|
||||
value: 'Samsung Galaxy A5 (2017)'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableTitle>{t('setup:configPC.title')}</TableTitle>
|
||||
<Table rows={rowsConfigPC} />
|
||||
|
||||
<TableTitle>{t('setup:peripheral.title')}</TableTitle>
|
||||
<Table rows={rowsPeripherals} />
|
||||
|
||||
<TableTitle>{t('setup:officeOther.title')}</TableTitle>
|
||||
<Table rows={rowsOffice} />
|
||||
|
||||
<div
|
||||
className='row row-padding justify-content-center'
|
||||
style={{ marginTop: 50 }}
|
||||
>
|
||||
<Image
|
||||
src='/images/setup/setup2019.png'
|
||||
alt='Setup Divlo'
|
||||
width={856.8}
|
||||
height={672.58}
|
||||
className='Setup__image'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row row-padding justify-content-center'>
|
||||
<Image
|
||||
src='/images/setup/setup2019-lights.jpg'
|
||||
alt='Setup Divlo'
|
||||
width={856.8}
|
||||
height={672.58}
|
||||
className='Setup__image'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row row-padding'>
|
||||
<TableTitle>{t('setup:connexion')}</TableTitle>
|
||||
<div style={{ marginBottom: 25 }} className='col-24 text-center'>
|
||||
<a
|
||||
href='https://www.speedtest.net/result/8533865940'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
aria-label='Speedtest link'
|
||||
>
|
||||
<Image
|
||||
src='/images/setup/speedtest-result.png'
|
||||
alt='Speedtest Result'
|
||||
width={308}
|
||||
height={165}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
.Setup__image {
|
||||
width: 85% !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
44
components/Skills/Skill.tsx
Normal file
44
components/Skills/Skill.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { skills } from './skills'
|
||||
|
||||
export interface SkillProps {
|
||||
skill: keyof typeof skills
|
||||
}
|
||||
|
||||
export const Skill: React.FC<SkillProps> = props => {
|
||||
const { skill } = props
|
||||
const skillProperties = skills[skill]
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={skillProperties.link}
|
||||
className='skills-link'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='skills-content text-center'>
|
||||
<Image
|
||||
width={60}
|
||||
height={60}
|
||||
alt={skill}
|
||||
src={skillProperties.image}
|
||||
/>
|
||||
<p className='skills-text'>{skill}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style jsx>{`
|
||||
.skills-link {
|
||||
max-width: 120px;
|
||||
margin: 0px 10px 0 10px;
|
||||
}
|
||||
.skills-text {
|
||||
margin-top: 5px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
44
components/Skills/SkillsSection.tsx
Normal file
44
components/Skills/SkillsSection.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
|
||||
export interface SkillsSectionProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const SkillsSection: React.FC<SkillsSectionProps> = props => {
|
||||
const { title, children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShadowContainer>
|
||||
<div className='container-fluid'>
|
||||
<div className='row row-padding'>
|
||||
<div className='col-24'>
|
||||
<div className='skills-header'>
|
||||
<h3 className='important'>{title}</h3>
|
||||
</div>
|
||||
<div className='skills-body'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShadowContainer>
|
||||
|
||||
<style jsx>{`
|
||||
.skills-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.skills-header > h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.skills-body {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-flow: row wrap;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Skills/index.tsx
Normal file
41
components/Skills/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Skill } from './Skill'
|
||||
import { SkillsSection } from './SkillsSection'
|
||||
|
||||
export const Skills: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkillsSection title={t('home:skills.languages')}>
|
||||
<Skill skill='JavaScript' />
|
||||
<Skill skill='TypeScript' />
|
||||
<Skill skill='Python' />
|
||||
<Skill skill='Dart' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Front-end'>
|
||||
<Skill skill='HTML' />
|
||||
<Skill skill='CSS' />
|
||||
<Skill skill='SASS' />
|
||||
<Skill skill='React.js (+ Next.js)' />
|
||||
<Skill skill='Flutter' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title='Back-end'>
|
||||
<Skill skill='Node.js' />
|
||||
<Skill skill='Strapi' />
|
||||
<Skill skill='MySQL' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title={t('home:skills.softwareTools')}>
|
||||
<Skill skill='Ubuntu' />
|
||||
<Skill skill='Hyper' />
|
||||
<Skill skill='Visual Studio Code' />
|
||||
<Skill skill='Git' />
|
||||
<Skill skill='Docker' />
|
||||
</SkillsSection>
|
||||
</>
|
||||
)
|
||||
}
|
70
components/Skills/skills.ts
Normal file
70
components/Skills/skills.ts
Normal file
@ -0,0 +1,70 @@
|
||||
export const skills = {
|
||||
JavaScript: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
||||
image: '/images/skills/JavaScript.png'
|
||||
},
|
||||
TypeScript: {
|
||||
link: 'https://www.typescriptlang.org/',
|
||||
image: '/images/skills/TypeScript.png'
|
||||
},
|
||||
Python: {
|
||||
link: 'https://www.python.org/',
|
||||
image: '/images/skills/Python.png'
|
||||
},
|
||||
Dart: {
|
||||
link: 'https://dart.dev/',
|
||||
image: '/images/skills/Dart.png'
|
||||
},
|
||||
Flutter: {
|
||||
link: 'https://flutter.dev/',
|
||||
image: '/images/skills/Flutter.webp'
|
||||
},
|
||||
HTML: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/HTML',
|
||||
image: '/images/skills/HTML.png'
|
||||
},
|
||||
CSS: {
|
||||
link: 'https://developer.mozilla.org/docs/Web/CSS',
|
||||
image: '/images/skills/CSS.png'
|
||||
},
|
||||
SASS: {
|
||||
link: 'https://sass-lang.com/',
|
||||
image: '/images/skills/SASS.svg'
|
||||
},
|
||||
'React.js (+ Next.js)': {
|
||||
link: 'https://reactjs.org/',
|
||||
image: '/images/skills/ReactJS.png'
|
||||
},
|
||||
'Node.js': {
|
||||
link: 'https://nodejs.org/',
|
||||
image: '/images/skills/NodeJS.png'
|
||||
},
|
||||
MySQL: {
|
||||
link: 'https://www.mysql.com/',
|
||||
image: '/images/skills/MySQL.png'
|
||||
},
|
||||
Strapi: {
|
||||
link: 'https://strapi.io/',
|
||||
image: '/images/skills/Strapi.png'
|
||||
},
|
||||
'Visual Studio Code': {
|
||||
link: 'https://code.visualstudio.com/',
|
||||
image: '/images/skills/Visual_Studio_Code.png'
|
||||
},
|
||||
Git: {
|
||||
link: 'https://git-scm.com/',
|
||||
image: '/images/skills/Git.png'
|
||||
},
|
||||
Hyper: {
|
||||
link: 'https://hyper.is/',
|
||||
image: '/images/skills/Hyper.svg'
|
||||
},
|
||||
Ubuntu: {
|
||||
link: 'https://ubuntu.com/',
|
||||
image: '/images/skills/Ubuntu.png'
|
||||
},
|
||||
Docker: {
|
||||
link: 'https://www.docker.com/',
|
||||
image: '/images/skills/Docker.png'
|
||||
}
|
||||
} as const
|
43
components/design/Button.tsx
Normal file
43
components/design/Button.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type ButtonProps = React.ComponentPropsWithRef<'button'>
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={ref} {...rest} className='btn btn-dark'>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn-dark {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40;
|
||||
}
|
||||
.btn-dark:hover {
|
||||
color: #fff;
|
||||
background-color: #23272b;
|
||||
border-color: #1d2124;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
75
components/design/Input.tsx
Normal file
75
components/design/Input.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface InputProps extends React.HTMLProps<HTMLInputElement> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const { label, name, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-group-animation'>
|
||||
<input ref={ref} {...rest} id={name} name={name} />
|
||||
<label htmlFor={name} className='label'>
|
||||
<span className='label-content'>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-group-animation {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.form-group-animation input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 35px;
|
||||
color: var(--color-text-1);
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
.form-group-animation label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
.form-group-animation label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -1px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-bottom: 3px solid var(--color-primary);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.label-content {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.form-group-animation input:focus + .label .label-content,
|
||||
.form-group-animation input:valid + .label .label-content {
|
||||
transform: translateY(-150%);
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.form-group-animation input:focus + .label::after,
|
||||
.form-group-animation input:valid + .label::after {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
})
|
49
components/design/RevealFade.tsx
Normal file
49
components/design/RevealFade.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const RevealFade: React.FC = props => {
|
||||
const { children } = props
|
||||
|
||||
const htmlElement = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('reveal-visible')
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0.28
|
||||
}
|
||||
)
|
||||
observer.observe(htmlElement.current as HTMLDivElement)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={htmlElement} className='reveal'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
.reveal-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transition: all 500ms ease-out 100ms;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
28
components/design/Section/SectionHeading.tsx
Normal file
28
components/design/Section/SectionHeading.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
||||
|
||||
export const SectionHeading = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
SectionHeadingProps
|
||||
>((props, ref) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 ref={ref} {...rest} className='Section__title'>
|
||||
{children}
|
||||
</h2>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.Section__title {
|
||||
font-size: 34px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
})
|
62
components/design/Section/index.tsx
Normal file
62
components/design/Section/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { ShadowContainer } from '../ShadowContainer'
|
||||
import { SectionHeading } from './SectionHeading'
|
||||
|
||||
type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
||||
heading?: string
|
||||
description?: string
|
||||
isMain?: boolean
|
||||
withoutShadowContainer?: boolean
|
||||
}
|
||||
|
||||
export const Section = forwardRef<HTMLElement, SectionProps>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
heading,
|
||||
description,
|
||||
isMain = false,
|
||||
withoutShadowContainer = false,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (isMain) {
|
||||
return (
|
||||
<ShadowContainer style={{ marginTop: 50 }}>
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='container-fluid'>{children}</div>
|
||||
</section>
|
||||
</ShadowContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (withoutShadowContainer) {
|
||||
return (
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && <SectionHeading>{heading}</SectionHeading>}
|
||||
<div className='container-fluid'>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={ref} {...rest}>
|
||||
{heading != null && (
|
||||
<SectionHeading style={{ ...(description != null && { margin: 0 }) }}>
|
||||
{heading}
|
||||
</SectionHeading>
|
||||
)}
|
||||
{description != null && (
|
||||
<p style={{ marginTop: 7 }} className='text-center'>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<ShadowContainer>
|
||||
<div className='container-fluid'>
|
||||
<div className='row row-padding'>{children}</div>
|
||||
</div>
|
||||
</ShadowContainer>
|
||||
</section>
|
||||
)
|
||||
})
|
32
components/design/ShadowContainer.tsx
Normal file
32
components/design/ShadowContainer.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||
|
||||
export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`shadow-container ${className != null ? className : ''}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.shadow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
39
components/design/Textarea.tsx
Normal file
39
components/design/Textarea.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface TextareaProps extends React.HTMLProps<HTMLTextAreaElement> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
(props, ref) => {
|
||||
const { label, name, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-group'>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<br />
|
||||
<textarea id={name} name={name} ref={ref} {...rest} />
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.form-group {
|
||||
padding-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-group textarea {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
49
components/design/Tooltip.tsx
Normal file
49
components/design/Tooltip.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
interface TooltipProps extends React.ComponentPropsWithRef<'div'> {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = props => {
|
||||
const { title, children, ...rest } = props
|
||||
return (
|
||||
<>
|
||||
<span className='tooltip' {...rest}>
|
||||
{children}
|
||||
<span className='title'>{title}</span>
|
||||
</span>
|
||||
|
||||
<style jsx>{`
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
background-color: #222222;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease-in;
|
||||
transform: translate3d(0, -15px, 0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.tooltip ~ .tooltip:hover .title,
|
||||
.tooltip:first-child:hover .title {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: all 0.35s ease-out;
|
||||
transform: translate3d(0, 0, 0);
|
||||
margin: 0;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user