Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
83231197dd | |||
a2fe2205bc | |||
e1f3dceb07 | |||
0f89fee52f | |||
2fcc7ac384 | |||
9351edf626 | |||
1f4aa54211 | |||
8bc1471cbb | |||
1ebdab18a5 | |||
b9b76e839a | |||
bc065a2e19 | |||
5d3a287b27 | |||
fb689c9bc1 | |||
2c3a70df2a | |||
bce254a355 | |||
f67d331416 | |||
6abc881e94 | |||
a67d6665ea |
@ -1,5 +1,3 @@
|
|||||||
version: '3.0'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
workspace:
|
workspace:
|
||||||
build:
|
build:
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
.next
|
|
||||||
.lighthouseci
|
|
||||||
storybook-static
|
|
||||||
coverage
|
|
||||||
node_modules
|
|
||||||
next-env.d.ts
|
|
||||||
**/workbox-*.js
|
|
||||||
**/sw.js
|
|
@ -6,12 +6,10 @@
|
|||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"node": true,
|
||||||
"browser": true,
|
"browser": true
|
||||||
"jest": true
|
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"unicorn/prefer-node-protocol": "off",
|
"unicorn/prefer-node-protocol": "error"
|
||||||
"@typescript-eslint/no-misused-promises": "off"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.gitignore
vendored
@ -11,7 +11,7 @@ out
|
|||||||
# production
|
# production
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
public/*.html
|
public/curriculum-vitae
|
||||||
# PWA
|
# PWA
|
||||||
public/workbox-*.js
|
public/workbox-*.js
|
||||||
public/sw.js
|
public/sw.js
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
"http://localhost:3000/blog",
|
"http://localhost:3000/blog",
|
||||||
"http://localhost:3000/blog/hello-world"
|
"http://localhost:3000/blog/hello-world"
|
||||||
],
|
],
|
||||||
"files": ["./public/curriculum-vitae.html"]
|
"files": ["./public/curriculum-vitae/index.html"]
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
{
|
{
|
||||||
"*": ["editorconfig-checker"],
|
"*": ["editorconfig-checker"],
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix",
|
|
||||||
"jest --findRelatedTests"
|
|
||||||
],
|
|
||||||
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
"*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
|
||||||
"*.{md,mdx}": ["prettier --write", "markdownlint --dot --fix"],
|
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
|
||||||
"resume.json": ["resume validate"]
|
|
||||||
}
|
}
|
||||||
|
11
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
"MD024": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD041": false
|
||||||
|
},
|
||||||
|
"globs": ["**/*.{md,mdx}"],
|
||||||
|
"ignores": ["**/node_modules"]
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"default": true,
|
|
||||||
"MD013": false,
|
|
||||||
"MD024": false,
|
|
||||||
"MD033": false,
|
|
||||||
"MD041": false
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
.next
|
|
||||||
.lighthouseci
|
|
||||||
storybook-static
|
|
||||||
coverage
|
|
||||||
node_modules
|
|
||||||
next-env.d.ts
|
|
||||||
**/workbox-*.js
|
|
||||||
**/sw.js
|
|
||||||
*.hbs
|
|
@ -81,7 +81,7 @@ npm run dev
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Setup and run all the services for you
|
# Setup and run all the services for you
|
||||||
docker-compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Services started
|
### Services started
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
FROM node:16.14.0 AS dependencies
|
FROM node:16.16.0 AS dependencies
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
FROM node:16.14.0 AS builder
|
FROM node:16.16.0 AS builder
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:16.14.0 AS runner
|
FROM node:16.16.0 AS runner
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||||
|
@ -27,6 +27,7 @@ export const Footer: React.FC<FooterProps> = (props) => {
|
|||||||
<p className='mt-1'>
|
<p className='mt-1'>
|
||||||
Version{' '}
|
Version{' '}
|
||||||
<a
|
<a
|
||||||
|
data-cy='version-link'
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||||
href={versionLink}
|
href={versionLink}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
|
@ -3,7 +3,7 @@ import NextHead from 'next/head'
|
|||||||
interface HeadProps {
|
interface HeadProps {
|
||||||
title?: string
|
title?: string
|
||||||
image?: string
|
image?: string
|
||||||
description: string
|
description?: string
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export const Head: React.FC<HeadProps> = (props) => {
|
|||||||
const {
|
const {
|
||||||
title = 'Divlo',
|
title = 'Divlo',
|
||||||
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
image = 'https://divlo.fr/images/icons/icon-96x96.png',
|
||||||
description,
|
description = 'Divlo - Developer Full Stack Junior • Passionate about High-Tech',
|
||||||
url = 'https://divlo.fr/'
|
url = 'https://divlo.fr/'
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import setLanguage from 'next-translate/setLanguage'
|
import setLanguage from 'next-translate/setLanguage'
|
||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
import i18n from 'i18n.json'
|
import i18n from 'i18n.json'
|
||||||
|
|
||||||
@ -11,31 +11,37 @@ import { LanguageFlag } from './LanguageFlag'
|
|||||||
export const Language: React.FC = () => {
|
export const Language: React.FC = () => {
|
||||||
const { lang: currentLanguage } = useTranslation()
|
const { lang: currentLanguage } = useTranslation()
|
||||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||||
|
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const handleHiddenMenu = useCallback(() => {
|
const handleHiddenMenu = useCallback(() => {
|
||||||
setHiddenMenu(!hiddenMenu)
|
setHiddenMenu((oldHiddenMenu) => !oldHiddenMenu)
|
||||||
}, [hiddenMenu])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hiddenMenu) {
|
const handleClickEvent = (event: MouseEvent): void => {
|
||||||
window.document.addEventListener('click', handleHiddenMenu)
|
if (languageClickRef.current == null || event.target == null) {
|
||||||
} else {
|
return
|
||||||
window.document.removeEventListener('click', handleHiddenMenu)
|
}
|
||||||
|
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||||
|
setHiddenMenu(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.document.addEventListener('click', handleClickEvent)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.document.removeEventListener('click', handleHiddenMenu)
|
return window.removeEventListener('click', handleClickEvent)
|
||||||
}
|
}
|
||||||
}, [hiddenMenu, handleHiddenMenu])
|
}, [])
|
||||||
|
|
||||||
const handleLanguage = async (language: string): Promise<void> => {
|
const handleLanguage = async (language: string): Promise<void> => {
|
||||||
await setLanguage(language)
|
await setLanguage(language)
|
||||||
handleHiddenMenu()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||||
<div
|
<div
|
||||||
|
ref={languageClickRef}
|
||||||
data-cy='language-click'
|
data-cy='language-click'
|
||||||
className='mr-5 flex items-center'
|
className='mr-5 flex items-center'
|
||||||
onClick={handleHiddenMenu}
|
onClick={handleHiddenMenu}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Header } from '..'
|
|
||||||
|
|
||||||
describe('<Header />', () => {
|
|
||||||
it('should render', () => {
|
|
||||||
const { getByText } = render(<Header />)
|
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,14 +1,24 @@
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import { DIVLO_BIRTHDAY, DIVLO_BIRTHDAY_DATE, getAge } from 'utils/getAge'
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from './ProfileItem'
|
||||||
|
|
||||||
export const ProfileList: React.FC = () => {
|
export const ProfileList: React.FC = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
|
|
||||||
|
const age = useMemo(() => {
|
||||||
|
return getAge(DIVLO_BIRTHDAY)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className='m-0 list-none p-0'>
|
<ul className='m-0 list-none p-0'>
|
||||||
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
<ProfileItem title={t('home:about.full-name')} value='Théo LUDWIG' />
|
||||||
<ProfileItem title={t('home:about.birth-date')} value='31/03/2003' />
|
<ProfileItem
|
||||||
|
title={t('home:about.birth-date')}
|
||||||
|
value={`${DIVLO_BIRTHDAY_DATE} (${age} ${t('home:about.years-old')})`}
|
||||||
|
/>
|
||||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title='Email'
|
title='Email'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className, ...rest } = props
|
||||||
|
@ -3,7 +3,9 @@ interface SocialMediaItemProps {
|
|||||||
ariaLabel: string
|
ariaLabel: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
|
export const SocialMediaItem: React.FC<
|
||||||
|
React.PropsWithChildren<SocialMediaItemProps>
|
||||||
|
> = (props) => {
|
||||||
const { link, ariaLabel, children } = props
|
const { link, ariaLabel, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { ErrorPage } from '../ErrorPage'
|
|
||||||
|
|
||||||
describe('<ErrorPage />', () => {
|
|
||||||
it('should render the message and statusCode', () => {
|
|
||||||
const messageContent = 'message content'
|
|
||||||
const statusCode = 404
|
|
||||||
const { getByText } = render(
|
|
||||||
<ErrorPage statusCode={statusCode} message={messageContent} />
|
|
||||||
)
|
|
||||||
expect(getByText(messageContent)).toBeInTheDocument()
|
|
||||||
expect(getByText(statusCode)).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,16 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { Footer } from '../Footer'
|
|
||||||
|
|
||||||
describe('<Footer />', () => {
|
|
||||||
it('should render with appropriate link tag version', () => {
|
|
||||||
const version = '1.0.0'
|
|
||||||
const { getByText } = render(<Footer version={version} />)
|
|
||||||
const versionLink = getByText(version) as HTMLAnchorElement
|
|
||||||
expect(getByText('Divlo')).toBeInTheDocument()
|
|
||||||
expect(versionLink).toBeInTheDocument()
|
|
||||||
expect(versionLink.href).toEqual(
|
|
||||||
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export const RevealFade: React.FC = (props) => {
|
export const RevealFade: React.FC<React.PropsWithChildren<{}>> = (props) => {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
const htmlElement = useRef<HTMLDivElement>(null)
|
const htmlElement = useRef<HTMLDivElement>(null)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||||
|
|
||||||
|
20
cypress.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: false,
|
||||||
|
video: false,
|
||||||
|
downloadsFolder: undefined,
|
||||||
|
screenshotOnRunFailure: false,
|
||||||
|
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
supportFile: false
|
||||||
|
},
|
||||||
|
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: 'next',
|
||||||
|
bundler: 'webpack'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"supportFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"video": false,
|
|
||||||
"screenshotOnRunFailure": false
|
|
||||||
}
|
|
18
cypress/component/Footer.cy.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Footer } from 'components/Footer'
|
||||||
|
|
||||||
|
describe('<Footer />', () => {
|
||||||
|
it('should render with appropriate link tag version', () => {
|
||||||
|
const version = '1.0.0'
|
||||||
|
cy.mount(<Footer version={version} />)
|
||||||
|
cy.contains('Divlo')
|
||||||
|
.get('[data-cy=version-link]')
|
||||||
|
.should('have.text', version)
|
||||||
|
.should(
|
||||||
|
'have.attr',
|
||||||
|
'href',
|
||||||
|
`https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
17
cypress/component/utils/getAge.cy.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { getAge } from '../../../utils/getAge'
|
||||||
|
|
||||||
|
describe('utils/getAge', () => {
|
||||||
|
it('should calculate the right age of a person', () => {
|
||||||
|
cy.clock(new Date('2018-03-20')).then(() => {
|
||||||
|
const birthDate = new Date('1980-02-20')
|
||||||
|
expect(getAge(birthDate)).equal(38)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should calculate the right age of a person (taking into account the months)', () => {
|
||||||
|
cy.clock(new Date('2018-03-20')).then(() => {
|
||||||
|
const birthDate = new Date('1980-07-20')
|
||||||
|
expect(getAge(birthDate)).equal(37)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -56,3 +56,5 @@ describe('Common > Header', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -5,3 +5,5 @@ describe('Page /404', () => {
|
|||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -5,3 +5,5 @@ describe('Page /500', () => {
|
|||||||
cy.get('[data-cy=status-code]').contains('500')
|
cy.get('[data-cy=status-code]').contains('500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -11,3 +11,5 @@ describe('Page /blog/[slug]', () => {
|
|||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get('[data-cy=status-code]').contains('404')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -20,3 +20,5 @@ describe('Page /blog', () => {
|
|||||||
.should('eq', '/blog/hello-world')
|
.should('eq', '/blog/hello-world')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
@ -17,3 +17,5 @@ describe('Page /', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
3
cypress/support/commands.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export {}
|
14
cypress/support/component-index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>Components App</title>
|
||||||
|
<!-- Used by Next.js to inject CSS. -->
|
||||||
|
<div id="__next_css__DO_NOT_USE__"></div>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
cypress/support/component.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { mount } from 'cypress/react'
|
||||||
|
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
mount: typeof mount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('mount', mount)
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": true,
|
|
||||||
"types": ["cypress"],
|
|
||||||
"isolatedModules": false
|
|
||||||
},
|
|
||||||
"include": ["../node_modules/cypress", "./**/*.ts"]
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
version: '3.0'
|
|
||||||
services:
|
services:
|
||||||
divlo.fr:
|
divlo.fr:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
const nextJest = require('next/jest')
|
|
||||||
|
|
||||||
const createJestConfig = nextJest()
|
|
||||||
const customJestConfig = {
|
|
||||||
moduleDirectories: ['node_modules', './'],
|
|
||||||
modulePathIgnorePatterns: ['<rootDir>/cypress'],
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
setupFilesAfterEnv: [
|
|
||||||
'@testing-library/jest-dom/extend-expect',
|
|
||||||
'@testing-library/react'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = createJestConfig(customJestConfig)
|
|
22
jsonresume-theme-custom/.gitignore
vendored
@ -1,4 +1,22 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
theme/index.html
|
|
||||||
dist
|
dist
|
||||||
.parcel-cache
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |
@ -5,10 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title><%= locals.basics.name %></title>
|
<title><%= locals.basics.name %></title>
|
||||||
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
<link rel="icon" type="image/png" href="<%= locals.basics.image %>" />
|
||||||
|
<link rel="stylesheet" href="./styles/global.css" />
|
||||||
<style>
|
|
||||||
@import './styles/global.css';
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -59,7 +56,7 @@
|
|||||||
<div class="background-details">
|
<div class="background-details">
|
||||||
<div class="detail" id="about">
|
<div class="detail" id="about">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img src="data-url:./images/user.svg" alt="user" />
|
<img src="./images/user.svg" alt="user" />
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h4 class="title text-uppercase">À propos</h4>
|
<h4 class="title text-uppercase">À propos</h4>
|
||||||
@ -75,10 +72,7 @@
|
|||||||
|
|
||||||
<div class="detail" id="work-experience">
|
<div class="detail" id="work-experience">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img
|
<img src="./images/building-columns.svg" alt="work" />
|
||||||
src="data-url:./images/building-columns.svg"
|
|
||||||
alt="work"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h4 class="title text-uppercase">Expériences</h4>
|
<h4 class="title text-uppercase">Expériences</h4>
|
||||||
@ -117,7 +111,7 @@
|
|||||||
|
|
||||||
<div class="detail" id="skills">
|
<div class="detail" id="skills">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img src="data-url:./images/toolbox.svg" alt="toolbox" />
|
<img src="./images/toolbox.svg" alt="toolbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h4 class="title text-uppercase">Compétences</h4>
|
<h4 class="title text-uppercase">Compétences</h4>
|
||||||
@ -144,10 +138,7 @@
|
|||||||
|
|
||||||
<div class="detail" id="education">
|
<div class="detail" id="education">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img
|
<img src="./images/graduation-cap.svg" alt="graduation" />
|
||||||
src="data-url:./images/graduation-cap.svg"
|
|
||||||
alt="graduation"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h4 class="title text-uppercase">Éducation</h4>
|
<h4 class="title text-uppercase">Éducation</h4>
|
||||||
@ -167,7 +158,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="text-muted clear-margin">
|
<p class="text-muted clear-margin">
|
||||||
<small>
|
<small>
|
||||||
<%= degree.startDate %> - <%= degree.endDate %>
|
<%= degree.startDate %> <%= degree.endDate != null
|
||||||
|
? " - " + degree.endDate : "" %>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -182,7 +174,7 @@
|
|||||||
|
|
||||||
<div class="detail" id="interests">
|
<div class="detail" id="interests">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img src="data-url:./images/heart.svg" alt="heart" />
|
<img src="./images/heart.svg" alt="heart" />
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h4 class="title text-uppercase">Intérets</h4>
|
<h4 class="title text-uppercase">Intérets</h4>
|
@ -1,28 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
import ejs from 'ejs'
|
|
||||||
import date from 'date-and-time'
|
|
||||||
import { Parcel } from '@parcel/core'
|
|
||||||
|
|
||||||
export const render = async (resume) => {
|
|
||||||
const themeIndexURL = new URL('./theme/index.ejs', import.meta.url)
|
|
||||||
const themeBuildURL = new URL('./theme/index.html', import.meta.url)
|
|
||||||
const indexHTMLURL = new URL('./dist/index.html', import.meta.url)
|
|
||||||
const themeBuildPath = fileURLToPath(themeBuildURL)
|
|
||||||
const html = await ejs.renderFile(fileURLToPath(themeIndexURL), {
|
|
||||||
date,
|
|
||||||
locals: {
|
|
||||||
...resume
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await fs.promises.writeFile(themeBuildURL, html, { encoding: 'utf-8' })
|
|
||||||
const bundler = new Parcel({
|
|
||||||
entries: themeBuildPath,
|
|
||||||
source: themeBuildPath,
|
|
||||||
mode: 'production',
|
|
||||||
defaultConfig: '@parcel/config-default'
|
|
||||||
})
|
|
||||||
await bundler.run()
|
|
||||||
return await fs.promises.readFile(indexHTMLURL, { encoding: 'utf-8' })
|
|
||||||
}
|
|
5816
jsonresume-theme-custom/package-lock.json
generated
@ -3,17 +3,18 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-and-time": "2.3.0",
|
|
||||||
"ejs": "3.1.6",
|
|
||||||
"modern-normalize": "1.1.0"
|
"modern-normalize": "1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/config-default": "2.3.2",
|
"@types/node": "18.7.11",
|
||||||
"@parcel/core": "2.3.2",
|
"date-and-time": "2.4.1",
|
||||||
"@parcel/optimizer-data-url": "^2.3.2",
|
"vite": "3.0.9",
|
||||||
"@parcel/transformer-inline-string": "^2.3.2",
|
"vite-plugin-html": "3.2.0"
|
||||||
"parcel": "2.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import fs from 'fs'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { render } from '../index.js'
|
import { build } from 'vite'
|
||||||
|
|
||||||
const jsonResumeURL = new URL('../../resume.json', import.meta.url)
|
const jsonResumeThemeCustom = new URL('../', import.meta.url)
|
||||||
const publicResumeURL = new URL(
|
const jsonResumeThemeCustomDist = new URL('./dist', jsonResumeThemeCustom)
|
||||||
'../../public/curriculum-vitae.html',
|
const publicResumeOutputURL = new URL(
|
||||||
|
'../../public/curriculum-vitae',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
)
|
)
|
||||||
|
|
||||||
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
await build({
|
||||||
encoding: 'utf-8'
|
root: fileURLToPath(jsonResumeThemeCustom),
|
||||||
|
base: '/curriculum-vitae/'
|
||||||
})
|
})
|
||||||
const dataResumeJSON = JSON.parse(dataResumeStringJSON)
|
|
||||||
const dataResumeIndexHTML = await render(dataResumeJSON)
|
await fs.promises.cp(jsonResumeThemeCustomDist, publicResumeOutputURL, {
|
||||||
await fs.promises.writeFile(publicResumeURL, dataResumeIndexHTML, {
|
recursive: true
|
||||||
encoding: 'utf-8'
|
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import 'npm:modern-normalize/modern-normalize.css';
|
@import 'modern-normalize/modern-normalize.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
33
jsonresume-theme-custom/vite.config.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||||
|
import date from 'date-and-time'
|
||||||
|
|
||||||
|
const jsonResumeURL = new URL('../resume.json', import.meta.url)
|
||||||
|
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
|
||||||
|
encoding: 'utf-8'
|
||||||
|
})
|
||||||
|
const resume = JSON.parse(dataResumeStringJSON)
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
assetsDir: './'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
createHtmlPlugin({
|
||||||
|
inject: {
|
||||||
|
data: {
|
||||||
|
date,
|
||||||
|
locals: {
|
||||||
|
...resume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
postcss: {}
|
||||||
|
}
|
||||||
|
})
|
@ -4,8 +4,9 @@
|
|||||||
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
"description": "Developer Full Stack Junior • Passionate about High-Tech",
|
||||||
"full-name": "Full name",
|
"full-name": "Full name",
|
||||||
"birth-date": "Birth date",
|
"birth-date": "Birth date",
|
||||||
|
"years-old": "years old",
|
||||||
"nationality": "Nationality",
|
"nationality": "Nationality",
|
||||||
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (first year)."
|
"description-bottom": "I am self-taught in Computer Science by following online trainings and I am also a student at the university following the French training \"BUT Informatique\" (second year)."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interests",
|
"title": "Interests",
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
"description": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||||
"full-name": "Prénom NOM",
|
"full-name": "Prénom NOM",
|
||||||
"birth-date": "Date de naissance",
|
"birth-date": "Date de naissance",
|
||||||
|
"years-old": "ans",
|
||||||
"nationality": "Nationalité",
|
"nationality": "Nationalité",
|
||||||
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (première année)."
|
"description-bottom": "Je me forme en autodidacte dans l'informatique en suivant des formations en ligne et je suis aussi un étudiant à l'université suivant la formation \"BUT Informatique\" (deuxième année)."
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Intérêts",
|
"title": "Intérêts",
|
||||||
|
@ -1,41 +1,12 @@
|
|||||||
const nextPWA = require('next-pwa')
|
const nextPWA = require('next-pwa')({
|
||||||
|
disable: process.env.NODE_ENV !== 'production',
|
||||||
|
dest: 'public'
|
||||||
|
})
|
||||||
const nextTranslate = require('next-translate')
|
const nextTranslate = require('next-translate')
|
||||||
const { createSecureHeaders } = require('next-secure-headers')
|
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
module.exports = nextTranslate(
|
module.exports = nextTranslate(
|
||||||
nextPWA({
|
nextPWA({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true
|
||||||
pwa: {
|
|
||||||
disable: process.env.NODE_ENV !== 'production',
|
|
||||||
dest: 'public'
|
|
||||||
},
|
|
||||||
headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: createSecureHeaders({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
scriptSrc: [
|
|
||||||
"'self'",
|
|
||||||
'data:',
|
|
||||||
"'unsafe-eval'",
|
|
||||||
"'unsafe-inline'"
|
|
||||||
],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
imgSrc: ['*', 'data:', 'blob:'],
|
|
||||||
mediaSrc: "'none'",
|
|
||||||
connectSrc: '*',
|
|
||||||
objectSrc: "'none'",
|
|
||||||
fontSrc: "'self'",
|
|
||||||
baseURI: "'none'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
23112
package-lock.json
generated
108
package.json
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "divlo",
|
"name": "divlo",
|
||||||
"version": "2.1.0",
|
"version": "2.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Divlo/Divlo"
|
"url": "https://github.com/Divlo/Divlo"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0",
|
"node": ">=16.0.0",
|
||||||
"npm": ">=7.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@ -17,86 +17,82 @@
|
|||||||
"export": "next export",
|
"export": "next export",
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint \"**/*.{md,mdx}\" --dot --ignore-path \".gitignore\"",
|
"lint:markdown": "markdownlint-cli2",
|
||||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
|
||||||
"lint:prettier": "prettier \".\" --check",
|
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test:unit": "jest",
|
"test:unit": "cypress run --component",
|
||||||
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://localhost:3000\" \"html-w3c-validator\"",
|
||||||
"test:lighthouse": "lhci autorun",
|
"test:lighthouse": "lhci autorun",
|
||||||
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
||||||
"test:e2e:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
|
||||||
"resume:build": "node ./jsonresume-theme-custom/scripts/build.js",
|
"resume:build": "node ./jsonresume-theme-custom/scripts/build.js",
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
"deploy": "vercel",
|
"deploy": "vercel",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/montserrat": "4.5.5",
|
"@fontsource/montserrat": "4.5.12",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.0.0",
|
"@fortawesome/free-brands-svg-icons": "6.1.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.0.0",
|
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "0.1.17",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"classnames": "2.3.1",
|
"@giscus/react": "2.2.0",
|
||||||
"date-and-time": "2.3.0",
|
"clsx": "1.2.1",
|
||||||
|
"date-and-time": "2.4.1",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "3.0.4",
|
||||||
"next": "12.1.0",
|
"next": "12.2.5",
|
||||||
"next-mdx-remote": "4.0.0",
|
"next-mdx-remote": "4.1.0",
|
||||||
"next-pwa": "5.4.6",
|
"next-pwa": "5.6.0",
|
||||||
"next-themes": "0.1.1",
|
"next-themes": "0.2.0",
|
||||||
"next-translate": "1.3.5",
|
"next-translate": "1.5.0",
|
||||||
"react": "17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"read-pkg": "7.1.0",
|
"read-pkg": "7.1.0",
|
||||||
"rehype-raw": "6.1.1",
|
"rehype-raw": "6.1.1",
|
||||||
"rehype-slug": "5.0.1",
|
"rehype-slug": "5.0.1",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"sharp": "0.30.2",
|
"sharp": "0.30.7",
|
||||||
"shiki": "0.10.1",
|
"shiki": "0.11.1",
|
||||||
"unified": "10.1.2",
|
"unified": "10.1.2",
|
||||||
"unist-util-visit": "4.1.0",
|
"unist-util-visit": "4.1.1",
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "16.2.1",
|
"@commitlint/cli": "17.0.3",
|
||||||
"@commitlint/config-conventional": "16.2.1",
|
"@commitlint/config-conventional": "17.0.3",
|
||||||
"@lhci/cli": "0.9.0",
|
"@lhci/cli": "0.9.0",
|
||||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||||
"@semantic-release/git": "10.0.1",
|
"@semantic-release/git": "10.0.1",
|
||||||
"@tailwindcss/typography": "0.5.2",
|
"@tailwindcss/typography": "0.5.4",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@types/node": "18.7.11",
|
||||||
"@testing-library/react": "12.1.4",
|
"@types/react": "18.0.17",
|
||||||
"@types/jest": "27.4.1",
|
|
||||||
"@types/node": "17.0.21",
|
|
||||||
"@types/react": "17.0.40",
|
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.14.0",
|
"@typescript-eslint/eslint-plugin": "5.34.0",
|
||||||
"autoprefixer": "10.4.2",
|
"autoprefixer": "10.4.8",
|
||||||
"cypress": "9.5.1",
|
"cypress": "10.6.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"eslint": "8.11.0",
|
"eslint": "8.22.0",
|
||||||
"eslint-config-conventions": "1.1.0",
|
"eslint-config-conventions": "3.0.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "12.2.5",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.0.0",
|
||||||
"eslint-plugin-unicorn": "41.0.0",
|
"eslint-plugin-unicorn": "43.0.2",
|
||||||
"html-w3c-validator": "1.1.0",
|
"html-w3c-validator": "1.2.0",
|
||||||
"husky": "7.0.4",
|
"husky": "8.0.1",
|
||||||
"jest": "27.5.1",
|
|
||||||
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
"jsonresume-theme-custom": "file:./jsonresume-theme-custom",
|
||||||
"lint-staged": "12.3.5",
|
"lint-staged": "13.0.3",
|
||||||
"markdownlint-cli": "0.31.1",
|
"markdownlint-cli2": "0.5.1",
|
||||||
"next-secure-headers": "2.2.0",
|
"postcss": "8.4.16",
|
||||||
"postcss": "8.4.8",
|
"prettier": "2.7.1",
|
||||||
"prettier": "2.5.1",
|
"prettier-plugin-tailwindcss": "0.1.13",
|
||||||
"prettier-plugin-tailwindcss": "0.1.8",
|
"semantic-release": "19.0.4",
|
||||||
"semantic-release": "19.0.2",
|
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"tailwindcss": "3.0.23",
|
"tailwindcss": "3.1.8",
|
||||||
"typescript": "4.6.2",
|
"typescript": "4.7.4",
|
||||||
"vercel": "24.0.0"
|
"vercel": "28.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,16 @@ import { ErrorPage } from 'components/ErrorPage'
|
|||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
|
||||||
|
|
||||||
interface Error404Props extends FooterProps {
|
interface Error404Props extends FooterProps {}
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Error404: NextPage<Error404Props> = (props) => {
|
const Error404: NextPage<Error404Props> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { version, description } = props
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='404 | Divlo' description={description} />
|
<Head title='404 | Divlo' />
|
||||||
|
|
||||||
<Header showLanguage />
|
<Header showLanguage />
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
@ -28,11 +25,10 @@ const Error404: NextPage<Error404Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
export const getStaticProps: GetStaticProps<Error404Props> = async () => {
|
||||||
const { readPackage } = await import('read-pkg')
|
const { readPackage } = await import('read-pkg')
|
||||||
const { version } = await readPackage()
|
const { version } = await readPackage()
|
||||||
const description = getDefaultDescription()
|
return { props: { version } }
|
||||||
return { props: { version, description } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Error404
|
export default Error404
|
||||||
|
@ -5,19 +5,16 @@ import { ErrorPage } from 'components/ErrorPage'
|
|||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
|
||||||
|
|
||||||
interface Error500Props extends FooterProps {
|
interface Error500Props extends FooterProps {}
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Error500: NextPage<Error500Props> = (props) => {
|
const Error500: NextPage<Error500Props> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { version, description } = props
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title='500 | Divlo' description={description} />
|
<Head title='500 | Divlo' />
|
||||||
|
|
||||||
<Header showLanguage />
|
<Header showLanguage />
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
@ -28,11 +25,10 @@ const Error500: NextPage<Error500Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
export const getStaticProps: GetStaticProps<Error500Props> = async () => {
|
||||||
const { readPackage } = await import('read-pkg')
|
const { readPackage } = await import('read-pkg')
|
||||||
const { version } = await readPackage()
|
const { version } = await readPackage()
|
||||||
const description = getDefaultDescription()
|
return { props: { version } }
|
||||||
return { props: { version, description } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Error500
|
export default Error500
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
import { GetStaticProps, GetStaticPaths, NextPage } from 'next'
|
||||||
import { MDXRemote } from 'next-mdx-remote'
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
import Giscus from '@giscus/react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
@ -14,6 +16,8 @@ interface BlogPostPageProps extends FooterProps {
|
|||||||
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
||||||
const { version, post } = props
|
const { version, post } = props
|
||||||
|
|
||||||
|
const { theme = 'dark' } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head
|
<Head
|
||||||
@ -43,6 +47,20 @@ const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Giscus
|
||||||
|
id='comments'
|
||||||
|
repo='Divlo/Divlo'
|
||||||
|
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
||||||
|
category='General'
|
||||||
|
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
||||||
|
mapping='pathname'
|
||||||
|
reactionsEnabled='1'
|
||||||
|
emitMetadata='0'
|
||||||
|
inputPosition='top'
|
||||||
|
theme={theme}
|
||||||
|
lang='en'
|
||||||
|
loading='lazy'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer version={version} />
|
<Footer version={version} />
|
||||||
|
@ -12,19 +12,16 @@ import { Skills } from 'components/Skills'
|
|||||||
import { OpenSource } from 'components/OpenSource'
|
import { OpenSource } from 'components/OpenSource'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
import { getDefaultDescription } from 'utils/getDefaultDescription'
|
|
||||||
|
|
||||||
interface HomeProps extends FooterProps {
|
interface HomeProps extends FooterProps {}
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Home: NextPage<HomeProps> = (props) => {
|
const Home: NextPage<HomeProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { version, description } = props
|
const { version } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head description={description} />
|
<Head />
|
||||||
|
|
||||||
<Header showLanguage />
|
<Header showLanguage />
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
||||||
@ -74,11 +71,10 @@ const Home: NextPage<HomeProps> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<FooterProps> = async () => {
|
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
|
||||||
const { readPackage } = await import('read-pkg')
|
const { readPackage } = await import('read-pkg')
|
||||||
const { version } = await readPackage()
|
const { version } = await readPackage()
|
||||||
const description = getDefaultDescription()
|
return { props: { version } }
|
||||||
return { props: { version, description } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
124
posts/thream-v1-0-0.mdx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
title: '🟢 Thream v1.0.0'
|
||||||
|
description: 'Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.'
|
||||||
|
isPublished: true
|
||||||
|
publishedOn: '2022-04-11T10:24:55.206Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
Hello! 👋
|
||||||
|
|
||||||
|
After months of hard work, [Thream v1.0.0](https://www.thream.divlo.fr/) has been released! 🎉
|
||||||
|
|
||||||
|
[**Thream**](https://www.thream.divlo.fr/) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
|
||||||
|
|
||||||
|
## Presentation
|
||||||
|
|
||||||
|
[**Thream**](https://www.thream.divlo.fr/) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
|
||||||
|
|
||||||
|
The project is largely inspired by [Discord](https://discord.com), a proprietary instant messaging service, but differentiates itself by its **non-profit open source philosophy** and will integrate special features.
|
||||||
|
|
||||||
|
The source code is available on [GitHub](https://github.com/Thream).
|
||||||
|
|
||||||
|
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[**Thream**](https://www.thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://www.thream.divlo.fr/).
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/Divlo_FR/status/1260638175246135296) by the creator: Divlo.
|
||||||
|
|
||||||
|
The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features.
|
||||||
|
|
||||||
|
The development of the project begins under the name of **SocialProject**, on August 20, 2020, with colors close to the image of Divlo.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When I started the project, I had little knowledge of database design, real-time management or the architecture of such a large <abbr title="Information Technology">IT</abbr> project, so this will be accompanied by many technical problems, to which we will need to find appropriate solutions.
|
||||||
|
|
||||||
|
On October 19, 2020, **SocialProject** becomes **Thream**, an invented name, not yet used and more original than the previous one, and also changes colors so that the application is accessible in two distinct themes (light and dark).
|
||||||
|
|
||||||
|
With the help of [Walidoux](https://github.com/Walidoux), a junior developer really good at making beautiful <abbr title="User Interface">UI</abbr> with <abbr title="Cascading Style Sheets">CSS</abbr>, we were able to collaborate on this side project together.
|
||||||
|
|
||||||
|
Since the project is mainly developed during free time (mainly on weekends), the project took longer to be developed than desired, but now we finally released the first version. 🥳
|
||||||
|
|
||||||
|
## Implementation and Technical Difficulties
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
**Thream** is divided into two distinct projects:
|
||||||
|
|
||||||
|
- The **server** part, called **backend**, which the user does not see, allows actions to be taken to save or recover data in the **database**, it is the technical and functional aspect of the project. This part uses a style of software architecture defining a set of constraints to be used to create web services that establish interoperability between computers on the Internet, called REST <abbr title="Application Programming Interface">API</abbr>.
|
||||||
|
|
||||||
|
- The **client** part, called **frontend**, what **the user sees on the screen**, such as forms, buttons and all the **graphic elements** with which the user can interact from a browser.
|
||||||
|
|
||||||
|
<p className='flex flex-col items-center justify-center'>
|
||||||
|
<img
|
||||||
|
alt='HTTP Communication Schema'
|
||||||
|
src='/images/posts/thream-v1-0-0/http-communication.png'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
This design allows the separation between the client and the server, as long as they both structure their communication according to the <abbr title="Representational state transfer">REST</abbr> architectural guidelines, using the <abbr title="Hypertext Transfer Protocol">HTTP</abbr> protocol, they will be able to communicate with each other, which makes it possible to work independently on the backend and on the frontend using different technologies and skills, really useful in teamwork.
|
||||||
|
|
||||||
|
To allow the development of this design, it is necessary to think about its architecture in order to solve the following problem: how to store and structure the folders and files of a source code in order to find which file to modify to correct a problem with a particular feature, or to add a feature?
|
||||||
|
|
||||||
|
There are two main architectures to solve this problem:
|
||||||
|
|
||||||
|
- The **Monorepo** architecture is a single directory containing several distinct projects with well-defined relationships, i.e. it allows the modification of the code in the client and server part simultaneously in the same place by verifying that a modification in the server part does not impact the client part.
|
||||||
|
- The **Polyrepo** architecture are several directories, a directory corresponding to a project.
|
||||||
|
|
||||||
|
Both architectures track source code file history using version control systems such as [git](https://git-scm.com/).
|
||||||
|
|
||||||
|
**Thream**, uses the **Polyrepo** architecture, to make it easier to set up and allows complete independence between client and server code.
|
||||||
|
|
||||||
|
### Technologies
|
||||||
|
|
||||||
|
Now that we have discussed, on the architecture of the source code, we will discuss the choice of technologies. The chosen technologies must meet the need, and allow the developer to be productive to quickly have a result. Often there are several possible technologies to meet the same need, so it is a question of choosing the technology that you prefer and that you know best.
|
||||||
|
|
||||||
|
To ease the development, we chose for **Thream** to use the [TypeScript](https://www.typescriptlang.org/), an open source programming language made by Microsoft. It's a stricter syntactic superset of **JavaScript**, and adds optional static typing to the language, meaning we can assign a "type" (`string`, `number`, `boolean` etc.) to each data/variable in the code, which has the advantage of identifying program errors even before execution and thus greatly improves developer productivity.
|
||||||
|
|
||||||
|
The **TypeScript** code is then compiled into **JavaScript** language which is one of the basic technologies of the **World Wide Web**, alongside HTML and CSS (in the client part), this makes it possible to make the pages of websites **interactive** by executing code depending on a certain event, for example when clicking on a button, or when pressing a key on the keyboard.
|
||||||
|
|
||||||
|
Since the creation of **Node.js** in 2009, it is now also possible to execute **JavaScript** outside the browser, for example on a server. **Node.js is a runtime environment** that offers modules that handle various basic features for interacting with files/folders, networking (DNS, HTTP, TCP, TLS/SSL, or UDP), and other functions inaccessible from a browser and designed to reduce the complexity of developing server applications.
|
||||||
|
|
||||||
|
**TypeScript** allows you to code with the same programming language, the client part and the server part, with different needs.
|
||||||
|
|
||||||
|
### User Interface (frontend)
|
||||||
|
|
||||||
|
The needs of the graphical interface (the client part):
|
||||||
|
|
||||||
|
- be accessible through a web browser
|
||||||
|
- allow the user to install the application
|
||||||
|
- adapts to screen size
|
||||||
|
- real-time data update (example: when a new message is sent)
|
||||||
|
- save the authenticated user (in cookie)
|
||||||
|
|
||||||
|
In order to meet its needs, Thream is a **<abbr title="Progressive Web App">PWA</abbr>**, this consists of making a website appear to the user as a native application, which makes it possible to combine the functionalities of browsers with those of the experience offered by native applications, such as the possibility of installing the application. The main advantage is to be able to **code once** and to provide the **application on several platforms (iOS, Android, Windows, GNU/Linux etc.)** without the need to develop specifically according to each platform.
|
||||||
|
|
||||||
|
To design a **<abbr title="Progressive Web App">PWA</abbr>**, and allow updating the data on the graphical interface, we can use a framework, a development infrastructure to offer us a set of tools and software components. For **Thream**, we use [Next.js](https://nextjs.org/), a framework based on [React.js](https://reactjs.org/), which allows you to create interactive user interfaces in JavaScript, to **update the graphical interface when the data changes**.
|
||||||
|
|
||||||
|
### Server (backend)
|
||||||
|
|
||||||
|
By using the protocol, **<abbr title="Hypertext Transfer Protocol">HTTP</abbr>**, it is the client who sends a request to the server, but to allow the transfer of data in real time, the **<abbr title="Hypertext Transfer Protocol">HTTP</abbr>** protocol is no longer sufficient.
|
||||||
|
|
||||||
|
We use **WebSockets** so that it is the server that send a response to all connected clients without the client requesting to the server to get a response, the server sends responses according to events, for example when creating or deleting a message.
|
||||||
|
|
||||||
|
Thanks to [Fastify](https://www.fastify.io/), a fast and low overhead web framework, for Node.js and [Socket.io](https://socket.io/), a bidirectional and low-latency communication for every platform, we can easily make REST API and real time communication.
|
||||||
|
|
||||||
|
To store the data, we use [PostgreSQL](https://www.postgresql.org/) database, and [Prisma](https://www.prisma.io/), a <abbr title="Object-Relational Mapping">ORM</abbr> for Node.js, which allows us to easily interact with the database without the need of writing <abbr title="Structured Query Language">SQL</abbr> ourselves.
|
||||||
|
|
||||||
|
## Current and future state
|
||||||
|
|
||||||
|
The main interest of **Thream** is to be able to put into practice the computer knowledge acquired as an autodidact on a concrete project, in order to learn, and understand the problems and potential solutions to complex computer applications such as social networks.
|
||||||
|
|
||||||
|
Now that the first version of **Thream** has been released, there may not be any major evolution thereafter, the project will continue to be maintained to fix any bugs, and remain accessible, for as long as possible.
|
||||||
|
|
||||||
|
The other interest of the project is that it is completely **open-source**, and allows those who want to contribute to the development, and add new features.
|
||||||
|
|
||||||
|
**Thream** is **non-profit** and therefore has no financial goal, deadline or specific feature target, which makes the design of the project a hobby and a way to learn new concepts.
|
||||||
|
|
||||||
|
Feel free to give feebacks and suggestions to improve the project, and to report any bug you find.
|
||||||
|
|
||||||
|
**Thream** is available: [**thream.divlo.fr**](https://www.thream.divlo.fr/).
|
0
public/curriculum-vitae/.gitkeep
Normal file
BIN
public/images/logo_orange.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
public/images/posts/thream-v1-0-0/http-communication.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
public/images/posts/thream-v1-0-0/social-project.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
public/images/posts/thream-v1-0-0/thream-ui.png
Normal file
After Width: | Height: | Size: 85 KiB |
@ -6,7 +6,7 @@
|
|||||||
"basics": {
|
"basics": {
|
||||||
"name": "Théo LUDWIG",
|
"name": "Théo LUDWIG",
|
||||||
"label": "Développeur Full Stack Junior • Passionné de High-Tech",
|
"label": "Développeur Full Stack Junior • Passionné de High-Tech",
|
||||||
"image": "https://s.gravatar.com/avatar/ebd6e0bf679562c20e28b5ffd02bf3e5?s=100&r=pg&d=mm",
|
"image": "https://divlo.fr/images/logo_orange.png",
|
||||||
"email": "contact@divlo.fr",
|
"email": "contact@divlo.fr",
|
||||||
"location": {},
|
"location": {},
|
||||||
"url": "https://divlo.fr",
|
"url": "https://divlo.fr",
|
||||||
@ -15,7 +15,6 @@
|
|||||||
"education": [
|
"education": [
|
||||||
{
|
{
|
||||||
"startDate": "2022",
|
"startDate": "2022",
|
||||||
"endDate": "2024",
|
|
||||||
"studyType": "Diplôme du Bachelor Universitaire de Technologie (BUT) Informatique",
|
"studyType": "Diplôme du Bachelor Universitaire de Technologie (BUT) Informatique",
|
||||||
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
||||||
"score": "En cours"
|
"score": "En cours"
|
||||||
@ -41,8 +40,8 @@
|
|||||||
"website": "https://www.nuitdelinfo.com/",
|
"website": "https://www.nuitdelinfo.com/",
|
||||||
"name": "La Nuit de l'info 2021",
|
"name": "La Nuit de l'info 2021",
|
||||||
"position": "Participation avec l'équipe <a href=\"https://www.nuitdelinfo.com/inscription/equipes/46\">Who are We</a>",
|
"position": "Participation avec l'équipe <a href=\"https://www.nuitdelinfo.com/inscription/equipes/46\">Who are We</a>",
|
||||||
"startDate": "2021-07-07",
|
"startDate": "2021-12-02",
|
||||||
"endDate": "2021-07-30"
|
"endDate": "2021-12-03"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"summary": "Agent administratif en vue de faire face au sucroît temporaire d'activités liés à la numérisation des plans des postes sources <br /> actuellement sous format papier calque suite à la libération des locaux des archives.",
|
"summary": "Agent administratif en vue de faire face au sucroît temporaire d'activités liés à la numérisation des plans des postes sources <br /> actuellement sous format papier calque suite à la libération des locaux des archives.",
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["jest", "@testing-library/jest-dom", "@testing-library/react"],
|
"types": ["cypress"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { getAge } from '../getAge'
|
|
||||||
|
|
||||||
describe('utils/getAge', () => {
|
|
||||||
it('should calculate the right age of a person', () => {
|
|
||||||
const birthDate = new Date('1980-02-20')
|
|
||||||
jest.useFakeTimers().setSystemTime(new Date('2018-03-20'))
|
|
||||||
expect(getAge(birthDate)).toBe(38)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should calculate the right age of a person (taking into account the months)', () => {
|
|
||||||
const birthDate = new Date('1980-07-20')
|
|
||||||
jest.useFakeTimers().setSystemTime(new Date('2018-03-20'))
|
|
||||||
expect(getAge(birthDate)).toBe(37)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,5 +1,5 @@
|
|||||||
import fs from 'fs'
|
import fs from 'node:fs'
|
||||||
import path from 'path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote'
|
import type { MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||||
import { nodeTypes } from '@mdx-js/mdx'
|
import { nodeTypes } from '@mdx-js/mdx'
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
export const DIVLO_BIRTHDAY = new Date('2003-03-31')
|
export const DIVLO_BIRTHDAY_DAY = '31' as const
|
||||||
|
export const DIVLO_BIRTHDAY_MONTH = '03' as const
|
||||||
|
export const DIVLO_BIRTHDAY_YEAR = '2003' as const
|
||||||
|
export const DIVLO_BIRTHDAY_DATE =
|
||||||
|
`${DIVLO_BIRTHDAY_DAY}/${DIVLO_BIRTHDAY_MONTH}/${DIVLO_BIRTHDAY_YEAR}` as const
|
||||||
|
export const DIVLO_BIRTHDAY_DATE_ISO_8061 =
|
||||||
|
`${DIVLO_BIRTHDAY_YEAR}-${DIVLO_BIRTHDAY_MONTH}-${DIVLO_BIRTHDAY_DAY}` as const
|
||||||
|
export const DIVLO_BIRTHDAY = new Date(DIVLO_BIRTHDAY_DATE_ISO_8061)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the age of a person based on their birth date
|
* Calculates the age of a person based on their birth date
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { DIVLO_BIRTHDAY, getAge } from './getAge'
|
|
||||||
|
|
||||||
export const getDefaultDescription = (): string => {
|
|
||||||
const age = getAge(DIVLO_BIRTHDAY)
|
|
||||||
return `I'm Divlo, I'm ${age} years old, I'm from France - Developer Full Stack Junior • Passionate about High-Tech`
|
|
||||||
}
|
|