diff --git a/src/css/custom.scss b/src/css/custom.scss index 96e6c90af..cc11e98ca 100644 --- a/src/css/custom.scss +++ b/src/css/custom.scss @@ -149,6 +149,29 @@ html { } } +//warning msg for older blogs +.warning { + background-color: var(--ifm-color-primary-lightest); + border: 1px solid var(--ifm-color-primary-light); + color: var(--ifm-color-primary-darkest); + padding: 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + box-shadow: var(--ifm-global-shadow-md); + font-size: 0.95rem; + line-height: 1.6; + + strong { + color: var(--electron-color-secondary-light); + } + + @media (prefers-color-scheme: dark) { + background-color: var(--ifm-color-primary-darker); + border-color: var(--ifm-color-primary-light); + color: var(--ifm-color-primary-lightest); + } +} + .footer { &--dark { --ifm-footer-background-color: --var(--electron-color-dark); diff --git a/src/theme/BlogPostItem/Container/index.tsx b/src/theme/BlogPostItem/Container/index.tsx new file mode 100644 index 000000000..5b09a03db --- /dev/null +++ b/src/theme/BlogPostItem/Container/index.tsx @@ -0,0 +1,9 @@ +import React, { type ReactNode } from 'react'; +import type { Props } from '@theme/BlogPostItem/Container'; + +export default function BlogPostItemContainer({ + children, + className, +}: Props): ReactNode { + return
{children}
; +} diff --git a/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx new file mode 100644 index 000000000..86a962e9e --- /dev/null +++ b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx @@ -0,0 +1,39 @@ +import React, { type ReactNode } from 'react'; +import Translate, { translate } from '@docusaurus/Translate'; +import Link from '@docusaurus/Link'; +import type { Props } from '@theme/BlogPostItem/Footer/ReadMoreLink'; + +function ReadMoreLabel() { + return ( + + + Read more + + + ); +} + +export default function BlogPostItemFooterReadMoreLink( + props: Props, +): ReactNode { + const { blogPostTitle, ...linkProps } = props; + return ( + + + + ); +} diff --git a/src/theme/BlogPostItem/Footer/index.tsx b/src/theme/BlogPostItem/Footer/index.tsx new file mode 100644 index 000000000..efdf623d1 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/index.tsx @@ -0,0 +1,85 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import EditMetaRow from '@theme/EditMetaRow'; +import TagsListInline from '@theme/TagsListInline'; +import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink'; + +export default function BlogPostItemFooter(): ReactNode { + const { metadata, isBlogPostPage } = useBlogPost(); + const { + tags, + title, + editUrl, + hasTruncateMarker, + lastUpdatedBy, + lastUpdatedAt, + } = metadata; + + // A post is truncated if it's in the "list view" and it has a truncate marker + const truncatedPost = !isBlogPostPage && hasTruncateMarker; + + const tagsExists = tags.length > 0; + + const renderFooter = tagsExists || truncatedPost || editUrl; + + if (!renderFooter) { + return null; + } + + // BlogPost footer - details view + if (isBlogPostPage) { + const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + + return ( + + ); + } + // BlogPost footer - list view + else { + return ( + + ); + } +} diff --git a/src/theme/BlogPostItem/Header/Authors/index.tsx b/src/theme/BlogPostItem/Header/Authors/index.tsx new file mode 100644 index 000000000..13ebccd8d --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/index.tsx @@ -0,0 +1,49 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import BlogAuthor from '@theme/Blog/Components/Author'; +import type { Props } from '@theme/BlogPostItem/Header/Authors'; +import styles from './styles.module.css'; + +// Component responsible for the authors layout +export default function BlogPostItemHeaderAuthors({ + className, +}: Props): ReactNode { + const { + metadata: { authors }, + assets, + } = useBlogPost(); + const authorsCount = authors.length; + if (authorsCount === 0) { + return null; + } + const imageOnly = authors.every(({ name }) => !name); + const singleAuthor = authors.length === 1; + return ( +
+ {authors.map((author, idx) => ( +
+ +
+ ))} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Authors/styles.module.css b/src/theme/BlogPostItem/Header/Authors/styles.module.css new file mode 100644 index 000000000..d80c57914 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/styles.module.css @@ -0,0 +1,13 @@ +.authorCol { + max-width: inherit !important; +} + +.imageOnlyAuthorRow { + display: flex; + flex-flow: row wrap; +} + +.imageOnlyAuthorCol { + margin-left: 0.3rem; + margin-right: 0.3rem; +} diff --git a/src/theme/BlogPostItem/Header/Info/index.tsx b/src/theme/BlogPostItem/Header/Info/index.tsx new file mode 100644 index 000000000..ca2512409 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/index.tsx @@ -0,0 +1,77 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import { translate } from '@docusaurus/Translate'; +import { usePluralForm } from '@docusaurus/theme-common'; +import { useDateTimeFormat } from '@docusaurus/theme-common/internal'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import type { Props } from '@theme/BlogPostItem/Header/Info'; + +import styles from './styles.module.css'; + +// Very simple pluralization: probably good enough for now +function useReadingTimePlural() { + const { selectMessage } = usePluralForm(); + return (readingTimeFloat: number) => { + const readingTime = Math.ceil(readingTimeFloat); + return selectMessage( + readingTime, + translate( + { + id: 'theme.blog.post.readingTime.plurals', + description: + 'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One min read|{readingTime} min read', + }, + { readingTime }, + ), + ); + }; +} + +function ReadingTime({ readingTime }: { readingTime: number }) { + const readingTimePlural = useReadingTimePlural(); + return <>{readingTimePlural(readingTime)}; +} + +function DateTime({ + date, + formattedDate, +}: { + date: string; + formattedDate: string; +}) { + return ; +} + +function Spacer() { + return <>{' · '}; +} + +export default function BlogPostItemHeaderInfo({ + className, +}: Props): ReactNode { + const { metadata } = useBlogPost(); + const { date, readingTime } = metadata; + + const dateTimeFormat = useDateTimeFormat({ + day: 'numeric', + month: 'long', + year: 'numeric', + timeZone: 'UTC', + }); + + const formatDate = (blogDate: string) => + dateTimeFormat.format(new Date(blogDate)); + + return ( +
+ + {typeof readingTime !== 'undefined' && ( + <> + + + + )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Info/styles.module.css b/src/theme/BlogPostItem/Header/Info/styles.module.css new file mode 100644 index 000000000..27d569e08 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/styles.module.css @@ -0,0 +1,3 @@ +.container { + font-size: 0.9rem; +} diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx new file mode 100644 index 000000000..72b25bcc2 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import type { Props } from '@theme/BlogPostItem/Header/Title'; + +import styles from './styles.module.css'; + +export default function BlogPostItemHeaderTitle({ + className, +}: Props): ReactNode { + const { metadata, isBlogPostPage } = useBlogPost(); + const { permalink, title } = metadata; + const TitleHeading = isBlogPostPage ? 'h1' : 'h2'; + return ( + + {isBlogPostPage ? title : {title}} + + ); +} diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css new file mode 100644 index 000000000..04a8d6adc --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -0,0 +1,12 @@ +.title { + font-size: 3rem; +} + +/** + Blog post title should be smaller on smaller devices +**/ +@media (max-width: 576px) { + .title { + font-size: 2rem; + } +} diff --git a/src/theme/BlogPostItem/Header/index.tsx b/src/theme/BlogPostItem/Header/index.tsx new file mode 100644 index 000000000..53cc6830c --- /dev/null +++ b/src/theme/BlogPostItem/Header/index.tsx @@ -0,0 +1,14 @@ +import React, { type ReactNode } from 'react'; +import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title'; +import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info'; +import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors'; + +export default function BlogPostItemHeader(): ReactNode { + return ( +
+ + + +
+ ); +} diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx new file mode 100644 index 000000000..d8ae73712 --- /dev/null +++ b/src/theme/BlogPostItem/index.tsx @@ -0,0 +1,45 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import BlogPostItemContainer from '@theme/BlogPostItem/Container'; +import BlogPostItemHeader from '@theme/BlogPostItem/Header'; +import BlogPostItemContent from '@theme/BlogPostItem/Content'; +import BlogPostItemFooter from '@theme/BlogPostItem/Footer'; +import type { Props } from '@theme/BlogPostItem'; + +// apply a bottom margin in list view +function useContainerClassName() { + const { isBlogPostPage } = useBlogPost(); + return !isBlogPostPage ? 'margin-bottom--xl' : undefined; +} + +export default function BlogPostItem({ + children, + className, +}: Props): ReactNode { + const containerClassName = useContainerClassName(); + const { metadata } = useBlogPost(); + const blogDate = new Date(metadata.date); // Date on which blog was posted + const currentDate = new Date(); + const blogAge = + (currentDate.getTime() - blogDate.getTime()) / (1000 * 60 * 60 * 24 * 365); // converting time in miliseconds to year + const isOld = blogAge >= 1; + const yearLabel = Math.floor(blogAge) === 1 ? 'year' : 'years'; // grammer check + return ( + + {isOld && ( +
+ ⚠️ Note: This blog post is over{' '} + + {' '} + {Math.floor(blogAge)} {yearLabel} old{' '} + + . Some information may be outdated. +
+ )} + + {children} + +
+ ); +}