From 4bd651b9e32c568d86b30463858c20ef290d8c07 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Thu, 7 Apr 2022 22:54:27 +0200 Subject: chore: add a HeadingButton component --- .../molecules/buttons/heading-button.module.scss | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/components/molecules/buttons/heading-button.module.scss (limited to 'src/components/molecules/buttons/heading-button.module.scss') diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss new file mode 100644 index 0000000..d068001 --- /dev/null +++ b/src/components/molecules/buttons/heading-button.module.scss @@ -0,0 +1,41 @@ +@use "@styles/abstracts/functions" as fun; + +.icon { + transition: all 0.25s ease-in-out 0s; +} + +.wrapper { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + width: 100%; + padding: 0; + position: sticky; + top: 0; + background: inherit; + border: none; + border-top: fun.convert-px(2) solid var(--color-primary-dark); + border-bottom: fun.convert-px(2) solid var(--color-primary-dark); + cursor: pointer; + + &:hover, + &:focus { + .icon { + background: var(--color-primary-light); + color: var(--color-fg-inverted); + transform: scale(1.25); + + &::before, + &::after { + background: var(--color-bg); + } + } + } +} + +.heading { + background: none; + padding: var(--spacing-2xs) 0; +} -- cgit v1.2.3 From 6ec16bc15cc78e62cb94e131699625fa5363437c Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 15 Apr 2022 15:34:21 +0200 Subject: chore: add a Widget component --- .../molecules/buttons/heading-button.module.scss | 2 +- .../molecules/buttons/heading-button.tsx | 19 ++--- src/components/molecules/layout/widget.module.scss | 40 ++++++++++ src/components/molecules/layout/widget.stories.tsx | 85 ++++++++++++++++++++++ src/components/molecules/layout/widget.test.tsx | 19 +++++ src/components/molecules/layout/widget.tsx | 54 ++++++++++++++ 6 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 src/components/molecules/layout/widget.module.scss create mode 100644 src/components/molecules/layout/widget.stories.tsx create mode 100644 src/components/molecules/layout/widget.test.tsx create mode 100644 src/components/molecules/layout/widget.tsx (limited to 'src/components/molecules/buttons/heading-button.module.scss') diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss index d068001..1d16410 100644 --- a/src/components/molecules/buttons/heading-button.module.scss +++ b/src/components/molecules/buttons/heading-button.module.scss @@ -11,7 +11,7 @@ justify-content: space-between; gap: var(--spacing-md); width: 100%; - padding: 0; + padding: 0 var(--spacing-2xs); position: sticky; top: 0; background: inherit; diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx index 700b3e1..fc79749 100644 --- a/src/components/molecules/buttons/heading-button.tsx +++ b/src/components/molecules/buttons/heading-button.tsx @@ -1,10 +1,14 @@ import Heading, { type HeadingProps } from '@components/atoms/headings/heading'; import PlusMinus from '@components/atoms/icons/plus-minus'; -import { FC, SetStateAction } from 'react'; +import { SetStateAction, VFC } from 'react'; import { useIntl } from 'react-intl'; import styles from './heading-button.module.scss'; export type HeadingButtonProps = Pick & { + /** + * Set additional classnames to the button. + */ + className?: string; /** * Accordion state. */ @@ -24,7 +28,8 @@ export type HeadingButtonProps = Pick & { * * Render a button as accordion title to toggle body. */ -const HeadingButton: FC = ({ +const HeadingButton: VFC = ({ + className = '', expanded, level, setExpanded, @@ -47,18 +52,14 @@ const HeadingButton: FC = ({ return ( ); }; diff --git a/src/components/molecules/layout/widget.module.scss b/src/components/molecules/layout/widget.module.scss new file mode 100644 index 0000000..727ffb7 --- /dev/null +++ b/src/components/molecules/layout/widget.module.scss @@ -0,0 +1,40 @@ +@use "@styles/abstracts/functions" as fun; + +.widget { + display: flex; + flex-flow: column; + + &__header { + background: var(--color-bg); + } + + &--has-borders & { + &__body { + border: fun.convert-px(2) solid var(--color-primary-dark); + } + } + + &--collapsed & { + &__body { + max-height: 0; + margin: 0; + visibility: hidden; + opacity: 0; + overflow: hidden; + border: 0 solid transparent; + transition: all 0.1s linear 0.3s, + max-height 0.5s cubic-bezier(0, 1, 0, 1) 0s, margin 0.3s ease-in-out 0s; + } + } + + &--expanded & { + &__body { + max-height: 10000px; // needs a fixed value for transition. + margin: var(--spacing-sm) 0; + opacity: 1; + visibility: visible; + transition: all 0.5s ease-in-out 0s, border 0s linear 0s, + max-height 0.6s ease-in-out 0s; + } + } +} diff --git a/src/components/molecules/layout/widget.stories.tsx b/src/components/molecules/layout/widget.stories.tsx new file mode 100644 index 0000000..d79f66e --- /dev/null +++ b/src/components/molecules/layout/widget.stories.tsx @@ -0,0 +1,85 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import WidgetComponent from './widget'; + +export default { + title: 'Molecules/Layout', + component: WidgetComponent, + args: { + expanded: true, + withBorders: false, + }, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The widget body', + type: { + name: 'string', + required: true, + }, + }, + expanded: { + control: { + type: 'boolean', + }, + description: 'The widget state (expanded or collapsed)', + table: { + category: 'Options', + defaultValue: { summary: true }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + level: { + control: { + type: 'number', + }, + description: 'The heading level.', + type: { + name: 'number', + required: true, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The widget title.', + type: { + name: 'string', + required: true, + }, + }, + withBorders: { + control: { + type: 'boolean', + }, + description: 'Define if the content should have borders.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + + + +); + +export const Widget = Template.bind({}); +Widget.args = { + children: 'Widget body', + level: 2, + title: 'Widget title', +}; diff --git a/src/components/molecules/layout/widget.test.tsx b/src/components/molecules/layout/widget.test.tsx new file mode 100644 index 0000000..af561ea --- /dev/null +++ b/src/components/molecules/layout/widget.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from '@test-utils'; +import Widget from './widget'; + +const children = 'Widget body'; +const title = 'Widget title'; +const titleLevel = 2; + +describe('Widget', () => { + it('renders the widget title', () => { + render( + + {children} + + ); + expect( + screen.getByRole('heading', { level: titleLevel }) + ).toHaveTextContent(title); + }); +}); diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx new file mode 100644 index 0000000..c04362a --- /dev/null +++ b/src/components/molecules/layout/widget.tsx @@ -0,0 +1,54 @@ +import { FC, useState } from 'react'; +import HeadingButton, { HeadingButtonProps } from '../buttons/heading-button'; +import styles from './widget.module.scss'; + +export type WidgetProps = Pick< + HeadingButtonProps, + 'expanded' | 'level' | 'title' +> & { + /** + * Set additional classnames to the widget wrapper. + */ + className?: string; + /** + * Determine if the widget body should have borders. Default: false. + */ + withBorders?: boolean; +}; + +/** + * Widget component + * + * Render an expandable widget. + */ +const Widget: FC = ({ + children, + className = '', + expanded = true, + level, + title, + withBorders = false, +}) => { + const [isExpanded, setIsExpanded] = useState(expanded); + const stateClass = isExpanded ? 'widget--expanded' : 'widget--collapsed'; + const bordersClass = withBorders + ? 'widget--has-borders' + : 'widget--no-borders'; + + return ( +
+ +
{children}
+
+ ); +}; + +export default Widget; -- cgit v1.2.3 From 83a029084f1bbfd78b7099d9bea3371d4533c6d9 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 3 May 2022 16:51:22 +0200 Subject: chore: add a LegalNotice page --- mdx.d.ts | 10 +- .../molecules/buttons/heading-button.module.scss | 4 +- src/components/molecules/layout/meta.module.scss | 8 +- .../molecules/layout/page-header.module.scss | 7 +- .../widgets/links-list-widget.module.scss | 10 +- .../organisms/widgets/links-list-widget.tsx | 5 +- .../widgets/table-of-contents.module.scss | 4 + .../organisms/widgets/table-of-contents.tsx | 2 + src/pages/mentions-legales.tsx | 140 +++++++++++++++++++++ src/ts/types/app.ts | 16 ++- src/ts/types/mdx.ts | 20 +++ src/ts/types/raw-data.ts | 6 +- src/utils/helpers/author.ts | 10 +- 13 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 src/components/organisms/widgets/table-of-contents.module.scss create mode 100644 src/pages/mentions-legales.tsx create mode 100644 src/ts/types/mdx.ts (limited to 'src/components/molecules/buttons/heading-button.module.scss') diff --git a/mdx.d.ts b/mdx.d.ts index f3a9a90..b4e333d 100644 --- a/mdx.d.ts +++ b/mdx.d.ts @@ -1,13 +1,9 @@ declare module '*.mdx' { + import { MDXData, MDXPageMeta, MDXProjectMeta } from '@ts/types/mdx'; import { MDXProps } from 'mdx/types'; - import { Meta } from '@ts/types/app'; let MDXComponent: (props: MDXProps) => JSX.Element; export default MDXComponent; - export const cover: string; - export const image: string; - export const intro: string; - export const meta: Meta; - export const pdf: string; - export const seo: { title: string; description: string }; + export const data: MDXData; + export const meta: MDXPageMeta | MDXProjectMeta; } diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss index 1d16410..9c278e4 100644 --- a/src/components/molecules/buttons/heading-button.module.scss +++ b/src/components/molecules/buttons/heading-button.module.scss @@ -36,6 +36,8 @@ } .heading { - background: none; padding: var(--spacing-2xs) 0; + background: none; + font-size: var(--font-size-xl); + text-align: left; } diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss index f7cc55b..0485545 100644 --- a/src/components/molecules/layout/meta.module.scss +++ b/src/components/molecules/layout/meta.module.scss @@ -2,12 +2,18 @@ .list { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(1, minmax(0, 1fr)); + gap: var(--spacing-sm); @include mix.media("screen") { + @include mix.dimensions("2xs") { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + @include mix.dimensions("sm") { display: flex; flex-flow: column nowrap; + gap: var(--spacing-2xs); } } } diff --git a/src/components/molecules/layout/page-header.module.scss b/src/components/molecules/layout/page-header.module.scss index 93f7595..4c7df5f 100644 --- a/src/components/molecules/layout/page-header.module.scss +++ b/src/components/molecules/layout/page-header.module.scss @@ -1,5 +1,4 @@ @use "@styles/abstracts/functions" as fun; -@use "@styles/abstracts/mixins" as mix; @use "@styles/abstracts/placeholders"; .wrapper { @@ -55,9 +54,5 @@ } .meta { - @include mix.media("screen") { - @include mix.dimensions("xs") { - font-size: var(--font-size-sm); - } - } + font-size: var(--font-size-sm); } diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss index cbad83e..4444df4 100644 --- a/src/components/organisms/widgets/links-list-widget.module.scss +++ b/src/components/organisms/widgets/links-list-widget.module.scss @@ -3,6 +3,12 @@ .widget { .list { + .list { + > *:first-child { + border-top: fun.convert-px(1) solid var(--color-primary); + } + } + &__link { display: block; padding: var(--spacing-2xs) var(--spacing-xs); @@ -50,9 +56,7 @@ &__item { &:not(:last-child) { - .list__link { - border-bottom: fun.convert-px(1) solid var(--color-primary); - } + border-bottom: fun.convert-px(1) solid var(--color-primary); } > .list { diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx index 559d0b6..37a20fc 100644 --- a/src/components/organisms/widgets/links-list-widget.tsx +++ b/src/components/organisms/widgets/links-list-widget.tsx @@ -24,7 +24,7 @@ export type LinksListItems = { }; export type LinksListWidgetProps = Pick & - Pick & { + Pick & { /** * An array of name/url couple. */ @@ -37,6 +37,7 @@ export type LinksListWidgetProps = Pick & * Render a list of links inside a widget. */ const LinksListWidget: FC = ({ + className = '', items, kind = 'unordered', ...props @@ -74,7 +75,7 @@ const LinksListWidget: FC = ({ items={getListItems(items)} kind={kind} withMargin={false} - className={`${styles.list} ${styles[listKindClass]}`} + className={`${styles.list} ${styles[listKindClass]} ${className}`} itemsClassName={styles.list__item} /> diff --git a/src/components/organisms/widgets/table-of-contents.module.scss b/src/components/organisms/widgets/table-of-contents.module.scss new file mode 100644 index 0000000..36217ed --- /dev/null +++ b/src/components/organisms/widgets/table-of-contents.module.scss @@ -0,0 +1,4 @@ +.list { + font-size: var(--font-size-sm); + font-weight: 500; +} diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx index 3778e02..800ff58 100644 --- a/src/components/organisms/widgets/table-of-contents.tsx +++ b/src/components/organisms/widgets/table-of-contents.tsx @@ -2,6 +2,7 @@ import useHeadingsTree, { type Heading } from '@utils/hooks/use-headings-tree'; import { FC } from 'react'; import { useIntl } from 'react-intl'; import LinksListWidget, { type LinksListItems } from './links-list-widget'; +import styles from './table-of-contents.module.scss'; type TableOfContentsProps = { /** @@ -46,6 +47,7 @@ const TableOfContents: FC = ({ wrapper }) => { title={title} level={2} items={getItems(headingsTree)} + className={styles.list} /> ); }; diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx new file mode 100644 index 0000000..8dd0a1d --- /dev/null +++ b/src/pages/mentions-legales.tsx @@ -0,0 +1,140 @@ +import Link from '@components/atoms/links/link'; +import ResponsiveImage from '@components/molecules/images/responsive-image'; +import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb'; +import PageLayout, { + type PageLayoutProps, +} from '@components/templates/page/page-layout'; +import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx'; +import { getFormattedDate } from '@utils/helpers/dates'; +import { loadTranslation } from '@utils/helpers/i18n'; +import useSettings from '@utils/hooks/use-settings'; +import { NestedMDXComponents } from 'mdx/types'; +import { GetStaticProps, NextPage } from 'next'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import Script from 'next/script'; +import { useIntl } from 'react-intl'; +import { Article, Graph, WebPage } from 'schema-dts'; + +/** + * Legal Notice page. + */ +const LegalNoticePage: NextPage = () => { + const intl = useIntl(); + 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: 'legal-notice', name: title, url: '/mentions-legales' }, + ]; + + 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 components: NestedMDXComponents = { + Image: (props) => , + Link: (props) => , + }; + + const { website } = useSettings(); + 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, + inLanguage: website.locales.default, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + reviewedBy: { '@id': `${website.url}/#branding` }, + url: `${pageUrl}`, + isPartOf: { + '@id': `${website.url}`, + }, + }; + + const articleSchema: Article = { + '@id': `${website.url}/#legal-notice`, + '@type': 'Article', + name: title, + description: intro, + author: { '@id': `${website.url}/#branding` }, + copyrightYear: pagePublicationDate.getFullYear(), + creator: { '@id': `${website.url}/#branding` }, + dateCreated: pagePublicationDate.toISOString(), + dateModified: pageUpdateDate.toISOString(), + datePublished: pagePublicationDate.toISOString(), + editor: { '@id': `${website.url}/#branding` }, + headline: title, + inLanguage: website.locales.default, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + mainEntityOfPage: { '@id': `${pageUrl}` }, + }; + + const schemaJsonLd: Graph = { + '@context': 'https://schema.org', + '@graph': [webpageSchema, articleSchema], + }; + + return ( + + + {`${seo.title} - ${website.name}`} + + + + + + +