diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml new file mode 100644 index 00000000000..bd402e9f365 --- /dev/null +++ b/.github/workflows/docs-tests.yml @@ -0,0 +1,30 @@ +name: Docs Tests + +on: + pull_request: + branches: ['master'] + paths: + - 'apps/docs/**/*.{ts,tsx}' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install deps + run: npm ci + + - name: Run tests + run: npm run test:docs diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml index b3d60d06629..b4631ceca5d 100644 --- a/.github/workflows/search.yml +++ b/.github/workflows/search.yml @@ -9,7 +9,12 @@ on: - 'supabase/migrations/**' - 'apps/docs/**' - 'spec/**' - workflow_dispatch: {} + workflow_dispatch: + inputs: + refresh: + description: 'Refresh all pages' + required: false + type: boolean schedule: - cron: '0 0 * * *' @@ -46,6 +51,12 @@ jobs: - name: Run migrations run: npx supabase db push + - name: Update embeddings + working-directory: ./apps/docs + if: ${{ !inputs.refresh }} + run: npm run embeddings + - name: Refresh embeddings working-directory: ./apps/docs - run: npm run embeddings + if: ${{ inputs.refresh }} + run: npm run embeddings:refresh diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 00000000000..0898d33a8c1 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,30 @@ +name: UI Tests + +on: + pull_request: + branches: [master] + paths: + - 'packages/ui/**' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install deps + run: npm ci + + - name: Run tests + run: npm run test:ui diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts new file mode 100644 index 00000000000..2d2d1285229 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts @@ -0,0 +1,96 @@ +import { getAnchor, removeAnchor } from './CustomHTMLElements.utils' + +describe('CustomHTMLElementsUtils', () => { + describe('getAnchor', () => { + describe('when value is an object', () => { + it('returns slugified version of props.children', () => { + const value = { + props: { + children: 'Full Text Search', + }, + } + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + }) + + describe('when value is an array', () => { + describe('when custom anchor exists', () => { + it('returns inner slug', () => { + const value = ['Proximity: <->', '[#proximity]'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + + it('trims whitespace', () => { + const value = ['Proximity: <->', ' [#proximity] '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + }) + + it('returns concatenated slug of elements', () => { + const value = ['Full', 'Text', 'Search'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('trims whitespace', () => { + const value = [' Full ', ' Text ', ' Search '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('removes special characters', () => { + const value = ['function()'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('function') + }) + }) + + describe('when value is a string', () => { + it('returns slugified version of string', () => { + const value = 'My (Very) Awesome Heading' + + const result = getAnchor(value) + + expect(result).toStrictEqual('my-very-awesome-heading') + }) + }) + }) + + describe('removeAnchor', () => { + describe('when value is an array', () => { + it('filters out custom anchor elements', () => { + const value = ['My (Very) Awesome Heading', '[#my-custom-heading]'] + + const result = removeAnchor(value) + + expect(result).toStrictEqual(['My (Very) Awesome Heading']) + }) + }) + + describe('when value is a string', () => { + it('strips out custom anchor string', () => { + const value = 'My (Very) Awesome Heading [#my-custom-heading]' + + const result = removeAnchor(value) + + // Original implementation didn't trim the resulting string - not sure if it really matters + expect(result).toStrictEqual('My (Very) Awesome Heading ') + }) + }) + }) +}) diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 0103cef6aa7..844724a5466 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -2,53 +2,60 @@ export const getAnchor = (text: any): string | undefined => { if (typeof text === 'object') { if (Array.isArray(text)) { - const customAnchor = text.find( - (x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']') - ) - if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']')) + const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x)) + if (customAnchor !== undefined) { + return parseCustomAnchor(customAnchor) + } const formattedText = text .map((x) => { - if (typeof x !== 'string') return x.props.children - else return x.trim() + if (typeof x !== 'string') { + return x.props.children + } + + return x.trim() }) .map((x) => { - if (typeof x !== 'string') return x - else + if (typeof x !== 'string') { return x - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + } + + return slugify(x) }) return formattedText.join('-').toLowerCase() } else { const anchor = text.props.children if (typeof anchor === 'string') { - return anchor - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + return slugify(anchor) } return anchor } } else if (typeof text === 'string') { - if (text.includes('[#') && text.endsWith(']')) { - return text.slice(text.indexOf('[#') + 2, text.indexOf(']')) - } else { - return text - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + if (hasCustomAnchor(text)) { + return parseCustomAnchor(text) } + return slugify(text) } else { return undefined } } +const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']') + +const parseCustomAnchor = (value: string): string => + value.slice(value.indexOf('[#') + 2, value.indexOf(']')) + +const slugify = (value: string): string => + value + .toLowerCase() + .trim() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + export const removeAnchor = (text: any) => { if (typeof text === 'object' && Array.isArray(text)) { - return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']'))) + return text.filter((x) => !(typeof x === 'string' && hasCustomAnchor(x))) } else if (typeof text === 'string') { if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#')) else return text diff --git a/apps/docs/components/DocsCoverLogo.tsx b/apps/docs/components/DocsCoverLogo.tsx new file mode 100644 index 00000000000..b7065feb555 --- /dev/null +++ b/apps/docs/components/DocsCoverLogo.tsx @@ -0,0 +1,497 @@ +import React from 'react' +import { LazyMotion, domAnimation, m } from 'framer-motion' + +const DocsCoverLogo = () => { + const pathMotionConfig = { + initial: { pathLength: 0 }, + animate: { pathLength: 1 }, + transition: { + duration: 1, + delay: 0.2, + ease: [0.5, 0.11, 0.13, 1], + }, + } + const logoMotionConfig = { + initial: { fillOpacity: 0 }, + animate: { fillOpacity: 1 }, + transition: { + duration: 1, + delay: 0.5, + ease: [0.25, 0.25, 0, 1], + }, + } + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} + +export default DocsCoverLogo diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx new file mode 100644 index 00000000000..edf3252ca64 --- /dev/null +++ b/apps/docs/components/HomePageCover.tsx @@ -0,0 +1,129 @@ +import Link from 'next/link' +import { IconBackground, IconPanel, IconPlay } from 'ui' +import { useBreakpoint } from 'common' +import DocsCoverLogo from './DocsCoverLogo' + +const HomePageCover = (props) => { + const isXs = useBreakpoint(639) + const iconSize = isXs ? 'sm' : 'lg' + + const GettingStarted = () => ( +
+
+
+
+ + + +

Getting Started

+
+

+ Discover how to set up a database to an app making queries in just a few minutes. +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ) + + return ( +
+
+
+ +
+

{props.meta?.title}

+

+ Learn how to get up and running with Supabase through tutorials, APIs and platform + resources. +

+
+
+
+ +
+
+
+ ) +} + +export default HomePageCover diff --git a/apps/docs/components/Navigation/Footer.tsx b/apps/docs/components/Navigation/Footer.tsx index f0cef649c60..e14892a8f39 100644 --- a/apps/docs/components/Navigation/Footer.tsx +++ b/apps/docs/components/Navigation/Footer.tsx @@ -1,68 +1,163 @@ -import Image from 'next/image' import Link from 'next/link' -import { Button, IconGitHub, IconTwitter, IconYoutube } from '~/../../packages/ui' -import footerData from '~/data/footer.json' -import { useTheme } from 'common/Providers' +import { Button } from 'ui' +import { primaryLinks, secondaryLinks } from '~/data/footer' +import { LayoutMainContent } from '../../layouts/DefaultLayout' -const Footer = () => { - const { isDarkMode } = useTheme() - return ( -
-
-
-
- - © Supabase Inc - - — - {footerData.map((item) => ( - - {item.title} - - ))} -
-
- - - + + + -
+ + + + + + + +
- ) -} + +) export default Footer diff --git a/apps/docs/components/Navigation/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index a644cc7c70d..3882fb26e77 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -9,8 +9,8 @@ export interface NavMenuGroup { export interface NavMenuSection { name: string - url?: string - items: NavMenuSection[] + url?: `/${string}` + items: Partial[] } export interface References { @@ -26,10 +26,17 @@ export interface References { type MenuItem = { label: string icon?: string - href?: string + href?: `/${string}` | `https://${string}` level?: string hasLightIcon?: boolean community?: boolean } export type HomepageMenuItems = MenuItem[][] + +export type NavMenuConstant = Readonly<{ + title: string + icon: string + url?: `/${string}` + items: ReadonlyArray> +}> diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx index f936d122966..fb51bb98ba7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx @@ -14,7 +14,7 @@ const NavigationMenuHome = () => { return (
-
    +
      {HOMEPAGE_MENU_ITEMS.map((section, sectionIndex) => { return ( diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx index b9df4569256..415c29f1b07 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx @@ -24,15 +24,15 @@ export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) { export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) { return ( @@ -404,17 +404,17 @@ export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) { export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) { return ( ) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 840c07ac2a0..64a9995d9f9 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1,4 +1,4 @@ -import { References, HomepageMenuItems } from '../Navigation.types' +import type { HomepageMenuItems, NavMenuConstant, References } from '../Navigation.types' export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [ [ @@ -194,7 +194,7 @@ export const REFERENCES: References = { }, } -export const gettingstarted = { +export const gettingstarted: NavMenuConstant = { icon: 'getting-started', title: 'Getting Started', items: [ @@ -321,7 +321,7 @@ export const SocialLoginItems = [ url: '/guides/auth/social-login/auth-apple', }, { - name: 'Azure', + name: 'Azure (Microsoft)', icon: '/docs/img/icons/microsoft-icon', url: '/guides/auth/social-login/auth-azure', }, @@ -507,7 +507,7 @@ export const auth = { ], } -export const database = { +export const database: NavMenuConstant = { icon: 'database', title: 'Database', url: '/guides/database', @@ -518,6 +518,8 @@ export const database = { { name: 'Database Webhooks', url: '/guides/database/webhooks' }, { name: 'Full Text Search', url: '/guides/database/full-text-search' }, { name: 'Database Testing', url: '/guides/database/testing' }, + { name: 'Managing Secrets with Vault', url: '/guides/database/vault' }, + { name: 'Column Encryption', url: '/guides/database/column-encryption' }, { name: 'Extensions', url: undefined, @@ -662,7 +664,7 @@ export const database = { ], } -export const api = { +export const api: NavMenuConstant = { icon: 'serverless-apis', title: 'Serverless APIs', url: '/guides/api', @@ -700,7 +702,7 @@ export const api = { ], } -export const functions = { +export const functions: NavMenuConstant = { icon: 'edge-functions', title: 'Edge Functions', url: '/guides/functions', @@ -778,7 +780,7 @@ export const functions = { ], } -export const realtime = { +export const realtime: NavMenuConstant = { icon: 'realtime', title: 'Realtime', url: '/guides/realtime', @@ -841,7 +843,7 @@ export const realtime = { ], } -export const storage = { +export const storage: NavMenuConstant = { icon: 'storage', title: 'Storage', url: '/guides/storage', @@ -855,7 +857,7 @@ export const storage = { ], } -export const supabase_cli = { +export const supabase_cli: NavMenuConstant = { icon: 'reference-cli', title: 'Supabase CLI', url: '/guides/cli', @@ -877,7 +879,7 @@ export const supabase_cli = { ], } -export const platform = { +export const platform: NavMenuConstant = { icon: 'platform', title: 'Platform', url: '/guides/platform', @@ -932,9 +934,19 @@ export const platform = { ], }, { - name: 'Go-live Checklist', + name: 'Production Readiness', url: undefined, - items: [{ name: 'Production Readiness', url: '/guides/platform/going-into-prod' }], + items: [ + { + name: 'Shared Responsibility Model', + url: '/guides/platform/shared-responsibility-model', + }, + { + name: 'Maturity Model', + url: '/guides/platform/maturity-model', + }, + { name: 'Production Checklist', url: '/guides/platform/going-into-prod' }, + ], }, { name: 'Troubleshooting', @@ -953,7 +965,7 @@ export const platform = { ], } -export const resources = { +export const resources: NavMenuConstant = { icon: 'resources', title: 'Resources', url: '/guides/resources', @@ -994,7 +1006,7 @@ export const resources = { ], } -export const self_hosting = { +export const self_hosting: NavMenuConstant = { title: 'Self-Hosting', icon: 'self-hosting', url: '/guides/self-hosting', @@ -1051,7 +1063,7 @@ export const migrate = { ], } -export const integrations = { +export const integrations: NavMenuConstant = { icon: 'integrations', title: 'Integrations', url: '/guides/integrations', @@ -1082,6 +1094,7 @@ export const integrations = { name: 'Developer Tools', url: undefined, items: [ + { name: 'Cloudflare Workers', url: '/guides/integrations/cloudflare-workers' }, { name: 'Estuary', url: '/guides/integrations/estuary' }, { name: 'OpenAI', url: '/guides/functions/examples/openai' }, { name: 'pgMustard', url: '/guides/integrations/pgmustard' }, diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx index 56d06558cb7..deb8caf1f17 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx @@ -48,7 +48,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => { } const Divider = () => { - return
      + return
      } const MenuSections = [ diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 60ea755c949..f264a4112b7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -46,7 +46,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any if (activeItem && activeItemRef.current) { // this is a hack, but seems a common one on Stackoverflow setTimeout(() => { - activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) }, 0) } }) @@ -54,7 +54,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any <> {props.subItemIndex === 0 && ( <> -
      +
      {props.parent.name} @@ -147,7 +147,7 @@ const Content = (props) => { const { menu, id } = props return ( -
        +
          { + const router = useRouter() + const activeRefId = useMenuActiveRefId() + + const activeSection = sections.find(({ id }) => id === activeRefId) + + // Remove the version number from URL to get the latest + const latestVersionUrl = router.asPath + .split('/') + .slice(0, -2) + .concat(activeSection ? [activeSection.slug] : []) + .join('/') + + return ( + + ) +} + +export default OldVersionAlert diff --git a/apps/docs/components/reference/RefSectionHandler.tsx b/apps/docs/components/reference/RefSectionHandler.tsx index 107a2c788a3..df5ed1c5700 100644 --- a/apps/docs/components/reference/RefSectionHandler.tsx +++ b/apps/docs/components/reference/RefSectionHandler.tsx @@ -8,6 +8,7 @@ import RefFunctionSection from '~/components/reference/RefFunctionSection' import RefSubLayout from '~/layouts/ref/RefSubLayout' import ApiOperationSection from './ApiOperationSection' import CliCommandSection from './CLICommandSection' +import OldVersionAlert from './OldVersionAlert' import { IAPISpec, ICommonSection, IRefStaticDoc, ISpec, TypeSpec } from './Reference.types' interface RefSectionHandlerProps { @@ -16,12 +17,13 @@ interface RefSectionHandlerProps { typeSpec?: TypeSpec pageProps: { docs: IRefStaticDoc[] } type: 'client-lib' | 'cli' | 'api' + isOldVersion?: boolean } const RefSectionHandler = (props: RefSectionHandlerProps) => { const router = useRouter() - const slug = router.query.slug[0] + const [slug] = router.query.slug // When user lands on a url like http://supabase.com/docs/reference/javascript/sign-up // find the #sign-up element and scroll to that @@ -30,7 +32,7 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => { }, [slug]) useEffect(() => { - function handler(e: PopStateEvent) { + function handler() { const [slug] = window.location.pathname.split('/').slice(-1) document.getElementById(slug)?.scrollIntoView() } @@ -69,6 +71,7 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => { /> + {props.isOldVersion && } {props.sections.map((section, i) => { const sectionType = section.type diff --git a/apps/docs/data/authProviders.ts b/apps/docs/data/authProviders.ts index ea5855061bd..a1053a30772 100644 --- a/apps/docs/data/authProviders.ts +++ b/apps/docs/data/authProviders.ts @@ -10,7 +10,7 @@ const authProviders = [ authType: 'social', }, { - name: 'Azure', + name: 'Azure (Microsoft)', logo: '/docs/img/icons/microsoft-icon', href: '/guides/auth/social-login/auth-azure', official: false, diff --git a/apps/docs/data/footer.json b/apps/docs/data/footer.json deleted file mode 100644 index d832d09175a..00000000000 --- a/apps/docs/data/footer.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { "title": "Contributing", "url": "/handbook/contributing" }, - { "title": "Changelog", "url": "https://supabase.com/changelog" }, - { - "title": "Author Styleguide", - "url": "https://github.com/supabase/supabase/blob/master/DEVELOPERS.md" - }, - { "title": "Open Source", "url": "https://supabase.com/open-source" }, - { "title": "SupaSquad", "url": "https://supabase.com/supasquad" } -] diff --git a/apps/docs/data/footer.ts b/apps/docs/data/footer.ts new file mode 100644 index 00000000000..15228f0e532 --- /dev/null +++ b/apps/docs/data/footer.ts @@ -0,0 +1,32 @@ +import { IconCheckCircle, IconLifeBuoy } from '~/../../packages/ui' + +export const primaryLinks = [ + { + featherIcon: IconLifeBuoy, + text: 'Need some help?', + ctaLabel: 'Contact support', + url: 'https://app.supabase.com/support/new', + }, + { + icon: 'M12.9521 10.5073C12.766 10.3212 12.5289 10.1943 12.2708 10.1427L10.6794 9.82467C9.80719 9.65024 8.90169 9.77152 8.10609 10.1693L7.89409 10.2747C7.0985 10.6725 6.193 10.7938 5.32076 10.6193L4.03343 10.362C3.81823 10.319 3.59574 10.3298 3.38571 10.3934C3.17569 10.457 2.9846 10.5715 2.82943 10.7267M5.33343 2.88867H10.6668L10.0001 3.55534V7.00334C10.0002 7.35693 10.1407 7.69602 10.3908 7.94601L13.7241 11.2793C14.5641 12.1193 13.9688 13.5553 12.7808 13.5553H3.21876C2.03076 13.5553 1.43609 12.1193 2.27609 11.2793L5.60943 7.94601C5.85949 7.69602 6.00002 7.35693 6.00009 7.00334V3.55534L5.33343 2.88867Z', + text: 'Lastest product updates?', + ctaLabel: 'See Changelog', + url: 'https://supabase.com/changelog', + }, + { + featherIcon: IconCheckCircle, + text: "Something's not right?", + ctaLabel: 'Check system status', + url: 'https://status.supabase.com/', + }, +] + +export const secondaryLinks = [ + { title: 'Contributing', url: '/handbook/contributing' }, + { + title: 'Author Styleguide', + url: 'https://github.com/supabase/supabase/blob/master/DEVELOPERS.md', + }, + { title: 'Open Source', url: 'https://supabase.com/open-source' }, + { title: 'SupaSquad', url: 'https://supabase.com/supasquad' }, +] diff --git a/apps/docs/docs/ref/cli/global-flags.mdx b/apps/docs/docs/ref/cli/global-flags.mdx index 39aaf77295b..088a10a07a0 100644 --- a/apps/docs/docs/ref/cli/global-flags.mdx +++ b/apps/docs/docs/ref/cli/global-flags.mdx @@ -1,5 +1,6 @@ --- title: Global Flags +subtitle: 'Supabase CLI supports global flags for every command.' description: 'Supabase CLI supports global flags for every command.' --- diff --git a/apps/docs/jest.config.ts b/apps/docs/jest.config.ts new file mode 100644 index 00000000000..348b75c0bb7 --- /dev/null +++ b/apps/docs/jest.config.ts @@ -0,0 +1,9 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], + testEnvironment: 'jsdom', +} + +export default config diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx index 4c93336fb87..393b86abf29 100644 --- a/apps/docs/layouts/DefaultLayout.tsx +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -21,7 +21,7 @@ interface Props { } const Layout: FC = (props: Props) => { - const { asPath, basePath } = useRouter() + const { asPath } = useRouter() const hasTableOfContents = props.toc !== undefined && @@ -60,26 +60,30 @@ const Layout: FC = (props: Props) => { }, }} /> -
          - {/*

          Tutorials

          */} -
          -

          {props.meta.title}

          - {/*
          */} - -
          -
          - {hasTableOfContents && !props.meta?.hide_table_of_contents && ( -
-
+
{children}
diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index cd5108189cd..ee025a2b358 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -5,20 +5,21 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { FC, useEffect, useRef, useState } from 'react' import { IconExternalLink } from 'ui' -import ExpandableVideo from 'ui/src/components/ExpandableVideo/ExpandableVideo' import components from '~/components' import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' -import FooterHelpCallout, { FooterHelpCalloutType } from '~/components/FooterHelpCallout' +import { FooterHelpCalloutType } from '~/components/FooterHelpCallout' import GuidesTableOfContents from '~/components/GuidesTableOfContents' import useHash from '~/hooks/useHash' +import { LayoutMainContent } from '../DefaultLayout' +import ExpandableVideo from 'ui/src/components/ExpandableVideo/ExpandableVideo' interface Props { meta: { title: string - description?: string + description?: string // used in meta tags hide_table_of_contents?: boolean breadcrumb?: string - subtitle?: string + subtitle?: string // used on the page under the H1 footerHelpType?: FooterHelpCalloutType video?: string tocVideo?: string @@ -105,74 +106,73 @@ const Layout: FC = (props) => { }, }} /> -
-
- {props.meta.breadcrumb && ( -

{props.meta.breadcrumb}

- )} -
-

{props.meta.title}

- {props.meta.subtitle && ( -

{props.meta.subtitle}

- )} -
- {props.children} - - {EDIT_BUTTON_EXCLUDE_LIST.includes(router.route) ? ( - <> - ) : ( - - )} -
-
- {!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && ( + +
- + {!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && ( + + )} +
+
) } diff --git a/apps/docs/layouts/ref/RefSubLayout.tsx b/apps/docs/layouts/ref/RefSubLayout.tsx index fb3668fc594..dc700bc4858 100644 --- a/apps/docs/layouts/ref/RefSubLayout.tsx +++ b/apps/docs/layouts/ref/RefSubLayout.tsx @@ -51,7 +51,11 @@ interface ISectionDetails {} interface ISectionExamples {} const RefSubLayout: FC & RefSubLayoutSubComponents = (props) => { - return
{props.children}
+ return ( +
+ {props.children} +
+ ) } const Section: FC = (props) => { @@ -128,7 +132,7 @@ const StickyHeader: FC = ({ icon, ...props }) => { } const Details: FC = (props) => { - return
{props.children}
+ return
{props.children}
} const Examples: FC = (props) => { diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 47fe88b4ffa..35c269c1b4b 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -1,3 +1,4 @@ +// @ts-check import nextMdx from '@next/mdx' import remarkGfm from 'remark-gfm' import rehypeSlug from 'rehype-slug' @@ -61,10 +62,12 @@ const nextConfig = { 'raw.githubusercontent.com', 'weweb-changelog.ghost.io', 'img.youtube.com', + 'archbee-image-uploads.s3.amazonaws.com' ], }, experimental: { - mdxRs: true, + // TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported. + // mdxRs: true, modularizeImports: { lodash: { transform: 'lodash/{{member}}', diff --git a/apps/docs/package.json b/apps/docs/package.json index a4d4a7133da..c59b1dcf3c4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,6 +9,7 @@ "build:analyze": "ANALYZE=true next build", "start": "next start", "lint": "next lint", + "test": "jest", "build:sitemap": "node ./internals/generate-sitemap.mjs", "embeddings": "tsx scripts/search/generate-embeddings.ts", "embeddings:refresh": "npm run embeddings -- --refresh", @@ -61,6 +62,7 @@ "clsx": "^1.2.1", "common": "*", "config": "*", + "framer-motion": "^6.5.1", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "isbot": "^3.6.5", diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index abde5d72106..6b24c000d4b 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -2,7 +2,7 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { AppPropsWithLayout } from 'types' import { CommandMenuProvider } from 'ui' import Favicons from '~/components/Favicons' @@ -22,17 +22,20 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined ) - function handlePageTelemetry(route: string) { - return post(`${API_URL}/telemetry/page`, { - referrer: document.referrer, - title: document.title, - route, - ga: { - screen_resolution: telemetryProps?.screenResolution, - language: telemetryProps?.language, - }, - }) - } + const handlePageTelemetry = useCallback( + (route: string) => { + return post(`${API_URL}/telemetry/page`, { + referrer: document.referrer, + title: document.title, + route, + ga: { + screen_resolution: telemetryProps?.screenResolution, + language: telemetryProps?.language, + }, + }) + }, + [telemetryProps] + ) useEffect(() => { function handleRouteChange(url: string) { @@ -59,7 +62,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { return () => { router.events.off('routeChangeComplete', handleRouteChange) } - }, [router.events]) + }, [router, handlePageTelemetry]) useEffect(() => { /** @@ -68,7 +71,26 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { if (router.isReady) { handlePageTelemetry(router.basePath + router.asPath) } - }, [router.isReady]) + }, [router, handlePageTelemetry]) + + /** + * Reference docs use `history.pushState()` to jump to + * sub-sections without causing a re-render. + * + * We need to the below handler to manually force a re-render + * when navigating away from, then back to reference docs + */ + useEffect(() => { + function handler() { + router.replace(window.location.href) + } + + window.addEventListener('popstate', handler) + + return () => { + window.removeEventListener('popstate', handler) + } + }, [router]) const SITE_TITLE = 'Supabase Documentation' diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx index 67f89450efd..4b0eeb64eef 100644 --- a/apps/docs/pages/_document.tsx +++ b/apps/docs/pages/_document.tsx @@ -3,9 +3,7 @@ import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( - - - +
diff --git a/apps/docs/pages/guides/api/creating-routes.mdx b/apps/docs/pages/guides/api/creating-routes.mdx index c48442f12d1..15c52ee156c 100644 --- a/apps/docs/pages/guides/api/creating-routes.mdx +++ b/apps/docs/pages/guides/api/creating-routes.mdx @@ -28,12 +28,10 @@ This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATC 1. Click **New Column** and create a column with the name `task` and type `text`. 1. Click **Save**. -