mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
add codehike, some basic parameters to ref docs, and some other stuff
This commit is contained in:
1 parent
7dde56af72
commit
c107c5df19
28 files changed
+5539
-1482
No files matched your search
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -2,19 +2,19 @@ import UserManagementSQLTemplate from './user_management_quickstart_sql_template
|
||||
import { Tabs } from 'ui'
|
||||
export const TabPanel = Tabs.Panel
|
||||
|
||||
## Project set up
|
||||
# Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
## Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
## Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
@@ -43,7 +43,7 @@ or you can just copy/paste the SQL from below and run it yourself.
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
## Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a basic user management app. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
|
||||
@@ -11,6 +11,7 @@ interface TOCHeader {
|
||||
|
||||
interface Props {
|
||||
toc: any
|
||||
video: string
|
||||
}
|
||||
|
||||
const formatSlug = (slug: string) => {
|
||||
@@ -40,26 +41,38 @@ const formatTOCHeader = (content: string) => {
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
const TableOfContents: FC<Props> = ({ toc }) => {
|
||||
const TableOfContents: FC<Props> = ({ 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 (
|
||||
<ul className="toc-menu list-none pl-4 text-[0.8rem] grid gap-2 mt-12">
|
||||
{(toc.json as TOCHeader[])
|
||||
.filter((item) => item.lvl !== 1 && item.lvl <= 3)
|
||||
.map((item: any, i: number) => {
|
||||
return (
|
||||
<li key={i} id={item.lvl} style={{ marginLeft: `${(item.lvl - 2) * 1}rem` }}>
|
||||
<a
|
||||
href={`#${formatSlug(item.slug)}`}
|
||||
className="text-scale-1000 hover:text-brand-900 transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<>
|
||||
{video && (
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/0Fs96oZ4se0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
)}
|
||||
<ul className="toc-menu list-none pl-4 text-[0.8rem] grid gap-2 mt-12">
|
||||
{(toc.json as TOCHeader[])
|
||||
.filter((item) => item.lvl !== 1 && item.lvl <= 3)
|
||||
.map((item: any, i: number) => {
|
||||
return (
|
||||
<li key={i} id={item.lvl} style={{ marginLeft: `${(item.lvl - 2) * 1}rem` }}>
|
||||
<a
|
||||
href={`#${formatSlug(item.slug)}`}
|
||||
className="text-scale-1000 hover:text-brand-900 transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,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
|
||||
|
||||
+38
-20
@@ -1,20 +1,22 @@
|
||||
import { useEffect, FC } from 'react'
|
||||
import Head from 'next/head'
|
||||
import NavBar from '~/components/Navigation/NavBar'
|
||||
import SideBar from '~/components/Navigation/SideBar'
|
||||
import Footer from '~/components/Footer'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import Head from 'next/head'
|
||||
import { FC, useEffect } from 'react'
|
||||
import Footer from '~/components/Footer'
|
||||
import SideBar from '~/components/Navigation/SideBar'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
import { Tabs } from 'ui'
|
||||
|
||||
// var toc = require('markdown-toc')
|
||||
|
||||
interface Props {
|
||||
meta: { title: string; description?: string; hide_table_of_contents?: boolean }
|
||||
meta: { title: string; description?: string; hide_table_of_contents?: boolean; video?: string }
|
||||
children: any
|
||||
toc?: any
|
||||
menuItems: any
|
||||
currentPage: string
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = ({ meta, children, toc, menuItems, currentPage }) => {
|
||||
const Layout: FC<Props> = (props: Props) => {
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (!key) {
|
||||
@@ -25,46 +27,62 @@ const Layout: FC<Props> = ({ meta, children, toc, menuItems, currentPage }) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// const contentString = renderToString(props.children)
|
||||
|
||||
// const content = serialize(contentString || '')
|
||||
|
||||
// console.log(props)
|
||||
|
||||
// const _toc = /toc(children, { maxdepth: 1, firsth1: false })
|
||||
|
||||
const MDXComponents = {
|
||||
Tabs: Tabs,
|
||||
}
|
||||
|
||||
const hasTableOfContents =
|
||||
toc !== undefined && toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
props.toc !== undefined &&
|
||||
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{meta?.title} | Supabase</title>
|
||||
<meta name="description" content={meta?.description} />
|
||||
<title>{props.meta?.title} | Supabase</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link rel="icon" href="/docs/favicon.ico" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={meta?.title} />
|
||||
<meta property="og:description" content={meta?.description} />
|
||||
<meta property="og:title" content={meta?.title} />
|
||||
<meta property="og:site_name" content={props.meta?.title} />
|
||||
<meta property="og:description" content={props.meta?.description} />
|
||||
<meta property="og:title" content={props.meta?.title} />
|
||||
</Head>
|
||||
|
||||
<main>
|
||||
{/* <NavBar currentPage={currentPage} /> */}
|
||||
<div className="flex w-full flex-row">
|
||||
<SideBar menuItems={menuItems} />
|
||||
<SideBar menuItems={props.menuItems} />
|
||||
<div className="main-content-pane docs-width grid md:grid-cols-12 gap-4 justify-between p-4 pb-8 w-full">
|
||||
<div
|
||||
className={`${
|
||||
meta?.hide_table_of_contents || !hasTableOfContents
|
||||
props.meta?.hide_table_of_contents || !hasTableOfContents
|
||||
? 'col-span-12 xl:col-start-2 xl:col-span-10 2xl:col-start-3 2xl:col-span-8'
|
||||
: 'col-span-12 md:col-span-9'
|
||||
} py-2 md:py-4 px-2 md:px-8`}
|
||||
>
|
||||
<article className="prose dark:prose-dark dark:bg-scale-200 width-full mt-8">
|
||||
<MDXProvider components={{}} children={children} />
|
||||
<p className="text-brand-900 tracking-wider">Tutorials</p>
|
||||
<article className="prose dark:prose-dark dark:bg-scale-200 max-w-4xl mt-8">
|
||||
<h1>{props.meta.title}</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-16"></div>
|
||||
<MDXProvider components={MDXComponents} children={props.children} />
|
||||
</article>
|
||||
</div>
|
||||
{hasTableOfContents && !meta?.hide_table_of_contents && (
|
||||
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-scale-400 dark:bg-scale-200 table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={toc} />
|
||||
<TableOfContents toc={props.toc} video={props.meta.video} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import Head from 'next/head'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { Tabs } from 'ui'
|
||||
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: Props) => {
|
||||
const contentString = renderToString(props.children)
|
||||
|
||||
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 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
|
||||
|
||||
const components = {
|
||||
// img: ResponsiveImage,
|
||||
// h1: Heading.H1,
|
||||
// h2: Heading.H2,
|
||||
// p: Text,
|
||||
// pre: Pre,
|
||||
// code: InlineCode,
|
||||
Tabs: Tabs,
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{props.meta?.title} | Supabase</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link rel="icon" href="/docs/favicon.ico" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={props.meta?.title} />
|
||||
<meta property="og:description" content={props.meta?.description} />
|
||||
<meta property="og:title" content={props.meta?.title} />
|
||||
</Head>
|
||||
|
||||
<main>
|
||||
{/* <NavBar currentPage={currentPage} /> */}
|
||||
<div className="flex w-full flex-row">
|
||||
<SideBar menuItems={props.menuItems} />
|
||||
<div className="main-content-pane docs-width grid md:grid-cols-12 gap-4 justify-between p-4 pb-8 w-full">
|
||||
<div
|
||||
className={`${
|
||||
props.meta?.hide_table_of_contents || !hasTableOfContents
|
||||
? 'col-span-12 xl:col-start-2 xl:col-span-10 2xl:col-start-3 2xl:col-span-8'
|
||||
: 'col-span-12 md:col-span-9'
|
||||
} py-2 md:py-4 px-2 md:px-8`}
|
||||
>
|
||||
<p className="text-brand-900 tracking-wider">Tutorials</p>
|
||||
<article className="prose dark:prose-dark dark:bg-scale-200 max-w-4xl mt-8">
|
||||
<h1>{props.meta.title}</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-16"></div>
|
||||
<MDXProvider components={components} children={props.children} />
|
||||
</article>
|
||||
</div>
|
||||
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-scale-400 dark:bg-scale-200 table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={props.toc} video={props.meta.video} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2017, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,359 @@
|
||||
# markdown-toc [](https://www.npmjs.com/package/markdown-toc) [](https://npmjs.org/package/markdown-toc) [](https://npmjs.org/package/markdown-toc) [](https://travis-ci.org/jonschlinkert/markdown-toc) [](https://ci.appveyor.com/project/jonschlinkert/markdown-toc)
|
||||
|
||||
> Generate a markdown TOC (table of contents) with Remarkable.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Install](#install)
|
||||
- [CLI](#cli)
|
||||
- [Highlights](#highlights)
|
||||
- [Usage](#usage)
|
||||
- [API](#api)
|
||||
* [toc.plugin](#tocplugin)
|
||||
* [toc.json](#tocjson)
|
||||
* [toc.insert](#tocinsert)
|
||||
* [Utility functions](#utility-functions)
|
||||
- [Options](#options)
|
||||
* [options.append](#optionsappend)
|
||||
* [options.filter](#optionsfilter)
|
||||
* [options.slugify](#optionsslugify)
|
||||
* [options.bullets](#optionsbullets)
|
||||
* [options.maxdepth](#optionsmaxdepth)
|
||||
* [options.firsth1](#optionsfirsth1)
|
||||
* [options.stripHeadingTags](#optionsstripheadingtags)
|
||||
- [About](#about)
|
||||
|
||||
_(TOC generated by [verb](https://github.com/verbose/verb) using [markdown-toc](https://github.com/jonschlinkert/markdown-toc))_
|
||||
|
||||
## Install
|
||||
|
||||
Install with [npm](https://www.npmjs.com/):
|
||||
|
||||
```sh
|
||||
$ npm install --save markdown-toc
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
Usage: markdown-toc [options] <input>
|
||||
|
||||
input: The Markdown file to parse for table of contents,
|
||||
or "-" to read from stdin.
|
||||
|
||||
-i: Edit the <input> file directly, injecting the TOC at <!-- toc -->
|
||||
(Without this flag, the default is to print the TOC to stdout.)
|
||||
|
||||
--json: Print the TOC in JSON format
|
||||
|
||||
--append: Append a string to the end of the TOC
|
||||
|
||||
--bullets: Bullets to use for items in the generated TOC
|
||||
(Supports multiple bullets: --bullets "*" --bullets "-" --bullets "+")
|
||||
(Default is "*".)
|
||||
|
||||
--maxdepth: Use headings whose depth is at most maxdepth
|
||||
(Default is 6.)
|
||||
|
||||
--no-firsth1: Include the first h1-level heading in a file
|
||||
|
||||
--no-stripHeadingTags: Do not strip extraneous HTML tags from heading
|
||||
text before slugifying
|
||||
```
|
||||
|
||||
## Highlights
|
||||
|
||||
**Features**
|
||||
|
||||
* Can optionally be used as a [remarkable](https://github.com/jonschlinkert/remarkable) plugin
|
||||
* Returns an object with the rendered TOC (on `content`), as well as a `json` property with the raw TOC object, so you can generate your own TOC using templates or however you want
|
||||
* Works with [repeated headings](https://gist.github.com/jonschlinkert/ac5d8122bfaaa394f896)
|
||||
* Uses sane defaults, so no customization is necessary, but you can if you need to.
|
||||
* [filter](#filter-headings) out headings you don't want
|
||||
* [Improve](#titleize) the headings you do want
|
||||
* Use a custom [slugify](#optionsslugify) function to change how links are created
|
||||
|
||||
**Safe!**
|
||||
|
||||
* Won't mangle markdown in code examples in gfm code blocks that other TOC generators mistake as being actual headings (this happens when markdown headings are show in _examples_, meaning they arent' actually headings that should be in the toc. Also happens with yaml and coffee-script comments, or any comments that use `#`)
|
||||
* Won't mangle front-matter, or mistake front-matter properties for headings like other TOC generators
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
var toc = require('markdown-toc');
|
||||
|
||||
toc('# One\n\n# Two').content;
|
||||
// Results in:
|
||||
// - [One](#one)
|
||||
// - [Two](#two)
|
||||
```
|
||||
|
||||
To allow customization of the output, an object is returned with the following properties:
|
||||
|
||||
* `content` **{String}**: The generated table of contents. Unless you want to customize rendering, this is all you need.
|
||||
* `highest` **{Number}**: The highest level heading found. This is used to adjust indentation.
|
||||
* `tokens` **{Array}**: Headings tokens that can be used for custom rendering
|
||||
|
||||
## API
|
||||
|
||||
### toc.plugin
|
||||
|
||||
Use as a [remarkable](https://github.com/jonschlinkert/remarkable) plugin.
|
||||
|
||||
```js
|
||||
var Remarkable = require('remarkable');
|
||||
var toc = require('markdown-toc');
|
||||
|
||||
function render(str, options) {
|
||||
return new Remarkable()
|
||||
.use(toc.plugin(options)) // <= register the plugin
|
||||
.render(str);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage example**
|
||||
|
||||
```js
|
||||
var results = render('# AAA\n# BBB\n# CCC\nfoo\nbar\nbaz');
|
||||
```
|
||||
|
||||
Results in:
|
||||
|
||||
```
|
||||
- [AAA](#aaa)
|
||||
- [BBB](#bbb)
|
||||
- [CCC](#ccc)
|
||||
```
|
||||
|
||||
### toc.json
|
||||
|
||||
Object for creating a custom TOC.
|
||||
|
||||
```js
|
||||
toc('# AAA\n## BBB\n### CCC\nfoo').json;
|
||||
|
||||
// results in
|
||||
[ { content: 'AAA', slug: 'aaa', lvl: 1 },
|
||||
{ content: 'BBB', slug: 'bbb', lvl: 2 },
|
||||
{ content: 'CCC', slug: 'ccc', lvl: 3 } ]
|
||||
```
|
||||
|
||||
### toc.insert
|
||||
|
||||
Insert a table of contents immediately after an _opening_ `<!-- toc -->` code comment, or replace an existing TOC if both an _opening_ comment and a _closing_ comment (`<!-- tocstop -->`) are found.
|
||||
|
||||
_(This strategy works well since code comments in markdown are hidden when viewed as HTML, like when viewing a README on GitHub README for example)._
|
||||
|
||||
**Example**
|
||||
|
||||
```
|
||||
<!-- toc -->
|
||||
- old toc 1
|
||||
- old toc 2
|
||||
- old toc 3
|
||||
<!-- tocstop -->
|
||||
|
||||
## abc
|
||||
This is a b c.
|
||||
|
||||
## xyz
|
||||
This is x y z.
|
||||
```
|
||||
|
||||
Would result in something like:
|
||||
|
||||
```
|
||||
<!-- toc -->
|
||||
- [abc](#abc)
|
||||
- [xyz](#xyz)
|
||||
<!-- tocstop -->
|
||||
|
||||
## abc
|
||||
This is a b c.
|
||||
|
||||
## xyz
|
||||
This is x y z.
|
||||
```
|
||||
|
||||
### Utility functions
|
||||
|
||||
As a convenience to folks who wants to create a custom TOC, markdown-toc's internal utility methods are exposed:
|
||||
|
||||
```js
|
||||
var toc = require('markdown-toc');
|
||||
```
|
||||
|
||||
* `toc.bullets()`: render a bullet list from an array of tokens
|
||||
* `toc.linkify()`: linking a heading `content` string
|
||||
* `toc.slugify()`: slugify a heading `content` string
|
||||
* `toc.strip()`: strip words or characters from a heading `content` string
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
var result = toc('# AAA\n## BBB\n### CCC\nfoo');
|
||||
var str = '';
|
||||
|
||||
result.json.forEach(function(heading) {
|
||||
str += toc.linkify(heading.content);
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### options.append
|
||||
|
||||
Append a string to the end of the TOC.
|
||||
|
||||
```js
|
||||
toc(str, {append: '\n_(TOC generated by Verb)_'});
|
||||
```
|
||||
|
||||
### options.filter
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Default: `undefined`
|
||||
|
||||
Params:
|
||||
|
||||
* `str` **{String}** the actual heading string
|
||||
* `ele` **{Objecct}** object of heading tokens
|
||||
* `arr` **{Array}** all of the headings objects
|
||||
|
||||
**Example**
|
||||
|
||||
From time to time, we might get junk like this in our TOC.
|
||||
|
||||
```
|
||||
[.aaa([foo], ...) another bad heading](#-aaa--foo--------another-bad-heading)
|
||||
```
|
||||
|
||||
Unless you like that kind of thing, you might want to filter these bad headings out.
|
||||
|
||||
```js
|
||||
function removeJunk(str, ele, arr) {
|
||||
return str.indexOf('...') === -1;
|
||||
}
|
||||
|
||||
var result = toc(str, {filter: removeJunk});
|
||||
//=> beautiful TOC
|
||||
```
|
||||
|
||||
### options.slugify
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Default: Basic non-word character replacement.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
var str = toc('# Some Article', {slugify: require('uslug')});
|
||||
```
|
||||
|
||||
### options.bullets
|
||||
|
||||
Type: `String|Array`
|
||||
|
||||
Default: `*`
|
||||
|
||||
The bullet to use for each item in the generated TOC. If passed as an array (`['*', '-', '+']`), the bullet point strings will be used based on the header depth.
|
||||
|
||||
### options.maxdepth
|
||||
|
||||
Type: `Number`
|
||||
|
||||
Default: `6`
|
||||
|
||||
Use headings whose depth is at most maxdepth.
|
||||
|
||||
### options.firsth1
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `true`
|
||||
|
||||
Exclude the first h1-level heading in a file. For example, this prevents the first heading in a README from showing up in the TOC.
|
||||
|
||||
### options.stripHeadingTags
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `true`
|
||||
|
||||
Strip extraneous HTML tags from heading text before slugifying. This is similar to GitHub markdown behavior.
|
||||
|
||||
## About
|
||||
|
||||
### Related projects
|
||||
|
||||
* [gfm-code-blocks](https://www.npmjs.com/package/gfm-code-blocks): Extract gfm (GitHub Flavored Markdown) fenced code blocks from a string. | [homepage](https://github.com/jonschlinkert/gfm-code-blocks "Extract gfm (GitHub Flavored Markdown) fenced code blocks from a string.")
|
||||
* [markdown-link](https://www.npmjs.com/package/markdown-link): Micro util for generating a single markdown link. | [homepage](https://github.com/jonschlinkert/markdown-link "Micro util for generating a single markdown link.")
|
||||
* [markdown-utils](https://www.npmjs.com/package/markdown-utils): Micro-utils for creating markdown snippets. | [homepage](https://github.com/jonschlinkert/markdown-utils "Micro-utils for creating markdown snippets.")
|
||||
* [pretty-remarkable](https://www.npmjs.com/package/pretty-remarkable): Plugin for prettifying markdown with Remarkable using custom renderer rules. | [homepage](https://github.com/jonschlinkert/pretty-remarkable "Plugin for prettifying markdown with Remarkable using custom renderer rules.")
|
||||
* [remarkable](https://www.npmjs.com/package/remarkable): Markdown parser, done right. 100% Commonmark support, extensions, syntax plugins, high speed - all in… [more](https://github.com/jonschlinkert/remarkable) | [homepage](https://github.com/jonschlinkert/remarkable "Markdown parser, done right. 100% Commonmark support, extensions, syntax plugins, high speed - all in one.")
|
||||
|
||||
### Contributing
|
||||
|
||||
Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new).
|
||||
|
||||
### Contributors
|
||||
|
||||
| **Commits** | **Contributor** |
|
||||
| --- | --- |
|
||||
| 196 | [jonschlinkert](https://github.com/jonschlinkert) |
|
||||
| 4 | [stefanwalther](https://github.com/stefanwalther) |
|
||||
| 3 | [Marsup](https://github.com/Marsup) |
|
||||
| 2 | [dvcrn](https://github.com/dvcrn) |
|
||||
| 2 | [maxogden](https://github.com/maxogden) |
|
||||
| 2 | [twang2218](https://github.com/twang2218) |
|
||||
| 2 | [angrykoala](https://github.com/angrykoala) |
|
||||
| 2 | [zeke](https://github.com/zeke) |
|
||||
| 1 | [Vortex375](https://github.com/Vortex375) |
|
||||
| 1 | [owzim](https://github.com/owzim) |
|
||||
| 1 | [chendaniely](https://github.com/chendaniely) |
|
||||
| 1 | [Daniel-Mietchen](https://github.com/Daniel-Mietchen) |
|
||||
| 1 | [Feder1co5oave](https://github.com/Feder1co5oave) |
|
||||
| 1 | [garygreen](https://github.com/garygreen) |
|
||||
| 1 | [TehShrike](https://github.com/TehShrike) |
|
||||
| 1 | [citizenmatt](https://github.com/citizenmatt) |
|
||||
| 1 | [rafaelsteil](https://github.com/rafaelsteil) |
|
||||
| 1 | [RichardBradley](https://github.com/RichardBradley) |
|
||||
| 1 | [sethvincent](https://github.com/sethvincent) |
|
||||
| 1 | [lu22do](https://github.com/lu22do) |
|
||||
|
||||
### Building docs
|
||||
|
||||
_(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_
|
||||
|
||||
To generate the readme, run the following command:
|
||||
|
||||
```sh
|
||||
$ npm install -g verbose/verb#dev verb-generate-readme && verb
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command:
|
||||
|
||||
```sh
|
||||
$ npm install && npm test
|
||||
```
|
||||
|
||||
### Author
|
||||
|
||||
**Jon Schlinkert**
|
||||
|
||||
* [github/jonschlinkert](https://github.com/jonschlinkert)
|
||||
* [twitter/jonschlinkert](https://twitter.com/jonschlinkert)
|
||||
|
||||
### License
|
||||
|
||||
Copyright © 2017, [Jon Schlinkert](https://github.com/jonschlinkert).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
***
|
||||
|
||||
_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.6.0, on September 19, 2017._
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var fs = require('fs');
|
||||
var toc = require('./index.js');
|
||||
var utils = require('./lib/utils');
|
||||
var args = utils.minimist(process.argv.slice(2), {
|
||||
boolean: ['i', 'json', 'firsth1', 'stripHeadingTags'],
|
||||
string: ['append', 'bullets'],
|
||||
default: {
|
||||
firsth1: true,
|
||||
stripHeadingTags: true
|
||||
}
|
||||
});
|
||||
|
||||
if (args._.length !== 1) {
|
||||
console.error([
|
||||
'Usage: markdown-toc [options] <input> ',
|
||||
'',
|
||||
' input: The Markdown file to parse for table of contents,',
|
||||
' or "-" to read from stdin.',
|
||||
'',
|
||||
' -i: Edit the <input> file directly, injecting the TOC at <!-- toc -->',
|
||||
' (Without this flag, the default is to print the TOC to stdout.)',
|
||||
'',
|
||||
' --json: Print the TOC in JSON format',
|
||||
'',
|
||||
' --append: Append a string to the end of the TOC',
|
||||
'',
|
||||
' --bullets: Bullets to use for items in the generated TOC',
|
||||
' (Supports multiple bullets: --bullets "*" --bullets "-" --bullets "+")',
|
||||
' (Default is "*".)',
|
||||
'',
|
||||
' --maxdepth: Use headings whose depth is at most maxdepth',
|
||||
' (Default is 6.)',
|
||||
'',
|
||||
' --no-firsth1: Include the first h1-level heading in a file',
|
||||
'',
|
||||
' --no-stripHeadingTags: Do not strip extraneous HTML tags from heading',
|
||||
' text before slugifying'
|
||||
].join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.i && args.json) {
|
||||
console.error('markdown-toc: you cannot use both --json and -i');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.i && args._[0] === '-') {
|
||||
console.error('markdown-toc: you cannot use -i with "-" (stdin) for input');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var input = process.stdin;
|
||||
if (args._[0] !== '-') input = fs.createReadStream(args._[0]);
|
||||
|
||||
input.pipe(utils.concat(function(input) {
|
||||
if (args.i) {
|
||||
var newMarkdown = toc.insert(input.toString(), args);
|
||||
fs.writeFileSync(args._[0], newMarkdown);
|
||||
} else {
|
||||
var parsed = toc(input.toString(), args);
|
||||
output(parsed);
|
||||
}
|
||||
}));
|
||||
|
||||
input.on('error', function onErr(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function output(parsed) {
|
||||
if (args.json) return console.log(JSON.stringify(parsed.json, null, ' '));
|
||||
process.stdout.write(parsed.content);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
'use strict'
|
||||
|
||||
/*!
|
||||
* markdown-toc <https://github.com/jonschlinkert/markdown-toc>
|
||||
*
|
||||
* Copyright © 2013-2017, Jon Schlinkert.
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
var utils = require('./lib/utils')
|
||||
var querystring = require('querystring')
|
||||
|
||||
/**
|
||||
* expose `toc`
|
||||
*/
|
||||
|
||||
module.exports = toc
|
||||
|
||||
/**
|
||||
* Load `generate` as a remarkable plugin and
|
||||
* expose the `toc` function.
|
||||
*
|
||||
* @param {String} `str` String of markdown
|
||||
* @param {Object} `options`
|
||||
* @return {String} Markdown-formatted table of contents
|
||||
*/
|
||||
|
||||
function toc(str, options) {
|
||||
return new utils.Remarkable().use(generate(options)).render(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose `insert` method
|
||||
*/
|
||||
|
||||
toc.insert = require('./lib/insert')
|
||||
|
||||
/**
|
||||
* Generate a markdown table of contents. This is the
|
||||
* function that does all of the main work with Remarkable.
|
||||
*
|
||||
* @param {Object} `options`
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function generate(options) {
|
||||
var opts = utils.merge({ firsth1: true, maxdepth: 6 }, options)
|
||||
var stripFirst = opts.firsth1 === false
|
||||
if (typeof opts.linkify === 'undefined') opts.linkify = true
|
||||
|
||||
return function (md) {
|
||||
md.renderer.render = function (tokens) {
|
||||
tokens = tokens.slice()
|
||||
var seen = {}
|
||||
var len = tokens.length,
|
||||
i = 0,
|
||||
num = 0
|
||||
var tocstart = -1
|
||||
var arr = []
|
||||
var res = {}
|
||||
|
||||
while (len--) {
|
||||
var token = tokens[i++]
|
||||
if (/<!--[ \t]*toc[ \t]*-->/.test(token.content)) {
|
||||
tocstart = token.lines[1]
|
||||
}
|
||||
|
||||
if (token.type === 'heading_open') {
|
||||
tokens[i].lvl = tokens[i - 1].hLevel
|
||||
tokens[i].i = num++
|
||||
arr.push(tokens[i])
|
||||
}
|
||||
}
|
||||
|
||||
var result = []
|
||||
res.json = []
|
||||
|
||||
// exclude headings that come before the actual
|
||||
// table of contents.
|
||||
var alen = arr.length,
|
||||
j = 0
|
||||
while (alen--) {
|
||||
var tok = arr[j++]
|
||||
|
||||
if (tok.lines && tok.lines[0] > tocstart) {
|
||||
var val = tok.content
|
||||
if (tok.children && tok.children[0].type === 'link_open') {
|
||||
if (tok.children[1].type === 'text') {
|
||||
val = tok.children[1].content
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen.hasOwnProperty(val)) {
|
||||
seen[val] = 0
|
||||
} else {
|
||||
seen[val]++
|
||||
}
|
||||
|
||||
tok.seen = opts.num = seen[val]
|
||||
tok.slug = utils.slugify(val, opts)
|
||||
res.json.push(utils.pick(tok, ['content', 'slug', 'lvl', 'i', 'seen']))
|
||||
if (opts.linkify) tok = linkify(tok, opts)
|
||||
result.push(tok)
|
||||
}
|
||||
}
|
||||
|
||||
opts.highest = highest(result)
|
||||
res.highest = opts.highest
|
||||
res.tokens = tokens
|
||||
|
||||
if (stripFirst) result = result.slice(1)
|
||||
res.content = bullets(result, opts)
|
||||
res.content += opts.append || ''
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render markdown list bullets
|
||||
*
|
||||
* @param {Array} `arr` Array of listitem objects
|
||||
* @param {Object} `opts`
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function bullets(arr, options) {
|
||||
var opts = utils.merge({ indent: ' ' }, options)
|
||||
opts.chars = opts.chars || opts.bullets || ['-', '*', '+']
|
||||
var unindent = 0
|
||||
|
||||
var listitem = utils.li(opts)
|
||||
var fn = typeof opts.filter === 'function' ? opts.filter : null
|
||||
|
||||
// Keep the first h1? This is `true` by default
|
||||
if (opts && opts.firsth1 === false) {
|
||||
unindent = 1
|
||||
}
|
||||
|
||||
var len = arr.length
|
||||
var res = []
|
||||
var i = 0
|
||||
|
||||
while (i < len) {
|
||||
var ele = arr[i++]
|
||||
ele.lvl -= unindent
|
||||
if (fn && !fn(ele.content, ele, arr)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (ele.lvl > opts.maxdepth) {
|
||||
continue
|
||||
}
|
||||
|
||||
var lvl = ele.lvl - opts.highest
|
||||
res.push(listitem(lvl, ele.content, opts))
|
||||
}
|
||||
return res.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highest heading level in the array, so
|
||||
* we can un-indent the proper number of levels.
|
||||
*
|
||||
* @param {Array} `arr` Array of tokens
|
||||
* @return {Number} Highest level
|
||||
*/
|
||||
|
||||
function highest(arr) {
|
||||
var res = arr.slice().sort(function (a, b) {
|
||||
return a.lvl - b.lvl
|
||||
})
|
||||
if (res && res.length) {
|
||||
return res[0].lvl
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn headings into anchors
|
||||
*/
|
||||
|
||||
function linkify(tok, options) {
|
||||
var opts = utils.merge({}, options)
|
||||
if (tok && tok.content) {
|
||||
opts.num = tok.seen
|
||||
var text = titleize(tok.content, opts)
|
||||
var slug = utils.slugify(tok.content, opts)
|
||||
slug = querystring.escape(slug)
|
||||
if (opts && typeof opts.linkify === 'function') {
|
||||
return opts.linkify(tok, text, slug, opts)
|
||||
}
|
||||
tok.content = utils.mdlink(text, '#' + slug)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
/**
|
||||
* Titleize the title part of a markdown link.
|
||||
*
|
||||
* @name options.titleize
|
||||
* @param {String} `str` The string to titleize
|
||||
* @param {Object} `opts` Pass a custom titleize function on `titleize`
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function titleize(str, opts) {
|
||||
if (opts && opts.strip) {
|
||||
return strip(str, opts)
|
||||
}
|
||||
if (opts && opts.titleize === false) return str
|
||||
if (opts && typeof opts.titleize === 'function') {
|
||||
return opts.titleize(str, opts)
|
||||
}
|
||||
str = utils.getTitle(str)
|
||||
str = str.split(/<\/?[^>]+>/).join('')
|
||||
str = str.split(/[ \t]+/).join(' ')
|
||||
return str.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally strip specified words from heading text (not url)
|
||||
*
|
||||
* @name options.strip
|
||||
* @param {String} `str`
|
||||
* @param {String} `opts`
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function strip(str, opts) {
|
||||
opts = opts || {}
|
||||
if (!opts.strip) return str
|
||||
if (typeof opts.strip === 'function') {
|
||||
return opts.strip(str, opts)
|
||||
}
|
||||
if (Array.isArray(opts.strip) && opts.strip.length) {
|
||||
var res = opts.strip.join('|')
|
||||
var re = new RegExp(res, 'g')
|
||||
str = str.trim().replace(re, '')
|
||||
return str.replace(/^-|-$/g, '')
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose utils
|
||||
*/
|
||||
|
||||
toc.bullets = bullets
|
||||
toc.linkify = linkify
|
||||
toc.slugify = utils.slugify
|
||||
toc.titleize = titleize
|
||||
toc.plugin = generate
|
||||
toc.strip = strip
|
||||
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
var toc = require('..');
|
||||
var utils = require('./utils');
|
||||
|
||||
/**
|
||||
* The basic idea:
|
||||
*
|
||||
* 1. when front-matter exists, we need to avoid turning its properties into headings.
|
||||
* 2. We need to detect toc markers on the page. For now it's a simple HTML code comment
|
||||
* to ensure the markdown is compatible with any parser.
|
||||
*
|
||||
* @param {String} `str` Pass a string of markdown
|
||||
* @param {Object} `options` Pass options to toc generation
|
||||
* @return {String} Get the same string back with a TOC inserted
|
||||
*/
|
||||
|
||||
module.exports = function insert(str, options) {
|
||||
options = options || {};
|
||||
|
||||
var regex = options.regex || /(?:<!-- toc(?:\s*stop)? -->)/g;
|
||||
var open = typeof options.open === 'string' ? options.open : '<!-- toc -->\n\n';
|
||||
var close = typeof options.close === 'string' ? options.close : '<!-- tocstop -->';
|
||||
var obj;
|
||||
|
||||
var newlines = '';
|
||||
var m = /\n+$/.exec(str);
|
||||
if (m) newlines = m[0];
|
||||
|
||||
// does the file have front-matter?
|
||||
if (/^---/.test(str)) {
|
||||
// extract it temporarily so the syntax
|
||||
// doesn't get mistaken for a heading
|
||||
obj = utils.matter(str);
|
||||
str = obj.content;
|
||||
}
|
||||
|
||||
var sections = split(str, regex);
|
||||
if (sections.length > 3) {
|
||||
throw new Error('markdown-toc only supports one Table of Contents per file.');
|
||||
}
|
||||
|
||||
var last = sections[sections.length - 1];
|
||||
if (sections.length === 3) {
|
||||
sections.splice(1, 1, open + (options.toc || toc(last, options).content));
|
||||
sections.splice(2, 0, close);
|
||||
}
|
||||
|
||||
if (sections.length === 2) {
|
||||
sections.splice(1, 0, open + toc(last, options).content + '\n\n' + close);
|
||||
}
|
||||
|
||||
var resultString = sections.join('\n\n') + newlines;
|
||||
// if front-matter was found, put it back now
|
||||
if (obj) {
|
||||
return utils.matter.stringify(resultString, obj.data);
|
||||
}
|
||||
return resultString;
|
||||
};
|
||||
|
||||
function split(str, re) {
|
||||
return str.split(re).map(trim);
|
||||
}
|
||||
|
||||
function trim(str) {
|
||||
return str.trim();
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
|
||||
var diacritics = require('diacritics-map')
|
||||
var utils
|
||||
|
||||
/**
|
||||
* Lazily required module dependencies
|
||||
*/
|
||||
|
||||
require('concat-stream', 'concat')
|
||||
require('gray-matter', 'matter')
|
||||
require('list-item', 'li')
|
||||
require('markdown-link', 'mdlink')
|
||||
require('minimist')
|
||||
require('mixin-deep', 'merge')
|
||||
require('object.pick', 'pick')
|
||||
require('remarkable', 'Remarkable')
|
||||
require('repeat-string', 'repeat')
|
||||
require('strip-color')
|
||||
require = fn
|
||||
|
||||
/**
|
||||
* Get the "title" from a markdown link
|
||||
*/
|
||||
|
||||
utils.getTitle = function (str) {
|
||||
if (/^\[[^\]]+\]\(/.test(str)) {
|
||||
var m = /^\[([^\]]+)\]/.exec(str)
|
||||
if (m) return m[1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugify the url part of a markdown link.
|
||||
*
|
||||
* @name options.slugify
|
||||
* @param {String} `str` The string to slugify
|
||||
* @param {Object} `options` Pass a custom slugify function on `options.slugify`
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
utils.slugify = function (str, options) {
|
||||
options = options || {}
|
||||
if (options.slugify === false) return str
|
||||
if (typeof options.slugify === 'function') {
|
||||
return options.slugify(str, options)
|
||||
}
|
||||
|
||||
str = utils.getTitle(str)
|
||||
str = utils.stripColor(str)
|
||||
str = str.toLowerCase()
|
||||
|
||||
// `.split()` is often (but not always) faster than `.replace()`
|
||||
str = str.split(' ').join('-')
|
||||
str = str.split(/\t/).join('--')
|
||||
if (options.stripHeadingTags !== false) {
|
||||
str = str.split(/<\/?[^>]+>/).join('')
|
||||
}
|
||||
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
|
||||
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
|
||||
str = replaceDiacritics(str)
|
||||
if (options.num) {
|
||||
str += '-' + options.num
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function replaceDiacritics(str) {
|
||||
return str.replace(/[À-ž]/g, function (ch) {
|
||||
return diacritics[ch] || ch
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose `utils` modules
|
||||
*/
|
||||
|
||||
utils.concat = concat
|
||||
utils.matter = matter
|
||||
utils.li = li
|
||||
utils.mdlink = mdlink
|
||||
utils.minimist = minimist
|
||||
utils.merge = merge
|
||||
utils.pick = pick
|
||||
utils.Remarkable = Remarkable
|
||||
utils.repeat = repeat
|
||||
// utils.color = strip-color
|
||||
module.exports = utils
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "markdown-toc",
|
||||
"description": "Generate a markdown TOC (table of contents) with Remarkable.",
|
||||
"version": "1.2.0",
|
||||
"homepage": "https://github.com/jonschlinkert/markdown-toc",
|
||||
"author": "Jon Schlinkert (https://github.com/jonschlinkert)",
|
||||
"contributors": [
|
||||
"=^._.^= (http://maxogden.com)",
|
||||
"Andrés (http://angrykoala.github.io)",
|
||||
"Benjamin Schmitz (https://github.com/Vortex375)",
|
||||
"Christian Raunitschka (raunitschka.de)",
|
||||
"Daniel Chen (http://chendaniely.github.io)",
|
||||
"Daniel Mietchen (http://about.me/daniel.mietchen)",
|
||||
"David Mohl (https://dvcrn.github.io)",
|
||||
"Federico Soave (https://github.com/Feder1co5oave)",
|
||||
"Gary Green (https://github.com/garygreen)",
|
||||
"Jon Schlinkert (http://twitter.com/jonschlinkert)",
|
||||
"Josh Duff (https://tehshrike.github.io)",
|
||||
"Matt Ellis (http://sticklebackplastic.com)",
|
||||
"Nicolas Morel (https://github.com/Marsup)",
|
||||
"Rafael Steil (http://rafaelsteil.com)",
|
||||
"Richard Bradley (https://github.com/RichardBradley)",
|
||||
"Seth Vincent (https://sethvincent.com)",
|
||||
"Stefan Walther (http://qliksite.io)",
|
||||
"Tao Wang (https://github.com/twang2218)",
|
||||
"(https://github.com/lu22do)",
|
||||
"Zeke Sikelianos (http://zeke.sikelianos.com)"
|
||||
],
|
||||
"repository": "jonschlinkert/markdown-toc",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jonschlinkert/markdown-toc/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"cli.js",
|
||||
"index.js",
|
||||
"lib"
|
||||
],
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"markdown-toc": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"concat-stream": "^1.5.2",
|
||||
"diacritics-map": "^0.1.0",
|
||||
"gray-matter": "^2.1.0",
|
||||
"lazy-cache": "^2.0.2",
|
||||
"list-item": "^1.1.1",
|
||||
"markdown-link": "^0.1.1",
|
||||
"minimist": "^1.2.0",
|
||||
"mixin-deep": "^1.1.3",
|
||||
"object.pick": "^1.2.0",
|
||||
"remarkable": "^1.7.1",
|
||||
"repeat-string": "^1.6.1",
|
||||
"strip-color": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp-format-md": "^0.1.11",
|
||||
"mocha": "^3.2.0"
|
||||
},
|
||||
"keywords": [
|
||||
"anchor",
|
||||
"commonmark",
|
||||
"docs",
|
||||
"document",
|
||||
"documentation",
|
||||
"glossary",
|
||||
"heading",
|
||||
"index",
|
||||
"links",
|
||||
"markdown",
|
||||
"md",
|
||||
"plugin",
|
||||
"readme",
|
||||
"reference",
|
||||
"remarkable",
|
||||
"remarkableplugin",
|
||||
"render",
|
||||
"renderer",
|
||||
"table",
|
||||
"table of contents",
|
||||
"table-of-contents",
|
||||
"toc",
|
||||
"write"
|
||||
],
|
||||
"verb": {
|
||||
"run": true,
|
||||
"toc": true,
|
||||
"layout": "default",
|
||||
"tasks": [
|
||||
"readme"
|
||||
],
|
||||
"plugins": [
|
||||
"gulp-format-md"
|
||||
],
|
||||
"related": {
|
||||
"list": [
|
||||
"gfm-code-blocks",
|
||||
"markdown-link",
|
||||
"markdown-utils",
|
||||
"pretty-remarkable",
|
||||
"remarkable"
|
||||
]
|
||||
},
|
||||
"reflinks": [
|
||||
"gfm-code-blocks",
|
||||
"markdown-link",
|
||||
"markdown-utils",
|
||||
"pretty-remarkable",
|
||||
"remarkable",
|
||||
"verb",
|
||||
"verb-readme-generator"
|
||||
],
|
||||
"lint": {
|
||||
"reflinks": true
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"test": "test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import React from 'react'
|
||||
import { TsDoc } from '~/generator/legacy/definitions'
|
||||
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
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 ''
|
||||
|
||||
const paramDefinitions: TsDoc.TypeDefinition[] = functionDeclaration.signatures[0].parameters // PMC: seems flaky.. why the [0]?
|
||||
if (!paramDefinitions) return ''
|
||||
|
||||
// const paramsComments: TsDoc.CommentTag = tsDefinition.comment?.tags?.filter(x => x.tag == 'param')
|
||||
let parameters = paramDefinitions.map((x) => recurseThroughParams(x)).join(`\n`)
|
||||
return methodListGroup(parameters)
|
||||
}
|
||||
|
||||
function recurseThroughParams(paramDefinition: TsDoc.TypeDefinition) {
|
||||
// If this is a reference to another Param, let's use the reference instead
|
||||
let param = isDereferenced(paramDefinition) ? paramDefinition.type?.dereferenced : paramDefinition
|
||||
const labelParams = generateLabelParam(param)
|
||||
let subContent = ''
|
||||
|
||||
let children = param?.children
|
||||
if (param.type?.declaration?.children) {
|
||||
children = param.type?.declaration?.children
|
||||
} else if (isUnion(param)) {
|
||||
// We don't want to show the union types if it's a literal
|
||||
const nonLiteralVariants = param.type.types.filter(({ type }) => type !== 'literal')
|
||||
|
||||
if (nonLiteralVariants.length === 0) {
|
||||
children = null
|
||||
} else {
|
||||
children = nonLiteralVariants
|
||||
}
|
||||
} else if (param.type === 'reflection') {
|
||||
children = param.declaration.children
|
||||
}
|
||||
|
||||
if (!!children) {
|
||||
let 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))
|
||||
|
||||
let heading = `<h5 class="method-list-title method-list-title-isChild expanded">Properties</h5>`
|
||||
subContent = methodListGroup([heading].concat(properties).join('\n'))
|
||||
return methodListItemLabel(labelParams, subContent)
|
||||
}
|
||||
return methodListItemLabel(labelParams, subContent)
|
||||
}
|
||||
|
||||
const isDereferenced = (paramDefinition: TsDoc.TypeDefinition) => {
|
||||
return paramDefinition.type?.type == 'reference' && paramDefinition.type?.dereferenced?.id
|
||||
}
|
||||
|
||||
const isUnion = (paramDefinition: TsDoc.TypeDefinition) => {
|
||||
return paramDefinition.type?.type == 'union'
|
||||
}
|
||||
|
||||
const mergeUnion = (paramDefinition: TsDoc.TypeDefinition) => {
|
||||
const joined = paramDefinition.type.types.reduce((acc, x) => {
|
||||
acc.push(...(x.declaration?.children || []))
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return uniqBy(joined, 'name')
|
||||
}
|
||||
|
||||
const methodListGroup = (items) => `
|
||||
<ul className="method-list-group">
|
||||
${items}
|
||||
</ul>
|
||||
`
|
||||
|
||||
const methodListItemLabel = ({ name, isOptional, type, description }, subContent) => `
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
${name}
|
||||
</span>
|
||||
<span className="method-list-item-label-badge ${!isOptional && 'required'}">
|
||||
${isOptional ? 'optional' : 'required'}
|
||||
</span>
|
||||
<span className="method-list-item-validation">
|
||||
${type}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="method-list-item-description">
|
||||
|
||||
${description ? description : 'No description provided. '}
|
||||
|
||||
</div>
|
||||
${subContent}
|
||||
</li>
|
||||
`
|
||||
|
||||
function generateLabelParam(param: any) {
|
||||
let labelParams = {}
|
||||
if (typeof param.type === 'string' && param.type === 'literal') {
|
||||
labelParams = {
|
||||
name: param.name ?? param.value,
|
||||
isOptional: !!param.flags?.isOptional,
|
||||
type: param.type,
|
||||
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
||||
}
|
||||
} else {
|
||||
labelParams = {
|
||||
name: param.name ?? extractParamTypeAsString(param),
|
||||
isOptional: !!param.flags?.isOptional,
|
||||
type: extractParamTypeAsString(param),
|
||||
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
||||
}
|
||||
}
|
||||
return labelParams
|
||||
}
|
||||
|
||||
function extractParamTypeAsString(paramDefinition) {
|
||||
if (paramDefinition.type?.name) {
|
||||
return `<code>${paramDefinition.type.name}</code>`
|
||||
} else if (paramDefinition.type?.type == 'union') {
|
||||
return paramDefinition.type.types
|
||||
.map((x) =>
|
||||
x.value
|
||||
? `<code>${x.value}</code>`
|
||||
: x.name
|
||||
? `<code>${x.name}</code>`
|
||||
: x.type
|
||||
? `<code>${x.type}</code>`
|
||||
: ''
|
||||
)
|
||||
.join(' | ')
|
||||
}
|
||||
|
||||
return '<code>object</code>'
|
||||
}
|
||||
|
||||
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,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
@@ -8,9 +8,9 @@ import theme from 'shiki/themes/nord.json' assert { type: 'json' }
|
||||
import { remarkCodeHike } from '@code-hike/mdx'
|
||||
|
||||
import withTM from 'next-transpile-modules'
|
||||
// import withYaml from 'next-plugin-yaml'
|
||||
import withYaml from 'next-plugin-yaml'
|
||||
// import admonitions from 'remark-admonitions'
|
||||
// import codeHikeTheme from './codeHikeTheme.js'
|
||||
import codeHikeTheme from './codeHikeTheme.js'
|
||||
|
||||
/**
|
||||
* Rewrites and redirects are handled by
|
||||
@@ -26,8 +26,9 @@ const withMDX = nextMdx({
|
||||
[
|
||||
remarkCodeHike,
|
||||
{
|
||||
theme: theme,
|
||||
theme: codeHikeTheme,
|
||||
autoImport: true,
|
||||
lineNumbers: true,
|
||||
},
|
||||
],
|
||||
remarkGfm,
|
||||
@@ -46,14 +47,14 @@ 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',
|
||||
}
|
||||
|
||||
// next.config.js
|
||||
export default () => {
|
||||
// const plugins = [withMDX]/
|
||||
const plugins = [withTM(['ui', 'common']), withMDX]
|
||||
const plugins = [withTM(['ui', 'common']), withMDX, withYaml]
|
||||
return plugins.reduce((acc, next) => next(acc), nextConfig)
|
||||
}
|
||||
@@ -53,6 +53,7 @@
|
||||
"next": "12.1.0",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-mdx-remote": "^3.0.8",
|
||||
"next-mdx-toc": "^0.1.3",
|
||||
"next-plugin-yaml": "^1.0.1",
|
||||
"next-themes": "0.0.15",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
@@ -64,10 +65,12 @@
|
||||
"react-syntax-highlighter": "^15.3.1",
|
||||
"remark": "^14.0.2",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"remark-autolink-headings": "^7.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.0",
|
||||
"remark-mdx": "^2.0.0-rc.2",
|
||||
"remark-prism": "^1.3.6",
|
||||
"remark-slug": "^7.0.1",
|
||||
"ui": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,7 +8,8 @@ import '../styles/main.scss?v=1.0.0'
|
||||
import '../styles/docsearch.scss'
|
||||
import '../styles/algolia-search.scss'
|
||||
import '../styles/prism-okaidia.scss'
|
||||
import '@code-hike/mdx/dist/index.css'
|
||||
import '../styles/ch.scss'
|
||||
import '../styles/new-docs.scss'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -2,21 +2,25 @@ import { useEffect, useRef, useState } from 'react'
|
||||
// pages/index.js
|
||||
|
||||
import fs from 'fs'
|
||||
|
||||
import matter from 'gray-matter'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import components from '~/components/index'
|
||||
import { getAllDocs } from '~/lib/docs'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
// @ts-ignore
|
||||
import jsTypeSpec from '~/../../spec/enrichments/tsdoc_v2/combined.json'
|
||||
import jsSpec from '~/../../spec/supabase_js_v2_temp.yaml'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
|
||||
import { Tabs } from '~/../../packages/ui'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers'
|
||||
import assert from 'assert'
|
||||
|
||||
const marginTop = 256
|
||||
export default function Ref(props) {
|
||||
@@ -43,7 +47,7 @@ export default function Ref(props) {
|
||||
}
|
||||
})
|
||||
|
||||
console.log('allSections', allSections)
|
||||
// console.log('allSections', allSections)
|
||||
setSections(allSections)
|
||||
}, [])
|
||||
|
||||
@@ -141,29 +145,40 @@ export default function Ref(props) {
|
||||
{props.docs.map((x) => {
|
||||
const sectionRef = useRef(null)
|
||||
|
||||
// console.log('x', jsSpec.pages[x.id])
|
||||
const hasTsRef = jsSpec.pages[x.id]['$ref'] || null
|
||||
// console.log('hasTsRef', hasTsRef)
|
||||
// console.log('jsTypeSpec', jsTypeSpec)
|
||||
const tsDefinition = hasTsRef && extractTsDocNode(hasTsRef, jsTypeSpec)
|
||||
// console.log('tsDefinition', tsDefinition)
|
||||
console.log(`tsDefinition for ${x.title ?? x.id}`, tsDefinition)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
// console.log('entries', entries)
|
||||
const entry = entries[0]
|
||||
|
||||
console.log(
|
||||
x.id,
|
||||
'intersectiong',
|
||||
entry.isIntersecting,
|
||||
'visible',
|
||||
entry.isVisible
|
||||
)
|
||||
// console.log(
|
||||
// x.id,
|
||||
// 'intersectiong',
|
||||
// entry.isIntersecting,
|
||||
// 'visible',
|
||||
// entry.isVisible
|
||||
// )
|
||||
})
|
||||
// console.log('myRef', myRef.current)
|
||||
observer.observe(sectionRef.current)
|
||||
}, [])
|
||||
|
||||
const parameters = hasTsRef ? generateParameters(tsDefinition) : ''
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 pb-32 ref-container" id={x.id} ref={sectionRef}>
|
||||
<div className="prose" key={x.id}>
|
||||
<h1 className="text-3xl not-prose" onClick={() => updateUrl(x.id)}>
|
||||
{x.title ?? x.id}
|
||||
</h1>
|
||||
|
||||
{x.content && (
|
||||
<div className="prose">
|
||||
<MDXRemote {...x.content} components={components} />
|
||||
@@ -174,6 +189,9 @@ export default function Ref(props) {
|
||||
<ReactMarkdown>{jsSpec.pages[x.id].notes}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* // parameters */}
|
||||
{parameters && <div dangerouslySetInnerHTML={{ __html: parameters }}></div>}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{jsSpec.pages[x.id].examples && (
|
||||
|
||||
@@ -6,37 +6,26 @@ import QuickstartIntro from '~/components/MDX/quickstart_intro.mdx'
|
||||
export const TabPanel = Tabs.Panel
|
||||
export const meta = {
|
||||
author: 'Rich Haines',
|
||||
title: 'Account management app with NextJS',
|
||||
}
|
||||
|
||||
We are going to create a simple app using NextJS
|
||||
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### Video Guide
|
||||
The sourcecode for this tutorial is available on [GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/0Fs96oZ4se0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
### GitHub
|
||||
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
---
|
||||
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
---
|
||||
|
||||
Let's start building the Next.js app from scratch.
|
||||
# Install packages and nextjs
|
||||
|
||||
### Initialize a Next.js app
|
||||
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize
|
||||
an app called `supabase-nextjs`:
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -62,13 +51,14 @@ cd supabase-nextjs
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Then install the Supabase client library [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env.local`.
|
||||
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=.env.local
|
||||
@@ -79,9 +69,56 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
And one optional step is to update the CSS file `styles/globals.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css).
|
||||
|
||||
### Set up a Login component
|
||||
---
|
||||
|
||||
#### Supabase Auth Helpers
|
||||
<CH.Scrollycoding>
|
||||
|
||||
# Setting up login
|
||||
|
||||
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
|
||||
|
||||
It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible.
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```js app.js focus=1:3
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
---
|
||||
|
||||
## Using the Context Provider
|
||||
|
||||
### Import the NextJS Auth Helpers
|
||||
|
||||
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
|
||||
|
||||
@@ -89,50 +126,57 @@ It can be challenging to authenticate your users in all these different environm
|
||||
|
||||
Install the auth helpers for React and Next.js
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
|
||||
```
|
||||
<CH.Code>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx title=pages/_app.js
|
||||
import '../styles/globals.css'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx lines=2,8 title=pages/_app.tsx
|
||||
```js app.js
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({
|
||||
export default function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
---
|
||||
|
||||
### Wrap the APP in the provider
|
||||
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```js app.js focus=12:16,18:19
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
@@ -149,74 +193,99 @@ function MyApp({
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript.
|
||||
</CH.Code>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
---
|
||||
|
||||
#### Supabase Auth UI
|
||||
## Set up Auth UI
|
||||
|
||||
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
Install the Supabase Auth UI for React
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-ui-react
|
||||
```
|
||||
|
||||
Add the `Auth` component to your home page
|
||||
|
||||
```jsx title=pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
```jsx pages/index.js
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<p>Account page will go here.</p>
|
||||
)}
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
{/* //.. the rest of the default NextJS app index page.. */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
### Account page
|
||||
---
|
||||
|
||||
### Import Auth UI components and Supabase client
|
||||
|
||||
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
```jsx pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Home() {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Add conditional to show/hide account page
|
||||
|
||||
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
```jsx pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Home() {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return !session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<p>Account page will go here.</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Set up account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
## Create a new component
|
||||
|
||||
Let's create a new component for that called `Account.js` within a `components` folder.
|
||||
Let's create a new component for that called `Account.tsx` within a `components` folder.
|
||||
|
||||
```tsx title=components/Account.js
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
```tsx components/Account.tsx
|
||||
export default function Account() {}
|
||||
```
|
||||
|
||||
export default function Account({ session }) {
|
||||
const supabase = useSupabaseClient()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [website, setWebsite] = useState(null)
|
||||
const [avatar_url, setAvatarUrl] = useState(null)
|
||||
---
|
||||
|
||||
## Set up user context
|
||||
|
||||
adsds
|
||||
|
||||
```tsx components/Account.tsx
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Account() {
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
@@ -224,6 +293,7 @@ export default function Account({ session }) {
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
@@ -246,78 +316,17 @@ export default function Account({ session }) {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const user = useUser()
|
||||
}
|
||||
|
||||
async function updateProfile({ username, website, avatar_url }) {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
</CH.Scrollycoding>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
Let's create a new component for that called `Account.tsx` within a `components` folder.
|
||||
|
||||
@@ -444,9 +453,6 @@ export default function Account({ session }: { session: Session }) {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `pages/index.js`:
|
||||
@@ -722,6 +728,141 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
<CH.Scrollycoding>
|
||||
|
||||
## Step 1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing something about points, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
|
||||
> Nova in illis at dabat legi harundine non, ova miratur? _Quid in_ sole aer
|
||||
> ad diffusa illis voluisti fidensque coniugiale laniata curam. Aras rivus
|
||||
> eripuit, qua fistula haec partus; serpens, negat.
|
||||
|
||||
Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.
|
||||
|
||||
```js app.js focus=3:10
|
||||
const { lorem, ipsum } = dolor({
|
||||
sit: {
|
||||
amet: 1,
|
||||
consectetur: 2,
|
||||
adipiscing: (elit) => ({
|
||||
sed: elit,
|
||||
}),
|
||||
eiusmod: (tempor) => ({
|
||||
incididunt: tempor,
|
||||
}),
|
||||
ut: (labore) => ({
|
||||
et: labore,
|
||||
dolore: labore + 1,
|
||||
}),
|
||||
magna: (aliqua) => ({
|
||||
ut: aliqua,
|
||||
}),
|
||||
nostrud: (elit) => ({
|
||||
exercitation: elit,
|
||||
ullamco: elit,
|
||||
}),
|
||||
laboris: (elit) => ({
|
||||
nisi: elit,
|
||||
}),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2
|
||||
|
||||
Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in.
|
||||
|
||||
Praesent elementum facilisis leo vel fringilla est ullamcorper eget.
|
||||
|
||||
Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.
|
||||
|
||||
Morbi quis commodo.
|
||||
|
||||
```js app.js focus=11:17
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3
|
||||
|
||||
Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu.
|
||||
|
||||
- Nisi lacus sed viverra tellus in
|
||||
- Nibh cras pulvinar mattis nunc sed
|
||||
- Luctus accumsan tortor posuere ac
|
||||
|
||||
Ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.
|
||||
|
||||
```js app.js focus=11:14
|
||||
const { lorem, ipsum } = dolor({
|
||||
sit: {
|
||||
amet: 1,
|
||||
consectetur: 2,
|
||||
adipiscing: (elit) => ({
|
||||
sed: elit,
|
||||
}),
|
||||
eiusmod: (tempor) => ({
|
||||
incididunt: tempor,
|
||||
}),
|
||||
ut: (labore) => ({
|
||||
et: lorem(labore * ipsum),
|
||||
dolore: lorem(labore + 1),
|
||||
}),
|
||||
nostrud: (elit) => ({
|
||||
exercitation: elit,
|
||||
ullamco: elit,
|
||||
}),
|
||||
laboris: (elit) => ({
|
||||
nisi: elit,
|
||||
}),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4
|
||||
|
||||
Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt.
|
||||
|
||||
Sed blandit libero volutpat sed cras.
|
||||
|
||||
- Nisi lacus sed viverra tellus in
|
||||
- Nibh cras pulvinar mattis nunc sed
|
||||
|
||||
Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae.
|
||||
|
||||
```js app.js focus=15:21
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5
|
||||
|
||||
Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in.
|
||||
|
||||
Praesent elementum facilisis leo vel fringilla est ullamcorper eget.
|
||||
|
||||
Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat.
|
||||
|
||||
Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra.
|
||||
|
||||
- Fringilla ut morbi tincidunt augue interdum velit euismod.
|
||||
- Luctus accumsan tortor posuere ac ut consequat semper viverra.
|
||||
|
||||
Morbi quis commodo.
|
||||
|
||||
```js app.js
|
||||
|
||||
```
|
||||
|
||||
</CH.Scrollycoding>
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
import ScrollyCodingLayout from '~/layouts/tutorials/ScrollyCodingLayout'
|
||||
import { Tabs } from 'ui'
|
||||
import ProjectSetup from '~/components/MDX/project_setup.mdx'
|
||||
import QuickstartIntro from '~/components/MDX/quickstart_intro.mdx'
|
||||
|
||||
export const TabPanel = Tabs.Panel
|
||||
export const meta = {
|
||||
author: 'Rich Haines',
|
||||
title: 'Account management app with NextJS',
|
||||
}
|
||||
|
||||
1. First item sadsds dasdas dasd asdasasds
|
||||
ad sdasd assdasdas dasd asd ad First item sadsds dasdas dasd asdasasds
|
||||
ad sdasd assdasdas dasd asd ad
|
||||
|
||||
First item sadsds dasdas dasd asdasasds First item sadsds dasdas dasd asdasasds
|
||||
|
||||
2. Second item
|
||||
3. Third item
|
||||
4. Fourth item
|
||||
|
||||
---
|
||||
|
||||
1. First item
|
||||
1. Second item
|
||||
1. Third item
|
||||
1. Fourth item
|
||||
|
||||
---
|
||||
|
||||
1. First item
|
||||
8. Second item
|
||||
3. Third item
|
||||
5. Fourth item
|
||||
|
||||
---
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
1. Indented item
|
||||
2. Indented item
|
||||
4. Fourth item
|
||||
|
||||
We are going to create a simple app using NextJS
|
||||
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
The sourcecode for this tutorial is available on [GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
|
||||
---
|
||||
|
||||
<ProjectSetup />
|
||||
|
||||
---
|
||||
|
||||
# Install packages and nextjs
|
||||
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --ts --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
Then install the Supabase client library [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env.local`.
|
||||
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash .env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `styles/globals.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css).
|
||||
|
||||
---
|
||||
|
||||
<CH.Scrollycoding>
|
||||
|
||||
# Setting up login
|
||||
|
||||
Lets walkthrough the steps involved here
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```jsx lines=2,8 pages/_app.tsx
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
```jsx pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<p>Account page will go here.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
```tsx components/Account.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState<Profiles['username']>(null)
|
||||
const [website, setWebsite] = useState<Profiles['website']>(null)
|
||||
const [avatar_url, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: Profiles['username']
|
||||
website: Profiles['website']
|
||||
avatar_url: Profiles['avatar_url']
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
---
|
||||
|
||||
## lets talk about this
|
||||
|
||||
ad adsdasd da sds dasdas d adsadsa das d asdas d asd asd asdas
|
||||
|
||||
```jsx pages/_app.tsx focus=4:7 mark=4:7
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## and talk about this
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
ad adsdasd da sds dasdas d adsadsa das d asdas d asd asd asdas
|
||||
|
||||
```jsx pages/_app.tsx focus=8:12 mark=8:12
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Setting up account page
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
ad adsdasd da sds dasdas d adsadsa das d asdas d asd asd asdas
|
||||
|
||||
Copy this file and paste it somewhere
|
||||
|
||||
```jsx components/Account.tsx
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## oooo this is cool
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
ad adsdasd da sds dasdas d adsadsa das d asdas d asd asd asdas
|
||||
|
||||
```jsx components/Account.tsx focus=8:12 mark=8:12
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## hmmm this is interesting
|
||||
|
||||
ad adsdasd da sds dasdas d adsadsa das d asdas d asd asd asdas
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
```jsx components/Account.tsx focus=21:24,34:35 mark=21:24,34:35
|
||||
|
||||
```
|
||||
|
||||
</CH.Scrollycoding>
|
||||
|
||||
export default ({ children }) => <ScrollyCodingLayout meta={meta}>{children}</ScrollyCodingLayout>
|
||||
@@ -0,0 +1,726 @@
|
||||
import TutorialLayout2 from '~/layouts/tutorials/TutorialLayout2'
|
||||
|
||||
import { Tabs } from 'ui'
|
||||
|
||||
export const meta = {
|
||||
title: 'Creating a user management dashboard with NextJS and Supabase',
|
||||
author: 'Rich Haines',
|
||||
video: 'https://www.youtube-nocookie.com/embed/0Fs96oZ4se0',
|
||||
}
|
||||
|
||||
In this 25-minute guide, we’ll be building a collaborative user managment app using NextJS and Supabase.
|
||||
As users add and move rectangles in a canvas, changes will be automatically synced and persisted, allowing for a canvas that updates in real-time across clients.
|
||||
Users will also be able to see other users selections, and undo and redo actions.
|
||||
|
||||

|
||||
|
||||
### GitHub
|
||||
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
|
||||
## Building the App
|
||||
|
||||
Let's start building the Next.js app from scratch.
|
||||
|
||||
### Initialize a Next.js app
|
||||
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize
|
||||
an app called `supabase-nextjs`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --ts --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env.local`.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=.env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `styles/globals.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css).
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
#### Supabase Auth Helpers
|
||||
|
||||
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
|
||||
|
||||
It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible.
|
||||
|
||||
Install the auth helpers for React and Next.js
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx title=pages/_app.js
|
||||
import '../styles/globals.css'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx lines=2,8 title=pages/_app.tsx
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript.
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
#### Supabase Auth UI
|
||||
|
||||
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
Install the Supabase Auth UI for React
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-ui-react
|
||||
```
|
||||
|
||||
Add the `Auth` component to your home page
|
||||
|
||||
```jsx title=pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<p>Account page will go here.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
Let's create a new component for that called `Account.js` within a `components` folder.
|
||||
|
||||
```tsx title=components/Account.js
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Account({ session }) {
|
||||
const supabase = useSupabaseClient()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [website, setWebsite] = useState(null)
|
||||
const [avatar_url, setAvatarUrl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({ username, website, avatar_url }) {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
Let's create a new component for that called `Account.tsx` within a `components` folder.
|
||||
|
||||
```tsx title=components/Account.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState<Profiles['username']>(null)
|
||||
const [website, setWebsite] = useState<Profiles['website']>(null)
|
||||
const [avatar_url, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: Profiles['username']
|
||||
website: Profiles['website']
|
||||
avatar_url: Profiles['avatar_url']
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `pages/index.js`:
|
||||
|
||||
```jsx lines=3,14 title=pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import Account from '../components/Account'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<Account session={session} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
|
||||
photos and videos.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar widget for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=components/Avatar.js
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Avatar({ uid, url, size, onUpload }) {
|
||||
const supabase = useSupabaseClient()
|
||||
const [avatarUrl, setAvatarUrl] = useState(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
async function downloadImage(path) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (event) => {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${uid}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file, { upsert: true })
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert('Error uploading avatar!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar no-image" style={{ height: size, width: size }} />
|
||||
)}
|
||||
<div style={{ width: size }}>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=components/Avatar.tsx
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Avatar({
|
||||
uid,
|
||||
url,
|
||||
size,
|
||||
onUpload,
|
||||
}: {
|
||||
uid: string
|
||||
url: Profiles['avatar_url']
|
||||
size: number
|
||||
onUpload: (url: string) => void
|
||||
}) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (event) => {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${uid}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file, { upsert: true })
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert('Error uploading avatar!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar no-image" style={{ height: size, width: size }} />
|
||||
)}
|
||||
<div style={{ width: size }}>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```jsx title=components/Account.js
|
||||
// Import the new component
|
||||
import Avatar from './Avatar'
|
||||
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
{/* Add to the body */}
|
||||
<Avatar
|
||||
uid={user.id}
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
}}
|
||||
/>
|
||||
{/* ... */}
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel.
|
||||
- Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui).
|
||||
- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs).
|
||||
- Explore the [Supabase Cache Helpers](https://github.com/psteinroe/supabase-cache-helpers).
|
||||
- See the [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) template on GitHub.
|
||||
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
|
||||
- Sign in: [app.supabase.com](https://app.supabase.com)
|
||||
|
||||
export default ({ children }) => <TutorialLayout2 meta={meta} children={children} />
|
||||
@@ -0,0 +1,604 @@
|
||||
.ch-terminal {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #1e1e1e;
|
||||
color: #fafafa;
|
||||
overflow: hidden;
|
||||
padding: 0 8px 8px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
// font-family: Ubuntu, Droid Sans, -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
|
||||
// sans-serif;
|
||||
}
|
||||
.ch-terminal-container .ch-frame-content {
|
||||
background-color: inherit;
|
||||
}
|
||||
.ch-terminal-prompt {
|
||||
color: #8fa2db;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.ch-terminal-content {
|
||||
margin: 0;
|
||||
}
|
||||
.ch-terminal-output {
|
||||
opacity: 0.66;
|
||||
}
|
||||
.ch-code-line-number {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
padding-right: 1.5ch;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ch-code-scroll-parent {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
letter-spacing: 0;
|
||||
border-radius: 0;
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
padding: 0;
|
||||
box-sizing: content-box;
|
||||
border: none;
|
||||
}
|
||||
.ch-code-scroll-parent ::-moz-selection {
|
||||
background-color: var(--ch-selection-background);
|
||||
color: inherit;
|
||||
}
|
||||
.ch-code-scroll-parent ::selection {
|
||||
background-color: var(--ch-selection-background);
|
||||
color: inherit;
|
||||
}
|
||||
.ch-code-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ch-no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
.ch-expand-dialog {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 900px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.ch-expand-dialog::-webkit-backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.ch-expand-dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.ch-expand-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
.ch-expand-dialog-content {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
inset: 40px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid;
|
||||
}
|
||||
.ch-code-browser {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
// font-family: 'Courier New', Courier, monospace;
|
||||
// font-family: Ubuntu, Droid Sans, -apple-system;
|
||||
}
|
||||
.ch-code-browser-sidebar {
|
||||
border-right: 1px solid;
|
||||
min-width: 100px;
|
||||
padding: 1em 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.ch-code-browser-content {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 1em;
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1.2rem;
|
||||
letter-spacing: 0;
|
||||
position: relative;
|
||||
}
|
||||
.ch-code-browser-content ::-moz-selection {
|
||||
background-color: var(--ch-selection-background);
|
||||
color: inherit;
|
||||
}
|
||||
.ch-code-browser-content ::selection {
|
||||
background-color: var(--ch-selection-background);
|
||||
color: inherit;
|
||||
}
|
||||
.ch-code-browser-sidebar-file,
|
||||
.ch-code-browser-sidebar-folder {
|
||||
padding: 0.1em 1em;
|
||||
}
|
||||
.ch-code-browser-sidebar-file {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ch-code-browser-sidebar-file:hover {
|
||||
background-color: var(--ch-hover-background);
|
||||
color: var(--ch-hover-foreground);
|
||||
}
|
||||
.ch-code-browser-button {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
min-width: 1.5em;
|
||||
min-height: 1.5em;
|
||||
position: absolute;
|
||||
right: 0.8em;
|
||||
top: 0.8em;
|
||||
}
|
||||
.ch-editor-tab {
|
||||
border-right: 1px solid #252526;
|
||||
min-width: -webkit-fit-content;
|
||||
min-width: -moz-fit-content;
|
||||
min-width: fit-content;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
background-color: #2d2d2d;
|
||||
color: hsla(0, 0%, 100%, 0.5);
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid;
|
||||
|
||||
font-family: 'Office Code Pro';
|
||||
}
|
||||
.ch-editor-tab-active {
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.ch-editor-tab > div {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.4em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ch-editor-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: Ubuntu, Droid Sans, -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
|
||||
sans-serif;
|
||||
-moz-column-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
--ch-title-bar-height: 40px;
|
||||
height: 100%;
|
||||
// border: 1px solid red;
|
||||
}
|
||||
.ch-editor-frame .ch-frame-title-bar {
|
||||
background: none;
|
||||
}
|
||||
.ch-editor-terminal {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: #1e1e1e;
|
||||
color: #e7e7e7;
|
||||
border-top: 1px solid hsla(0, 0%, 50.2%, 0.35);
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ch-editor-terminal-tab {
|
||||
text-transform: uppercase;
|
||||
padding: 4px 10px 3px;
|
||||
font-size: 11px;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
}
|
||||
.ch-editor-terminal-tab > span {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
}
|
||||
.ch-editor-terminal-content {
|
||||
margin-top: 8px;
|
||||
height: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ch-editor-terminal-content .ch-terminal {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.ch-editor-button {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
min-width: 1.5em;
|
||||
min-height: 1.5em;
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
.ch-frame {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ch-frame,
|
||||
.ch-simple-frame {
|
||||
font-family: Ubuntu, Droid Sans, -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
|
||||
sans-serif;
|
||||
-moz-column-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
--ch-title-bar-height: 30px;
|
||||
}
|
||||
.ch-simple-frame {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25), 0 8px 16px -8px rgba(0, 0, 0, 0.3),
|
||||
0 -6px 16px -6px rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
.ch-frame-content {
|
||||
background-color: var(--ch-content-background, #fafafa);
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.ch-frame-zoom {
|
||||
--ch-frame-zoom: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: calc(100% / var(--ch-frame-zoom));
|
||||
height: calc(100% / var(--ch-frame-zoom));
|
||||
transform: scale(var(--ch-frame-zoom));
|
||||
transform-origin: left top;
|
||||
}
|
||||
.ch-frame-title-bar {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
height: var(--ch-title-bar-height);
|
||||
min-height: var(--ch-title-bar-height);
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--ch-content-background, #252526);
|
||||
color: #ebebed;
|
||||
position: relative;
|
||||
}
|
||||
.ch-frame-middle-bar {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.ch-frame-left-bar,
|
||||
.ch-frame-right-bar {
|
||||
flex-grow: 1;
|
||||
flex-basis: 1em;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
}
|
||||
.ch-frame-buttons {
|
||||
margin: 0 0.8em;
|
||||
flex-shrink: 0;
|
||||
height: 1em;
|
||||
width: 4.16em;
|
||||
// display: flex;
|
||||
display: none; // @mildtomato
|
||||
}
|
||||
.ch-frame-button {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 0.08em solid;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ch-frame-button-space {
|
||||
width: 0.56em;
|
||||
}
|
||||
.ch-frame-button-left {
|
||||
border-color: #ce5347;
|
||||
background-color: #ed6b60;
|
||||
}
|
||||
.ch-frame-button-middle {
|
||||
border-color: #d6a243;
|
||||
background-color: #f5be4f;
|
||||
}
|
||||
.ch-frame-button-right {
|
||||
border-color: #58a942;
|
||||
background-color: #62c554;
|
||||
}
|
||||
.ch-mini-browser {
|
||||
height: 100%;
|
||||
}
|
||||
.ch-mini-browser .ch-frame-content iframe,
|
||||
.ch-mini-browser .ch-frame-content video {
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.ch-mini-browser .ch-frame-title-bar input {
|
||||
height: 1.4em;
|
||||
font-size: 1em;
|
||||
border-radius: 0.5em;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
color: #544;
|
||||
min-width: 5px;
|
||||
width: 5px;
|
||||
}
|
||||
.ch-browser-button {
|
||||
margin: 0 1em;
|
||||
color: #999;
|
||||
}
|
||||
.ch-browser-back-button {
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
.ch-browser-forward-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.ch-browser-open-button {
|
||||
color: inherit;
|
||||
}
|
||||
.ch-browser-open-icon {
|
||||
display: block;
|
||||
}
|
||||
.ch-spotlight {
|
||||
display: flex;
|
||||
gap: 1.1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.ch-spotlight-tabs {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.ch-spotlight-tab {
|
||||
border-radius: 0.25rem;
|
||||
margin: 0 -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid #e3e3e3;
|
||||
}
|
||||
.ch-spotlight-tab:hover {
|
||||
border-color: red;
|
||||
}
|
||||
.ch-spotlight-tab[data-selected] {
|
||||
border-color: red;
|
||||
}
|
||||
.ch-spotlight-sticker {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 10vh;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
width: 420px;
|
||||
min-height: min(100%, 80vh);
|
||||
max-height: 80vh;
|
||||
}
|
||||
.ch-spotlight-sticker .ch-codeblock,
|
||||
.ch-spotlight-sticker .ch-codegroup {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: min(100%, 80vh);
|
||||
max-height: 80vh;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.ch-spotlight-with-preview .ch-spotlight-sticker {
|
||||
height: 80vh;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.ch-spotlight-with-preview .ch-spotlight-sticker .ch-codeblock,
|
||||
.ch-spotlight-with-preview .ch-spotlight-sticker .ch-codegroup {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.ch-spotlight-with-preview .ch-spotlight-preview {
|
||||
height: 280px;
|
||||
}
|
||||
.ch-scrollycoding {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 1rem 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
.ch-scrollycoding-content {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
.ch-scrollycoding-step-content {
|
||||
border-radius: 8px;
|
||||
margin: 0 -0.5rem;
|
||||
padding: 1rem 0.5rem;
|
||||
border: 1.5px solid transparent;
|
||||
min-height: 180px;
|
||||
}
|
||||
.ch-scrollycoding-step-content[data-selected] {
|
||||
border-color: #0070f3;
|
||||
}
|
||||
.ch-scrollycoding-step-content > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.ch-scrollycoding-step-content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ch-scrollycoding-sticker {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 10vh;
|
||||
display: flex;
|
||||
align-self: start;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
width: var(--ch-scrollycoding-sticker-width, 420px);
|
||||
max-height: 80vh;
|
||||
}
|
||||
.ch-scrollycoding-with-preview .ch-scrollycoding-sticker {
|
||||
height: 80vh;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.ch-scrollycoding-with-preview .ch-scrollycoding-sticker .ch-codeblock,
|
||||
.ch-scrollycoding-with-preview .ch-scrollycoding-sticker .ch-codegroup {
|
||||
flex: 1;
|
||||
}
|
||||
.ch-scrollycoding-with-preview .ch-scrollycoding-preview {
|
||||
height: 280px;
|
||||
}
|
||||
.ch-scrollycoding-sticker .ch-codeblock,
|
||||
.ch-scrollycoding-sticker .ch-codegroup {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: var(--ch-scrollycoding-code-min-height, 200px);
|
||||
max-height: 80vh;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ch-scrollycoding-static .ch-preview {
|
||||
height: 150px;
|
||||
}
|
||||
.ch-slideshow {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.ch-slideshow-slide {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
.ch-slideshow-slide .ch-codeblock,
|
||||
.ch-slideshow-slide .ch-codegroup {
|
||||
flex: 2;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
height: auto;
|
||||
}
|
||||
.ch-slideshow .ch-slideshow-preview {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.ch-slideshow-range {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.ch-slideshow-range input {
|
||||
flex: 1;
|
||||
}
|
||||
.ch-slideshow-notes {
|
||||
border-radius: 0.25rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e3e3e3;
|
||||
}
|
||||
.ch-slideshow-note {
|
||||
min-height: 140px;
|
||||
max-height: 140px;
|
||||
padding: 0.05px;
|
||||
overflow: auto;
|
||||
}
|
||||
.ch-codeblock,
|
||||
.ch-codegroup,
|
||||
.ch-preview {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: -webkit-max-content;
|
||||
height: -moz-max-content;
|
||||
height: max-content;
|
||||
box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25), 0 8px 16px -8px rgba(0, 0, 0, 0.3),
|
||||
0 -6px 16px -6px rgba(0, 0, 0, 0.025);
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.ch-codeblock > *,
|
||||
.ch-codegroup > *,
|
||||
.ch-preview > * {
|
||||
height: 100%;
|
||||
max-height: inherit;
|
||||
min-height: inherit;
|
||||
}
|
||||
.ch-codeblock,
|
||||
.ch-codegroup {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
border: 1px solid var(--colors-scale5); // @mildtomato
|
||||
}
|
||||
.ch-inline-code > code {
|
||||
padding: 0.2em 0.15em;
|
||||
margin: 0.1em -0.05em;
|
||||
border-radius: 0.25em;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.ch-section-link,
|
||||
.ch-section-link * {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration-style: dotted;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-thickness: 1px;
|
||||
-webkit-text-decoration-color: var(--ch-code-foreground, currentColor);
|
||||
text-decoration-color: var(--ch-code-foreground, currentColor);
|
||||
}
|
||||
.ch-section-link[data-active='true'] {
|
||||
background-color: rgba(186, 230, 253, 0.4);
|
||||
}
|
||||
.ch-section-link[data-active='true'],
|
||||
.ch-section-link[data-active='true'] * {
|
||||
text-decoration-thickness: 1.5px;
|
||||
}
|
||||
.ch-code-inline-mark {
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.2rem 0.15rem 0.1rem;
|
||||
margin: 0 -0.15rem;
|
||||
}
|
||||
.ch-code-multiline-mark-border {
|
||||
background: var(--colors-scale12) !important;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.ch-code-inline-link,
|
||||
.ch-code-link :not(span) > span {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration-style: dotted;
|
||||
text-decoration-style: dotted;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -34,9 +34,9 @@ body {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
// a {
|
||||
// text-decoration: none !important;
|
||||
// }
|
||||
|
||||
article h1 {
|
||||
margin-bottom: 2rem !important;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// overwrite for typography
|
||||
|
||||
// .prose ul {
|
||||
// li {
|
||||
// position: relative;
|
||||
// }
|
||||
// li::before {
|
||||
// position: absolute;
|
||||
// top: 0.75rem;
|
||||
// left: -1rem;
|
||||
// height: 0.125rem;
|
||||
// width: 0.5rem;
|
||||
// border-radius: 0.25rem;
|
||||
// background-color: var(--colors-scale9);
|
||||
// opacity: 0.6;
|
||||
// content: '';
|
||||
// }
|
||||
// }
|
||||
|
||||
// article {
|
||||
// background: yellow !important;
|
||||
// }
|
||||
Generated
+1698
-1232
File diff suppressed because it is too large.
Load diff
@@ -93,6 +93,47 @@ module.exports = ui({
|
||||
padding: 0,
|
||||
marginBottom: '32px',
|
||||
},
|
||||
ul: {
|
||||
listStyleType: 'none',
|
||||
paddingLeft: '1rem',
|
||||
},
|
||||
'ul li': {
|
||||
position: 'relative',
|
||||
},
|
||||
'ul li::before': {
|
||||
position: 'absolute',
|
||||
top: '0.75rem',
|
||||
left: '-1rem',
|
||||
height: '0.125rem',
|
||||
width: '0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'var(--colors-scale7)',
|
||||
content: '""',
|
||||
},
|
||||
ol: {
|
||||
paddingLeft: '1rem',
|
||||
counterReset: 'item',
|
||||
listStyleType: 'none',
|
||||
},
|
||||
'ol li': { display: 'block', position: 'relative', paddingLeft: '1rem' },
|
||||
'ol li::before': {
|
||||
position: 'absolute',
|
||||
top: '0.25rem',
|
||||
left: '-1rem',
|
||||
height: '1.2rem',
|
||||
width: '1.2rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'var(--colors-scale3)',
|
||||
border: '1px solid var(--colors-scale5)',
|
||||
content: 'counter(item) " "',
|
||||
counterIncrement: 'item',
|
||||
fontSize: '12px',
|
||||
color: 'var(--colors-scale9)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
'p img': {
|
||||
border: '1px solid var(--colors-scale4)',
|
||||
borderRadius: '4px',
|
||||
@@ -114,14 +155,17 @@ module.exports = ui({
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
a: {
|
||||
transition: 'box-shadow 0.1s ease-in-out',
|
||||
position: 'relative',
|
||||
transition: 'color 0.3s ease-in-out',
|
||||
paddingBottom: '2px',
|
||||
textDecoration: 'none',
|
||||
boxShadow: "theme('colors.brand[900]') 0px -3px 0px -1px inset",
|
||||
color: 'var(--colors-scale12)',
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: 'var(--colors-brand7)',
|
||||
textDecorationThickness: '1px',
|
||||
textUnderlineOffset: '4px',
|
||||
},
|
||||
'a:hover': {
|
||||
boxShadow: "inset 0 -30px 0 -1px theme('colors.brand[900]')",
|
||||
color: 'var(--tw-prose-headings)',
|
||||
textDecorationColor: 'var(--colors-brand9)',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -246,7 +290,7 @@ module.exports = ui({
|
||||
// },
|
||||
fontFamily: {
|
||||
sans: ['custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
mono: ['Source Code Pro', 'Menlo', 'monospace'],
|
||||
mono: ['Office Code Pro', 'Source Code Pro', 'Menlo', 'monospace'],
|
||||
},
|
||||
// stroke: (theme) => ({
|
||||
// white: theme('colors.white'),
|
||||
|
||||
@@ -50,7 +50,7 @@ function Tabs({
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
const hash = router.asPath?.split('#')[1]?.toUpperCase()
|
||||
const hash = router?.asPath?.split('#')[1]?.toUpperCase()
|
||||
|
||||
let __styles = styleHandler('tabs')
|
||||
|
||||
|
||||
Reference in new issue
Block a user