diff options
Diffstat (limited to 'src')
35 files changed, 1036 insertions, 301 deletions
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx index ef8dce4..b819aea 100644 --- a/src/components/atoms/forms/form.tsx +++ b/src/components/atoms/forms/form.tsx @@ -35,7 +35,6 @@ export type FormProps = { */ const Form: FC<FormProps> = ({ children, - className = '', grouped = true, onSubmit, ...props @@ -68,7 +67,7 @@ const Form: FC<FormProps> = ({ }; return ( - <form onSubmit={handleSubmit} className={className} {...props}> + <form onSubmit={handleSubmit} {...props}> {getFormItems()} </form> ); diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss index 5c97bd2..1b89727 100644 --- a/src/components/atoms/links/link.module.scss +++ b/src/components/atoms/links/link.module.scss @@ -2,29 +2,6 @@ @use "@styles/abstracts/variables" as var; .link { - background: linear-gradient(to top, var(--color-primary) 50%, transparent 50%) - 0 0 / 100% 201% no-repeat; - color: var(--color-primary); - text-decoration-thickness: 0.15em; - text-underline-offset: 20%; - transition: all 0.3s linear 0s, text-decoration 0.18s ease-in-out 0s; - - &:hover { - color: var(--color-primary-light); - text-decoration-thickness: 0.25em; - } - - &:focus { - background-position: 0 100%; - color: var(--color-fg-inverted); - } - - &:active { - background-position: 0 0; - color: var(--color-primary-dark); - text-decoration-thickness: 18%; - } - &[hreflang] { &::after { display: inline-block; diff --git a/src/components/atoms/lists/description-list-item.module.scss b/src/components/atoms/lists/description-list-item.module.scss index 60cad57..aba90ce 100644 --- a/src/components/atoms/lists/description-list-item.module.scss +++ b/src/components/atoms/lists/description-list-item.module.scss @@ -27,6 +27,8 @@ } &--inline-values { + row-gap: var(--spacing-2xs); + .term { flex: 1 1 100%; } diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx index 1f6219a..74bd4ff 100644 --- a/src/components/molecules/layout/meta.tsx +++ b/src/components/molecules/layout/meta.tsx @@ -92,13 +92,17 @@ export type MetaData = { */ topics?: string[] | JSX.Element[]; /** - * A total. + * A total number of posts. */ - total?: string; + total?: number; /** * The update date. */ update?: MetaDate; + /** + * An url. + */ + website?: string; }; export type MetaKey = keyof MetaData; @@ -145,80 +149,86 @@ const Meta: FC<MetaProps> = ({ case 'author': return intl.formatMessage({ defaultMessage: 'Written by:', - id: 'OI0N37', description: 'Meta: author label', + id: 'OI0N37', }); case 'comments': return intl.formatMessage({ defaultMessage: 'Comments:', - id: 'jTVIh8', description: 'Meta: comments label', + id: 'jTVIh8', }); case 'creation': return intl.formatMessage({ defaultMessage: 'Created on:', - id: 'b4fdYE', description: 'Meta: creation date label', + id: 'b4fdYE', }); case 'license': return intl.formatMessage({ defaultMessage: 'License:', - id: 'AuGklx', description: 'Meta: license label', + id: 'AuGklx', }); case 'popularity': return intl.formatMessage({ defaultMessage: 'Popularity:', - id: 'pWTj2W', description: 'Meta: popularity label', + id: 'pWTj2W', }); case 'publication': return intl.formatMessage({ defaultMessage: 'Published on:', - id: 'QGi5uD', description: 'Meta: publication date label', + id: 'QGi5uD', }); case 'readingTime': return intl.formatMessage({ defaultMessage: 'Reading time:', - id: 'EbFvsM', description: 'Meta: reading time label', + id: 'EbFvsM', }); case 'repositories': return intl.formatMessage({ defaultMessage: 'Repositories:', - id: 'DssFG1', description: 'Meta: repositories label', + id: 'DssFG1', }); case 'technologies': return intl.formatMessage({ defaultMessage: 'Technologies:', - id: 'ADQmDF', description: 'Meta: technologies label', + id: 'ADQmDF', }); case 'thematics': return intl.formatMessage({ defaultMessage: 'Thematics:', - id: 'bz53Us', description: 'Meta: thematics label', + id: 'bz53Us', }); case 'topics': return intl.formatMessage({ defaultMessage: 'Topics:', - id: 'gJNaBD', description: 'Meta: topics label', + id: 'gJNaBD', }); case 'total': return intl.formatMessage({ defaultMessage: 'Total:', - id: '92zgdp', description: 'Meta: total label', + id: '92zgdp', }); case 'update': return intl.formatMessage({ defaultMessage: 'Updated on:', - id: 'tLC7bh', description: 'Meta: update date label', + id: 'tLC7bh', + }); + case 'website': + return intl.formatMessage({ + defaultMessage: 'Official website:', + description: 'Meta: official website label', + id: 'GRyyfy', }); default: return ''; @@ -279,8 +289,8 @@ const Meta: FC<MetaProps> = ({ { defaultMessage: '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>', - id: '02rgLO', description: 'Meta: comments count', + id: '02rgLO', }, { a11y: (chunks: ReactNode) => ( @@ -316,6 +326,23 @@ const Meta: FC<MetaProps> = ({ case 'publication': case 'update': return getDate(value as MetaDate); + case 'total': + return intl.formatMessage( + { + defaultMessage: + '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}', + description: 'BlogPage: posts count meta', + id: 'OF5cPz', + }, + { postsCount: value as number } + ); + case 'website': + const url = value as string; + return ( + <Link href={url} external={true}> + {url} + </Link> + ); default: return value as string | ReactNode | ReactNode[]; } diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx index e998b1e..97e449f 100644 --- a/src/components/molecules/layout/page-footer.tsx +++ b/src/components/molecules/layout/page-footer.tsx @@ -19,7 +19,9 @@ export type PageFooterProps = { */ const PageFooter: FC<PageFooterProps> = ({ meta, ...props }) => { return ( - <footer {...props}>{meta && <Meta data={meta} layout="column" />}</footer> + <footer {...props}> + {meta && <Meta data={meta} withSeparator={false} />} + </footer> ); }; diff --git a/src/components/molecules/layout/page-header.module.scss b/src/components/molecules/layout/page-header.module.scss index 4c7df5f..232023a 100644 --- a/src/components/molecules/layout/page-header.module.scss +++ b/src/components/molecules/layout/page-header.module.scss @@ -56,3 +56,9 @@ .meta { font-size: var(--font-size-sm); } + +.intro { + > *:last-child { + margin-bottom: 0; + } +} diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx index 9abe9af..6759c7f 100644 --- a/src/components/molecules/layout/page-header.tsx +++ b/src/components/molecules/layout/page-header.tsx @@ -1,5 +1,5 @@ import Heading from '@components/atoms/headings/heading'; -import { FC } from 'react'; +import { FC, ReactNode } from 'react'; import Meta, { type MetaData } from './meta'; import styles from './page-header.module.scss'; @@ -19,7 +19,7 @@ export type PageHeaderProps = { /** * The page title. */ - title: string; + title: ReactNode; }; /** @@ -35,9 +35,12 @@ const PageHeader: FC<PageHeaderProps> = ({ }) => { const getIntro = () => { return typeof intro === 'string' ? ( - <div dangerouslySetInnerHTML={{ __html: intro }} /> + <div + className={styles.intro} + dangerouslySetInnerHTML={{ __html: intro }} + /> ) : ( - <div>{intro}</div> + <div className={styles.intro}>{intro}</div> ); }; diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx index 9e0abdf..5ff4ea4 100644 --- a/src/components/organisms/forms/comment-form.tsx +++ b/src/components/organisms/forms/comment-form.tsx @@ -1,5 +1,5 @@ import Button from '@components/atoms/buttons/button'; -import Form from '@components/atoms/forms/form'; +import Form, { type FormProps } from '@components/atoms/forms/form'; import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; import Spinner from '@components/atoms/loaders/spinner'; import LabelledField from '@components/molecules/forms/labelled-field'; @@ -15,11 +15,7 @@ export type CommentFormData = { website?: string; }; -export type CommentFormProps = { - /** - * Set additional classnames to the form wrapper. - */ - className?: string; +export type CommentFormProps = Pick<FormProps, 'className'> & { /** * Pass a component to print a success/error message. */ @@ -44,12 +40,12 @@ export type CommentFormProps = { }; const CommentForm: FC<CommentFormProps> = ({ - className = '', Notice, parentId, saveComment, title, titleLevel = 2, + ...props }) => { const intl = useIntl(); const [name, setName] = useState<string>(''); @@ -116,9 +112,9 @@ const CommentForm: FC<CommentFormProps> = ({ return ( <Form onSubmit={submitHandler} - className={className} aria-label={formAriaLabel} aria-labelledby={formLabelledBy} + {...props} > {title && ( <Heading id={formId} level={titleLevel}> diff --git a/src/components/organisms/widgets/sharing.stories.tsx b/src/components/organisms/widgets/sharing.stories.tsx index 47213b6..59b86d3 100644 --- a/src/components/organisms/widgets/sharing.stories.tsx +++ b/src/components/organisms/widgets/sharing.stories.tsx @@ -1,5 +1,4 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { IntlProvider } from 'react-intl'; import SharingWidget from './sharing'; /** @@ -9,6 +8,19 @@ export default { title: 'Organisms/Widgets', component: SharingWidget, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the sharing links list.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, data: { description: 'The page data.', type: { @@ -58,13 +70,6 @@ export default { }, }, }, - decorators: [ - (Story) => ( - <IntlProvider locale="en"> - <Story /> - </IntlProvider> - ), - ], } as ComponentMeta<typeof SharingWidget>; const Template: ComponentStory<typeof SharingWidget> = (args) => ( diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx index 85dadb0..c63f5db 100644 --- a/src/components/organisms/widgets/sharing.tsx +++ b/src/components/organisms/widgets/sharing.tsx @@ -23,6 +23,10 @@ export type SharingData = { export type SharingProps = { /** + * Set additional classnames to the sharing links list. + */ + className?: string; + /** * The page data to share. */ data: SharingData; @@ -46,6 +50,7 @@ export type SharingProps = { * Render a list of sharing links inside a widget. */ const Sharing: FC<SharingProps> = ({ + className = '', data, media, expanded = true, @@ -201,7 +206,7 @@ const Sharing: FC<SharingProps> = ({ return ( <Widget expanded={expanded} level={level} title={widgetTitle} {...props}> - <ul className={styles.list}>{getItems()}</ul> + <ul className={`${styles.list} ${className}`}>{getItems()}</ul> </Widget> ); }; diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss index 7602492..83e5a80 100644 --- a/src/components/templates/page/page-layout.module.scss +++ b/src/components/templates/page/page-layout.module.scss @@ -72,6 +72,7 @@ .footer { grid-column: 2; + margin: var(--spacing-sm) 0 var(--spacing-2xs); } .comments { @@ -87,4 +88,9 @@ grid-column: 2; margin: var(--spacing-md) 0 0; } + + &__form { + max-width: 40ch; + margin: auto; + } } diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 002b951..8af5f98 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -473,7 +473,7 @@ export const Blog = Template.bind({}); Blog.args = { breadcrumb: postsListBreadcrumb, title: 'Blog', - headerMeta: { total: `${posts.length} posts` }, + headerMeta: { total: posts.length }, children: ( <> <PostsList posts={posts} byYear={true} total={posts.length} /> diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx index bc90f4c..f3f3ea8 100644 --- a/src/components/templates/page/page-layout.tsx +++ b/src/components/templates/page/page-layout.tsx @@ -20,7 +20,7 @@ import TableOfContents from '@components/organisms/widgets/table-of-contents'; import { type SendCommentVars } from '@services/graphql/api'; import { sendComment } from '@services/graphql/comments'; import useIsMounted from '@utils/hooks/use-is-mounted'; -import { FC, ReactNode, useRef, useState } from 'react'; +import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import Layout, { type LayoutProps } from '../layout/layout'; import styles from './page-layout.module.scss'; @@ -33,6 +33,11 @@ export type PageLayoutProps = Pick< * True if the page accepts new comments. Default: false. */ allowComments?: boolean; + bodyAttributes?: HTMLAttributes<HTMLDivElement>; + /** + * Set additional classnames to the body wrapper. + */ + bodyClassName?: string; /** * The breadcrumb items. */ @@ -83,6 +88,8 @@ export type PageLayoutProps = Pick< const PageLayout: FC<PageLayoutProps> = ({ children, allowComments = false, + bodyAttributes, + bodyClassName = '', breadcrumb, breadcrumbSchema, comments, @@ -91,8 +98,8 @@ const PageLayout: FC<PageLayoutProps> = ({ id, intro, isHome = false, - widgets, title, + widgets, withToC = false, }) => { const intl = useIntl(); @@ -202,11 +209,12 @@ const PageLayout: FC<PageLayoutProps> = ({ {typeof children === 'string' ? ( <div ref={bodyRef} - className={styles.body} + className={`${styles.body} ${bodyClassName}`} dangerouslySetInnerHTML={{ __html: children }} + {...bodyAttributes} /> ) : ( - <div ref={bodyRef} className={styles.body}> + <div ref={bodyRef} className={`${styles.body} ${bodyClassName}`}> {children} </div> )} @@ -245,6 +253,7 @@ const PageLayout: FC<PageLayoutProps> = ({ {allowComments && ( <section className={styles.comments__section}> <CommentForm + className={styles.comments__form} saveComment={saveComment} title={commentFormTitle} Notice={ diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 5eeabd9..995e3a9 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -1,5 +1,6 @@ import ButtonLink from '@components/atoms/buttons/button-link'; import Link from '@components/atoms/links/link'; +import ResponsiveImage from '@components/molecules/images/responsive-image'; import Sharing from '@components/organisms/widgets/sharing'; import PageLayout, { type PageLayoutProps, @@ -9,15 +10,20 @@ import { getArticleBySlug, } from '@services/graphql/articles'; import { getPostComments } from '@services/graphql/comments'; +import styles from '@styles/pages/article.module.scss'; import { type Article, type Comment } from '@ts/types/app'; import { loadTranslation, type Messages } from '@utils/helpers/i18n'; +import useAddPrismClassAttr from '@utils/hooks/use-add-prism-class-attr'; import useBreadcrumb from '@utils/hooks/use-breadcrumb'; +import usePrismPlugins, { PrismPlugin } from '@utils/hooks/use-prism-plugins'; import useSettings from '@utils/hooks/use-settings'; import { GetStaticPaths, GetStaticProps, NextPage } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; import { ParsedUrlQuery } from 'querystring'; +import { HTMLAttributes } from 'react'; +import { useIntl } from 'react-intl'; import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts'; import useSWR from 'swr'; @@ -54,16 +60,31 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => { )), }; + const intl = useIntl(); + const footerMetaLabel = intl.formatMessage({ + defaultMessage: 'Read more articles about:', + description: 'ArticlePage: footer topics list label', + id: '50xc4o', + }); + const footerMeta: PageLayoutProps['footerMeta'] = { - topics: - topics && - topics.map((topic) => { + custom: topics && { + label: footerMetaLabel, + value: topics.map((topic) => { return ( - <ButtonLink key={topic.id} target={`/sujet/${topic.slug}`}> + <ButtonLink + key={topic.id} + target={`/sujet/${topic.slug}`} + className={styles.btn} + > + {topic.logo && ( + <ResponsiveImage className={styles.btn__icon} {...topic.logo} /> + )}{' '} {topic.name} </ButtonLink> ); }), + }, }; const { website } = useSettings(); @@ -156,6 +177,15 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => { }); }; + const prismPlugins: PrismPlugin[] = ['command-line', 'line-numbers']; + const { pluginsAttribute, pluginsClassName } = usePrismPlugins(prismPlugins); + useAddPrismClassAttr({ + attributes: { + 'data-filter-output': '#output#"', + }, + classNames: pluginsClassName, + }); + return ( <> <Head> @@ -173,6 +203,10 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => { /> <PageLayout allowComments={true} + bodyAttributes={{ + ...(pluginsAttribute as HTMLAttributes<HTMLDivElement>), + }} + bodyClassName={styles.body} breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} comments={data && getCommentsList(data)} @@ -185,6 +219,7 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => { widgets={[ <Sharing key="sharing-widget" + className={styles.widget} data={{ excerpt: intro, title, url: pageUrl }} media={[ 'diaspora', diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index a5ef045..b6ce221 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -114,16 +114,6 @@ const BlogPage: NextPage<BlogPageProps> = ({ '@graph': [webpageSchema, blogSchema], }; - const postsCount = intl.formatMessage( - { - defaultMessage: - '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}', - id: 'OF5cPz', - description: 'BlogPage: posts count meta', - }, - { postsCount: totalArticles } - ); - /** * Retrieve the formatted meta. * @@ -231,7 +221,7 @@ const BlogPage: NextPage<BlogPageProps> = ({ title={title} breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} - headerMeta={{ total: postsCount }} + headerMeta={{ total: totalArticles }} widgets={[ <LinksListWidget key="thematics-list" diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx index b3dec10..7936c84 100644 --- a/src/pages/cv.tsx +++ b/src/pages/cv.tsx @@ -1,4 +1,6 @@ +import Heading from '@components/atoms/headings/heading'; import Link from '@components/atoms/links/link'; +import List from '@components/atoms/lists/list'; import ImageWidget from '@components/organisms/widgets/image-widget'; import SocialMedia from '@components/organisms/widgets/social-media'; import PageLayout, { @@ -9,11 +11,12 @@ import styles from '@styles/pages/cv.module.scss'; import { loadTranslation } from '@utils/helpers/i18n'; import useBreadcrumb from '@utils/hooks/use-breadcrumb'; 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 { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { useIntl } from 'react-intl'; import { AboutPage, Graph, WebPage } from 'schema-dts'; @@ -141,6 +144,18 @@ const CVPage: NextPage = () => { '@graph': [webpageSchema, cvSchema], }; + const components: NestedMDXComponents = { + a: (props) => <Link external={true} {...props} />, + h1: (props) => <Heading level={1} {...props} />, + h2: (props) => <Heading level={2} {...props} />, + h3: (props) => <Heading level={3} {...props} />, + h4: (props) => <Heading level={4} {...props} />, + h5: (props) => <Heading level={5} {...props} />, + h6: (props) => <Heading level={6} {...props} />, + Link: (props) => <Link {...props} />, + List: (props) => <List {...props} />, + }; + return ( <PageLayout breadcrumb={breadcrumbItems} @@ -166,7 +181,7 @@ const CVPage: NextPage = () => { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> - <CVContent /> + <CVContent components={components} /> </PageLayout> ); }; diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx index 0a7dc60..d88a293 100644 --- a/src/pages/recherche/index.tsx +++ b/src/pages/recherche/index.tsx @@ -140,16 +140,6 @@ const SearchPage: NextPage<SearchPageProps> = ({ getTotalArticles(query.s as string) ); - const postsCount = intl.formatMessage( - { - defaultMessage: - '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}', - id: 'LtsVOx', - description: 'SearchPage: posts count meta', - }, - { postsCount: totalArticles || 0 } - ); - /** * Retrieve the formatted meta. * @@ -244,7 +234,7 @@ const SearchPage: NextPage<SearchPageProps> = ({ title={title} breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} - headerMeta={{ total: postsCount }} + headerMeta={{ total: totalArticles }} widgets={[ <LinksListWidget key="thematics-list" diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx index 447d969..348fe05 100644 --- a/src/pages/sujet/[slug].tsx +++ b/src/pages/sujet/[slug].tsx @@ -1,4 +1,5 @@ import Heading from '@components/atoms/headings/heading'; +import ResponsiveImage from '@components/molecules/images/responsive-image'; import PostsList, { type Post } from '@components/organisms/layout/posts-list'; import LinksListWidget from '@components/organisms/widgets/links-list-widget'; import PageLayout, { @@ -10,6 +11,7 @@ import { getTopicsPreview, getTotalTopics, } from '@services/graphql/topics'; +import styles from '@styles/pages/topic.module.scss'; import { type Article, type PageLink, type Topic } from '@ts/types/app'; import { loadTranslation, type Messages } from '@utils/helpers/i18n'; import { @@ -35,7 +37,14 @@ export type TopicPageProps = { const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => { const { content, intro, meta, slug, title } = currentTopic; - const { articles, dates, seo, thematics } = meta; + const { + articles, + cover, + dates, + seo, + thematics, + website: officialWebsite, + } = meta; const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, @@ -45,6 +54,8 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => { const headerMeta: PageLayoutProps['headerMeta'] = { publication: { date: dates.publication }, update: dates.update ? { date: dates.update } : undefined, + website: officialWebsite, + total: articles ? articles.length : undefined, }; const { website } = useSettings(); @@ -98,10 +109,10 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => { ...remainingData } = article; - const { cover, ...remainingMeta } = articleMeta; + const { cover: articleCover, ...remainingMeta } = articleMeta; return { - cover, + cover: articleCover, excerpt: articleIntro, meta: getPostMeta(remainingMeta), url: `/article/${articleSlug}`, @@ -122,6 +133,15 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => { id: '/sRqPT', }); + const getPageHeading = () => { + return ( + <> + {cover && <ResponsiveImage className={styles.logo} {...cover} />} + {title} + </> + ); + }; + return ( <> <Head> @@ -140,7 +160,7 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => { <PageLayout breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} - title={title} + title={getPageHeading()} intro={intro} headerMeta={headerMeta} widgets={ @@ -206,14 +226,15 @@ export const getStaticProps: GetStaticProps<TopicPageProps> = async ({ const allTopics = allTopicsEdges.edges.map((edge) => getPageLinkFromRawData(edge.node) ); + const topicsLinks = allTopics.filter( + (topic) => topic.slug !== (params!.slug as TopicParams['slug']) + ); const translation = await loadTranslation(locale); return { props: { currentTopic: JSON.parse(JSON.stringify(currentTopic)), - topics: allTopics.filter( - (topic) => topic.slug !== (params!.slug as TopicParams['slug']) - ), + topics: JSON.parse(JSON.stringify(topicsLinks)), translation, }, }; diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts index d65bc9e..e02ca8e 100644 --- a/src/services/graphql/articles.query.ts +++ b/src/services/graphql/articles.query.ts @@ -14,6 +14,17 @@ export const articleBySlugQuery = `query PostBy($slug: ID!) { postsInTopic { ... on Topic { databaseId + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } slug title } diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts index 19be5d7..4574256 100644 --- a/src/services/graphql/topics.query.ts +++ b/src/services/graphql/topics.query.ts @@ -94,6 +94,17 @@ export const topicsListQuery = `query TopicsList($after: String = "", $first: In cursor node { databaseId + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } slug title } diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss index 7b7a695..2c3c8cc 100644 --- a/src/styles/base/_typography.scss +++ b/src/styles/base/_typography.scss @@ -120,21 +120,15 @@ dl { & & { margin: var(--spacing-2xs) 0 0; } -} - -dt { - flex: 0 0 max-content; - font-weight: 600; -} -dd { - flex: 0 0 auto; - margin: 0; + ::marker { + color: var(--color-primary-dark); + } } a { background: linear-gradient(to top, var(--color-primary) 50%, transparent 50%) - 0 0 / 100% 200% no-repeat; + 0 0 / 100% 201% no-repeat; color: var(--color-primary); text-decoration-thickness: 0.15em; text-underline-offset: 20%; @@ -199,13 +193,3 @@ pre { word-break: normal; word-wrap: normal; } - -figure { - margin: var(--spacing-md) 0; -} - -figcaption { - margin-top: var(--spacing-xs); - font-size: var(--font-size-sm); - text-align: center; -} diff --git a/src/styles/components/_wp-blocks.scss b/src/styles/components/_wp-blocks.scss deleted file mode 100644 index efd6db5..0000000 --- a/src/styles/components/_wp-blocks.scss +++ /dev/null @@ -1,166 +0,0 @@ -@use "@styles/abstracts/functions" as fun; -@use "@styles/abstracts/mixins" as mix; -@use "@styles/abstracts/placeholders"; - -.wp-block-quote { - margin: var(--spacing-sm) 0; - padding: var(--spacing-sm); - position: relative; - border: fun.convert-px(1) solid var(--color-primary-lighter); - border-left: fun.convert-px(5) solid var(--color-primary-lighter); - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 - var(--color-shadow-light), - fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 - var(--color-shadow-light); - font-style: italic; - - > *:last-child { - margin: 0; - } - - cite { - font-size: var(--font-size-sm); - font-style: normal; - font-weight: 600; - } -} - -.wp-block-code, -.wp-block-preformatted { - margin: 0 auto var(--spacing-md); - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-bg-secondary); - border: fun.convert-px(1) solid var(--color-border-light); - color: var(--color-fg); -} - -.wp-block-columns { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--spacing-md); - margin: var(--spacing-md) 0; - - @include mix.media("screen") { - @include mix.dimensions("sm") { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - - &.are-vertically-aligned-center { - align-items: center; - } -} - -.wp-block-column { - > *:first-child { - margin-top: 0; - } - - > *:last-child { - margin-bottom: 0; - } -} - -.wp-block-gallery { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--spacing-sm); - - .blocks-gallery-grid { - @extend %reset-list; - - grid-column: 1 / -1; - grid-row: 1 / -1; - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--spacing-sm); - } - - .blocks-gallery-item { - figure { - margin: 0; - } - - a { - display: block; - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 - var(--color-shadow-light), - fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 - var(--color-shadow-light); - - &:hover, - &:focus { - transform: scale(1.05); - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(3) fun.convert-px(3) fun.convert-px(2) 0 - var(--color-shadow-light), - fun.convert-px(5) fun.convert-px(5) fun.convert-px(8) 0 - var(--color-shadow-light); - } - - &:focus { - outline: solid var(--color-primary-light); - } - - &:active { - transform: scale(0.95); - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 - var(--color-shadow-light), - 0 0 0 0 var(--color-shadow-light); - outline: none; - } - } - } - - &.aligncenter { - .blocks-gallery-grid { - align-items: center; - } - } - - @for $i from 0 to 6 { - &.columns-#{$i} { - @include mix.media("screen") { - @include mix.dimensions("xs") { - grid-template-columns: repeat(2, minmax(0, 1fr)); - - .blocks-gallery-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - - @include mix.dimensions("sm") { - grid-template-columns: repeat(#{$i}, minmax(0, 1fr)); - - .blocks-gallery-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - } - } - } -} - -.wp-block-image { - img { - display: block; - margin: auto; - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 - var(--color-shadow-light), - fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 - var(--color-shadow-light); - text-align: center; - } -} - -.wp-block-video { - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 - var(--color-shadow-light), - fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 - var(--color-shadow-light); -} diff --git a/src/styles/globals.scss b/src/styles/globals.scss index f9a1281..8ece909 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -6,7 +6,6 @@ * Import each files separately to define vendors styles order. */ @use "modern-normalize"; -@use "vendors/prism"; /** * 2.0. Base @@ -22,14 +21,7 @@ @use "base/typography"; /** - * 3.0. Components - * - * Define styles for external components (like WordPress blocks). - */ -@use "components/wp-blocks"; - -/** - * 4.0. Themes + * 3.0. Themes * * Define themes specific styles. */ diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss new file mode 100644 index 0000000..a42c633 --- /dev/null +++ b/src/styles/pages/article.module.scss @@ -0,0 +1,36 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; +@use "@styles/abstracts/variables" as var; +@use "partials/article-links"; +@use "partials/article-lists"; +@use "partials/article-media"; +@use "partials/article-prism"; +@use "partials/article-wp-blocks"; + +.btn { + margin-right: var(--spacing-2xs); + padding: var(--spacing-2xs) var(--spacing-xs); + + &__icon { + max-width: fun.convert-px(22); + margin-right: var(--spacing-2xs); + } +} + +.body { + :global { + @include article-links.styles; + @include article-lists.styles; + @include article-media.styles; + @include article-prism.styles; + @include article-wp-blocks.styles; + } +} + +.widget { + @include mix.media("screen") { + @include mix.dimensions("md") { + width: min-content; + } + } +} diff --git a/src/styles/pages/partials/_article-headings.scss b/src/styles/pages/partials/_article-headings.scss new file mode 100644 index 0000000..c0c3519 --- /dev/null +++ b/src/styles/pages/partials/_article-headings.scss @@ -0,0 +1,57 @@ +@use "@styles/abstracts/functions" as fun; + +@mixin styles { + h1 { + font-size: var(--font-size-3xl); + font-weight: 500; + } + + h2 { + padding-bottom: fun.convert-px(3); + background: linear-gradient( + to top, + var(--color-primary-dark) 0.3rem, + transparent 0.3rem + ) + 0 0 / 3rem 100% no-repeat; + font-size: var(--font-size-2xl); + font-weight: 500; + text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light); + } + + h3 { + font-size: var(--font-size-xl); + font-weight: 500; + } + + h4 { + font-size: var(--font-size-lg); + font-weight: 500; + } + + h5 { + font-size: var(--font-size-md); + font-weight: 600; + } + + h6 { + font-size: var(--font-size-md); + font-weight: 500; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: var(--color-primary-dark); + font-family: var(--font-family-secondary); + letter-spacing: 0.01ex; + margin: 0 0 var(--spacing-sm); + + & + & { + margin-top: var(--spacing-md); + } + } +} diff --git a/src/styles/pages/partials/_article-links.scss b/src/styles/pages/partials/_article-links.scss new file mode 100644 index 0000000..df86dcf --- /dev/null +++ b/src/styles/pages/partials/_article-links.scss @@ -0,0 +1,104 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/variables" as var; + +@mixin styles { + a { + &[hreflang] { + &::after { + display: inline-block; + + /* Prettier is removing spacing between content parts. */ + + /* prettier-ignore */ + content: "\0000a0[" attr(hreflang) "]"; + font-size: var(--font-size-sm); + } + } + } + + a.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>')); + } + } + } + + a.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>')); + } + + &[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="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_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>')); + } + } + } + + a.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/styles/pages/partials/_article-lists.scss b/src/styles/pages/partials/_article-lists.scss new file mode 100644 index 0000000..c0084b0 --- /dev/null +++ b/src/styles/pages/partials/_article-lists.scss @@ -0,0 +1,65 @@ +@mixin styles { + ol { + padding: 0; + list-style-type: none; + counter-reset: li; + + > li { + display: table; + counter-increment: li; + + &::before { + content: counters(li, ".") ". "; + display: table-cell; + padding-right: var(--spacing-2xs); + color: var(--color-secondary); + } + } + + li ol > li::before { + content: counters(li, ".") ". "; + } + } + + ul, + ol { + li:not(:last-child) { + margin-bottom: var(--spacing-2xs); + } + + ::marker { + color: var(--color-primary-dark); + } + } + + ul { + padding-left: var(--spacing-sm); + } + + dl { + display: flex; + flex-flow: row wrap; + gap: var(--spacing-2xs); + width: fit-content; + } + + ul, + ol, + dl { + margin: var(--spacing-sm) 0; + + & & { + margin: var(--spacing-2xs) 0 0; + } + } + + dt { + color: var(--color-fg-light); + font-weight: 600; + } + + dd { + margin: 0; + word-break: break-all; + } +} diff --git a/src/styles/pages/partials/_article-media.scss b/src/styles/pages/partials/_article-media.scss new file mode 100644 index 0000000..8359881 --- /dev/null +++ b/src/styles/pages/partials/_article-media.scss @@ -0,0 +1,11 @@ +@mixin styles { + figure { + margin: var(--spacing-md) 0; + } + + figcaption { + margin-top: var(--spacing-xs); + font-size: var(--font-size-sm); + text-align: center; + } +} diff --git a/src/styles/pages/partials/_article-prism.scss b/src/styles/pages/partials/_article-prism.scss new file mode 100644 index 0000000..025e0c0 --- /dev/null +++ b/src/styles/pages/partials/_article-prism.scss @@ -0,0 +1,301 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +@mixin styles { + .code-toolbar { + --gutter-size: clamp(#{fun.convert-px(75)}, 20vw, #{fun.convert-px(90)}); + --toolbar-height: #{fun.convert-px(90)}; + + position: relative; + margin-top: calc(var(--toolbar-height) + var(--spacing-md)); + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + --toolbar-height: #{fun.convert-px(60)}; + } + } + + .toolbar { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + justify-items: end; + width: 100%; + height: var(--toolbar-height); + position: absolute; + top: calc(var(--toolbar-height) * -1); + left: 0; + right: 0; + background: var(--color-bg-tertiary); + border: fun.convert-px(1) solid var(--color-border); + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + display: flex; + flex-flow: row wrap; + } + } + } + + .toolbar-item { + display: flex; + align-items: center; + } + + .toolbar-item:nth-child(1) { + grid-column: 1; + grid-row: 1 / 3; + margin-right: auto; + padding: 0 var(--spacing-sm); + background: var(--color-bg-code); + border-right: fun.convert-px(1) solid var(--color-border); + color: var(--color-primary-darker); + font-size: var(--font-size-sm); + font-weight: 600; + } + + .toolbar-item:nth-child(2) { + grid-column: 2; + grid-row: 1; + margin: 0 var(--spacing-2xs); + } + + .toolbar-item:nth-child(3) { + grid-column: 2; + grid-row: 2; + margin: 0 var(--spacing-2xs); + } + } + + pre[class*="language-"] { + max-height: max(30vw, fun.convert-px(300)); + margin: var(--spacing-md) 0; + padding: 0; + position: relative; + background: var(--color-bg-secondary); + color: var(--color-fg); + border: fun.convert-px(1) solid var(--color-border); + + > code { + display: block; + padding: var(--spacing-xs) 0 var(--spacing-xs) + calc(var(--gutter-size) + var(--spacing-xs)); + } + + .line-numbers-rows, + .command-line-prompt { + width: var(--gutter-size); + min-height: 100%; + padding: var(--spacing-xs) var(--spacing-2xs); + position: absolute; + top: 0; + left: 0; + pointer-events: none; + user-select: none; + background: var(--color-bg); + border-right: fun.convert-px(1) solid var(--color-border); + } + + .token { + &.comment, + &.doc-comment { + color: var(--color-fg-light); + } + + &.punctuation { + color: var(--color-fg); + } + + &.attr-name, + &.hexcode, + &.inserted, + &.string { + color: var(--color-token-green); + } + + &.class, + &.coord, + &.id, + &.function { + color: var(--color-token-purple); + } + + &.builtin, + &.builtin.class-name, + &.property-access, + &.regex, + &.scope { + color: var(--color-token-magenta); + } + + &.class-name, + &.constant, + &.global, + &.interpolation, + &.key, + &.package, + &.this, + &.title, + &.variable { + color: var(--color-token-blue); + } + + &.combinator, + &.keyword, + &.operator, + &.pseudo-class, + &.pseudo-element, + &.rule, + &.selector, + &.unit { + color: var(--color-token-orange); + } + + &.attr-value, + &.boolean, + &.number { + color: var(--color-token-yellow); + } + + &.delimiter, + &.doctype, + &.parameter, + &.parent, + &.property, + &.shebang, + &.tag { + color: var(--color-token-cyan); + } + + &.deleted { + color: var(--color-token-red); + } + + &.punctuation.brace-hover, + &.punctuation.brace-selected { + background: var(--color-bg); + outline: solid fun.convert-px(1) var(--color-primary-light); + } + } + + span.inline-color-wrapper { + background: url(fun.encode-svg( + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>' + )); + + /* Prevent glitches where 1px from the repeating pattern could be seen. */ + background-position: center; + background-size: 110%; + + display: inline-block; + height: 1.1ch; + width: 1.1ch; + margin: 0 0.5ch 0 0; + border: fun.convert-px(1) solid var(--color-bg); + outline: fun.convert-px(1) solid var(--color-border-dark); + overflow: hidden; + } + + span.inline-color { + display: block; + + /* To prevent visual glitches again */ + height: 120%; + width: 120%; + } + } + + pre.line-numbers { + counter-reset: lineNumber; + + .line-numbers-rows { + > span { + counter-increment: lineNumber; + + &::before { + display: block; + padding: 0 var(--spacing-xs); + content: counter(lineNumber); + color: var(--color-primary-darker); + text-align: right; + line-height: var(--line-height); + } + } + } + } + + pre.command-line { + --gutter-size: clamp(#{fun.convert-px(195)}, 48vw, #{fun.convert-px(235)}); + + ~ .toolbar { + --gutter-size: clamp( + #{fun.convert-px(195)}, + 48vw, + #{fun.convert-px(235)} + ); + } + + .command-line-prompt { + > span { + &::before { + display: block; + content: ""; + } + + &[data-user]::before { + content: "[" attr(data-user) "@" attr(data-host) "] $"; + } + + &[data-user="root"]::before { + content: "[" attr(data-user) "@" attr(data-host) "] #"; + } + + &[data-prompt]::before { + content: attr(data-prompt); + } + } + } + } + + .copy-to-clipboard-button, + .prism-color-scheme-button { + display: block; + padding: fun.convert-px(3) var(--spacing-xs); + background: var(--color-bg); + border: 0.4ex solid var(--color-primary); + border-radius: fun.convert-px(30); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow); + color: var(--color-primary); + font-size: var(--font-size-sm); + font-weight: 600; + transition: all 0.35s ease-in-out 0s; + + &:hover, + &:focus { + transform: translateX(#{fun.convert-px(-2)}) + translateY(#{fun.convert-px(-2)}); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow-light), + fun.convert-px(4) fun.convert-px(7) fun.convert-px(8) fun.convert-px(-3) + var(--color-shadow-light); + } + + &:focus { + text-decoration: underline var(--color-primary) fun.convert-px(3); + } + + &:active { + text-decoration: none; + transform: translateY(#{fun.convert-px(2)}); + box-shadow: 0 0 0 0 var(--color-shadow); + } + } +} diff --git a/src/styles/pages/partials/_article-wp-blocks.scss b/src/styles/pages/partials/_article-wp-blocks.scss new file mode 100644 index 0000000..d4fed5a --- /dev/null +++ b/src/styles/pages/partials/_article-wp-blocks.scss @@ -0,0 +1,168 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; +@use "@styles/abstracts/placeholders"; + +@mixin styles { + .wp-block-quote { + margin: var(--spacing-sm) 0; + padding: var(--spacing-sm); + position: relative; + border: fun.convert-px(1) solid var(--color-primary-lighter); + border-left: fun.convert-px(5) solid var(--color-primary-lighter); + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 + var(--color-shadow-light); + font-style: italic; + + > *:last-child { + margin: 0; + } + + cite { + font-size: var(--font-size-sm); + font-style: normal; + font-weight: 600; + } + } + + .wp-block-code, + .wp-block-preformatted { + margin: 0 auto var(--spacing-md); + padding: var(--spacing-xs) var(--spacing-sm); + background: var(--color-bg-secondary); + border: fun.convert-px(1) solid var(--color-border-light); + color: var(--color-fg); + } + + .wp-block-columns { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--spacing-md); + margin: var(--spacing-md) 0; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + + &.are-vertically-aligned-center { + align-items: center; + } + } + + .wp-block-column { + > *:first-child { + margin-top: 0; + } + + > *:last-child { + margin-bottom: 0; + } + } + + .wp-block-gallery { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--spacing-sm); + + .blocks-gallery-grid { + @extend %reset-list; + + grid-column: 1 / -1; + grid-row: 1 / -1; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--spacing-sm); + } + + .blocks-gallery-item { + figure { + margin: 0; + } + + a { + display: block; + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 + var(--color-shadow-light); + + &:hover, + &:focus { + transform: scale(1.05); + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(2) 0 + var(--color-shadow-light), + fun.convert-px(5) fun.convert-px(5) fun.convert-px(8) 0 + var(--color-shadow-light); + } + + &:focus { + outline: solid var(--color-primary-light); + } + + &:active { + transform: scale(0.95); + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 + var(--color-shadow-light), + 0 0 0 0 var(--color-shadow-light); + outline: none; + } + } + } + + &.aligncenter { + .blocks-gallery-grid { + align-items: center; + } + } + + @for $i from 0 to 6 { + &.columns-#{$i} { + @include mix.media("screen") { + @include mix.dimensions("xs") { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + .blocks-gallery-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + + @include mix.dimensions("sm") { + grid-template-columns: repeat(#{$i}, minmax(0, 1fr)); + + .blocks-gallery-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + } + } + } + } + + .wp-block-image { + img { + display: block; + margin: auto; + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 + var(--color-shadow-light); + text-align: center; + } + } + + .wp-block-video { + box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 + var(--color-shadow-light); + } +} diff --git a/src/styles/pages/topic.module.scss b/src/styles/pages/topic.module.scss new file mode 100644 index 0000000..badb694 --- /dev/null +++ b/src/styles/pages/topic.module.scss @@ -0,0 +1,6 @@ +@use "@styles/abstracts/functions" as fun; + +.logo { + max-width: fun.convert-px(50); + margin-right: var(--spacing-xs); +} diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts index f354118..a3b9889 100644 --- a/src/ts/types/app.ts +++ b/src/ts/types/app.ts @@ -81,6 +81,7 @@ export type Page<T extends PageKind> = { export type PageLink = { id: number; + logo?: Image; name: string; slug: string; }; diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts index 7e12e7f..ba6d596 100644 --- a/src/ts/types/raw-data.ts +++ b/src/ts/types/raw-data.ts @@ -89,14 +89,17 @@ export type RawThematic = RawPage & { export type RawThematicPreview = Pick< RawThematic, - 'databaseId' | 'slug' | 'title' + 'databaseId' | 'featuredImage' | 'slug' | 'title' >; export type RawTopic = RawPage & { acfTopics: ACFTopics; }; -export type RawTopicPreview = Pick<RawTopic, 'databaseId' | 'slug' | 'title'>; +export type RawTopicPreview = Pick< + RawTopic, + 'databaseId' | 'featuredImage' | 'slug' | 'title' +>; export type TotalItems = { pageInfo: Pick<PageInfo, 'total'>; diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts index 93582f0..62337db 100644 --- a/src/utils/helpers/pages.ts +++ b/src/utils/helpers/pages.ts @@ -5,12 +5,14 @@ import { type RawThematicPreview, type RawTopicPreview, } from '@ts/types/raw-data'; +import { getImageFromRawData } from './images'; /** * Convert raw data to a Link object. * * @param data - An object. * @param {number} data.databaseId - The data id. + * @param {number} [data.logo] - The data logo. * @param {string} data.slug - The data slug. * @param {string} data.title - The data name. * @returns {PageLink} The link data (id, slug and title). @@ -18,10 +20,11 @@ import { export const getPageLinkFromRawData = ( data: RawThematicPreview | RawTopicPreview ): PageLink => { - const { databaseId, slug, title } = data; + const { databaseId, featuredImage, slug, title } = data; return { id: databaseId, + logo: featuredImage ? getImageFromRawData(featuredImage?.node) : undefined, name: title, slug, }; diff --git a/src/utils/hooks/use-add-prism-class-attr.tsx b/src/utils/hooks/use-add-prism-class-attr.tsx new file mode 100644 index 0000000..7d33cc2 --- /dev/null +++ b/src/utils/hooks/use-add-prism-class-attr.tsx @@ -0,0 +1,60 @@ +import { useCallback, useEffect, useState } from 'react'; + +export type AttributesMap = { + [key: string]: string; +}; + +export type useAddPrismClassAttrProps = { + attributes?: AttributesMap; + classNames?: string; +}; + +/** + * Add classnames and/or attributes to pre elements. + * + * @param props - An object of attributes and classnames. + */ +const useAddPrismClassAttr = ({ + attributes, + classNames, +}: useAddPrismClassAttrProps) => { + const [elements, setElements] = useState<HTMLPreElement[]>([]); + + useEffect(() => { + const targetElements = document.querySelectorAll('pre'); + setElements(Array.from(targetElements)); + }, []); + + const setClassNameAndAttributes = useCallback( + (array: HTMLElement[]) => { + array.forEach((el) => { + if (classNames) { + const classNamesArray = classNames.split(' '); + const isCommandLine = el.classList.contains('command-line'); + const removedClassName = isCommandLine + ? 'line-numbers' + : 'command-line'; + const filteredClassNames = classNamesArray.filter( + (className) => className !== removedClassName + ); + filteredClassNames.forEach((className) => + el.classList.add(className) + ); + } + + if (attributes) { + for (const [key, value] of Object.entries(attributes)) { + el.setAttribute(key, value); + } + } + }); + }, + [attributes, classNames] + ); + + useEffect(() => { + if (elements.length > 0) setClassNameAndAttributes(elements); + }, [elements, setClassNameAndAttributes]); +}; + +export default useAddPrismClassAttr; |
