add codehike, some basic parameters to ref docs, and some other stuff

This commit is contained in:
Jonathan Summers-Muir committed 2022-11-10 01:15:59 +08:00
1 parent 7dde56af72
commit c107c5df19
28 files changed
+5539 -1482

No files matched your search

+372
View File
@@ -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',
},
},
],
}
+4 -4
View File
@@ -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.
+29 -16
View File
@@ -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>
</>
)
}
+1 -1
View File
@@ -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
@@ -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
+21
View File
@@ -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.
+359
View File
@@ -0,0 +1,359 @@
# markdown-toc [![NPM version](https://img.shields.io/npm/v/markdown-toc.svg?style=flat)](https://www.npmjs.com/package/markdown-toc) [![NPM monthly downloads](https://img.shields.io/npm/dm/markdown-toc.svg?style=flat)](https://npmjs.org/package/markdown-toc) [![NPM total downloads](https://img.shields.io/npm/dt/markdown-toc.svg?style=flat)](https://npmjs.org/package/markdown-toc) [![Linux Build Status](https://img.shields.io/travis/jonschlinkert/markdown-toc.svg?style=flat&label=Travis)](https://travis-ci.org/jonschlinkert/markdown-toc) [![Windows Build Status](https://img.shields.io/appveyor/ci/jonschlinkert/markdown-toc.svg?style=flat&label=AppVeyor)](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 &lt;!-- toc --&gt;
(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._
+75
View File
@@ -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);
}
+255
View File
@@ -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
+67
View File
@@ -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();
}
+94
View File
@@ -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
+127
View File
@@ -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"
}
}
+178
View File
@@ -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,
// })
// })
// }
+7 -6
View File
@@ -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)
}
+3
View File
@@ -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": {
+2 -1
View File
@@ -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()
+28 -10
View File
@@ -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 && (
+321 -180
View File
@@ -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 />
![Supabase User Management example](/docs/img/user-management-demo.png)
### 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!
+352
View File
@@ -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 />
![Supabase User Management example](/docs/img/user-management-demo.png)
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>
+726
View File
@@ -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.
![Supabase User Management example](/docs/img/user-management-demo.png)
### 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} />
+604
View File
@@ -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;
}
+3 -3
View File
@@ -34,9 +34,9 @@ body {
color: #333 !important;
}
a {
text-decoration: none !important;
}
// a {
// text-decoration: none !important;
// }
article h1 {
margin-bottom: 2rem !important;
+22
View File
@@ -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;
// }
+1698 -1232
View File
File diff suppressed because it is too large. Load diff
+50 -6
View File
@@ -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'),
+1 -1
View File
@@ -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')