diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 00000000000..0a4d1b1fd57 --- /dev/null +++ b/.github/workflows/mirror.yml @@ -0,0 +1,38 @@ +name: Mirror Image + +on: + workflow_call: + inputs: + version: + required: true + type: string + workflow_dispatch: + inputs: + version: + description: 'Image tag' + required: true + type: string + +jobs: + mirror: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: docker/login-action@v2 + with: + registry: public.ecr.aws + username: ${{ secrets.PROD_ACCESS_KEY_ID }} + password: ${{ secrets.PROD_SECRET_ACCESS_KEY }} + - uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: akhilerm/tag-push-action@v2.1.0 + with: + src: docker.io/supabase/studio:${{ inputs.version }} + dst: | + public.ecr.aws/supabase/studio:${{ inputs.version }} + ghcr.io/supabase/studio:${{ inputs.version }} diff --git a/.github/workflows/publish_image.yml b/.github/workflows/publish_image.yml index 6106dad6b42..f57fd2515d3 100644 --- a/.github/workflows/publish_image.yml +++ b/.github/workflows/publish_image.yml @@ -14,25 +14,39 @@ on: type: string jobs: - publish: + settings: runs-on: ubuntu-latest + outputs: + image_version: ${{ steps.meta.outputs.version }} steps: - id: meta uses: docker/metadata-action@v4 with: images: | supabase/studio - public.ecr.aws/t3w2s2c9/studio flavor: | latest=false tags: | type=ref,event=tag - type=sha,prefix={{date 'YYYYMMDD'}},enable=${{ github.ref_type == 'branch' }} - type=raw,value=${{ inputs.version }},enable=${{ github.event_name != 'push' }} + type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'push' && github.ref_type == 'branch' }} + type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} - - uses: docker/setup-qemu-action@v2 + release_x86: + needs: settings + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + arch: amd64 + outputs: + image_digest: ${{ steps.build.outputs.digest }} + steps: + - id: meta + uses: docker/metadata-action@v4 with: - platforms: amd64,arm64 + images: | + supabase/studio + tags: | + type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }} - uses: docker/setup-buildx-action@v2 @@ -42,20 +56,86 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Login to ECR - uses: docker/login-action@v2 - with: - registry: public.ecr.aws - username: ${{ secrets.PROD_ACCESS_KEY_ID }} - password: ${{ secrets.PROD_SECRET_ACCESS_KEY }} - - - uses: docker/build-push-action@v3 + - id: build + uses: docker/build-push-action@v3 with: push: true context: '{{defaultContext}}' file: studio/Dockerfile target: production - platforms: linux/amd64,linux/arm64 + platforms: linux/${{ env.arch }} tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max + + release_arm: + needs: settings + runs-on: arm-runner + timeout-minutes: 120 + env: + arch: arm64 + outputs: + image_digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v3 + + - id: meta + uses: docker/metadata-action@v4 + with: + images: | + supabase/studio + tags: | + type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }} + + - uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - uses: docker/setup-buildx-action@v2 + with: + driver: docker + driver-opts: | + image=moby/buildkit:master + network=host + + - id: build + uses: docker/build-push-action@v3 + with: + push: true + context: . + file: studio/Dockerfile + target: production + platforms: linux/${{ env.arch }} + tags: ${{ steps.meta.outputs.tags }} + no-cache: true + + merge_manifest: + needs: + - settings + - release_x86 + - release_arm + runs-on: ubuntu-latest + steps: + - uses: docker/setup-buildx-action@v2 + + - uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Merge multi-arch manifests + run: | + docker buildx imagetools create -t supabase/studio:${{ needs.settings.outputs.image_version }} \ + supabase/studio@${{ needs.release_x86.outputs.image_digest }} \ + supabase/studio@${{ needs.release_arm.outputs.image_digest }} + + publish: + needs: + - settings + - merge_manifest + # Call workflow explicitly because events from actions cannot trigger more actions + uses: ./.github/workflows/mirror.yml + with: + version: ${{ needs.settings.outputs.image_version }} + secrets: inherit diff --git a/.gitignore b/.gitignore index d3c05c4e0d9..f536bae18d7 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,5 @@ typings/ # Supabase **/supabase/.branches **/supabase/.temp + +apps/new-docs/* \ No newline at end of file diff --git a/.turbo-cookie b/.turbo-cookie deleted file mode 100755 index 9e5d1817106..00000000000 --- a/.turbo-cookie +++ /dev/null @@ -1 +0,0 @@ -cookie \ No newline at end of file diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 585e7359e3d..0281b1b0bd7 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -8,6 +8,7 @@ - [Running turborepo](#running-turborepo) - [Shared components](#shared-components) - [Installing packages](#installing-packages) + - [New Supabase docs](#new-supabase-docs) 3. [Create a pull request](#create-a-pull-request) - [Common tasks](#common-tasks) @@ -35,8 +36,6 @@ We are in the process of migrating this repository to monorepo, using [Turborepo Eventually, all the apps will be run using [Turborepo](https://turborepo.org/docs), which will significantly improve the developer workflow. -If you are working on the ([docs site](https://supabase.com/docs)), please refer to this [developers guide](https://github.com/supabase/supabase/tree/master/apps/reference/DEVELOPERS.md). - ### Fork the repository To contribute code to [Supabase](https://supabase.com), you must fork the [Supabase Repository](https://github.com/supabase/supabase). @@ -71,11 +70,11 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab Then visit, and edit, any of the following sites: -| Site | Directory | Scope name | Description | Local development server | -| ---------------------------------------------- | -------------- | ---------- | ------------------------------------ | ------------------------ | -| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 | -| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 | -| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001 | +| Site | Directory | Scope name | Description | Local development server | +| ---------------------------------------------- | ------------ | ---------- | ------------------------------------ | -------------------------- | +| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 | +| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 | +| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001/docs | #### Running sites individually @@ -108,6 +107,14 @@ For example: You do not need to install `devDependencies` in each workspace. These can all be installed in the root package. +#### New Supabase docs + +Following the changes to the [Supabase docs](https://supabase.com/blog/new-supabase-docs-built-with-nextjs) the following is needed to run the new docs locally: + +- Inside of `apps/docs` create a `.env.local` file with the following: `NEXT_PUBLIC_NEW_DOCS=true` + +Now when you run a local devlopment docs server you will see the new docs site. + --- ## Create a pull request diff --git a/README.md b/README.md index c24e86943fa..4c35921e1a5 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Supabase is a combination of open source tools. We’re building the features of Supabase is a [hosted platform](https://app.supabase.com). You can sign up and start using Supabase without installing anything. You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development). -![Architecture](https://user-images.githubusercontent.com/70828596/187547862-ffa9d058-0c3a-4851-a3e7-92ccfca4b596.png) +![Architecture](https://github.com/supabase/supabase/blob/master/apps/docs/public/img/supabase-architecture.png) - [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. - [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients. diff --git a/apps/docs/codeHikeTheme.js b/apps/docs/codeHikeTheme.js new file mode 100644 index 00000000000..e34b5b89779 --- /dev/null +++ b/apps/docs/codeHikeTheme.js @@ -0,0 +1,372 @@ +module.exports = { + name: 'Stripe Docs Blue', + type: 'dark', + colors: { + 'editor.background': '#232323', + 'editor.foreground': '#fafafa', + 'activityBar.background': 'var(--colors-scale2)', + 'sideBar.background': 'yellow', + 'editorGroupHeader.tabsBackground': 'var(--colors-scale2)', + 'sideBarSectionHeader.background': 'var(--colors-scale2)', + 'tab.activeBackground': 'var(--colors-scale3)', + 'tab.inactiveBackground': 'var(--colors-scale2)', + 'tab.border': 'var(--colors-scale2)', + 'input.background': '#ffffff1a', + 'panel.background': '#1A2652', + 'panel.border': '#1A2652', + 'editorWidget.background': '#0d0f2b', + 'editorWidget.foreground': '#ffffff4d', + 'editorWidget.border': 'var(--colors-scale5)', + 'list.hoverBackground': '#ffffff1a', + 'list.activeSelectionBackground': '#ffffff1a', + 'list.inactiveSelectionBackground': '#ffffff1a', + 'editor.hoverHighlightBackground': '#ffffff1a', + 'editor.selectionHighlightBackground': '#ffffff1a', + 'activityBarBadge.background': 'yellow', + 'sideBarTitle.foreground': 'var(--colors-scale2)', + 'statusBar.background': 'var(--colors-scale2)', + }, + tokenColors: [ + { + name: 'Comment', + scope: ['comment', 'punctuation.definition.comment'], + settings: { + foreground: '#a3acb9', + fontStyle: '', + }, + }, + { + name: 'Variables', + scope: ['source', 'variable', 'variable.other.object', 'string constant.other.placeholder'], + settings: { + foreground: '#f5fbff', + }, + }, + { + name: 'Colors', + scope: ['variable.other.constant', 'constant.other.color'], + settings: { + foreground: '#ffffff', + fontStyle: 'bold', + }, + }, + { + name: 'Invalid', + scope: ['invalid', 'invalid.illegal'], + settings: { + foreground: '#FF5370', + }, + }, + { + name: 'Keyword, Storage', + scope: ['keyword', 'storage.type', 'storage.modifier'], + settings: { + foreground: '#98C1FE', + fontStyle: 'bold', + }, + }, + + { + name: 'Function', + scope: ['entity.name.function'], + settings: { + foreground: '#7fd3ed', + fontStyle: 'bold', + }, + }, + + { + name: 'Tag', + scope: ['entity.name.tag', 'meta.tag.sgml', 'markup.deleted.git_gutter'], + settings: { + foreground: '#98C1FE', + fontStyle: 'bold', + }, + }, + + { + name: 'Parameter, Property', + scope: [ + 'variable.parameter', + 'variable.other.object.property', + 'variable.other.property', + 'keyword.other.unit', + 'keyword.other', + ], + settings: { + foreground: '#F2AFE3', + }, + }, + { + name: 'Number, Constant, Function Argument, Tag Attribute, Embedded', + scope: [ + 'constant.numeric', + 'constant.language', + 'support.constant', + 'constant.character', + 'constant.escape', + ], + settings: { + foreground: '#f8b886', + }, + }, + { + name: 'String, Symbols, Inherited Class, Markup Heading', + scope: [ + 'string', + 'constant.other.symbol', + 'constant.other.key', + 'entity.other.inherited-class', + 'markup.heading', + 'markup.inserted.git_gutter', + 'meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js', + ], + settings: { + foreground: '#85d99e', + }, + }, + { + name: 'Entity Types', + scope: ['support.type'], + settings: { + foreground: '#B2CCD6', + }, + }, + { + name: 'CSS Class and Support', + scope: [ + 'source.css support.type.property-name', + 'source.sass support.type.property-name', + 'source.scss support.type.property-name', + 'source.less support.type.property-name', + 'source.stylus support.type.property-name', + 'source.postcss support.type.property-name', + ], + settings: { + foreground: '#B2CCD6', + }, + }, + { + name: 'Language methods', + scope: ['variable.language'], + settings: { + fontStyle: 'italic', + foreground: '#FF5370', + }, + }, + + { + name: 'Attributes', + scope: ['entity.other.attribute-name'], + settings: { + foreground: '#98C1FE', + fontStyle: 'italic', + }, + }, + { + name: 'Inserted', + scope: ['markup.inserted'], + settings: { + foreground: '#C3E88D', + }, + }, + { + name: 'Deleted', + scope: ['markup.deleted'], + settings: { + foreground: '#FF5370', + }, + }, + { + name: 'Changed', + scope: ['markup.changed'], + settings: { + foreground: '#C792EA', + }, + }, + { + name: 'Regular Expressions', + scope: ['string.regexp'], + settings: { + foreground: '#89DDFF', + }, + }, + { + name: 'Escape Characters', + scope: ['constant.character.escape'], + settings: { + foreground: '#89DDFF', + }, + }, + { + name: 'URL', + scope: ['*url*', '*link*', '*uri*'], + settings: { + fontStyle: 'underline', + }, + }, + { + name: 'ES7 Bind Operator', + scope: ['source.js constant.other.object.key.js string.unquoted.label.js'], + settings: { + fontStyle: 'italic', + foreground: '#FF5370', + }, + }, + + { + name: 'Markdown - Plain', + scope: ['text.html', 'punctuation.definition.list_item'], + settings: { + foreground: '#f5fbff', + }, + }, + { + name: 'Markdown - Markup Raw Inline', + scope: ['text.html.markdown markup.inline.raw.markdown'], + settings: { + foreground: '#C792EA', + }, + }, + { + name: 'Markdown - Markup Raw Inline Punctuation', + scope: ['text.html.markdown markup.inline.raw.markdown punctuation.definition.raw.markdown'], + settings: { + foreground: '#65737E', + }, + }, + { + name: 'Markdown - Heading', + scope: [ + 'markdown.heading', + 'markup.heading | markup.heading entity.name', + 'markup.heading.markdown punctuation.definition.heading.markdown', + ], + settings: { + foreground: '#C3E88D', + }, + }, + { + name: 'Markup - Italic', + scope: ['markup.italic'], + settings: { + fontStyle: 'italic', + foreground: '#f07178', + }, + }, + { + name: 'Markup - Bold', + scope: ['markup.bold', 'markup.bold string'], + settings: { + fontStyle: 'bold', + foreground: '#f07178', + }, + }, + { + name: 'Markup - Bold-Italic', + scope: [ + 'markup.bold markup.italic', + 'markup.italic markup.bold', + 'markup.quote markup.bold', + 'markup.bold markup.italic string', + 'markup.italic markup.bold string', + 'markup.quote markup.bold string', + ], + settings: { + fontStyle: 'bold', + foreground: '#f07178', + }, + }, + { + name: 'Markup - Underline', + scope: ['markup.underline'], + settings: { + fontStyle: 'underline', + foreground: '#F78C6C', + }, + }, + { + name: 'Markdown - Blockquote', + scope: ['markup.quote punctuation.definition.blockquote.markdown'], + settings: { + foreground: '#65737E', + }, + }, + { + name: 'Markup - Quote', + scope: ['markup.quote'], + settings: { + fontStyle: 'italic', + }, + }, + + { + name: 'Markdown - Link Description', + scope: ['string.other.link.description.title.markdown'], + settings: { + foreground: '#C792EA', + }, + }, + { + name: 'Markdown - Link Anchor', + scope: ['constant.other.reference.link.markdown'], + settings: { + foreground: '#FFCB6B', + }, + }, + { + name: 'Markup - Raw Block', + scope: ['markup.raw.block'], + settings: { + foreground: '#C792EA', + }, + }, + { + name: 'Markdown - Raw Block Fenced', + scope: ['markup.raw.block.fenced.markdown'], + settings: { + foreground: '#00000050', + }, + }, + { + name: 'Markdown - Fenced Bode Block', + scope: ['punctuation.definition.fenced.markdown'], + settings: { + foreground: '#00000050', + }, + }, + { + name: 'Markdown - Fenced Bode Block Variable', + scope: [ + 'markup.raw.block.fenced.markdown', + 'variable.language.fenced.markdown', + 'punctuation.section.class.end', + ], + settings: { + foreground: '#EEFFFF', + }, + }, + { + name: 'Markdown - Fenced Language', + scope: ['variable.language.fenced.markdown'], + settings: { + foreground: '#65737E', + }, + }, + { + name: 'Markdown - Separator', + scope: ['meta.separator'], + settings: { + fontStyle: 'bold', + foreground: '#65737E', + }, + }, + { + name: 'Markup - Table', + scope: ['markup.table'], + settings: { + foreground: '#EEFFFF', + }, + }, + ], +} diff --git a/apps/docs/components/ButtonCard.tsx b/apps/docs/components/ButtonCard.tsx index 7d7f7e12809..f1aa4caefbc 100644 --- a/apps/docs/components/ButtonCard.tsx +++ b/apps/docs/components/ButtonCard.tsx @@ -25,7 +25,7 @@ const ButtonCard: FC = ({ className={[ 'h-full block shadow-none bg-scale-300 rounded transition', 'border border-transparent hover:border-scale-600', - ].join('')} + ].join(' ')} > {children ? ( children @@ -43,7 +43,7 @@ const ButtonCard: FC = ({ ) : ( icon )} -

{title}

+

{title}

