diff --git a/package-lock.json b/package-lock.json index 59896686..3e391c7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "cmdk": "^1.0.0", "gray-matter": "^4.0.3", "image-size": "^1.1.1", + "lodash-es": "^4.17.21", "match-sorter": "^6.3.4", "next": "^14.2.11", "next-mdx-remote": "^5.0.0", @@ -38,6 +39,7 @@ "devDependencies": { "@savvywombat/tailwindcss-grid-areas": "^4.0.0", "@types/hast": "^3.0.4", + "@types/lodash-es": "^4.17.12", "@types/minimist": "^1.2.5", "@types/node": "^18.19.50", "@types/react": "^18.3.5", @@ -1915,6 +1917,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -7558,7 +7577,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.capitalize": { diff --git a/package.json b/package.json index 5e971d97..9ca6f010 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@savvywombat/tailwindcss-grid-areas": "^4.0.0", "@types/hast": "^3.0.4", + "@types/lodash-es": "^4.17.12", "@types/minimist": "^1.2.5", "@types/node": "^18.19.50", "@types/react": "^18.3.5", @@ -41,6 +42,7 @@ "cmdk": "^1.0.0", "gray-matter": "^4.0.3", "image-size": "^1.1.1", + "lodash-es": "^4.17.21", "match-sorter": "^6.3.4", "next": "^14.2.11", "next-mdx-remote": "^5.0.0", diff --git a/src/components/mdx/Codesandbox/Codesandbox.tsx b/src/components/mdx/Codesandbox/Codesandbox.tsx index a1e626bd..34c55f62 100644 --- a/src/components/mdx/Codesandbox/Codesandbox.tsx +++ b/src/components/mdx/Codesandbox/Codesandbox.tsx @@ -1,16 +1,25 @@ import { Img } from '@/components/mdx' import cn from '@/lib/cn' +import { ComponentProps } from 'react' +import { fetchCSB } from './fetchCSB' export type CSB = { id: string - title: string - screenshot_url: string - description: string - tags: string[] + title?: string + screenshot_url?: string + description?: string + tags?: string[] } -export function Codesandbox({ +type Codesandbox0Props = CSB & { + hideTitle?: boolean + embed?: boolean +} & ComponentProps<'a'> & { + imgProps?: ComponentProps<'img'> + } + +export function Codesandbox0({ id, title = '', description = '', @@ -19,10 +28,9 @@ export function Codesandbox({ // hideTitle = false, embed = false, -}: CSB & { - hideTitle: boolean - embed: boolean -}) { + className, + imgProps: { className: imgClassName } = {}, +}: Codesandbox0Props) { return ( <> {embed ? ( @@ -38,7 +46,7 @@ export function Codesandbox({ href={`https://codesandbox.io/s/${id}`} target="_blank" rel="noreferrer" - className="mb-2 block" + className={cn('mb-2 block', className)} > {screenshot_url && ( {title} )} @@ -76,3 +84,20 @@ export function Codesandbox({ ) } + +export async function Codesandbox1({ boxes, ...props }: { boxes: string[] } & Codesandbox0Props) { + const ids = boxes // populated from 1. + // console.log('ids', ids) + + // + // Batch fetch all CSBs of the page + // + const csbs = await fetchCSB(...ids) + // console.log('boxes', boxes) + const data = csbs[props.id] + // console.log('data', data) + + // Merge initial props with data + const merged = { ...props, ...data } + return +} diff --git a/src/components/mdx/Entries/Entries.tsx b/src/components/mdx/Entries/Entries.tsx new file mode 100644 index 00000000..ae7840af --- /dev/null +++ b/src/components/mdx/Entries/Entries.tsx @@ -0,0 +1,56 @@ +import { ComponentProps } from 'react' + +import { groupBy } from 'lodash-es' +import { Codesandbox1 } from '../Codesandbox' + +type Entry = { + title: string + url: string + slug: string[] + boxes: string[] +} + +export async function Entries({ + items, + excludedGroups = ['getting-started'], + ...props +}: { items: Entry[]; excludedGroups?: string[] } & ComponentProps<'div'>) { + const groupedEntries = groupBy(items, ({ slug }) => slug[0]) + + return ( +
+ {Object.entries(groupedEntries) + .filter(([group]) => !excludedGroups.includes(group)) + .map(([group, entries]) => { + return ( + <> +

{group}

+ + + ) + })} +
+ ) +} diff --git a/src/components/mdx/Entries/index.ts b/src/components/mdx/Entries/index.ts new file mode 100644 index 00000000..031cf172 --- /dev/null +++ b/src/components/mdx/Entries/index.ts @@ -0,0 +1 @@ +export * from './Entries' diff --git a/src/components/mdx/index.tsx b/src/components/mdx/index.tsx index 7587aa38..79646017 100644 --- a/src/components/mdx/index.tsx +++ b/src/components/mdx/index.tsx @@ -1,6 +1,7 @@ export * from './Code' -export * from './Codesandbox' +// export * from './Codesandbox' export * from './Details' +export * from './Entries' export * from './Gha' export * from './Grid' export * from './Hint' diff --git a/src/utils/docs.tsx b/src/utils/docs.tsx index 212c06a4..0d77d33a 100644 --- a/src/utils/docs.tsx +++ b/src/utils/docs.tsx @@ -1,8 +1,8 @@ import type { Doc, DocToC } from '@/app/[...slug]/DocsContext' import * as components from '@/components/mdx' +import { Entries } from '@/components/mdx' import { rehypeCode } from '@/components/mdx/Code/rehypeCode' -import { Codesandbox } from '@/components/mdx/Codesandbox' -import { fetchCSB } from '@/components/mdx/Codesandbox/fetchCSB' +import { Codesandbox1 } from '@/components/mdx/Codesandbox' import { rehypeCodesandbox } from '@/components/mdx/Codesandbox/rehypeCodesandbox' import { rehypeDetails } from '@/components/mdx/Details/rehypeDetails' import { rehypeGha } from '@/components/mdx/Gha/rehypeGha' @@ -15,7 +15,7 @@ import matter from 'gray-matter' import { compileMDX } from 'next-mdx-remote/rsc' import fs from 'node:fs' import { dirname } from 'node:path' -import React, { cache } from 'react' +import { cache } from 'react' import rehypePrismPlus from 'rehype-prism-plus' import remarkGFM from 'remark-gfm' @@ -73,34 +73,18 @@ async function _getDocs( ) // console.log('files', files) - const docs = await Promise.all( - files.map(async (file) => { - const relFilePath = file.substring(root.length) // "/getting-started/tutorials/store.mdx" + // + // 1st pass for `entries` + // + const entries = await Promise.all( + files.map(async (file) => { // Get slug from local path const path = file.replace(`${root}/`, '') const slug = [...path.replace(MARKDOWN_REGEX, '').toLowerCase().split('/')] - // - // "Lightest" version of the doc (for `generateStaticParams`) - // - - if (slugOnly) { - return { slug } as Doc - } - - // - // Common infos (for every `docs`) - // - const url = `/${slug.join('/')}` - // editURL - const EDIT_BASEURL = process.env.EDIT_BASEURL - const editURL = EDIT_BASEURL?.length ? file.replace(root, EDIT_BASEURL) : undefined - - // Read & parse doc - // // frontmatter // @@ -110,109 +94,159 @@ async function _getDocs( const frontmatter = compiled.data const _lastSegment = slug[slug.length - 1] - const title: string = frontmatter.title ?? _lastSegment.replace(/\-/g, ' ') - - const description: string = frontmatter.description ?? '' - - const sourcecode: string = frontmatter.sourcecode ?? '' - const SOURCECODE_BASEURL = process.env.SOURCECODE_BASEURL - const sourcecodeURL = SOURCECODE_BASEURL?.length - ? `${SOURCECODE_BASEURL}/${sourcecode}` - : undefined - - const nav: number = frontmatter.nav ?? Infinity - - const frontmatterImage: string | undefined = frontmatter.image - const srcImage = frontmatterImage || process.env.LOGO - const image: string = srcImage ? resolveMdxUrl(srcImage, relFilePath, MDX_BASEURL) : '' - - // - // MDX content - // - - // Skip docs other than `slugOfInterest` -- better perfs) - // if (JSON.stringify(slug) !== JSON.stringify(slugOfInterest)) { - // return { - // slug, - // url, - // editURL, - // title, - // description, - // nav, - // } as Doc - // } - - // Sanitize markdown - let content = compiled.content - // Remove comments from frontMatter - .replace(FRONTMATTER_REGEX, '') - // Remove extraneous comments from post - .replace(COMMENT_REGEX, '') - // Remove inline link syntax - .replace(INLINE_LINK_REGEX, '$1') - - // - // inline images - // + const title: string = frontmatter.title.trim() ?? _lastSegment.replace(/\-/g, ' ') const boxes: string[] = [] - const tableOfContents: DocToC[] = [] - const { content: jsx } = await compileMDX({ - source: `# ${title}\n ${content}`, + await compileMDX({ + source: compiled.content, options: { mdxOptions: { - remarkPlugins: [remarkGFM], rehypePlugins: [ - rehypeImg(relFilePath, MDX_BASEURL), - rehypeDetails, - rehypeSummary, - rehypeGha, - rehypePrismPlus, - rehypeCode(), - rehypeCodesandbox(boxes), // 1. put all Codesandbox[id] into `doc.boxes` - rehypeToc(tableOfContents, url, title), // 2. will populate `doc.tableOfContents` - rehypeSandpack(dirname(file)), + rehypeCodesandbox(boxes), // 1. put all Codesandbox[id] into `boxes` ], }, }, - components: { - ...components, - Codesandbox: async (props: React.ComponentProps) => { - const ids = boxes // populated from 1. - // console.log('ids', ids) - - // - // Batch fetch all CSBs of the page - // - const csbs = await fetchCSB(...ids) - // console.log('boxes', boxes) - const data = csbs[props.id] - // console.log('data', data) - - // Merge initial props with data - const merged = { ...props, ...data } - return - }, - }, }) return { slug, url, - editURL, - sourcecode, - sourcecodeURL, title, - image, - description, - nav, - content: jsx, boxes, - tableOfContents, + // + file, + compiled, } }), ) + // console.log('entries', entries) + + // + // 2nd pass for `docs` + // + + const docs = await Promise.all( + entries.map( + async ({ + slug, + url, + title, + boxes, + // Passed from the 1st pass + file, + compiled, + }) => { + const relFilePath = file.substring(root.length) // "/getting-started/tutorials/store.mdx" + + // + // "Lightest" version of the doc (for `generateStaticParams`) + // + + if (slugOnly) { + return { slug } as Doc + } + + // + // Common infos (for every `docs`) + // + + // editURL + const EDIT_BASEURL = process.env.EDIT_BASEURL + const editURL = EDIT_BASEURL?.length ? file.replace(root, EDIT_BASEURL) : undefined + + // + // frontmatter + // + + const frontmatter = compiled.data + + const description: string = frontmatter.description ?? '' + + const sourcecode: string = frontmatter.sourcecode ?? '' + const SOURCECODE_BASEURL = process.env.SOURCECODE_BASEURL + const sourcecodeURL = SOURCECODE_BASEURL?.length + ? `${SOURCECODE_BASEURL}/${sourcecode}` + : undefined + + const nav: number = frontmatter.nav ?? Infinity + + const frontmatterImage: string | undefined = frontmatter.image + const srcImage = frontmatterImage || process.env.LOGO + const image: string = srcImage ? resolveMdxUrl(srcImage, relFilePath, MDX_BASEURL) : '' + + // + // MDX content + // + + // Skip docs other than `slugOfInterest` -- better perfs) + // if (JSON.stringify(slug) !== JSON.stringify(slugOfInterest)) { + // return { + // slug, + // url, + // editURL, + // title, + // description, + // nav, + // } as Doc + // } + + // Sanitize markdown + let content = compiled.content + // Remove comments from frontMatter + .replace(FRONTMATTER_REGEX, '') + // Remove extraneous comments from post + .replace(COMMENT_REGEX, '') + // Remove inline link syntax + .replace(INLINE_LINK_REGEX, '$1') + + // + // inline images + // + + const tableOfContents: DocToC[] = [] + + const { content: jsx } = await compileMDX({ + source: `# ${title}\n ${content}`, + options: { + mdxOptions: { + remarkPlugins: [remarkGFM], + rehypePlugins: [ + rehypeImg(relFilePath, MDX_BASEURL), + rehypeDetails, + rehypeSummary, + rehypeGha, + rehypePrismPlus, + rehypeCode(), + rehypeToc(tableOfContents, url, title), // 2. will populate `doc.tableOfContents` + rehypeSandpack(dirname(file)), + ], + }, + }, + components: { + ...components, + Codesandbox: (props) => , + Entries: () => , + }, + }) + + return { + slug, + url, + editURL, + sourcecode, + sourcecodeURL, + title, + image, + description, + nav, + content: jsx, + boxes, + tableOfContents, + } + }, + ), + ) // console.log('docs', docs) return docs.sort((a, b) => a.nav - b.nav)