mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
New GuideTemplate with composable components (#36893)
* Add db wrappers open in dashboard cta * Restore getLatestRelease return to null * Add guide template components with flexible content positioning * Add Guide components with composition pattern * Remove default branch on tag value * Add GuideTemplate component and do some clean up * Restore original GuideTemplate * Clean up code and unused props * Remove displayName * Remove unwanted export * Refactor EditLink to its own helper file * Apply several fixes * Fix underline on cta button * Remove default main tag * More descriptive button cta label * fix(docs): add iceberg_wrapper dashboard integration --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
1 parent
6e84bd1d72
commit
ec1c534013
10 files changed
+293
-2
No files matched your search
@@ -3,8 +3,12 @@ import { readFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import emoji from 'remark-emoji'
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { Guide, GuideArticle, GuideHeader, GuideFooter, GuideMdxContent } from '~/features/ui/guide'
|
||||
import { newEditLink } from '~/features/helpers.edit-link'
|
||||
import {
|
||||
genGuideMeta,
|
||||
genGuidesStaticParams,
|
||||
@@ -102,6 +106,7 @@ const pageMap = [
|
||||
slug: 'airtable',
|
||||
meta: {
|
||||
title: 'Airtable',
|
||||
dashboardIntegrationPath: 'airtable_wrapper',
|
||||
},
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
@@ -109,6 +114,7 @@ const pageMap = [
|
||||
slug: 'auth0',
|
||||
meta: {
|
||||
title: 'Auth0',
|
||||
dashboardIntegrationPath: 'auth0_wrapper',
|
||||
},
|
||||
remoteFile: 'auth0.md',
|
||||
},
|
||||
@@ -116,6 +122,7 @@ const pageMap = [
|
||||
slug: 'bigquery',
|
||||
meta: {
|
||||
title: 'BigQuery',
|
||||
dashboardIntegrationPath: 'bigquery_wrapper',
|
||||
},
|
||||
remoteFile: 'bigquery.md',
|
||||
},
|
||||
@@ -123,6 +130,7 @@ const pageMap = [
|
||||
slug: 'clerk',
|
||||
meta: {
|
||||
title: 'Clerk',
|
||||
dashboardIntegrationPath: 'clerk_wrapper',
|
||||
},
|
||||
remoteFile: 'clerk.md',
|
||||
},
|
||||
@@ -130,6 +138,7 @@ const pageMap = [
|
||||
slug: 'clickhouse',
|
||||
meta: {
|
||||
title: 'ClickHouse',
|
||||
dashboardIntegrationPath: 'clickhouse_wrapper',
|
||||
},
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
@@ -137,6 +146,7 @@ const pageMap = [
|
||||
slug: 'cognito',
|
||||
meta: {
|
||||
title: 'AWS Cognito',
|
||||
dashboardIntegrationPath: 'cognito_wrapper',
|
||||
},
|
||||
remoteFile: 'cognito.md',
|
||||
},
|
||||
@@ -151,6 +161,7 @@ const pageMap = [
|
||||
slug: 'firebase',
|
||||
meta: {
|
||||
title: 'Firebase',
|
||||
dashboardIntegrationPath: 'firebase_wrapper',
|
||||
},
|
||||
remoteFile: 'firebase.md',
|
||||
},
|
||||
@@ -158,6 +169,7 @@ const pageMap = [
|
||||
slug: 'iceberg',
|
||||
meta: {
|
||||
title: 'Iceberg',
|
||||
dashboardIntegrationPath: 'iceberg_wrapper',
|
||||
},
|
||||
remoteFile: 'iceberg.md',
|
||||
},
|
||||
@@ -165,6 +177,7 @@ const pageMap = [
|
||||
slug: 'logflare',
|
||||
meta: {
|
||||
title: 'Logflare',
|
||||
dashboardIntegrationPath: 'logflare_wrapper',
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
@@ -172,6 +185,7 @@ const pageMap = [
|
||||
slug: 'mssql',
|
||||
meta: {
|
||||
title: 'MSSQL',
|
||||
dashboardIntegrationPath: 'mssql_wrapper',
|
||||
},
|
||||
remoteFile: 'mssql.md',
|
||||
},
|
||||
@@ -179,6 +193,7 @@ const pageMap = [
|
||||
slug: 'notion',
|
||||
meta: {
|
||||
title: 'Notion',
|
||||
dashboardIntegrationPath: 'notion_wrapper',
|
||||
},
|
||||
remoteFile: 'notion.md',
|
||||
},
|
||||
@@ -186,6 +201,7 @@ const pageMap = [
|
||||
slug: 'paddle',
|
||||
meta: {
|
||||
title: 'Paddle',
|
||||
dashboardIntegrationPath: 'paddle_wrapper',
|
||||
},
|
||||
remoteFile: 'paddle.md',
|
||||
},
|
||||
@@ -193,6 +209,7 @@ const pageMap = [
|
||||
slug: 'redis',
|
||||
meta: {
|
||||
title: 'Redis',
|
||||
dashboardIntegrationPath: 'redis_wrapper',
|
||||
},
|
||||
remoteFile: 'redis.md',
|
||||
},
|
||||
@@ -200,6 +217,7 @@ const pageMap = [
|
||||
slug: 's3',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
dashboardIntegrationPath: 's3_wrapper',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
@@ -207,6 +225,7 @@ const pageMap = [
|
||||
slug: 'snowflake',
|
||||
meta: {
|
||||
title: 'Snowflake',
|
||||
dashboardIntegrationPath: 'snowflake_wrapper',
|
||||
},
|
||||
remoteFile: 'snowflake.md',
|
||||
},
|
||||
@@ -214,6 +233,7 @@ const pageMap = [
|
||||
slug: 'stripe',
|
||||
meta: {
|
||||
title: 'Stripe',
|
||||
dashboardIntegrationPath: 'stripe_wrapper',
|
||||
},
|
||||
remoteFile: 'stripe.md',
|
||||
},
|
||||
@@ -254,7 +274,31 @@ const WrappersDocs = async (props: { params: Promise<Params> }) => {
|
||||
} as SerializeOptions)
|
||||
: undefined
|
||||
|
||||
return <GuideTemplate meta={meta} mdxOptions={options} {...data} />
|
||||
const dashboardIntegrationURL = getDashboardIntegrationURL(meta.dashboardIntegrationPath)
|
||||
|
||||
return (
|
||||
<Guide meta={meta}>
|
||||
<GuideArticle>
|
||||
<GuideHeader />
|
||||
|
||||
{dashboardIntegrationURL && (
|
||||
<Admonition type="tip" className="mb-4">
|
||||
<p>You can enable the {meta.title} wrapper right from the Supabase dashboard.</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={dashboardIntegrationURL} className="no-underline">
|
||||
Open wrapper in dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
<GuideMdxContent content={data.content} mdxOptions={options} />
|
||||
|
||||
<GuideFooter editLink={data.editLink} />
|
||||
</GuideArticle>
|
||||
</Guide>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +367,12 @@ const getContent = async (params: Params) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getDashboardIntegrationURL = (wrapperPath?: string) => {
|
||||
return wrapperPath
|
||||
? `https://supabase.com/dashboard/project/_/integrations/${wrapperPath}/overview`
|
||||
: null
|
||||
}
|
||||
|
||||
const assetUrlTransform = (url: string, baseUrl: string): string => {
|
||||
const assetPattern = /(\.\.\/)+assets\//
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
|
||||
{content && <MDXRemoteBase source={content} options={mdxOptions} />}
|
||||
{children}
|
||||
|
||||
<footer className="mt-16 not-prose">
|
||||
<a
|
||||
href={
|
||||
@@ -135,3 +136,4 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
}
|
||||
|
||||
export { GuideTemplate, newEditLink }
|
||||
export type { EditLink }
|
||||
@@ -0,0 +1,39 @@
|
||||
const EDIT_LINK_SYMBOL = Symbol('edit link')
|
||||
|
||||
interface EditLink {
|
||||
[EDIT_LINK_SYMBOL]: true
|
||||
link: string
|
||||
includesProtocol: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object representing a link where the original content can be
|
||||
* edited.
|
||||
*
|
||||
* Takes either a relative path, which will be prefixed with
|
||||
* `https://github.com/`, or a full URL including protocol.
|
||||
*/
|
||||
const newEditLink = (str: string): EditLink => {
|
||||
if (str.startsWith('/')) {
|
||||
throw Error(`Edit links cannot start with slashes. Received: ${str}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch strings that provide FQDNS without https?:
|
||||
*
|
||||
* At the start of a string, before the first slash, there is a dot
|
||||
* surrounded by non-slash characters.
|
||||
*/
|
||||
if (/^[^\/]+\.[^\/]+\//.test(str)) {
|
||||
throw Error(`Fully qualified domain names must start with 'https?'. Received: ${str}`)
|
||||
}
|
||||
|
||||
return {
|
||||
[EDIT_LINK_SYMBOL]: true,
|
||||
link: str,
|
||||
includesProtocol: str.startsWith('http://') || str.startsWith('https://'),
|
||||
}
|
||||
}
|
||||
|
||||
export { newEditLink }
|
||||
export type { EditLink }
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
import type { WithRequired } from '~/features/helpers.types'
|
||||
import { EditLink } from '~/features/helpers.edit-link'
|
||||
import { type GuideFrontmatter } from '~/lib/docs'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from './guide'
|
||||
|
||||
interface BaseGuideTemplateProps {
|
||||
meta?: GuideFrontmatter
|
||||
content?: string
|
||||
children?: ReactNode
|
||||
editLink: EditLink
|
||||
mdxOptions?: SerializeOptions
|
||||
}
|
||||
|
||||
type GuideTemplateProps =
|
||||
| WithRequired<BaseGuideTemplateProps, 'children'>
|
||||
| WithRequired<BaseGuideTemplateProps, 'content'>
|
||||
|
||||
const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideTemplateProps) => {
|
||||
return (
|
||||
<Guide meta={meta}>
|
||||
<GuideArticle>
|
||||
<GuideHeader />
|
||||
<GuideMdxContent content={content} mdxOptions={mdxOptions}></GuideMdxContent>
|
||||
{children}
|
||||
<GuideFooter editLink={editLink} />
|
||||
</GuideArticle>
|
||||
</Guide>
|
||||
)
|
||||
}
|
||||
|
||||
export { GuideTemplate }
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { type GuideFrontmatter } from '~/lib/docs'
|
||||
|
||||
interface GuideContextValue {
|
||||
meta?: GuideFrontmatter
|
||||
}
|
||||
|
||||
const GuideContext = createContext<GuideContextValue | undefined>(undefined)
|
||||
|
||||
export const useGuide = () => {
|
||||
const context = useContext(GuideContext)
|
||||
if (!context) {
|
||||
throw new Error('useGuide must be used within a GuideProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface GuideProps {
|
||||
meta?: GuideFrontmatter
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Guide({ meta, children, className }: GuideProps) {
|
||||
const hideToc = meta?.hideToc || meta?.hide_table_of_contents
|
||||
|
||||
return (
|
||||
<GuideContext.Provider value={{ meta }}>
|
||||
<TocAnchorsProvider>
|
||||
<div className={cn('grid grid-cols-12 relative gap-4', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
'transition-all ease-out',
|
||||
'duration-100',
|
||||
hideToc ? 'col-span-12' : 'col-span-12 md:col-span-9'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!hideToc && (
|
||||
<GuidesTableOfContents
|
||||
video={meta?.tocVideo}
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
'col-span-3 self-start',
|
||||
'sticky',
|
||||
/**
|
||||
* --header-height: height of nav
|
||||
* 1px: height of nav border
|
||||
* 2rem: content padding
|
||||
*/
|
||||
'top-[calc(var(--header-height)+1px+2rem)]',
|
||||
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
|
||||
'max-h-[calc(100vh-var(--header-height)-3rem)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TocAnchorsProvider>
|
||||
</GuideContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import Breadcrumbs from '~/components/Breadcrumbs'
|
||||
|
||||
interface GuideArticleProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GuideArticle({ children, className }: GuideArticleProps) {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs className="mb-2" />
|
||||
<article
|
||||
// Used to get headings for the table of contents
|
||||
id="sb-docs-guide-main-article"
|
||||
className={cn('prose max-w-none', className)}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { cn } from 'ui'
|
||||
import { type EditLink } from '~/features/helpers.edit-link'
|
||||
|
||||
interface GuideFooterProps {
|
||||
className?: string
|
||||
editLink: EditLink
|
||||
}
|
||||
|
||||
export function GuideFooter({ className, editLink }: GuideFooterProps) {
|
||||
if (!editLink) return null
|
||||
|
||||
return (
|
||||
<footer className={cn('mt-16 not-prose', className)}>
|
||||
<a
|
||||
href={editLink.includesProtocol ? editLink.link : `https://github.com/${editLink.link}`}
|
||||
className={cn(
|
||||
'w-fit',
|
||||
'flex items-center gap-1',
|
||||
'text-sm text-scale-1000 hover:text-scale-1200',
|
||||
'transition-colors'
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener edit"
|
||||
>
|
||||
Edit this page on GitHub <ExternalLink size={14} strokeWidth={1.5} />
|
||||
</a>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useGuide } from './Guide'
|
||||
|
||||
interface GuideHeaderProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GuideHeader({ className }: GuideHeaderProps) {
|
||||
const { meta } = useGuide()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<h1 className="mb-0 [&>p]:m-0">
|
||||
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
|
||||
</h1>
|
||||
{meta?.subtitle && (
|
||||
<h2 className="mt-3 text-xl text-foreground-light">
|
||||
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
|
||||
</h2>
|
||||
)}
|
||||
<hr className="not-prose border-t-0 border-b my-8" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
interface GuideArticleProps {
|
||||
content?: string
|
||||
mdxOptions?: SerializeOptions
|
||||
}
|
||||
|
||||
export function GuideMdxContent({ content, mdxOptions }: GuideArticleProps) {
|
||||
return content ? <MDXRemoteBase source={content} options={mdxOptions} /> : null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { Guide, useGuide } from './Guide'
|
||||
export { GuideArticle } from './GuideArticle'
|
||||
export { GuideMdxContent } from './GuideMdxContent'
|
||||
export { GuideHeader } from './GuideHeader'
|
||||
export { GuideFooter } from './GuideFooter'
|
||||
Reference in new issue
Block a user