1
0
Fork 0

shared components and utils for sanity pages

This commit is contained in:
Enoch Riese 2023-06-19 16:27:13 -05:00
parent 39655daf6a
commit e9d21ddd62
11 changed files with 350 additions and 136 deletions

View file

@ -27,6 +27,7 @@ module.exports = {
extends: 'eslint:recommended',
env: {
es2021: true,
node: true,
},
// Required when using experimental EcmaScript features
parser: '@babel/eslint-parser',

View file

@ -0,0 +1,43 @@
import { SanityMdxWrapper } from './mdx-wrapper.mjs'
import { useTranslation } from 'next-i18next'
export const Author = ({ author = {} }) => {
const { t } = useTranslation(['posts'])
return (
<div id="author" className="flex flex-col lg:flex-row m-auto p-2 items-center">
<div className="theme-gradient w-40 h-40 p-2 rounded-full aspect-square hidden lg:block">
<div
className={`
w-lg bg-cover bg-center rounded-full aspect-square
hidden lg:block
`}
style={{ backgroundImage: `url(${author.image})` }}
></div>
</div>
<div className="theme-gradient p-2 rounded-full aspect-square w-40 h-40 lg:hidden m-auto">
<img
className={`block w-full h-full mx-auto rounded-full`}
src={author.image}
alt={author.displayname}
/>
</div>
<div
className={`
text-center p-2 px-4 rounded-r-lg bg-opacity-50
lg:text-left
`}
>
<p
className="text-xl"
dangerouslySetInnerHTML={{
__html: t('xMadeThis', { x: author.displayname }),
}}
/>
<div className="prose mdx">
<SanityMdxWrapper MDX={author.about} />
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,29 @@
import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime' // Production.
import { useState, useEffect } from 'react'
import { PlainMdxWrapper } from 'shared/components/wrappers/mdx.mjs'
export const useEvaledMdx = (mdxStr = '') => {
const [mdxModule, setMdxModule] = useState(false)
useEffect(() => {
;(async () => {
const code = await compile(mdxStr, {
outputFormat: 'function-body',
development: false,
})
const evaled = await run(code, runtime)
setMdxModule(() => evaled.default)
})()
}, [mdxStr])
return mdxModule
}
export const MdxEvalWrapper = ({ MDX = false, components = {}, site = 'org' }) => {
const evaled = useEvaledMdx(MDX)
return <PlainMdxWrapper {...{ MDX: evaled, components, site }} />
}
export const SanityMdxWrapper = MdxEvalWrapper

View file

@ -0,0 +1,80 @@
import Head from 'next/head'
import { PageLink } from 'shared/components/page-link.mjs'
import { Lightbox } from 'shared/components/lightbox.mjs'
import { ImageWrapper } from 'shared/components/wrappers/img.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Author } from './author.mjs'
import { TimeAgo } from 'shared/components/wrappers/mdx.mjs'
import { SanityMdxWrapper } from './mdx-wrapper.mjs'
import { useTranslation } from 'next-i18next'
export const ns = ['common', 'posts', ...pageNs]
export const SanityPageWrapper = ({
post = {},
author = {},
page = {},
namespaces = ['common'],
}) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper title={post.title} {...page}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={post.intro || post.title} key="description" />
<meta property="og:article:author" content={author.displayname} key="author" />
<meta property="og:url" content={`https://freesewing.org/blog/${post.slug}`} key="url" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/blog/${post.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.org" key="site" />
</Head>
<article className="mb-12 px-8 max-w-7xl">
<div className="flex flex-row justify-between text-sm mb-1 mt-2">
<div>
<TimeAgo date={post.date} t={t} /> [{post.date}]
</div>
<div>
{post.designs?.map((design) => (
<PageLink
href={`/showcase/designs/${design}`}
txt={design}
key={design}
className="px-2 capitalize"
/>
))}
</div>
<div>
By{' '}
<a href="#maker" className="text-secondary hover:text-secondary-focus">
{author.displayname || 'FIXME: No displayname'}
</a>
</div>
</div>
<figure>
<Lightbox>
<ImageWrapper>
<img src={post.image} alt={post.caption} className="shadow m-auto" />
</ImageWrapper>
<figcaption
className="text-center mb-8 prose m-auto"
dangerouslySetInnerHTML={{ __html: post.caption }}
/>
</Lightbox>
</figure>
<div className="strapi prose lg:prose-lg mb-12 m-auto">
<SanityMdxWrapper MDX={post.body} />
</div>
<div className="max-w-prose text-lg lg:text-xl">
<Author author={author} />
</div>
</article>
</PageWrapper>
)
}

View file

@ -3,7 +3,7 @@ import { createClient, groq } from 'next-sanity'
const sanityId = process.env.SANITY_PROJECT || 'hl5bw8cj'
let sanityClient
export const sanityLoader = ({ query, language, type, slug }) => {
export const sanityLoader = ({ query, language, type, slug, order }) => {
sanityClient =
sanityClient ||
createClient({
@ -20,6 +20,10 @@ export const sanityLoader = ({ query, language, type, slug }) => {
query += ']'
}
if (order) {
query += ` | order(${order})`
}
return sanityClient.fetch(query)
}

View file

@ -1,113 +1,12 @@
import { useState, useEffect } from 'react'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { PlainMdxWrapper, TimeAgo } from 'shared/components/wrappers/mdx.mjs'
import ReactMarkdown from 'react-markdown'
import Head from 'next/head'
import { Lightbox } from 'shared/components/lightbox.mjs'
import { ImageWrapper } from 'shared/components/wrappers/img.mjs'
import { SanityPageWrapper, ns as sanityNs } from 'site/components/sanity/page-wrapper.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
import { useTranslation } from 'next-i18next'
import { sanityLoader, sanityImage } from 'shared/strapi/loader.js'
import { strapiImage } from 'shared/utils.mjs'
const strapi = 'https://posts.freesewing.org'
const namespaces = [...sanityNs]
const Author = ({ author }) => {
return (
<div id="author" className="flex flex-col lg:flex-row m-auto p-2 items-center">
<div className="theme-gradient w-40 h-40 p-2 rounded-full aspect-square hidden lg:block">
<div
className={`
w-lg bg-cover bg-center rounded-full aspect-square
hidden lg:block
`}
style={{ backgroundImage: `url(${strapi}${author?.image?.sizes?.small?.url})` }}
></div>
</div>
<div className="theme-gradient p-2 rounded-full aspect-square w-40 h-40 lg:hidden m-auto">
<img
className={`block w-full h-full mx-auto rounded-full`}
src={`${strapi}${author?.image?.sizes.small.url}`}
alt={author?.displayname}
width={author?.image?.sizes.small.w}
height={author?.image?.sizes.small.h}
/>
</div>
<div
className={`
text-center p-2 px-4 rounded-r-lg bg-opacity-50
lg:text-left
`}
>
<p className="text-xl">
<span className="font-semibold"> {author?.displayname}</span>
<span className="text-sm pl-2 opacity-70">Wrote this</span>
</p>
<div className="prose mdx">
<PlainMdxWrapper compile includeMeta={false}>
{author.about}
</PlainMdxWrapper>
</div>
</div>
</div>
)
}
const BlogPostPage = ({ post, author }) => {
const { t } = useTranslation(['common'])
return (
<PageWrapper title={post?.title}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={post.intro || post.title} key="description" />
<meta property="og:article:author" content={author.displayname} key="author" />
<meta property="og:url" content={`https://freesewing.org/blog/${post.slug}`} key="url" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/blog/${post.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
</Head>
<article className="mb-12">
<div className="flex flex-row justify-between text-sm mb-1 mt-2">
<span>
<TimeAgo date={post.date} t={t} /> [{post.date}]
</span>
<span>
By{' '}
<a href="#author" className="text-secondary hover:text-secondary-focus">
{author.displayname || 'FIXME: No displayname'}
</a>
</span>
</div>
<figure>
<Lightbox>
<ImageWrapper>
<img src={post.image} alt={post.caption} className="shadow m-auto" />
</ImageWrapper>
<figcaption
className="text-center mb-8 prose m-auto mt-1"
dangerouslySetInnerHTML={{ __html: post.caption }}
/>
</Lightbox>
</figure>
<div className="strapi prose lg:prose-lg mb-12 m-auto">
<PlainMdxWrapper compile includeMeta={false}>
{post.body}
</PlainMdxWrapper>
</div>
<div className="max-w-prose text-lg lg:text-xl">
<Author author={author} />
</div>
</article>
</PageWrapper>
)
const BlogPostPage = (props) => {
return <SanityPageWrapper {...props} namespaces={namespaces} />
}
/*
@ -122,7 +21,7 @@ const BlogPostPage = ({ post, author }) => {
*/
export async function getStaticProps({ params, locale }) {
const { slug } = params
const post = await sanityLoader({ type: 'blog', language: locale, slug, withImage: true })
const post = await sanityLoader({ type: 'blog', language: locale, slug })
.then((data) => data[0])
.catch((err) => console.log(err))
@ -144,7 +43,7 @@ export async function getStaticProps({ params, locale }) {
// image: strapiImage(post.author.picture, ['small']),
// about: post.author.about,
},
...(await serverSideTranslations(locale)),
...(await serverSideTranslations(locale, namespaces)),
},
}
}

View file

@ -1,6 +1,6 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } from 'shared/sanity.mjs'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components

View file

@ -0,0 +1,72 @@
import { SanityPageWrapper, ns as sanityNs } from 'site/components/sanity/page-wrapper.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
import { useTranslation } from 'next-i18next'
const namespaces = [...sanityNs]
const ShowcasePage = (props) => {
return <SanityPageWrapper {...props} namespaces={namespaces} />
}
/*
* getStaticProps() is used to fetch data at build-time.
*
* On this page, it is loading the showcase content from strapi.
*
* This, in combination with getStaticPaths() below means this
* page will be used to render/generate all showcase content.
*
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
*/
export async function getStaticProps({ params, locale }) {
const { slug } = params
const post = await sanityLoader({ type: 'showcase', language: locale, slug })
.then((data) => data[0])
.catch((err) => console.log(err))
const designs = [post.design1 || null]
if (post.design2 && post.design2.length > 2) designs.push(post.design2)
if (post.design3 && post.design3.length > 2) designs.push(post.design3)
return {
props: {
post: {
slug,
body: post.body,
title: post.title,
date: post.date,
caption: post.caption,
image: sanityImage(post.image[0]),
designs,
},
// FIXME load the author separately
author: {
displayname: post.maker,
// slug: post.maker.slug,
// image: strapiImage(post.maker.picture, ['small']),
// ...(await mdxCompiler(post.maker.about)),
},
...(await serverSideTranslations(locale, namespaces)),
},
}
}
export const getStaticPaths = async () => {
const paths = await sanityLoader({ language: 'en', type: 'showcase' })
.then((data) => data.map((post) => `/showcase/${post.slug.current}`))
.catch((err) => console.log(err))
return {
paths: [
...paths,
...paths.map((p) => `/de${p}`),
...paths.map((p) => `/es${p}`),
...paths.map((p) => `/fr${p}`),
...paths.map((p) => `/nl${p}`),
],
fallback: false,
}
}
export default ShowcasePage

View file

@ -1,34 +1,99 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
import { useMemo } from 'react'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { V3Wip } from 'shared/components/v3-wip.mjs'
import Link from 'next/link'
import { TimeAgo } from 'shared/components/wrappers/mdx.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['showcase', ...pageNs])]
const namespaces = [...new Set(['common', 'designs', ...pageNs])]
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const DesignsPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
export const PreviewTile = ({ img, slug, title }) => (
<Link href={`/showcase/${slug}`} className="text-center">
<span
style={{ backgroundImage: `url(${img})`, backgroundSize: 'cover' }}
className={`
rounded-full inline-block border-base-100
w-40 h-40
md:w-56 md:h-56
`}
></span>
<p>{title}</p>
</Link>
)
export default DesignsPage
// const DesignPosts = ({ design, posts }) => {
// const { t } = useTranslation(['patterns'])
// return (
// <div className='py-2'>
// <h2>
// <Link href={`/showcase/designs/${design}`}>
// <a className="hover:text-secondary-focus hover:underline">{t(`${design}.t`)}</a>
// </Link>
// </h2>
// </div>
// )
// }
// FIXME paginate
const Posts = ({ posts }) => (
<div className="grid grid-cols-1 gap-4 xl:gap-8 lg:grid-cols-2 xl:grid-cols-3 lg:pr-4 xl:pr-8">
{posts.map((post) => (
<PreviewTile img={post.image} slug={post.slug} title={post.title} key={post.slug} />
))}
</div>
)
const ShowcaseIndexPage = (props) => {
const { t } = useTranslation()
const { posts } = props
// const designKeys = useMemo(() => Object.keys(designs).sort(), [designs])
return (
<PageWrapper title={t('showcase')} {...props.page}>
<Posts posts={posts} />
</PageWrapper>
)
}
export default ShowcaseIndexPage
export async function getStaticProps({ locale }) {
const posts = await sanityLoader({
language: locale,
type: 'showcase',
order: 'date desc',
}).catch((err) => console.log(err))
const designs = {}
const propPosts = []
posts.forEach((post) => {
// for (const design of post.designs) {
// if (typeof designs[design] === 'undefined') designs[design] = []
// designs[design].push(post)
// }
propPosts.push({
slug: post.slug.current,
title: post.title,
date: post.date,
// FIXME get the authors separately
author: post.maker,
image: sanityImage(post.image[0]) + '?fit=clip&w=400',
})
})
return {
props: {
posts: propPosts,
designs,
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
// title: 'Freesewing Blog',
path: ['showcase'],
},
},

View file

@ -102,20 +102,20 @@ const MetaData = ({ authors = [], maintainers = [], updated = '20220825', locale
</div>
)
export const PlainMdxWrapper = ({ MDX = false, components = {}, compile, children }) => {
const allComponents = { ...baseComponents, ...components }
const compiledMdx = MDX ? (
<MDX components={allComponents} />
) : compile ? (
<ReactMarkdown components={allComponents}>{children}</ReactMarkdown>
) : (
children
)
export const PlainMdxWrapper = ({ MDX = false, components = {}, children, site = 'org' }) => {
const allComponents = { ...baseComponents(site), ...components }
const CompiledMdx = MDX ? <MDX components={allComponents} /> : children || ''
return <div className="searchme">{compiledMdx}</div>
return <div className="searchme">{MDX ? <MDX components={allComponents} /> : children}</div>
}
export const MdxWrapper = ({ MDX = false, frontmatter = {}, components = {}, children = [] }) => {
export const MdxWrapper = ({
MDX = false,
frontmatter = {},
components = {},
children = [],
site = 'org',
}) => {
const { t } = useTranslation('docs')
const { locale, slug } = useContext(NavigationContext)

View file

@ -0,0 +1,21 @@
import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime' // Production.
import { useState, useEffect, Fragment } from 'react'
export const useEvaledMdx = (mdxStr = '') => {
const [mdxModule, setMdxModule] = useState(false)
useEffect(() => {
;(async () => {
const code = await compile(mdxStr, {
outputFormat: 'function-body',
development: false,
})
const evaled = await run(code, runtime)
setMdxModule(() => evaled.default)
})()
}, [mdxStr])
return mdxModule
}