diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3eed078f08f..839bd310041 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,5 +3,6 @@ /apps/studio/ @supabase/Dashboard /apps/www/ @supabase/Website -/apps/www/_blog +/apps/www/_blog @supabase/marketing +/apps/www/public/images/blog @supabase/marketing /docker/ @supabase/cli diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index eb248c823e0..d5b441349a4 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -5,61 +5,41 @@ on: jobs: check-external: runs-on: ubuntu-latest - outputs: - result: ${{ steps.is-member.outputs.result }} + permissions: + issues: write steps: - - name: Get deps - run: | - sudo apt-get update && sudo apt-get install -y jq - - name: Get org members - id: is-member - env: - GH_ORG: supabase - GH_USER: ${{ github.event.issue.user.login }} - GH_TOKEN: ${{ secrets.SB_ORG_MEMBERSHIP_PAT }} - run: | - gh api /orgs/supabase/members/$GH_USER - - if: failure() - name: is-member - run: | - echo "User is not a member of Supabase org" - echo "result=false" >> "$GITHUB_OUTPUT" - - if: success() - run: | - echo "User is a member of Supabase org" - echo "result=true" >> "$GITHUB_OUTPUT" - - name: Label contributor - id: label-contributor - if: ${{ steps.is-member.outputs.result == 'false' }} + - name: Check if organization member + id: is-org-member + uses: JamesSingleton/is-organization-member@1.0.0 + with: + organization: ${{ github.repository_owner }} + username: ${{ github.event.issue.user.login }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: label-member env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - USERNAME: ${{ github.event.issue.user.login }} GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} - LABELS: external-issue run: | - echo "applying external label" - gh issue edit $NUMBER --add-label $LABELS - - name: Label internal - id: label-internal - if: ${{ steps.is-member.outputs.result == 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - USERNAME: ${{ github.event.issue.user.login }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - LABELS: internal-issue - run: | - echo "applying internal label" - gh issue edit $NUMBER --add-label $LABELS + if [ ${{ steps.is-org-member.outputs.result }} != "true" ]; then + echo "User is outside of organization, labeling external" + gh issue edit "$NUMBER" --add-label "external-issue" + else + echo "User is within the organization, labeling internal" + gh issue edit "$NUMBER" --add-label "internal-issue" + fi triage-new: runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Label triage env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} LABELS: to-triage run: | - echo "applying triage label" - gh issue edit $NUMBER --add-label $LABELS + echo "Applying triage label for new issue" + gh issue edit "$NUMBER" --add-label "$LABELS" + diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index eb4e5885a55..70481bb7bb3 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -1,4 +1,4 @@ -name: Check code with TypeScript +name: Check code with TypeScript & Lint on: pull_request: @@ -30,3 +30,6 @@ jobs: - name: Run TypeScript type check run: npx turbo run typecheck + + - name: Run Lint + run: npx turbo run lint diff --git a/.misspell-fixer.ignore b/.misspell-fixer.ignore index 2bfab3e9e99..1b26bc40f97 100644 --- a/.misspell-fixer.ignore +++ b/.misspell-fixer.ignore @@ -1,2 +1,3 @@ ^./i18n -^./packages/api-types \ No newline at end of file +^./packages/api-types +^./apps/www/lib/redirects.js \ No newline at end of file diff --git a/README.md b/README.md index d63f425b65d..8cc82c6f339 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,15 @@ Our approach for client libraries is modular. Each sub-library is a standalone i storage-swift functions-swift + + Python + supabase-py + postgrest-py + gotrue-py + realtime-py + storage-py + functions-py + 💚 Community 💚 @@ -159,15 +168,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i storage-kt functions-kt - - Python - supabase-py - postgrest-py - gotrue-py - realtime-py - storage-py - functions-py - Ruby supabase-rb diff --git a/apps/database-new/package.json b/apps/database-new/package.json index 26b1fbd3d6a..e1d6f6a9e29 100644 --- a/apps/database-new/package.json +++ b/apps/database-new/package.json @@ -40,6 +40,6 @@ "autoprefixer": "^10.4.14", "nextjs-node-loader": "^1.1.5", "postcss": "^8.4.31", - "typescript": "^5.4.3" + "typescript": "^5.5.0" } } diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index e17eb4f9c02..5c7e7d1f248 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -489,6 +489,94 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "commandmenu-demo": { + name: "commandmenu-demo", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-demo")), + source: "", + files: ["registry/default/example/commandmenu-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-badge": { + name: "commandmenu-badge", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-badge")), + source: "", + files: ["registry/default/example/commandmenu-badge.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-icon": { + name: "commandmenu-icon", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-icon")), + source: "", + files: ["registry/default/example/commandmenu-icon.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-hidden": { + name: "commandmenu-hidden", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-hidden")), + source: "", + files: ["registry/default/example/commandmenu-hidden.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-force": { + name: "commandmenu-force", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-force")), + source: "", + files: ["registry/default/example/commandmenu-force.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-conditional": { + name: "commandmenu-conditional", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-conditional")), + source: "", + files: ["registry/default/example/commandmenu-conditional.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-subpage": { + name: "commandmenu-subpage", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-subpage")), + source: "", + files: ["registry/default/example/commandmenu-subpage.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "commandmenu-subpage-custom": { + name: "commandmenu-subpage-custom", + type: "components:example", + registryDependencies: ["commandmenu"], + component: React.lazy(() => import("@/registry/default/example/commandmenu-subpage-custom")), + source: "", + files: ["registry/default/example/commandmenu-subpage-custom.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "context-menu-demo": { name: "context-menu-demo", type: "components:example", @@ -1072,6 +1160,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "sonner-demo": { + name: "sonner-demo", + type: "components:example", + registryDependencies: ["sonner"], + component: React.lazy(() => import("@/registry/default/example/sonner-demo")), + source: "", + files: ["registry/default/example/sonner-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "sonner-types": { + name: "sonner-types", + type: "components:example", + registryDependencies: ["sonner"], + component: React.lazy(() => import("@/registry/default/example/sonner-types")), + source: "", + files: ["registry/default/example/sonner-types.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "sonner-upload": { + name: "sonner-upload", + type: "components:example", + registryDependencies: ["sonner"], + component: React.lazy(() => import("@/registry/default/example/sonner-upload")), + source: "", + files: ["registry/default/example/sonner-upload.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "switch-demo": { name: "switch-demo", type: "components:example", @@ -1831,6 +1952,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "assistant-chat-demo": { + name: "assistant-chat-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/assistant-chat-demo")), + source: "", + files: ["registry/default/example/assistant-chat-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "assistant-chat-commands": { + name: "assistant-chat-commands", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/assistant-chat-commands")), + source: "", + files: ["registry/default/example/assistant-chat-commands.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "color-usage-surface-www-and-docs": { name: "color-usage-surface-www-and-docs", type: "docs:example", @@ -1864,5 +2007,16 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "expanding-textarea-demo": { + name: "expanding-textarea-demo", + type: "docs:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/expanding-textarea-demo")), + source: "", + files: ["registry/default/example/expanding-textarea-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, }, } diff --git a/apps/design-system/app/SonnerToast.tsx b/apps/design-system/app/SonnerToast.tsx new file mode 100644 index 00000000000..819a8890e9d --- /dev/null +++ b/apps/design-system/app/SonnerToast.tsx @@ -0,0 +1,9 @@ +'use client' + +import { useConfig } from '@/hooks/use-config' +import { SonnerToaster as Toaster } from 'ui' + +export function SonnerToaster() { + const [config] = useConfig() + return +} diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 7e8ff99a5a9..d28af7c710a 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -2,11 +2,13 @@ import '@/styles/globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' import { ThemeProvider } from './Providers' +import { Toaster } from 'ui' +import { SonnerToaster } from './SonnerToast' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { - title: 'Create Next App', + title: 'Supabase Design System', description: 'Generated by create next app', } @@ -15,22 +17,16 @@ interface RootLayoutProps { } export default async function Layout({ children }: RootLayoutProps) { - // console.log('Root Layout mounted or re-rendered') - return ( - +
{children}
+ +
diff --git a/apps/design-system/components/design-system-marks.tsx b/apps/design-system/components/design-system-marks.tsx deleted file mode 100644 index fab36f93991..00000000000 --- a/apps/design-system/components/design-system-marks.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -import SVG from 'react-inlinesvg' -import { useTheme } from 'next-themes' - -function DesignSystemMarks() { - const { resolvedTheme } = useTheme() - - const isDark = resolvedTheme?.includes('dark') - - return ( - - ) -} - -export { DesignSystemMarks } diff --git a/apps/design-system/components/homepage-svg-handler.tsx b/apps/design-system/components/homepage-svg-handler.tsx index e359eb0e83b..8a565c86f4e 100644 --- a/apps/design-system/components/homepage-svg-handler.tsx +++ b/apps/design-system/components/homepage-svg-handler.tsx @@ -2,14 +2,15 @@ import SVG from 'react-inlinesvg' import { useTheme } from 'next-themes' +import { cn } from 'ui' -const HomepageSvgHandler = ({ name }: { name: string }) => { +const HomepageSvgHandler = ({ name, className }: { name: string; className?: string }) => { const { resolvedTheme } = useTheme() return (
diff --git a/apps/design-system/components/mdx-components.tsx b/apps/design-system/components/mdx-components.tsx index 52e02bb9bc5..8074b7aebb1 100644 --- a/apps/design-system/components/mdx-components.tsx +++ b/apps/design-system/components/mdx-components.tsx @@ -44,6 +44,8 @@ import { Icons } from '@/components/icons' import { ThemeSettings } from '@/components/theme-settings' import { CodeFragment } from '@/components/code-fragment' import { Admonition } from 'ui-patterns/admonition' +import { SonnerExpandConfig } from './sonner-expand-config' +import { SonnerPositionConfig } from './sonner-expand-position' const components = { Accordion, @@ -273,6 +275,8 @@ const components = { ThemeSettings, CodeFragment, Admonition, + SonnerExpandConfig, + SonnerPositionConfig, } interface MdxProps { diff --git a/apps/design-system/components/sonner-expand-config.tsx b/apps/design-system/components/sonner-expand-config.tsx new file mode 100644 index 00000000000..43f2295c298 --- /dev/null +++ b/apps/design-system/components/sonner-expand-config.tsx @@ -0,0 +1,37 @@ +import { useConfig } from '@/hooks/use-config' +import { Switch } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +function SonnerExpandConfig() { + const [config, setConfig] = useConfig() + + return ( +
+
+ + + setConfig({ + ...config, + sonnerExpand: e, + }) + } + /> + +
+
+ ) +} + +export { SonnerExpandConfig } diff --git a/apps/design-system/components/sonner-expand-position.tsx b/apps/design-system/components/sonner-expand-position.tsx new file mode 100644 index 00000000000..82510e03394 --- /dev/null +++ b/apps/design-system/components/sonner-expand-position.tsx @@ -0,0 +1,70 @@ +import { useConfig } from '@/hooks/use-config' +import { ComponentProps } from 'react' +import { + SelectContent_Shadcn_, + SelectGroup_Shadcn_, + SelectItem_Shadcn_, + SelectLabel_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Select_Shadcn_, + SonnerToaster, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +function SonnerPositionConfig() { + const [config, setConfig] = useConfig() + + type Position = NonNullable['position']> + + const positions: Position[] = [ + 'top-left', + 'top-right', + 'bottom-left', + 'bottom-right', + 'top-center', + 'bottom-center', + ] + + return ( +
+
+ + { + setConfig({ + ...config, + sonnerPosition: e, + }) + }} + > + + + + + + Positions available + {positions.map((position) => ( + + {position} + + ))} + + + + +
+
+ ) +} + +export { SonnerPositionConfig } diff --git a/apps/design-system/components/top-navigation.tsx b/apps/design-system/components/top-navigation.tsx index 7301be52fc7..4aec103a965 100644 --- a/apps/design-system/components/top-navigation.tsx +++ b/apps/design-system/components/top-navigation.tsx @@ -1,9 +1,8 @@ // import { docsConfig } from '@/config/docs' import Link from 'next/link' -import { DesignSystemMarks } from './design-system-marks' -import { ThemeSwitcherDropdown } from './theme-switcher-dropdown' -import { TopNavigationSearch } from './top-navigation-search' import { CommandMenu } from './command-menu' +import { HomepageSvgHandler } from './homepage-svg-handler' +import { ThemeSwitcherDropdown } from './theme-switcher-dropdown' function TopNavigation() { return ( @@ -15,7 +14,7 @@ function TopNavigation() {

Supabase Design System

- + {/* {docsConfig.mainNav.map((section) => ( <> diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 4e1a7be559d..f2c5ee94b8a 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -82,6 +82,11 @@ export const docsConfig: DocsConfig = { href: '/docs/fragments/admonition', items: [], }, + { + title: 'Assistant Chat', + href: '/docs/fragments/assistant-chat', + items: [], + }, { title: 'Modal', href: '/docs/fragments/modal', @@ -193,6 +198,11 @@ export const docsConfig: DocsConfig = { href: '/docs/components/command', items: [], }, + { + title: 'Command Menu (cmdk)', + href: '/docs/components/commandmenu', + items: [], + }, { title: 'Context Menu', href: '/docs/components/context-menu', @@ -374,6 +384,11 @@ export const docsConfig: DocsConfig = { href: '/docs/components/tree-view', items: [], }, + { + title: 'Expanding Textarea', + href: '/docs/components/expanding-textarea', + items: [], + }, ], }, ], diff --git a/apps/design-system/content/docs/changelog.mdx b/apps/design-system/content/docs/changelog.mdx index c84c07d16da..cbee68f1521 100644 --- a/apps/design-system/content/docs/changelog.mdx +++ b/apps/design-system/content/docs/changelog.mdx @@ -4,12 +4,47 @@ description: Latest updates and announcements. toc: false --- +## 12th June 2024 - Button styles updated + +- [` + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} +``` + +## Examples + +To avoid keyboard shortcuts clashing wildly, the open shortcut is disabled for the following examples. Use the button to open the example previews. + +### With experimental badge + +You can add an experimental badge to any command item: + + + +### With icon + +You can add an icon to any command item: + + + +### With default hidden + +You can hide any command menu item unless it matches an active search query. Try searching for `open sesame` here: + + + +### With force-mount + +You can force-mount any command item (force it to always show, regardless of search query). You can also force-mount an entire section. + + + +### With conditional commands + +You can use the dependencies (`deps`) array on the register command options to change the registered commands in response to state changes. + +Dependency equality is calculated using the `lodash` [isEqual](https://lodash.com/docs/4.17.15#isEqual) function, so you may need suitable use of `useCallback` or `useMemo` to maintain or break equality. + + + +### With subpage + +You can define a subpage of commands: + + + +### With arbitrary subpage + +You can define an arbitrary subpage that loads a custom component: + + diff --git a/apps/design-system/content/docs/components/expanding-textarea.mdx b/apps/design-system/content/docs/components/expanding-textarea.mdx new file mode 100644 index 00000000000..dded1c63207 --- /dev/null +++ b/apps/design-system/content/docs/components/expanding-textarea.mdx @@ -0,0 +1,25 @@ +--- +title: Expanding Textarea +description: Displays a textarea which autoexpands (or shrinks) based on its content. +component: true +source: + shadcn: true +--- + + + +## Usage + +```tsx +import { ExpandingTextArea } from 'ui' +``` + +```tsx + +``` + +## Examples + +### Default + + diff --git a/apps/design-system/content/docs/components/sonner.mdx b/apps/design-system/content/docs/components/sonner.mdx index 3af18cfd4a8..3ae4ce8ee0a 100644 --- a/apps/design-system/content/docs/components/sonner.mdx +++ b/apps/design-system/content/docs/components/sonner.mdx @@ -16,46 +16,6 @@ Sonner is built and maintained by [emilkowalski\_](https://twitter.com/emilkowal ## Installation - - - - CLI - Manual - - - - - -Run the following command: - -```bash -npx shadcn-ui@latest add sonner -``` - -Add the Toaster component - -```tsx title="app/layout.tsx" {1,9} -import { Toaster } from '@/components/ui/sonner' - -export default function RootLayout({ children }) { - return ( - - - -
{children}
- - - - ) -} -``` - -
- -
- - - Install the following dependencies: @@ -88,10 +48,6 @@ export default function RootLayout({ children }) { - - -
- ## Usage ```tsx @@ -101,3 +57,41 @@ import { toast } from 'sonner' ```tsx toast('Event has been created.') ``` + +## Expand + +You can change the amount of toasts visible through the visibleToasts prop. + +```tsx + +``` + +You can use the Input below to try it out. + + + +## Position + +You can change the amount of toasts visible through the visibleToasts prop. + +```tsx + +``` + +You can use the Input below to try it out. + + + +## Types + +You can customize the type of toast you want to render, and pass an options object as the second argument. + + + +## Upload example + + diff --git a/apps/design-system/content/docs/fragments/assistant-chat.mdx b/apps/design-system/content/docs/fragments/assistant-chat.mdx new file mode 100644 index 00000000000..215ff3b60c8 --- /dev/null +++ b/apps/design-system/content/docs/fragments/assistant-chat.mdx @@ -0,0 +1,52 @@ +--- +title: Assistant Chat +description: Bespoke Assistant chat text area, with support for a commands Popover. +component: true +--- + + + +## Usage + +```tsx +import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm' +``` + +```tsx +} + value={value} + loading={loading} + disabled={loading} + onValueChange={(e) => setValueState(e.target.value)} + onSubmit={async (event) => { + event.preventDefault() + handleSubmit(event) + }} +/> +``` + +## Commands + +The Assistant Chat support a commands Popover that can be used to display a list of available commands. + +It requires a useState and a ref though for it to work correctly. +This is so that the input is still in focus, and the user can still interact with the Popover with the keyboard. + +```tsx +const [commandsOpen, setCommandsOpen] = useState(false) +const textAreaRef = createRef() + + + + +``` + + diff --git a/apps/design-system/hooks/use-config.ts b/apps/design-system/hooks/use-config.ts index 858ffacc11e..7177c77890b 100644 --- a/apps/design-system/hooks/use-config.ts +++ b/apps/design-system/hooks/use-config.ts @@ -1,22 +1,27 @@ -import { useAtom } from 'jotai' -import { atomWithStorage } from 'jotai/utils' - -// import { BackendProvider } from '@/registry/backend-provider' import { Style } from '@/registry/styles' import { Theme } from '@/registry/themes' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { ComponentProps } from 'react' +import { SonnerToaster } from 'ui' +// import { BackendProvider } from '@/registry/backend-provider' type Config = { style: Style['name'] theme: Theme['name'] radius: number + sonnerPosition: ComponentProps['position'] + sonnerExpand: boolean // backendProvider: BackendProvider['name'] | undefined } const configAtom = atomWithStorage('config', { style: 'default', - // backendProvider: 'nextjs-auth', theme: 'zinc', radius: 0.5, + sonnerPosition: 'bottom-right', + sonnerExpand: false, + // backendProvider: 'nextjs-auth', }) export function useConfig() { diff --git a/apps/design-system/next.config.mjs b/apps/design-system/next.config.mjs index 3c67b4ac18e..2280b564d29 100644 --- a/apps/design-system/next.config.mjs +++ b/apps/design-system/next.config.mjs @@ -1,5 +1,7 @@ import { withContentlayer } from 'next-contentlayer2' +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system' + /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'], @@ -22,11 +24,11 @@ const nextConfig = { }, async redirects() { return [ - ...(process.env.NEXT_PUBLIC_BASE_PATH?.length + ...(BASE_PATH.length ? [ { source: '/', - destination: process.env.NEXT_PUBLIC_BASE_PATH, + destination: BASE_PATH, basePath: false, permanent: false, }, diff --git a/apps/design-system/package.json b/apps/design-system/package.json index bdd27eba449..eb74f256f5d 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.0.5", - "contentlayer2": "0.4.5", + "contentlayer2": "0.4.6", "eslint-config-supabase": "*", "fs": "^0.0.1-security", "jotai": "^2.8.0", @@ -22,7 +22,7 @@ "lucide-react": "^0.338.0", "markdown-wasm": "^1.2.0", "next": "^14.2.3", - "next-contentlayer2": "0.4.5", + "next-contentlayer2": "0.4.6", "react": "^18.2.0", "react-docgen": "^7.0.3", "react-dom": "^18.2.0", @@ -52,7 +52,7 @@ "shiki": "^1.1.7", "tailwindcss": "^3.3.0", "tsconfig": "*", - "typescript": "^5", + "typescript": "^5.5.0", "unist-builder": "3.0.0" } } diff --git a/apps/design-system/public/img/design-system-marks/atoms-illustration--dark.svg b/apps/design-system/public/img/design-system-marks/atoms-illustration--dark.svg new file mode 100644 index 00000000000..fae400998e3 --- /dev/null +++ b/apps/design-system/public/img/design-system-marks/atoms-illustration--dark.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/design-system/public/img/design-system-marks/design-system-marks--deep-dark.svg b/apps/design-system/public/img/design-system-marks/design-system-marks--deep-dark.svg new file mode 100644 index 00000000000..928eafbfc38 --- /dev/null +++ b/apps/design-system/public/img/design-system-marks/design-system-marks--deep-dark.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/design-system/public/img/design-system-marks/fragments-illustration--dark.svg b/apps/design-system/public/img/design-system-marks/fragments-illustration--dark.svg new file mode 100644 index 00000000000..01331d3d8de --- /dev/null +++ b/apps/design-system/public/img/design-system-marks/fragments-illustration--dark.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/design-system/registry/default/example/assistant-chat-commands.tsx b/apps/design-system/registry/default/example/assistant-chat-commands.tsx new file mode 100644 index 00000000000..d09a27fdf74 --- /dev/null +++ b/apps/design-system/registry/default/example/assistant-chat-commands.tsx @@ -0,0 +1,55 @@ +import React, { useRef, useState } from 'react' +import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm' +import { AssistantCommandsPopover } from 'ui-patterns/AssistantChat/AssistantCommandsPopover' + +export default function AssistantChatCommands() { + const [commandsOpen, setCommandsOpen] = useState(false) + const textAreaRef = useRef(null) + const [value, setValueState] = useState('') + const [loading, setLoading] = useState(false) + + function handleSubmit(event: React.FormEvent) { + // set loading for 3 seconds and then reset + setLoading(true) + setTimeout(() => { + setLoading(false) + }, 1500) + } + + const suggestions = [ + 'Add policy for org Inserted User Access', + 'Add policy for User-Specific Todo Access', + 'Add policy for Org Update Restriction', + ] + + return ( +
+ setValueState(e)} + suggestions={suggestions} + > + setValueState(e.target.value)} + commandsOpen={commandsOpen} + setCommandsOpen={setCommandsOpen} + onSubmit={async (event) => { + event.preventDefault() + handleSubmit(event) + }} + /> + +

+ Press / to open + commands +

+
+ ) +} diff --git a/apps/design-system/registry/default/example/assistant-chat-demo.tsx b/apps/design-system/registry/default/example/assistant-chat-demo.tsx new file mode 100644 index 00000000000..54ad73a33f2 --- /dev/null +++ b/apps/design-system/registry/default/example/assistant-chat-demo.tsx @@ -0,0 +1,30 @@ +import { Box } from 'lucide-react' +import { useState } from 'react' +import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm' + +export default function AssistantChatDemo() { + const [value, setValueState] = useState('') + const [loading, setLoading] = useState(false) + + function handleSubmit(event: React.FormEvent) { + // set loading for 3 seconds and then reset + setLoading(true) + setTimeout(() => { + setLoading(false) + }, 1500) + } + + return ( + } + value={value} + loading={loading} + disabled={loading} + onValueChange={(e) => setValueState(e.target.value)} + onSubmit={async (event) => { + event.preventDefault() + handleSubmit(event) + }} + /> + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-badge.tsx b/apps/design-system/registry/default/example/commandmenu-badge.tsx new file mode 100644 index 00000000000..309f8cd2afa --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-badge.tsx @@ -0,0 +1,42 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { + BadgeExperimental, + CommandInput, + CommandList, + CommandMenu, + CommandProvider, + useRegisterCommands, + useSetCommandMenuOpen, +} from 'ui-patterns/CommandMenu' + +function Commands() { + useRegisterCommands('Commands', [ + { + id: 'experimental', + name: 'Experimental', + action: () => alert('You triggered an experimental command'), + badge: () => , + }, + ]) + + return null +} + +function CommandMenuTrigger() { + const setOpen = useSetCommandMenuOpen() + + return +} + +export default function CommandMenuDemo() { + return ( + + + + + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-conditional.tsx b/apps/design-system/registry/default/example/commandmenu-conditional.tsx new file mode 100644 index 00000000000..4d8159b3573 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-conditional.tsx @@ -0,0 +1,70 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { useMemo, useState } from 'react' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + useRegisterCommands, +} from 'ui-patterns/CommandMenu' + +function Commands() { + const [activeCommand, setActiveCommand] = useState(1) + + const toggleCommands = () => setActiveCommand((state) => (state === 1 ? 2 : 1)) + + const commandOne = useMemo( + () => [ + { + id: 'one', + name: 'One', + action: () => alert('One'), + }, + ], + [] + ) + + const commandTwo = useMemo( + () => [ + { + id: 'two', + name: 'Two', + action: () => alert('Two'), + }, + ], + [] + ) + + const command = activeCommand === 1 ? commandOne : commandTwo + + useRegisterCommands('Commands', command, { deps: [command] }) + + return ( + + ) +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + +
+ }> + + + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-demo.tsx b/apps/design-system/registry/default/example/commandmenu-demo.tsx new file mode 100644 index 00000000000..a1cfec496cd --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-demo.tsx @@ -0,0 +1,48 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + useRegisterCommands, +} from 'ui-patterns/CommandMenu' + +function Commands() { + useRegisterCommands('Action commands', [ + { + id: 'alert', + name: 'Alert', + action: () => alert('You triggered a command'), + }, + ]) + useRegisterCommands('Route commands', [ + { + id: 'supabase-website', + name: 'Go to Supabase website', + route: 'https://supabase.com', + }, + ]) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-force.tsx b/apps/design-system/registry/default/example/commandmenu-force.tsx new file mode 100644 index 00000000000..ac475beb238 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-force.tsx @@ -0,0 +1,60 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + useRegisterCommands, +} from 'ui-patterns/CommandMenu' + +function Commands() { + useRegisterCommands('Commands', [ + { + id: 'forced', + name: 'Force-mounted', + action: () => alert("This command always shows even if it doesn't match the search query"), + forceMount: true, + }, + { + id: 'not-forced', + name: 'Not force-mounted', + action: () => alert("This command will disappear if it doesn't match the search query"), + }, + ]) + useRegisterCommands( + 'Force-mounted section', + [ + { + id: 'example', + name: 'An example', + action: () => alert('This entire section is force-mounted'), + }, + ], + { + forceMountSection: true, + } + ) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-hidden.tsx b/apps/design-system/registry/default/example/commandmenu-hidden.tsx new file mode 100644 index 00000000000..a9d60c69025 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-hidden.tsx @@ -0,0 +1,47 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + useRegisterCommands, +} from 'ui-patterns/CommandMenu' + +function Commands() { + useRegisterCommands('Commands', [ + { + id: 'shown', + name: 'shown', + action: () => alert('This command is shown by default'), + }, + { + id: 'hidden', + name: 'open sesame', + action: () => alert('This command is only shown when searched'), + defaultHidden: true, + }, + ]) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-icon.tsx b/apps/design-system/registry/default/example/commandmenu-icon.tsx new file mode 100644 index 00000000000..c877f83e450 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-icon.tsx @@ -0,0 +1,43 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { Github } from 'lucide-react' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + useRegisterCommands, +} from 'ui-patterns/CommandMenu' + +function Commands() { + useRegisterCommands('Commands', [ + { + id: 'github', + name: 'Go to repo', + route: 'https://github.com/supabase/supabase', + icon: () => , + }, + ]) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx b/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx new file mode 100644 index 00000000000..0cd74c1d7a1 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx @@ -0,0 +1,65 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { useMemo } from 'react' +import { + Breadcrumb, + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + PageType, + useRegisterCommands, + useRegisterPage, + useSetPage, +} from 'ui-patterns/CommandMenu' + +function CustomSubpage() { + return ( +
+ +

Custom subpage

+

+ Here is a custom subpage built from a component that you provide. +

+
+ ) +} + +function Commands() { + const setPage = useSetPage() + + useRegisterPage('Subpage', { + type: PageType.Component, + component: CustomSubpage, + }) + + useRegisterCommands('Commands', [ + { + id: 'subpage', + name: 'Go to subpage', + action: () => setPage('Subpage'), + }, + ]) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/commandmenu-subpage.tsx b/apps/design-system/registry/default/example/commandmenu-subpage.tsx new file mode 100644 index 00000000000..ef842b43143 --- /dev/null +++ b/apps/design-system/registry/default/example/commandmenu-subpage.tsx @@ -0,0 +1,73 @@ +import { Button } from '@ui/components/shadcn/ui/button' +import { useMemo } from 'react' +import { + CommandInput, + CommandList, + CommandMenu, + CommandMenuTrigger as CommandMenuTriggerPrimitive, + CommandProvider, + PageType, + useRegisterCommands, + useRegisterPage, + useSetPage, +} from 'ui-patterns/CommandMenu' + +function Commands() { + const setPage = useSetPage() + + const commands = useMemo( + () => [ + { + id: 'subcommand', + name: 'Subcommand', + action: () => alert('Triggered from subpage'), + }, + ], + [] + ) + + useRegisterPage( + 'Subpage', + { + type: PageType.Commands, + sections: [ + { + id: 'subpage', + name: 'Subpage', + commands, + }, + ], + }, + { deps: [commands] } + ) + + useRegisterCommands('Commands', [ + { + id: 'subpage', + name: 'Go to subpage', + action: () => setPage('Subpage'), + }, + ]) + + return null +} + +function CommandMenuTrigger() { + return ( + + + + ) +} + +export default function CommandMenuDemo() { + return ( + + + }> + + + + + ) +} diff --git a/apps/design-system/registry/default/example/expanding-textarea-demo.tsx b/apps/design-system/registry/default/example/expanding-textarea-demo.tsx new file mode 100644 index 00000000000..464d1db42ab --- /dev/null +++ b/apps/design-system/registry/default/example/expanding-textarea-demo.tsx @@ -0,0 +1,16 @@ +import { useState } from 'react' +import { ExpandingTextArea } from 'ui' + +export default function ExpandingTextareaDemo() { + const [value, setValue] = useState('') + + return ( + { + setValue(event.target.value) + }} + /> + ) +} diff --git a/apps/design-system/registry/default/example/sonner-demo.tsx b/apps/design-system/registry/default/example/sonner-demo.tsx new file mode 100644 index 00000000000..d2687e6071e --- /dev/null +++ b/apps/design-system/registry/default/example/sonner-demo.tsx @@ -0,0 +1,21 @@ +import { toast } from 'sonner' +import { Button } from 'ui' + +export default function SonnerDemo() { + return ( + + ) +} diff --git a/apps/design-system/registry/default/example/sonner-types.tsx b/apps/design-system/registry/default/example/sonner-types.tsx new file mode 100644 index 00000000000..0fc1212baee --- /dev/null +++ b/apps/design-system/registry/default/example/sonner-types.tsx @@ -0,0 +1,97 @@ +import { toast } from 'sonner' +import { Button } from 'ui' + +export default function SonnerDemo() { + const promise = () => + new Promise((resolve) => setTimeout(() => resolve({ name: 'Sonner' }), 2000)) + + return ( +
+ + + + + + + + + + + + + ) + } + > + Custom + +
+ ) +} diff --git a/apps/design-system/registry/default/example/sonner-upload.tsx b/apps/design-system/registry/default/example/sonner-upload.tsx new file mode 100644 index 00000000000..dcc25b45692 --- /dev/null +++ b/apps/design-system/registry/default/example/sonner-upload.tsx @@ -0,0 +1,88 @@ +import { Check, Loader2 } from 'lucide-react' +import { useState } from 'react' +import { toast } from 'sonner' +import { Button, Progress } from 'ui' + +export default function SonnerUpload() { + const [isUploading, setIsUploadingState] = useState(false) + + const uploadPromise = (files: number, toastId: number) => { + toast.loading(, { + id: toastId, + }) + + return new Promise((resolve) => { + let currentProgress = 0 + + toast.loading(, { + id: toastId, + }) + + const intervalId = setInterval(() => { + currentProgress += Math.floor(Math.random() * 10) + 1 // Increment progress by a random value + if (currentProgress > 100) currentProgress = 100 // Ensure progress does not exceed 100% + + if (currentProgress >= 100) { + clearInterval(intervalId) // Clear the interval once upload is complete + resolve({ progress: currentProgress, message: 'Upload complete', files: 3, toastId }) // Resolve the promise when progress reaches 100 + } + + toast.loading(, { + id: toastId, + }) + }, 200) // Adjust the interval time as needed + }) + } + + const UploadContent = ({ progress, files }: { progress: number; files: number }) => ( +
+ {progress >= 100 ? ( + + ) : ( + + )} +
+
+

+ {progress >= 100 + ? `Upload complete` + : files + ? `Uploading ${files} files...` + : `Uploading...`} +

+

{`${progress}%`}

+
+ + + Please do not close the browser until completed + +
+
+ ) + + return ( +
+ +
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 26a7386efbe..aebdd16585d 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -331,6 +331,54 @@ export const examples: Registry = [ registryDependencies: ['command', 'dialog'], files: ['example/command-dialog.tsx'], }, + { + name: 'commandmenu-demo', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-demo.tsx'], + }, + { + name: 'commandmenu-badge', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-badge.tsx'], + }, + { + name: 'commandmenu-icon', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-icon.tsx'], + }, + { + name: 'commandmenu-hidden', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-hidden.tsx'], + }, + { + name: 'commandmenu-force', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-force.tsx'], + }, + { + name: 'commandmenu-conditional', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-conditional.tsx'], + }, + { + name: 'commandmenu-subpage', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-subpage.tsx'], + }, + { + name: 'commandmenu-subpage-custom', + type: 'components:example', + registryDependencies: ['commandmenu'], + files: ['example/commandmenu-subpage-custom.tsx'], + }, { name: 'context-menu-demo', type: 'components:example', @@ -665,12 +713,24 @@ export const examples: Registry = [ registryDependencies: ['slider'], files: ['example/slider-demo.tsx'], }, - // { - // name: 'sonner-demo', - // type: 'components:example', - // registryDependencies: ['sonner'], - // files: ['example/sonner-demo.tsx'], - // }, + { + name: 'sonner-demo', + type: 'components:example', + registryDependencies: ['sonner'], + files: ['example/sonner-demo.tsx'], + }, + { + name: 'sonner-types', + type: 'components:example', + registryDependencies: ['sonner'], + files: ['example/sonner-types.tsx'], + }, + { + name: 'sonner-upload', + type: 'components:example', + registryDependencies: ['sonner'], + files: ['example/sonner-upload.tsx'], + }, { name: 'switch-demo', type: 'components:example', @@ -1050,6 +1110,16 @@ export const examples: Registry = [ type: 'components:example', files: ['example/modal-hide-footer.tsx'], }, + { + name: 'assistant-chat-demo', + type: 'components:example', + files: ['example/assistant-chat-demo.tsx'], + }, + { + name: 'assistant-chat-commands', + type: 'components:example', + files: ['example/assistant-chat-commands.tsx'], + }, { name: 'color-usage-surface-www-and-docs', type: 'docs:example', @@ -1065,4 +1135,9 @@ export const examples: Registry = [ type: 'docs:example', files: ['example/color-usage-surface-studio-frame.tsx'], }, + { + name: 'expanding-textarea-demo', + type: 'docs:example', + files: ['example/expanding-textarea-demo.tsx'], + }, ] diff --git a/apps/docs/app/contributing/ContributingToC.tsx b/apps/docs/app/contributing/ContributingToC.tsx new file mode 100644 index 00000000000..e1d8423f89a --- /dev/null +++ b/apps/docs/app/contributing/ContributingToC.tsx @@ -0,0 +1,82 @@ +'use client' + +import { Menu } from 'lucide-react' +import type { HTMLAttributes } from 'react' +import { useEffect, useState } from 'react' + +import { useBreakpoint } from 'common' +import { cn, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui' + +interface TocItem extends HTMLAttributes { + label: string + anchor: string +} + +export function ContributingToc({ className }: { className?: string }) { + const mobileToc = useBreakpoint('xl') + const [tocItems, setTocItems] = useState>([]) + + useEffect(() => { + const headings = [ + ...document.querySelectorAll('article.prose > h2,h3'), + ] as Array + const tocItems = headings + .filter((heading) => !!heading.id) + .map((heading) => ({ + label: heading.textContent.substring(0, heading.textContent.length - 1), // Remove ending `#` + anchor: heading.id, + })) + setTocItems(tocItems) + }, []) + + return mobileToc ? ( + + ) : ( + + ) +} + +function MobileToc({ items, className }: { items: Array; className?: string }) { + const [open, setOpen] = useState(false) + + return ( + + + + + {open ? 'Close table of contents' : 'Open table of contents'} + + + + + + + ) +} + +function TocBase({ items, className }: { items: Array; className?: string }) { + return ( + + ) +} diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx new file mode 100644 index 00000000000..cbee66c3616 --- /dev/null +++ b/apps/docs/app/contributing/content.mdx @@ -0,0 +1,399 @@ +# Contributing to Supabase Docs + +Thanks for contributing to Supabase Docs! Here are a few resources to help you get started. + +The code and content for our docs site are located in the main [Supabase GitHub repo](https://github.com/supabase/supabase), under the `apps/docs` directory. + +In the repo, you'll also find: + +- The [developers guide](https://github.com/supabase/supabase/blob/master/apps/docs/DEVELOPERS.md), which will help you set up your local machine to develop the docs site +- The [contributing guide](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md), which goes over the content organization and some general guidelines for writing docs content + +## Components + +Our docs content is mainly written in MDX. Aside from standard GitHub-flavored Markdown, you can use the following helper components to help you organize and display your content: + +### Accordion + +For content that requires progressive disclosure: + +```mdx + +
+ + + Your content here. + + + +
+
+ + + More content here. + + + +
+
+``` + + +
+ + + Your content here. + + + +
+
+ + + More content here. + + + +
+
+ +### Admonition + +For extra information that doesn't fit into the main flow. There are 5 supported types of admonitions: + +- `danger` to warn the user about any missteps that could cause data loss or data leaks +- `deprecation` to notify the user about features that are (or will soon be) deprecated +- `caution` to warn about anything that could cause a bug or serious user inconvenience +- `tip` to point out helpful but optional actions +- `note` for anything else + +Leave a blank line between the admonition tag and the contained content. This will prevent Prettier from trying to break the lines within the content. + +```mdx + + +This could lead to data loss! + + + + + +This feature is deprecated. + + + + + +You should make sure you don't set this up wrong. + + + + + +In certain cases, you may want to do this. + + + + + +Additional helpful information. + + +``` + + + +This could lead to data loss! + + + + + +This feature is deprecated. + + + + + +You should make sure you don't set this up wrong. + + + + + +In certain cases, you may want to do this. + + + + + +Additional helpful information. + + + +### Icons + +The following icons are available. They can be styled with [Tailwind](https://tailwindcss.com/) classes: + +```mdx + + + +``` + +
+ + + +
+ +### Image + +You can include images with regular Markdown syntax: + +```mdx +![Supabase architectural diagram](/docs/img/supabase-architecture.svg) +``` + +![Supabase architectural diagram](/docs/img/supabase-architecture.svg) + +If your image has alternate light and dark versions, or you want to make it zoomable, you can also use the image component: + +```mdx +Supabase architectural diagram +``` + +Supabase architectural diagram + +### Project Variables + +Some guides and tutorials will require that users copy their Supabase project URL and anon key. You can provide those inline if the user is signed in: + +```mdx + + +``` + + + + +### Step Hike + +For tutorials, which feature step-by-step instructions, often with accompanying code, we use the `StepHike` pattern: + +````mdx + + + + + + + Explanation of what to do first. + + + + + + ```sql + select ... + ``` + + + + + + + + + + Explanation of what to do next. This stretches the full width of the section: Sweet tiramisu apple biscuit candy cake. Orange ipsum muffin cookie cake biscuit. Orange muffin vanilla sweet sugar candy. Sprinkles jelly sweet orange candy cream. + + + + + + +```` + + + + + + + + Explanation of what to do first. + + + + + + ```sql + select ... + ``` + + + + + + + + + + Explanation of what to do next. This stretches the full width of the section: Sweet tiramisu apple biscuit candy cake. Orange ipsum muffin cookie cake biscuit. Orange muffin vanilla sweet sugar candy. Sprinkles jelly sweet orange candy cream. + + + + + + + + +### Tabs + +Use tabs when users can select between multiple versions of the content. For example, the content might differ based on language or package manager. + +If you include the `queryGroup` prop, the user's selection will sync with other tab groups. Leave out this prop to omit this behavior. + +````mdx + + + +```js +const supabase = createSupabaseClient() +``` + + + + +```dart +void main() async { + Supabase.initialize(); +} +``` + + + +```` + + + + +```js +const supabase = createSupabaseClient() +``` + + + + +```dart +void main() async { + Supabase.initialize(); +} +``` + + + + +## Partials + +We incorporate content reuse in the docs to avoid duplication. If you find yourself writing the same content over and over, you can put it in a partial instead. Here are some examples of commonly used partials: + + +
+ + + ```mdx + + ``` + + + + + +
+
+ + + ```mdx + + ``` + + + + + +
+
+ +To make a new partial: + +1. Make a new MDX file in `apps/docs/components/MDX`. +1. Write your reusable content. +1. Inside `apps/docs/components/MDX/partials.tsx`, import and re-export your partial. +1. Inside `apps/docs/features/docs/mdx.shared.tsx`, import your partial and include it in the `components` object. +1. You can now use your partial inside any other MDX file by using: ``. diff --git a/apps/docs/app/contributing/page.tsx b/apps/docs/app/contributing/page.tsx new file mode 100644 index 00000000000..666836b4d07 --- /dev/null +++ b/apps/docs/app/contributing/page.tsx @@ -0,0 +1,26 @@ +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { ContributingToc } from '~/app/contributing/ContributingToC' +import { MDXProviderGuides } from '~/features/docs/GuidesMdx.client' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import { SidebarSkeleton } from '~/layouts/MainSkeleton' + +export default async function ContributingPage() { + const contentFile = join(dirname(fileURLToPath(import.meta.url)), 'content.mdx') + const content = await readFile(contentFile, 'utf-8') + + return ( + +
+
+ + + +
+ +
+
+ ) +} diff --git a/apps/docs/app/error.tsx b/apps/docs/app/error.tsx new file mode 100644 index 00000000000..3c721fbeb9d --- /dev/null +++ b/apps/docs/app/error.tsx @@ -0,0 +1,22 @@ +'use client' + +import Link from 'next/link' +import { Button } from 'ui' + +const ErrorPage = () => ( +
+ + Sorry, something went wrong + +
+ + +
+
+) + +export default ErrorPage diff --git a/apps/docs/app/guides/[...slug]/page.tsx b/apps/docs/app/guides/[...slug]/page.tsx new file mode 100644 index 00000000000..2bbfc101eea --- /dev/null +++ b/apps/docs/app/guides/[...slug]/page.tsx @@ -0,0 +1,19 @@ +import { + getGuidesMarkdown, + genGuideMeta, + genGuidesStaticParams, +} from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' + +type Params = { slug: string[] } + +const GuidePage = async ({ params }: { params: Params }) => { + const data = await getGuidesMarkdown(params) + return +} + +const generateStaticParams = genGuidesStaticParams() +const generateMetadata = genGuideMeta(getGuidesMarkdown) + +export default GuidePage +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/ai/python/[slug]/page.tsx new file mode 100644 index 00000000000..1882cf7e20c --- /dev/null +++ b/apps/docs/app/guides/ai/python/[slug]/page.tsx @@ -0,0 +1,134 @@ +import { type SerializeOptions } from 'next-mdx-remote/dist/types' +import { redirect } from 'next/navigation' +import { relative } from 'path' +import rehypeSlug from 'rehype-slug' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { notFoundLink } from '~/features/recommendations/NotFound.utils' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' + +// We fetch these docs at build time from an external repo +const org = 'supabase' +const repo = 'vecs' +const branch = 'main' +const docsDir = 'docs' +const externalSite = 'https://supabase.github.io/vecs' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + slug: 'api', + meta: { + title: 'API', + }, + remoteFile: 'api.md', + }, + { + slug: 'collections', + meta: { + title: 'Collections', + }, + remoteFile: 'concepts_collections.md', + }, + { + slug: 'indexes', + meta: { + title: 'Indexes', + }, + remoteFile: 'concepts_indexes.md', + }, + { + slug: 'metadata', + meta: { + title: 'Metadata', + }, + remoteFile: 'concepts_metadata.md', + }, +] + +interface Params { + slug: string +} + +const PythonClientDocs = async ({ params }: { params: Params }) => { + const { meta, ...data } = await getContent(params) + + const options = { + mdxOptions: { + remarkPlugins: [remarkMkDocsAdmonition, [removeTitle, meta.title]], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + } as SerializeOptions + + return +} + +/** + * Fetch markdown from external repo + */ +const getContent = async ({ slug }: Params) => { + const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) + + if (!page) { + redirect(notFoundLink(`api/python/${slug}`)) + } + + const { remoteFile, meta } = page + + const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) + + const response = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + ) + + const content = await response.text() + + return { + pathname: `/guides/ai/python/${slug}` satisfies `/${string}`, + meta, + content, + editLink, + } +} + +const urlTransform: UrlTransformFunction = (url) => { + try { + const externalSiteUrl = new URL(externalSite) + + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const relativePage = ( + pathname.endsWith('.md') + ? pathname.replace(/\.md$/, '') + : relative(externalSiteUrl.pathname, pathname) + ).replace(/^\//, '') + + const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile) + + // If we have a mapping for this page, use the mapped path + if (page) { + return page.slug + hash + } + + // If we don't have this page in our docs, link to original docs + return `${externalSite}/${relativePage}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug })) +const generateMetadata = genGuideMeta(getContent) + +export default PythonClientDocs +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/pages/guides/cli/config.tsx b/apps/docs/app/guides/cli/config/page.tsx similarity index 56% rename from apps/docs/pages/guides/cli/config.tsx rename to apps/docs/app/guides/cli/config/page.tsx index 89b16e55973..4707208e7e0 100644 --- a/apps/docs/pages/guides/cli/config.tsx +++ b/apps/docs/app/guides/cli/config/page.tsx @@ -1,60 +1,45 @@ -import Head from 'next/head' import ReactMarkdown from 'react-markdown' - -import { CodeBlock, cn } from 'ui' - +import { CodeBlock } from 'ui' import { Heading } from 'ui/src/components/CustomHTMLElements' -import GuidesTableOfContents from '~/components/GuidesTableOfContents' -import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' -import { MainSkeleton } from '~/layouts/MainSkeleton' -import { Parameter } from '~/lib/refGenerator/refTypes' +import { type TOCHeader } from '~/components/GuidesTableOfContents' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import type { Parameter } from '~/lib/refGenerator/refTypes' import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' } -// Parameters are grouped on the page by tag -const tocList = [] -const content = specFile.info.tags.map((tag) => { - tocList.push({ text: tag.title, link: `${tag.id}-config`, level: 2 }) +const meta = { + title: 'Supabase CLI config', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/cli/config', + meta, +})) + +const tocList: TOCHeader[] = [] +const content = specFile.info.tags.map((tag: { id: string; title: string }, id: number) => { + tocList.push({ id: `${id}`, text: tag.title, link: `${tag.id}-config`, level: 2 }) return (
{tag.title} Config {specFile.parameters - .filter((param: Parameter) => param.tags[0] === tag.id) - .map((parameter: Parameter) => { - tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 }) + .filter((param: Parameter) => param.tags && param.tags[0] === tag.id) + .map((parameter: Parameter, id: number) => { + tocList.push({ id: `${id}`, text: parameter.id, link: `#${parameter.id}`, level: 3 }) return })}
) }) -export default function Config() { +const Config = () => { + const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml') + return ( - <> - - Supabase CLI config - - -
-
-
-

CLI configuration

-
- {specFile.info.description} -
{content}
-
-
- -
-
- + + {specFile.info.description} +
{content}
+
) } @@ -81,7 +66,7 @@ function Info({ parameter }: { parameter: Parameter }) { {parameter.id} {parameter.default ? parameter.default.toString() : 'None'} - {parameter.required.toString()} + {parameter.required !== undefined && parameter.required.toString()} @@ -116,3 +101,6 @@ function Info({ parameter }: { parameter: Parameter }) { ) } + +export default Config +export { generateMetadata } diff --git a/apps/docs/app/guides/cli/github-action/[slug]/page.tsx b/apps/docs/app/guides/cli/github-action/[slug]/page.tsx new file mode 100644 index 00000000000..5ec1e903952 --- /dev/null +++ b/apps/docs/app/guides/cli/github-action/[slug]/page.tsx @@ -0,0 +1,134 @@ +import { type SerializeOptions } from 'next-mdx-remote/dist/types' +import { redirect } from 'next/navigation' +import { relative } from 'node:path' +import rehypeSlug from 'rehype-slug' + +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { notFoundLink } from '~/features/recommendations/NotFound.utils' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' + +// We fetch these docs at build time from an external repo +const org = 'supabase' +const repo = 'setup-cli' +const branch = 'gh-pages' +const docsDir = 'docs' +const externalSite = 'https://supabase.github.io/setup-cli' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + slug: 'generating-types', + meta: { + title: 'Generate types using GitHub Actions', + description: 'End-to-end type safety across client, server, and database.', + subtitle: 'End-to-end type safety across client, server, and database.', + tocVideo: 'VSNgAIObBdw', + }, + remoteFile: 'generating-types.md', + }, + { + slug: 'testing', + meta: { + title: 'Automated testing using GitHub Actions', + description: 'Run your tests when you or your team make changes.', + subtitle: 'Run your tests when you or your team make changes.', + }, + remoteFile: 'testing.md', + }, + { + slug: 'backups', + meta: { + title: 'Automated backups using GitHub Actions', + description: 'Backup your database on a regular basis.', + subtitle: 'Backup your database on a regular basis.', + }, + remoteFile: 'backups.md', + }, +] + +type Params = { slug: string } + +const ActionDocs = async ({ params }: { params: Params }) => { + const { meta, ...data } = await getContent(params) + + const options = { + mdxOptions: { + remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + } as SerializeOptions + + return +} + +/** + * Fetch markdown from external repo + */ +const getContent = async ({ slug }: Params) => { + const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) + + if (!page) { + redirect(notFoundLink(`cli/github-action/${slug}`)) + } + + const { remoteFile, meta } = page + + const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) + + const response = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + ) + + const content = await response.text() + + return { + pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`, + meta, + content, + editLink, + } +} + +const urlTransform: UrlTransformFunction = (url) => { + try { + const externalSiteUrl = new URL(externalSite) + + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const relativePage = ( + pathname.endsWith('.md') + ? pathname.replace(/\.md$/, '') + : relative(externalSiteUrl.pathname, pathname) + ).replace(/^\//, '') + + const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile) + + // If we have a mapping for this page, use the mapped path + if (page) { + return page.slug + hash + } + + // If we don't have this page in our docs, link to original docs + return `${externalSite}/${relativePage}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug })) +const generateMetadata = genGuideMeta(getContent) + +export default ActionDocs +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/pages/guides/database/database-advisors.tsx b/apps/docs/app/guides/database/database-advisors/page.tsx similarity index 52% rename from apps/docs/pages/guides/database/database-advisors.tsx rename to apps/docs/app/guides/database/database-advisors/page.tsx index a699383dce3..184c8fee410 100644 --- a/apps/docs/pages/guides/database/database-advisors.tsx +++ b/apps/docs/app/guides/database/database-advisors/page.tsx @@ -1,36 +1,37 @@ -import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx' import { Octokit } from '@octokit/core' import { capitalize } from 'lodash' -import { GetStaticProps, InferGetStaticPropsType } from 'next' -import { MDXRemote } from 'next-mdx-remote' -import { serialize } from 'next-mdx-remote/serialize' -import remarkGfm from 'remark-gfm' +import { type SerializeOptions } from 'next-mdx-remote/dist/types' import rehypeSlug from 'rehype-slug' -import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' } - -import components from '~/components' import { Heading } from 'ui' -import { TabPanel, Tabs } from '~/components/Tabs' -import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' -import Layout from '~/layouts/DefaultGuideLayout' + +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { Tabs, TabPanel } from '~/features/ui/Tabs' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' // We fetch these docs at build time from an external repo -export const org = 'supabase' -export const repo = 'splinter' -export const branch = 'main' -export const docsDir = 'docs' +const org = 'supabase' +const repo = 'splinter' +const branch = 'main' +const docsDir = 'docs' const meta = { title: 'Performance and Security Advisors', subtitle: 'Check your database for performance and security issues', } -const editLink = 'https://github.com/supabase/splinter/tree/main/docs' +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/database/database-linter', + meta, +})) + +const editLink = newEditLink('supabase/splinter/tree/main/docs') const markdownIntro = ` You can use the Database Performance and Security Advisors to check your database for issues such as missing indexes and improperly set-up RLS policies. @@ -40,15 +41,21 @@ You can use the Database Performance and Security Advisors to check your databas In the dashboard, navigate to [Security Advisor](https://supabase.com/dashboard/project/_/database/security-advisor) and [Performance Advisor](https://supabase.com/dashboard/project/_/database/performance-advisor) under Database. The advisors run automatically. You can also manually rerun them after you've resolved issues. `.trim() -const getBasename = (path: string) => path.split('/').at(-1).replace(/\.md$/, '') +const getBasename = (path: string) => path.split('/').at(-1)!.replace(/\.md$/, '') + +const DatabaseAdvisorDocs = async () => { + const { lints, lintsList } = await getLints() + + const options = { + mdxOptions: { + remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]], + rehypePlugins: [[linkTransform, urlTransform(lintsList)], rehypeSlug], + }, + } as SerializeOptions -export default function DatabaseAdvisorDocs({ - intro, - lints, -}: InferGetStaticPropsType) { return ( - - + + Available checks {lints.map((lint) => ( @@ -58,12 +65,12 @@ export default function DatabaseAdvisorDocs({ label={capitalize(getBasename(lint.path).replace(/_/g, ' '))} >
- +
))}
-
+
) } @@ -101,42 +108,11 @@ const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = ( } } -const transformMarkdown = async ( - rawContent: string, - { replacementLinks = [] }: { replacementLinks?: Array<{ path: string }> } = {} -) => { - const codeHikeOptions: CodeHikeConfig = { - theme: codeHikeTheme, - lineNumbers: true, - showCopyButton: true, - skipLanguages: [], - autoImport: false, - } - - const content = await serialize(rawContent, { - scope: { - chCodeConfig: codeHikeOptions, - }, - mdxOptions: { - remarkPlugins: [ - remarkGfm, - remarkMkDocsAdmonition, - remarkPyMdownTabs, - [removeTitle, meta.title], - [remarkCodeHike, codeHikeOptions], - ], - rehypePlugins: [[linkTransform, urlTransform(replacementLinks)], rehypeSlug], - }, - }) - - return content -} - /** - * Fetch markdown from external repo and transform links + * Fetch lint remediation Markdown from external repo */ -export const getStaticProps = (async () => { - const octokit = new Octokit() +const getLints = async () => { + const octokit = new Octokit({ request: { fetch: fetchRevalidatePerDay } }) const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: org, @@ -154,37 +130,33 @@ export const getStaticProps = (async () => { if (!Array.isArray(response.data)) { throw Error( - `Reading a directory, not a file. Should not reach this, solely to appease Typescript.` + 'Reading a directory, not a file. Should not reach this, solely to appease Typescript.' ) } - const [intro, ...lints] = await Promise.all([ - await transformMarkdown(markdownIntro), - ...response.data - .filter(({ path }) => /docs\/\d+.+\.md$/.test(path)) - .map(async ({ path }, _, data) => { - const fileResponse = await fetch( - `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}` - ) + const lintsList = response.data.filter(({ path }) => /docs\/\d+.+\.md$/.test(path)) - if (fileResponse.status >= 400) { - throw Error(`Could not get contents of file ${org}/${repo}/${path}`) - } + const lints = await Promise.all( + lintsList.map(async ({ path }) => { + const fileResponse = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}` + ) - const rawContent = await fileResponse.text() - const content = await transformMarkdown(rawContent, { replacementLinks: data }) + if (fileResponse.status >= 400) { + throw Error(`Could not get contents of file ${org}/${repo}/${path}`) + } - return { - path: getBasename(path), - content, - } - }), - ]) + const content = await fileResponse.text() - return { - props: { - intro, - lints, - }, - } -}) satisfies GetStaticProps + return { + path: getBasename(path), + content, + } + }) + ) + + return { lints, lintsList } +} + +export default DatabaseAdvisorDocs +export { generateMetadata } diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx new file mode 100644 index 00000000000..836657bce71 --- /dev/null +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -0,0 +1,222 @@ +import matter from 'gray-matter' +import { type SerializeOptions } from 'next-mdx-remote/dist/types' +import { readFile } from 'node:fs/promises' +import { join, relative } from 'node:path' +import rehypeSlug from 'rehype-slug' +import emoji from 'remark-emoji' + +import { genGuideMeta, genGuidesStaticParams } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' + +// We fetch these docs at build time from an external repo +const org = 'supabase' +const repo = 'wrappers' +const branch = 'main' +const docsDir = 'docs' +const externalSite = 'https://supabase.github.io/wrappers' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + slug: 'airtable', + meta: { + title: 'Airtable', + }, + remoteFile: 'airtable.md', + }, + { + slug: 'auth0', + meta: { + title: 'Auth0', + }, + remoteFile: 'auth0.md', + }, + { + slug: 'bigquery', + meta: { + title: 'BigQuery', + }, + remoteFile: 'bigquery.md', + }, + { + slug: 'clickhouse', + meta: { + title: 'ClickHouse', + }, + remoteFile: 'clickhouse.md', + }, + { + slug: 'cognito', + meta: { + title: 'AWS Cognito', + }, + remoteFile: 'cognito.md', + }, + { + slug: 'firebase', + meta: { + title: 'Firebase', + }, + remoteFile: 'firebase.md', + }, + { + slug: 'logflare', + meta: { + title: 'Logflare', + }, + remoteFile: 'logflare.md', + }, + { + slug: 'mssql', + meta: { + title: 'MSSQL', + }, + remoteFile: 'mssql.md', + }, + { + slug: 'redis', + meta: { + title: 'Redis', + }, + remoteFile: 'redis.md', + }, + { + slug: 's3', + meta: { + title: 'AWS S3', + }, + remoteFile: 's3.md', + }, + { + slug: 'stripe', + meta: { + title: 'Stripe', + }, + remoteFile: 'stripe.md', + }, +] + +interface Params { + slug?: string[] +} + +const WrappersDocs = async ({ params }: { params: Params }) => { + const { isExternal, meta, ...data } = await getContent(params) + + const options = isExternal + ? ({ + mdxOptions: { + remarkPlugins: [ + remarkMkDocsAdmonition, + emoji, + remarkPyMdownTabs, + [removeTitle, meta.title], + ], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + } as SerializeOptions) + : undefined + + return +} + +/** + * Fetch markdown from external repo + */ +const getContent = async (params: Params) => { + const federatedPage = pageMap.find( + ({ slug }) => params && slug && params.slug && slug === params.slug.at(0) + ) + + let isExternal: boolean + let meta: any + let content: string + let editLink: string + + if (!federatedPage) { + isExternal = false + editLink = `supabase/supabase/apps/docs/content/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx` + const rawContent = await readFile( + join( + GUIDES_DIRECTORY, + 'database', + 'extensions', + `wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx` + ), + 'utf-8' + ) + ;({ data: meta, content } = matter(rawContent)) + if (!isValidGuideFrontmatter(meta)) { + throw Error(`Expected valid frontmatter, got ${JSON.stringify(meta, null, 2)}`) + } + } else { + isExternal = true + let remoteFile: string + ;({ remoteFile, meta } = federatedPage) + const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` + const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`) + content = await response.text() + } + + return { + pathname: + `/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` satisfies `/${string}`, + isExternal, + editLink: newEditLink(editLink), + meta, + content, + } +} + +const urlTransform: UrlTransformFunction = (url) => { + try { + const externalSiteUrl = new URL(externalSite) + + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + const relativePage = ( + pathname.endsWith('.md') + ? pathname.replace(/\.md$/, '') + : relative(externalSiteUrl.pathname, pathname) + ).replace(/^\//, '') + + const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile) + + // If we have a mapping for this page, use the mapped path + if (page) { + return page.slug + hash + } + + // If we don't have this page in our docs, link to original docs + return `${externalSite}/${relativePage}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +const generateStaticParams = async () => { + const mdxPaths = await genGuidesStaticParams('database/extensions/wrappers')() + const federatedPaths = pageMap.map(({ slug }) => ({ + slug: [slug], + })) + + return [...mdxPaths, ...federatedPaths] +} + +const generateMetadata = genGuideMeta(getContent) + +export default WrappersDocs +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx new file mode 100644 index 00000000000..0034e6beece --- /dev/null +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -0,0 +1,194 @@ +import { type SerializeOptions } from 'next-mdx-remote/dist/types' +import { redirect } from 'next/navigation' +import { isAbsolute, relative } from 'path' +import rehypeSlug from 'rehype-slug' + +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { notFoundLink } from '~/features/recommendations/NotFound.utils' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' + +// We fetch these docs at build time from an external repo +const org = 'supabase' +const repo = 'pg_graphql' +const branch = 'master' +const docsDir = 'docs' +const externalSite = 'https://supabase.github.io/pg_graphql' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + meta: { + id: 'graphql-overview', + title: 'GraphQL', + subtitle: 'Autogenerated GraphQL APIs with Postgres.', + }, + remoteFile: 'supabase.md', + }, + { + slug: 'api', + meta: { + id: 'graphql-api', + title: 'GraphQL API', + subtitle: 'Understanding the core concepts of the GraphQL API.', + }, + remoteFile: 'api.md', + }, + { + slug: 'views', + meta: { + id: 'graphql-views', + title: 'Views', + subtitle: 'Using Postgres Views with GraphQL.', + }, + remoteFile: 'views.md', + }, + { + slug: 'functions', + meta: { + id: 'graphql-functions', + title: 'Functions', + subtitle: 'Using Postgres Functions with GraphQL.', + }, + remoteFile: 'functions.md', + }, + { + slug: 'computed-fields', + meta: { + id: 'graphql-computed-fields', + title: 'Computed Fields', + subtitle: 'Using Postgres Computed Fields with GraphQL.', + }, + remoteFile: 'computed-fields.md', + }, + { + slug: 'configuration', + meta: { + id: 'graphql-configuration', + title: 'Configuration & Customization', + subtitle: 'Extra configuration options can be set on SQL entities using comment directives.', + }, + remoteFile: 'configuration.md', + }, + { + slug: 'security', + meta: { + id: 'graphql-security', + title: 'Security', + subtitle: 'Securing your GraphQL API.', + }, + remoteFile: 'security.md', + }, + { + slug: 'with-apollo', + meta: { + id: 'graphql-with-apollo', + title: 'With Apollo', + subtitle: 'Using pg_grapqhl with Apollo.', + }, + remoteFile: 'usage_with_apollo.md', + }, + { + slug: 'with-relay', + meta: { + id: 'graphql-with-relay', + title: 'With Relay', + subtitle: 'Using pg_grapqhl with Relay.', + }, + remoteFile: 'usage_with_relay.md', + }, +] + +interface Params { + slug?: string[] +} + +const PGGraphQLDocs = async ({ params }: { params: Params }) => { + const { meta, ...data } = await getContent(params) + + const options = { + mdxOptions: { + remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + } as SerializeOptions + + return +} + +/** + * Fetch markdown from external repo and transform links + */ +const getContent = async ({ slug }: Params) => { + const page = pageMap.find((page) => page.slug === slug?.at(0)) + + if (!page) { + redirect(notFoundLink(`graphql/${slug ? slug.join('/') : ''}`)) + } + + const { remoteFile, meta } = page + + const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) + + const response = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + ) + + const content = await response.text() + + return { + pathname: `/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`, + meta, + content, + editLink, + } +} + +const urlTransform: UrlTransformFunction = (url) => { + try { + const externalSiteUrl = new URL(externalSite) + + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const getRelativePath = () => { + if (pathname.endsWith('.md')) { + return pathname.replace(/\.md$/, '') + } + if (isAbsolute(url)) { + return relative(externalSiteUrl.pathname, pathname) + } + return pathname + } + + const relativePath = getRelativePath().replace(/^\//, '') + + const page = pageMap.find(({ remoteFile }) => `${relativePath}.md` === remoteFile) + + // If we have a mapping for this page, use the mapped path + if (page) { + return 'graphql/' + page.slug + hash + } + + // If we don't have this page in our docs, link to original docs + return `${externalSite}/${relativePath}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] })) +const generateMetadata = genGuideMeta(getContent) + +export default PGGraphQLDocs +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/app/guides/layout.tsx b/apps/docs/app/guides/layout.tsx new file mode 100644 index 00000000000..8df012ac97c --- /dev/null +++ b/apps/docs/app/guides/layout.tsx @@ -0,0 +1,16 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { type PropsWithChildren } from 'react' + +import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils' +import Layout from '~/layouts/guides' + +const GuidesLayout = ({ children }: PropsWithChildren) => { + const pathname = usePathname() + const menuId = getMenuId(pathname) + + return {children} +} + +export default GuidesLayout diff --git a/apps/docs/app/guides/platform/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/platform/terraform/[[...slug]]/page.tsx new file mode 100644 index 00000000000..f9cbcc62a0c --- /dev/null +++ b/apps/docs/app/guides/platform/terraform/[[...slug]]/page.tsx @@ -0,0 +1,142 @@ +import matter from 'gray-matter' +import { type SerializeOptions } from 'next-mdx-remote/dist/types' +import { redirect } from 'next/navigation' +import rehypeSlug from 'rehype-slug' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { notFoundLink } from '~/features/recommendations/NotFound.utils' +import { isValidGuideFrontmatter } from '~/lib/docs' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { + terraformDocsBranch, + terraformDocsDocsDir, + terraformDocsOrg, + terraformDocsRepo, +} from '../terraformConstants' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + remoteFile: 'README.md', + meta: { + title: 'Terraform Provider', + }, + useRoot: true, + }, + { + slug: 'tutorial', + remoteFile: 'tutorial.md', + meta: { + title: 'Using the Supabase Terraform Provider', + }, + }, +] + +interface Params { + slug?: string[] +} + +const TerraformDocs = async ({ params }: { params: Params }) => { + const { meta, ...data } = await getContent(params) + + const options = { + mdxOptions: { + remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + } as SerializeOptions + + return +} + +/** + * The GitHub repo uses relative links, which don't lead to the right locations + * in docs. + * + * @param url The original link, as written in the Markdown file + * @returns The rewritten link + */ +const urlTransform: UrlTransformFunction = (url: string) => { + try { + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const getBasename = (pathname: string) => + pathname.endsWith('.md') ? pathname.replace(/\.md$/, '') : pathname + const stripLeadingPrefix = (pathname: string) => pathname.replace(/^\//, '') + const stripLeadingDocs = (pathname: string) => pathname.replace(/^docs\//, '') + + const relativePath = stripLeadingPrefix(getBasename(pathname)) + + const page = pageMap.find( + ({ remoteFile, useRoot }) => + (useRoot && `${relativePath}.md` === remoteFile) || + (!useRoot && `${stripLeadingDocs(relativePath)}.md` === remoteFile) + ) + + if (page) { + return 'terraform' + `/${page.slug}` + hash + } + + // If we don't have this page in our docs, link to GitHub repo + return `https://github.com/${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}${pathname}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +/** + * Fetch markdown from external repo + */ +const getContent = async ({ slug }: Params) => { + const [requestedSlug] = slug ?? [] + const page = pageMap.find((page) => page.slug === requestedSlug) + + if (!page) { + redirect(notFoundLink('platform/terraform' + (slug?.join('/') ?? ''))) + } + + const { meta, remoteFile, useRoot } = page + + const editLink = newEditLink( + `${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}` + ) + + let response = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}` + ) + + let rawContent = await response.text() + // Strip out HTML comments + rawContent = rawContent.replace(//, '') + const { content, data } = matter(rawContent) + Object.assign(meta, data) + + if (!isValidGuideFrontmatter(meta)) { + throw Error('Guide frontmatter is invalid.') + } + + return { + pathname: + `/guides/platform/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`, + meta, + content, + editLink, + } +} + +const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] })) +const generateMetadata = genGuideMeta(getContent) + +export default TerraformDocs +export { generateStaticParams, generateMetadata } diff --git a/apps/docs/pages/guides/platform/terraform/reference.tsx b/apps/docs/app/guides/platform/terraform/reference/page.tsx similarity index 88% rename from apps/docs/pages/guides/platform/terraform/reference.tsx rename to apps/docs/app/guides/platform/terraform/reference/page.tsx index 8004ab606c9..c251d8133ea 100644 --- a/apps/docs/pages/guides/platform/terraform/reference.tsx +++ b/apps/docs/app/guides/platform/terraform/reference/page.tsx @@ -1,22 +1,36 @@ import { codeBlock } from 'common-tags' -import { GetStaticProps, InferGetStaticPropsType } from 'next' +import { Check, PlusCircle } from 'lucide-react' import Link from 'next/link' import ReactMarkdown from 'react-markdown' import { CodeBlock, Heading, - IconCheck, - IconPlusCircle, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, - Tabs, } from 'ui' -import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' -import Layout from '~/layouts/DefaultGuideLayout' -import { org, repo, branch, docsDir } from './[[...slug]]' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { TabPanel, Tabs } from '~/features/ui/Tabs' +import { + terraformDocsBranch, + terraformDocsDocsDir, + terraformDocsOrg, + terraformDocsRepo, +} from '../terraformConstants' + +const meta = { + title: 'Terraform Provider reference', + subtitle: 'Resources and data sources available through the Terraform Provider', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/platform/terraform/reference', + meta, +})) function ProviderSettings({ schema }: { schema: any }) { const attributes = schema.block.attributes @@ -63,7 +77,7 @@ function ProviderSettings({ schema }: { schema: any }) { {attributes[attribute].optional && ( <> - + true )} @@ -71,7 +85,7 @@ function ProviderSettings({ schema }: { schema: any }) { {attributes[attribute].sensitive && ( <> - + true )} @@ -92,7 +106,7 @@ function Resources({ schema }: { schema: any }) {

You can configure these resources using the Supabase Terraform provider:

{Object.keys(schema).map((resource) => ( - + Example usage {codeBlock` resource "${resource}" " + ))} @@ -216,7 +230,7 @@ function DataSources({ schema }: { schema: any }) {

You can read these resources using the Supabase Terraform provider:

{Object.keys(schema).map((dataSource) => ( - + Example usage {codeBlock` resource "${dataSource}" "all" { @@ -263,7 +277,7 @@ function DataSources({ schema }: { schema: any }) { @@ -306,7 +320,7 @@ function DataSources({ schema }: { schema: any }) { {schema[dataSource].block.attributes[attribute].required && ( <> - + true )} @@ -314,7 +328,7 @@ function DataSources({ schema }: { schema: any }) { {schema[dataSource].block.attributes[attribute].optional && ( <> - + true )} @@ -322,7 +336,7 @@ function DataSources({ schema }: { schema: any }) { {schema[dataSource].block.attributes[attribute].computed && ( <> - + true )} @@ -331,19 +345,20 @@ function DataSources({ schema }: { schema: any }) { ))} - + ))} ) } -export default function PGGraphQLDocs({ - meta, - schema, -}: InferGetStaticPropsType) { +const TerraformReferencePage = async () => { + const { schema } = await getSchema() + + const editLink = newEditLink('supabase/terraform-provider-supabase') + return ( - + The Terraform Provider provices access to{' '} - +
) } /** * Fetch JSON schema from external repo */ -export const getStaticProps = (async () => { - const meta = { - title: 'Terraform Provider reference', - subtitle: 'Resources and data sources available through the Terraform Provider', - } - - let response = await fetch( - `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/schema.json` +const getSchema = async () => { + let response = await fetchRevalidatePerDay( + `https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${terraformDocsDocsDir}/schema.json` ) if (!response.ok) throw Error('Failed to fetch Terraform JSON schema from GitHub') const schema = await response.json() return { - props: { - meta, - schema, - }, + schema, } -}) satisfies GetStaticProps +} + +export default TerraformReferencePage +export { generateMetadata } diff --git a/apps/docs/app/guides/platform/terraform/terraformConstants.ts b/apps/docs/app/guides/platform/terraform/terraformConstants.ts new file mode 100644 index 00000000000..0cc7ad3125b --- /dev/null +++ b/apps/docs/app/guides/platform/terraform/terraformConstants.ts @@ -0,0 +1,9 @@ +/** + * Information on where to fetch docs content + */ +const terraformDocsOrg = 'supabase' +const terraformDocsRepo = 'terraform-provider-supabase' +const terraformDocsBranch = 'v1.1.3' +const terraformDocsDocsDir = 'docs' + +export { terraformDocsOrg, terraformDocsRepo, terraformDocsBranch, terraformDocsDocsDir } diff --git a/apps/docs/app/guides/self-hosting/analytics/config/page.tsx b/apps/docs/app/guides/self-hosting/analytics/config/page.tsx new file mode 100644 index 00000000000..d397c0003f0 --- /dev/null +++ b/apps/docs/app/guides/self-hosting/analytics/config/page.tsx @@ -0,0 +1,65 @@ +import Param from '~/components/Params' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import specAnalyticsV0 from '~/spec/analytics_v0_config.yaml' assert { type: 'yml' } + +const meta = { + title: 'Analytics Self-hosting Config', + description: 'How to configure and deploy Supabase Analytics.', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/self-hosting/analytics/config', + meta, +})) + +const AnalyticsConfigPage = async () => { + const descriptionMdx = specAnalyticsV0.info.description + + return ( + + + +
+ {specAnalyticsV0.info.tags.map( + (tag: ReturnType['info']['tags']) => { + return ( + <> +

{tag.title}

+

{tag.description}

+
+
Parameters
+
    + {specAnalyticsV0.parameters + .filter((param: ReturnType['parameters']) => + param.tags.includes(tag.id) + ) + .map((param: ReturnType['parameters']) => { + return ( + + ) + })} +
+
+ + ) + } + )} +
+
+ ) +} + +export default AnalyticsConfigPage +export { generateMetadata } diff --git a/apps/docs/app/guides/self-hosting/auth/config/page.tsx b/apps/docs/app/guides/self-hosting/auth/config/page.tsx new file mode 100644 index 00000000000..d8a33d04694 --- /dev/null +++ b/apps/docs/app/guides/self-hosting/auth/config/page.tsx @@ -0,0 +1,63 @@ +import Param from '~/components/Params' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import specAuthV1 from '~/spec/gotrue_v1_config.yaml' assert { type: 'yml' } + +const meta = { + title: 'Auth Self-hosting Config', + description: 'How to configure and deploy Supabase Auth.', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/self-hosting/auth/config', + meta, +})) + +const AuthConfigPage = async () => { + const descriptionMdx = specAuthV1.info.description + + return ( + + + +
+ {specAuthV1.info.tags.map((tag: ReturnType['info']['tags']) => { + return ( + <> +

{tag.title}

+

{tag.description}

+
+
Parameters
+
    + {specAuthV1.parameters + .filter((param: ReturnType['parameters']) => + param.tags.includes(tag.id) + ) + .map((param: ReturnType['parameters']) => { + return ( + + ) + })} +
+
+ + ) + })} +
+
+ ) +} + +export default AuthConfigPage +export { generateMetadata } diff --git a/apps/docs/app/guides/self-hosting/realtime/config/page.tsx b/apps/docs/app/guides/self-hosting/realtime/config/page.tsx new file mode 100644 index 00000000000..01180ba859c --- /dev/null +++ b/apps/docs/app/guides/self-hosting/realtime/config/page.tsx @@ -0,0 +1,63 @@ +import Param from '~/components/Params' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import specRealtimeV0 from '~/spec/realtime_v0_config.yaml' assert { type: 'yml' } + +const meta = { + title: 'Realtime Self-hosting Config', + description: 'How to configure and deploy Supabase Realtime.', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/self-hosting/realtime/config', + meta, +})) + +const RealtimeConfigPage = async () => { + const descriptionMdx = specRealtimeV0.info.description + + return ( + + + +
+ {specRealtimeV0.info.tags.map((tag: ReturnType['info']['tags']) => { + return ( + <> +

{tag.title}

+

{tag.description}

+
+
Parameters
+
    + {specRealtimeV0.parameters + .filter((param: ReturnType['parameters']) => + param.tags.includes(tag.id) + ) + .map((param: ReturnType['parameters']) => { + return ( + + ) + })} +
+
+ + ) + })} +
+
+ ) +} + +export default RealtimeConfigPage +export { generateMetadata } diff --git a/apps/docs/app/guides/self-hosting/storage/config/page.tsx b/apps/docs/app/guides/self-hosting/storage/config/page.tsx new file mode 100644 index 00000000000..766bc4d190a --- /dev/null +++ b/apps/docs/app/guides/self-hosting/storage/config/page.tsx @@ -0,0 +1,63 @@ +import Param from '~/components/Params' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import specStorageV0 from '~/spec/storage_v0_config.yaml' assert { type: 'yml' } + +const meta = { + title: 'Storage Self-hosting Config', + description: 'How to configure and deploy Supabase Storage.', +} + +const generateMetadata = genGuideMeta(() => ({ + pathname: '/guides/self-hosting/storage/config', + meta, +})) + +const StorageConfigPage = async () => { + const descriptionMdx = specStorageV0.info.description + + return ( + + + +
+ {specStorageV0.info.tags.map((tag: ReturnType['info']['tags']) => { + return ( + <> +

{tag.title}

+

{tag.description}

+
+
Parameters
+
    + {specStorageV0.parameters + .filter((param: ReturnType['parameters']) => + param.tags.includes(tag.id) + ) + .map((param: ReturnType['parameters']) => { + return ( + + ) + })} +
+
+ + ) + })} +
+
+ ) +} + +export default StorageConfigPage +export { generateMetadata } diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx new file mode 100644 index 00000000000..a35a75df6e3 --- /dev/null +++ b/apps/docs/app/layout.tsx @@ -0,0 +1,58 @@ +import '@code-hike/mdx/styles' +import 'config/code-hike.scss' +import '../styles/main.scss' +import '../styles/new-docs.scss' +import '../styles/prism-okaidia.scss' + +import { genFaviconData } from 'common/MetaFavicons/app-router' +import { type Metadata, type Viewport } from 'next' + +import { BASE_PATH } from '~/lib/constants' +import { GlobalProviders } from '~/features/app.providers' +import { TopNavSkeleton } from '~/layouts/MainSkeleton' + +const metadata: Metadata = { + applicationName: 'Supabase Docs', + title: 'Supabase Docs', + description: + 'Supabase is an open source Firebase alternative providing all the backend features you need to build a product.', + metadataBase: new URL('https://supabase.com'), + icons: genFaviconData(BASE_PATH), + robots: { + index: true, + follow: true, + }, + openGraph: { + type: 'article', + authors: 'Supabase', + url: `${BASE_PATH}`, + images: `${BASE_PATH}/img/supabase-og-image.png`, + publishedTime: new Date().toISOString(), + modifiedTime: new Date().toISOString(), + }, + twitter: { + card: 'summary_large_image', + site: '@supabase', + creator: '@supabase', + images: `${BASE_PATH}/img/supabase-og-image.png`, + }, +} + +const viewport: Viewport = { + themeColor: '#1E1E1E', +} + +const RootLayout = ({ children }: { children: React.ReactNode }) => { + return ( + + + + {children} + + + + ) +} + +export { metadata, viewport } +export default RootLayout diff --git a/apps/docs/app/not-found/layout.tsx b/apps/docs/app/not-found/layout.tsx new file mode 100644 index 00000000000..b34cd4356b2 --- /dev/null +++ b/apps/docs/app/not-found/layout.tsx @@ -0,0 +1,20 @@ +import { type Metadata } from 'next' +import { type PropsWithChildren } from 'react' +import { LayoutMainContent } from '~/layouts/DefaultLayout' +import { SidebarSkeleton } from '~/layouts/MainSkeleton' + +const metadata: Metadata = { + title: 'Not found', + robots: { + index: false, + }, +} + +const NotFoundLayout = ({ children }: PropsWithChildren) => ( + + {children} + +) + +export default NotFoundLayout +export { metadata } diff --git a/apps/docs/app/not-found/page.tsx b/apps/docs/app/not-found/page.tsx new file mode 100644 index 00000000000..129aa70aec9 --- /dev/null +++ b/apps/docs/app/not-found/page.tsx @@ -0,0 +1,47 @@ +import { type Metadata } from 'next' +import Link from 'next/link' + +import { Button } from 'ui' + +import { SearchButton, Recommendations } from '~/features/recommendations/NotFound.client' + +const metadata: Metadata = { + robots: { + index: false, + follow: false, + }, +} + +const NotFoundPage = async ({ searchParams: { page } }: { searchParams: { page?: string } }) => { + return ( +
+

404: We couldn't find that page

+

+ Sorry, we couldn't find that page. It might be missing, or we had a temporary error + generating it. +

+
+ + + +
+ +
+ ) +} + +export default NotFoundPage +export { metadata } diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx new file mode 100644 index 00000000000..a272040b75e --- /dev/null +++ b/apps/docs/app/page.tsx @@ -0,0 +1,347 @@ +import { type Metadata, type ResolvingMetadata } from 'next' +import Link from 'next/link' +import { IconBackground, TextLink } from 'ui' +import { IconPanel } from 'ui-patterns/IconPanel' + +import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker' +import { GlassPanelWithIconPicker } from '~/features/ui/GlassPanelWithIconPicker' +import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker' +import HomeLayout from '~/layouts/HomeLayout' +import { BASE_PATH } from '~/lib/constants' + +const generateMetadata = async (_, parent: ResolvingMetadata): Promise => { + const parentAlternates = (await parent).alternates + + return { + alternates: { + ...parentAlternates, + canonical: `${BASE_PATH}`, + }, + } +} + +const products = [ + { + title: 'Database', + icon: 'database', + hasLightIcon: true, + href: '/guides/database/overview', + description: + 'Supabase provides a full Postgres database for every project with Realtime functionality, database backups, extensions, and more.', + }, + { + title: 'Auth', + icon: 'auth', + hasLightIcon: true, + href: '/guides/auth', + description: + 'Add and manage email and password, passwordless, OAuth, and mobile logins to your project through a suite of identity providers and APIs.', + }, + { + title: 'Storage', + icon: 'storage', + hasLightIcon: true, + href: '/guides/storage', + description: + 'Store, organize, transform, and serve large files—fully integrated with your Postgres database with Row Level Security access policies.', + }, + { + title: 'AI & Vectors', + icon: 'ai', + hasLightIcon: true, + href: '/guides/ai', + description: 'Use Supabase to store and search embedding vectors.', + }, + { + title: 'Realtime', + icon: 'realtime', + hasLightIcon: true, + href: '/guides/realtime', + description: + 'Listen to database changes, store and sync user states across clients, broadcast data to clients subscribed to a channel, and more.', + }, + { + title: 'Edge Functions', + icon: 'edge-functions', + hasLightIcon: true, + href: '/guides/functions', + description: + 'Globally distributed, server-side functions to execute your code closest to your users for the lowest latency.', + }, +] + +const migrationGuides = [ + { + title: 'Auth', + icon: '/docs/img/icons/auth0-icon', + href: '/guides/resources/migrating-to-supabase/auth0', + }, + { + title: 'Firebase Auth', + icon: '/docs/img/icons/firebase-icon', + href: '/guides/resources/migrating-to-supabase/firebase-auth', + }, + { + title: 'Firestore Data', + icon: '/docs/img/icons/firebase-icon', + href: '/guides/resources/migrating-to-supabase/firestore-data', + }, + { + title: 'Firebase Storage', + icon: '/docs/img/icons/firebase-icon', + href: '/guides/resources/migrating-to-supabase/firebase-storage', + }, + { + title: 'Heroku', + icon: '/docs/img/icons/heroku-icon', + href: '/guides/resources/migrating-to-supabase/heroku', + }, + { + title: 'Render', + icon: '/docs/img/icons/render-icon', + href: '/guides/resources/migrating-to-supabase/render', + }, + { + title: 'Amazon RDS', + icon: '/docs/img/icons/aws-rds-icon', + href: '/guides/resources/migrating-to-supabase/amazon-rds', + }, + { + title: 'Postgres', + icon: '/docs/img/icons/postgres-icon', + href: '/guides/resources/migrating-to-supabase/postgres', + }, + { + title: 'MySQL', + icon: '/docs/img/icons/mysql-icon', + href: '/guides/resources/migrating-to-supabase/mysql', + }, + { + title: 'MSSQL', + icon: '/docs/img/icons/mssql-icon', + href: '/guides/resources/migrating-to-supabase/mssql', + }, +] + +const selfHostingOptions = [ + { + title: 'Auth', + icon: 'auth', + href: '/reference/self-hosting-auth/introduction', + }, + { + title: 'Realtime', + icon: 'realtime', + href: '/reference/self-hosting-realtime/introduction', + }, + { + title: 'Storage', + icon: 'storage', + href: '/reference/self-hosting-storage/introduction', + }, + { + title: 'Analytics', + icon: 'analytics', + href: '/reference/self-hosting-analytics/introduction', + }, +] + +const clientLibraries = [ + { + title: 'Javascript', + icon: 'reference-javascript', + href: '/reference/javascript/introduction', + }, + { + title: 'Flutter', + icon: 'reference-dart', + href: '/reference/dart/introduction', + }, + { + title: 'Python', + icon: 'reference-python', + href: '/reference/python/introduction', + }, + { + title: 'C#', + icon: 'reference-csharp', + href: '/reference/csharp/introduction', + }, + { + title: 'Swift', + icon: 'reference-swift', + href: '/reference/swift/introduction', + }, + { + title: 'Kotlin', + icon: 'reference-kotlin', + href: '/reference/kotlin/introduction', + }, +] + +const additionalResources = [ + { + title: 'Management API', + description: 'Manage your Supabase projects and organizations.', + icon: 'reference-api', + href: '/reference/api/introduction', + }, + { + title: 'Supabase CLI', + description: 'Use the CLI to develop, manage and deploy your projects.', + icon: 'reference-cli', + href: '/reference/cli/introduction', + }, + { + title: 'Platform Guides', + description: 'Learn more about the tools and services powering Supabase.', + icon: 'platform', + href: '/guides/platform', + }, + { + title: 'Integrations', + description: 'Explore a variety of integrations from Supabase partners.', + icon: 'integrations', + href: '/guides/integrations', + }, +] + +const HomePage = () => ( + +
+

Products

+
    + {products.map((product) => { + return ( +
  • + + + {product.description} + + +
  • + ) + })} +
+ +
+
+
+
+

+ Client Libraries +

+
+
+
+ +
+ {clientLibraries.map((library) => { + return ( + + + + ) + })} +
+
+ +
+
+

+ Migrate to Supabase +

+

+ Bring your existing data, auth and storage to Supabase following our migration guides. +

+ +
+ +
    + {migrationGuides.map((guide) => { + return ( +
  • + + + +
  • + ) + })} +
+
+ +
+
+

+ Additional resources +

+
+ +
    + {additionalResources.map((resource) => { + return ( +
  • + + + {resource.description} + + +
  • + ) + })} +
+
+ +
+
+
+
+ + + +

+ Self-Hosting +

+
+

Get started with self-hosting Supabase.

+ +
+
+ +
+
    + {selfHostingOptions.map((option) => { + return ( +
  • + + + +
  • + ) + })} +
+
+
+
+
+) + +export default HomePage +export { generateMetadata } diff --git a/apps/docs/components/ApiSchema.tsx b/apps/docs/components/ApiSchema.tsx index 98ee6ab01e2..42a5b195566 100644 --- a/apps/docs/components/ApiSchema.tsx +++ b/apps/docs/components/ApiSchema.tsx @@ -1,6 +1,6 @@ import { CodeBlock } from '@ui/components/CodeBlock' import { sample } from '@har-sdk/openapi-sampler' -import { Tabs, TabPanel } from '~/components/Tabs' +import { Tabs, TabPanel } from '~/features/ui/Tabs' type IParamProps = any diff --git a/apps/docs/components/AppleSecretGenerator/index.tsx b/apps/docs/components/AppleSecretGenerator/index.tsx index ea66eadd23a..ea556ac7de9 100644 --- a/apps/docs/components/AppleSecretGenerator/index.tsx +++ b/apps/docs/components/AppleSecretGenerator/index.tsx @@ -1,11 +1,9 @@ -import { lazy, Suspense } from 'react' +'use client' -const Generator = lazy(() => import('./AppleSecretGenerator')) +import dynamic from 'next/dynamic' + +const Generator = dynamic(() => import('./AppleSecretGenerator'), { ssr: false }) export function AppleSecretGenerator() { - return ( - - - - ) + return } diff --git a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx index e0e74bcd7ac..2039a78854e 100644 --- a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx +++ b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx @@ -1,3 +1,5 @@ +'use client' + import { useEffect, useReducer, useRef } from 'react' import { PhoneLoginsItems } from '../Navigation/NavigationMenu/NavigationMenu.constants' import { IconPanel } from 'ui-patterns/IconPanel' diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx new file mode 100644 index 00000000000..c8225015f67 --- /dev/null +++ b/apps/docs/components/Breadcrumbs.tsx @@ -0,0 +1,159 @@ +'use client' + +import React, { Fragment } from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { + Breadcrumb_Shadcn_ as Breadcrumb, + BreadcrumbList_Shadcn_ as BreadcrumbList, + BreadcrumbItem_Shadcn_ as BreadcrumbItem, + BreadcrumbLink_Shadcn_ as BreadcrumbLink, + BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator, + BreadcrumbPage_Shadcn_ as BreadcrumbPage, + BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis, + Button, + cn, + Drawer, + DrawerClose, + DrawerContent, + DrawerFooter, + DrawerTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' +import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils' +import { useBreakpoint } from 'common' +import * as NavItems from './Navigation/NavigationMenu/NavigationMenu.constants' + +const Breadcrumbs = ({ className }: { className?: string }) => { + const pathname = usePathname() + const menuId = getMenuId(pathname) + const menu = NavItems[menuId] + const breadcrumbs = findMenuItemByUrl(menu, pathname, []) + const [open, setOpen] = React.useState(false) + const isMobile = useBreakpoint('md') + + const ITEMS_TO_DISPLAY = isMobile ? 4 : 3 + + if (!breadcrumbs?.length || breadcrumbs?.length === 1) return null + + const appendedBreadcrumbs = breadcrumbs?.slice(-ITEMS_TO_DISPLAY + 1, isMobile ? -1 : undefined) + + return ( + + + {breadcrumbs.length >= ITEMS_TO_DISPLAY && ( + <> + + {breadcrumbs[0].url ? ( + + {breadcrumbs[0].title || breadcrumbs[0].name} + + ) : ( + {breadcrumbs[0].title || breadcrumbs[0].name} + )} + + + + )} + {breadcrumbs.length > ITEMS_TO_DISPLAY && ( + <> + + {!isMobile ? ( + + + + + + {breadcrumbs.slice(1, -2).map((crumb, index) => ( + + {crumb.url ? ( + {crumb.title || crumb.name} + ) : ( + crumb.title || crumb.name + )} + + ))} + + + ) : ( + + + + + +
+ {breadcrumbs + .slice(1, -2) + .map((crumb) => + crumb.url ? ( + {crumb.title || crumb.name} + ) : ( + crumb.title || crumb.name + ) + )} +
+ + + + + +
+
+ )} +
+ + + )} + {appendedBreadcrumbs?.map((crumb, i) => ( + + + {crumb.url ? ( + + {crumb.title || crumb.name} + + ) : ( + {crumb.title || crumb.name} + )} + + + + ))} +
+
+ ) +} + +function findMenuItemByUrl(menu, targetUrl, parents = []) { + // If the menu has items, recursively search through them + if (menu.items) { + for (let item of menu.items) { + const result = findMenuItemByUrl(item, targetUrl, [...parents, menu]) + if (result) { + return result + } + } + } + + // Check if the current menu object itself has the target URL + if (menu.url === targetUrl) { + return [...parents, menu] + } + + // If the URL is not found, return null + return null +} + +export default Breadcrumbs diff --git a/apps/docs/components/ButtonCard.tsx b/apps/docs/components/ButtonCard.tsx index 5180cc90786..ecc50fd15c0 100644 --- a/apps/docs/components/ButtonCard.tsx +++ b/apps/docs/components/ButtonCard.tsx @@ -1,6 +1,7 @@ import React, { FC } from 'react' import Image from 'next/legacy/image' import Link from 'next/link' +import { cn } from 'ui' interface Props { title: string @@ -9,6 +10,7 @@ interface Props { icon?: string | any children?: any layout?: 'vertical' | 'horizontal' + className?: string } const ButtonCard: FC = ({ @@ -18,14 +20,16 @@ const ButtonCard: FC = ({ description = '', to, layout = 'vertical', + className, }) => { return ( {children ? ( children diff --git a/apps/docs/components/DocsCoverLogo.tsx b/apps/docs/components/DocsCoverLogo.tsx index 0e8965c50b6..8a66b35ea13 100644 --- a/apps/docs/components/DocsCoverLogo.tsx +++ b/apps/docs/components/DocsCoverLogo.tsx @@ -1,3 +1,5 @@ +'use client' + import React, { type PropsWithChildren } from 'react' import { LazyMotion, domAnimation, m } from 'framer-motion' diff --git a/apps/docs/components/Extensions/Extensions.tsx b/apps/docs/components/Extensions/Extensions.tsx index 9f7304ebab2..cd2a80942c9 100644 --- a/apps/docs/components/Extensions/Extensions.tsx +++ b/apps/docs/components/Extensions/Extensions.tsx @@ -58,7 +58,7 @@ export default function Extensions() {
-
+

Filter

    {tags.sort().map((tag) => ( diff --git a/apps/docs/components/Extensions/index.tsx b/apps/docs/components/Extensions/index.tsx index e2debfda82f..466582bc117 100644 --- a/apps/docs/components/Extensions/index.tsx +++ b/apps/docs/components/Extensions/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { lazy, Suspense } from 'react' const ExtensionsInternal = lazy(() => import('./Extensions')) diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index 3b86b5dabe1..cd0a6341f8b 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -74,7 +74,7 @@ function Feedback() { const [modalOpen, setModalOpen] = useState(false) const feedbackButtonRef = useRef(null) - const pathname = usePathname() + const pathname = usePathname() ?? '' const sendTelemetryEvent = useSendTelemetryEvent() const { mutate: sendFeedbackComment } = useSendFeedbackMutation() const supabase = useSupabaseClient() diff --git a/apps/docs/components/GuidesTableOfContents.tsx b/apps/docs/components/GuidesTableOfContents.tsx index 1125f974076..78b438090a9 100644 --- a/apps/docs/components/GuidesTableOfContents.tsx +++ b/apps/docs/components/GuidesTableOfContents.tsx @@ -1,3 +1,5 @@ +'use client' + import { usePathname } from 'next/navigation' import { useEffect, useState } from 'react' import { cn } from 'ui' @@ -20,7 +22,7 @@ const formatSlug = (slug: string) => { const formatTOCHeader = (content: string) => { let begin = false - const res = [] + const res: Array = [] for (const x of content) { if (x === '`') { if (!begin) { @@ -52,17 +54,24 @@ const useTocRerenderTrigger = () => { return toggleRenderFlag } +interface TOCHeader { + id?: string + text: string + link: string + level: number +} + const GuidesTableOfContents = ({ className, overrideToc, video, }: { className?: string - overrideToc?: Array<{ text: string; link: string; level: number }> + overrideToc?: Array video?: string }) => { useSubscribeTocRerender() - const [tocList, setTocList] = useState([]) + const [tocList, setTocList] = useState([]) const pathname = usePathname() const [hash] = useHash() @@ -79,6 +88,7 @@ const GuidesTableOfContents = ({ const headings = Array.from( document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? [] ) + const newHeadings = headings .filter((heading) => heading.id) .map((heading) => { @@ -87,9 +97,10 @@ const GuidesTableOfContents = ({ if (!link) return null const level = heading.tagName === 'H2' ? 2 : 3 - return { text, link, level } + + return { text, link, level } as Partial }) - .filter(Boolean) + .filter((x): x is TOCHeader => !!x && !!x.text && !!x.link && !!x.level) setTocList(newHeadings) }) @@ -140,3 +151,4 @@ const GuidesTableOfContents = ({ export default GuidesTableOfContents export { useTocRerenderTrigger } +export type { TOCHeader } diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index d9ee30ad170..6c084ce3ec9 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -1,3 +1,5 @@ +'use client' + import Link from 'next/link' import { IconBackground, IconPlay } from 'ui' import { useBreakpoint } from 'common' @@ -100,12 +102,12 @@ const HomePageCover = (props) => { ) return ( -
    +