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

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

This commit is contained in:
Divlo
2021-05-08 19:52:04 +02:00
committed by GitHub
parent 26f24329c7
commit c62e66a86a
83 changed files with 5803 additions and 7623 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -4,25 +4,11 @@ export const Footer: React.FC = () => {
const { t } = useTranslation()
return (
<>
<footer className='Footer text-center'>
<p>
<span className='important'>Divlo</span> | {t('common:allRightsReserved')}
</p>
</footer>
<style jsx>
{`
.Footer {
border-top: var(--border-header-footer);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px;
}
`}
</style>
</>
<footer className='bg-white flex justify-center py-6 text-lg border-t-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
<p>
<span className='text-yellow dark:text-yellow-dark'>Divlo</span> |{' '}
{t('common:allRightsReserved')}
</p>
</footer>
)
}

View File

@ -7,12 +7,12 @@ interface HeadProps {
url?: string
}
export const Head: React.FC<HeadProps> = props => {
export 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/'
url = 'https://divlo.fr/'
} = props
return (
@ -21,7 +21,7 @@ export const Head: React.FC<HeadProps> = props => {
<link rel='icon' type='image/png' href={image} />
{/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content={description} />
<meta name='Language' content='fr, en' />
<meta name='theme-color' content='#ffd800' />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +1,20 @@
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 => {
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>
</>
<li className='interest-item my-2 mx-2 w-8 h-8' title={title}>
<FontAwesomeIcon
className='text-yellow cursor-pointer h-full w-full block dark:text-yellow-dark'
icon={fontAwesomeIcon}
/>
</li>
)
}

View File

@ -5,41 +5,18 @@ 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>
</>
<div className='flex justify-center my-4'>
<ul className='flex justify-around p-0 m-0 list-none w-96'>
<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>
)
}

View File

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

View File

@ -1,3 +1,4 @@
import { ShadowContainer } from 'components/design/ShadowContainer'
import Image from 'next/image'
export interface PortfolioItemProps {
@ -7,96 +8,34 @@ export interface PortfolioItemProps {
image: string
}
export const PortfolioItem: React.FC<PortfolioItemProps> = props => {
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>
</>
<ShadowContainer className='cursor-pointer relative items-center sm:ml-10'>
<a
className='group inline-flex justify-center'
target='_blank'
rel='noopener noreferrer'
href={link}
aria-label={title}
>
<div className='flex justify-center'>
<Image
className='transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
width={300}
height={300}
src={image}
alt={title}
/>
</div>
<div className='opacity-0 transition-opacity duration-500 h-auto absolute text-center overflow-hidden bottom-0 group-hover:opacity-100'>
<h3 className='text-yellow text-xl font-semibold my-6 dark:text-yellow-dark'>
{title}
</h3>
<p className='my-6'>{description}</p>
</div>
</a>
</ShadowContainer>
)
}

View File

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

View File

@ -2,27 +2,11 @@ import Translation from 'next-translate/Trans'
export const ProfileDescriptionBottom: React.FC = () => {
return (
<>
<p className='profile-description-bottom'>
<Translation
i18nKey='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>
</>
<p className='block mt-8 mb-0 font-normal text-base text-gray dark:text-gray-dark'>
<Translation
i18nKey='home:about.descriptionBottom'
components={[<br key='break' />]}
/>
</p>
)
}

View File

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

View File

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

View File

@ -6,32 +6,14 @@ 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>
</>
<ul className='m-0 p-0 list-none'>
<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>
)
}

View File

@ -2,25 +2,13 @@ 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>
</>
<div className='px-2 py-6'>
<Image
width={370}
height={370}
src='/images/divlo_logo.png'
alt='Divlo'
/>
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,50 +1,22 @@
import { Tooltip } from 'components/design/Tooltip'
import Image from 'next/image'
interface SocialMediaItemProps {
link: string
socialMedia: 'Email' | 'GitHub' | 'Twitch' | 'Twitter' | 'YouTube'
ariaLabel: string
}
export const SocialMediaItem: React.FC<SocialMediaItemProps> = props => {
const { link, socialMedia } = props
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
const { link, ariaLabel, children } = 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>
</>
<li className='inline-block mx-4 my-1'>
<a
href={link}
aria-label={ariaLabel}
target='_blank'
rel='noopener noreferrer'
className='relative inline-block bg-transparent'
>
{children}
</a>
</li>
)
}