{description}

)} diff --git a/apps/docs/components/CodeBlock/CodeBlock.tsx b/apps/docs/components/CodeBlock/CodeBlock.tsx index 1ca6b128c12..f4e962bb2aa 100644 --- a/apps/docs/components/CodeBlock/CodeBlock.tsx +++ b/apps/docs/components/CodeBlock/CodeBlock.tsx @@ -10,13 +10,14 @@ import py from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql' import bash from 'react-syntax-highlighter/dist/cjs/languages/hljs/bash' import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart' +import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json' import { useState } from 'react' -import { useTheme } from '../Providers' +import { useTheme } from 'common/Providers' interface Props { title?: string - language: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' + language: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' | 'json' linesToHighlight?: number[] hideCopy?: boolean hideLineNumbers?: boolean @@ -47,6 +48,11 @@ const CodeBlock: FC = ({ }, 1000) } + // check the length of the string inside the tag + // if it's fewer than 70 characters, add a white-space: pre so it doesn't wrap + const shortCodeBlockClasses = + typeof children === 'string' && children.length < 70 ? 'short-inline-codeblock' : '' + let lang = language ? language : className ? className.replace('language-', '') : 'js' // force jsx to be js highlighted if (lang === 'jsx') lang = 'js' @@ -56,81 +62,88 @@ const CodeBlock: FC = ({ SyntaxHighlighter.registerLanguage('sql', sql) SyntaxHighlighter.registerLanguage('bash', bash) SyntaxHighlighter.registerLanguage('dart', dart) + SyntaxHighlighter.registerLanguage('json', json) const large = false // don't show line numbers if bash == lang - const showLineNumbers = hideLineNumbers || lang !== 'bash' + if (lang !== 'bash') hideLineNumbers = true + const showLineNumbers = !hideLineNumbers return ( -
+ <> {title && (
{title.replace(/%20/g, ' ')}
)} {className ? ( - { - if (linesToHighlight.includes(lineNumber)) { - return { - style: { display: 'block', backgroundColor: 'var(--colors-scale6)' }, +
+ { + if (linesToHighlight.includes(lineNumber)) { + return { + style: { display: 'block', backgroundColor: 'var(--colors-scale6)' }, + } } - } - return {} - }} - lineNumberContainerStyle={{ - paddingTop: '128px', - }} - lineNumberStyle={{ - minWidth: '44px', - paddingLeft: '4px', - paddingRight: '4px', - marginRight: '12px', - color: '#828282', - textAlign: 'center', - fontSize: large ? 14 : 12, - paddingTop: '4px', - paddingBottom: '4px', - }} - > - {(value || children)?.trimEnd()} - - ) : ( - {value || children} - )} - {!hideCopy && (value || children) && className ? ( -
- - - + {/* // + @ts-ignore */} + + + +
+ ) : null}
- ) : null} -
+ ) : ( + {value || children} + )} + ) } diff --git a/apps/docs/components/ComesFrom.tsx b/apps/docs/components/ComesFrom.tsx new file mode 100644 index 00000000000..052b50d4099 --- /dev/null +++ b/apps/docs/components/ComesFrom.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react' +import { IconSearch } from '~/../../packages/ui' +// [Terry] — delete this after development +// just for making it easy to see where things are coming from +interface Props { + link: string + text: string + className?: string +} +export const ComesFrom: FC = ({ link, text, className }) => { + return ( +
+ + + Comes from: + + + {text} + +
+ ) +} diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 305ac4fa170..eb214e4963b 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -77,3 +77,19 @@ export const unHighlightSelectedTocItems = () => { const currentActiveItem = document.querySelector('.toc-menu .toc__menu-item--active') currentActiveItem?.classList.remove('toc__menu-item--active') } + +export const highlightSelectedNavItem = (id: string) => { + const navMenuItems = document.querySelectorAll('.function-link-item a') + + // find any currently active items and remove them + const currentActiveItems = document.querySelectorAll('.function-link-list .text-brand-900') + currentActiveItems.forEach((item) => item.classList.remove('text-brand-900')) + + // Add active class to the current item + navMenuItems.forEach((item) => { + // @ts-ignore + if (item.href.split('/').at(-1) === id) { + item.classList.add('text-brand-900') + } + }) +} diff --git a/apps/docs/components/CustomHTMLElements/Heading.tsx b/apps/docs/components/CustomHTMLElements/Heading.tsx index b07ea6cab17..f1248c38d1c 100644 --- a/apps/docs/components/CustomHTMLElements/Heading.tsx +++ b/apps/docs/components/CustomHTMLElements/Heading.tsx @@ -6,16 +6,25 @@ import { } from './CustomHTMLElements.utils' import { useInView } from 'react-intersection-observer' -const Heading = ({ tag, children }) => { +/** + * [Joshen] The trick with rootMargin + * We are shrinking the top of the root element by 20 percent, which is currently our entire page, + * and the bottom by 35 percent. Therefore, when a header is at the top 20 percent and bottom 35 percent + * of our page, it will not be counted as visible. + */ + +interface Props { + tag: string +} + +const Heading: React.FC = ({ tag, children }) => { const HeadingTag = `${tag}` as any const anchor = getAnchor(children) const link = `#${anchor}` - // check if current heading is in view, update TOC active item accordingly - // [Joshen] Ideally we highlight the section in the TOC when it's at the top of the page - // much like when we click on the item in the TOC itself, the rootMargin fix is insufficient const { ref } = useInView({ threshold: 1, + rootMargin: '-20% 0% -35% 0px', onChange: (inView, entry) => { if (window.scrollY === 0) unHighlightSelectedTocItems() if (inView) highlightSelectedTocItem(entry.target.id) diff --git a/apps/docs/components/CustomHTMLElements/InlineCode.tsx b/apps/docs/components/CustomHTMLElements/InlineCode.tsx index 95dc7603bb0..5ee5811c2f4 100644 --- a/apps/docs/components/CustomHTMLElements/InlineCode.tsx +++ b/apps/docs/components/CustomHTMLElements/InlineCode.tsx @@ -1,11 +1,13 @@ -const InlineCodeTag = ({ children }) => { +import { FC } from 'react' + +const InlineCodeTag = ({ children }: any) => { // If children isn't a string, just return it as is, just in case if (typeof children !== 'string') return children // check the length of the string inside the tag // if it's fewer than 70 characters, add a white-space: pre so it doesn't wrap - const classes = children.length < 70 ? 'short-inline-codeblock' : '' + const className = children.length < 70 ? 'short-inline-codeblock' : '' - return {children} + return HELLO WORLD {children} } export default InlineCodeTag diff --git a/apps/docs/components/DocSearch.tsx b/apps/docs/components/DocSearch.tsx index d8bde374580..2a4f56d1e48 100644 --- a/apps/docs/components/DocSearch.tsx +++ b/apps/docs/components/DocSearch.tsx @@ -20,7 +20,6 @@ export function SearchProvider({ children }: any) { const router = useRouter() const [isOpen, setIsOpen] = useState(false) const [initialQuery, setInitialQuery] = useState(null) - const onOpen = useCallback(() => { setIsOpen(true) }, [setIsOpen]) @@ -85,6 +84,7 @@ export function SearchProvider({ children }: any) { hitComponent={Hit} transformItems={(items) => { return items.map((item, index) => { + console.log('item', item) // We transform the absolute URL into a relative URL to // leverage Next's preloading. const a = document.createElement('a') @@ -96,7 +96,8 @@ export function SearchProvider({ children }: any) { ...item, url: `${a.pathname}${hash}`, __is_result: () => true, - __is_parent: () => item.type === 'lvl1' && items.length > 1 && index === 0, + __is_parent: () => item.type === 'lvl1' && index === 0, + __is_guide: () => item.hierarchy.lvl0 === 'Guides', __is_child: () => item.type !== 'lvl1' && items.length > 1 && @@ -125,6 +126,7 @@ function Hit({ hit, children }) { 'DocSearch-Hit--FirstChild': hit.__is_first?.(), 'DocSearch-Hit--LastChild': hit.__is_last?.(), 'DocSearch-Hit--Child': hit.__is_child?.(), + 'DocSearch-Hit--Guide': hit.__is_guide?.(), })} > {children} diff --git a/apps/docs/components/Footer/DarkModeToggle.tsx b/apps/docs/components/Footer/DarkModeToggle.tsx index 230e973e4ba..04f9d225854 100644 --- a/apps/docs/components/Footer/DarkModeToggle.tsx +++ b/apps/docs/components/Footer/DarkModeToggle.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '~/components/Providers' +import { useTheme } from 'common/Providers' import { IconSun, IconMoon } from '@supabase/ui' function DarkModeToggle() { @@ -19,7 +19,7 @@ function DarkModeToggle() { type="button" aria-pressed="false" className={` - relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent + relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ isDarkMode ? 'bg-scale-500' : 'bg-scale-900' } mx-5 diff --git a/apps/docs/components/Footer/index.tsx b/apps/docs/components/Footer/index.tsx index b690dd003aa..01424c906f5 100644 --- a/apps/docs/components/Footer/index.tsx +++ b/apps/docs/components/Footer/index.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import Image from 'next/image' import FooterLinks from 'data/footer.json' import DarkModeToggle from './DarkModeToggle' -import { useTheme } from '../Providers' +import { useTheme } from 'common/Providers' import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png' import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png' diff --git a/apps/docs/components/FooterHelpCallout.tsx b/apps/docs/components/FooterHelpCallout.tsx new file mode 100644 index 00000000000..82f1a03c683 --- /dev/null +++ b/apps/docs/components/FooterHelpCallout.tsx @@ -0,0 +1,13 @@ +const FooterHelpCallout = () => { + return ( +
+
Need some help?
+

+ Not to worry, our specialist engineers are here to help. Submit a support ticket through the{' '} + Dashboard. +

+
+ ) +} + +export default FooterHelpCallout diff --git a/apps/docs/components/Frameworks.tsx b/apps/docs/components/Frameworks.tsx index 1716a2600ef..a1b6d0da29b 100644 --- a/apps/docs/components/Frameworks.tsx +++ b/apps/docs/components/Frameworks.tsx @@ -1,5 +1,5 @@ import ButtonCard from './ButtonCard' -import { useTheme } from '~/components/Providers' +import { useTheme } from 'common/Providers' const Frameworks = () => { const { isDarkMode } = useTheme() @@ -8,86 +8,86 @@ const Frameworks = () => { { name: 'Angular', logo: { - light: '/docs/img/libraries/angular-icon.svg', - dark: '/docs/img/libraries/angular-icon.svg', + light: '/docs/img/icons/angular-icon.svg', + dark: '/docs/img/icons/angular-icon.svg', }, href: '/guides/with-angular', }, { name: 'Expo', logo: { - light: '/docs/img/libraries/expo-icon.svg', - dark: '/docs/img/libraries/expo-icon-dark.svg', + light: '/docs/img/icons/expo-icon.svg', + dark: '/docs/img/icons/expo-icon-dark.svg', }, href: '/guides/with-expo', }, { name: 'Flutter', logo: { - light: '/docs/img/libraries/flutter-icon.svg', - dark: '/docs/img/libraries/flutter-icon.svg', + light: '/docs/img/icons/flutter-icon.svg', + dark: '/docs/img/icons/flutter-icon.svg', }, href: '/guides/with-flutter', }, { name: 'JavaScript', logo: { - light: '/docs/img/libraries/javascript-icon.svg', - dark: '/docs/img/libraries/javascript-icon.svg', + light: '/docs/img/icons/javascript-icon.svg', + dark: '/docs/img/icons/javascript-icon.svg', }, href: '/reference/javascript/installing#javascript', }, { name: 'Next.js', logo: { - light: '/docs/img/libraries/nextjs-light-icon.svg', - dark: '/docs/img/libraries/nextjs-dark-icon.svg', + light: '/docs/img/icons/nextjs-light-icon.svg', + dark: '/docs/img/icons/nextjs-dark-icon.svg', }, href: '/guides/with-nextjs', }, { name: 'React', logo: { - light: '/docs/img/libraries/react-icon.svg', - dark: '/docs/img/libraries/react-icon.svg', + light: '/docs/img/icons/react-icon.svg', + dark: '/docs/img/icons/react-icon.svg', }, href: '/guides/with-react', }, { name: 'RedwoodJS', logo: { - light: '/docs/img/libraries/redwoodjs-icon.svg', - dark: '/docs/img/libraries/redwoodjs-icon.svg', + light: '/docs/img/icons/redwoodjs-icon.svg', + dark: '/docs/img/icons/redwoodjs-icon.svg', }, href: '/guides/with-redwoodjs', }, { name: 'SolidJS', logo: { - light: '/docs/img/libraries/solidjs-icon.svg', - dark: '/docs/img/libraries/solidjs-icon.svg', + light: '/docs/img/icons/solidjs-icon.svg', + dark: '/docs/img/icons/solidjs-icon.svg', }, href: '/guides/with-solidjs', }, { name: 'Svelte', logo: { - light: '/docs/img/libraries/svelte-icon.svg', - dark: '/docs/img/libraries/svelte-icon.svg', + light: '/docs/img/icons/svelte-icon.svg', + dark: '/docs/img/icons/svelte-icon.svg', }, href: '/guides/with-svelte', }, { name: 'Vue', logo: { - light: '/docs/img/libraries/vuejs-icon.svg', - dark: '/docs/img/libraries/vuejs-icon.svg', + light: '/docs/img/icons/vuejs-icon.svg', + dark: '/docs/img/icons/vuejs-icon.svg', }, href: '/guides/with-vue-3', }, ] return ( -
+
{frameworks.map((x) => (
= ({ list }) => { return ( -
+ + + + {menu.items.map((x, index) => { + // console.log('1st type of link?', x.items && x.items.length > 0) + // console.log() + return ( +
+ {x.items && x.items.length > 0 ? ( +
+ {x.items.map((subItem, subItemIndex) => { + // console.log('subitem', { subItem }) + // console.log('router', router) + //console.log('subitem url', subItem.url) + let subItemMenuOpen = false + + if (router.asPath.includes(subItem.url)) { + subItemMenuOpen = true + } + + const LinkContainer = (props) => { + return ( + + {props.children} + + ) + } + + return ( + <> + {subItemIndex === 0 && ( + <> +
+ + {x.name} + + + )} + +
  • + + {subItem.icon && ( + {subItem.name + )} + {subItem.name} + +
  • + + {subItem.items && subItem.items.length > 0 && ( + + {subItem.items.map((subSubItem, subSubItemIndex) => { + return ( +
  • + + + {subSubItem.name} + + +
  • + ) + })} +
    + )} +
    + + ) + })} +
    + ) : ( + <> +
  • + + + {x.icon && } + {x.name} + + +
  • + + )} +
    + ) + })} + {menu.extras && ( + <> +
    + + Resources + + + )} + {menu.extras?.map((x) => { + return ( + + ) + })} + + + ) +} + +export default NavigationMenuGuideList diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx new file mode 100644 index 00000000000..e2db35f487a --- /dev/null +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx @@ -0,0 +1,307 @@ +import * as Accordion from '@radix-ui/react-accordion' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { IconChevronLeft } from 'ui' +import * as NavItems from './NavigationMenu.constants' + +import { find } from 'lodash' +import Image from 'next/image' +import { useTheme } from 'common/Providers' + +// import apiCommonSections from '~/../../spec/common-client-libs-sections.json' + +import RevVersionDropdown from '~/components/RefVersionDropdown' +import { useMenuActiveRefId, useMenuLevelId } from '~/hooks/useMenuState' +import { RefIdOptions, RefKeyOptions } from './NavigationMenu' + +const FunctionLink = ({ + title, + id, + icon, + library, + slug, +}: { + title: string + name?: string + id: string + icon?: string + product?: string + library: string + slug: string +}) => { + const router = useRouter() + const activeAccordianItem = useMenuActiveRefId() + + // check if we're on a versioned page + let version = '' + if (router.asPath.includes('v1')) { + version = 'v1' + } + + if (router.asPath.includes('v0')) { + version = 'v0' + } + + const active = activeAccordianItem === id + return ( +
  • + + + {icon && {icon}} + {title} + + +
  • + ) +} + +const SideMenuTitle = ({ title }: { title: string }) => { + return ( + + {title} + + ) +} + +const Divider = () => { + return
    +} + +interface INavigationMenuRefList { + id: RefIdOptions + lib: RefKeyOptions + commonSections: any[] // to do type up + + // the keys of menu items that are allowed to be shown on the side menu + // if undefined, we show all the menu items + allowedClientKeys?: string[] +} + +const NavigationMenuRefList: React.FC = ({ + id, + lib, + commonSections, + allowedClientKeys, +}) => { + const router = useRouter() + const { isDarkMode } = useTheme() + + let sections = commonSections + + const allowedKeys = allowedClientKeys + + if (!sections) console.error('no common sections imported') + + const menu = NavItems[id] + + const databaseFunctions = find(sections, { title: 'Database' }) + ? find(sections, { title: 'Database' }).items + : [] + + const authFunctions = find(sections, { title: 'Auth' }) + ? find(sections, { title: 'Auth' }).items + : [] + + const filterIds = + databaseFunctions.length > 0 + ? find(databaseFunctions, { + id: 'using-filters', + }) && + find(databaseFunctions, { + id: 'using-filters', + }) + .items.filter((x) => allowedKeys.includes(x.id)) + .map((x) => x.id) + : [] + const modifierIds = + databaseFunctions.length > 0 + ? find(databaseFunctions, { + id: 'using-modifiers', + }) && + find(databaseFunctions, { + id: 'using-modifiers', + }) + .items.filter((x) => allowedKeys.includes(x.id)) + .map((x) => x.id) + : [] + + const authServerIds = + databaseFunctions.length > 0 + ? find(authFunctions, { + id: 'admin-api', + }) && + find(authFunctions, { + id: 'admin-api', + }).items.map((x) => x.id) + : [] + + // console.log(filterIds) + // console.log(modifierIds) + + const level = useMenuLevelId() + + return ( +
    +
    + + +
    +
    + +
    +
    + Back to Main Menu +
    + + +
    + {id} + + {menu.title} + + +
    + {/* )} */} + +
      + {sections.map((fn: any, fnIndex) => { + // + // check if the link is allowed to be displayed + function isFuncNotInLibraryOrVersion(id, type) { + if (id && allowedKeys && !allowedKeys.includes(id) && type !== 'markdown') { + /* + * Remove this menu link from side bar, as it does not exist for either + * this language, or for this lib version + * + * */ + return true + } else { + return false + } + } + + // run allow check + if (isFuncNotInLibraryOrVersion(fn.id, fn.type)) { + return <> + } + + const RenderLink = (props) => { + const activeAccordianItem = useMenuActiveRefId() + let active = false + + const isFilter = + filterIds && filterIds.length > 0 && filterIds.includes(activeAccordianItem) + const isModifier = + modifierIds && modifierIds.length > 0 && modifierIds.includes(activeAccordianItem) + const isAuthServer = + authServerIds && + authServerIds.length > 0 && + authServerIds.includes(activeAccordianItem) + + if ( + (isFilter && !isModifier && !isAuthServer && props.id === 'using-filters') || + (activeAccordianItem === 'using-filters' && props.id === 'using-filters') + ) { + active = true + } else if ( + (isModifier && !isFilter && !isAuthServer && props.id === 'using-modifiers') || + (activeAccordianItem === 'using-modifiers' && props.id === 'using-modifiers') + ) { + active = true + } else if ( + (isAuthServer && !isFilter && !isModifier && props.id === 'admin-api') || + (activeAccordianItem === 'admin-api' && props.id === 'admin-api') + ) { + active = true + } else { + active = false + } + + return ( + + + + + {props.items && + props.items + .filter((item) => allowedKeys.includes(item.id)) + .map((item) => { + return + })} + + + + ) + } + + // handle subtitles with subitems + if (!fn.id) { + return ( + <> + + + {fn.items && + fn.items + //.filter((item) => item.libs && item.libs.includes(lib)) + .map((item) => { + // run allow check + if (isFuncNotInLibraryOrVersion(item.id, item.type)) return <> + return + })} + + ) + } else { + // handle normal links + return ( + <> + + {fn.items && + fn.items + //.filter((item) => item.libs.includes(lib)) + .map((item) => )} + + ) + } + })} +
    +
    +
    + ) +} + +export default NavigationMenuRefList diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx new file mode 100644 index 00000000000..3748821840b --- /dev/null +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx @@ -0,0 +1,172 @@ +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { FC, useEffect, useState } from 'react' +import { Button, IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Input, Listbox } from 'ui' +import { SearchButton } from '~/components/DocSearch' +import { REFERENCES } from '~/components/Navigation/Navigation.constants' +import { useTheme } from 'common/Providers' + +import { getPageType } from '~/lib/helpers' + +const TopNavBar: FC = () => { + const { isDarkMode, toggleTheme } = useTheme() + const [mounted, setMounted] = useState(false) + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + const { asPath, push } = useRouter() + const pathSegments = asPath.split('/') + + const library = pathSegments.length >= 3 ? pathSegments[2] : undefined + const libraryMeta = REFERENCES?.[library] ?? undefined + const versions = libraryMeta?.versions ?? [] + + const version = versions.includes(pathSegments[pathSegments.indexOf(library) + 1]) + ? pathSegments[pathSegments.indexOf(library) + 1] + : versions[0] + + const pageType = getPageType(asPath) + + useEffect(() => { + setMounted(true) + }, [isDarkMode]) + + const pageLinks = [ + { text: 'Guides', key: 'docs', link: '/' }, + { text: 'Reference', key: 'reference', link: '/reference' }, + ] + + const toggleDarkMode = () => { + localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString()) + toggleTheme() + + const key = localStorage.getItem('supabaseDarkMode') + document.documentElement.className = key === 'true' ? 'dark' : '' + } + + const onSelectVersion = (version: string) => { + // [Joshen] Ideally we use but this works for now + if (!library) return + if (version === versions[0]) { + push(`/reference/${library}`) + } else { + push(`/reference/${library}/${version}`) + } + } + + // [Joshen] Kaizen: Use UI library's SidePanel for this + const toggleMobileMenu = () => { + setMobileMenuOpen(!mobileMenuOpen) + const sidebar = document.querySelector('.sidebar-menu-container') + const contentPane = document.querySelector('.main-content-pane') + + sidebar.classList.toggle('hidden') + contentPane.classList.toggle('hidden') + } + + return ( + + ) +} +export default TopNavBar diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx new file mode 100644 index 00000000000..6fcae17712d --- /dev/null +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx @@ -0,0 +1,173 @@ +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { FC, useEffect, useState } from 'react' +import { + Badge, + Button, + Dropdown, + IconCommand, + IconMenu, + IconMessageSquare, + IconMoon, + IconSearch, + IconSun, + Input, + Listbox, +} from 'ui' +import { SearchButton } from '~/components/DocSearch' +import { REFERENCES } from '~/components/Navigation/Navigation.constants' +import { useTheme } from 'common/Providers' + +import { getPageType } from '~/lib/helpers' + +const TopNavBarRef: FC = () => { + const { isDarkMode, toggleTheme } = useTheme() + const [mounted, setMounted] = useState(false) + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + const { asPath, push } = useRouter() + const pathSegments = asPath.split('/') + + const library = pathSegments.length >= 3 ? pathSegments[2] : undefined + const libraryMeta = REFERENCES?.[library] ?? undefined + const versions = libraryMeta?.versions ?? [] + + const version = versions.includes(pathSegments[pathSegments.indexOf(library) + 1]) + ? pathSegments[pathSegments.indexOf(library) + 1] + : versions[0] + + const pageType = getPageType(asPath) + + useEffect(() => { + setMounted(true) + }, [isDarkMode]) + + const pageLinks = [ + { text: 'Guides', key: 'docs', link: '/' }, + { text: 'Reference', key: 'reference', link: '/reference' }, + ] + + const toggleDarkMode = () => { + localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString()) + toggleTheme() + + const key = localStorage.getItem('supabaseDarkMode') + document.documentElement.className = key === 'true' ? 'dark' : '' + } + + const onSelectVersion = (version: string) => { + // [Joshen] Ideally we use but this works for now + if (!library) return + if (version === versions[0]) { + push(`/reference/${library}`) + } else { + push(`/reference/${library}/${version}`) + } + } + + // [Joshen] Kaizen: Use UI library's SidePanel for this + const toggleMobileMenu = () => { + setMobileMenuOpen(!mobileMenuOpen) + const sidebar = document.querySelector('.sidebar-menu-container') + const contentPane = document.querySelector('.main-content-pane') + + sidebar.classList.toggle('hidden') + contentPane.classList.toggle('hidden') + } + + return ( + + ) +} +export default TopNavBarRef diff --git a/apps/docs/components/Navigation/RefSwitcher.tsx b/apps/docs/components/Navigation/RefSwitcher.tsx new file mode 100644 index 00000000000..25a2335da54 --- /dev/null +++ b/apps/docs/components/Navigation/RefSwitcher.tsx @@ -0,0 +1,174 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { Badge, Dropdown, GlassPanel, IconChevronDown, IconChevronRight, Popover } from 'ui' + +import { references } from './NavigationMenu/NavigationMenu.constants' + +const RefSwitcher = () => { + const router = useRouter() + const [currentRef, setCurrentRef] = useState() + const [open, setOpen] = useState(false) + + useEffect(() => { + setOpen(false) + }, [router.pathname]) + + return ( +
    + setOpen(!open)} + overlay={ + <> + {references.map((section) => { + return ( + <> +
    + + {section.label} + +
    +
    + {section.items.map((item) => { + return ( + + setOpen(!open)} + > +
    + +
    +
    + {item.label} +
    +
    + +
    +
    +
    + {item.description} +
    +
    +
    + + ) + })} +
    + + ) + })} + + } + > +
    +
    +
    + +

    + supabase-js +

    +
    +
    + +
    +
    +
    +
    + }> +
    +
    +
    +
    +

    + v2.0 +

    +
    +
    + +
    +
    +
    +
    +
    +
    + ) +} + +const VersionOverlay = () => { + return ( + <> + Stable releases + + version 2.0 + + Latest + + + Version 1.0 + + Alpha releases + +

    Currently no alpha releases

    +
    + + ) +} +export default RefSwitcher diff --git a/apps/docs/components/Options.tsx b/apps/docs/components/Options.tsx new file mode 100644 index 00000000000..c2783edeaa4 --- /dev/null +++ b/apps/docs/components/Options.tsx @@ -0,0 +1,81 @@ +import { FC, useState } from 'react' +import { IconXCircle } from '~/../../packages/ui' + +interface IOptions { + name?: string +} + +type IOption = any + +type OptionsSubComponents = { + Option: IOption +} + +const Options: FC & OptionsSubComponents = (props) => { + const [open, setOpen] = useState(false) + return ( +
    + +
    + {props.children} +
    +
    + ) +} + +const Option: FC = (props) => { + return ( +
    +
    + + {props.name ?? 'no-name'} + + + {props.isOptional ? ( +
    Optional
    + ) : ( +
    + REQUIRED +
    + )} +
    + {props.type ?? 'no type'} +
    +

    {props.description ?? 'nodescription'}

    + {props.children} +
    + ) +} + +Options.Option = Option + +export default Options diff --git a/apps/docs/components/Params.tsx b/apps/docs/components/Params.tsx new file mode 100644 index 00000000000..ede7f8bcac7 --- /dev/null +++ b/apps/docs/components/Params.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' + +type IParamProps = any + +const Param: FC = (paramItem) => { + return ( +
  • +
    + + {paramItem.name ?? 'no-name'} + + + {paramItem.isOptional ? ( +
    Optional
    + ) : ( +
    + REQUIRED +
    + )} +
    + {paramItem.type ?? 'no type'} +
    +

    {paramItem.description ?? 'nodescription'}

    + {paramItem.children} +
  • + ) +} + +export default Param diff --git a/apps/docs/components/RefVersionDropdown.tsx b/apps/docs/components/RefVersionDropdown.tsx new file mode 100644 index 00000000000..7a5b8bb88ec --- /dev/null +++ b/apps/docs/components/RefVersionDropdown.tsx @@ -0,0 +1,73 @@ +import { Badge, Dropdown, IconChevronDown } from 'ui' +import { useRouter } from 'next/router' +import { REFERENCES } from './Navigation/NavigationMenu/NavigationMenu.constants' + +const RevVersionDropdown = () => { + const { asPath, push } = useRouter() + const pathSegments = asPath.split('/') + + const library = pathSegments.length >= 3 ? pathSegments[2] : undefined + const libraryMeta = REFERENCES?.[library] ?? undefined + const versions = libraryMeta?.versions ?? [] + + const currentVersion = versions.includes(pathSegments[pathSegments.indexOf(library) + 1]) + ? pathSegments[pathSegments.indexOf(library) + 1] + : versions[0] + + const onSelectVersion = (version: string) => { + if (!library) return + if (version === versions[0]) { + push(`/reference/${library}/start`) + } else { + push(`/reference/${library}/${version}/start`) + } + } + + if (!versions || versions.length === 0) { + return <> + } + + return ( + + Stable releases + {versions.map((version, index) => ( + onSelectVersion(version)}> + + Version {version}.0 + + + {index === 0 && Latest} + + + ))} + + } + > +
    + {/* version */} + + {currentVersion}.0 + + +
    +
    + ) +} +export default RevVersionDropdown diff --git a/apps/docs/components/StepHike/StepHikeContext.tsx b/apps/docs/components/StepHike/StepHikeContext.tsx new file mode 100644 index 00000000000..977ca043cef --- /dev/null +++ b/apps/docs/components/StepHike/StepHikeContext.tsx @@ -0,0 +1,8 @@ +import { createContext } from 'react' + +// Make sure the shape of the default value passed to +// createContext matches the shape that the consumers expect! +export const StepHikeContext = createContext({ + activeStep: undefined, + steps: undefined, +}) diff --git a/apps/docs/components/StepHike/index.tsx b/apps/docs/components/StepHike/index.tsx new file mode 100644 index 00000000000..cbb2731004f --- /dev/null +++ b/apps/docs/components/StepHike/index.tsx @@ -0,0 +1,187 @@ +import { useEffect, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { StepHikeContext } from './StepHikeContext' + +const StepHike = ({ children, title }) => { + const [activeStep, setActiveStep] = useState(undefined) + + // check if there are any children + if (!children) throw 'StepHike component requires children' + + const steps = children.filter((x) => { + return x.type.name === 'Step' + }) + + useEffect(() => { + setActiveStep({ + titleId: steps[0].props.title.replaceAll(' ', '-').toLowerCase(), + step: 0, + }) + }, []) + + // check if there is at least 1 StepHike subcomponent + if (steps.length === 0 || !steps) + throw 'StepHike component needs at least 1 child' + + // console.log('length of the steps filter', steps.length) + + return ( +
    + {/*
    +

    {title}

    +
    + + +
    +
    */} + + {/*
    +
    +
    + {activeStep?.step + 1} +
    +

    + {steps[activeStep?.step] && steps[activeStep?.step].props.title} +

    + + Step {activeStep?.step + 1} of {steps?.length} + +
    +
    + + +
    +
    */} + {children} +
    + ) +} + +const Step = ({ children, title, step }) => { + const [show, setShow] = useState(false) + + useEffect(() => { + setShow(true) + }, []) + + const ChildrenRender = ({ active }) =>
    {children}
    + + // const ref = useRef(null) + + // useEffect(() => { + // const cachedRef = ref.current + // const observer = new IntersectionObserver( + // ([e]) => + // // setStuck(e.intersectionRatio < 1) + // console.log('scroll', title), + // { + // threshold: [0], + // rootMargin: '0px 0px 0px 0px', + // } + // ) + + // // const cachedRef: HTMLDivElement | null + // // Argument of type 'HTMLDivElement | null' is not assignable to parameter of type 'Element'. + // // Type 'null' is not assignable to type 'Element'.ts(2345) + // observer.observe(cachedRef) + // return () => observer.unobserve(cachedRef) + // }, [ref]) + + const { ref } = useInView({ + rootMargin: '10px 20px 30px 40px', + threshold: 1, + onChange: (inView, entry) => { + if (window.scrollY === 0) console.log('out of view', title) + if (inView) console.log('in view', title) // highlightSelectedTocItem(entry.target.id) + }, + }) + + return ( + <> + + {({ activeStep, steps }) => { + // console.log('activeStep', activeStep) + const cleanTitleId = title.replaceAll(' ', '-').toLowerCase() + const active = cleanTitleId === activeStep?.titleId + + // useEffect(() => {}, []) + + return ( +
    + {/*
    */} +
    +
    +
    + {step} +
    +

    + {title} +

    + + Step {step} of {steps?.length} + +
    +
    + +
    + ) + }} +
    + + ) +} + +StepHike.Step = Step +export default StepHike diff --git a/apps/docs/components/StepHikeCompact/StepHikeContext.tsx b/apps/docs/components/StepHikeCompact/StepHikeContext.tsx new file mode 100644 index 00000000000..977ca043cef --- /dev/null +++ b/apps/docs/components/StepHikeCompact/StepHikeContext.tsx @@ -0,0 +1,8 @@ +import { createContext } from 'react' + +// Make sure the shape of the default value passed to +// createContext matches the shape that the consumers expect! +export const StepHikeContext = createContext({ + activeStep: undefined, + steps: undefined, +}) diff --git a/apps/docs/components/StepHikeCompact/index.tsx b/apps/docs/components/StepHikeCompact/index.tsx new file mode 100644 index 00000000000..6ef75c7140c --- /dev/null +++ b/apps/docs/components/StepHikeCompact/index.tsx @@ -0,0 +1,93 @@ +//import { Step } from 'next-seo/lib/types' +import React, { FC } from 'react' + +interface IStep { + title: string + step: number | string +} + +interface IStepHikeCompactSubcomponents { + Step: FC + Details: FC + Code: FC +} +interface IDetails { + title?: string +} +interface ICode {} + +interface IStepHikeCompact { + title: string +} + +const StepHikeCompact: FC & IStepHikeCompactSubcomponents = ({ + children, + title, +}) => { + return
    {children}
    +} + +const Step: FC = ({ children, title, step }) => { + return ( +
    +
    +
    +
    +
    +
    +
    + {step} +
    +
    +
    +
    {children}
    +
    + ) +} + +const Details: FC = ({ children, title }) => { + return ( +
    +

    {title}

    + {children} +
    + ) +} + +const Code: FC = ({ children }) => { + return
    {children}
    +} + +StepHikeCompact.Step = Step +StepHikeCompact.Details = Details +StepHikeCompact.Code = Code +export default StepHikeCompact diff --git a/apps/docs/components/TableOfContents.tsx b/apps/docs/components/TableOfContents.tsx index 7bfc8c19258..8be2a831646 100644 --- a/apps/docs/components/TableOfContents.tsx +++ b/apps/docs/components/TableOfContents.tsx @@ -15,6 +15,7 @@ interface TOCHeader { interface Props { toc: any + video?: string } const formatSlug = (slug: string) => { @@ -44,26 +45,38 @@ const formatTOCHeader = (content: string) => { return res.join('') } -const TableOfContents: FC = ({ toc }) => { +const TableOfContents: FC = ({ toc, video }) => { // [Joshen] markdown-toc doesn't seem to read maxdepth from the options passed in // Our first level headers will be H2s (H1 is ignored), and we only show up to H3 return ( -
      - {(toc.json as TOCHeader[]) - .filter((item) => item.lvl !== 1 && item.lvl <= 3) - .map((item: any, i: number) => { - return ( -
    • - -
    • - ) - })} -
    + <> + {video && ( +
    + +
    + )} +
    + ) } diff --git a/apps/docs/components/index.tsx b/apps/docs/components/index.tsx index cd2a76eb223..4351050cbf0 100644 --- a/apps/docs/components/index.tsx +++ b/apps/docs/components/index.tsx @@ -19,6 +19,13 @@ import QuickstartIntro from './MDX/quickstart_intro.mdx' import ProjectSetup from './MDX/project_setup.mdx' import { Mermaid } from 'mdx-mermaid/lib/Mermaid' import InlineCodeTag from './CustomHTMLElements/InlineCode' +import React from 'react' +import RefSubLayout from '~/layouts/ref/RefSubLayout' +// import { CH } from '@code-hike/mdx/components' +import RefHeaderSection from './reference/RefHeaderSection' + +// Ref version specific +import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler' const components = { Admonition, @@ -40,7 +47,6 @@ const components = { ), Tabs, TabPanel: (props: any) => {props.children}, - h2: (props: any) => ( {props.children} @@ -51,12 +57,17 @@ const components = { {props.children} ), - code: (props: any) => { - const linesToHighlight = parseNumericRange(props.lines ?? '') - return - }, + // pre: (props: any) => { + // const linesToHighlight = parseNumericRange(props.lines ?? '') + // return + // }, mono: (props: any) => {props.children}, - inlineCode: (props: any) => {props.children}, + // inlineCode: (props: { children: string }) => , + RefSubLayout, + // CH, + code: (props: any) => , + RefHeaderSection: (props: any) => , + CliGlobalFlagsHandler: () => , } export default components diff --git a/apps/docs/components/reference/ApiOperationSection.tsx b/apps/docs/components/reference/ApiOperationSection.tsx new file mode 100644 index 00000000000..6272e2c3e40 --- /dev/null +++ b/apps/docs/components/reference/ApiOperationSection.tsx @@ -0,0 +1,115 @@ +import { Tabs } from '~/../../packages/ui' +import CodeBlock from '~/components/CodeBlock/CodeBlock' +import Param from '~/components/Params' +import RefSubLayout from '~/layouts/ref/RefSubLayout' + +const ApiOperationSection = (props) => { + const operation = props.spec.operations.find((x: any) => x.operationId === props.funcData.id) + + // gracefully return nothing if function does not exist + if (!operation) return <> + + return ( + + +
    + + + {operation.operation} + + {operation.fullPath} + +
    +
    +

    {operation.description}

    +
    + {/* Path Parameters */} + {operation.parameters && + operation.parameters.filter((parameter) => parameter.in === 'path').length > 0 && ( +
    +
    Path Parameters
    +
      + {operation.parameters && + operation.parameters + .filter((parameter: any) => parameter.in === 'path') + .map((parameter: any) => ( + + ))} +
    +
    + )} + + {/* Query Parameters */} + {operation.parameters && + operation.parameters.filter((parameter) => parameter.in === 'query').length > 0 && ( +
    +
    Query Parameters
    +
      + {operation.parameters && + operation.parameters + .filter((parameter: any) => parameter.in === 'query') + .map((parameter: any) => ( + + ))} +
    +
    + )} + + {/* Header Parameters */} + {operation.parameters && + operation.parameters.filter((parameter) => parameter.in === 'header').length > 0 && ( +
    +
    Query Parameters
    +
      + {operation.parameters && + operation.parameters + .filter((parameter: any) => parameter.in === 'header') + .map((parameter: any) => ( + + ))} +
    +
    + )} +
    + {operation.responseList && operation.responseList.length > 0 && ( + +
    Responses
    + + {operation.responseList.map((response: any) => ( + +

    {response.description}

    + {response?.content && response?.content['application/json'] && ( +
    + + {JSON.stringify(response.content['application/json'], null, 2)} + +
    + )} +
    + ))} +
    +
    + )} +
    + ) +} +export default ApiOperationSection diff --git a/apps/docs/components/reference/CLICommandSection.tsx b/apps/docs/components/reference/CLICommandSection.tsx new file mode 100644 index 00000000000..9f685157482 --- /dev/null +++ b/apps/docs/components/reference/CLICommandSection.tsx @@ -0,0 +1,140 @@ +import { IconChevronRight } from '~/../../packages/ui' +// @ts-expect-error +import spec from '~/../../spec/cli_v1_commands.yaml' assert { type: 'yml' } +import CodeBlock from '~/components/CodeBlock/CodeBlock' +import Options from '~/components/Options' +import Param from '~/components/Params' +import RefSubLayout from '~/layouts/ref/RefSubLayout' + +interface ICommonFunc { + id: string + title: string + slug: string + product: string + libs: string + items: ICommonFunc[] +} + +interface IRefFunctionSection { + funcData: any + commonFuncData: ICommonFunc + spec: any +} + +export type Flag = { + id: string + name: string + description: string + default_value: string + accepted_values: AcceptedValue[] + required?: boolean +} + +export type AcceptedValue = { + id: string + name: string + type: 'string' | 'boolean' | 'object' + description?: string +} + +export type Command = { + id: string + title: string + description: string + flags?: Flag[] + summary: string + tags?: [] + links?: [] + subcommands?: [] + usage?: string +} + +const CliCommandSection = (props) => { + const command = spec.commands.find((x: any) => x.id === props.funcData.id) + + return ( + + +
    +
    +
    +

    + {command.summary} +

    +
    + + {/* {command.usage && ( + + {command.usage} + + )} */} + + {command.subcommands.length > 0 && ( +
    + )} + {command.flags.length > 0 && ( + <> +

    Flags

    +
      + {command.flags.map((flag: Flag) => ( + <> +
    • + + {flag?.accepted_values && ( + + {flag?.accepted_values.map((value) => { + return + })} + + )} + +
    • + + ))} +
    + + )} +
    +
    + + + {command.usage && ( + + {command.usage} + + )} + + + ) +} + +export default CliCommandSection diff --git a/apps/docs/components/reference/RefDetailCollapse.tsx b/apps/docs/components/reference/RefDetailCollapse.tsx new file mode 100644 index 00000000000..fa152727c01 --- /dev/null +++ b/apps/docs/components/reference/RefDetailCollapse.tsx @@ -0,0 +1,44 @@ +import * as Accordion from '@radix-ui/react-accordion' +import React from 'react' +import { IconChevronRight } from '~/../../packages/ui' + +const RefDetailCollapse: React.FC<{ id: string; label: string; defaultOpen?: boolean }> = ({ + defaultOpen = true, + ...props +}) => { + return ( + + + + + + + {props.children} + + + + ) +} + +export default RefDetailCollapse diff --git a/apps/docs/components/reference/RefEducationSection.tsx b/apps/docs/components/reference/RefEducationSection.tsx new file mode 100644 index 00000000000..a388c11425c --- /dev/null +++ b/apps/docs/components/reference/RefEducationSection.tsx @@ -0,0 +1,27 @@ +import RefSubLayout from '~/layouts/ref/RefSubLayout' +import { MDXRemote } from 'next-mdx-remote' +import components from '~/components' + +const RefEducationSection = (props) => { + // gracefully reject pages we can't render + if (!props.markdownContent) { + //console.log(props.item.id) + return
    + } + + return ( + + + + ) +} + +export default RefEducationSection diff --git a/apps/docs/components/reference/RefFunctionSection.tsx b/apps/docs/components/reference/RefFunctionSection.tsx new file mode 100644 index 00000000000..a31c52af992 --- /dev/null +++ b/apps/docs/components/reference/RefFunctionSection.tsx @@ -0,0 +1,234 @@ +import ReactMarkdown from 'react-markdown' + +import { IconDatabase, Tabs } from 'ui' +import CodeBlock from '~/components/CodeBlock/CodeBlock' + +import Options from '~/components/Options' +import Param from '~/components/Params' +import RefSubLayout from '~/layouts/ref/RefSubLayout' +import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers' + +import RefDetailCollapse from '~/components/reference/RefDetailCollapse' + +interface ICommonFunc { + id: string + title: string + slug: string + product: string + libs: string + items: ICommonFunc[] +} + +interface IRefFunctionSection { + funcData: any + commonFuncData: ICommonFunc + spec: any + typeSpec?: any +} + +const RefFunctionSection: React.FC = (props) => { + const item = props.spec.functions.find((x: any) => x.id === props.funcData.id) + + // gracefully return nothing if function does not exist + if (!item) return <> + + if (item && !item['$ref']) { + //console.warn('🚩 issue with $ref in:', item.id) + } + + // console.log(item) + const hasTsRef = item['$ref'] || null + + const tsDefinition = + hasTsRef && props.typeSpec ? extractTsDocNode(hasTsRef, props.typeSpec) : null + const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : '' + const shortText = hasTsRef && tsDefinition ? tsDefinition.signatures[0].comment.shortText : '' + + return ( + <> + + + <> +
    + {shortText && {shortText}} +
    + + {item.description && ( +
    + {item.description} +
    + )} + {/* {functionMarkdownContent && ( +
    + +
    + )} */} + {item.notes && ( +
    + {item.notes} +
    + )} + {/* // parameters */} + {parameters && ( +
    +
    Parameters
    +
      + {parameters.map((param) => { + // grab override params from yaml file + const overrideParams = item.overrideParams + + // params from the yaml file can override the params from parameters if it matches the name + const overide = overrideParams?.filter((x) => { + return param.name === x.name + }) + + const paramItem = overide?.length > 0 ? overide[0] : param + + return ( + + {paramItem.subContent && ( +
      + + {param.subContent.map((param) => { + return ( + <> + + {param.subContent && ( + + {param.subContent.map((param) => { + return + })} + + )} + + + ) + })} + +
      + )} + + ) + })} +
    +
    + )} + +
    + + {item.examples && ( + <> +
    + + {item.examples && + item.examples.map((example, exampleIndex) => { + const exampleString = '' + // ` + // import { createClient } from '@supabase/supabase-js' + + // // Create a single supabase client for interacting with your database + // const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key') + // ` + const currentExampleId = example.id + const staticExample = item.examples[exampleIndex] + + const response = staticExample.response + const sql = staticExample?.data?.sql + const tables = staticExample?.data?.tables + + return ( + + {((tables && tables.length > 0) || sql) && ( + + <> + {tables && + tables.length > 0 && + tables.map((table) => { + return ( +
    +
    +
    +
    + +
    +
    + {table.name} +
    +
    +
    +
    + ) + })} + {sql && ( + + {sql.replace(/sql/g, '').replace(/```/g, '')} + + )} + +
    + )} + + + {exampleString + + (example.code && + example.code + .replace(/```/g, '') + .replace('js', '') + .replace('ts', ''))} + + {response && ( + + + {response.replace(/```/g, '').replace('json', '')} + + + )} +
    + ) + })} +
    +
    + + )} +
    +
    + + ) +} + +export default RefFunctionSection diff --git a/apps/docs/components/reference/RefHeaderSection.tsx b/apps/docs/components/reference/RefHeaderSection.tsx new file mode 100644 index 00000000000..0d84d792b20 --- /dev/null +++ b/apps/docs/components/reference/RefHeaderSection.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import RefSubLayout from '~/layouts/ref/RefSubLayout' + +interface Props {} + +const RefHeaderSection: React.FC = (props) => { + return ( + <> + + {props.children} + + + ) +} + +export default RefHeaderSection diff --git a/apps/docs/components/reference/RefSectionHandler.tsx b/apps/docs/components/reference/RefSectionHandler.tsx new file mode 100644 index 00000000000..cca2e834ac9 --- /dev/null +++ b/apps/docs/components/reference/RefSectionHandler.tsx @@ -0,0 +1,88 @@ +import { MDXRemote } from 'next-mdx-remote' +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +import components from '~/components' +import RefEducationSection from '~/components/reference/RefEducationSection' +import RefFunctionSection from '~/components/reference/RefFunctionSection' +import OldLayout from '~/layouts/Default' +import RefSubLayout from '~/layouts/ref/RefSubLayout' +import ApiOperationSection from './ApiOperationSection' +import CliCommandSection from './CLICommandSection' + +interface Props { + sections: any[] // to do + spec: any // to do + typeSpec: any // to do + pageProps: any // to do, from staticProps + + type: 'client-lib' | 'cli' | 'api' +} + +const RefSectionHandler = (props) => { + const router = useRouter() + + const slug = router.query.slug[0] + + const isNewDocs = process.env.NEXT_PUBLIC_NEW_DOCS === 'true' + + // When user lands on a url like http://supabase.com/docs/reference/javascript/sign-up + // find the #sign-up element and scroll to that + useEffect(() => { + if (isNewDocs && document && slug !== 'start') { + document.querySelector(`#${slug}`) && document.querySelector(`#${slug}`).scrollIntoView() + } + }) + + /* + * handle old ref pages + */ + if (!isNewDocs) { + return ( + // @ts-ignore + + + + ) + } + + return ( + + {props.sections.map((x) => { + switch (x.type) { + case 'markdown': + const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id) + + return + break + case 'function': + return ( + + ) + case 'cli-command': + return + break + case 'operation': + return + default: + return ( + + ) + break + } + })} + + ) +} + +export default RefSectionHandler diff --git a/apps/docs/components/reference/enrichments/cli/CliGlobalFlagsHandler.tsx b/apps/docs/components/reference/enrichments/cli/CliGlobalFlagsHandler.tsx new file mode 100644 index 00000000000..778f518b173 --- /dev/null +++ b/apps/docs/components/reference/enrichments/cli/CliGlobalFlagsHandler.tsx @@ -0,0 +1,30 @@ +import RefSubLayout from '~/layouts/ref/RefSubLayout' + +// @ts-expect-error +import spec from '~/../../spec/cli_v1_commands.yaml' assert { type: 'yaml' } +import Param from '~/components/Params' +import Options from '~/components/Options' + +const CliGlobalFlagsHandler = () => { + return ( + + +

    Flags

    +
      + {spec.flags.map((flag) => { + return ( + + ) + })} +
    +
    + + +
    + ) +} + +export default CliGlobalFlagsHandler diff --git a/apps/docs/data/authProviders.ts b/apps/docs/data/authProviders.ts index f9a32a71454..95c2f6b2dbf 100644 --- a/apps/docs/data/authProviders.ts +++ b/apps/docs/data/authProviders.ts @@ -1,26 +1,8 @@ const authProviders = [ - // { - // name: 'Email', - // // logo: '/img/libraries/dart-icon.svg', - // href: '/docs/guides/auth/auth-apple', - // official: true, - // supporter: 'Supabase', - // platform: true, - // selfHosted: true, - // }, - // { - // name: 'Magic Links', - // // logo: '/img/libraries/dart-icon.svg', - // href: '/docs/guides/auth/auth-apple', - // official: true, - // supporter: 'Supabase', - // platform: true, - // selfHosted: true, - // }, { name: 'Apple', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-apple', + href: '/guides/auth/social-login/auth-apple', official: true, supporter: 'Supabase', platform: true, @@ -29,7 +11,7 @@ const authProviders = [ { name: 'Azure', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-azure', + href: '/guides/auth/social-login/auth-azure', official: false, supporter: 'TBD', platform: true, @@ -38,7 +20,7 @@ const authProviders = [ { name: 'Bitbucket', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-bitbucket', + href: '/guides/auth/social-login/auth-bitbucket', official: true, supporter: 'Supabase', platform: true, @@ -47,7 +29,7 @@ const authProviders = [ { name: 'Discord', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-discord', + href: '/guides/auth/social-login/auth-discord', official: true, supporter: 'Supabase', platform: true, @@ -56,7 +38,7 @@ const authProviders = [ { name: 'Facebook', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-facebook', + href: '/guides/auth/social-login/auth-facebook', official: true, supporter: 'Supabase', platform: true, @@ -65,7 +47,7 @@ const authProviders = [ { name: 'GitHub', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-github', + href: '/guides/auth/social-login/auth-github', official: true, supporter: 'Supabase', platform: true, @@ -74,7 +56,7 @@ const authProviders = [ { name: 'GitLab', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-gitlab', + href: '/guides/auth/social-login/auth-gitlab', official: true, supporter: 'Supabase', platform: true, @@ -83,7 +65,7 @@ const authProviders = [ { name: 'Google', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-google', + href: '/guides/auth/social-login/auth-google', official: true, supporter: 'Supabase', platform: true, @@ -91,7 +73,7 @@ const authProviders = [ }, { name: 'Keycloak', - href: '/guides/auth/auth-keycloak', + href: '/guides/auth/social-login/auth-keycloak', official: true, supporter: 'Supabase', platform: true, @@ -100,7 +82,7 @@ const authProviders = [ { name: 'LinkedIn', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-linkedin', + href: '/guides/auth/social-login/auth-linkedin', official: true, supporter: 'Supabase', platform: true, @@ -109,7 +91,7 @@ const authProviders = [ { name: 'MessageBird', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-messagebird', + href: '/guides/auth/phone-login/messagebird', official: false, supporter: 'MessageBird', platform: true, @@ -118,7 +100,7 @@ const authProviders = [ { name: 'Notion', // logo: '/img/libraries/notion-icon.svg', - href: '/guides/auth/auth-notion', + href: '/guides/auth/social-login/auth-notion', official: true, supporter: 'Supabase', platform: true, @@ -127,7 +109,7 @@ const authProviders = [ { name: 'Slack', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-slack', + href: '/guides/auth/social-login/auth-slack', official: true, supporter: 'Supabase', platform: true, @@ -136,7 +118,7 @@ const authProviders = [ { name: 'Spotify', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-spotify', + href: '/guides/auth/social-login/auth-spotify', official: true, supporter: 'Supabase', platform: true, @@ -145,7 +127,7 @@ const authProviders = [ { name: 'Twitter', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-twitter', + href: '/guides/auth/social-login/auth-twitter', official: true, supporter: 'Supabase', platform: true, @@ -154,7 +136,7 @@ const authProviders = [ { name: 'Twitch', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-twitch', + href: '/guides/auth/social-login/auth-twitch', official: true, supporter: 'Supabase', platform: true, @@ -163,7 +145,7 @@ const authProviders = [ { name: 'Zoom', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-zoom', + href: '/guides/auth/social-login/auth-zoom', official: true, supporter: 'Supabase', platform: true, @@ -172,7 +154,7 @@ const authProviders = [ { name: 'Twilio', // logo: '/img/libraries/dart-icon.svg', - href: '/guides/auth/auth-twilio', + href: '/guides/auth/phone-login/twilio', official: true, supporter: 'Supabase', platform: true, @@ -180,7 +162,7 @@ const authProviders = [ }, { name: 'Vonage', - href: '/guides/auth/auth-vonage', + href: '/guides/auth/phone-login/vonage', official: false, supporter: 'Supabase', platform: true, diff --git a/apps/docs/data/footer.json b/apps/docs/data/footer.json index c67c52972bc..3d3286890c4 100644 --- a/apps/docs/data/footer.json +++ b/apps/docs/data/footer.json @@ -90,7 +90,7 @@ }, { "text": "SupaSquad", - "url": "/docs/handbook/supasquad" + "url": "/supasquad" }, { "text": "DevTo", diff --git a/apps/docs/data/nav/supabase-dart/v1.ts b/apps/docs/data/nav/supabase-dart/v1.ts index fb1491e2555..8fe2fad4791 100644 --- a/apps/docs/data/nav/supabase-dart/v1.ts +++ b/apps/docs/data/nav/supabase-dart/v1.ts @@ -118,6 +118,11 @@ const Nav = [ url: '/reference/dart/storage-from-createsignedurl', items: [], }, + { + name: 'from.createSignedUrls()', + url: '/reference/dart/storage-from-createsignedurls', + items: [], + }, { name: 'from.getPublicUrl()', url: '/reference/dart/storage-from-getpublicurl', diff --git a/apps/docs/docs/ref/README.md b/apps/docs/docs/ref/README.md new file mode 100644 index 00000000000..3b50031fdf4 --- /dev/null +++ b/apps/docs/docs/ref/README.md @@ -0,0 +1,3 @@ +# testing + +@mildtomato is testing a new ref layout/mdx setup diff --git a/apps/docs/docs/ref/api/api.mdx b/apps/docs/docs/ref/api/api.mdx new file mode 100644 index 00000000000..371041af0bd --- /dev/null +++ b/apps/docs/docs/ref/api/api.mdx @@ -0,0 +1,37 @@ +--- +slug: / +sidebar_position: 1 +id: management-api +title: Management API +sidebar_label: Management API +--- + +The Management API allows you to manage your projects programmatically. + +## Status + +The Management API is in `beta`. It is usable in it's current state, but it's likely that there will be breaking changes. + +## Authentication + +All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization Bearer + +
    +

    Management API

    +
    +
    + + + + + Manage your Supabase organizations and projects programmatically. + + ## Status + + The Management API is in `beta`. It is usable in it's current state, but it's likely that there will be breaking changes. + + ## Authentication + + All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization Bearer + + + + Additional links + + - [OpenAPI Docs](https://api.supabase.com/api/v1) + - [OpenAPI Spec](https://api.supabase.com/api/v1-json) + - [Report bugs and issues](https://github.com/supabase/supabase) + + + + diff --git a/apps/docs/docs/ref/cli/global-flags.mdx b/apps/docs/docs/ref/cli/global-flags.mdx new file mode 100644 index 00000000000..39aaf77295b --- /dev/null +++ b/apps/docs/docs/ref/cli/global-flags.mdx @@ -0,0 +1,8 @@ +--- +title: Global Flags +description: 'Supabase CLI supports global flags for every command.' +--- + +Supabase CLI supports global flags for every command. + + diff --git a/apps/docs/docs/ref/cli/introduction.mdx b/apps/docs/docs/ref/cli/introduction.mdx new file mode 100644 index 00000000000..e204038abae --- /dev/null +++ b/apps/docs/docs/ref/cli/introduction.mdx @@ -0,0 +1,43 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Supabase CLI

    +
    +
    + + + + + The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform. + The CLI is still under development, but it contains all the functionality for working with your Supabase projects and the Supabase Platform. + + - Run Supabase locally: [`supabase start`](/docs/reference/cli/usage#supabase-start) + - Manage database migrations: [`supabase migration`](/docs/reference/cli/usage#supabase-migration) + - CI/CD for releasing to production: [`supabase db push`](/docs/reference/cli/usage#supabase-db-push) + - Manage your Supabase projects: [`supabase projects`](/docs/reference/cli/usage#supabase-projects) + - Generate types directly from your database schema: [`supabase gen types`](/docs/reference/cli/usage#supabase-gen) + - A [community-supported GitHub Action](https://github.com/lyqht/generate-supabase-db-types-github-action) to generate TypeScript types + - Shell autocomplete: [`supabase completion`](/docs/reference/cli/usage#supabase-completion) + - A [community-supported Fig autocomplete spec](https://fig.io/manual/supabase) for macOS terminal + + + + + + ### Additional links + + - [Install the Supabase CLI](/docs/guides/cli) + - [Source code](https://github.com/supabase/cli) + - [Known bugs and issues](https://github.com/supabase/cli/issues) + - [Supabase CLI v1 and Management API Beta](https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta) + - [Video: Announcing CLI V1 and Management API Beta](https://www.youtube.com/watch?v=OpPOaJI_Z28) + + + + diff --git a/apps/docs/docs/ref/dart/initializing.mdx b/apps/docs/docs/ref/dart/initializing.mdx new file mode 100644 index 00000000000..501efaef48f --- /dev/null +++ b/apps/docs/docs/ref/dart/initializing.mdx @@ -0,0 +1,50 @@ +--- +id: initializing +title: 'Initializing' +slug: initializing +--- + + + + + Initialize Supabase with the static `initialize()` method on `Supabase` class. + + + + + + + + + ```dart main.dart + Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Supabase.initialize( + url: 'https://xyzcompany.supabase.co', + anonKey: 'public-anon-key', + ); + + runApp(MyApp()); + } + ``` + + + + + ```dart + final supabase = SupabaseClient( + 'https://xyzcompany.supabase.co', + 'public-anon-key', + ); + ``` + + + + + + diff --git a/apps/docs/docs/ref/dart/installing.mdx b/apps/docs/docs/ref/dart/installing.mdx new file mode 100644 index 00000000000..d53e3c99218 --- /dev/null +++ b/apps/docs/docs/ref/dart/installing.mdx @@ -0,0 +1,41 @@ +--- +id: installing +title: 'Installing' +slug: installing +custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml +--- + +### Install from pub.dev + + + + + You can install Supabase package from [pub.dev](https://pub.dev/packages/supabase_flutter) + + + + + + + + + ```sh Terminal + flutter pub add supabase_flutter + ``` + + + + + ```sh Terminal + dart pub add supabase + ``` + + + + + + diff --git a/apps/docs/docs/ref/dart/introduction.mdx b/apps/docs/docs/ref/dart/introduction.mdx new file mode 100644 index 00000000000..8dabccf22b2 --- /dev/null +++ b/apps/docs/docs/ref/dart/introduction.mdx @@ -0,0 +1,23 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Flutter Client Library

    +

    supabase-flutter

    +
    +
    + +
    + This reference documents every object and method available in Supabase's Flutter + library, [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can + use supabase-flutter to interact with your Postgres database, listen to database changes, invoke + Deno Edge Functions, build login and user management functionality, and manage large files. +
    +
    + We also provide [supabase](https://pub.dev/packages/supabase) package for non-Flutter projects. +
    diff --git a/apps/docs/docs/ref/dart/release-notes.mdx b/apps/docs/docs/ref/dart/release-notes.mdx new file mode 100644 index 00000000000..b5e0e218918 --- /dev/null +++ b/apps/docs/docs/ref/dart/release-notes.mdx @@ -0,0 +1,6 @@ +--- +id: release-notes +title: Release Notes +--- + +## dart this is the release notes file. diff --git a/apps/docs/docs/ref/dart/v0/release-notes.mdx b/apps/docs/docs/ref/dart/v0/release-notes.mdx new file mode 100644 index 00000000000..2ecb0ef3990 --- /dev/null +++ b/apps/docs/docs/ref/dart/v0/release-notes.mdx @@ -0,0 +1,6 @@ +--- +id: release-notes +title: Release Notes +--- + +## dart v0 this is the release notes file. diff --git a/apps/docs/docs/ref/javascript/auth.mdx b/apps/docs/docs/ref/javascript/auth.mdx new file mode 100644 index 00000000000..387db9bdfa8 --- /dev/null +++ b/apps/docs/docs/ref/javascript/auth.mdx @@ -0,0 +1,12 @@ +--- +id: auth +title: 'Auth' +slug: auth +icon: /docs/img/icons/menu/auth +--- + + + + Every Supabase project comes with a full Postgres database, a free and open source database which is considered one of the world's most stable and advanced databases. + + diff --git a/apps/docs/docs/ref/javascript/database.mdx b/apps/docs/docs/ref/javascript/database.mdx new file mode 100644 index 00000000000..900107af0b4 --- /dev/null +++ b/apps/docs/docs/ref/javascript/database.mdx @@ -0,0 +1,12 @@ +--- +id: database +title: 'Database' +slug: database +icon: /docs/img/icons/menu/database +--- + + + + Every Supabase project comes with a full Postgres database, a free and open source database which is considered one of the world's most stable and advanced databases. + + diff --git a/apps/docs/docs/ref/javascript/installing.mdx b/apps/docs/docs/ref/javascript/installing.mdx new file mode 100644 index 00000000000..fb4e334389e --- /dev/null +++ b/apps/docs/docs/ref/javascript/installing.mdx @@ -0,0 +1,79 @@ +--- +id: installing +title: 'Installing' +slug: installing +custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml +--- + +### Install as package + + + + + You can install @supabase/supabase-js via the terminal. + + + + + + + + + ```sh Terminal + npm install @supabase/supabase-js + ``` + + + + + ```sh Terminal + yarn add @supabase/supabase-js + ``` + + + + + + + +### Install via CDN + + + + + You can install @supabase/supabase-js via CDN links. + + + + + + ```js + + //or + + ``` + + + + +### Use at runtime in Deno + + + + + You can use supabase-js in the Deno runtime via esm.sh: + + + + + + ```ts + import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' + ``` + + + diff --git a/apps/docs/docs/ref/javascript/introduction.mdx b/apps/docs/docs/ref/javascript/introduction.mdx new file mode 100644 index 00000000000..d3c14042cb6 --- /dev/null +++ b/apps/docs/docs/ref/javascript/introduction.mdx @@ -0,0 +1,20 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Javascript Client Library

    +

    @supabase/supabase-js

    +
    +
    + +
    + This reference documents every object and method available in Supabase's isomorphic JavaScript + library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to + database changes, invoke Deno Edge Functions, build login and user management functionality, and + manage large files. +
    diff --git a/apps/docs/docs/ref/javascript/release-notes.mdx b/apps/docs/docs/ref/javascript/release-notes.mdx new file mode 100644 index 00000000000..f997c312e77 --- /dev/null +++ b/apps/docs/docs/ref/javascript/release-notes.mdx @@ -0,0 +1,309 @@ +--- +id: release-notes +title: Release Notes 2.0.0 +--- + +Supabase.js v2 release notes. + + + + + Install the latest version of @supabase/supabase-js + + + + + ```bash Terminal + npm install @supabase/supabase-js + ``` + + + + +### Explicit constructor options + + + + + All client specific options within the constructor are keyed to the library. + + See [PR](https://github.com/supabase/supabase-js/pull/458): + + + + + ```jsx + const supabase = createClient(apiURL, apiKey, { + db: { + schema: 'public', + }, + auth: { + storage: AsyncStorage, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + }, + realtime: { + channels, + endpoint, + }, + global: { + fetch: customFetch, + headers: DEFAULT_HEADERS, + }, + }) + ``` + + + + +### Typescript support + + + + + The libraries now support typescript. + + + + + ```ts v1.0 + // previously definitions were injected in the `from()` method + supabase.from('messages').select('\*') + ``` + + --- + + ```ts v2.0 + import type { Database } from './DatabaseDefinitions' + + // definitions are injected in `createClient()` + const supabase = createClient(SUPABASE_URL, ANON_KEY) + + const { data } = await supabase.from('messages').select().match({ id: 1 }) + ``` + + + + + Types can be generated via the CLI: + + + + + ```bash Terminal + supabase start + supabase gen types typescript --local > DatabaseDefinitions.ts + ``` + + + + +### Data operations return minimal + + + + + `.insert()` / `.upsert()` / `.update()` / `.delete()` don't return rows by default: [PR](https://github.com/supabase/postgrest-js/pull/276). + + Previously, these methods return inserted/updated/deleted rows by default (which caused [some confusion](https://github.com/supabase/supabase/discussions/1548)), and you can opt to not return it by specifying `returning: 'minimal'`. Now the default behavior is to not return rows. To return inserted/updated/deleted rows, add a `.select()` call at the end, e.g.: + + + + + ```sql + const { data, error } = await supabase + .from('my_table') + .delete() + .eq('id', 1) + .select() + ``` + + + + +### New ordering defaults + + + + + `.order()` now defaults to Postgres’s default: [PR](https://github.com/supabase/postgrest-js/pull/283). + + Previously `nullsFirst` defaults to `false` , meaning `null`s are ordered last. This is bad for performance if e.g. the column uses an index with `NULLS FIRST` (which is the default direction for indexes). + + + + + + + +### Cookies and localstorage namespace + + + + + Storage key name in the Auth library has changed to include project reference which means that existing websites that had their JWT expiry set to a longer time could find their users logged out with this upgrade. + + + + + ```jsx + const defaultStorageKey = `sb-${new URL(this.authUrl).hostname.split('.')[0]}-auth-token` + ``` + + + + +### New Auth Types + + + + + Typescript typings have been reworked. `Session` interface now guarantees that it will always have an `access_token`, `refresh_token` and `user` + + + + + ```jsx ./types.ts + interface Session { + provider_token?: string | null + access_token: string + expires_in?: number + expires_at?: number + refresh_token: string + token_type: string + user: User + } + ``` + + + + +### New Auth methods + + + + + We're removing the `signIn()` method in favor of more explicit function signatures: + `signInWithPassword()`, `signInWithOtp()`, and `signInWithOtp()`. + + + + + ```ts v1.0 + const { data } = await supabase.auth.signIn({ + email: 'hello@example', + password: 'pass', + }) + ``` + --- + + ```ts v2.0 + const { data } = await supabase.auth.signInWithPassword({ + email: 'hello@example', + password: 'pass', + }) + ``` + + + + +### New Realtime methods + + + + + There is a new `channel()` method in the Realtime library, which will be used for our Multiplayer updates. + + We will deprecate the `.from().on().subscribe()` method previosuly used for listening to postgres changes. + + + + + ```ts + supabase + .channel('any_string_you_want') + .on('presence', { event: 'track' }, (payload) => { + console.log(payload) + }) + .subscribe() + + supabase + .channel('any_string_you_want') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'movies', + }, + (payload) => { + console.log(payload) + } + ) + .subscribe() + ``` + + + + +### Deprecated setAuth() + +Deprecated and removed `setAuth()` . To set a custom `access_token` jwt instead, pass the custom header into the `createClient()` method provided: ([PR](https://github.com/supabase/gotrue-js/pull/340)) + +### All changes + +- `supabase-js` + - `shouldThrowOnError` has been removed until all the client libraries support this option ([PR](https://github.com/supabase/supabase-js/pull/490)). +- `postgrest-js` + - TypeScript typings have been reworked [PR](https://github.com/supabase/postgrest-js/pull/279) + - Use `undefined` instead of `null` for function params, types, etc. (https://github.com/supabase/postgrest-js/pull/278) + - Some features are now obsolete: (https://github.com/supabase/postgrest-js/pull/275) + - filter shorthands (e.g. `cs` vs. `contains`) + - `body` in response (vs. `data`) + - `upsert`ing through the `.insert()` method + - `auth` method on `PostgrestClient` + - client-level `throwOnError` +- `gotrue-js` + - `supabase-js` client allows passing a `storageKey` param which will allow the user to set the key used in local storage for storing the session. By default, this will be namespace-d with the supabase project ref. ([PR](https://github.com/supabase/supabase-js/pull/460)) + - `signIn` method is now split into `signInWithPassword` , `signInWithOtp` , `signInWithOAuth` ([PR](https://github.com/supabase/gotrue-js/pull/304)) + - Deprecated and removed `session()` , `user()` in favour of using `getSession()` instead. `getSession()` will always return a valid session if a user is already logged in, meaning no more random logouts. ([PR](https://github.com/supabase/gotrue-js/pull/299)) + - Deprecated and removed setting for `multitab` support because `getSession()` and gotrue’s reuse interval setting takes care of session management across multiple tabs ([PR](https://github.com/supabase/gotrue-js/pull/366)) + - No more throwing of random errors, gotrue-js v2 always returns a custom error type: ([PR](https://github.com/supabase/gotrue-js/pull/341)) + - `AuthSessionMissingError` + - Indicates that a session is expected but missing + - `AuthNoCookieError` + - Indicates that a cookie is expected but missing + - `AuthInvalidCredentialsError` + - Indicates that the incorrect credentials were passed + - Renamed the `api` namespace to `admin` , the `admin` namespace will only contain methods that should only be used in a trusted server-side environment with the service role key + - Moved `resetPasswordForEmail` , `getUser` and `updateUser` to the `GoTrueClient` which means they will be accessible from the `supabase.auth` namespace in `supabase-js` instead of having to do `supabase.auth.api` to access them + - Removed `sendMobileOTP` , `sendMagicLinkEmail` in favor of `signInWithOtp` + - Removed `signInWithEmail`, `signInWithPhone` in favor of `signInWithPassword` + - Removed `signUpWithEmail` , `signUpWithPhone` in favor of `signUp` + - Replaced `update` with `updateUser` +- `storage-js` + - Return types are more strict. Functions types used to indicate that the data returned could be null even if there was no error. We now make use of union types which only mark the data as null if there is an error and vice versa. ([PR](https://github.com/supabase/storage-js/pull/60)) + - The `upload` and `update` function returns the path of the object uploaded as the `path` parameter. Previously the returned value had the bucket name prepended to the path which made it harder to pass the value on to other storage-js methods since all methods take the bucket name and path separately. We also chose to call the returned value `path` instead of `Key` ([PR](https://github.com/supabase/storage-js/pull/75)) + - `getPublicURL` only returns the public URL inside the data object. This keeps it consistent with our other methods of returning only within the data object. No error is returned since this method cannot does not throw an error ([PR](https://github.com/supabase/storage-js/pull/93)) + - signed urls are returned as `signedUrl` instead of `signedURL` in both `createSignedUrl` and `createSignedUrls` ([PR](https://github.com/supabase/storage-js/pull/94)) + - Encodes URLs returned by `createSignedUrl`, `createSignedUrls` and `getPublicUrl` ([PR](https://github.com/supabase/storage-js/pull/86)) + - `createsignedUrl` used to return a url directly and and within the data object. This was inconsistent. Now we always return values only inside the data object across all methods. ([PR](https://github.com/supabase/storage-js/pull/88)) + - `createBucket` returns a data object instead of the name of the bucket directly. ([PR](https://github.com/supabase/storage-js/pull/89)) + - Fixed types for metadata ([PR](https://github.com/supabase/storage-js/pull/90)) + - Better error types make it easier to track down what went wrong quicker. + - `SupabaseStorageClient` is no longer exported. Use `StorageClient` instead. ([PR](https://github.com/supabase/storage-js/pull/92)). +- `realtime-js` + - `RealtimeSubscription` class no longer exists and replaced by `RealtimeChannel`. + - `RealtimeClient`'s `disconnect` method now returns type of `void` . It used to return type of `Promise<{ error: Error | null; data: boolean }`. + - Removed `removeAllSubscriptions` and `removeSubscription` methods from `SupabaseClient` class. + - Removed `SupabaseRealtimeClient` class. + - Removed `SupabaseQueryBuilder` class. + - Removed `SupabaseEventTypes` type. + - Thinking about renaming this to something like `RealtimePostgresChangeEvents` and moving it to `realtime-js` v2. + - Removed `.from(’table’).on(’INSERT’, () ⇒ {}).subscribe()` in favor of new Realtime client API. +- `functions-js` + - supabase-js v1 only threw an error if the fetch call itself threw an error (network errors, etc) and not if the function returned HTTP errors like 400s or 500s. We have changed this behaviour to return an error if your function throws an error. + - We have introduced new error types to distinguish between different kinds of errors. A `FunctionsHttpError` error is returned if your function throws an error, `FunctionsRelayError` if the Supabase Relay has an error processing your function and `FunctionsFetchError` if there is a network error in calling your function. + - The correct content-type headers are automatically attached when sending the request if you don’t pass in a `Content-Type` header and pass in an argument to your function. We automatically attach the content type for `Blob`, `ArrayBuffer`, `File`, `FormData` ,`String` . If it doesn’t match any of these we assume the payload is `json` , we serialise the payload as JSON and attach the content type as `application/json`. + - `responseType` does not need to be explicitly passed in. We parse the response based on the `Content-Type` response header sent by the function. We support parsing the responses as `text`, `json`, `blob`, `form-data` and are parsed as `text` by default. diff --git a/apps/docs/docs/ref/javascript/typescript-support.mdx b/apps/docs/docs/ref/javascript/typescript-support.mdx new file mode 100644 index 00000000000..51162da7b93 --- /dev/null +++ b/apps/docs/docs/ref/javascript/typescript-support.mdx @@ -0,0 +1,128 @@ +--- +id: typescript-support +title: Typescript Support +--- + +`supabase-js` supports Typescript. + +## Generating types + + + + + You can use our CLI to generate types: + + + + + ```bash Terminal + supabase start + supabase gen types typescript --local > lib/database.types.ts + ``` + + + + + + + + These types are generated directly from your database. + + There is a difference between `selects`, `inserts`, and `updates`, because often you will set default values in your database for specific columns. + With default values you do not need to send any data over the network, even if that column is a "required" field. Our type system is granular + enough to handle these situations. + + + + + Given a table `public.movies`, the definition will provide the following data: + + ```ts /types.ts + interface Database { + public: { + Tables: { + movies: { + Row: {} // The data expected to be returned from a "select" statement. + Insert: {} // The data expected passed to an "insert" statement. + Update: {} // The data expected passed to an "update" statement. + } + } + } + } + ``` + + + + +## Injecting type definitions + + + + + You can enrich the supabase client with the types you generated with Supabase. + + + + ```ts ./index.tsx + import { createClient } from '@supabase/supabase-js' + import { Database } from 'lib/database.types' + + const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY + ) + ``` + + + + +## Type hints + + + + + `supabase-js` always returns a `data` object (for success), and an `error` response (for unsuccessful requests). + + This provides a simple interface to get the relevant types returned from any function: + + + + ```ts + export async function getMovies() { + return await supabase.from('movies').select(`id, title`) + } + + type MoviesResponse = Awaited> + export type MoviesResponseSuccess = MoviesResponse['data'] + export type MoviesResponseError = MoviesResponse['error'] + + ```` + + + + +## Nested tables + + + + For advanced queries such as nested tables, you may want to construct your own types. + + + ```ts ./index.ts + import supabase from '~/lib/supabase' + import type { Database } from '~/lib/database.types' + + async function getMovies() { + return await supabase.from('movies').select('id, title, actors(\*)') + } + + type Actors = Database['public']['tables']['actors']['row'] + type MoviesResponse = Awaited> + type MoviesResponseSuccess = MoviesResponse['data'] & { + actors: Actors[] + } + + ``` + + + diff --git a/apps/docs/docs/ref/javascript/v1/introduction.mdx b/apps/docs/docs/ref/javascript/v1/introduction.mdx new file mode 100644 index 00000000000..3e1af1ff9dd --- /dev/null +++ b/apps/docs/docs/ref/javascript/v1/introduction.mdx @@ -0,0 +1,20 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Javascript Client Library

    +

    @supabase/supabase-js

    +
    +
    + +
    + This reference documents every object and method available in Supabase's isomorphic JavaScript + library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to + database changes, invoke Deno Edge Functions, build login and user management functionality, and + manage large files. +
    diff --git a/apps/docs/docs/ref/javascript/v1/release-notes.mdx b/apps/docs/docs/ref/javascript/v1/release-notes.mdx new file mode 100644 index 00000000000..725cbb5201e --- /dev/null +++ b/apps/docs/docs/ref/javascript/v1/release-notes.mdx @@ -0,0 +1,6 @@ +--- +id: release-notes +title: Release Notes +--- + +## js v1 this is the release notes file. diff --git a/apps/docs/docs/ref/realtime/realtime.mdx b/apps/docs/docs/ref/realtime/realtime.mdx new file mode 100644 index 00000000000..890440d0ee9 --- /dev/null +++ b/apps/docs/docs/ref/realtime/realtime.mdx @@ -0,0 +1,57 @@ +--- +slug: / +sidebar_position: 1 +id: realtime +title: Supabase Realtime Server +sidebar_label: Supabase Realtime Server +--- + +Supabase Realtime is a server built with Elixir using the [Phoenix Framework](https://www.phoenixframework.org) that allows you to listen to changes in your PostgreSQL database via logical replication and then broadcast those changes via WebSockets. + +There are two versions of this server: `Realtime` and `Realtime RLS`. + +`Realtime` server works by: + +1. listening to PostgreSQL's replication functionality (using PostgreSQL's logical decoding) +2. converting the byte stream into JSON +3. broadcasting to all connected clients over WebSockets + +`Realtime RLS` server works by: + +1. polling PostgreSQL's replication functionality (using PostgreSQL's logical decoding and [wal2json](https://github.com/eulerto/wal2json) output plugin) +2. passing database changes to a [Write Ahead Log Realtime Unified Security (WALRUS)](https://github.com/supabase/walrus) PostgresSQL function and receiving a list of authorized subscribers depending on Row Level Security (RLS) policies +3. converting the changes into JSON +4. broadcasting to authorized subscribers over WebSockets + +## Why not just use PostgreSQL's `NOTIFY`? + +A few reasons: + +1. You don't have to set up triggers on every table. +2. `NOTIFY` has a payload limit of 8000 bytes and will fail for anything larger. The usual solution is to send an ID and then fetch the record, but that's heavy on the database. +3. `Realtime` server consumes two connections to the database, then you can connect many clients to this server. Easier on your database, and to scale up you just add additional `Realtime` servers. + +## Benefits + +1. The beauty of listening to the replication functionality is that you can make changes to your database from anywhere - your API, directly in the DB, via a console, etc. - and you will still receive the changes via WebSockets. +2. Decoupling. For example, if you want to send a new slack message every time someone makes a new purchase you might build that functionality directly into your API. This allows you to decouple your async functionality from your API. +3. This is built with Phoenix, an [extremely scalable Elixir framework](https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections). + +## Does this server guarantee delivery of every data change? + +Not yet! Due to the following limitations: + +1. Postgres database runs out of disk space due to Write-Ahead Logging (WAL) buildup, which can crash the database and prevent Realtime server from receiving and broadcasting changes. This can be mitigated in the Realtime RLS version of this server by setting the Postgres config `max_slot_wal_keep_size` to a reasonable size. +2. Realtime server can crash due to a larger replication lag than available memory, forcing the creation of a new replication slot and resetting replication to read from the latest WAL data. +3. When Realtime server falls too far behind for any reason, for example disconnecting from database as WAL continues to build up, then database can delete WAL segments the server still needs to read from, for example after reconnecting. + +## Client libraries + +- [JavaScript](https://github.com/supabase/realtime-js) +- [Dart](https://github.com/supabase/realtime-dart) + +## Additional Links + +- [Source Code](https://github.com/supabase/realtime) +- [Known bugs and issues](https://github.com/supabase/realtime/issues) +- [Realtime Guides](https://supabase.com/docs/guides/realtime) diff --git a/apps/docs/docs/ref/self-hosting-auth/introduction.mdx b/apps/docs/docs/ref/self-hosting-auth/introduction.mdx new file mode 100644 index 00000000000..d33aee3de4f --- /dev/null +++ b/apps/docs/docs/ref/self-hosting-auth/introduction.mdx @@ -0,0 +1,38 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Self-Hosting Auth

    +
    +
    + + + + + The Supabase Auth Server (GoTrue) is a JSON Web Token (JWT)-based API for managing users and issuing access tokens. + + GoTrue is an open-source API written in Golang, that acts as a self-standing API service for handling user registration and authentication for JAM projects. It's based on OAuth2 and JWT and handles user signup, authentication, and custom user data. + + + + + + ### Client libraries + + - [JavaScript](https://github.com/supabase/gotrue-js) + - [Dart](https://github.com/supabase/gotrue-dart) + + ### Additional links + + - [Source code](https://github.com/supabase/gotrue) + - [Known bugs and issues](https://github.com/supabase/gotrue/issues) + - [Auth guides](/docs/guides/auth) + + + + diff --git a/apps/docs/docs/ref/self-hosting-realtime/introduction.mdx b/apps/docs/docs/ref/self-hosting-realtime/introduction.mdx new file mode 100644 index 00000000000..310e0c96192 --- /dev/null +++ b/apps/docs/docs/ref/self-hosting-realtime/introduction.mdx @@ -0,0 +1,73 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Self-Hosting Realtime

    +
    +
    + + + + + Supabase Realtime is a server built with Elixir using the [Phoenix Framework](https://www.phoenixframework.org) that allows you to listen to changes in your PostgreSQL database via logical replication and then broadcast those changes via WebSockets. + + There are two versions of this server: `Realtime` and `Realtime RLS`. + + `Realtime` server works by: + + 1. Listening to PostgreSQL's replication functionality (using PostgreSQL's logical decoding) + 2. Converting the byte stream into JSON + 3. Broadcasting to all connected clients over WebSockets + + `Realtime RLS` server works by: + + 1. Polling PostgreSQL's replication functionality (using PostgreSQL's logical decoding and [wal2json](https://github.com/eulerto/wal2json) output plugin) + 2. Passing database changes to a [Write Ahead Log Realtime Unified Security (WALRUS)](https://github.com/supabase/walrus) PostgresSQL function and receiving a list of authorized subscribers depending on Row Level Security (RLS) policies + 3. Converting the changes into JSON + 4. Broadcasting to authorized subscribers over WebSockets + + ## Why not just use PostgreSQL's `NOTIFY`? + + A few reasons: + + 1. You don't have to set up triggers on every table. + 2. `NOTIFY` has a payload limit of 8000 bytes and will fail for anything larger. The usual solution is to send an ID and then fetch the record, but that's heavy on the database. + 3. `Realtime` server consumes two connections to the database, then you can connect many clients to this server. Easier on your database, and to scale up you just add additional `Realtime` servers. + + ## Benefits + + 1. The beauty of listening to the replication functionality is that you can make changes to your database from anywhere - your API, directly in the DB, via a console, etc. - and you will still receive the changes via WebSockets. + 2. Decoupling. For example, if you want to send a new slack message every time someone makes a new purchase you might build that functionality directly into your API. This allows you to decouple your async functionality from your API. + 3. This is built with Phoenix, an [extremely scalable Elixir framework](https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections). + + ## Does this server guarantee delivery of every data change? + + Not yet! Due to the following limitations: + + 1. Postgres database runs out of disk space due to Write-Ahead Logging (WAL) buildup, which can crash the database and prevent Realtime server from receiving and broadcasting changes. This can be mitigated in the Realtime RLS version of this server by setting the Postgres config `max_slot_wal_keep_size` to a reasonable size. + 2. Realtime server can crash due to a larger replication lag than available memory, forcing the creation of a new replication slot and resetting replication to read from the latest WAL data. + 3. When Realtime server falls too far behind for any reason, for example disconnecting from database as WAL continues to build up, then database can delete WAL segments the server still needs to read from, for example after reconnecting. + + + + + + ### Client libraries + + - [JavaScript](https://github.com/supabase/realtime-js) + - [Dart](https://github.com/supabase/realtime-dart) + + ### Additional links + + - [Source code](https://github.com/supabase/realtime) + - [Known bugs and issues](https://github.com/supabase/realtime/issues) + - [Realtime guides](/docs/guides/realtime) + + + + diff --git a/apps/docs/docs/ref/self-hosting-storage/introduction.mdx b/apps/docs/docs/ref/self-hosting-storage/introduction.mdx new file mode 100644 index 00000000000..40fd27d4ce8 --- /dev/null +++ b/apps/docs/docs/ref/self-hosting-storage/introduction.mdx @@ -0,0 +1,43 @@ +--- +id: introduction +title: Introduction +hideTitle: true +--- + +
    + +
    +

    Self-Hosting Storage

    +
    +
    + + + + + An S3 compatible object storage service that integrates with Postgres. + + - Uses Postgres as it's datastore for storing metadata + - Authorization rules are written as Postgres Row Level Security policies + - Integrates with S3 as the storage backend (with more in the pipeline!) + - Extremely lightweight and performant + + + + + + ### Client libraries + + - [JavaScript](https://github.com/supabase/storage-js) + - [Dart](https://github.com/supabase/storage-dart) + + ### Additional links + + - [Source code](https://github.com/supabase/storage-api) + - [Known bugs and issues](https://github.com/supabase/storage-js/issues) + - [Storage guides](/docs/guides/storage) + - [OpenAPI docs](https://supabase.github.io/storage-api/) + - [Why we built a new object storage service](https://supabase.com/blog/supabase-storage) + + + + diff --git a/apps/docs/docs/reference/dart/initializing.mdx b/apps/docs/docs/reference/dart/initializing.mdx index 00a69bbf1f3..7d914d71efd 100644 --- a/apps/docs/docs/reference/dart/initializing.mdx +++ b/apps/docs/docs/reference/dart/initializing.mdx @@ -28,6 +28,33 @@ Once you initialize Supabase in your `main()` method, you can access the `Supaba final supabase = Supabase.instance.client; ``` +### Call `initialize()` with custom headers + +You can pass `headers` to initialize your Supabase client with customer headers. +Here is an example of passing a custom auth header to Supabase client. +```dart title=main.dart +Future main() async { + await Supabase.initialize( + url: 'https://xyzcompany.supabase.co', + anonKey: 'public-anon-key', + headers: { + 'Authorization': 'Bearer $accessToken', + }, + ); + runApp(const MyApp()); +} +``` + +### Call `initialize()`multiple times + +You can initialize Supabase multiple times in your app. When doing so, call `dispose()` each time before you initialize again. + +```dart +Supabase.instance.dispose(); + +await Supabase.initialize(url: 'https://xyzcompany.supabase.co', anonKey: 'public-anon-key'); +``` + ## Other Dart Projects You can initialize a new Supabase client using the `SupabaseClient()` method. diff --git a/apps/docs/docs/reference/javascript/installing.mdx b/apps/docs/docs/reference/javascript/installing.mdx index b5be32e09a4..b948b44d38e 100644 --- a/apps/docs/docs/reference/javascript/installing.mdx +++ b/apps/docs/docs/reference/javascript/installing.mdx @@ -5,9 +5,6 @@ slug: installing custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml --- -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' - ## JavaScript lib/database.types.ts These types are generated directly from your database. Given a table `public.movies`, the definition will provide the following data: -```ts +```tsx hello.ts interface Database { public: { Tables: { diff --git a/apps/docs/docs/reference/javascript/v1/initializing.mdx b/apps/docs/docs/reference/javascript/v1/initializing.mdx index 46d057e1cca..7ea5f5cf164 100644 --- a/apps/docs/docs/reference/javascript/v1/initializing.mdx +++ b/apps/docs/docs/reference/javascript/v1/initializing.mdx @@ -16,7 +16,7 @@ and is the easiest way to interact with everything we offer within the Supabase ## Parameters
      - +
    • @@ -34,7 +34,7 @@ and is the easiest way to interact with everything we offer within the Supabase The unique Supabase URL which is supplied when you create a new project in your project dashboard.

    - +
  • @@ -54,7 +54,7 @@ The unique Supabase URL which is supplied when you create a new project in your The unique Supabase Key which is supplied when you create a new project in your project dashboard. - +
  • @@ -74,7 +74,7 @@ The unique Supabase Key which is supplied when you create a new project in your No description provided. - +
  • @@ -117,7 +117,7 @@ const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key ``` By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard. -Go to `Settings > API > Schema` and add the schema which you want to expose to the API. +Go to `Settings > API > Schema` and add the schema which you want to expose to the API. You also need to grant `USAGE` on your new schema with the grants you desire, such as `SELECT, INSERT, UPDATE, DELETE`. Note: each client connection can only access a single schema, so the code above can access the `other_schema` schema but cannot access the `public` schema. diff --git a/apps/docs/generator/api.ts b/apps/docs/generator/api.ts index 9358306f733..6682e3a65ff 100644 --- a/apps/docs/generator/api.ts +++ b/apps/docs/generator/api.ts @@ -33,7 +33,7 @@ export default async function gen(inputFileName: string, outputDir: string, apiU type v3OperationWithPath = OpenAPIV3.OperationObject & { path: string } -type enrichedOperation = OpenAPIV3.OperationObject & { +export type enrichedOperation = OpenAPIV3.OperationObject & { path: string fullPath: string operationId: string diff --git a/apps/docs/generator/legacy.ts b/apps/docs/generator/legacy.ts index 451177f92ad..29ff1c5617e 100644 --- a/apps/docs/generator/legacy.ts +++ b/apps/docs/generator/legacy.ts @@ -16,6 +16,13 @@ import { TsDoc, OpenRef } from './legacy/definitions' import { uniqBy } from 'lodash' import * as fs from 'fs' import * as yaml from 'js-yaml' +import { flattenSections } from '../lib/helpers' + +const commonDocSpecJson = JSON.parse( + fs.readFileSync('../../spec/common-client-libs-sections.json', 'utf8') +) + +const flattenedCommonDocSpecJson = flattenSections(commonDocSpecJson) export default async function gen(inputFileName: string, outputDir: string) { const docSpec = yaml.load(fs.readFileSync(inputFileName, 'utf8')) @@ -24,7 +31,7 @@ export default async function gen(inputFileName: string, outputDir: string) { const definition = JSON.parse(defRef) const id = docSpec.info.id const allLanguages = docSpec.info.libraries - const pages = Object.entries(docSpec.pages).map(([name, x]: [string, OpenRef.Page]) => ({ + const pages = Object.entries(docSpec.functions).map(([name, x]: [string, OpenRef.Page]) => ({ ...x, pageName: name, })) @@ -38,7 +45,9 @@ export default async function gen(inputFileName: string, outputDir: string) { // Generate Pages pages.forEach(async (pageSpec: OpenRef.Page) => { try { - const slug = slugify(pageSpec.pageName) + // get the slug from common-client-libs.yml + const slug = flattenedCommonDocSpecJson.find((item) => item.id === pageSpec.id).slug + const hasTsRef = pageSpec['$ref'] || null const tsDefinition = hasTsRef && extractTsDocNode(hasTsRef, definition) if (hasTsRef && !tsDefinition) throw new Error('Definition not found: ' + hasTsRef) @@ -48,8 +57,8 @@ export default async function gen(inputFileName: string, outputDir: string) { // Create page const content = Page({ - slug: (docSpec.info.slugPrefix || '') + slug, - id: slug, + slug: slug, + id: pageSpec.id, specFileName: docSpec.info.specUrl || inputFileName, title: pageSpec.title || pageSpec.pageName, description, @@ -58,7 +67,7 @@ export default async function gen(inputFileName: string, outputDir: string) { examples: generateExamples(id, pageSpec['examples'] || [], allLanguages), notes: pageSpec.notes, }) - + //console.log({ slug }) // Write to disk const dest = outputDir + `/${slug}.mdx` await writeToDisk(dest, content) @@ -146,7 +155,7 @@ const mergeUnion = (paramDefinition: TsDoc.TypeDefinition) => { } const methodListGroup = (items) => ` -
      +
        ${items}
      ` @@ -265,7 +274,7 @@ function extractParamTypeAsString(paramDefinition) { * Iterates through the definition to find the correct definition. * You can pass it a deeply nested node using dot notation. eg: 'LoggedInUser.data.email' */ -function extractTsDocNode(nodeToFind: string, definition: any) { +export function extractTsDocNode(nodeToFind: string, definition: any) { const nodePath = nodeToFind.split('.') let i = 0 let previousNode = definition diff --git a/apps/docs/generator/legacy/definitions.ts b/apps/docs/generator/legacy/definitions.ts index 96f1f44dfe3..72d50ce8f29 100644 --- a/apps/docs/generator/legacy/definitions.ts +++ b/apps/docs/generator/legacy/definitions.ts @@ -28,6 +28,7 @@ export namespace OpenRef { title?: string description?: string notes?: string + id: string // $ref?: string // reference to a TSDoc node } } diff --git a/apps/docs/generator/templates/ApiTemplate.ts b/apps/docs/generator/templates/ApiTemplate.ts index bf95eef4f0c..d27a424d9b1 100644 --- a/apps/docs/generator/templates/ApiTemplate.ts +++ b/apps/docs/generator/templates/ApiTemplate.ts @@ -35,7 +35,7 @@ toc_max_heading_level: 3 <% if(operation.parameters && operation.parameters.filter((parameter) => parameter.in === 'path').length > 0){ %> #### Path Parameters -
        +
          <% operation.parameters .filter((parameter) => parameter.in === 'path').forEach(function(parameter){ %>
        • @@ -68,7 +68,7 @@ toc_max_heading_level: 3 <% if(operation.parameters && operation.parameters.filter((parameter) => parameter.in === 'header').length > 0){ %> #### Header Parameters -
            +
              <% operation.parameters .filter((parameter) => parameter.in === 'header').forEach(function(parameter){ %>
            • diff --git a/apps/docs/generator/templates/CliTemplate.ts b/apps/docs/generator/templates/CliTemplate.ts index 0904e47fe37..92bc55d3831 100644 --- a/apps/docs/generator/templates/CliTemplate.ts +++ b/apps/docs/generator/templates/CliTemplate.ts @@ -6,19 +6,19 @@ title: Usage toc_max_heading_level: 3 --- - +{/* */} <%- info.description %> <% commands.forEach(function(command){ %> - +{/* */}

              <%- command.heading %> - +{/* */} <%- command.description %> @@ -27,7 +27,7 @@ toc_max_heading_level: 3 <%- command.usage %> <% }; %> - +{/* */} <% if(command.subcommands.length > 0){ %> **Available Commands** @@ -38,7 +38,7 @@ toc_max_heading_level: 3 <% } %> - +{/* */} **Options** @@ -46,7 +46,7 @@ toc_max_heading_level: 3 <% }); %> - +{/* */} `.trim() export default template diff --git a/apps/docs/generator/templates/ConfigTemplate.ts b/apps/docs/generator/templates/ConfigTemplate.ts index 3300e030de8..45185b23a7e 100644 --- a/apps/docs/generator/templates/ConfigTemplate.ts +++ b/apps/docs/generator/templates/ConfigTemplate.ts @@ -22,7 +22,7 @@ toc_max_heading_level: 3 <%- parameter.description %> -
                +
                • Required: <%= parameter.required %>
                • Default: <%- parameter?.default ? parameter?.default : 'None' %>
                • <% if(parameter?.links?.length){ %>
                • diff --git a/apps/docs/hooks/useHash.ts b/apps/docs/hooks/useHash.ts new file mode 100644 index 00000000000..92c0cec4fb8 --- /dev/null +++ b/apps/docs/hooks/useHash.ts @@ -0,0 +1,29 @@ +import { useState, useCallback, useEffect } from 'react' + +const useHash = () => { + const [hash, setHash] = useState(() => + typeof window !== 'undefined' ? window.location.hash.split('#')[1] : undefined + ) + + const hashChangeHandler = useCallback(() => { + setHash(window.location.hash.split('#')[1]) + }, []) + + useEffect(() => { + window.addEventListener('hashchange', hashChangeHandler) + return () => { + window.removeEventListener('hashchange', hashChangeHandler) + } + }, []) + + const updateHash = useCallback( + (newHash) => { + if (newHash !== hash) window.location.hash = newHash + }, + [hash] + ) + + return [hash, updateHash] +} + +export default useHash diff --git a/apps/docs/hooks/useIsMounted.ts b/apps/docs/hooks/useIsMounted.ts new file mode 100644 index 00000000000..d1ff05f505f --- /dev/null +++ b/apps/docs/hooks/useIsMounted.ts @@ -0,0 +1,12 @@ +import { useState, useEffect } from 'react' + +const useIsMounted = (): boolean => { + const [isMounted, setIsMounted] = useState(false) + useEffect(() => { + setIsMounted(() => true) + }, []) + + return isMounted +} + +export default useIsMounted diff --git a/apps/docs/hooks/useMenuState.ts b/apps/docs/hooks/useMenuState.ts new file mode 100644 index 00000000000..760796e1244 --- /dev/null +++ b/apps/docs/hooks/useMenuState.ts @@ -0,0 +1,30 @@ +import { proxy, useSnapshot } from 'valtio' + +export const menuState = proxy({ + // values + menuActiveRefId: 'home', + // set states + setMenuActiveRefId: (value) => { + menuState.menuActiveRefId = value + }, + menuLevelId: '', + setMenuLevelId: (value) => { + menuState.menuMobileOpen = false + menuState.menuLevelId = value + }, + menuMobileOpen: false, + setMenuMobileOpen: (value) => { + menuState.menuMobileOpen = value + }, +}) + +export const useMenuActiveRefId = () => { + return useSnapshot(menuState).menuActiveRefId +} +export const useMenuLevelId = () => { + return useSnapshot(menuState).menuLevelId +} + +export const useMenuMobileOpen = () => { + return useSnapshot(menuState).menuMobileOpen +} diff --git a/apps/docs/hooks/useWindowLocation.ts b/apps/docs/hooks/useWindowLocation.ts new file mode 100644 index 00000000000..5545b45f211 --- /dev/null +++ b/apps/docs/hooks/useWindowLocation.ts @@ -0,0 +1,37 @@ +import useIsMounted from './useIsMounted' +import { useEffect, useState } from 'react' + +const useWindowLocation = (): Location | void => { + const isMounted = useIsMounted() + const [location, setLocation] = useState(isMounted ? window.location : undefined) + + const setWindowLocation = (location) => { + setLocation(location) + } + + useEffect(() => { + if (!isMounted) return + + if (!location) { + setWindowLocation(window.location) + } + + const handler = () => { + setWindowLocation(window.location) + } + + window.addEventListener('popstate', handler) + + return () => { + window.removeEventListener('popstate', handler) + } + }, [isMounted]) + + useEffect(() => { + // console.log('location, in hook useffect', location) + }, [location]) + + return location +} + +export default useWindowLocation diff --git a/apps/docs/internals/files/api.mjs b/apps/docs/internals/files/api.mjs new file mode 100644 index 00000000000..0f405c4edf2 --- /dev/null +++ b/apps/docs/internals/files/api.mjs @@ -0,0 +1,13 @@ +import sections from '../../../../spec/common-api-sections.json' assert { type: 'json' } +import { flattenSections } from '../helpers.mjs' + +const flatSections = flattenSections(sections) + +export function generateAPIPages() { + let apiPages = [] + + flatSections.map((section) => { + apiPages.push(`reference/api/${section.slug}`) + }) + return apiPages +} diff --git a/apps/docs/internals/files/cli.mjs b/apps/docs/internals/files/cli.mjs new file mode 100644 index 00000000000..9b7dad46168 --- /dev/null +++ b/apps/docs/internals/files/cli.mjs @@ -0,0 +1,18 @@ +import fs from 'fs' +import yaml from 'js-yaml' +import cliCommonSections from '../../../../spec/common-cli-sections.json' assert { type: 'json' } +import { flattenSections } from '../helpers.mjs' + +const flatCLISections = flattenSections(cliCommonSections) + +const cliSpec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8')) + +export function generateCLIPages() { + let cliPages = [] + + cliSpec.commands.map((section) => { + const slug = flatCLISections.find((item) => item.id === section.id)?.slug + if (slug) cliPages.push(`reference/cli/${slug}`) + }) + return cliPages +} diff --git a/apps/docs/internals/files/reference-lib.mjs b/apps/docs/internals/files/reference-lib.mjs new file mode 100644 index 00000000000..ee95d4ee625 --- /dev/null +++ b/apps/docs/internals/files/reference-lib.mjs @@ -0,0 +1,27 @@ +import fs from 'fs' + +import yaml from 'js-yaml' +import commonLibSections from '../../../../spec/common-client-libs-sections.json' assert { type: 'json' } +import { flattenSections } from '../helpers.mjs' + +const flatCommonLibSections = flattenSections(commonLibSections) + +const clientLibFiles = [ + { fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false }, + { fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true }, + { fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false }, + { fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true }, + { fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true }, +] + +export function generateReferencePages() { + let refPages = [] + clientLibFiles.map((file) => { + const spec = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8')) + spec.functions.map((fn) => { + const slug = flatCommonLibSections.find((item) => item.id === fn.id)?.slug + refPages.push(`reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${slug}`) + }) + }) + return refPages +} diff --git a/apps/docs/internals/generate-sitemap.mjs b/apps/docs/internals/generate-sitemap.mjs index 2260e0aebf9..52c00e86bbc 100644 --- a/apps/docs/internals/generate-sitemap.mjs +++ b/apps/docs/internals/generate-sitemap.mjs @@ -1,65 +1,49 @@ -import { writeFileSync } from 'fs' -import { globby } from 'globby' -import prettier from 'prettier' - /* * kudos to leerob from vercel * https://leerob.io/blog/nextjs-sitemap-robots */ +import { writeFileSync } from 'fs' +import { globby } from 'globby' +import prettier from 'prettier' +import { generateCLIPages } from './files/cli.mjs' +import { generateReferencePages } from './files/reference-lib.mjs' +import { generateAPIPages } from './files/api.mjs' + +const referencePages = generateReferencePages() +const cliPages = generateCLIPages() +const apiPages = generateAPIPages() + async function generate() { const prettierConfig = await prettier.resolveConfig('./.prettierrc.js') const rawPages = await globby([ // guides 'docs/*.mdx', - '!docs/404.mdx', 'pages/**/*.mdx', - // reference - 'docs/reference/*.mdx', - 'docs/reference/javascript/*.mdx', - 'docs/reference/javascript/generated/*.mdx', - '!docs/reference/javascript/v1.mdx', // ignore this - 'docs/reference/dart/*.mdx', - 'docs/reference/dart/generated/*.mdx', - '!docs/reference/dart/v0.mdx', // ignore this - 'docs/reference/api/*.mdx', - 'docs/reference/api/generated/*.mdx', - 'docs/reference/cli/*.mdx', - 'docs/reference/cli/generated/*.mdx', - // misc reference - 'docs/reference/postgres/*.mdx', - 'docs/reference/postgres/generated/*.mdx', - 'docs/reference/realtime/*.mdx', - 'docs/reference/realtime/generated/*.mdx', - 'docs/reference/storage/*.mdx', - 'docs/reference/storage/generated/*.mdx', - 'docs/reference/auth/*.mdx', - 'docs/reference/auth/generated/*.mdx', + '!pages/404.mdx', + '!pages/404.tsx', + '!pages/ref-pages.mdx', ]) - const pages = rawPages.map((x) => { + const guidePages = rawPages.map((x) => { let string = x - string = string.replace('/generated', '') + string = string.replace('pages/', '') string = string.replace('.mdx', '') return string }) - // add static OSS page - pages.unshift('docs/oss') - // add static homepage - pages.unshift('docs') + // combine the guidePages with the ref pages + const allPages = guidePages.concat(referencePages, cliPages, apiPages) const sitemap = ` - ${pages + ${allPages .map((path) => { - // fix homepage - path = path.replace('pages/index', '') return ` - ${`https://supabase.com/${path.replace('pages/', 'docs/')}`} + ${`https://supabase.com/docs/${path}`} weekly 0.5 @@ -75,7 +59,9 @@ async function generate() { }) const sitemapFilePath = `public/sitemap.xml` - console.log(`Total of ${pages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`) + console.log( + `Total of ${allPages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}` + ) // eslint-disable-next-line no-sync writeFileSync(sitemapFilePath, formatted) diff --git a/apps/docs/internals/helpers.mjs b/apps/docs/internals/helpers.mjs new file mode 100644 index 00000000000..afb833fee4d --- /dev/null +++ b/apps/docs/internals/helpers.mjs @@ -0,0 +1,15 @@ +export function flattenSections(sections) { + var a = [] + for (var i = 0; i < sections.length; i++) { + if (sections[i].id) { + // only push a section that has an id + // these are reserved for sidebar subtitles + a.push(sections[i]) + } + if (sections[i].items) { + // if there are subitems, loop through + a = a.concat(flattenSections(sections[i].items)) + } + } + return a +} diff --git a/apps/docs/internals/tsconfig.json b/apps/docs/internals/tsconfig.json new file mode 100644 index 00000000000..de1ecb67a32 --- /dev/null +++ b/apps/docs/internals/tsconfig.json @@ -0,0 +1,15 @@ +// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig +{ + "compilerOptions": { + "incremental": true, + "noImplicitAny": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/apps/docs/layouts/Default.tsx b/apps/docs/layouts/Default.tsx index 473fdff4bf3..515f73b89ac 100644 --- a/apps/docs/layouts/Default.tsx +++ b/apps/docs/layouts/Default.tsx @@ -4,16 +4,21 @@ import NavBar from '../components/Navigation/NavBar' import SideBar from '../components/Navigation/SideBar' import Footer from '../components/Footer' import TableOfContents from '~/components/TableOfContents' +import { menuItems } from '~/components/Navigation/Navigation.constants' +import { useRouter } from 'next/router' +import { getPageType } from '~/lib/helpers' interface Props { meta: { title: string; description?: string; hide_table_of_contents?: boolean } children: any toc?: any - menuItems: any - currentPage: string + menuItems?: any + currentPage?: string } -const Layout: FC = ({ meta, children, toc, menuItems, currentPage }) => { +const Layout: FC = ({ meta, children, toc }) => { + const { asPath } = useRouter() + useEffect(() => { const key = localStorage.getItem('supabaseDarkMode') if (!key) { @@ -27,6 +32,10 @@ const Layout: FC = ({ meta, children, toc, menuItems, currentPage }) => { const hasTableOfContents = toc !== undefined && toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 + const pageType = getPageType(asPath) + + console.log('Default, pageType', pageType) + return ( <> = ({ meta, children, toc, menuItems, currentPage }) => { openGraph={{ title: meta?.title, description: meta?.description, - url: `https://supabase.com/docs/${currentPage}`, + url: `https://supabase.com/docs/${asPath}`, images: [ { url: `https://supabase.com/docs/img/supabase-og-image.png`, @@ -45,9 +54,9 @@ const Layout: FC = ({ meta, children, toc, menuItems, currentPage }) => { />
                  - +
                  - +
                  = ({ meta, children }) => { - const { asPath } = useRouter() - const page = getPageType(asPath) - - useEffect(() => { - const key = localStorage.getItem('supabaseDarkMode') - if (!key) { - // Default to dark mode if no preference config - document.documentElement.className = 'dark' - } else { - document.documentElement.className = key === 'true' ? 'dark' : '' - } - }, []) - - const articleRef = useRef() - const [tocList, setTocList] = useState([]) - - useEffect(() => { - const articleEl = articleRef.current as HTMLElement - - if (!articleRef.current) return - const headings = Array.from(articleEl.querySelectorAll('h2, h3')) - const newHeadings = headings - .filter((heading) => heading.id) - .map((heading) => { - const text = heading.textContent.replace('#', '') - const link = heading.querySelector('a').getAttribute('href') - const level = heading.tagName === 'H2' ? 2 : 3 - return { text, link, level } - }) - setTocList(newHeadings) - }, []) - - const hasTableOfContents = tocList.length > 0 - - return ( - <> - - -
                  - -
                  - -
                  -
                  -
                  - {meta?.title &&

                  {meta.title}

                  } - {children} -
                  -
                  - {hasTableOfContents && !meta?.hide_table_of_contents && ( - - )} -
                  -
                  -
                  -
                  - - ) +const Layout: FC = (props) => { + return GuideLayout(props) } export default Layout diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx new file mode 100644 index 00000000000..db490d4c9e7 --- /dev/null +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -0,0 +1,54 @@ +import { MDXProvider } from '@mdx-js/react' +import Head from 'next/head' +import { FC } from 'react' +import components from '~/components' +import TableOfContents from '~/components/TableOfContents' + +interface Props { + meta: { title: string; description?: string; hide_table_of_contents?: boolean; video?: string } + children: any + toc?: any + menuItems: any +} + +const Layout: FC = (props: Props) => { + const hasTableOfContents = + props.toc !== undefined && + props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 + + return ( + <> + + {props.meta?.title} | Supabase + + + + + + + + + +
                  + {/*

                  Tutorials

                  */} +
                  +

                  {props.meta.title}

                  + {/*
                  */} + +
                  +
                  + {hasTableOfContents && !props.meta?.hide_table_of_contents && ( + + )} + + ) +} + +export default Layout diff --git a/apps/docs/layouts/SiteLayout.tsx b/apps/docs/layouts/SiteLayout.tsx new file mode 100644 index 00000000000..9feac530869 --- /dev/null +++ b/apps/docs/layouts/SiteLayout.tsx @@ -0,0 +1,320 @@ +import { useTheme } from 'common/Providers' +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' +import NavigationMenu from '~/components/Navigation/NavigationMenu/NavigationMenu' +import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef' + +import FooterHelpCallout from '~/components/FooterHelpCallout' + +import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState' +import { useEffect } from 'react' + +const SiteRefLayout = ({ children }) => { + const { isDarkMode } = useTheme() + + useEffect(() => { + const key = localStorage.getItem('supabaseDarkMode') + if (!key) { + // Default to dark mode if no preference config + document.documentElement.className = 'dark' + } else { + document.documentElement.className = key === 'true' ? 'dark' : '' + } + }, []) + + const menuLevel = useMenuLevelId() + const mobileMenuOpen = useMenuMobileOpen() + + const levelsData = { + home: { + icon: '/docs/img/icons/menu/home', + name: 'Home', + }, + gettingstarted: { + icon: '/docs/img/icons/menu/getting-started', + name: 'Getting Started', + }, + database: { + icon: '/docs/img/icons/menu/database', + name: 'Database', + }, + auth: { + icon: '/docs/img/icons/menu/auth', + name: 'Auth', + }, + functions: { + icon: '/docs/img/icons/menu/functions', + name: 'Functions', + }, + realtime: { + icon: '/docs/img/icons/menu/realtime', + name: 'Realtime', + }, + storage: { + icon: '/docs/img/icons/menu/storage', + name: 'Storage', + }, + platform: { + icon: '/docs/img/icons/menu/platform', + name: 'Platform', + }, + resources: { + icon: '/docs/img/icons/menu/resources', + name: 'Resources', + }, + integrations: { + icon: '/docs/img/icons/menu/integrations', + name: 'Integrations', + }, + reference_javascript_v1: { + icon: '/docs/img/icons/menu/reference-javascript', + name: 'Javascript Reference v1.0', + }, + reference_javascript_v2: { + icon: '/docs/img/icons/menu/reference-javascript', + name: 'Javascript Reference v2.0', + }, + reference_dart_v0: { + icon: '/docs/img/icons/menu/reference-dart', + name: 'Dart Reference v0.0', + }, + reference_dart_v1: { + icon: '/docs/img/icons/menu/reference-dart', + name: 'Dart Reference v0.0', + }, + reference_cli: { + icon: '/docs/img/icons/menu/reference-cli', + name: 'CLI Reference', + }, + reference_api: { + icon: '/docs/img/icons/menu/reference-api', + name: 'Management API Reference', + }, + reference_self_hosting_auth: { + icon: '/docs/img/icons/menu/reference-auth', + name: 'Auth Server Reference', + }, + reference_self_hosting_storage: { + icon: '/docs/img/icons/menu/reference-storage', + name: 'Storage Server Reference', + }, + reference_self_hosting_realtime: { + icon: '/docs/img/icons/menu/reference-realtime', + name: 'Realtime Server Reference', + }, + } + + return ( +
                  + {/* */} + +
                  + +
                  +
                  + {/* */} +
                  + +
                  +
                  +
                  +
                  + +
                  + +
                  + + {mobileMenuOpen + ? 'Close' + : menuLevel + ? levelsData[menuLevel].name + : levelsData['home'].name} + +
                  +
                  + +
                  +
                  +
                  +
                  + {children} + +
                  + +
                  +
                  +
                  + Supabase 2022 + — + + Contributing + + + Changelog + + + + Author Styleguide + + + Open Source + + + SupaSquad + +
                  +
                  + + +
                  +
                  +
                  +
                  + ) +} + +export default SiteRefLayout diff --git a/apps/docs/layouts/SiteRefLayout.tsx b/apps/docs/layouts/SiteRefLayout.tsx new file mode 100644 index 00000000000..930daa4d684 --- /dev/null +++ b/apps/docs/layouts/SiteRefLayout.tsx @@ -0,0 +1,321 @@ +import { useTheme } from 'common/Providers' +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' +import NavigationMenu from '~/components/Navigation/NavigationMenu/NavigationMenu' +import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef' + +import FooterHelpCallout from '~/components/FooterHelpCallout' + +import { menuState } from '~/hooks/useMenuState' +import { useEffect } from 'react' + +const SiteRefLayout = ({ children }) => { + const { isDarkMode } = useTheme() + + useEffect(() => { + const key = localStorage.getItem('supabaseDarkMode') + if (!key) { + // Default to dark mode if no preference config + document.documentElement.className = 'dark' + } else { + document.documentElement.className = key === 'true' ? 'dark' : '' + } + }, []) + + const menuLevel = 'home' + // useMenuLevelId() + const mobileMenuOpen = false + + const levelsData = { + home: { + icon: '/docs/img/icons/menu/home', + name: 'Home', + }, + gettingstarted: { + icon: '/docs/img/icons/menu/getting-started', + name: 'Getting Started', + }, + database: { + icon: '/docs/img/icons/menu/database', + name: 'Database', + }, + auth: { + icon: '/docs/img/icons/menu/auth', + name: 'Auth', + }, + functions: { + icon: '/docs/img/icons/menu/functions', + name: 'Functions', + }, + realtime: { + icon: '/docs/img/icons/menu/realtime', + name: 'Realtime', + }, + storage: { + icon: '/docs/img/icons/menu/storage', + name: 'Storage', + }, + platform: { + icon: '/docs/img/icons/menu/platform', + name: 'Platform', + }, + resources: { + icon: '/docs/img/icons/menu/resources', + name: 'Resources', + }, + integrations: { + icon: '/docs/img/icons/menu/integrations', + name: 'Integrations', + }, + reference_javascript_v1: { + icon: '/docs/img/icons/menu/reference-javascript', + name: 'Javascript Reference v1.0', + }, + reference_javascript_v2: { + icon: '/docs/img/icons/menu/reference-javascript', + name: 'Javascript Reference v2.0', + }, + reference_dart_v0: { + icon: '/docs/img/icons/menu/reference-dart', + name: 'Dart Reference v0.0', + }, + reference_dart_v1: { + icon: '/docs/img/icons/menu/reference-dart', + name: 'Dart Reference v0.0', + }, + reference_cli: { + icon: '/docs/img/icons/menu/reference-cli', + name: 'CLI Reference', + }, + reference_api: { + icon: '/docs/img/icons/menu/reference-api', + name: 'Management API Reference', + }, + reference_self_hosting_auth: { + icon: '/docs/img/icons/menu/reference-auth', + name: 'Auth Server Reference', + }, + reference_self_hosting_storage: { + icon: '/docs/img/icons/menu/reference-storage', + name: 'Storage Server Reference', + }, + reference_self_hosting_realtime: { + icon: '/docs/img/icons/menu/reference-realtime', + name: 'Realtime Server Reference', + }, + } + + return ( +
                  + {/* */} + +
                  + +
                  +
                  + {/* */} +
                  + +
                  +
                  +
                  +
                  + +
                  + +
                  + + {mobileMenuOpen + ? 'Close' + : menuLevel + ? levelsData[menuLevel].name + : levelsData['home'].name} + +
                  +
                  + +
                  +
                  +
                  +
                  + {children} + +
                  + +
                  +
                  +
                  + Supabase 2022 + — + + Contributing + + + Changelog + + + + Author Styleguide + + + Open Source + + + SupaSquad + +
                  +
                  + + +
                  +
                  +
                  +
                  + ) +} + +export default SiteRefLayout diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx new file mode 100644 index 00000000000..10ccfed870d --- /dev/null +++ b/apps/docs/layouts/guides/index.tsx @@ -0,0 +1,140 @@ +import { MDXProvider } from '@mdx-js/react' +import { NextSeo } from 'next-seo' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { FC, useEffect, useRef, useState } from 'react' +import { IconExternalLink } from 'ui' +import components from '~/components' +import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' +import GuidesTableOfContents from '~/components/GuidesTableOfContents' +import useHash from '~/hooks/useHash' +import { getPageType } from '~/lib/helpers' + +interface Props { + meta: { + title: string + description?: string + hide_table_of_contents?: boolean + breadcrumb?: string + subtitle?: string + } + children: any + toc?: any + currentPage?: string + hideToc?: boolean +} + +const Layout: FC = (props) => { + const [hash] = useHash() + + const articleRef = useRef() + const [tocList, setTocList] = useState([]) + + const { asPath } = useRouter() + const page = getPageType(asPath) + + const router = useRouter() + + useEffect(() => { + if (hash && tocList.length > 0) { + highlightSelectedTocItem(hash as string) + } + }, [hash, JSON.stringify(tocList)]) + + useEffect(() => { + const articleEl = articleRef.current as HTMLElement + + if (!articleRef.current) return + const headings = Array.from(articleEl.querySelectorAll('h2, h3')) + const newHeadings = headings + .filter((heading) => heading.id) + .map((heading) => { + const text = heading.textContent.replace('#', '') + const link = heading.querySelector('a').getAttribute('href') + const level = heading.tagName === 'H2' ? 2 : 3 + return { text, link, level } + }) + setTocList(newHeadings) + }, []) + + const hasTableOfContents = tocList.length > 0 + + return ( + <> + + +
                  +
                  + {props.meta.breadcrumb && ( +

                  {props.meta.breadcrumb}

                  + )} + +
                  + {!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && ( + + )} +
                  + + ) +} + +export default Layout diff --git a/apps/docs/layouts/ref/RefSubLayout.tsx b/apps/docs/layouts/ref/RefSubLayout.tsx new file mode 100644 index 00000000000..26e1c612515 --- /dev/null +++ b/apps/docs/layouts/ref/RefSubLayout.tsx @@ -0,0 +1,162 @@ +import { useInView } from 'react-intersection-observer' +import { FC } from 'react' +import { highlightSelectedNavItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' +import { useRouter } from 'next/router' +import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context' +import { menuState } from '~/hooks/useMenuState' + +interface ISectionContainer { + id: string + title?: string + monoFont?: boolean + slug: string + scrollSpyHeader?: boolean + singleColumn?: boolean + icon?: string +} + +type RefSubLayoutSubComponents = { + Section: FC + EducationSection: FC + EducationRow: FC + Details: FC + Examples: FC +} + +type StickyHeader = { + id: string + slug?: string + title?: string + monoFont?: boolean + scrollSpyHeader?: boolean // whether or not the header updates the url on scroll + icon?: string +} + +type RefSubLayoutType = {} + +interface IEducationRow { + className?: string +} +interface IEducationSection { + id: string + title?: string + monoFont?: boolean + slug: string + scrollSpyHeader?: boolean + hideTitle?: boolean + icon?: string +} +interface ISectionDetails {} +interface ISectionExamples {} + +const RefSubLayout: FC & RefSubLayoutSubComponents = (props) => { + return
                  {props.children}
                  +} + +const Section: FC = (props) => { + // console.log({ props }) + return ( +
                  + +
                  + {props.children} +
                  +
                  + ) +} + +const StickyHeader: FC = ({ icon, ...props }) => { + const router = useRouter() + const { setActiveRefItem } = useNavigationMenuContext() + + const { ref } = useInView({ + threshold: 1, + rootMargin: '30% 0% -35% 0px', + onChange: (inView, entry) => { + if (inView && window) highlightSelectedNavItem(entry.target.attributes['data-ref-id'].value) + if (inView && props.scrollSpyHeader) { + window.history.replaceState(null, '', entry.target.id) + // if (setActiveRefItem) setActiveRefItem(entry.target.attributes['data-ref-id'].value) + menuState.setMenuActiveRefId(entry.target.attributes['data-ref-id'].value) + // router.push(`/reference/javascript/${entry.target.attributes['data-ref-id'].value}`, null, { + // shallow: true, + // }) + } + }, + }) + + return ( +
                  + {icon && ( +
                  + +
                  + )} +

                  + {props.title && {props.title}} +

                  +
                  + ) +} + +const Details: FC = (props) => { + return
                  {props.children}
                  +} + +const Examples: FC = (props) => { + return ( +
                  +
                  {props.children}
                  +
                  + ) +} + +const EducationRow: FC = (props) => { + return ( +
                  + {props.children} +
                  + ) +} + +const EducationSection: FC = ({ icon, hideTitle = false, ...props }) => { + // console.log({ props }) + return ( +
                  + {!hideTitle && } + {props.children} +
                  + ) +} + +// function based layout +RefSubLayout.Section = Section +// education based layout +RefSubLayout.EducationSection = EducationSection +RefSubLayout.EducationRow = EducationRow +// common columns +RefSubLayout.Details = Details +RefSubLayout.Examples = Examples +export default RefSubLayout diff --git a/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx b/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx new file mode 100644 index 00000000000..d21aa1d79a4 --- /dev/null +++ b/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx @@ -0,0 +1,114 @@ +import { useInView } from 'react-intersection-observer' +import { FC } from 'react' +import { highlightSelectedNavItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' +import { useRouter } from 'next/router' +import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context' +import { menuState } from '~/hooks/useMenuState' + +interface ISectionContainer { + id: string + title?: string + monoFont?: boolean + slug: string + scrollSpyHeader?: boolean + singleColumn?: boolean +} + +type RefSubLayoutNonFuncSubComponents = { + Section: FC + Details: FC + Examples: FC +} + +type StickyHeader = { + id: string + slug?: string + title?: string + monoFont?: boolean + scrollSpyHeader?: boolean // whether or not the header updates the url on scroll +} + +type RefSubLayoutNonFuncType = {} + +const RefSubLayoutNonFunc: FC & RefSubLayoutNonFuncSubComponents = ( + props +) => { + return
                  {props.children}
                  +} + +const Section: FC = (props) => { + // console.log({ props }) + return ( +
                  + +
                  + {props.children} +
                  +
                  + ) +} + +const StickyHeader: FC = (props) => { + const router = useRouter() + const { setActiveRefItem } = useNavigationMenuContext() + + const { ref } = useInView({ + threshold: 1, + rootMargin: '30% 0% -35% 0px', + onChange: (inView, entry) => { + if (inView && window) highlightSelectedNavItem(entry.target.attributes['data-ref-id'].value) + if (inView && props.scrollSpyHeader) { + window.history.replaceState(null, '', entry.target.id) + // if (setActiveRefItem) setActiveRefItem(entry.target.attributes['data-ref-id'].value) + menuState.setMenuActiveRefId(entry.target.attributes['data-ref-id'].value) + // router.push(`/reference/javascript/${entry.target.attributes['data-ref-id'].value}`, null, { + // shallow: true, + // }) + } + }, + }) + + return ( +

                  + {props.title && {props.title}} +

                  + ) +} + +interface ISectionDetails {} + +const Details: FC = (props) => { + return
                  {props.children}
                  +} + +interface ISectionExamples {} + +const Examples: FC = (props) => { + return ( +
                  +
                  {props.children}
                  +
                  + ) +} + +RefSubLayoutNonFunc.Section = Section +RefSubLayoutNonFunc.Details = Details +RefSubLayoutNonFunc.Examples = Examples +export default RefSubLayoutNonFunc diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx new file mode 100644 index 00000000000..b49391425b2 --- /dev/null +++ b/apps/docs/layouts/tutorials/TutorialLayout.tsx @@ -0,0 +1,93 @@ +import { MDXProvider } from '@mdx-js/react' +import Head from 'next/head' +import { FC, useEffect, useState } from 'react' +import components from '~/components' +import Footer from '~/components/Footer' +import SideBar from '~/components/Navigation/SideBar' +import TableOfContents from '~/components/TableOfContents' + +interface Props { + meta: { title: string; description?: string; hide_table_of_contents?: boolean; video?: string } + children: any + toc?: any + menuItems: any + currentPage: string +} + +const Layout: FC = (props: Props) => { + // const contentString = renderToString(props.children) + const [active, setActive] = useState(false) + + useEffect(() => { + const key = localStorage.getItem('supabaseDarkMode') + if (!key) { + // Default to dark mode if no preference config + document.documentElement.className = 'dark' + } else { + document.documentElement.className = key === 'true' ? 'dark' : '' + } + }, []) + + useEffect(() => { + setTimeout(function () { + setActive(true) + }, 150) + }, []) + + // const contentString = renderToString(props.children) + + // const content = serialize(contentString || '') + + // console.log('contentString', contentString) + + // const _toc = toc('#hello world', { maxdepth: 1, firsth1: false }) + + const hasTableOfContents = + props.toc !== undefined && + props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 + + return ( + <> + + {props.meta?.title} | Supabase + + + + + + + + + +
                  +
                  +

                  Tutorials

                  +
                  +

                  {props.meta.title}

                  +
                  + + +
                  +
                  + {hasTableOfContents && !props.meta?.hide_table_of_contents && ( + + )} +
                  + + ) +} + +export default Layout diff --git a/apps/docs/lib/helpers.ts b/apps/docs/lib/helpers.ts index b5df2c0d5c0..899aea15618 100644 --- a/apps/docs/lib/helpers.ts +++ b/apps/docs/lib/helpers.ts @@ -32,3 +32,19 @@ export function getPageType(asPath: string) { return page } + +export function flattenSections(sections) { + var a = [] + for (var i = 0; i < sections.length; i++) { + if (sections[i].id) { + // only push a section that has an id + // these are reserved for sidebar subtitles + a.push(sections[i]) + } + if (sections[i].items) { + // if there are subitems, loop through + a = a.concat(flattenSections(sections[i].items)) + } + } + return a +} diff --git a/apps/docs/lib/mdx/generateOldRefMarkdown.tsx b/apps/docs/lib/mdx/generateOldRefMarkdown.tsx new file mode 100644 index 00000000000..2358fd5c698 --- /dev/null +++ b/apps/docs/lib/mdx/generateOldRefMarkdown.tsx @@ -0,0 +1,26 @@ +import { serialize } from 'next-mdx-remote/serialize' + +import toc from 'markdown-toc' + +import { getDocsBySlug } from '~/lib/docs' + +async function generateOldRefMarkdown(slug) { + let doc = getDocsBySlug(slug) + const content = await serialize(doc.content ?? '', { + // MDX's available options, see the MDX docs for more info. + // https://mdxjs.com/packages/mdx/#compilefile-options + // Indicates whether or not to parse the frontmatter from the mdx source + }) + return { + props: { + /* + * old reference docs are below + */ + ...doc, + content, + toc: toc(doc.content, { maxdepth: 1, firsth1: false }), + }, + } +} + +export default generateOldRefMarkdown diff --git a/apps/docs/lib/mdx/generateRefMarkdown.tsx b/apps/docs/lib/mdx/generateRefMarkdown.tsx new file mode 100644 index 00000000000..1d5f7a539e9 --- /dev/null +++ b/apps/docs/lib/mdx/generateRefMarkdown.tsx @@ -0,0 +1,62 @@ +import fs from 'fs' + +import matter from 'gray-matter' +import { serialize } from 'next-mdx-remote/serialize' + +// import { remarkCodeHike } from '@code-hike/mdx' +// import codeHikeTheme from '~/codeHikeTheme.js' +// import theme from 'shiki/themes/solarized-dark.json' + +async function generateRefMarkdown(sections, slug) { + let markdownContent = [] + /** + * Read all the markdown files that might have + * - custom text + * - call outs + * - important notes regarding implementation + */ + await Promise.all( + sections.map(async (x, i) => { + if (!x.id) return null + + const pathName = `docs/ref${slug}/${x.id}.mdx` + + function checkFileExists(x) { + if (fs.existsSync(x)) { + return true + } else { + return false + } + } + + const markdownExists = checkFileExists(pathName) + + if (!markdownExists) return null + + const fileContents = markdownExists ? fs.readFileSync(pathName, 'utf8') : '' + const { data, content } = matter(fileContents) + + markdownContent.push({ + id: x.id, + title: x.title, + meta: data, + // introPage: introPages.includes(x), + content: content + ? await serialize(content ?? '', { + // MDX's available options, see the MDX docs for more info. + // https://mdxjs.com/packages/mdx/#compilefile-options + mdxOptions: { + // remarkPlugins: [[remarkCodeHike, { autoImport: false, theme }]], + useDynamicImport: true, + }, + // Indicates whether or not to parse the frontmatter from the mdx source + }) + : null, + }) + }) + ) + + return markdownContent +} + +export default generateRefMarkdown diff --git a/apps/docs/lib/mdx/handleRefStaticPaths.tsx b/apps/docs/lib/mdx/handleRefStaticPaths.tsx new file mode 100644 index 00000000000..c1f13e4a8c4 --- /dev/null +++ b/apps/docs/lib/mdx/handleRefStaticPaths.tsx @@ -0,0 +1,18 @@ +import { getAllDocs } from '../docs' + +async function handleRefGetStaticPaths() { + let docs = getAllDocs() + + return { + paths: docs.map(() => { + return { + params: { + slug: docs.map((d) => d.slug), + }, + } + }), + fallback: 'blocking', + } +} + +export default handleRefGetStaticPaths diff --git a/apps/docs/lib/mdx/handleRefStaticProps.tsx b/apps/docs/lib/mdx/handleRefStaticProps.tsx new file mode 100644 index 00000000000..c9c47b4496d --- /dev/null +++ b/apps/docs/lib/mdx/handleRefStaticProps.tsx @@ -0,0 +1,34 @@ +import generateOldRefMarkdown from '~/lib/mdx/generateOldRefMarkdown' +import generateRefMarkdown from '~/lib/mdx/generateRefMarkdown' + +async function handleRefStaticProps(sections, params, librarypath, urlPath) { + let markdownContent = await generateRefMarkdown(sections, librarypath) + + /* + * old content generation + * this is for grabbing to old markdown files + */ + + let slug + if (params.slug.length > 1) { + slug = `docs/reference${urlPath}/${params.slug.join('/')}` + } else { + slug = `docs/reference${urlPath}/${params.slug[0]}` + } + + /* + * handle old ref pages + */ + if (process.env.NEXT_PUBLIC_NEW_DOCS === 'false') { + const oldMarkdown = await generateOldRefMarkdown(slug) + return oldMarkdown + } else { + return { + props: { + docs: markdownContent, + }, + } + } +} + +export default handleRefStaticProps diff --git a/apps/docs/lib/refGenerator/helpers.ts b/apps/docs/lib/refGenerator/helpers.ts new file mode 100644 index 00000000000..77a7aa16755 --- /dev/null +++ b/apps/docs/lib/refGenerator/helpers.ts @@ -0,0 +1,285 @@ +import { TsDoc } from '../../generator/legacy/definitions' + +import { values, mapValues } from 'lodash' +import { OpenAPIV3 } from 'openapi-types' + +export function extractTsDocNode(nodeToFind: string, definition: any) { + const nodePath = nodeToFind.split('.') + let i = 0 + let previousNode = definition + let currentNode = definition + while (i < nodePath.length) { + previousNode = currentNode + currentNode = previousNode.children.find((x) => x.name == nodePath[i]) || null + if (currentNode == null) { + console.log(`Cant find ${nodePath[i]} in ${previousNode.children.map((x) => '\n' + x.name)}`) + break + } + i++ + } + + return currentNode +} + +export function generateParameters(tsDefinition: any) { + let functionDeclaration = null + if (tsDefinition.kindString == 'Method') { + functionDeclaration = tsDefinition + } else if (tsDefinition.kindString == 'Constructor') { + functionDeclaration = tsDefinition + } else functionDeclaration = tsDefinition?.type?.declaration + if (!functionDeclaration) return '' + + // Functions can have multiple signatures - select the last one since that + // tends to be closer to primitive types (citation needed). + const paramDefinitions: TsDoc.TypeDefinition[] = functionDeclaration.signatures.at(-1).parameters + if (!paramDefinitions) return '' + + // const paramsComments: TsDoc.CommentTag = tsDefinition.comment?.tags?.filter(x => x.tag == 'param') + let parameters = paramDefinitions.map((x) => recurseThroughParams(x)) // old join // .join(`\n`) + return parameters +} + +function recurseThroughParams(paramDefinition: any) { + const param = { ...paramDefinition } + const labelParams = generateLabelParam(param) + + let children: any[] + if (param.type?.type === 'literal') { + // skip: literal types have no children + } else if (param.type?.type === 'intrinsic') { + // primitive types + if (!['string', 'number', 'boolean', 'object', 'unknown'].includes(param.type?.name)) { + // skip for now + //throw new Error('unexpected intrinsic type') + } + } else if (param.type?.dereferenced) { + const dereferenced = param.type.dereferenced + + if (dereferenced.children) { + children = dereferenced.children + } else if (dereferenced.type?.declaration?.children) { + children = dereferenced.type.declaration.children + } else if (dereferenced.type?.type === 'union') { + // skip: we don't want to show unions as nested parameters + } else if (Object.keys(dereferenced).length === 0) { + // skip: {} have no children + } else { + throw new Error('unexpected case for dereferenced param type') + } + } else if (param.type?.type === 'reflection') { + const declaration = param.type.declaration + + if (!declaration) { + throw new Error('reflection must have a declaration') + } + + if (declaration.children) { + children = declaration.children + } else if (declaration.signatures) { + // skip: functions have no children + } else if (declaration.name === '__type') { + // skip: mostly inlined object type + } else { + throw new Error('unexpected case for reflection param type') + } + } else if (param.type?.type === 'indexedAccess') { + // skip: too complex, e.g. PromisifyMethods> + } else if (param.type?.type === 'reference') { + // skip: mostly unexported types + } else if (param.type?.type === 'union') { + // skip: we don't want to show unions as nested parameters + } else if (param.type?.type === 'array') { + // skip: no use for it for now + } else { + // skip: no use for now + //throw new Error(`unexpected param type`) + } + + if (children) { + const properties = children + .sort((a, b) => a.name?.localeCompare(b.name)) // first alphabetical + .sort((a, b) => (a.flags?.isOptional ? 1 : -1)) // required params first + .map((x) => recurseThroughParams(x)) + labelParams.subContent = properties + } + return labelParams +} + +// const isDereferenced = (paramDefinition: TsDoc.TypeDefinition) => { +// // @ts-ignore +// return paramDefinition.type?.type == 'reference' && paramDefinition.type?.dereferenced?.id +// } + +function generateLabelParam(param: any) { + let labelParams: any = {} + if (param.type?.type === 'intrinsic' && param.type?.name === 'unknown') { + labelParams = { + name: param.name ?? param.value, + isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param, + type: 'any', + description: param.comment ? tsDocCommentToMdComment(param.comment) : null, + } + } else if (param.type?.declaration?.signatures) { + labelParams = { + name: param.name ?? param.value, + isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param, + type: 'function', + description: param.comment ? tsDocCommentToMdComment(param.comment) : null, + } + } else if (param.type?.type === 'literal') { + labelParams = { + name: param.name ?? param.value, + isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param, + type: typeof param.type.value === 'string' ? `"${param.type.value}"` : `${param.type.value}`, + description: param.comment ? tsDocCommentToMdComment(param.comment) : null, + } + } else { + labelParams = { + name: param.name ?? extractParamTypeAsString(param), + isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param, + type: extractParamTypeAsString(param), + description: param.comment ? tsDocCommentToMdComment(param.comment) : null, + } + } + return labelParams +} + +function extractParamTypeAsString(paramDefinition) { + if (paramDefinition.type?.name) { + // return `${paramDefinition.type.name}` // old + return paramDefinition.type.name + } else if (paramDefinition.type?.type === 'union') { + // only do this for literal/primitive types - for complex objects we just return 'object' + if (paramDefinition.type.types.every(({ type }) => ['literal', 'intrinsic'].includes(type))) { + return paramDefinition.type.types + .map((x) => { + if (x.type === 'literal') { + if (typeof x.value === 'string') { + return `"${x.value}"` + } + return `${x.value}` + } else if (x.type === 'intrinsic') { + if (x.name === 'unknown') { + return 'any' + } + return x.name + } + }) + .join(' | ') + } + } else if (paramDefinition.type?.type === 'array') { + const elementType = paramDefinition.type.elementType + + if (elementType.type === 'intrinsic') { + if (elementType.name === 'unknown') { + return 'any[]' + } + return `${elementType.name}[]` + } + + return 'object[]' + } + + return 'object' // old 'object' +} + +const tsDocCommentToMdComment = (commentObject: TsDoc.DocComment) => + ` +${commentObject?.shortText || ''} + +${commentObject?.text || ''} + +`.trim() + +// function generateExamples(id: string, specExamples: any, allLanguages: any) { +// return specExamples.map((example) => { +// let allTabs = example.hideCodeBlock ? '' : generateCodeBlocks(allLanguages, example) +// return Example({ +// name: example.name, +// description: example.description, +// tabs: allTabs, +// note: example.note, +// }) +// }) +// } + +// OPENAPI-SPEC-VERSION: 3.0.0 +type v3OperationWithPath = OpenAPIV3.OperationObject & { + path: string +} + +export type enrichedOperation = OpenAPIV3.OperationObject & { + path: string + fullPath: string + operationId: string + operation: string + responseList: [] +} + +export function gen_v3(spec: OpenAPIV3.Document, dest: string, { apiUrl }: { apiUrl: string }) { + const specLayout = spec.tags || [] + const operations: enrichedOperation[] = [] + + Object.entries(spec.paths).forEach(([key, val]) => { + const fullPath = `${apiUrl}${key}` + + toArrayWithKey(val!, 'operation').forEach((o) => { + const operation = o as v3OperationWithPath + const enriched = { + ...operation, + path: key, + fullPath, + operationId: slugify(operation.summary!), + + responseList: toArrayWithKey(operation.responses!, 'responseCode') || [], + } + // @ts-expect-error // missing 'responses', see OpenAPIV3.OperationObject.responses + operations.push(enriched) + }) + }) + + const sections = specLayout.map((section) => { + return { + ...section, + title: toTitle(section.name), + id: slugify(section.name), + operations: operations.filter((operation) => operation.tags?.includes(section.name)), + } + }) + + const content = { + info: spec.info, + sections, + operations, + } + + return content +} + +const slugify = (text: string) => { + return text + .toString() + .toLowerCase() + .replace(/[. )(]/g, '-') // Replace spaces and brackets - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, '') // Trim - from end of text +} + +// Uppercase the first letter of a string +const toTitle = (text: string) => { + return text.charAt(0).toUpperCase() + text.slice(1) +} + +/** + * Convert Object to Array of values + */ +export const toArrayWithKey = (obj: object, keyAs: string) => + values( + mapValues(obj, (value: any, key: string) => { + value[keyAs] = key + return value + }) + ) diff --git a/apps/docs/lib/refGenerator/refTypes.ts b/apps/docs/lib/refGenerator/refTypes.ts new file mode 100644 index 00000000000..c3563de9f01 --- /dev/null +++ b/apps/docs/lib/refGenerator/refTypes.ts @@ -0,0 +1,17 @@ +export type Link = { + name: string + link: string +} + +export type Parameter = { + id: string + title: string + description: string + summary: string + tags?: string[] + links?: Link[] + subcommands?: [] + usage?: string + required?: boolean + default?: boolean +} diff --git a/apps/docs/middleware.ts b/apps/docs/middleware.ts new file mode 100644 index 00000000000..d13ce4a442f --- /dev/null +++ b/apps/docs/middleware.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import isbot from 'isbot' + +export function middleware(request: NextRequest) { + const specs = ['javascript', 'dart'] + + let version = '' + if (request.url.includes('/v1/')) { + version = 'v1' + } + if (request.url.includes('/v0/')) { + version = 'v0' + } + + if (isbot(request.headers.get('user-agent'))) { + for (const lib of specs) { + if (request.url.includes(`reference/${lib}`)) { + const requestSlug = request.url.split('/').pop() + + return NextResponse.rewrite( + new URL( + `/docs/reference/${lib}/${version ? version + '/' : ''}crawlers/${requestSlug}`, + request.url + ).toString() + ) + } + } + } else { + return NextResponse.next() + } +} + +export const config = { + matcher: '/((?!api|_next|static|public|favicon.ico).*)', +} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 151a73426e6..9762c179622 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -2,12 +2,37 @@ import nextMdx from '@next/mdx' import remarkGfm from 'remark-gfm' import rehypeSlug from 'rehype-slug' +//import theme from 'shiki/themes/nord.json' assert { type: 'json' } + import withTM from 'next-transpile-modules' +import withYaml from 'next-plugin-yaml' +// import admonitions from 'remark-admonitions' + +// import { remarkCodeHike } from '@code-hike/mdx' +// import codeHikeTheme from './codeHikeTheme.js' + +/** + * Rewrites and redirects are handled by + * apps/www nextjs config + * + * Do not add them in this config + */ const withMDX = nextMdx({ extension: /\.mdx?$/, options: { - remarkPlugins: [remarkGfm], + remarkPlugins: [ + // [ + // remarkCodeHike, + // { + // theme: codeHikeTheme, + // autoImport: false, + // lineNumbers: true, + // showCopyButton: true, + // }, + // ], + remarkGfm, + ], rehypePlugins: [rehypeSlug], // This is required for `MDXProvider` component // providerImportSource: '@mdx-js/react', @@ -18,17 +43,20 @@ const withMDX = nextMdx({ const nextConfig = { // Append the default value with md extensions pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], - reactStrictMode: true, - swcMinify: true, + // reactStrictMode: true, + // swcMinify: true, basePath: '/docs', images: { dangerouslyAllowSVG: true, domains: ['avatars.githubusercontent.com', 'github.com', 'user-images.githubusercontent.com'], }, + experimental: { + mdxRs: true, + }, async headers() { return [ { - source: '/(.*)', + source: '/:path*', headers: [ { key: 'Strict-Transport-Security', @@ -50,6 +78,7 @@ const nextConfig = { // next.config.js export default () => { - const plugins = [withMDX, withTM(['ui', 'common'])] + // const plugins = [withMDX]/ + const plugins = [withTM(['ui', 'common']), withMDX, withYaml] return plugins.reduce((acc, next) => next(acc), nextConfig) } diff --git a/apps/docs/package.json b/apps/docs/package.json index 7a6df1f501a..2abc71adb53 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,12 +8,12 @@ "start": "next start", "lint": "next lint", "build:sitemap": "node ./internals/generate-sitemap.mjs", - "postbuild": "node ./scripts/build-search.js && node ./internals/generate-sitemap.mjs", + "postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs", "generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime", "gen:api": "npm-run-all gen:api:usage", "gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx", "gen:cli": "npm-run-all gen:cli:commands gen:cli:config", - "gen:cli:commands": "ts-node ./generator/index.ts gen --type cli --input ../../spec/cli_v1_commands.yaml --output ./docs/reference/cli/generated/usage.mdx ", + "gen:cli:commands": "ts-node ./generator/index.ts gen --type cli --input ../../spec/cli_v1_commands--old.yaml --output ./docs/reference/cli/generated/usage.mdx ", "gen:cli:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/cli_v1_config.yaml --output ./docs/reference/cli/generated/config.mdx", "gen:gotrue": "npm-run-all gen:gotrue:config gen:gotrue:usage", "gen:gotrue:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/gotrue_v1_config.yaml --output ./docs/reference/auth/generated/config.mdx", @@ -35,31 +35,28 @@ "dependencies": { "@algolia/autocomplete-js": "^1.7.2", "@algolia/autocomplete-plugin-recent-searches": "^1.7.2", - "@apollo/client": "^3.6.6", "@docsearch/react": "^3.3.0", "@mdx-js/loader": "^1.6.22", "@mdx-js/react": "^1.6.22", "@next/mdx": "^12.0.4", "@radix-ui/react-accordion": "^1.0.1", - "@tailwindcss/typography": "^0.4.1", "algoliasearch": "^4.14.2", "babel": "^6.23.0", "common": "*", "config": "*", - "dotenv": "^16.0.3", - "graphql": "^16.5.0", "gray-matter": "^4.0.3", + "isbot": "^3.6.5", "jsrsasign": "^10.5.26", "lodash": "^4.17.21", "markdown-toc": "^1.2.0", "mdx-mermaid": "2.0.0-rc3", - "next": "12.1.0", + "next": "12.3.2", "next-compose-plugins": "^2.2.1", - "next-mdx-remote": "^3.0.8", + "next-mdx-remote": "^4.1.0", + "next-mdx-toc": "^0.1.3", + "next-plugin-yaml": "^1.0.1", "next-seo": "^5.14.1", - "next-themes": "0.0.15", "next-transpile-modules": "^9.0.0", - "octokit": "^2.0.10", "react": "17.0.2", "react-copy-to-clipboard": "^5.0.2", "react-dom": "17.0.2", @@ -68,12 +65,14 @@ "react-syntax-highlighter": "^15.3.1", "remark": "^14.0.2", "remark-admonitions": "^1.2.1", - "remark-html": "^15.0.0", - "remark-mdx": "^2.0.0-rc.2", - "remark-prism": "^1.3.6", - "ui": "*" + "remark-gfm": "^3.0.1", + "remark-slug": "^7.0.1", + "shiki": "^0.11.1", + "ui": "*", + "valtio": "^1.7.6" }, "devDependencies": { + "dotenv": "^16.0.3", "@types/node": "^17.0.12", "@types/react": "17.0.39", "config": "*", diff --git a/apps/docs/pages/404.mdx b/apps/docs/pages/404.mdx index 92807aeb5af..b75a4a59620 100644 --- a/apps/docs/pages/404.mdx +++ b/apps/docs/pages/404.mdx @@ -1,6 +1,13 @@ +import Layout from '~/layouts/DefaultGuideLayout' + export const meta = { - title: '404', - description: 'Page not found', + id: '404', + title: '404 not found', + description: '404 not found', } -# Page not found +404 not found + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/404.tsx b/apps/docs/pages/404.tsx new file mode 100644 index 00000000000..71b9bb5f6f5 --- /dev/null +++ b/apps/docs/pages/404.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react' +import Link from 'next/link' +import { Button } from 'ui' +import Head from 'next/head' + +export default function Custom404() { + const [show404, setShow404] = useState(false) + + useEffect(() => { + setTimeout(() => { + setShow404(true) + }, 500) + }, []) + + return ( + <> + + 404 | Supabase + +
                  +
                  +

                  + 404 +

                  +
                  +
                  +
                  +

                  Looking for something?

                  +

                  + We could not find the page that you are looking for! +

                  +
                  +
                  + + + +
                  +
                  +
                  + + ) +} diff --git a/apps/docs/pages/[...slug].tsx b/apps/docs/pages/[...slug].tsx index bb0e2a0d943..6b34513a155 100644 --- a/apps/docs/pages/[...slug].tsx +++ b/apps/docs/pages/[...slug].tsx @@ -1,13 +1,11 @@ import toc from 'markdown-toc' -import { MDXProvider } from '@mdx-js/react' -import { useRouter } from 'next/router' import { MDXRemote } from 'next-mdx-remote' import { serialize } from 'next-mdx-remote/serialize' import components from '../components/index' -import { menuItems } from '../components/Navigation/Navigation.constants' -import { getPageType } from '../lib/helpers' -import { getAllDocs, getDocsBySlug } from '../lib/docs' +import NewLayout from '~/layouts/DefaultGuideLayout' + import Layout from '~/layouts/Default' +import { getAllDocs, getDocsBySlug } from '../lib/docs' interface Meta { id: string @@ -21,18 +19,22 @@ interface Props { content: any toc: any } +const isNewDocs = process.env.NEXT_PUBLIC_NEW_DOCS === 'true' export default function Doc({ meta, content, toc }: Props) { - const { asPath } = useRouter() - const page = getPageType(asPath) - return ( // @ts-ignore - - - - - + <> + {Object.entries(meta).length > 0 || !isNewDocs ? ( + + + + ) : ( + + + + )} + ) } diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 050f6074fea..52a0b995643 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -1,17 +1,21 @@ -import { useEffect } from 'react' -import { useRouter } from 'next/router' -import type { AppProps } from 'next/app' -import { post } from 'lib/fetchWrappers' -import { ThemeProvider } from '../components/Providers' -import { SearchProvider } from '~/components/DocSearch' import { DefaultSeo } from 'next-seo' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { AppPropsWithLayout } from 'types' +import { SearchProvider } from '~/components/DocSearch' import Favicons from '~/components/Favicons' -import '../styles/main.scss?v=1.0.0' -import '../styles/docsearch.scss' +import { ThemeProvider } from 'common/Providers' +import SiteLayout from '~/layouts/SiteLayout' import '../styles/algolia-search.scss' +import '../styles/ch.scss' +import '../styles/docsearch.scss' +import '../styles/main.scss?v=1.0.0' +import '../styles/new-docs.scss' import '../styles/prism-okaidia.scss' +import { post } from '~/lib/fetchWrappers' +import { menuState } from '~/hooks/useMenuState' -function MyApp({ Component, pageProps }: AppProps) { +function MyApp({ Component, pageProps }: AppPropsWithLayout) { const router = useRouter() function telemetry(route: string) { @@ -24,7 +28,22 @@ function MyApp({ Component, pageProps }: AppProps) { useEffect(() => { function handleRouteChange(url: string) { + /* + * handle telemetry + */ telemetry(url) + /* + * handle "scroll to top" behaviour on route change + */ + if (document) { + // do not scroll to top for reference docs + if (!url.includes('reference/')) { + // scroll container div to top + const container = document.getElementById('docs-content-container') + // check container exists (only avail on new docs) + if (container) container.scrollTop = 0 + } + } } // Listen for page changes after a navigation or when the query changes @@ -65,7 +84,9 @@ function MyApp({ Component, pageProps }: AppProps) { /> - + + + diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx index f550928130a..98acbb79912 100644 --- a/apps/docs/pages/_document.tsx +++ b/apps/docs/pages/_document.tsx @@ -4,7 +4,7 @@ export default function Document() { return ( - +
                  diff --git a/apps/docs/pages/getting-started.mdx b/apps/docs/pages/getting-started.mdx new file mode 100644 index 00000000000..6313b6cb018 --- /dev/null +++ b/apps/docs/pages/getting-started.mdx @@ -0,0 +1,161 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'Overview', + description: 'An overview of Supabase\'s architecture and product principles.', +} + +Supabase is open source. We choose open source tools which are scalable and make them simple to use. + +Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way: +our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch. + +Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase, +while maintaining the scalability required to go beyond it. + +## Architecture + +Each Supabase project consists of several tools: + +![Supabase Architecture](/docs/img/supabase-architecture.png) + +### PostgreSQL (Database) + +PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database — you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase. + +- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html) +- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror) +- License: [PostgreSQL Licence](https://www.postgresql.org/about/licence/) +- Language: C + +### Studio (Dashboard) + +An open source Dashboard for managing your database and services. + +- Official Docs: [Supabase docs](/docs) +- Source code: [github.com/supabase/supabase](https://github.com/supabase/supabase/tree/master/studio) +- License: [Apache 2](https://github.com/supabase/supabase/blob/master/LICENSE) +- Language: TypeScript + +### GoTrue (Auth) + +A JWT-based API for managing users and issuing access tokens. This integrates with PostgreSQL's Row Level Security and the API servers. + +- Official Docs: [Supabase Auth reference docs](/docs/reference/auth) +- Source code: [github.com/supabase/gotrue](https://github.com/supabase/gotrue) +- License: [MIT](https://github.com/supabase/gotrue/blob/master/LICENSE) +- Language: Go + +### PostgREST (API) + +A standalone web server that turns your PostgreSQL database directly into a RESTful API. +We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API. + +- Official Docs: [postgrest.org](https://postgrest.org/) +- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest) +- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE) +- Language: Haskel + +### Realtime (API & multiplayer) + +A scalable websocket engine for managing user Presence, broadcasting messages, and streaming database changes. + +- Official Docs: [Supabase Realtime docs](/docs/guides/realtime) +- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime) +- License: [Apache 2](https://github.com/supabase/realtime/blob/master/LICENSE) +- Language: Elixir + +### Storage API (large file storage) + +An S3-compatible object storage service that stores metadata in Postgres. + +- Official Docs: [Supabase Storage reference docs](/docs/reference/storage) +- Source code: [github.com/supabase/storage-api](https://github.com/supabase/storage-api) +- License: [Apache 2.0](https://github.com/supabase/storage-api/blob/master/LICENSE) +- Language: NodeJS / TypeScript + +### Deno (Edge Functions) + +A modern runtime for JavaScript and TypeScript. + +- Official Docs: [deno.land](https://deno.land/) +- Source code: [github.com/denoland/deno](https://github.com/denoland/deno) +- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md) +- Language: TypeScript / Rust + +### postgres-meta (Database management) + +A RESTful API for managing your Postgres. Fetch tables, add roles, and run queries. + +- Official Docs: [supabase.github.io/postgres-meta](https://supabase.github.io/postgres-meta/) +- Source code: [github.com/supabase/postgres-meta](https://github.com/supabase/postgres-meta) +- License: [Apache 2.0](https://github.com/supabase/postgres-meta/blob/master/LICENSE) +- Language: NodeJS / TypeScript + +### PgBouncer + +A lightweight connection pooler for PostgreSQL. This is useful for connecting to Postgres when using Serverless functions. + +- Official Docs: [pgbouncer.org](http://www.pgbouncer.org/) +- Source code: [pgbouncer/pgbouncer](https://github.com/pgbouncer/pgbouncer) +- License: [ISC](https://github.com/pgbouncer/pgbouncer/blob/master/COPYRIGHT) +- Language: C + +### Kong (API Gateway) + +A cloud-native API gateway, built on top of Nginx. + +- Official Docs: [docs.konghq.com](https://docs.konghq.com/) +- Source code: [github.com/kong/kong](https://github.com/kong/kong) +- License: [Apache 2.0](https://github.com/Kong/kong/blob/master/LICENSE) +- Language: Lua + +## Product Principles + +It is our goal to provide an architecture that any large-scale company would design for themselves, +and then provide tooling around that architecture that is easy-to-use for indie-developers and small teams. + +We use a series of principles to ensure that scalability and usability are never mutually exclusive: + +### Everything works in isolation + +Each system must work as a standalone tool with as few moving parts as possible. +The litmus test for this is: "Can a user run this product with nothing but a Postgres database?" + +### Everything is integrated + +Supabase is composable. Even though every product works in isolation, each product on the platform needs to 10x the other products. +For integration, each tool should expose an API and Webhooks. + +### Everything is extensible + +We're deliberate about adding a new tool, and prefer instead to extend an existing one. +This is the opposite of many cloud providers whose product offering expands into niche use-cases. We provide _primitives_ for developers, which allow them to achieve any goal. +Less, but better. + +### Everything is portable + +To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product. +We use existing standards to increase portability (like pg_dump an CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard. +This forces us compete on experience. We aim to be the best Postgres hosting service. + +### Play the long game + +We sacrifice short-term wins for long-term gains. For example, it is tempting to run a fork of Postgres with additional functionality which only our customers need. +Instead, we prefer to support efforts to upstream missing functionality so that the entire community benefits. This has the additional benefit of ensuring portability and longevity. + +### Build for developers + +"Developers" are a specific profile of user: they are _builders_. +When assessing impact as a function of effort, developers have a large efficiency due to the type of products and systems they can build. +As the profile of a developer changes over time, Supabase will continue to evolve the product to fit this evolving profile. + +### Support existing tools + +Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community +which we work with. +Open source is something we approach [collaboratively](https://supabase.com/blog/supabase-series-b#giving-back): we employ maintainers, sponsor projects, invest in businesses, and develop our own open source tools. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/auth/auth-email-templates.mdx b/apps/docs/pages/guides/auth/auth-email-templates.mdx new file mode 100644 index 00000000000..a82c244000b --- /dev/null +++ b/apps/docs/pages/guides/auth/auth-email-templates.mdx @@ -0,0 +1,50 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'Email Templates', + description: 'Learn how to configure the email templates on Supabase.', +} + +You can customize the email messages used for the authentication flows. You can edit the following email templates: + +- Confirm signup +- Invite user +- Magic Link +- Change Email Address +- Reset Password + +## Terminology + +The templating system provides the following variables for use: + +| Name | Description | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . | +| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . | +| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. | +| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](https://app.supabase.com/project/_/auth/url-configuration). | + +## Limitations + +### Email Prefetching + +Certain email providers may have spam detection or other security features that prefetch URL links from incoming emails. +In this scenario, the `{{ .ConfirmationURL }}` sent will be consumed instantly which leads to a "Token has expired or is invalid" error. +To guard against this: + +- Use an email OTP instead by including `{{ .Token }}` in the email template. +- Create your own custom email link to redirect the user to a page where they can click on a button to confirm the action. + For example, you can include the following in your email template: + + ```html + Confirm your signup + + ``` + + The user should be brought to a page on your site where they can confirm the action by clicking a button. + The button should contain the actual confirmation link which can be obtained from parsing the `confirmation_url={{ .ConfirmationURL }}` query parameter in the URL. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx b/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx index 2a7814999e3..1d07fb9db94 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx @@ -34,7 +34,10 @@ Pass `supabaseClient` from `@supabase/supabase-js` as a prop to the component. import { createClient } from '@supabase/supabase-js' import { Auth } from '@supabase/auth-ui-react' -const supabase = createClient('', '') +const supabase = createClient( + '', + '' +) const App = () => ``` @@ -43,13 +46,18 @@ This renders the Auth component without any styling. We recommend using one of the predefined themes to style the UI. Import the theme you want to use and pass it to the `appearence.theme` prop. -```js lines=4,11 title=/src/index.js +```js lines=4,16 title=/src/index.js import { Auth, // Import predefined theme ThemeSupa, } from '@supabase/auth-ui-react' +const supabase = createClient( + '', + '' +) + const App = () => ( ( ) ``` +### Social Providers + +The Auth component also supports login with [offical social providers](../../auth#providers). + +```js lines=13 title=/src/index.js +import { createClient } from '@supabase/supabase-js' +import { Auth, ThemeSupa } from '@supabase/auth-ui-react' + +const supabase = createClient( + '', + '' +) + +const App = () => ( + +) +``` + ## Customization There are several ways to customize Auth UI: @@ -74,11 +104,14 @@ There are several ways to customize Auth UI: Auth UI comes with several themes to customize the appearance. Each predefined theme comes with at least two variations, a `default` variation, and a `dark` variation. You can switch between these themes using the `theme` prop. Import the theme you want to use and pass it to the `appearence.theme` prop. -```js lines=2,10 title=/src/index.js +```js lines=2,13 title=/src/index.js import { createClient } from '@supabase/supabase-js' import { Auth, ThemeSupa } from '@supabase/auth-ui-react' -const supabase = createClient('', '') +const supabase = createClient( + '', + '' +) const App = () => ( ', '') +const supabase = createClient( + '', + '' +) const App = () => ( ', '') +const supabase = createClient( + '', + '' +) const App = () => ( ', '') +const supabase = createClient( + '', + '' +) const App = () => ( ', '') +const supabase = createClient( + '', + '' +) const App = () => ( ', '') +const supabase = createClient( + '', + '' +) const App = () => ( () ```tsx // Creating a new supabase server client object (e.g. in API route): +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' import type { NextApiRequest, NextApiResponse } from 'next' import type { Database } from 'types_db' @@ -365,7 +366,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { -## Server-side data fetching to OAuth APIs using `provider token` {#oauth-provider-token} +## Server-side data fetching to OAuth APIs using `provider token` {`#oauth-provider-token`} When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. diff --git a/apps/docs/pages/guides/auth/auth-helpers/remix.mdx b/apps/docs/pages/guides/auth/auth-helpers/remix.mdx index 6df649ea7b1..0aa105dd66a 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/remix.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/remix.mdx @@ -9,6 +9,8 @@ export const meta = { This submodule provides convenience helpers for implementing user authentication in Remix applications. +> Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) for a full example. + ## Install the Remix helper library -Loader functions run on the server immediately before the component is rendered. They respond to all GET requests on a route. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`. - ```jsx import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' @@ -95,17 +101,17 @@ export const loader = async ({ request }) => { -Loader functions run on the server immediately before the component is rendered. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`. - ```jsx -import { LoaderFunction, json } from '@remix-run/node' // change this import to whatever runtime you are using +import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' -export const loader: LoaderFunction = async ({ request }: { request: Request }) => { +import type { LoaderArgs } from '@remix-run/node' // change this import to whatever runtime you are using + +export const loader = async ({ request }: LoaderArgs) => { const response = new Response() const supabaseClient = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY, + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, { request, response } ) @@ -125,7 +131,9 @@ export const loader: LoaderFunction = async ({ request }: { request: Request }) -## Action +### Action + +Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`. -Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`. - ```jsx import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' @@ -166,18 +172,18 @@ export const action = async ({ request }) => { -Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`. - ```jsx -import { ActionFunction, json } from '@remix-run/node' // change this import to whatever runtime you are using +import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' -export const action: ActionFunction = async ({ request }: { request: Request }) => { +import type { ActionArgs } from '@remix-run/node' // change this import to whatever runtime you are using + +export const action = async ({ request }: ActionArgs) => { const response = new Response() const supabaseClient = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY, + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, { request, response } ) @@ -213,10 +219,22 @@ The session contains a user property. const user = session?.user ``` -> This is the recommended way for accessing the logged in user. There is also a `getUser()` function but this does not refresh the session if it has expired. +Or, if you don't need the session, you can call the `getUser()` function. + +```jsx +const { + data: { user }, +} = await supabaseClient.auth.getUser() +``` ## Client-side +We still need to use Supabase client-side for things like authentication and realtime subscriptions. Anytime we use Supabase client-side it needs to be a single instance. + +### Creating a singleton Supabase client + +Since our environment variables are not available client-side, we need to plumb them through from the loader. + -In order to use the Supabase client in the browser - fetching data in `useEffect` or subscribing to realtime events - we need to do a little more plumbing. Remix does not include a way to make environment variables available to the browser, so we need to pipe them through from a `loader` function in our `root.jsx` route and attach them to the `window`. - ```jsx title=app/root.jsx export const loader = () => { - const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env - return json({ - env: { - SUPABASE_URL, - SUPABASE_ANON_KEY, - }, - }) -} -``` - -> These may not be stored in `process.env` for environments other than Node. - -Next, we call the `useLoaderData` hook in our component to get the `env` object. - -```jsx title=app/root.jsx -const { env } = useLoaderData() -``` - -And then, add a `