aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-03 16:53:55 +0200
committerArmand Philippot <git@armandphilippot.com>2022-05-03 16:53:55 +0200
commitee04742d1f0645908baa30e47845126c28848f50 (patch)
tree1d80dca17437be6a351e932885e95c940833e571 /src
parent83a029084f1bbfd78b7099d9bea3371d4533c6d9 (diff)
chore: add a CV page
Diffstat (limited to 'src')
-rw-r--r--src/components/atoms/links/link.module.scss87
-rw-r--r--src/components/atoms/links/link.stories.tsx20
-rw-r--r--src/components/atoms/links/link.tsx18
-rw-r--r--src/components/molecules/images/responsive-image.module.scss11
-rw-r--r--src/components/molecules/images/responsive-image.stories.tsx1
-rw-r--r--src/components/molecules/images/responsive-image.tsx6
-rw-r--r--src/components/organisms/widgets/image-widget.module.scss13
-rw-r--r--src/components/organisms/widgets/image-widget.stories.tsx25
-rw-r--r--src/components/organisms/widgets/image-widget.test.tsx11
-rw-r--r--src/components/organisms/widgets/image-widget.tsx18
-rw-r--r--src/pages/cv.tsx198
-rw-r--r--src/styles/pages/cv.module.scss3
12 files changed, 375 insertions, 36 deletions
diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
index d23667a..1b89727 100644
--- a/src/components/atoms/links/link.module.scss
+++ b/src/components/atoms/links/link.module.scss
@@ -5,23 +5,64 @@
&[hreflang] {
&::after {
display: inline-block;
+
/* Prettier is removing spacing between content parts. */
+
/* prettier-ignore */
content: "\0000a0[" attr(hreflang) "]";
font-size: var(--font-size-sm);
}
}
+ &--download {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+ }
+ }
+ }
+
&--external {
&::after {
display: inline-block;
+
/* Prettier is removing spacing between content parts. */
+
/* prettier-ignore */
content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
}
&:focus:not(:active)::after {
/* Prettier is removing spacing between content parts. */
+
/* prettier-ignore */
content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
}
@@ -29,18 +70,56 @@
&[hreflang] {
&::after {
/* Prettier is removing spacing between content parts. */
+
/* prettier-ignore */
- content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(
+fun.encode-svg(
'<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
- ));
+));
}
&:focus:not(:active)::after {
/* Prettier is removing spacing between content parts. */
+
/* prettier-ignore */
- content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(
+fun.encode-svg(
'<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
- ));
+));
+ }
+ }
+ }
+
+ &--external#{&}--download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(
+fun.encode-svg(
+ '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
}
}
}
diff --git a/src/components/atoms/links/link.stories.tsx b/src/components/atoms/links/link.stories.tsx
index c3b3465..420eafe 100644
--- a/src/components/atoms/links/link.stories.tsx
+++ b/src/components/atoms/links/link.stories.tsx
@@ -31,14 +31,29 @@ export default {
required: false,
},
},
- external: {
+ download: {
control: {
type: 'boolean',
},
+ description: 'Determine if the link purpose is to download a file.',
table: {
category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ external: {
+ control: {
+ type: 'boolean',
},
description: 'Determine if the link is external of the current website.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
type: {
name: 'boolean',
required: false,
@@ -79,6 +94,7 @@ export const Default = Template.bind({});
Default.args = {
children: 'A link',
href: '#',
+ download: false,
external: false,
};
@@ -89,6 +105,7 @@ export const External = Template.bind({});
External.args = {
children: 'A link',
href: '#',
+ download: false,
external: true,
};
@@ -99,6 +116,7 @@ export const ExternalWithLang = Template.bind({});
ExternalWithLang.args = {
children: 'A link',
href: '#',
+ download: false,
external: true,
lang: 'en',
};
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 674c07b..c8ba273 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -12,6 +12,10 @@ export type LinkProps = {
*/
className?: string;
/**
+ * True if it is a download link. Default: false.
+ */
+ download?: boolean;
+ /**
* True if it is an external link. Default: false.
*/
external?: boolean;
@@ -33,21 +37,29 @@ export type LinkProps = {
const Link: FC<LinkProps> = ({
children,
className = '',
+ download = false,
+ external = false,
href,
lang,
- external = false,
}) => {
+ const downloadClass = download ? styles['link--download'] : '';
+
return external ? (
<a
href={href}
hrefLang={lang}
- className={`${styles.link} ${styles['link--external']} ${className}`}
+ className={`${styles.link} ${styles['link--external']} ${downloadClass} ${className}`}
>
{children}
</a>
) : (
<NextLink href={href}>
- <a className={`${styles.link} ${className}`}>{children}</a>
+ <a
+ hrefLang={lang}
+ className={`${styles.link} ${downloadClass} ${className}`}
+ >
+ {children}
+ </a>
</NextLink>
);
};
diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss
index 3566421..3a4f283 100644
--- a/src/components/molecules/images/responsive-image.module.scss
+++ b/src/components/molecules/images/responsive-image.module.scss
@@ -3,8 +3,7 @@
.wrapper {
display: flex;
flex-flow: column;
- width: 100%;
- max-width: max-content;
+ width: fit-content;
margin: 0;
position: relative;
text-align: center;
@@ -14,9 +13,7 @@
margin: 0;
padding: fun.convert-px(4) var(--spacing-2xs);
background: var(--color-bg-secondary);
- border: fun.convert-px(1) solid var(--color-border);
- box-shadow: 0 fun.convert-px(-1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light);
+ border: fun.convert-px(1) solid var(--color-border-light);
font-weight: 500;
}
@@ -32,7 +29,7 @@
&:hover,
&:focus {
- transform: scale(1.1);
+ transform: scale(var(--scale-up, 1.05));
}
&:focus {
@@ -43,7 +40,7 @@
}
&:active {
- transform: scale(0.9);
+ transform: scale(var(--scale-down, 0.95));
.caption {
text-decoration: none;
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
index a1f5295..35d9116 100644
--- a/src/components/molecules/images/responsive-image.stories.tsx
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -64,6 +64,7 @@ export default {
required: false,
},
},
+ unoptimized: { table: { disable: true } },
width: {
control: {
type: 'number',
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 31cbcd1..d07dd6c 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
import Link, { type LinkProps } from '@components/atoms/links/link';
import Image, { type ImageProps } from 'next/image';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
import styles from './responsive-image.module.scss';
export type ResponsiveImageProps = Omit<
@@ -14,7 +14,7 @@ export type ResponsiveImageProps = Omit<
/**
* A figure caption.
*/
- caption?: string;
+ caption?: ReactNode;
/**
* Set additional classnames to the figure wrapper.
*/
@@ -55,6 +55,7 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
alt={alt}
layout={layout || 'intrinsic'}
objectFit={objectFit || 'contain'}
+ className={styles.img}
{...props}
/>
{caption && (
@@ -67,6 +68,7 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
alt={alt}
layout={layout || 'intrinsic'}
objectFit={objectFit || 'contain'}
+ className={styles.img}
{...props}
/>
{caption && (
diff --git a/src/components/organisms/widgets/image-widget.module.scss b/src/components/organisms/widgets/image-widget.module.scss
index 8e4d0aa..0d69441 100644
--- a/src/components/organisms/widgets/image-widget.module.scss
+++ b/src/components/organisms/widgets/image-widget.module.scss
@@ -1,7 +1,12 @@
@use "@styles/abstracts/functions" as fun;
-.img {
+.figure {
+ --scale-up: 1.02;
+ --scale-down: 0.98;
+
margin: 0;
+ padding: fun.convert-px(5);
+ border: fun.convert-px(1) solid var(--color-border);
}
.txt {
@@ -10,7 +15,7 @@
.widget {
&--left {
- .img {
+ .figure {
margin-right: auto;
}
@@ -20,7 +25,7 @@
}
&--center {
- .img {
+ .figure {
margin-left: auto;
margin-right: auto;
}
@@ -31,7 +36,7 @@
}
&--right {
- .img {
+ .figure {
margin-left: auto;
}
diff --git a/src/components/organisms/widgets/image-widget.stories.tsx b/src/components/organisms/widgets/image-widget.stories.tsx
index 27871ae..3014b36 100644
--- a/src/components/organisms/widgets/image-widget.stories.tsx
+++ b/src/components/organisms/widgets/image-widget.stories.tsx
@@ -50,7 +50,7 @@ export default {
required: true,
},
},
- img: {
+ image: {
description: 'An image object.',
type: {
name: 'object',
@@ -58,6 +58,19 @@ export default {
value: {},
},
},
+ imageClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the image wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
level: {
control: {
type: 'number',
@@ -107,7 +120,7 @@ const Template: ComponentStory<typeof ImageWidget> = (args) => (
<ImageWidget {...args} />
);
-const img = {
+const image = {
alt: 'Et perferendis quaerat',
height: 480,
src: 'http://placeimg.com/640/480/nature',
@@ -122,7 +135,7 @@ export const AlignLeft = Template.bind({});
AlignLeft.args = {
alignment: 'left',
expanded: true,
- img,
+ image,
level: 2,
title: 'Quo et totam',
};
@@ -134,7 +147,7 @@ export const AlignCenter = Template.bind({});
AlignCenter.args = {
alignment: 'center',
expanded: true,
- img,
+ image,
level: 2,
title: 'Quo et totam',
};
@@ -146,7 +159,7 @@ export const AlignRight = Template.bind({});
AlignRight.args = {
alignment: 'right',
expanded: true,
- img,
+ image,
level: 2,
title: 'Quo et totam',
};
@@ -158,7 +171,7 @@ export const WithDescription = Template.bind({});
WithDescription.args = {
description: 'Sint enim harum',
expanded: true,
- img,
+ image,
level: 2,
title: 'Quo et totam',
};
diff --git a/src/components/organisms/widgets/image-widget.test.tsx b/src/components/organisms/widgets/image-widget.test.tsx
index 8c24bd9..c6b1a3a 100644
--- a/src/components/organisms/widgets/image-widget.test.tsx
+++ b/src/components/organisms/widgets/image-widget.test.tsx
@@ -18,7 +18,12 @@ const url = '/another-page';
describe('ImageWidget', () => {
it('renders an image', () => {
render(
- <ImageWidget expanded={true} img={img} title={title} level={titleLevel} />
+ <ImageWidget
+ expanded={true}
+ image={img}
+ title={title}
+ level={titleLevel}
+ />
);
expect(screen.getByRole('img', { name: img.alt })).toBeInTheDocument();
});
@@ -27,7 +32,7 @@ describe('ImageWidget', () => {
render(
<ImageWidget
expanded={true}
- img={img}
+ image={img}
title={title}
level={titleLevel}
url={url}
@@ -43,7 +48,7 @@ describe('ImageWidget', () => {
render(
<ImageWidget
expanded={true}
- img={img}
+ image={img}
description={description}
title={title}
level={titleLevel}
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
index ba04c6a..873337b 100644
--- a/src/components/organisms/widgets/image-widget.tsx
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -14,7 +14,7 @@ export type Image = Pick<
export type ImageWidgetProps = Pick<
WidgetProps,
- 'expanded' | 'level' | 'title'
+ 'className' | 'expanded' | 'level' | 'title'
> & {
/**
* The content alignment.
@@ -27,7 +27,11 @@ export type ImageWidgetProps = Pick<
/**
* An object describing the image.
*/
- img: Image;
+ image: Image;
+ /**
+ * Set additional classnames to the image wrapper.
+ */
+ imageClassName?: string;
/**
* Add a link to the image.
*/
@@ -41,20 +45,22 @@ export type ImageWidgetProps = Pick<
*/
const ImageWidget: FC<ImageWidgetProps> = ({
alignment = 'left',
+ className = '',
description,
- img,
+ image,
+ imageClassName = '',
url,
...props
}) => {
const alignmentClass = `widget--${alignment}`;
return (
- <Widget className={styles[alignmentClass]} {...props}>
+ <Widget className={`${styles[alignmentClass]} ${className}`} {...props}>
<ResponsiveImage
target={url}
caption={description}
- className={styles.img}
- {...img}
+ className={`${styles.figure} ${imageClassName}`}
+ {...image}
/>
</Widget>
);
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
new file mode 100644
index 0000000..d47edc6
--- /dev/null
+++ b/src/pages/cv.tsx
@@ -0,0 +1,198 @@
+import Link from '@components/atoms/links/link';
+import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import ImageWidget from '@components/organisms/widgets/image-widget';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import CVContent, { data, meta } from '@content/pages/cv.mdx';
+import styles from '@styles/pages/cv.module.scss';
+import { getFormattedDate } from '@utils/helpers/dates';
+import { loadTranslation } from '@utils/helpers/i18n';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps, NextPage } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import { AboutPage, Graph, WebPage } from 'schema-dts';
+
+/**
+ * CV page.
+ */
+const CVPage: NextPage = () => {
+ const intl = useIntl();
+ const { file, image } = data;
+ const { dates, intro, seo, title } = meta;
+ const homeLabel = intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'Breadcrumb: home label',
+ id: 'j5k9Fe',
+ });
+ const breadcrumb: BreadcrumbItem[] = [
+ { id: 'home', name: homeLabel, url: '/' },
+ { id: 'cv', name: title, url: '/cv' },
+ ];
+
+ const imageWidgetTitle = intl.formatMessage({
+ defaultMessage: 'Others formats',
+ description: 'CVPage: cv preview widget title',
+ id: 'B9OCyV',
+ });
+ const socialMediaTitle = intl.formatMessage({
+ defaultMessage: 'Open-source projects',
+ description: 'CVPage: social media widget title',
+ id: '+Dre5J',
+ });
+
+ const publicationLabel = intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Meta: publication date label',
+ id: 'QGi5uD',
+ });
+
+ const updateLabel = intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Meta: update date label',
+ id: 'tLC7bh',
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: {
+ name: publicationLabel,
+ value: getFormattedDate(dates.publication),
+ },
+ update: { name: updateLabel, value: getFormattedDate(dates.update) },
+ };
+
+ const { website } = useSettings();
+ const cvAlt = intl.formatMessage(
+ {
+ defaultMessage: '{name} CV',
+ description: 'CVPage: CV image alternative text',
+ id: 'KUowUk',
+ },
+ { name: website.name }
+ );
+ const cvCaption = intl.formatMessage(
+ {
+ defaultMessage: '<link>Download the CV in PDF</link>',
+ id: 'fN04AJ',
+ description: 'CVPage: download CV in PDF text',
+ },
+ {
+ link: (chunks: ReactNode) => (
+ <Link download={true} href={file}>
+ {chunks}
+ </Link>
+ ),
+ }
+ );
+
+ const widgets = [
+ <ImageWidget
+ key="image-widget"
+ expanded={true}
+ title={imageWidgetTitle}
+ level={2}
+ image={{ alt: cvAlt, ...image }}
+ description={cvCaption}
+ imageClassName={styles.image}
+ />,
+ <SocialMedia
+ key="social-media"
+ title={socialMediaTitle}
+ level={2}
+ media={[
+ { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+ { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+ {
+ name: 'LinkedIn',
+ url: 'https://www.linkedin.com/in/armandphilippot',
+ },
+ ]}
+ />,
+ ];
+
+ const { asPath } = useRouter();
+ const pageUrl = `${website.url}${asPath}`;
+ const pagePublicationDate = new Date(dates.publication);
+ const pageUpdateDate = new Date(dates.update);
+
+ const webpageSchema: WebPage = {
+ '@id': `${pageUrl}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${website.url}/#breadcrumb` },
+ name: seo.title,
+ description: seo.description,
+ reviewedBy: { '@id': `${website.url}/#branding` },
+ url: `${pageUrl}`,
+ isPartOf: {
+ '@id': `${website.url}`,
+ },
+ };
+
+ const cvSchema: AboutPage = {
+ '@id': `${website.url}/#cv`,
+ '@type': 'AboutPage',
+ name: seo.title,
+ description: intro,
+ author: { '@id': `${website.url}/#branding` },
+ creator: { '@id': `${website.url}/#branding` },
+ dateCreated: pagePublicationDate.toISOString(),
+ dateModified: pageUpdateDate.toISOString(),
+ datePublished: pagePublicationDate.toISOString(),
+ editor: { '@id': `${website.url}/#branding` },
+ image: image.src,
+ inLanguage: website.locales.default,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ thumbnailUrl: image.src,
+ mainEntityOfPage: { '@id': `${pageUrl}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, cvSchema],
+ };
+
+ return (
+ <PageLayout
+ title={title}
+ intro={intro}
+ headerMeta={headerMeta}
+ breadcrumb={breadcrumb}
+ withToC={true}
+ widgets={widgets}
+ >
+ <Head>
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
+ <meta property="og:image" content={image.src} />
+ <meta property="og:image:alt" content={title} />
+ </Head>
+ <Script
+ id="schema-cv"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <CVContent />
+ </PageLayout>
+ );
+};
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ translation,
+ },
+ };
+};
+
+export default CVPage;
diff --git a/src/styles/pages/cv.module.scss b/src/styles/pages/cv.module.scss
new file mode 100644
index 0000000..615c50d
--- /dev/null
+++ b/src/styles/pages/cv.module.scss
@@ -0,0 +1,3 @@
+.image {
+ max-width: min(100%, 25ch);
+}