View File

@ -1,41 +1,31 @@
import { SocialMediaItem } from './SocialMediaItem'
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon'
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon'
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon'
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
export const SocialMediaList: React.FC = () => {
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>
</>
<ul className='social-media-list m-0 p-0 list-none text-center mt-2 px-0 py-4'>
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
<TwitterIcon />
</SocialMediaItem>
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
<GitHubIcon />
</SocialMediaItem>
<SocialMediaItem
link='https://www.youtube.com/c/Divlo'
ariaLabel='YouTube'
>
<YouTubeIcon />
</SocialMediaItem>
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
<TwitchIcon />
</SocialMediaItem>
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
<EmailIcon />
</SocialMediaItem>
</ul>
)
}

View File

@ -5,29 +5,13 @@ 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 className='flex flex-col justify-center items-center px-10 pt-2 pb-6 md:pt-10 xl:pt-0 md:flex-row'>
<ProfileLogo />
<div className='col-sm-24 col-md-14'>
<ProfileInfo />
<ProfileList />
<ProfileDescriptionBottom />
</div>
<style jsx>
{`
.profile {
padding: 40px 50px 15px 50px;
}
@media (max-width: 576px) {
.profile {
padding: 40px 10px 0 10px;
}
}
`}
</style>
</>
</div>
)
}

View File

@ -6,39 +6,21 @@ export interface SkillProps {
skill: keyof typeof skills
}
export const Skill: React.FC<SkillProps> = props => {
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>
</>
<a
href={skillProperties.link}
className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
target='_blank'
rel='noopener noreferrer'
>
<div className='text-center'>
<Image width={60} height={60} alt={skill} src={skillProperties.image} />
<p className='mt-1'>{skill}</p>
</div>
</a>
)
}

View File

@ -5,40 +5,23 @@ export interface SkillsSectionProps {
children: React.ReactNode
}
export const SkillsSection: React.FC<SkillsSectionProps> = props => {
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>
<ShadowContainer>
<div className='w-full px-4 mx-auto'>
<div className='flex flex-wrap px-4 py-6'>
<div className='flex-1'>
<div className='mb-8 border-b border-gray-600 dark:border-opacity-10 dark:border-white'>
<h3 className='text-yellow font-semibold text-xl my-3 dark:text-yellow-dark'>
{title}
</h3>
</div>
<div className='flex justify-around flex-wrap'>{children}</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>
</>
</div>
</ShadowContainer>
)
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
export const RevealFade: React.FC = props => {
export const RevealFade: React.FC = (props) => {
const { children } = props
const htmlElement = useRef<HTMLDivElement>(null)
@ -8,7 +8,7 @@ export const RevealFade: React.FC = props => {
useEffect(() => {
const observer = new window.IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('reveal-visible')
observer.unobserve(entry.target)
@ -30,19 +30,20 @@ export const RevealFade: React.FC = props => {
{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 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

@ -3,26 +3,18 @@ import { forwardRef } from 'react'
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
export const SectionHeading = forwardRef<
HTMLHeadingElement,
SectionHeadingProps
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>
</>
<h2
ref={ref}
{...rest}
className='text-4xl font-semibold text-center mt-1 mb-7'
>
{children}
</h2>
)
})

View File

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

View File

@ -1,12 +1,17 @@
import classNames from 'classnames'
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
const { children, className, ...rest } = props
return (
<>
<div
className={`shadow-container ${className != null ? className : ''}`}
className={classNames(
'shadow-container flex flex-col h-full max-w-full break-words',
className
)}
{...rest}
>
{children}
@ -15,14 +20,9 @@ export const ShadowContainer: React.FC<ShadowContainerProps> = props => {
<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;
}
`}

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
import { render } from '@testing-library/react'
import { Input } from '../Input'
describe('<Input />', () => {
it('should render the label', async () => {
const labelContent = 'label content'
const { getByText } = render(<Input label={labelContent} />)
expect(getByText(labelContent)).toBeInTheDocument()
})
})