Merge branch 'develop' into joost
This commit is contained in:
commit
7f7ceeb0e2
21 changed files with 1058 additions and 470 deletions
|
@ -27,6 +27,7 @@ module.exports = {
|
|||
extends: 'eslint:recommended',
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
// Required when using experimental EcmaScript features
|
||||
parser: '@babel/eslint-parser',
|
||||
|
|
|
@ -192,12 +192,12 @@ yuri:
|
|||
|
||||
backend:
|
||||
_:
|
||||
'@aws-sdk/client-sesv2': '3.352.0'
|
||||
'@aws-sdk/client-sesv2': '3.354.0'
|
||||
'@prisma/client': &prisma '4.15.0'
|
||||
'bcryptjs': '2.4.3'
|
||||
'cors': '2.8.5'
|
||||
'crypto': '1.0.1'
|
||||
'dotenv': '16.1.4'
|
||||
'dotenv': '16.3.1'
|
||||
'express': '4.18.2'
|
||||
'js-yaml': *jsyaml
|
||||
'lodash.get': *_get
|
||||
|
@ -213,7 +213,7 @@ backend:
|
|||
dev:
|
||||
'chai': *chai
|
||||
'chai-http': '4.4.0'
|
||||
'esbuild': '0.18.2'
|
||||
'esbuild': '0.18.5'
|
||||
'mocha': *mocha
|
||||
'mocha-steps': '1.3.0'
|
||||
'nodemon': '2.0.22'
|
||||
|
@ -228,7 +228,7 @@ dev:
|
|||
'@next/bundle-analyzer': &next '13.4.6'
|
||||
'@tailwindcss/typography': &tailwindTypography '0.5.9'
|
||||
'algoliasearch': '4.17.2'
|
||||
'daisyui': &daisyui '3.1.0'
|
||||
'daisyui': &daisyui '3.1.1'
|
||||
'lodash.get': *_get
|
||||
'lodash.orderby': &_orderby '4.6.0'
|
||||
'lodash.set': *_set
|
||||
|
@ -238,7 +238,7 @@ dev:
|
|||
'react-dom': *react
|
||||
'react-hotkeys-hook': &reactHotkeysHook '4.4.0'
|
||||
'react-instantsearch-dom': &reactInstantsearchDom '6.40.0'
|
||||
'react-instantsearch-hooks-web': '6.44.2'
|
||||
'react-instantsearch-hooks-web': '6.44.3'
|
||||
'react-markdown': &reactMarkdown '8.0.7'
|
||||
'react-swipeable': &reactSwipeable '7.0.1'
|
||||
'react-timeago': &reactTimeago '7.1.0'
|
||||
|
@ -314,6 +314,8 @@ org:
|
|||
'@mdx-js/mdx': *mdx
|
||||
'@mdx-js/react': *mdx
|
||||
'@mdx-js/runtime': *mdxRuntime
|
||||
'@portabletext/react': '^1.0.6'
|
||||
'@sanity/client': '^6.1.2'
|
||||
'@tailwindcss/typography': *tailwindTypography
|
||||
'algoliasearch': *algoliasearch
|
||||
'react-copy-to-clipboard': 5.1.0
|
||||
|
@ -323,6 +325,7 @@ org:
|
|||
'lodash.set': *_set
|
||||
'luxon': '3.3.0'
|
||||
'next': *next
|
||||
'next-sanity': '^4.3.3'
|
||||
'react-dropzone': '14.2.3'
|
||||
'react-hotkeys-hook': *reactHotkeysHook
|
||||
'react-instantsearch-dom': *reactInstantsearchDom
|
||||
|
@ -344,8 +347,8 @@ org:
|
|||
|
||||
sanity:
|
||||
_:
|
||||
'@sanity/vision': &sanity '3.12.0'
|
||||
'easymde': '2.16.0'
|
||||
'@sanity/vision': &sanity '3.12.1'
|
||||
'easymde': '2.18.0'
|
||||
'react': *react
|
||||
'react-dom': *react
|
||||
'react-is': *react
|
||||
|
|
|
@ -28,12 +28,12 @@
|
|||
},
|
||||
"peerDependencies": {},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sesv2": "3.352.0",
|
||||
"@aws-sdk/client-sesv2": "3.354.0",
|
||||
"@prisma/client": "4.15.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cors": "2.8.5",
|
||||
"crypto": "1.0.1",
|
||||
"dotenv": "16.1.4",
|
||||
"dotenv": "16.3.1",
|
||||
"express": "4.18.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash.get": "4.4.2",
|
||||
|
@ -50,7 +50,7 @@
|
|||
"devDependencies": {
|
||||
"chai": "4.3.7",
|
||||
"chai-http": "4.4.0",
|
||||
"esbuild": "0.18.2",
|
||||
"esbuild": "0.18.5",
|
||||
"mocha": "10.2.0",
|
||||
"mocha-steps": "1.3.0",
|
||||
"nodemon": "2.0.22",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"@next/bundle-analyzer": "13.4.6",
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"algoliasearch": "4.17.2",
|
||||
"daisyui": "3.1.0",
|
||||
"daisyui": "3.1.1",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.orderby": "4.6.0",
|
||||
"lodash.set": "4.3.2",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-hotkeys-hook": "4.4.0",
|
||||
"react-instantsearch-dom": "6.40.0",
|
||||
"react-instantsearch-hooks-web": "6.44.2",
|
||||
"react-instantsearch-hooks-web": "6.44.3",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-swipeable": "7.0.1",
|
||||
"react-timeago": "7.1.0",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"d3-dispatch": "3.0.1",
|
||||
"d3-drag": "3.0.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"daisyui": "3.1.0",
|
||||
"daisyui": "3.1.1",
|
||||
"i18next": "22.5.1",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.orderby": "4.6.0",
|
||||
|
|
43
sites/org/components/sanity/author.mjs
Normal file
43
sites/org/components/sanity/author.mjs
Normal 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>
|
||||
)
|
||||
}
|
30
sites/org/components/sanity/mdx-wrapper.mjs
Normal file
30
sites/org/components/sanity/mdx-wrapper.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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(() => {
|
||||
const runEffect = async () => {
|
||||
const code = await compile(mdxStr, {
|
||||
outputFormat: 'function-body',
|
||||
development: false,
|
||||
})
|
||||
const evaled = await run(code, runtime)
|
||||
setMdxModule(() => evaled.default)
|
||||
}
|
||||
runEffect()
|
||||
}, [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
|
80
sites/org/components/sanity/page-wrapper.mjs
Normal file
80
sites/org/components/sanity/page-wrapper.mjs
Normal 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>
|
||||
)
|
||||
}
|
32
sites/org/components/sanity/utils.mjs
Normal file
32
sites/org/components/sanity/utils.mjs
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createClient } from 'next-sanity'
|
||||
import { siteConfig } from 'site/site.config.mjs'
|
||||
|
||||
let sanityClient
|
||||
export const sanityLoader = ({ query, language, type, slug, order }) => {
|
||||
sanityClient =
|
||||
sanityClient ||
|
||||
createClient({
|
||||
projectId: 'hl5bw8cj',
|
||||
dataset: 'site-content',
|
||||
apiVersion: '2023-06-17',
|
||||
token: process.env.SANITY_TOKEN,
|
||||
useCdn: false,
|
||||
})
|
||||
|
||||
if (!query) {
|
||||
query = `*[_type == "${type}${language}"`
|
||||
if (slug) query += ` && slug.current == "${slug}"`
|
||||
query += ']'
|
||||
}
|
||||
|
||||
if (order) {
|
||||
query += ` | order(${order})`
|
||||
}
|
||||
|
||||
return sanityClient.fetch(query)
|
||||
}
|
||||
|
||||
export const sanityImage = (image, dataset = 'site-content') => {
|
||||
const [, assetName, origSize, format] = image.asset._ref.split('-')
|
||||
return `https://cdn.sanity.io/images/${siteConfig.sanity.projectt}/${dataset}/${assetName}-${origSize}.${format}`
|
||||
}
|
|
@ -34,15 +34,18 @@
|
|||
"@mdx-js/mdx": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@mdx-js/runtime": "2.0.0-next.9",
|
||||
"@portabletext/react": "^1.0.6",
|
||||
"@sanity/client": "^6.1.2",
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"algoliasearch": "4.17.2",
|
||||
"react-copy-to-clipboard": "5.1.0",
|
||||
"daisyui": "3.1.0",
|
||||
"daisyui": "3.1.1",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.orderby": "4.6.0",
|
||||
"lodash.set": "4.3.2",
|
||||
"luxon": "3.3.0",
|
||||
"next": "13.4.6",
|
||||
"next-sanity": "^4.3.3",
|
||||
"react-dropzone": "14.2.3",
|
||||
"react-hotkeys-hook": "4.4.0",
|
||||
"react-instantsearch-dom": "6.40.0",
|
||||
|
|
67
sites/org/pages/blog/[slug].mjs
Normal file
67
sites/org/pages/blog/[slug].mjs
Normal file
|
@ -0,0 +1,67 @@
|
|||
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'
|
||||
|
||||
const namespaces = [...sanityNs]
|
||||
|
||||
const BlogPostPage = (props) => {
|
||||
return <SanityPageWrapper {...props} namespaces={namespaces} />
|
||||
}
|
||||
|
||||
/*
|
||||
* getStaticProps() is used to fetch data at build-time.
|
||||
*
|
||||
* On this page, it is loading the blog content from strapi.
|
||||
*
|
||||
* This, in combination with getStaticPaths() below means this
|
||||
* page will be used to render/generate all blog 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: 'blog', language: locale, slug })
|
||||
.then((data) => data[0])
|
||||
.catch((err) => console.log(err))
|
||||
|
||||
return {
|
||||
props: {
|
||||
post: {
|
||||
slug,
|
||||
body: post.body,
|
||||
title: post.title,
|
||||
date: post.date,
|
||||
caption: post.caption,
|
||||
image: sanityImage(post.image),
|
||||
},
|
||||
// FIXME load the author separately
|
||||
author: {
|
||||
displayname: post.author,
|
||||
// slug: post.author.slug,
|
||||
// about: post.author.about,
|
||||
// image: strapiImage(post.author.picture, ['small']),
|
||||
// about: post.author.about,
|
||||
},
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const paths = await sanityLoader({ language: 'en', type: 'blog' })
|
||||
.then((data) => data.map((post) => `/blog/${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 BlogPostPage
|
|
@ -1,34 +1,111 @@
|
|||
// Dependencies
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { TimeAgo } from 'shared/components/wrappers/mdx.mjs'
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { V3Wip } from 'shared/components/v3-wip.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set(['designs', ...pageNs])]
|
||||
|
||||
const textShadow = {
|
||||
style: {
|
||||
textShadow:
|
||||
'1px 1px 1px #000000, -1px -1px 1px #000000, 1px -1px 1px #000000, -1px 1px 1px #000000, 2px 2px 1px #000000',
|
||||
},
|
||||
}
|
||||
|
||||
const Preview = ({ post, t }) => (
|
||||
<div className="shadow rounded-lg">
|
||||
<Link href={`blog/${post.slug}`} className="hover:underline">
|
||||
<div
|
||||
className="bg-base-100 w-full h-full overflow-hidden shadow flex flex-column items-center rounded-lg"
|
||||
style={{
|
||||
backgroundImage: `url(${post.image})`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
>
|
||||
<div className="text-right my-2 w-full">
|
||||
<div
|
||||
className={`
|
||||
bg-neutral text-neutral-content bg-opacity-40 text-right
|
||||
px-4 py-1
|
||||
lg:px-8 lg:py-4
|
||||
|
||||
`}
|
||||
>
|
||||
<h5
|
||||
className={`
|
||||
text-neutral-content
|
||||
text-xl font-bold
|
||||
md:text-2xl md:font-normal
|
||||
xl:text-3xl
|
||||
`}
|
||||
{...textShadow}
|
||||
>
|
||||
{post.title}
|
||||
</h5>
|
||||
<p
|
||||
className={`
|
||||
hidden md:block
|
||||
m-0 p-1 -mt-2
|
||||
text-neutral-content
|
||||
leading-normal text-sm font-normal
|
||||
opacity-70
|
||||
`}
|
||||
{...textShadow}
|
||||
>
|
||||
<TimeAgo date={post.date} t={t} /> by <strong>{post.author}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
* 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 BlogIndexPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<div className="max-w-2xl">
|
||||
<V3Wip />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
const BlogIndexPage = ({ page, posts }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PageWrapper {...page}>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 max-w-7xl lg:pr-4 xl:pr-6">
|
||||
{posts.map((post) => (
|
||||
<Preview post={post} t={t} key={post.slug} />
|
||||
))}
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogIndexPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
const posts = await sanityLoader({ language: locale, type: 'blog' }).catch((err) =>
|
||||
console.log(err)
|
||||
)
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts: posts.map((post) => ({
|
||||
slug: post.slug.current,
|
||||
title: post.title,
|
||||
date: post.date,
|
||||
// FIXME get the authors separately
|
||||
author: post.author,
|
||||
image: sanityImage(post.image) + '?fit=clip&w=400',
|
||||
})),
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
page: {
|
||||
locale,
|
||||
// title: 'Freesewing Blog',
|
||||
path: ['blog'],
|
||||
},
|
||||
},
|
||||
|
|
71
sites/org/pages/showcase/[slug].mjs
Normal file
71
sites/org/pages/showcase/[slug].mjs
Normal file
|
@ -0,0 +1,71 @@
|
|||
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'
|
||||
|
||||
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
|
|
@ -1,34 +1,97 @@
|
|||
// Dependencies
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
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'
|
||||
|
||||
// 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'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,6 +7,9 @@ export const siteConfig = {
|
|||
bugsnag: {
|
||||
key: '1b3a900d6ebbfd071975e39b534e1ff5',
|
||||
},
|
||||
sanity: {
|
||||
project: process.env.SANITY_PROJECT || 'hl5bw8cj',
|
||||
},
|
||||
languages: ['en', 'es', 'de', 'fr', 'nl'],
|
||||
site: 'FreeSewing.org',
|
||||
}
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
},
|
||||
"peerDependencies": {},
|
||||
"dependencies": {
|
||||
"@sanity/vision": "3.12.0",
|
||||
"easymde": "2.16.0",
|
||||
"@sanity/vision": "3.12.1",
|
||||
"easymde": "2.18.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-is": "18.2.0",
|
||||
"sanity": "3.12.0",
|
||||
"sanity": "3.12.1",
|
||||
"styled-components": "5.3.11",
|
||||
"sanity-plugin-markdown": "4.1.0"
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next'
|
|||
//import { PrevNext } from '../mdx/prev-next.mjs'
|
||||
//
|
||||
//
|
||||
const TimeAgo = ({ date, t }) => {
|
||||
export const TimeAgo = ({ date, t }) => {
|
||||
const i = Interval.fromDateTimes(DateTime.fromISO(date), DateTime.now())
|
||||
.toDuration(['hours', 'days', 'months', 'years'])
|
||||
.toObject()
|
||||
|
@ -101,9 +101,21 @@ const MetaData = ({ authors = [], maintainers = [], updated = '20220825', locale
|
|||
</div>
|
||||
)
|
||||
|
||||
export const MdxWrapper = ({ MDX = false, frontmatter = {}, components = {}, children = [] }) => {
|
||||
export const PlainMdxWrapper = ({ MDX = false, components = {}, children, site = 'org' }) => {
|
||||
const allComponents = { ...baseComponents(site), ...components }
|
||||
|
||||
return <div className="searchme">{MDX ? <MDX components={allComponents} /> : children}</div>
|
||||
}
|
||||
|
||||
export const MdxWrapper = ({
|
||||
MDX = false,
|
||||
frontmatter = {},
|
||||
components = {},
|
||||
children = [],
|
||||
site = 'org',
|
||||
}) => {
|
||||
const { t } = useTranslation('docs')
|
||||
const allComponents = { ...baseComponents, ...components }
|
||||
|
||||
const { locale, slug } = useContext(NavigationContext)
|
||||
|
||||
const updates = docUpdates[slug] || {}
|
||||
|
@ -116,7 +128,7 @@ export const MdxWrapper = ({ MDX = false, frontmatter = {}, components = {}, chi
|
|||
updated={updates.u}
|
||||
{...{ locale, slug, t }}
|
||||
/>
|
||||
<div className="searchme">{MDX ? <MDX components={allComponents} /> : children}</div>
|
||||
<PlainMdxWrapper {...{ MDX, components, children }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, { useState, useEffect, useContext } from 'react'
|
|||
// Hooks
|
||||
import { useTheme } from 'shared/hooks/use-theme.mjs'
|
||||
// Components
|
||||
import Head from 'next/head'
|
||||
import { SwipeWrapper } from 'shared/components/wrappers/swipes.mjs'
|
||||
import { LayoutWrapper, ns as layoutNs } from 'site/components/wrappers/layout.mjs'
|
||||
import { DocsLayout, ns as docsNs } from 'site/components/layouts/docs.mjs'
|
||||
|
@ -80,6 +81,11 @@ export const PageWrapper = (props) => {
|
|||
// Return wrapper
|
||||
return (
|
||||
<SwipeWrapper>
|
||||
{pageTitle && (
|
||||
<Head>
|
||||
<meta property="og:title" content={pageTitle} key="title" />
|
||||
</Head>
|
||||
)}
|
||||
<div
|
||||
data-theme={currentTheme} // This facilitates CSS selectors
|
||||
key={currentTheme} // This forces the data-theme update
|
||||
|
|
21
sites/shared/hooks/use-evaled-mdx.mjs
Normal file
21
sites/shared/hooks/use-evaled-mdx.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { compile, run } from '@mdx-js/mdx'
|
||||
import * as runtime from 'react/jsx-runtime' // Production.
|
||||
|
||||
import { useState, useEffect } 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
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
"d3-dispatch": "3.0.1",
|
||||
"d3-drag": "3.0.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"daisyui": "3.1.0",
|
||||
"daisyui": "3.1.1",
|
||||
"feed": "4.2.2",
|
||||
"file-saver": "2.0.5",
|
||||
"front-matter": "4.0.2",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue