Merge branch 'master' of github.com:supabase/supabase
No files matched your search
@@ -89,13 +89,13 @@ export const highlightSelectedNavItem = (id: string) => {
|
||||
const navMenuItems = document.querySelectorAll<HTMLAnchorElement>('.function-link-item a')
|
||||
|
||||
// find any currently active items and remove them
|
||||
const currentActiveItems = document.querySelectorAll('.function-link-list .text-brand-900')
|
||||
currentActiveItems.forEach((item) => item.classList.remove('text-brand-900'))
|
||||
const currentActiveItems = document.querySelectorAll('.function-link-list .text-brand')
|
||||
currentActiveItems.forEach((item) => item.classList.remove('text-brand'))
|
||||
|
||||
// Add active class to the current item
|
||||
navMenuItems.forEach((item) => {
|
||||
if (item.href.split('/').at(-1) === id) {
|
||||
item.classList.add('text-brand-900')
|
||||
item.classList.add('text-brand')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -46,7 +46,7 @@ const GuidesTableOfContents: FC<Props> = ({ list }) => {
|
||||
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
||||
<a
|
||||
href={`#${formatSlug(item.link)}`}
|
||||
className="text-scale-1000 hover:text-brand-900 transition-colors"
|
||||
className="text-scale-1000 hover:text-brand transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -69,7 +69,7 @@ const HomePageCover = (props) => {
|
||||
<div className="md:max-w-xs xl:max-w-none">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<IconBackground>
|
||||
<IconPlay className="text-brand-1100 dark:text-brand-900 w-4" />
|
||||
<IconPlay className="text-brand-600 dark:text-brand w-4" strokeWidth={2} />
|
||||
</IconBackground>
|
||||
<h3 className="text-2xl m-0">Getting Started</h3>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function LinkCard({
|
||||
}) {
|
||||
return (
|
||||
<Link href={link || '#'}>
|
||||
<a className="border-scale-200 hover:border-brand-900 dark:bg-scale-400 m-4 w-full cursor-pointer rounded-md border p-4 shadow-sm hover:shadow-2xl lg:w-2/5">
|
||||
<a className="border-scale-200 hover:border-brand dark:bg-scale-400 m-4 w-full cursor-pointer rounded-md border p-4 shadow-sm hover:shadow-2xl lg:w-2/5">
|
||||
{description ? (
|
||||
<>
|
||||
<h4 className="m-0 mb-4 p-0 text-base font-semibold">{title}</h4>
|
||||
|
||||
@@ -29,11 +29,7 @@ const Footer = () => (
|
||||
{Icon && <Icon width={16} height={16} />}
|
||||
<p>{text}</p>
|
||||
<Link href={url} passHref>
|
||||
<a
|
||||
className="text-brand-900 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<a className="text-brand hover:underline" target="_blank" rel="noreferrer noopener">
|
||||
{ctaLabel}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -663,10 +663,23 @@ export const database: NavMenuConstant = {
|
||||
name: 'RUM: inverted index for full-text search',
|
||||
url: '/guides/database/extensions/rum',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Foreign Data Wrappers',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/extensions/wrappers/overview' },
|
||||
{ name: 'Connecting to Airtable', url: '/guides/database/extensions/wrappers/airtable' },
|
||||
{ name: 'Connecting to AWS S3', url: '/guides/database/extensions/wrappers/s3' },
|
||||
{ name: 'Connecting to BigQuery', url: '/guides/database/extensions/wrappers/bigquery' },
|
||||
{
|
||||
name: 'wrappers: 3rd Party Integrations',
|
||||
url: '/guides/database/extensions/wrappers',
|
||||
name: 'Connecting to ClickHouse',
|
||||
url: '/guides/database/extensions/wrappers/clickhouse',
|
||||
},
|
||||
{ name: 'Connecting to Firebase', url: '/guides/database/extensions/wrappers/firebase' },
|
||||
{ name: 'Connecting to Logflare', url: '/guides/database/extensions/wrappers/logflare' },
|
||||
{ name: 'Connecting to Stripe', url: '/guides/database/extensions/wrappers/stripe' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
|
||||
return (
|
||||
<li key={id} className="function-link-item text-scale-1000 leading-3">
|
||||
<Link href={`#${id}`} passHref>
|
||||
<a className="cursor-pointer transition text-scale-1000 text-sm hover:text-brand-900 flex gap-3">
|
||||
<a className="cursor-pointer transition text-scale-1000 text-sm hover:text-brand flex gap-3">
|
||||
{icon && <img className="w-3" src={`${router.basePath}${icon}`} />}
|
||||
{title}
|
||||
</a>
|
||||
@@ -100,7 +100,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
|
||||
<a
|
||||
className={[
|
||||
'flex items-center gap-1 text-xs group mb-3',
|
||||
'text-base transition-all duration-200 text-brand-900 hover:text-brand-1200 hover:cursor-pointer ',
|
||||
'text-base transition-all duration-200 text-brand hover:text-brand-600 hover:cursor-pointer ',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="relative w-2">
|
||||
|
||||
@@ -19,7 +19,7 @@ const HeaderLink = React.memo(function HeaderLink(props: {
|
||||
className={[
|
||||
' ',
|
||||
!props.title && 'capitalize',
|
||||
props.url === router.pathname ? 'text-brand-900' : 'hover:text-brand-900 text-scale-1200',
|
||||
props.url === router.pathname ? 'text-brand' : 'hover:text-brand text-scale-1200',
|
||||
].join(' ')}
|
||||
>
|
||||
{props.title ?? props.id}
|
||||
@@ -68,7 +68,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
|
||||
'flex items-center gap-2',
|
||||
'cursor-pointer transition text-sm',
|
||||
activeItem
|
||||
? 'text-brand-900 font-medium'
|
||||
? 'text-brand font-medium'
|
||||
: 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000',
|
||||
].join(' ')}
|
||||
parent={props.subItem.parent}
|
||||
@@ -97,8 +97,8 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
|
||||
className={[
|
||||
'cursor-pointer transition text-sm',
|
||||
subSubItem.url === router.pathname
|
||||
? 'text-brand-900'
|
||||
: 'hover:text-brand-900 text-scale-1000',
|
||||
? 'text-brand'
|
||||
: 'hover:text-brand text-scale-1000',
|
||||
].join(' ')}
|
||||
>
|
||||
{subSubItem.name}
|
||||
@@ -124,7 +124,7 @@ const ContentLink = React.memo(function ContentLink(props: any) {
|
||||
className={[
|
||||
'cursor-pointer transition text-sm',
|
||||
props.url === router.pathname
|
||||
? 'text-brand-900'
|
||||
? 'text-brand'
|
||||
: 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000',
|
||||
].join(' ')}
|
||||
>
|
||||
@@ -152,7 +152,7 @@ const Content = (props) => {
|
||||
<a
|
||||
className={[
|
||||
'flex items-center gap-1 text-xs group mb-3',
|
||||
'text-base transition-all duration-200 text-brand-900 hover:text-brand-1200 hover:cursor-pointer ',
|
||||
'text-base transition-all duration-200 text-brand hover:text-brand-600 hover:cursor-pointer ',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="relative w-2">
|
||||
@@ -166,7 +166,7 @@ const Content = (props) => {
|
||||
|
||||
<Link href={menu.url ?? ''} passHref>
|
||||
<a>
|
||||
<div className="flex items-center gap-3 my-3 text-brand-900">
|
||||
<div className="flex items-center gap-3 my-3 text-brand">
|
||||
<HomeMenuIconPicker icon={menu.icon} />
|
||||
<HeaderLink title={menu.title} url={menu.url} id={id} />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { deepFilterSections } from './NavigationMenu.utils'
|
||||
|
||||
const HeaderLink = React.memo(function HeaderLink(props: any) {
|
||||
return (
|
||||
<span className={['text-base text-brand-1200 ', !props.title && 'capitalize'].join(' ')}>
|
||||
<span className={['text-base text-brand-600 ', !props.title && 'capitalize'].join(' ')}>
|
||||
{props.title ?? props.id}
|
||||
</span>
|
||||
)
|
||||
@@ -64,7 +64,7 @@ const FunctionLink = React.memo(function FunctionLink({
|
||||
className={cn(
|
||||
'cursor-pointer transition text-sm hover:text-scale-1200 gap-3 relative',
|
||||
isParent ? 'flex justify-between' : 'leading-3',
|
||||
active ? 'text-brand-900' : 'text-scale-1000'
|
||||
active ? 'text-brand' : 'text-scale-1000'
|
||||
)}
|
||||
>
|
||||
{icon && <Image width={16} height={16} alt={icon} src={`${router.basePath}${icon}`} />}
|
||||
@@ -72,7 +72,7 @@ const FunctionLink = React.memo(function FunctionLink({
|
||||
{active && !isSubItem && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute -left-[13px] top-0 bottom-0 w-[1px] bg-brand-1000"
|
||||
className="absolute -left-[13px] top-0 bottom-0 w-[1px] bg-brand-600"
|
||||
></div>
|
||||
)}
|
||||
{isParent && (
|
||||
@@ -180,7 +180,7 @@ const NavigationMenuRefListItems = ({
|
||||
<a
|
||||
className={[
|
||||
'flex items-center gap-1 text-xs group mb-3',
|
||||
'text-base transition-all duration-200 text-scale-1100 hover:text-brand-1200 hover:cursor-pointer ',
|
||||
'text-base transition-all duration-200 text-scale-1100 hover:text-brand-600 hover:cursor-pointer ',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="relative w-2">
|
||||
|
||||
@@ -56,7 +56,7 @@ const TopNavBarRef: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="h-[60px] border-b backdrop-blur backdrop-filter bg-white-1200 dark:bg-scale-200/90">
|
||||
<nav className="h-[60px] border-b backdrop-blur backdrop-filter bg bg-opacity-75">
|
||||
<div className="px-5 max-w-7xl mx-auto flex gap-3 justify-between items-center h-full">
|
||||
<div className={['lg:hidden'].join(' ')}>
|
||||
<Link href="/">
|
||||
@@ -68,7 +68,7 @@ const TopNavBarRef: FC = () => {
|
||||
height={24}
|
||||
alt="Supabase Logo"
|
||||
/>
|
||||
<span className="font-mono text-sm font-medium text-brand-900">DOCS</span>
|
||||
<span className="font-mono text-sm font-medium text-brand">DOCS</span>
|
||||
</a>
|
||||
</Link>
|
||||
{/* {router.asPath.includes('/reference/') && <RefSwitcher />} */}
|
||||
@@ -82,21 +82,22 @@ const TopNavBarRef: FC = () => {
|
||||
group
|
||||
items-center
|
||||
justify-between
|
||||
bg-scaleA-200
|
||||
bg-surface-100
|
||||
bg-opacity-75
|
||||
hover:bg-surface-200
|
||||
hover:bg-opacity-100
|
||||
border
|
||||
transition
|
||||
hover:border-scale-600
|
||||
hover:bg-scaleA-300
|
||||
border-scale-500 pl-1.5 md:pl-3 pr-1.5 w-full h-[32px] rounded"
|
||||
border-scale-500 pl-1.5 md:pl-3 pr-1.5 w-full h-[32px] rounded
|
||||
text-lighter
|
||||
"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconSearch className="text-scale-1100" size={18} strokeWidth={2} />
|
||||
<p className="hidden md:flex text-scale-1100 text-sm group-hover:text-scale-1200 transition">
|
||||
Search docs...
|
||||
</p>
|
||||
<IconSearch className="" size={18} strokeWidth={2} />
|
||||
<p className="hidden md:flex text-sm">Search docs...</p>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
<div className="text-scale-1200 md:flex items-center justify-center h-5 w-10 border rounded bg-scale-500 border-scale-700 gap-1">
|
||||
<div className="md:flex items-center justify-center h-5 w-10 border rounded bg-surface-300 gap-1">
|
||||
<IconCommand size={12} strokeWidth={1.5} />
|
||||
<span className="text-[12px]">K</span>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
|
||||
'py-1.5 px-5 rounded text-sm transition',
|
||||
`${
|
||||
item.url === asPath
|
||||
? 'bg-scale-200 text-brand-900'
|
||||
? 'bg-scale-200 text-brand'
|
||||
: 'text-scale-1100 hover:text-scale-1200'
|
||||
}`,
|
||||
].join(' ')}
|
||||
@@ -109,7 +109,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="text-scale-1200 text-sm group-hover:text-brand-900 transition">
|
||||
<span className="text-scale-1200 text-sm group-hover:text-brand transition">
|
||||
{group.label}
|
||||
</span>
|
||||
</Accordion.Trigger>
|
||||
@@ -124,7 +124,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
|
||||
'py-1.5 px-5 rounded text-sm transition',
|
||||
`${
|
||||
section.url === asPath
|
||||
? 'bg-scale-200 text-brand-900'
|
||||
? 'bg-scale-200 text-brand'
|
||||
: 'text-scale-1100 hover:text-scale-1200'
|
||||
}`,
|
||||
].join(' ')}
|
||||
@@ -150,7 +150,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="text-scale-1200 text-sm group-hover:text-brand-900 transition">
|
||||
<span className="text-scale-1200 text-sm group-hover:text-brand transition">
|
||||
{section.name}
|
||||
</span>
|
||||
</Accordion.Trigger>
|
||||
@@ -164,7 +164,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
|
||||
'py-1.5 ml-4 px-5 rounded text-sm transition',
|
||||
`${
|
||||
item.url === asPath
|
||||
? 'bg-scale-200 text-brand-900'
|
||||
? 'bg-scale-200 text-brand'
|
||||
: 'text-scale-1100 hover:text-scale-1200'
|
||||
}`,
|
||||
].join(' ')}
|
||||
|
||||
@@ -27,7 +27,7 @@ const Pagination = ({ currentPage, totalCount }: { currentPage: number; totalCou
|
||||
<Link href={`/discussions?page=${page}`}>
|
||||
<a
|
||||
className={`border-scale-600 inline-flex h-8 w-8 items-center justify-center rounded border ${
|
||||
currentPage === page ? 'bg-brand-900' : 'bg-scale-300'
|
||||
currentPage === page ? 'bg-brand' : 'bg-scale-300'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
|
||||
@@ -69,7 +69,7 @@ const TableOfContents: FC<Props> = ({ toc, video }) => {
|
||||
<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"
|
||||
className="text-scale-1000 hover:text-brand transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -96,7 +96,7 @@ const CliCommandSection = (props) => {
|
||||
</div>
|
||||
<a
|
||||
href={`#${subcommand}`}
|
||||
className="transition text-scale-1100 hover:text-brand-900"
|
||||
className="transition text-scale-1100 hover:text-brand"
|
||||
>
|
||||
$ {subcommand.replace(/-/g, ' ')}
|
||||
</a>
|
||||
|
||||
@@ -27,7 +27,7 @@ const OldVersionAlert = ({ sections }: OldVersionAlertProps) => {
|
||||
You're viewing an older version of this library.
|
||||
<br />
|
||||
<Link href={latestVersionUrl}>
|
||||
<a className="underline decoration-brand-700 underline-offset-4 decoration-1">
|
||||
<a className="underline decoration-brand-400 underline-offset-4 decoration-1">
|
||||
Switch to the latest
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -174,7 +174,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
<div className="bg-scale-300 border rounded prose max-w-none">
|
||||
<div className="bg-scale-200 px-5 py-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="text-brand-900">
|
||||
<div className="text-brand">
|
||||
<IconDatabase size={16} />
|
||||
</div>
|
||||
<h5 className="text-xs text-scale-1200">
|
||||
|
||||
@@ -228,7 +228,7 @@ const HeaderLogo = memo(function HeaderLogo() {
|
||||
height={24}
|
||||
alt="Supabase Logo"
|
||||
/>
|
||||
<span className="font-mono text-sm font-medium text-brand-900">DOCS</span>
|
||||
<span className="font-mono text-sm font-medium text-brand">DOCS</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ const Layout: FC<Props> = (props) => {
|
||||
].join(' ')}
|
||||
>
|
||||
{props.meta.breadcrumb && (
|
||||
<p className="text-brand-900 tracking-wider mb-3">{props.meta.breadcrumb}</p>
|
||||
<p className="text-brand tracking-wider mb-3">{props.meta.breadcrumb}</p>
|
||||
)}
|
||||
<article
|
||||
ref={articleRef}
|
||||
|
||||
@@ -107,7 +107,7 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
|
||||
return (
|
||||
<div className={['flex items-center gap-3 not-prose', icon && 'mb-8'].join(' ')}>
|
||||
{icon && (
|
||||
<div className="w-8 h-8 bg-brand-500 rounded flex items-center justify-center">
|
||||
<div className="w-8 h-8 bg-brand-300 rounded flex items-center justify-center">
|
||||
<Image width={16} height={16} alt={icon} src={`${icon}.svg`} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -61,10 +61,10 @@ const Layout: FC<Props> = (props: Props) => {
|
||||
].join(' ')}
|
||||
>
|
||||
<div>
|
||||
<p className="text-brand-900 tracking-wider">Tutorials</p>
|
||||
<p className="text-brand tracking-wider">Tutorials</p>
|
||||
<article className={['prose dark:prose-dark ', 'max-w-none'].join(' ')}>
|
||||
<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>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-300 to-brand my-16"></div>
|
||||
|
||||
<MDXProvider components={components} children={props.children} />
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Content, Paragraph, Parent } from 'mdast'
|
||||
import { MdxJsxFlowElement } from 'mdast-util-mdx'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
/**
|
||||
* Transforms PyMdown Tabs to Supabase Tabs.
|
||||
*
|
||||
* https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/
|
||||
*/
|
||||
const remarkPyMdownTabs = function () {
|
||||
return function transformer(root: Parent) {
|
||||
visit(root, 'paragraph', (node: Paragraph, nodeIndex: number, parent: Parent) => {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i]
|
||||
if (child.type !== 'text') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for 3 '=', followed by an optionally quoted title,
|
||||
// followed by optional newlines of text
|
||||
const match = child.value.match(/^\n*=== ("?)(.+)\1\n?((?:.|\n)*)/)
|
||||
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the tab title along with the remaining text
|
||||
const [, , title, value] = match
|
||||
|
||||
// Rewrite the node's value to remove the tab syntax
|
||||
child.value = value
|
||||
|
||||
// Extract sibling nodes that should be linked to this tab
|
||||
const siblingsToNest = extractLinkedSiblings(parent, node, nodeIndex)
|
||||
|
||||
const children: any[] = [...node.children.slice(i), ...siblingsToNest]
|
||||
|
||||
const tabPanelElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TabPanel',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'id',
|
||||
value: title,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'label',
|
||||
value: title,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
|
||||
let nodesAdded = 0
|
||||
const previousNode = parent.children[nodeIndex - 1]
|
||||
|
||||
if (previousNode?.type === 'mdxJsxFlowElement' && previousNode.name === 'Tabs') {
|
||||
// Add TabPanel to existing Tabs component
|
||||
previousNode.children.push(tabPanelElement)
|
||||
|
||||
// Remove this node
|
||||
parent.children.splice(nodeIndex, 1)
|
||||
nodesAdded--
|
||||
} else {
|
||||
// Create new Tabs components and add TabPanel
|
||||
const tabsElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Tabs',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'scrollable',
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'size',
|
||||
value: 'small',
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: 'underlined',
|
||||
},
|
||||
],
|
||||
children: [tabPanelElement],
|
||||
}
|
||||
|
||||
// Overwrite this paragraph node with Tabs component
|
||||
parent.children.splice(nodeIndex, 1, tabsElement)
|
||||
}
|
||||
|
||||
// If this wasn't the first child of the paragraph, create
|
||||
// a new paragraph before this with the previous text children
|
||||
if (i > 0) {
|
||||
const previousChildren = node.children.slice(0, i)
|
||||
const paragraph: Paragraph = {
|
||||
type: 'paragraph',
|
||||
children: previousChildren,
|
||||
}
|
||||
parent.children.splice(nodeIndex, 0, paragraph)
|
||||
|
||||
nodesAdded++
|
||||
}
|
||||
|
||||
// Return the correct index for the next visit, since
|
||||
// we may have added or removed an element in the array
|
||||
return nodeIndex + nodesAdded
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies sibling nodes that should be linked to this admonition
|
||||
* based on their indent level (ie. 4 spaces).
|
||||
*
|
||||
* Iterates through proceeding siblings until one is found that is
|
||||
* not indented relative to the original node.
|
||||
*
|
||||
* Splices the discovered siblings out of the original parent and returns them.
|
||||
*/
|
||||
function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) {
|
||||
const { column } = node.position.start
|
||||
|
||||
let nextSibling: Content
|
||||
let i = index
|
||||
|
||||
do {
|
||||
nextSibling = parent.children[++i]
|
||||
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)
|
||||
|
||||
return parent.children.splice(index + 1, i - index - 1)
|
||||
}
|
||||
|
||||
export default remarkPyMdownTabs
|
||||
@@ -97,6 +97,7 @@
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-slug": "^7.0.1",
|
||||
"remark-emoji": "^3.1.2",
|
||||
"shiki": "^0.11.1",
|
||||
"sse.js": "^0.6.1",
|
||||
"ui": "*",
|
||||
|
||||
@@ -162,6 +162,8 @@ import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const code = requestUrl.searchParams.get('code')
|
||||
@@ -190,6 +192,8 @@ import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const code = requestUrl.searchParams.get('code')
|
||||
@@ -369,13 +373,9 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc
|
||||
|
||||
### Server-side
|
||||
|
||||
The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) can be used to trigger the authentication process from form submissions.
|
||||
The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) can be used to trigger the authentication process from form submissions.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Next.js [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) are currently in Alpha and may change. For now we recommend [triggering the authentication flow client-side](/docs/guides/auth/auth-helpers/nextjs#client-side) for production applications.
|
||||
|
||||
</Admonition>
|
||||
#### Sign Up Route
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -385,57 +385,248 @@ Next.js [Server Actions](https://nextjs.org/docs/app/building-your-application/d
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/login.js
|
||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
```jsx app/auth/sign-up.js
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export default async function Login() {
|
||||
const handleSignUp = async (formData) => {
|
||||
'use server'
|
||||
const email = formData.get('email')
|
||||
const password = formData.get('password')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const supabase = createServerActionClient({ cookies })
|
||||
await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
export async function POST(request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email')
|
||||
const password = formData.get('password')
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
|
||||
revalidatePath('/')
|
||||
}
|
||||
await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${requestUrl.origin}/auth/callback`,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSignIn = async (formData) => {
|
||||
'use server'
|
||||
const email = formData.get('email')
|
||||
const password = formData.get('password')
|
||||
return NextResponse.redirect(requestUrl.origin, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
const supabase = createServerActionClient({ cookies })
|
||||
await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
</TabPanel>
|
||||
|
||||
revalidatePath('/')
|
||||
}
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
const handleSignOut = async () => {
|
||||
'use server'
|
||||
const supabase = createServerActionClient({ cookies })
|
||||
await supabase.auth.signOut()
|
||||
revalidatePath('/')
|
||||
}
|
||||
```tsx app/auth/sign-up.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const formData = await request.formData()
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
const supabase = createRouteHandlerClient<Database>({ cookies })
|
||||
|
||||
await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${requestUrl.origin}/auth/callback`,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.redirect(requestUrl.origin, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Returning a `301` status redirects from a POST to a GET route
|
||||
|
||||
</Admonition>
|
||||
|
||||
#### Login Route
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/auth/login.js
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email')
|
||||
const password = formData.get('password')
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
|
||||
await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
return NextResponse.redirect(requestUrl.origin, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/auth/login.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const formData = await request.formData()
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
const supabase = createRouteHandlerClient<Database>({ cookies })
|
||||
|
||||
await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
return NextResponse.redirect(requestUrl.origin, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Returning a `301` status redirects from a POST to a GET route
|
||||
|
||||
</Admonition>
|
||||
|
||||
#### Logout Route
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/auth/logout.js
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
|
||||
await supabase.auth.signOut()
|
||||
|
||||
return NextResponse.redirect(`${requestUrl.origin}/login`, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/auth/logout.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const supabase = createRouteHandlerClient<Database>({ cookies })
|
||||
|
||||
await supabase.auth.signOut()
|
||||
|
||||
return NextResponse.redirect(`${requestUrl.origin}/login`, {
|
||||
status: 301,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
#### Login Page
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/login.jsx
|
||||
export default function Login() {
|
||||
return (
|
||||
<form action={handleSignUp}>
|
||||
<form action="/auth/login" method="post">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input name="email" />
|
||||
<label htmlFor="password">Password</label>
|
||||
<input type="password" name="password" />
|
||||
<button>Sign up</button>
|
||||
<button formAction={handleSignIn}>Sign in</button>
|
||||
<button formAction={handleSignOut}>Sign out</button>
|
||||
<button>Sign In</button>
|
||||
<button formAction="/auth/sign-up">Sign Up</button>
|
||||
<button formAction="/auth/logout">Sign Out</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -445,70 +636,21 @@ export default async function Login() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/login.ts
|
||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export default async function Login() {
|
||||
const handleSignUp = async (formData: FormData) => {
|
||||
'use server'
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
|
||||
const supabase = createServerActionClient<Database>({ cookies })
|
||||
await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
}
|
||||
|
||||
const handleSignIn = async (formData: FormData) => {
|
||||
'use server'
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
|
||||
const supabase = createServerActionClient<Database>({ cookies })
|
||||
await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
'use server'
|
||||
const supabase = createServerActionClient<Database>({ cookies })
|
||||
await supabase.auth.signOut()
|
||||
revalidatePath('/')
|
||||
}
|
||||
|
||||
```tsx app/login.tsx
|
||||
export default function Login() {
|
||||
return (
|
||||
<form action={handleSignUp}>
|
||||
<form action="/auth/login" method="post">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input name="email" />
|
||||
<label htmlFor="password">Password</label>
|
||||
<input type="password" name="password" />
|
||||
<button>Sign up</button>
|
||||
<button formAction={handleSignIn}>Sign in</button>
|
||||
<button formAction={handleSignOut}>Sign out</button>
|
||||
<button>Sign In</button>
|
||||
<button formAction="/auth/sign-up">Sign Up</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerActionClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -551,7 +693,7 @@ This allows for the Supabase client to be easily instantiated in the correct con
|
||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function Home() {
|
||||
export default function Page() {
|
||||
const [todos, setTodos] = useState()
|
||||
const supabase = createClientComponentClient()
|
||||
|
||||
@@ -582,7 +724,7 @@ import type { Database } from '@/lib/database.types'
|
||||
|
||||
type Todo = Database['public']['Tables']['todos']['Row']
|
||||
|
||||
export default function Home() {
|
||||
export default function Page() {
|
||||
const [todos, setTodos] = useState<Todo[] | null>(null)
|
||||
const supabase = createClientComponentClient<Database>()
|
||||
|
||||
@@ -653,7 +795,9 @@ In order to use Supabase in Server Components, you need to have implemented the
|
||||
import { cookies } from 'next/headers'
|
||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default async function Home() {
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page() {
|
||||
const supabase = createServerComponentClient({ cookies })
|
||||
const { data } = await supabase.from('todos').select()
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
@@ -670,6 +814,8 @@ import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createServerComponentClient<Database>({ cookies })
|
||||
const { data } = await supabase.from('todos').select()
|
||||
@@ -724,6 +870,8 @@ import { cookies } from 'next/headers'
|
||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function NewTodo() {
|
||||
const addTodo = async (formData) => {
|
||||
'use server'
|
||||
@@ -753,6 +901,8 @@ import { revalidatePath } from 'next/cache'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function NewTodo() {
|
||||
const addTodo = async (formData: FormData) => {
|
||||
'use server'
|
||||
@@ -801,11 +951,13 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/api/todos/route.jsx
|
||||
```jsx app/api/todos/route.js
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request) {
|
||||
const { title } = await request.json()
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
@@ -818,13 +970,15 @@ export async function POST(request) {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/api/todos/route.tsx
|
||||
```tsx app/api/todos/route.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { title } = await request.json()
|
||||
const supabase = createRouteHandlerClient<Database>({ cookies })
|
||||
@@ -846,6 +1000,258 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc
|
||||
|
||||
See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) above.
|
||||
|
||||
### Edge Runtime
|
||||
|
||||
The Next.js Edge Runtime allows you to host Server Components and Route Handlers from Edge nodes, serving the routes as close as possible to your user's location.
|
||||
|
||||
A route can be configured to use the Edge Runtime by exporting a `runtime` variable set to `edge`. Additionally, the `cookies()` function must be called from the Edge route, before creating a Supabase Client.
|
||||
|
||||
#### Server Components
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/page.jsx
|
||||
import { cookies } from 'next/headers'
|
||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export const runtime = 'edge'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
const supabase = createServerComponentClient({
|
||||
cookies: () => cookieStore,
|
||||
})
|
||||
|
||||
const { data } = await supabase.from('todos').select()
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/page.tsx
|
||||
import { cookies } from 'next/headers'
|
||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const runtime = 'edge'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
const supabase = createServerComponentClient<Database>({
|
||||
cookies: () => cookieStore,
|
||||
})
|
||||
|
||||
const { data } = await supabase.from('todos').select()
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerComponentClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
#### Route Handlers
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/api/todos/route.js
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export const runtime = 'edge'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request) {
|
||||
const { title } = await request.json()
|
||||
const cookieStore = cookies()
|
||||
|
||||
const supabase = createRouteHandlerClient({
|
||||
cookies: () => cookieStore,
|
||||
})
|
||||
|
||||
const { data } = await supabase.from('todos').insert({ title }).select()
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/api/todos/route.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export const runtime = 'edge'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { title } = await request.json()
|
||||
const cookieStore = cookies()
|
||||
|
||||
const supabase = createRouteHandlerClient<Database>({
|
||||
cookies: () => cookieStore,
|
||||
})
|
||||
|
||||
const { data } = await supabase.from('todos').insert({ title }).select()
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Static Routes
|
||||
|
||||
Server Components and Route Handlers are static by default - data is fetched once at build time and the value is cached. Since the request to Supabase now happens at build time, there is no user, session or cookie to pass along with the request to Supabase. Therefore, the `createClient` function from `supabase-js` can be used to fetch data for static routes.
|
||||
|
||||
#### Server Components
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/page.jsx
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export default async function Page() {
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
)
|
||||
|
||||
const { data } = await supabase.from('todos').select()
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/page.tsx
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export default async function Page() {
|
||||
const supabase = createClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
|
||||
const { data } = await supabase.from('todos').select()
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
#### Route Handlers
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx app/api/todos/route.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request) {
|
||||
const { title } = await request.json()
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
)
|
||||
|
||||
const { data } = await supabase.from('todos').insert({ title }).select()
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/api/todos/route.ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { title } = await request.json()
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
|
||||
const { data } = await supabase.from('todos').insert({ title }).select()
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## More examples
|
||||
|
||||
- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
|
||||
|
||||
@@ -11,6 +11,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
@@ -31,6 +32,13 @@ const pageMap = [
|
||||
},
|
||||
remoteFile: 'generating-types.md',
|
||||
},
|
||||
{
|
||||
slug: 'api',
|
||||
meta: {
|
||||
title: 'GraphQL API',
|
||||
},
|
||||
remoteFile: 'api.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface ActionDocsProps {
|
||||
@@ -116,6 +124,7 @@ export const getStaticProps: GetStaticProps<ActionDocsProps> = async ({ params }
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
|
||||
import { GetStaticPaths, GetStaticProps } from 'next'
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import emoji from 'remark-emoji'
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
|
||||
import components from '~/components'
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'wrappers'
|
||||
const branch = 'main'
|
||||
const docsDir = 'docs'
|
||||
const externalSite = 'https://supabase.github.io/wrappers'
|
||||
|
||||
// Each external docs page is mapped to a local page
|
||||
const pageMap = [
|
||||
{
|
||||
slug: 'airtable',
|
||||
meta: {
|
||||
title: 'Airtable',
|
||||
},
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
{
|
||||
slug: 'bigquery',
|
||||
meta: {
|
||||
title: 'BigQuery',
|
||||
},
|
||||
remoteFile: 'bigquery.md',
|
||||
},
|
||||
{
|
||||
slug: 'clickhouse',
|
||||
meta: {
|
||||
title: 'ClickHouse',
|
||||
},
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
{
|
||||
slug: 'firebase',
|
||||
meta: {
|
||||
title: 'Firebase',
|
||||
},
|
||||
remoteFile: 'firebase.md',
|
||||
},
|
||||
{
|
||||
slug: 'logflare',
|
||||
meta: {
|
||||
title: 'Logflare',
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'stripe',
|
||||
meta: {
|
||||
title: 'Stripe',
|
||||
},
|
||||
remoteFile: 'stripe.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface WrappersDocsProps {
|
||||
source: MDXRemoteSerializeResult
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function WrappersDocs({ source, meta }: WrappersDocsProps) {
|
||||
return (
|
||||
<Layout meta={meta}>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo and transform links
|
||||
*/
|
||||
export const getStaticProps: GetStaticProps<WrappersDocsProps> = async ({ params }) => {
|
||||
const page = pageMap.find(({ slug }) => slug === params.slug)
|
||||
|
||||
if (!page) {
|
||||
throw new Error(`No page mapping found for slug '${params.slug}'`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
)
|
||||
|
||||
const source = await response.text()
|
||||
|
||||
const urlTransform: UrlTransformFunction = (url) => {
|
||||
try {
|
||||
const externalSiteUrl = new URL(externalSite)
|
||||
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePage = (
|
||||
pathname.endsWith('.md')
|
||||
? pathname.replace(/\.md$/, '')
|
||||
: relative(externalSiteUrl.pathname, pathname)
|
||||
).replace(/^\//, '')
|
||||
|
||||
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
|
||||
|
||||
// If we have a mapping for this page, use the mapped path
|
||||
if (page) {
|
||||
return page.slug + hash
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to original docs
|
||||
return `${externalSite}/${relativePage}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
showCopyButton: true,
|
||||
skipLanguages: [],
|
||||
autoImport: false,
|
||||
}
|
||||
|
||||
const mdxSource = await serialize(source, {
|
||||
scope: {
|
||||
chCodeConfig: codeHikeOptions,
|
||||
},
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
emoji,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
return { props: { source: mdxSource, meta } }
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: pageMap.map(({ slug }) => ({
|
||||
params: {
|
||||
slug,
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'wrappers',
|
||||
title: 'wrappers: 3rd Party Integrations',
|
||||
title: 'Wrappers: 3rd Party Integrations',
|
||||
description: '3rd party integrations for PostgreSQL ',
|
||||
}
|
||||
|
||||
@@ -60,12 +60,13 @@ A web interface for connecting to external data is coming to Supabase Studio in
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Firebase](https://supabase.github.io/wrappers/firebase/)
|
||||
- [Stripe](https://supabase.github.io/wrappers/stripe/)
|
||||
- [BigQuery](https://supabase.github.io/wrappers/bigquery/)
|
||||
- [AWS S3](https://supabase.github.io/wrappers/s3/)
|
||||
- Airtable - Coming Soon
|
||||
- Clickhouse - Coming Soon
|
||||
- [Airtable](/docs/guides/database/extensions/wrappers/airtable)
|
||||
- [AWS S3](/docs/guides/database/extensions/wrappers/s3)
|
||||
- [BigQuery](/docs/guides/database/extensions/wrappers/bigquery)
|
||||
- [ClickHouse](/docs/guides/database/extensions/wrappers/clickhouse)
|
||||
- [Firebase](/docs/guides/database/extensions/wrappers/firebase)
|
||||
- [Logflare](/docs/guides/database/extensions/wrappers/logflare)
|
||||
- [Stripe](/docs/guides/database/extensions/wrappers/stripe)
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -73,3 +73,15 @@ max_parallel_workers |3 |
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
|
||||
## Pooler Config
|
||||
|
||||
You can also [customize some parameters](https://supabase.com/dashboard/project/_/settings/database) for the Connection Pooler:
|
||||
|
||||
1. [Pooling Mode](https://devcenter.heroku.com/articles/best-practices-pgbouncer-configuration#pgbouncer-s-connection-pooling-modes)
|
||||
1. [Default Pool Size](https://www.pgbouncer.org/config.html)
|
||||
1. [Max Client Connections](https://www.pgbouncer.org/config.html)
|
||||
|
||||
The default pool size, and the maximum number of clients allowed to connect concurrently is automatically optimized based on the compute add-on being used. At the moment, the Dashboard only reflects any custom configuration being used, and does not include the default optimized numbers used for your project.
|
||||
|
||||
Custom Pooler Config may also require manual updates to any relevant overrides when changing compute add-ons.
|
||||
@@ -48,7 +48,7 @@ We can use your JWT Secret to generate new `anon` and `service` API keys using t
|
||||
|
||||
### Update API Keys
|
||||
|
||||
Replace the values the `.env` file:
|
||||
Replace the values in the `.env` file:
|
||||
|
||||
- `ANON_KEY` - replace with an `anon` key
|
||||
- `SERVICE_ROLE_KEY` - replace with a `service` key
|
||||
|
||||
@@ -25,7 +25,7 @@ export const meta = {
|
||||
## Products
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-1100 [&_svg]dark:text-brand-900">
|
||||
<div className="grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-600 [&_svg]dark:text-brand">
|
||||
{products.map((product) => {
|
||||
return (
|
||||
<Link href={product.href} key={product.title} passHref>
|
||||
@@ -44,7 +44,7 @@ export const meta = {
|
||||
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
|
||||
|
||||
<div className="md:max-w-xs 2xl:max-w-none">
|
||||
<div className="flex items-center gap-3 mb-3 text-brand-1100 dark:text-brand-900">
|
||||
<div className="flex items-center gap-3 mb-3 text-brand-600 dark:text-brand">
|
||||
{/* <IconBackground>
|
||||
<HomeMenuIconPicker icon="reference-cli" width={18} height={18} />
|
||||
</IconBackground> */}
|
||||
@@ -89,7 +89,7 @@ export const meta = {
|
||||
<TextLink
|
||||
label="Explore more resources"
|
||||
url="/guides/resources"
|
||||
className="no-underline text-brand-900 text-sm"
|
||||
className="no-underline text-brand text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +148,7 @@ export const meta = {
|
||||
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
|
||||
|
||||
<div className="md:max-w-xs 2xl:max-w-none">
|
||||
<div className="flex items-center gap-3 mb-3 text-brand-1100 dark:text-brand-900">
|
||||
<div className="flex items-center gap-3 mb-3 text-brand-600 dark:text-brand">
|
||||
<IconBackground>
|
||||
<HomeMenuIconPicker icon="self-hosting" width={18} height={18} />
|
||||
</IconBackground>
|
||||
@@ -160,7 +160,7 @@ export const meta = {
|
||||
<TextLink
|
||||
label="More on Self-Hosting"
|
||||
url="/guides/self-hosting"
|
||||
className="no-underline text-brand-900 text-sm"
|
||||
className="no-underline text-brand text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function Config() {
|
||||
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
|
||||
<div className="w-full prose">
|
||||
<h1 className="">CLI configuration</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-300 to-brand my-8"></div>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
|
||||
@@ -169,7 +169,7 @@ pre[class*='language-'] {
|
||||
|
||||
// ToC styles
|
||||
.toc__menu-item--active {
|
||||
color: var(--colors-brand9) !important;
|
||||
color: hsl(var(--brand-default)) !important;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
|
||||
@@ -46,8 +46,6 @@ There will be a winner and a runner-up prize for each category. Every team membe
|
||||
|
||||
## Submission
|
||||
|
||||
Make sure you [register here](https://madewithsupabase.com/hackathons/launch-week-8).
|
||||
|
||||
You should submit your project using [this form](https://madewithsupabase.com/hackathons/launch-week-8) before 11:59 pm Sunday midnight PT 14th August 2023. Extra points if you include a simple video demoing the app in the description.
|
||||
|
||||
## Judges
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
---
|
||||
title: 'React Native file upload with Supabase Storage'
|
||||
description: Learn how to implement authentication and file upload in a React Native app.
|
||||
author: simon_grimm
|
||||
image: 2023-08-01-react-native-storage/react-native-storage.png
|
||||
thumb: 2023-08-01-react-native-storage/react-native-storage.png
|
||||
tags:
|
||||
- react-native
|
||||
- storage
|
||||
date: '2023-08-01'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
If you want to upload files from your React Native app, you need a backend to store the files, [Supabase Storage](https://supabase.com/docs/guides/storage) is a great choice for this as it provides a simple API to upload files, and we can easily combine this with authentication to build a powerful app.
|
||||
|
||||
This means you can quickly build your own image-sharing app, a file-sharing app, or any other app that needs to upload files to a backend!
|
||||
|
||||

|
||||
|
||||
In this tutorial you will learn to:
|
||||
|
||||
- Set up a React Native app with [Expo SDK49](https://expo.dev/)
|
||||
- Use [Supabase Authentication](https://supabase.com/docs/guides/auth)
|
||||
- Work with Expo Router v2 and protected routes
|
||||
- Upload files to [Supabase Storage](https://supabase.com/docs/guides/storage)
|
||||
|
||||
You can also directly check out the [full source code on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) so you can get started with Supabase fast!
|
||||
|
||||
Before we get into the app, let's quickly [start a new Supabase project](https://supabase.com/dashboard).
|
||||
|
||||
## Creating the Supabase Project
|
||||
|
||||
To use authentication and storage we need a new Supabase project. If you don't have a Supabase account yet, you can [get started for free](https://supabase.com/dashboard)!
|
||||
|
||||
In your dashboard, click "New Project" and leave it to the default settings, but make sure you keep a copy o your Database password!
|
||||
|
||||

|
||||
|
||||
After a minute your project should be ready, and we can configure the authentication and storage.
|
||||
|
||||
## Setting up Authentication
|
||||
|
||||
Authentication will be enabled by default, but we want to turn off email confirmation for this tutorial.
|
||||
|
||||
Select **Authentication** from the menu, go to the **Providers** section, and expand Email.
|
||||
|
||||

|
||||
|
||||
Here you can disable the confirmation email, and apply other changes later if you want to!
|
||||
|
||||
## Setting up Storage
|
||||
|
||||
Now we want to create a **bucket** under storage where we will upload our files, and also add some security rules to protect the files of a user.
|
||||
|
||||
First, select **Storage** from the menu, then click **New bucket** and call it `files`.
|
||||
|
||||

|
||||
|
||||
Make sure that this is not a public bucket, otherwise, even unauthenticated users can upload or read the files!
|
||||
|
||||
To protect that bucket and allow users only access to their own folder, we need to add some [Storage policies](https://supabase.com/docs/guides/storage/access-control).
|
||||
|
||||
You can either do this through the UI and pick from examples, or simply run my SQL script in the **SQL Editor** which you can select from the menu:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "Enable storage access for users based on user_id" ON "storage"."objects"
|
||||
AS PERMISSIVE FOR ALL
|
||||
TO public
|
||||
USING (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1])
|
||||
WITH CHECK (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1])
|
||||
```
|
||||
|
||||
This will allow users to only access their own folder, and not any other files in the bucket.
|
||||
|
||||
## Setting up the React Native app
|
||||
|
||||
Now that we have our Supabase project ready, we can start building the React Native app!
|
||||
|
||||
Get started by setting up a new Expo app with the tabs template and install some dependencies:
|
||||
|
||||
```sh
|
||||
# Create a new Expo app
|
||||
npx create-expo-app@latest cloudApp --template tabs@49
|
||||
|
||||
# Install dependencies
|
||||
npm i @supabase/supabase-js
|
||||
npm i react-native-url-polyfill base64-arraybuffer react-native-loading-spinner-overlay
|
||||
|
||||
# Install Expo packages
|
||||
npx expo install expo-secure-store
|
||||
npx expo install expo-image-picker
|
||||
npx expo install expo-file-system
|
||||
```
|
||||
|
||||
We will use the [Expo Secure Store](https://docs.expo.dev/versions/latest/sdk/securestore/) to store the Supabase session, and the [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to select images from the device. We also need the [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/) to read the image from the device and upload its data.
|
||||
|
||||
You can now already run your project with `npx expo` and then select a platform to run on.
|
||||
|
||||
However, the tabs template contains a lot of code that we don't need, so to simplify things we can remove the **app**, **constants** and **components** folder.
|
||||
|
||||
This gives us a much cleaner project structure.
|
||||
|
||||
## Connecting to Supabase from React Native
|
||||
|
||||
To use Supabase we need to initialize the client with our project URL and the public key, which you can find in the **Settings** of your project under **API**.
|
||||
|
||||
You can put both of them in a `.env` file at the root of your project:
|
||||
|
||||
```env
|
||||
EXPO_PUBLIC_SUPABASE_URL=
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=
|
||||
```
|
||||
|
||||
We can now simply read those values from the environment variables and initialize the Supabase client, so create a file at `config/initSupabase.ts` and add the following code:
|
||||
|
||||
```ts
|
||||
import * as SecureStore from 'expo-secure-store'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Use a custom secure storage solution for the Supabase client to store the JWT
|
||||
const ExpoSecureStoreAdapter = {
|
||||
getItem: (key: string) => {
|
||||
return SecureStore.getItemAsync(key)
|
||||
},
|
||||
setItem: (key: string, value: string) => {
|
||||
SecureStore.setItemAsync(key, value)
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
SecureStore.deleteItemAsync(key)
|
||||
},
|
||||
}
|
||||
|
||||
const url = process.env.EXPO_PUBLIC_SUPABASE_URL
|
||||
const key = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
// Initialize the Supabase client
|
||||
export const supabase = createClient(url, key, {
|
||||
auth: {
|
||||
storage: ExpoSecureStoreAdapter as any,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
We are using the SecureStore from Expo to handle the session of our Supabase client and add in the `createClient` function.
|
||||
|
||||
Later we can import the `supabase` client from this file and use it in our app whenever we need to access Supabase.
|
||||
|
||||
## Building the authentication flow
|
||||
|
||||
Currently, the app won't work as we have no entry point. Because we are using the Expon Router and file-based routing, we can create a new file at `app/index.tsx` which will be the first page that comes up in our app.
|
||||
|
||||
On this page we will handle both login and registration, so let's start by creating a simple form with a few inputs and buttons inside the `app/index.tsx` file:
|
||||
|
||||
```tsx
|
||||
import { Alert, View, Button, TextInput, StyleSheet, Text, TouchableOpacity } from 'react-native'
|
||||
import { useState } from 'react'
|
||||
import React from 'react'
|
||||
import Spinner from 'react-native-loading-spinner-overlay'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Sign in with email and password
|
||||
const onSignInPress = async () => {
|
||||
setLoading(true)
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
const onSignUpPress = async () => {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Spinner visible={loading} />
|
||||
|
||||
<Text style={styles.header}>My Cloud</Text>
|
||||
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="john@doe.com"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.inputField}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
style={styles.inputField}
|
||||
/>
|
||||
|
||||
<TouchableOpacity onPress={onSignInPress} style={styles.button}>
|
||||
<Text style={{ color: '#fff' }}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
<Button onPress={onSignUpPress} title="Create Account" color={'#fff'}></Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 200,
|
||||
padding: 20,
|
||||
backgroundColor: '#151515',
|
||||
},
|
||||
header: {
|
||||
fontSize: 30,
|
||||
textAlign: 'center',
|
||||
margin: 50,
|
||||
color: '#fff',
|
||||
},
|
||||
inputField: {
|
||||
marginVertical: 4,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: '#2b825b',
|
||||
borderRadius: 4,
|
||||
padding: 10,
|
||||
color: '#fff',
|
||||
backgroundColor: '#363636',
|
||||
},
|
||||
button: {
|
||||
marginVertical: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#2b825b',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
},
|
||||
})
|
||||
|
||||
export default Login
|
||||
```
|
||||
|
||||
There's nothing fancy going on here, but this is all we need to use Supabase Authentication in our app!
|
||||
|
||||

|
||||
|
||||
You can try it out right now and create a new user account or sign in with an existing one and log the values to the console to see what's going on.
|
||||
|
||||
However, we are not handling the authentication state yet so let's create a **Context** to listen to changes.
|
||||
|
||||
We will wrap a Provider around our app, which will use the `onAuthStateChange` function from Supabase to listen to changes in the authentication state and accordingly set our state.
|
||||
|
||||
For this, create a new file at `provider/AuthProvider.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect, createContext, PropsWithChildren } from 'react'
|
||||
import { Session, User } from '@supabase/supabase-js'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
|
||||
type AuthProps = {
|
||||
user: User | null
|
||||
session: Session | null
|
||||
initialized?: boolean
|
||||
signOut?: () => void
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<Partial<AuthProps>>({})
|
||||
|
||||
// Custom hook to read the context values
|
||||
export function useAuth() {
|
||||
return React.useContext(AuthContext)
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for changes to authentication state
|
||||
const { data } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
setSession(session)
|
||||
setUser(session ? session.user : null)
|
||||
setInitialized(true)
|
||||
})
|
||||
return () => {
|
||||
data.subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Log out the user
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut()
|
||||
}
|
||||
|
||||
const value = {
|
||||
user,
|
||||
session,
|
||||
initialized,
|
||||
signOut,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
To use the context we can now wrap it around our app, and while we do this we can also take care of the navigation:
|
||||
|
||||
In the topmost layout file we can check whether a user has an active session or not, and either directly sign the user into the inside area (that we will create soon) or automatically bring her back to the login screen.
|
||||
|
||||
To make this work with the Expo Router we can create a file at `app/_layout.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { Slot, useRouter, useSegments } from 'expo-router'
|
||||
import { useEffect } from 'react'
|
||||
import { AuthProvider, useAuth } from '../provider/AuthProvider'
|
||||
|
||||
// Makes sure the user is authenticated before accessing protected pages
|
||||
const InitialLayout = () => {
|
||||
const { session, initialized } = useAuth()
|
||||
const segments = useSegments()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) return
|
||||
|
||||
// Check if the path/url is in the (auth) group
|
||||
const inAuthGroup = segments[0] === '(auth)'
|
||||
|
||||
if (session && !inAuthGroup) {
|
||||
// Redirect authenticated users to the list page
|
||||
router.replace('/list')
|
||||
} else if (!session) {
|
||||
// Redirect unauthenticated users to the login page
|
||||
router.replace('/')
|
||||
}
|
||||
}, [session, initialized])
|
||||
|
||||
return <Slot />
|
||||
}
|
||||
|
||||
// Wrap the app with the AuthProvider
|
||||
const RootLayout = () => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<InitialLayout />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootLayout
|
||||
```
|
||||
|
||||
Whenever the `initialized` or `session` state changes, we check if the user is authenticated and redirect her to the correct page.
|
||||
|
||||
This also means we don't have to worry about the authentication state in our pages anymore, we can just assume that the user is authenticated and use the `useAuth` hook to access the user and session data later on.
|
||||
|
||||
Your app might show an error right now because the `/list` route doesn't exist yet, but we will create it in the next step.
|
||||
|
||||
## File Upload to Supabase Storage
|
||||
|
||||
Now that we have the authentication set up, we can start working on the file upload.
|
||||
|
||||
First, let's define another layout for this inside area so create a file at `/app/(auth)/_layout.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { Stack } from 'expo-router'
|
||||
import { useAuth } from '../../provider/AuthProvider'
|
||||
import React from 'react'
|
||||
import { TouchableOpacity } from 'react-native'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
|
||||
// Simple stack layout within the authenticated area
|
||||
const StackLayout = () => {
|
||||
const { signOut } = useAuth()
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: '#0f0f0f',
|
||||
},
|
||||
headerTintColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="list"
|
||||
options={{
|
||||
headerTitle: 'My Files',
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={signOut}>
|
||||
<Ionicons name="log-out-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
></Stack.Screen>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default StackLayout
|
||||
```
|
||||
|
||||
This defines a simple stack navigation and adds a button to trigger the logout, so we can now also fully test the authentication flow.
|
||||
|
||||
Next, we create the page for uploading and displaying all files of a user from Supabase Storage.
|
||||
|
||||
You won't have any files to show yet, but loading the files of a user is as easy as calling `list()` on the storage bucket and passing the user id as the folder name.
|
||||
|
||||
Additionally, we add a little FAB (floating action button) to trigger the file picker, so create a file at `/app/(auth)/list.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { useAuth } from '../../provider/AuthProvider'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import { decode } from 'base64-arraybuffer'
|
||||
import { supabase } from '../../config/initSupabase'
|
||||
import { FileObject } from '@supabase/storage-js'
|
||||
|
||||
const list = () => {
|
||||
const { user } = useAuth()
|
||||
const [files, setFiles] = useState<FileObject[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
// Load user images
|
||||
loadImages()
|
||||
}, [user])
|
||||
|
||||
const loadImages = async () => {
|
||||
const { data } = await supabase.storage.from('files').list(user!.id)
|
||||
if (data) {
|
||||
setFiles(data)
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectImage = async () => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* FAB to add images */}
|
||||
<TouchableOpacity onPress={onSelectImage} style={styles.fab}>
|
||||
<Ionicons name="camera-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
backgroundColor: '#151515',
|
||||
},
|
||||
fab: {
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 70,
|
||||
position: 'absolute',
|
||||
bottom: 40,
|
||||
right: 30,
|
||||
height: 70,
|
||||
backgroundColor: '#2b825b',
|
||||
borderRadius: 100,
|
||||
},
|
||||
})
|
||||
|
||||
export default list
|
||||
```
|
||||
|
||||
This should give us a nice and clean UI.
|
||||
|
||||

|
||||
|
||||
Now we can implement the image picker and upload the selected image to Supabase Storage.
|
||||
|
||||
Using the image picker from Expo gives us a **URI**, which we can use to read the file from the file system and convert it to a base64 string.
|
||||
|
||||
We can then use the `upload()` method from the storage client to upload the file to the storage bucket. Life can be easy.
|
||||
|
||||
At this point, you should be able to upload files to Supabase Storage, and you can already see them in your UI (or log them to the console).
|
||||
|
||||
To finally display them we will add a `ScrollView` component, which will render one item for every file of a user.
|
||||
|
||||
Let's start by creating those component rows in another file, so create a `components/ImageItem.tsx` file and add the following code:
|
||||
|
||||
```tsx
|
||||
import { FileObject } from '@supabase/storage-js'
|
||||
import { Image, View, Text, TouchableOpacity } from 'react-native'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
import { useState } from 'react'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
|
||||
// Image item component that displays the image from Supabase Storage and a delte button
|
||||
const ImageItem = ({
|
||||
item,
|
||||
userId,
|
||||
onRemoveImage,
|
||||
}: {
|
||||
item: FileObject
|
||||
userId: string
|
||||
onRemoveImage: () => void
|
||||
}) => {
|
||||
const [image, setImage] = useState<string>('')
|
||||
|
||||
supabase.storage
|
||||
.from('files')
|
||||
.download(`${userId}/${item.name}`)
|
||||
.then(({ data }) => {
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(data!)
|
||||
fr.onload = () => {
|
||||
setImage(fr.result as string)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', margin: 1, alignItems: 'center', gap: 5 }}>
|
||||
{image ? (
|
||||
<Image style={{ width: 80, height: 80 }} source={{ uri: image }} />
|
||||
) : (
|
||||
<View style={{ width: 80, height: 80, backgroundColor: '#1A1A1A' }} />
|
||||
)}
|
||||
<Text style={{ flex: 1, color: '#fff' }}>{item.name}</Text>
|
||||
{/* Delete image button */}
|
||||
<TouchableOpacity onPress={onRemoveImage}>
|
||||
<Ionicons name="trash-outline" size={20} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageItem
|
||||
```
|
||||
|
||||
This component will display the image from Supabase Storage, the name of the file and a delete button.
|
||||
|
||||
To display the image we use the `download()` method from the storage client, which returns a `FileObject` with the file data. We can then use the `FileReader` to convert the file data to a base64 string, which we can use as the image source.
|
||||
|
||||
Now let's use this component in our `list.tsx` file to render the list of images by updating the `return` statement:
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView>
|
||||
{files.map((item, index) => (
|
||||
<ImageItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
userId={user!.id}
|
||||
onRemoveImage={() => onRemoveImage(item, index)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* FAB to add images */}
|
||||
<TouchableOpacity onPress={onSelectImage} style={styles.fab}>
|
||||
<Ionicons name="camera-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
```
|
||||
|
||||
Don't forget to also include the import to the `ImageItem` component!
|
||||
|
||||

|
||||
|
||||
Finally, we can also include the delete functionality by adding the following code to the `list.tsx`:
|
||||
|
||||
```tsx
|
||||
const onRemoveImage = async (item: FileObject, listIndex: number) => {
|
||||
supabase.storage.from('files').remove([`${user!.id}/${item.name}`])
|
||||
const newFiles = [...files]
|
||||
newFiles.splice(listIndex, 1)
|
||||
setFiles(newFiles)
|
||||
}
|
||||
```
|
||||
|
||||
We are handling the deletion here so we can accordingly update the state of the files list after removing an item.
|
||||
|
||||
And with that in place, you have a fully functional image gallery app with React Native and Supabase Storage!
|
||||
|
||||
## What about resumable uploads?
|
||||
|
||||
Initially, I wanted to include [resumable uploads](https://supabase.com/blog/storage-v3-resumable-uploads) in this tutorial, but apparently, the [Uppy](https://uppy.io/) client didn't work 100% for React Native yet.
|
||||
|
||||
You can still see an [initial implementation of resumable downloads with Supabase and React Native](https://github.com/saimon24/react-native-resumable-upload-supabase/commit/bb45455dc2d4d3fdd42089a8a26a521cc5b5e60f#diff-2d97fcb7ad2b1ac426a8d5391cfd0ab970e525ede1f11e14885f65e6dcd116a1) in the repository of this tutorial.
|
||||
|
||||
However, ultimately the uploaded file was always 0 bytes, so I decided to leave it out for now.
|
||||
|
||||
The Supabas team is investigating this issue, so I'm very sure that we will have resumable uploads working with React Native soon.
|
||||
|
||||
## Conclusion
|
||||
|
||||
It's almost too easy to use Supabase Storage, and it's a great way to store files for your apps.
|
||||
|
||||
You now have a fully functional image gallery app with React Native and Supabase Storage including user authentication without writing a line of backend code!
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) where you just need to insert your own Supabase URL and API key.
|
||||
|
||||
If you enjoyed the tutorial, you can [learn React Native on Galaxies.dev](https://galaxies.dev) where I help developers build awesome React Native apps.
|
||||
|
||||
Until next time and happy coding with Supabase!
|
||||
@@ -19,9 +19,9 @@ function AuthWidgetSection() {
|
||||
const { basePath } = useRouter()
|
||||
const [radius, setRadius] = useState('4px')
|
||||
const [brandColor, setBrandColor] = useState({
|
||||
brand: 'var(--colors-brand9)',
|
||||
brandAccent: 'var(--colors-brand11)',
|
||||
brandAccent2: 'var(--colors-brand3)',
|
||||
brand: 'hsl(var(--brand-default))',
|
||||
brandAccent: 'hsl(var(--brand-500))',
|
||||
brandAccent2: 'hsl(var(--brand-200))',
|
||||
})
|
||||
const [layout, setLayout] = useState('horizontal')
|
||||
|
||||
@@ -140,15 +140,15 @@ function AuthWidgetSection() {
|
||||
<button
|
||||
onClick={() =>
|
||||
setBrandColor({
|
||||
brand: 'var(--colors-brand9)',
|
||||
brandAccent: 'var(--colors-brand11)',
|
||||
brandAccent2: 'var(--colors-brand3)',
|
||||
brand: 'hsl(var(--brand-default))',
|
||||
brandAccent: 'hsl(var(--brand-500))',
|
||||
brandAccent2: 'hsl(var(--brand-200))',
|
||||
})
|
||||
}
|
||||
className={[
|
||||
'bg-brand-300 border-brand-900 h-10 w-10 rounded-full border-2 transition hover:scale-105',
|
||||
brandColor.brand === 'var(--colors-brand9)'
|
||||
? ' !bg-brand-900 ring-scale-400 border-scale-800 ring-2 drop-shadow-lg dark:ring-white'
|
||||
'bg-brand-300 border-brand h-10 w-10 rounded-full border-2 transition hover:scale-105',
|
||||
brandColor.brand === 'hsl(var(--brand-default))'
|
||||
? ' !bg-brand ring-scale-400 border-scale-800 ring-2 drop-shadow-lg dark:ring-white'
|
||||
: '',
|
||||
].join(' ')}
|
||||
></button>
|
||||
|
||||
@@ -13,8 +13,8 @@ const Button = (props: Props) => {
|
||||
|
||||
const colorClass =
|
||||
type === 'primary'
|
||||
? 'px-3 py-2 shadow-sm border border-transparent text-white bg-brand-700 hover:bg-brand-800 focus:ring-2 focus:ring-offset-2 focus:ring-brand-500'
|
||||
: 'text-brand-700 bg-none'
|
||||
? 'px-3 py-2 shadow-sm border border-transparent text-white bg-brand-400 hover:bg-brand-300 focus:ring-2 focus:ring-offset-2 focus:ring-brand-300'
|
||||
: 'text-brand-400 bg-none'
|
||||
|
||||
const textClass = type === 'primary' ? 'font-medium left-3 group-hover:left-0' : 'font-normal'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as data from '~/data/blog/PostgresFTSComparison'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
const barColors = {
|
||||
pg: 'var(--colors-brand11)',
|
||||
pg: 'hsl(var(--brand-600))',
|
||||
meilisearch: 'var(--colors-blue11)',
|
||||
typesense: 'var(--colors-violet11)',
|
||||
'sqlite-disk': 'var(--colors-orange11)',
|
||||
|
||||
@@ -119,7 +119,7 @@ function CodeBlock(props: CodeBlockProps) {
|
||||
type="text"
|
||||
icon={
|
||||
copied ? (
|
||||
<span className="text-brand-900">
|
||||
<span className="text-brand">
|
||||
<IconCheck strokeWidth={3} />
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -41,7 +41,7 @@ const Hero = () => {
|
||||
{showAnnouncement && (
|
||||
<Link href="/launch-week">
|
||||
<a className="group w-auto rounded-full p-1 mb-6 mt-8 md:mb-10 bg-gradient-to-b from-[#1b1f2124] to-[#02040550] hover:from-[#ffffff15] hover:to-[#ffffff05] transition-all backdrop-blur-lg border flex items-center justify-between gap-2 md:gap-4 text-scale-1100 hover:text-scale-1200 text-sm">
|
||||
<div className="text-scale-200 rounded-full bg-brand-900 px-3 py-1 flex items-center justify-center">
|
||||
<div className="text-scale-200 rounded-full bg-brand px-3 py-1 flex items-center justify-center">
|
||||
Get your ticket
|
||||
</div>
|
||||
<span>Launch Week 8 - August 7-11</span>
|
||||
|
||||
@@ -34,9 +34,9 @@ const InteractiveShimmerCard = ({
|
||||
const isActive = x > -3 && x < width + 3 && y > -3 && y < height + 3
|
||||
const activeGlow =
|
||||
hasActiveOnHover && isActive
|
||||
? `radial-gradient(65rem circle at ${x}px ${y}px, var(--colors-brand9), transparent), `
|
||||
? `radial-gradient(65rem circle at ${x}px ${y}px, hsl(var(--brand-default)), transparent), `
|
||||
: ''
|
||||
outerElement.style.background = isActive ? `var(--colors-brand9)` : `var(--colors-scale3)`
|
||||
outerElement.style.background = isActive ? `hsl(var(--brand-default))` : `var(--colors-scale3)`
|
||||
outerElement.style.backgroundImage = `
|
||||
${activeGlow}radial-gradient(30rem circle at ${x}px ${y}px, ${
|
||||
shimmerFromColor ?? 'var(--colors-scale8)'
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function LaunchHero() {
|
||||
<h3 className="text-scale-1200 text-lg">Submission</h3>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Submit your project through{' '}
|
||||
<a className="text-brand-900" href="https://madewithsupabase.com">
|
||||
<a className="text-brand" href="https://madewithsupabase.com">
|
||||
madewithsupabase.com
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -9,7 +9,7 @@ const ArticleButtonListItem = (props: Article) => {
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-scale-1200 group-hover:text-brand-900 text-left text-base transition">
|
||||
<span className="text-scale-1200 group-hover:text-brand text-left text-base transition">
|
||||
{props.title}
|
||||
</span>
|
||||
<p className="text-scale-1100 text-left text-sm transition">{props.description}</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ const ProductButton = (props: Article) => {
|
||||
<div className="mr-2 mb-2 inline-block">
|
||||
<button
|
||||
className="
|
||||
text-brand-1200
|
||||
text-brand-600
|
||||
hover:bg-scale-300 border-scale-500 flex items-start gap-3 rounded-md border bg-transparent p-3
|
||||
px-6 transition
|
||||
dark:drop-shadow-sm"
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ProductButtonListItem = (props: Article) => {
|
||||
<div className="inline-block">
|
||||
<button
|
||||
className="
|
||||
text-brand-1200
|
||||
text-brand-600
|
||||
group
|
||||
flex items-start gap-3 bg-transparent
|
||||
transition
|
||||
@@ -18,7 +18,7 @@ export const ProductButtonListItem = (props: Article) => {
|
||||
<SparklesIcon strokeWidth={1} />
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-0">
|
||||
<span className="group-hover:text-brand-900 text-sm">{props.title}</span>
|
||||
<span className="group-hover:text-brand text-sm">{props.title}</span>
|
||||
<p className="text-scale-1100 text-left text-sm">{props.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const LaunchSection = (props: WeekDayProps) => {
|
||||
<div
|
||||
className={[
|
||||
'absolute mt-[4px] -ml-[21px] h-3 w-3 rounded-full border md:-ml-[37px] lg:-ml-[37.5px]',
|
||||
props.shipped ? 'border-brand-900 bg-brand-400' : 'border-purple-900 bg-purple-300',
|
||||
props.shipped ? 'border-brand bg-brand-400' : 'border-purple-900 bg-purple-300',
|
||||
].join(' ')}
|
||||
></div>
|
||||
{/* END timeline dot */}
|
||||
@@ -63,7 +63,7 @@ export const LaunchSection = (props: WeekDayProps) => {
|
||||
nextDayNotShipped
|
||||
? 'launch-week-timeline-border--approaching'
|
||||
: props.shipped
|
||||
? 'border-brand-900'
|
||||
? 'border-brand'
|
||||
: 'border-purple-700',
|
||||
className,
|
||||
].join(' ')}
|
||||
@@ -152,7 +152,7 @@ export const LaunchSection = (props: WeekDayProps) => {
|
||||
md:pl-8
|
||||
lg:pl-8
|
||||
`,
|
||||
props.shipped ? 'border-brand-900' : 'border-purple-700',
|
||||
props.shipped ? 'border-brand' : 'border-purple-700',
|
||||
props.shippingHasStarted && 'lg:col-span-6',
|
||||
!lastIndex && 'pb-8 lg:pb-16',
|
||||
].join(' ')}
|
||||
|
||||
@@ -130,8 +130,8 @@
|
||||
height: 56px;
|
||||
margin-top: var(--space-4x);
|
||||
border-radius: var(--space-2x);
|
||||
border: 1px solid var(--brand);
|
||||
background: var(--brand);
|
||||
border: 1px solid hsl(var(--brand-default));
|
||||
background: hsl(var(--brand-default));
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: var(--text-md);
|
||||
@@ -170,7 +170,7 @@
|
||||
.submit.default:hover,
|
||||
.submit.default:focus {
|
||||
background: black;
|
||||
color: var(--brand);
|
||||
color: hsl(var(--brand-default));
|
||||
}
|
||||
|
||||
.submit.default.generate-with-github:hover path,
|
||||
|
||||
@@ -54,9 +54,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
|
||||
Connected account
|
||||
</span>
|
||||
)}
|
||||
{IS_SAVED && (
|
||||
<span className="opacity-0 animate-fade-in text-xs text-brand-900">Saved</span>
|
||||
)}
|
||||
{IS_SAVED && <span className="opacity-0 animate-fade-in text-xs text-brand">Saved</span>}
|
||||
{HAS_ERROR && (
|
||||
<span className="opacity-0 animate-fade-in text-xs text-tomato-900">
|
||||
Something went wrong
|
||||
@@ -72,7 +70,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
|
||||
disabled
|
||||
placeholder="name"
|
||||
value={user.name}
|
||||
icon={<IconCheck strokeWidth={2} className="w-3 text-brand-900" />}
|
||||
icon={<IconCheck strokeWidth={2} className="w-3 text-brand" />}
|
||||
/>
|
||||
<Input
|
||||
className="[&_input]:border-scale-200"
|
||||
@@ -91,7 +89,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
|
||||
className={[
|
||||
'w-3',
|
||||
IS_SAVING && 'text-scale-300',
|
||||
!!formData.role ? 'text-brand-900' : 'text-scale-300',
|
||||
!!formData.role ? 'text-brand' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
@@ -113,7 +111,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
|
||||
className={[
|
||||
'w-3',
|
||||
IS_SAVING && 'text-scale-300',
|
||||
!!formData.company ? 'text-brand-900' : 'text-scale-300',
|
||||
!!formData.company ? 'text-brand' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
@@ -135,7 +133,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
|
||||
className={[
|
||||
'w-3 flex spin',
|
||||
IS_SAVING && 'text-scale-300',
|
||||
!!formData.location ? 'text-brand-900' : 'text-scale-300',
|
||||
!!formData.location ? 'text-brand' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -130,8 +130,8 @@
|
||||
height: 56px;
|
||||
margin-top: var(--space-4x);
|
||||
border-radius: var(--space-2x);
|
||||
border: 1px solid var(--brand);
|
||||
background: var(--brand);
|
||||
border: 1px solid hsl(var(--brand-default));
|
||||
background: hsl(var(--brand-default));
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: var(--text-md);
|
||||
@@ -170,12 +170,12 @@
|
||||
.submit.default:hover,
|
||||
.submit.default:focus {
|
||||
background: black;
|
||||
color: var(--brand);
|
||||
color: hsl(var(--brand-default));
|
||||
}
|
||||
|
||||
.submit.default.generate-with-github:hover path,
|
||||
.submit.default.generate-with-github:focus path {
|
||||
fill: var(--brand);
|
||||
fill: hsl(var(--brand-default));
|
||||
}
|
||||
|
||||
.submit.error:hover,
|
||||
|
||||
@@ -93,7 +93,7 @@ function Product({
|
||||
lazyBoundary="100px"
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="text-brand-1100 dark:text-brand-900 flex justify-center relative opacity-70 md:absolute md:bottom-0 md:opacity-0 group-hover:opacity-100 transition-opacity md:translate-y-8 md:-left-20 md:md:-right-20 font-mono uppercase text-center text-xs mt-2">
|
||||
<div className="text-brand-600 dark:text-brand flex justify-center relative opacity-70 md:absolute md:bottom-0 md:opacity-0 group-hover:opacity-100 transition-opacity md:translate-y-8 md:-left-20 md:md:-right-20 font-mono uppercase text-center text-xs mt-2">
|
||||
<span>{product.name}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -82,7 +82,7 @@ const Company = () => {
|
||||
<div className="mt-6 text-sm font-medium">
|
||||
<a
|
||||
href="/blog"
|
||||
className="dark:text-brand-900 dark:hover:text-brand-700 text-gray-600 transition duration-150 ease-in-out hover:text-gray-500"
|
||||
className="dark:text-brand dark:hover:text-brand-400 text-gray-600 transition duration-150 ease-in-out hover:text-gray-500"
|
||||
>
|
||||
View all posts <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
|
||||
@@ -85,7 +85,7 @@ const Nav = () => {
|
||||
<p className="mt-1 text-sm text-scale-1100 dark:text-dark-100">{description}</p>
|
||||
</div>
|
||||
{url && (
|
||||
<p className="mt-2 text-sm font-medium text-brand-900 lg:mt-4">
|
||||
<p className="mt-2 text-sm font-medium text-brand lg:mt-4">
|
||||
<TextLink label={label ? 'Get notified' : 'Learn more'} url={url} />
|
||||
</p>
|
||||
)}
|
||||
@@ -120,7 +120,7 @@ const Nav = () => {
|
||||
>
|
||||
<button
|
||||
className={[
|
||||
'text-scale-900 focus:ring-brand-900 dark:bg-scale-200 dark:hover:bg-scale-300 inline-flex items-center justify-center rounded-md bg-gray-50 p-2 hover:bg-white focus:outline-none focus:ring-2 focus:ring-inset',
|
||||
'text-scale-900 focus:ring-brand dark:bg-scale-200 dark:hover:bg-scale-300 inline-flex items-center justify-center rounded-md bg-gray-50 p-2 hover:bg-white focus:outline-none focus:ring-2 focus:ring-inset',
|
||||
showLaunchWeekNavMode && '!bg-transparent border border-[#be9eea]',
|
||||
].join(' ')}
|
||||
aria-expanded="false"
|
||||
@@ -166,7 +166,7 @@ const Nav = () => {
|
||||
<div
|
||||
className={[
|
||||
`
|
||||
text-scale-1200 hover:text-brand-900
|
||||
text-scale-1200 hover:text-brand
|
||||
inline-flex cursor-pointer items-center
|
||||
border-b-2
|
||||
border-transparent
|
||||
@@ -258,7 +258,7 @@ const Nav = () => {
|
||||
<Link href="/pricing">
|
||||
<a
|
||||
className={[
|
||||
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
`text-scale-1200 hover:text-brand hover:border-brand dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
border-b-2 border-transparent p-5 px-1
|
||||
text-sm font-medium`,
|
||||
showLaunchWeekNavMode && '!text-white',
|
||||
@@ -270,7 +270,7 @@ const Nav = () => {
|
||||
<Link href="/docs">
|
||||
<a
|
||||
className={[
|
||||
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
`text-scale-1200 hover:text-brand hover:border-brand dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
border-b-2 border-transparent p-5 px-1
|
||||
text-sm font-medium`,
|
||||
showLaunchWeekNavMode && '!text-white',
|
||||
@@ -282,7 +282,7 @@ const Nav = () => {
|
||||
<Link href="/blog">
|
||||
<a
|
||||
className={[
|
||||
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
`text-scale-1200 hover:text-brand hover:border-brand dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
|
||||
border-b-2 border-transparent p-5 px-1
|
||||
text-sm font-medium`,
|
||||
showLaunchWeekNavMode && '!text-white',
|
||||
@@ -299,7 +299,7 @@ const Nav = () => {
|
||||
className="hidden group lg:flex"
|
||||
type="text"
|
||||
icon={
|
||||
<div className="flex items-center justify-center w-4 h-4 text-brand-800">
|
||||
<div className="flex items-center justify-center w-4 h-4 text-brand-300">
|
||||
<div
|
||||
className={[
|
||||
`text-scale-900 flex h-3 w-3 items-center justify-center
|
||||
@@ -376,7 +376,7 @@ const Nav = () => {
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center p-2 bg-white rounded-md text-scale-900 focus:ring-brand-900 dark:bg-scale-300 dark:hover:bg-scale-400 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset"
|
||||
className="inline-flex items-center justify-center p-2 bg-white rounded-md text-scale-900 focus:ring-brand dark:bg-scale-300 dark:hover:bg-scale-400 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset"
|
||||
>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<svg
|
||||
|
||||
@@ -19,7 +19,7 @@ const PartnerLinkBox = ({
|
||||
'bg-amber-400 dark:bg-scale-100 group-hover:bg-amber-500 dark:group-hover:bg-amber-300 text-amber-900',
|
||||
blue: 'bg-blue-400 dark:bg-scale-100 group-hover:bg-blue-500 dark:group-hover:bg-blue-300 text-blue-900',
|
||||
brand:
|
||||
'bg-brand-400 dark:bg-scale-100 group-hover:bg-brand-500 dark:group-hover:bg-brand-300 text-brand-900',
|
||||
'bg-brand-400 dark:bg-scale-100 group-hover:bg-brand-300 dark:group-hover:bg-brand-300 text-brand',
|
||||
}
|
||||
|
||||
const content = (
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
</th>
|
||||
<td
|
||||
className={`px-4 py-3 ${
|
||||
column.key === 'plan' ? 'text-brand-900 pt-16 lg:pt-3' : ''
|
||||
column.key === 'plan' ? 'text-brand pt-16 lg:pt-3' : ''
|
||||
}`}
|
||||
>
|
||||
{column.key === 'dedicated' ? (
|
||||
|
||||
@@ -9,13 +9,13 @@ function ProductIcon({ icon, color }: ProductIcon) {
|
||||
className={[
|
||||
'inline-flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md',
|
||||
!color || color === 'black' ? 'bg-scale-1200 text-scale-100' : '',
|
||||
color && color === 'green' ? 'bg-brand-800 dark:bg-brand-600 text-brand-100' : '',
|
||||
color && color === 'green' ? 'bg-brand-300 dark:bg-brand-600 text-brand-100' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<svg
|
||||
className={[
|
||||
'h-5 w-5 stroke-white dark:stroke-black',
|
||||
color && color === 'green' ? 'dark:stroke-brand-900' : '',
|
||||
color && color === 'green' ? 'dark:stroke-brand' : '',
|
||||
].join(' ')}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Quote(props: Props) {
|
||||
<Avatar caption={caption} img={img} />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute rounded-full w-1 h-full inset-0 right-auto bg-brand-900" />
|
||||
<div className="absolute rounded-full w-1 h-full inset-0 right-auto bg-brand" />
|
||||
</div>
|
||||
</figure>
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ const SOC2RequestForm = () => {
|
||||
return (
|
||||
<div>
|
||||
{formSubmitted ? (
|
||||
<h3 className="h3 text-brand-900 mt-12 text-center">
|
||||
<h3 className="h3 text-brand mt-12 text-center">
|
||||
Thank you for your submission! A member from the Supabase team will reach out to you
|
||||
shortly!
|
||||
</h3>
|
||||
|
||||
@@ -30,7 +30,7 @@ const ScrollProgress = () => {
|
||||
return (
|
||||
<div className="h-[2px] w-full flex justify-start relative">
|
||||
<div
|
||||
className="h-full top-0 bottom-0 right-0 absolute w-screen bg-brand-900 will-change-transform transition-opacity"
|
||||
className="h-full top-0 bottom-0 right-0 absolute w-screen bg-brand will-change-transform transition-opacity"
|
||||
style={{
|
||||
display: isActive ? 'absolute' : 'relative',
|
||||
transform: `translate3d(${isActive ? progressPercentage - 100 + '%' : '0'},0,0)`,
|
||||
|
||||
@@ -130,7 +130,7 @@ const CommunitySlider = () => {
|
||||
<p className="text-scale-900 my-3 text-base">{card.abstract}</p>
|
||||
|
||||
{card.url && (
|
||||
<div className="text-brand-900 block cursor-pointer text-sm">Read Customer Story</div>
|
||||
<div className="text-brand block cursor-pointer text-sm">Read Customer Story</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ const CustomerQuotesSection = (props: Props) => {
|
||||
{customer.author}, {customer.role}
|
||||
</p>
|
||||
{customer.url && (
|
||||
<div className="text-brand-900 border-t mt-4 pt-4 cursor-pointer text-sm flex items-center justify-between">
|
||||
<div className="text-brand border-t mt-4 pt-4 cursor-pointer text-sm flex items-center justify-between">
|
||||
<span>Read Customer Story</span>
|
||||
<IconArrowUpRight />
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ const ProductHeaderCentered = (props: Types) => (
|
||||
{props.icon && <ProductIcon icon={props.icon} />}
|
||||
{props.title && (
|
||||
<span
|
||||
className="text-brand-900 font-mono uppercase tracking-widest text-sm"
|
||||
className="text-brand font-mono uppercase tracking-widest text-sm"
|
||||
key={`product-name-${props.title}`}
|
||||
>
|
||||
{props.title}
|
||||
|
||||
@@ -29,7 +29,7 @@ const Tab = ({ isActive, label, paragraph, onClick, progress, intervalDuration }
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className={[
|
||||
'absolute inset-0 w-full right-full bg-brand-900 h-full transition-opacity',
|
||||
'absolute inset-0 w-full right-full bg-brand h-full transition-opacity',
|
||||
progress! > 99.7 ? 'opacity-0' : 'opacity-100',
|
||||
].join(' ')}
|
||||
style={{ x: `${progress! - 100}%` }}
|
||||
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
<>
|
||||
<span className="heading-gradient">Simple yet</span>
|
||||
<br />
|
||||
<span className="text-brand-900">powerful APIs</span>
|
||||
<span className="text-brand">powerful APIs</span>
|
||||
</>
|
||||
),
|
||||
paragraph: 'Easy-to-use client libraries for managing and querying vector stores in Postgres.',
|
||||
@@ -242,7 +242,7 @@ docs.query(
|
||||
<>
|
||||
<span className="heading-gradient">Powerful Features</span>
|
||||
<br />
|
||||
<span className="text-brand-900">Scale to millions</span>
|
||||
<span className="text-brand">Scale to millions</span>
|
||||
</>
|
||||
),
|
||||
paragraph:
|
||||
|
||||
@@ -90,7 +90,7 @@ const LayoutComparison = ({ components, props }: Props) => {
|
||||
{/* Title and description */}
|
||||
<div className="mb-16 max-w-5xl space-y-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-brand-900 text-center">Alternative</p>
|
||||
<p className="text-brand text-center">Alternative</p>
|
||||
<h1 className="h1 text-center">{props.blog.title}</h1>
|
||||
<div className="text-scale-900 flex justify-center space-x-3 text-sm">
|
||||
<p>{props.blog.date}</p>
|
||||
|
||||
@@ -118,7 +118,7 @@ const Hero = (props: Props) => {
|
||||
<div className="col-span-12 text-scale-900 dark:text-white md:col-span-9 lg:col-span-8 xl:col-span-6">
|
||||
<p className="mb-10 text-4xl">Supabase is an open source Firebase alternative.</p>
|
||||
<p className="text-2xl">
|
||||
Today, we're moving to <span className="text-brand-900">Beta</span>
|
||||
Today, we're moving to <span className="text-brand">Beta</span>
|
||||
</p>
|
||||
<time itemProp="datePublished" dateTime="2020-12-03" className="text-sm opacity-80">
|
||||
Published December 3rd, 2020
|
||||
@@ -139,7 +139,7 @@ const Introduction = () => {
|
||||
<a
|
||||
href="https://news.ycombinator.com/item?id=23319901"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Alpha
|
||||
</a>{' '}
|
||||
@@ -154,7 +154,7 @@ const Introduction = () => {
|
||||
<a
|
||||
href="/blog/2020/12/02/case-study-xendit"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Xendit
|
||||
</a>
|
||||
@@ -162,7 +162,7 @@ const Introduction = () => {
|
||||
<a
|
||||
href="/blog/2020/12/02/case-study-monitoro"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Monitoro
|
||||
</a>
|
||||
@@ -170,7 +170,7 @@ const Introduction = () => {
|
||||
<a
|
||||
href="/blog/2020/12/02/case-study-tayfa"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
TAYFA
|
||||
</a>{' '}
|
||||
@@ -223,8 +223,7 @@ const TableOfContents = (props: any) => {
|
||||
<div className="container grid grid-cols-12 gap-4 px-8 py-20 mx-auto lg:px-28">
|
||||
<div className="col-span-12 mb-10 text-base">
|
||||
<p className="text-2xl text-black dark:text-white">
|
||||
Supabase <span className="text-brand-900 dark:text-brand-900">Beta</span> is starting
|
||||
now.
|
||||
Supabase <span className="text-brand dark:text-brand">Beta</span> is starting now.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 col-span-12 gap-y-10">
|
||||
@@ -314,7 +313,7 @@ const Performance = () => {
|
||||
<Bar
|
||||
color={
|
||||
stat.name === 'Supabase'
|
||||
? 'bg-brand-900 dark:bg-brand-800'
|
||||
? 'bg-brand dark:bg-brand-300'
|
||||
: 'bg-brand-300 dark:bg-brand-400'
|
||||
}
|
||||
finalPercentage={Math.ceil((stat.value / maxValue) * 100)}
|
||||
@@ -353,7 +352,7 @@ const Performance = () => {
|
||||
<a
|
||||
href="https://elixir-lang.org/"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Elixir
|
||||
</a>{' '}
|
||||
@@ -361,7 +360,7 @@ const Performance = () => {
|
||||
<a
|
||||
href="https://github.com/supabase/realtime"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Realtime engine
|
||||
</a>
|
||||
@@ -369,7 +368,7 @@ const Performance = () => {
|
||||
<a
|
||||
href="https://postgrest.org/en/v7.0.0/"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
PostgREST
|
||||
</a>{' '}
|
||||
@@ -381,7 +380,7 @@ const Performance = () => {
|
||||
<a
|
||||
href="https://github.com/supabase/benchmarks/"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
benchmarks
|
||||
</a>{' '}
|
||||
@@ -451,11 +450,7 @@ const Security = () => {
|
||||
</li>
|
||||
<li className="mb-5">
|
||||
Adopted the{' '}
|
||||
<a
|
||||
href="https://snyk.io/"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
>
|
||||
<a href="https://snyk.io/" target="_blank" className="text-brand hover:text-brand">
|
||||
Snyk
|
||||
</a>{' '}
|
||||
dependency monitor as part of our SSDLC on several key component of our system, to
|
||||
@@ -467,7 +462,7 @@ const Security = () => {
|
||||
<a
|
||||
href="https://github.com/PostgREST/postgrest/pull/1600#issuecomment-735257952"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
now uses
|
||||
</a>{' '}
|
||||
@@ -513,7 +508,7 @@ const Reliability = () => {
|
||||
<a
|
||||
href="https://status.supabase.com"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
https://status.supabase.com
|
||||
</a>{' '}
|
||||
@@ -557,7 +552,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
<a
|
||||
href="/docs/reference/javascript/auth-signup"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
JavaScript
|
||||
</a>{' '}
|
||||
@@ -565,7 +560,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
<a
|
||||
href="/docs/gotrue/server/about#endpoints"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
HTTP
|
||||
</a>
|
||||
@@ -587,7 +582,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
<a
|
||||
href="/docs/guides/client-libraries#realtime-changes"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
subscribe to changes in your database
|
||||
</a>{' '}
|
||||
@@ -606,7 +601,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
<a
|
||||
href="/docs/reference/javascript/select#query-foreign-tables"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
querying from multiple tables
|
||||
</a>{' '}
|
||||
@@ -614,7 +609,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
<a
|
||||
href="/docs/reference/javascript/rpc"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
invoke complex functions
|
||||
</a>
|
||||
@@ -703,7 +698,7 @@ const BetaPricing = () => (
|
||||
<a
|
||||
href="mailto:rory@supabase.io"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
rory@supabase.io
|
||||
</a>
|
||||
@@ -716,7 +711,7 @@ const BetaPricing = () => (
|
||||
</ul>
|
||||
<p>
|
||||
The Supabase Base plan is now called the Supabase Pro plan as per the{' '}
|
||||
<a href="https://supabase.com/pricing" className="text-brand-900 hover:text-brand-900">
|
||||
<a href="https://supabase.com/pricing" className="text-brand hover:text-brand">
|
||||
pricing page
|
||||
</a>
|
||||
.
|
||||
@@ -747,7 +742,7 @@ const OpenSource = () => (
|
||||
<a
|
||||
href="https://github.com/sponsors/supabase/"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
sponsorship
|
||||
</a>{' '}
|
||||
@@ -759,7 +754,7 @@ const OpenSource = () => (
|
||||
<a
|
||||
href="/blog/2020/12/02/supabase-striveschool"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
partnering with Strive School
|
||||
</a>{' '}
|
||||
@@ -772,7 +767,7 @@ const OpenSource = () => (
|
||||
<a
|
||||
href="mailto:rory@supabase.io"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
rory@supabase.io
|
||||
</a>{' '}
|
||||
@@ -783,7 +778,7 @@ const OpenSource = () => (
|
||||
<a
|
||||
href="https://github.com/supabase"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
our GitHub.
|
||||
</a>{' '}
|
||||
@@ -819,7 +814,7 @@ const FundingPartners = () => (
|
||||
<a
|
||||
href="https://twitter.com/supabase"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
Twitter
|
||||
</a>{' '}
|
||||
@@ -856,7 +851,7 @@ const ScalingOurTeam = () => (
|
||||
<a
|
||||
href="mailto:work@supabase.io"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
work@supabase.io
|
||||
</a>
|
||||
@@ -917,7 +912,7 @@ const WhatsNext = () => (
|
||||
<a
|
||||
href="mailto:support@supabase.io"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
support@supabase.io
|
||||
</a>{' '}
|
||||
@@ -925,7 +920,7 @@ const WhatsNext = () => (
|
||||
<a
|
||||
href="https://github.com/supabase/supabase/discussions"
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-900"
|
||||
className="text-brand hover:text-brand"
|
||||
>
|
||||
discussion
|
||||
</a>{' '}
|
||||
|
||||
@@ -202,7 +202,7 @@ function BlogPostPage(props: any) {
|
||||
{/* Title and description */}
|
||||
<div className="mb-16 max-w-5xl space-y-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-brand-900">Blog post</p>
|
||||
<p className="text-brand">Blog post</p>
|
||||
<h1 className="h1">{props.blog.title}</h1>
|
||||
<div className="text-scale-900 flex space-x-3 text-sm">
|
||||
<p>{props.blog.date}</p>
|
||||
|
||||
@@ -101,7 +101,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
|
||||
<DefaultLayout>
|
||||
<div className="text-scale-1200">
|
||||
<div className="container relative mx-auto px-6 py-10 lg:pt-12 lg:px-16 xl:px-20 text-center space-y-4">
|
||||
<h1 className="text-sm text-brand-900 md:text-base">
|
||||
<h1 className="text-sm text-brand md:text-base">
|
||||
<span className="sr-only">Supabase </span>Careers
|
||||
</h1>
|
||||
<h2 className="text-3xl md:text-4xl xl:text-5xl lg:max-w-2xl xl:max-w-3xl lg:mx-auto tracking-[-1.5px]">
|
||||
@@ -121,9 +121,9 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="border-t-[1px] mt-6 mx-2 md:mx-2 md:mt-0 md:border-0 border-brand-900 w-[134px] md:max-w-none"
|
||||
className="border-t-[1px] mt-6 mx-2 md:mx-2 md:mt-0 md:border-0 border-brand w-[134px] md:max-w-none"
|
||||
>
|
||||
<div className="hidden md:block border-t-[1px] lg:border-t-2 border-brand-900 w-[60px] lg:w-[100px]"></div>
|
||||
<div className="hidden md:block border-t-[1px] lg:border-t-2 border-brand w-[60px] lg:w-[100px]"></div>
|
||||
<h2 className="text-3xl lg:text-4xl pt-3 tracking-[-1.5px]">
|
||||
{company.number}
|
||||
</h2>
|
||||
@@ -153,7 +153,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
|
||||
Discord to stay connected to our team, and our community.
|
||||
</p>
|
||||
<div className="max-w-[300px] sm:max-w-md lg:max-w-md mt-20">
|
||||
<div className="border-t-2 border-brand-900 w-4/12"></div>
|
||||
<div className="border-t-2 border-brand w-4/12"></div>
|
||||
<h2 className="text-2xl sm:text-3xl lg:text-4xl pt-2 tracking-[-1.5px]">
|
||||
We deeply believe in the efficacy of collaborative open source
|
||||
</h2>
|
||||
@@ -445,10 +445,10 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
|
||||
className="flex lg:block items-start space-x-6 lg:space-x-0 lg:w-full"
|
||||
>
|
||||
<div className="lg:flex items-center">
|
||||
<h3 className="bg-brand-600 border-[1px] border-brand-800 text-brand-900 text-md text-center w-[44px] px-2 py-1.5 rounded-md">
|
||||
<h3 className="bg-brand-600 border-[1px] border-brand-300 text-brand text-md text-center w-[44px] px-2 py-1.5 rounded-md">
|
||||
{i + 1}
|
||||
</h3>
|
||||
<div className="h-[100px] w-[1px] sm:h-[100px] mx-auto lg:h-[1px] lg:w-full bg-brand-800 lg:pr-6"></div>
|
||||
<div className="h-[100px] w-[1px] sm:h-[100px] mx-auto lg:h-[1px] lg:w-full bg-brand-300 lg:pr-6"></div>
|
||||
</div>
|
||||
<div className="lg:mt-6">
|
||||
<h2 className="sm:text-lg max-w-[75%] xl:max-w-none xl:w-11/12 lg:max-w-none">
|
||||
@@ -461,7 +461,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<h3 className="bg-brand-600 border-[1px] border-brand-800 text-brand-900 text-xl w-[44px] min-h-[40px] px-2 py-1 rounded-md grid justify-items-center items-center">
|
||||
<h3 className="bg-brand-600 border-[1px] border-brand-300 text-brand text-xl w-[44px] min-h-[40px] px-2 py-1 rounded-md grid justify-items-center items-center">
|
||||
<IconCheck />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -122,9 +122,7 @@ function CaseStudyPage(props: any) {
|
||||
<article className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-8 max-w-xxl">
|
||||
<Link passHref href="/customers">
|
||||
<a className="text-brand-900 hover:text-brand-1000 mb-2 mt-0">
|
||||
Customer Stories
|
||||
</a>
|
||||
<a className="text-brand hover:text-brand-600 mb-2 mt-0">Customer Stories</a>
|
||||
</Link>
|
||||
<h1 className="text-scale-1200 text-4xl font-semibold xl:text-5xl">
|
||||
{props.blog.title}
|
||||
|
||||
@@ -145,7 +145,7 @@ function Database() {
|
||||
className="bg-scale-100 dark:bg-scale-300 group flex flex-col gap-4 rounded border px-8 py-6"
|
||||
key={i}
|
||||
>
|
||||
<div className="bg-scale-300 dark:bg-scale-500 text-scale-1200 group-hover:text-brand-900 flex h-12 w-12 items-center justify-center rounded-md border transition-all group-hover:scale-105">
|
||||
<div className="bg-scale-300 dark:bg-scale-500 text-scale-1200 group-hover:text-brand flex h-12 w-12 items-center justify-center rounded-md border transition-all group-hover:scale-105">
|
||||
{item.icon ? item.icon : <IconCode strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1018,7 +1018,7 @@ export default function launchweek() {
|
||||
href="https://supabase.com/blog/launch-week-6-hackathon"
|
||||
target="_blank"
|
||||
rel="nooper noreferrer"
|
||||
className="text-brand-900"
|
||||
className="text-brand"
|
||||
>
|
||||
blog post
|
||||
</a>
|
||||
@@ -1052,7 +1052,7 @@ export default function launchweek() {
|
||||
<a
|
||||
href="https://discord.supabase.com/"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-900 flex items-center"
|
||||
className="text-brand flex items-center"
|
||||
target="_blank"
|
||||
>
|
||||
Join our Discord <IconExternalLink size="small" className="inline-block ml-2" />
|
||||
@@ -1064,7 +1064,7 @@ export default function launchweek() {
|
||||
<p className="text-slate-900 w-[90%] lg:w-[80%]">
|
||||
Submit your project through{' '}
|
||||
<a
|
||||
className="text-brand-900"
|
||||
className="text-brand"
|
||||
href="https://www.madewithsupabase.com/launch-week-6"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -1091,7 +1091,7 @@ export default function launchweek() {
|
||||
<div className="border border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-[#121212] w-fit relative max-w-[250px]">
|
||||
<img
|
||||
src="/images/launchweek/link.svg"
|
||||
className="absolute top-[16px] right-[10px] text-brand-900"
|
||||
className="absolute top-[16px] right-[10px] text-brand"
|
||||
></img>
|
||||
<h3 className="text-white">Supabase Cache Helpers</h3>
|
||||
<p className="text-slate-1000 text-xs">Previous Best Overall Project Winner</p>
|
||||
@@ -1105,7 +1105,7 @@ export default function launchweek() {
|
||||
<div className="border border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-[#121212] w-fit relative max-w-[250px]">
|
||||
<img
|
||||
src="/images/launchweek/link.svg"
|
||||
className="absolute top-[16px] right-[10px] text-brand-900"
|
||||
className="absolute top-[16px] right-[10px] text-brand"
|
||||
></img>
|
||||
<h3 className="text-white">Super UI</h3>
|
||||
<p className="text-slate-1000 text-xs">Previous Best Overall Project Runner Up</p>
|
||||
@@ -1120,7 +1120,7 @@ export default function launchweek() {
|
||||
<div className="border border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-[#121212] w-fit relative max-w-[250px]">
|
||||
<img
|
||||
src="/images/launchweek/link.svg"
|
||||
className="absolute top-[16px] right-[10px] text-brand-900"
|
||||
className="absolute top-[16px] right-[10px] text-brand"
|
||||
></img>
|
||||
<h3 className="text-white">Quid Faciam Hodie?</h3>
|
||||
<p className="text-slate-1000 text-xs">Winner Best Flutter Project</p>
|
||||
@@ -1130,7 +1130,7 @@ export default function launchweek() {
|
||||
<div className="border border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-[#121212] w-fit relative max-w-[250px]">
|
||||
<img
|
||||
src="/images/launchweek/link.svg"
|
||||
className="absolute top-[16px] right-[10px] text-brand-900"
|
||||
className="absolute top-[16px] right-[10px] text-brand"
|
||||
></img>
|
||||
<h3 className="text-white">That Hot Dog Game</h3>
|
||||
<p className="text-slate-1000 text-xs">Previous Winner: Most Fun/Interesting</p>
|
||||
@@ -1145,7 +1145,7 @@ export default function launchweek() {
|
||||
<div className="border border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 bg-[#121212] w-fit relative max-w-[250px]">
|
||||
<img
|
||||
src="/images/launchweek/link.svg"
|
||||
className="absolute top-[16px] right-[10px] text-brand-900"
|
||||
className="absolute top-[16px] right-[10px] text-brand"
|
||||
></img>
|
||||
<h3 className="text-white">RepoWatch</h3>
|
||||
<p className="text-slate-1000 text-xs">Previous Winner Best Realtime Project</p>
|
||||
@@ -1177,10 +1177,10 @@ export default function launchweek() {
|
||||
}}
|
||||
>
|
||||
<div className="relative group">
|
||||
<div className="absolute -inset-0.5 bg-brand-1100 rounded-full opacity-75 group-hover:opacity-100 group-hover:blur-sm transition duration-500"></div>
|
||||
<div className="absolute -inset-0.5 bg-brand-600 rounded-full opacity-75 group-hover:opacity-100 group-hover:blur-sm transition duration-500"></div>
|
||||
<a href={creator.link} target="_blank" rel="noopener">
|
||||
<img
|
||||
className="relative rounded-full w-12 h-12 border border-[#2E2E2E] border-brand-900 hover:shadow-md"
|
||||
className="relative rounded-full w-12 h-12 border border-[#2E2E2E] border-brand hover:shadow-md"
|
||||
src={creator.profile_picture}
|
||||
/>
|
||||
</a>
|
||||
@@ -1204,7 +1204,7 @@ export default function launchweek() {
|
||||
We worked with more than 30 content creators from around the world to drop a mountain
|
||||
of content simultaneously!
|
||||
<a rel="noopener" target="_blank" href="/blog/the-supabase-content-storm">
|
||||
<div className="text-brand-900 flex items-center mt-2">
|
||||
<div className="text-brand flex items-center mt-2">
|
||||
See all the content
|
||||
<IconExternalLink size="small" className="inline-block ml-1" />
|
||||
</div>
|
||||
@@ -1220,7 +1220,7 @@ export default function launchweek() {
|
||||
{activeCreator !== null && (
|
||||
<p className="text-slate-900 ">{creators[activeCreator].description}</p>
|
||||
)}
|
||||
<p className="text-brand-900 mt-1">
|
||||
<p className="text-brand mt-1">
|
||||
<a rel="noopener" target="_blank" href={creators[activeCreator].link}>
|
||||
<span>
|
||||
{creators[activeCreator].link_title}
|
||||
|
||||
@@ -52,7 +52,7 @@ const DPA = () => {
|
||||
<p>You can download our latest DPA document by submitting your email here.</p>
|
||||
|
||||
{formSubmitted ? (
|
||||
<p className="text-brand-900">
|
||||
<p className="text-brand">
|
||||
Thank you for your submission! A new tab should have opened with the DPA
|
||||
document
|
||||
</p>
|
||||
|
||||
@@ -82,8 +82,8 @@ const OpenSource = () => {
|
||||
{maintainerPills.map((x) => (
|
||||
<li
|
||||
key={x}
|
||||
className={`mx-4 rounded-t-lg inline-block p-2 cursor-pointer hover:text-brand-800 ${
|
||||
activePill == x ? 'bg-gray-200 dark:bg-gray-400 text-brand-800' : ''
|
||||
className={`mx-4 rounded-t-lg inline-block p-2 cursor-pointer hover:text-brand-300 ${
|
||||
activePill == x ? 'bg-gray-200 dark:bg-gray-400 text-brand-300' : ''
|
||||
}`}
|
||||
onClick={() => setActivePill(x)}
|
||||
>
|
||||
|
||||
@@ -160,7 +160,7 @@ function Partner({
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-scale-900">Category</span>
|
||||
<Link href={`/partners/experts#${partner.category.toLowerCase()}`}>
|
||||
<a className="text-brand-900 hover:text-brand-800 transition-colors">
|
||||
<a className="text-brand hover:text-brand-300 transition-colors">
|
||||
{partner.category}
|
||||
</a>
|
||||
</Link>
|
||||
@@ -172,7 +172,7 @@ function Partner({
|
||||
href={partner.website}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-900 hover:text-brand-800 transition-colors"
|
||||
className="text-brand hover:text-brand-300 transition-colors"
|
||||
>
|
||||
{new URL(partner.website).host}
|
||||
</a>
|
||||
@@ -185,7 +185,7 @@ function Partner({
|
||||
href={partner.docs}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-900 hover:text-brand-800 transition-colors"
|
||||
className="text-brand hover:text-brand-300 transition-colors"
|
||||
>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>Learn</span>
|
||||
|
||||
@@ -201,7 +201,7 @@ function Partner({
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-scale-900">Category</span>
|
||||
<Link href={`/partners/integrations#${partner.category.toLowerCase()}`}>
|
||||
<a className="text-brand-900 hover:text-brand-800 transition-colors">
|
||||
<a className="text-brand hover:text-brand-300 transition-colors">
|
||||
{partner.category}
|
||||
</a>
|
||||
</Link>
|
||||
@@ -213,7 +213,7 @@ function Partner({
|
||||
href={partner.website}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-900 hover:text-brand-800 transition-colors"
|
||||
className="text-brand hover:text-brand-300 transition-colors"
|
||||
>
|
||||
{new URL(partner.website).host}
|
||||
</a>
|
||||
@@ -226,7 +226,7 @@ function Partner({
|
||||
href={partner.docs}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-900 hover:text-brand-800 transition-colors"
|
||||
className="text-brand hover:text-brand-300 transition-colors"
|
||||
>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>Learn</span>
|
||||
|
||||
@@ -202,10 +202,10 @@ function IntegrationPartnersPage(props: Props) {
|
||||
<a className="text-scale-1200">INTEGRATIONS</a>
|
||||
</Link>
|
||||
<Link href={`/partners/experts`}>
|
||||
<a className="transition-colors text-brand-900 hover:text-brand-800">EXPERTS</a>
|
||||
<a className="transition-colors text-brand hover:text-brand-300">EXPERTS</a>
|
||||
</Link>
|
||||
<Link href={`/partners/integrations#become-a-partner`}>
|
||||
<a className="flex items-center space-x-1 transition-colors text-brand-900 hover:text-brand-800">
|
||||
<a className="flex items-center space-x-1 transition-colors text-brand hover:text-brand-300">
|
||||
BECOME A PARTNER <IconArrowRight />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function IndexPage() {
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-8 px-4 mobile-header">
|
||||
<h2 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-3xl font-medium uppercase font-mono">
|
||||
<h2 className="gradient-text-brand-300 dark:gradient-text-brand-100 text-3xl font-medium uppercase font-mono">
|
||||
{plan}
|
||||
</h2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
@@ -150,7 +150,7 @@ export default function IndexPage() {
|
||||
<div className="relative z-10 py-16 lg:py-28">
|
||||
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl space-y-2 lg:max-w-none">
|
||||
<h1 className="text-brand-900 text-base">Pricing</h1>
|
||||
<h1 className="text-brand text-base">Pricing</h1>
|
||||
<h2 className="h1">Predictable pricing, no surprises</h2>
|
||||
<p className="p text-lg">
|
||||
Start building for free, collaborate with a team, then scale to millions of users
|
||||
@@ -174,7 +174,7 @@ export default function IndexPage() {
|
||||
key={`row-${plan.name}`}
|
||||
className={[
|
||||
plan.name === 'Pro'
|
||||
? 'bg-brand-1100 dark:bg-brand-900 border px-0.5 -mt-8 rounded-[6px]'
|
||||
? 'bg-brand-600 dark:bg-brand border px-0.5 -mt-8 rounded-[6px]'
|
||||
: '',
|
||||
].join(' ')}
|
||||
>
|
||||
@@ -198,13 +198,13 @@ export default function IndexPage() {
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3
|
||||
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
className="gradient-text-brand-300 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
uppercase flex items-center gap-4 font-mono"
|
||||
>
|
||||
{plan.name}
|
||||
</h3>
|
||||
{plan.nameBadge && (
|
||||
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
<span className="bg-brand-300 text-brand-600 rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
{plan.nameBadge}
|
||||
</span>
|
||||
)}
|
||||
@@ -249,7 +249,7 @@ export default function IndexPage() {
|
||||
|
||||
{plan.warning && (
|
||||
<p className="-mt-2">
|
||||
<span className="bg-scale-200 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
<span className="bg-scale-200 text-brand-600 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
{plan.warning}
|
||||
</span>
|
||||
</p>
|
||||
@@ -273,7 +273,7 @@ export default function IndexPage() {
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck
|
||||
className="text-brand-900 h-4 w-4"
|
||||
className="text-brand h-4 w-4"
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
@@ -308,7 +308,7 @@ export default function IndexPage() {
|
||||
<div className="flex flex-col justify-center lg:px-8">
|
||||
<div>
|
||||
<h3
|
||||
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
className="gradient-text-brand-300 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
uppercase flex items-center gap-4 font-mono"
|
||||
>
|
||||
{planEnterprise.name}
|
||||
@@ -331,11 +331,7 @@ export default function IndexPage() {
|
||||
>
|
||||
{planEnterprise.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck
|
||||
className="text-brand-900 h-4 w-4 "
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<IconCheck className="text-brand h-4 w-4 " aria-hidden="true" strokeWidth={3} />
|
||||
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -407,7 +403,7 @@ export default function IndexPage() {
|
||||
</Link>
|
||||
{addon.name === 'Optimized Compute' ? (
|
||||
<button
|
||||
className="text-brand-1000 text-[13px] leading-4 hover:underline "
|
||||
className="text-brand-600 text-[13px] leading-4 hover:underline "
|
||||
onClick={() => setShowComputeModal(true)}
|
||||
>
|
||||
{addon.rightCtaText}
|
||||
@@ -440,7 +436,7 @@ export default function IndexPage() {
|
||||
className="grid lg:grid-cols-2 gap-8 items-center mt-12 lg:mt-8 max-w-6xl mx-auto"
|
||||
>
|
||||
<div>
|
||||
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 inline-block dark:bg-scale-400 dark:text-scale-1100 py-0.5 px-2 text-[13px] leading-4 mt-2">
|
||||
<span className="bg-brand-300 text-brand-600 rounded-md bg-opacity-30 inline-block dark:bg-scale-400 dark:text-scale-1100 py-0.5 px-2 text-[13px] leading-4 mt-2">
|
||||
Available for Pro plan
|
||||
</span>
|
||||
<h2 className="text-scale-1200 text-4xl mt-4">Cost control with spend caps</h2>
|
||||
@@ -735,7 +731,7 @@ export default function IndexPage() {
|
||||
scope="col"
|
||||
key={plan.name}
|
||||
>
|
||||
<h3 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-mono font-normal uppercase flex items-center gap-4">
|
||||
<h3 className="gradient-text-brand-300 dark:gradient-text-brand-100 text-2xl font-mono font-normal uppercase flex items-center gap-4">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<div
|
||||
@@ -796,7 +792,7 @@ export default function IndexPage() {
|
||||
|
||||
{plan.warning && (
|
||||
<p className="-mt-2">
|
||||
<span className="bg-scale-100 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
<span className="bg-scale-100 text-brand-600 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-[13px] leading-4">
|
||||
{plan.warning}
|
||||
</span>
|
||||
</p>
|
||||
@@ -953,7 +949,7 @@ export default function IndexPage() {
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://supabase.com/dashboard/support/new"
|
||||
className="transition text-brand-900 hover:text-brand-1000"
|
||||
className="transition text-brand hover:text-brand-600"
|
||||
>
|
||||
open a support ticket
|
||||
</a>{' '}
|
||||
@@ -964,7 +960,7 @@ export default function IndexPage() {
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://supabase.com/dashboard/support/new"
|
||||
className="transition text-brand-900 hover:text-brand-1000"
|
||||
className="transition text-brand hover:text-brand-600"
|
||||
>
|
||||
you can contact the team here
|
||||
</a>
|
||||
|
||||
@@ -66,7 +66,7 @@ function RealtimePage() {
|
||||
<div className="bg-scale-300 border-scale-500 relative flex h-[372px] w-[560px] items-center justify-center overflow-hidden rounded border drop-shadow-md">
|
||||
<div
|
||||
className={[
|
||||
'border-brand-800 relative h-12 w-48 bg-[#34B27B]',
|
||||
'border-brand-300 relative h-12 w-48 bg-[#34B27B]',
|
||||
`flex items-center justify-center ${RealtimeStyles['shape']}`,
|
||||
].join(' ')}
|
||||
>
|
||||
|
||||
@@ -38,9 +38,7 @@ const Index = ({}: Props) => {
|
||||
<Container>
|
||||
<SectionContainer className="">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-brand-900 font-mono text-base uppercase tracking-widest">
|
||||
Support
|
||||
</h1>
|
||||
<h1 className="text-brand font-mono text-base uppercase tracking-widest">Support</h1>
|
||||
<h1 className="h1">Get Support</h1>
|
||||
<p className="text-scale-1100 lg:text-lg">
|
||||
The Supabase Support Team is ready to help.
|
||||
@@ -64,8 +62,8 @@ const Index = ({}: Props) => {
|
||||
our GitHub issues. This can be feature requests too!
|
||||
</p>
|
||||
<p className="text-scale-1100 mt-2 text-sm">
|
||||
<span className="text-brand-900 font-bold">Use this for:</span> Bugs and
|
||||
other issues
|
||||
<span className="text-brand font-bold">Use this for:</span> Bugs and other
|
||||
issues
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
@@ -112,8 +110,8 @@ const Index = ({}: Props) => {
|
||||
join us on Discord. Browse and ask questions.
|
||||
</p>
|
||||
<p className="text-scale-1100 mt-2 text-sm">
|
||||
<span className="text-brand-900 text-sm font-bold">Use this for:</span>{' '}
|
||||
General questions
|
||||
<span className="text-brand text-sm font-bold">Use this for:</span> General
|
||||
questions
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
@@ -183,8 +181,8 @@ const Index = ({}: Props) => {
|
||||
or have an issue, please contact us here.
|
||||
</p>
|
||||
<p className="text-scale-1100 mt-2 text-sm">
|
||||
<span className="text-brand-900 text-sm font-bold">Use this for:</span>{' '}
|
||||
Issues or questions specific to you
|
||||
<span className="text-brand text-sm font-bold">Use this for:</span> Issues
|
||||
or questions specific to you
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 153 KiB |
@@ -95,11 +95,11 @@ h6:not(.overwrite) {
|
||||
}
|
||||
|
||||
.heading-gradient-brand {
|
||||
@apply text-[#3ECF8E00] bg-clip-text bg-gradient-to-b from-brand-1000 to-brand-900 dark:to-brand-800;
|
||||
@apply text-[#3ECF8E00] bg-clip-text bg-gradient-to-b from-brand-600 to-brand dark:to-brand-300;
|
||||
}
|
||||
|
||||
.toc-animate {
|
||||
@apply !translate-x-1 !text-brand-900;
|
||||
@apply !translate-x-1 !text-brand;
|
||||
}
|
||||
|
||||
.prose-toc a {
|
||||
@@ -328,7 +328,7 @@ launch week
|
||||
*/
|
||||
|
||||
.launch-week-timeline-border--approaching {
|
||||
border-image: linear-gradient(to bottom, var(--colors-brand9), var(--colors-purple7)) 1 100%;
|
||||
border-image: linear-gradient(to bottom, hsl(var(--brand-default)), var(--colors-purple7)) 1 100%;
|
||||
}
|
||||
|
||||
.launch-week-gradientBg {
|
||||
@@ -472,9 +472,9 @@ launch week
|
||||
filter: blur(calc(var(--auth-ui-card-height) / 6));
|
||||
background-image: linear-gradient(
|
||||
var(--rotate),
|
||||
var(--colors-brand),
|
||||
hsl(var(--brand-default)),
|
||||
#3c67e3 43%,
|
||||
var(--colors-brand)
|
||||
hsl(var(--brand-default))
|
||||
);
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
@@ -683,19 +683,26 @@ launch week
|
||||
}
|
||||
|
||||
[class*='gradient-text-']::selection {
|
||||
background-color: var(--colors-brand9);
|
||||
background-color: hsl(var(--brand-default));
|
||||
background-clip: unset;
|
||||
-webkit-text-fill-color: white;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.gradient-text-brand-100 {
|
||||
background-image: linear-gradient(45deg, var(--colors-brand8) 0%, var(--colors-brand9) 100%);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
hsl(var(--brand-500)) 0%,
|
||||
hsl(var(--brand-default)) 100%
|
||||
);
|
||||
}
|
||||
.gradient-text-brand-500 {
|
||||
background-image: linear-gradient(45deg, var(--colors-brand8) 0%, var(--colors-brand9) 100%);
|
||||
.gradient-text-brand-300 {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
hsl(var(--brand-500)) 0%,
|
||||
hsl(var(--brand-default)) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.gradient-text-scale-100 {
|
||||
background-image: -webkit-linear-gradient(45deg, #fff 0%, var(--colors-green12) 100%);
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ Jos haluat nähdä, miten osallistuminen tapahtuu, käy osoitteessa [Getting Sta
|
||||
|
||||
## Yhteisö ja tuki
|
||||
|
||||
- [Yhteisön foorumi](https://github.com/supabase/supabase/discussions). Paras: Apua rakentamiseen, keskustelua tietokannan parhaista käytännöistä.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Parasta: Supabasea käytettäessä kohdatut viat ja virheet.
|
||||
- [Sähköpostituki](https://supabase.com/docs/support#business-support). Paras: tietokantaan tai infrastruktuuriin liittyvät ongelmat.
|
||||
- [Discord](https://discord.supabase.com). Paras: sovellusten jakamiseen ja yhteisön kanssa hengailuun.
|
||||
- [Yhteisön foorumi](https://github.com/supabase/supabase/discussions). Sopii parhaiten: Apua rakentamiseen, keskustelua tietokannan parhaista käytännöistä.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Sopii parhaiten: Supabasea käytettäessä kohdatut viat ja virheet.
|
||||
- [Sähköpostituki](https://supabase.com/docs/support#business-support). Sopii parhaiten: Tietokantaan tai infrastruktuuriin liittyvät ongelmat.
|
||||
- [Discord](https://discord.supabase.com). Sopii parhaiten: Sovellusten jakamiseen ja yhteisön kanssa hengailuun.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark": "^14.0.2",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"remark-emoji": "^3.1.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-slug": "^7.0.1",
|
||||
"shiki": "^0.11.1",
|
||||
@@ -20160,6 +20161,15 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoticon": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz",
|
||||
"integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
@@ -29753,6 +29763,14 @@
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
||||
"integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"license": "MIT",
|
||||
@@ -34592,6 +34610,19 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-emoji": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-3.1.2.tgz",
|
||||
"integrity": "sha512-QwhAzNk27Ol64uV4z/3n55MKrNz9bhr8wg+mO5aGqIYDS+jUarS1d8Y0ZIeEBVhfGkXj6gGYM+727sOgAPvV/A==",
|
||||
"dependencies": {
|
||||
"emoticon": "^4.0.1",
|
||||
"mdast-util-find-and-replace": "^2.2.2",
|
||||
"node-emoji": "^1.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-external-links": {
|
||||
"version": "8.0.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
--ch-3: #ffcda1;
|
||||
--ch-4: #f8f8f2;
|
||||
--ch-5: #bda4ff;
|
||||
--ch-6: var(--colors-brand10);
|
||||
--ch-6: hsl(var(--brand-600));
|
||||
--ch-7: #569cd6;
|
||||
--ch-8: var(--colors-brand9);
|
||||
--ch-8: hsl(var(--brand-default));
|
||||
--ch-9: #ffa198;
|
||||
--ch-10: #f0f6fc;
|
||||
--ch-11: #490202;
|
||||
@@ -15,7 +15,7 @@
|
||||
--ch-13: #5a1e02;
|
||||
--ch-14: #161b22;
|
||||
--ch-15: #8b949e;
|
||||
--ch-16: var(--colors-scale1);
|
||||
--ch-16: hsl(var(--foreground-lighter));
|
||||
--ch-17: #264f78;
|
||||
--ch-18: #3794ff;
|
||||
--ch-19: #ffffff0b;
|
||||
@@ -34,9 +34,9 @@
|
||||
--ch-3: #953800;
|
||||
--ch-4: #24292f;
|
||||
--ch-5: #8250df;
|
||||
--ch-6: var(--colors-brand10);
|
||||
--ch-6: hsl(var(--brand-600));
|
||||
--ch-7: #cf222e;
|
||||
--ch-8: var(--colors-brand9);
|
||||
--ch-8: hsl(var(--brand-default));
|
||||
--ch-9: #82071e;
|
||||
--ch-10: #f6f8fa;
|
||||
--ch-11: #ffebe9;
|
||||
@@ -44,7 +44,7 @@
|
||||
--ch-13: #ffd8b5;
|
||||
--ch-14: #eaeef2;
|
||||
--ch-15: #57606a;
|
||||
--ch-16: var(--colors-scale1);
|
||||
--ch-16: hsl(var(--foreground-lighter));
|
||||
--ch-17: #add6ff;
|
||||
--ch-18: #1a85ff;
|
||||
--ch-19: #fdff0033;
|
||||
|
||||
@@ -28,8 +28,8 @@ module.exports = {
|
||||
brand12: 'hsla(153, 60%, 95%, 1)',
|
||||
},
|
||||
contrast: {
|
||||
'brand-hiContrast': 'var(--colors-brand-9)',
|
||||
'brand-loContrast': 'var(--colors-brand-4)',
|
||||
'brand-hiContrast': 'hsl(var(--brand-default))',
|
||||
'brand-loContrast': 'hsl(var(--brand-300))',
|
||||
},
|
||||
scale: {
|
||||
scale1: 'var(--colors-slate1)',
|
||||
|
||||
@@ -216,12 +216,12 @@ const uiConfig = ui({
|
||||
fontWeight: '400',
|
||||
color: 'var(--colors-scale12)',
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: 'var(--colors-brand7)',
|
||||
textDecorationColor: 'hsl(var(--brand-400))',
|
||||
textDecorationThickness: '1px',
|
||||
textUnderlineOffset: '4px',
|
||||
},
|
||||
'a:hover': {
|
||||
textDecorationColor: 'var(--colors-brand9)',
|
||||
textDecorationColor: 'hsl(var(--brand-default))',
|
||||
},
|
||||
figcaption: {
|
||||
color: 'var(--colors-scale9)',
|
||||
|
||||
@@ -26,7 +26,7 @@ const excludedRadixColors = [
|
||||
|
||||
// generates fixed scales
|
||||
// based on the root/light mode version
|
||||
const fixedOptions = ['scale', 'scaleA', 'brand']
|
||||
const fixedOptions = ['scale', 'scaleA']
|
||||
|
||||
function radixColorKeys() {
|
||||
let keys = Object.keys(radixUiColors)
|
||||
@@ -51,7 +51,7 @@ function radixColorKeys() {
|
||||
}
|
||||
|
||||
function generateColorClasses() {
|
||||
const brandColors = ['brand', 'scale', 'scaleA']
|
||||
const brandColors = ['scale', 'scaleA']
|
||||
const colors = [...radixColorKeys(), ...brandColors]
|
||||
|
||||
let mappedColors = {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Sun, 30 Jul 2023 09:03:47 GMT
|
||||
*/
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Sun, 30 Jul 2023 09:03:47 GMT
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--brand-default: 152.9deg 60% 52.9%;
|
||||
--brand-400: 153.6deg 41% 12%;
|
||||
--brand-300: 152.7deg 48.5% 26.7%;
|
||||
--brand-200: 148.5deg 43.5% 82%;
|
||||
--brand-600: 153deg 59.5% 70%;
|
||||
--brand-500: 153.5deg 61.8% 21.6%;
|
||||
--brand-400: 153.3deg 65.2% 13.5%;
|
||||
--brand-300: 153.8deg 69.6% 9%;
|
||||
--brand-200: 152.5deg 75% 6.3%;
|
||||
--warning-default: 38.9deg 100% 57.1%;
|
||||
--warning-600: 38.9deg 89.8% 49.8%;
|
||||
--warning-500: 34.8deg 90.9% 21.6%;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Sun, 30 Jul 2023 09:03:47 GMT
|
||||
*/
|
||||
|
||||
.light {
|
||||
--brand-primary: 152.9deg 60% 52.9%;
|
||||
--brand-400: 153.7deg 68.7% 83.7%;
|
||||
--brand-300: 153deg 46% 65.9%;
|
||||
--brand-200: 148.5deg 43.5% 82%;
|
||||
--brand-default: 152.9deg 60% 52.9%;
|
||||
--brand-600: 153.2deg 49.7% 33.5%;
|
||||
--brand-500: 148.7deg 42.7% 69.2%;
|
||||
--brand-400: 149.1deg 59.3% 88.4%;
|
||||
--brand-300: 148.6deg 77.8% 94.7%;
|
||||
--brand-200: 156deg 71.4% 98.6%;
|
||||
--warning-default: 38.9deg 100% 57.1%;
|
||||
--warning-600: 30.2deg 100% 33.9%;
|
||||
--warning-500: 36.3deg 85.7% 67.1%;
|
||||
|
||||
@@ -529,15 +529,23 @@ module.exports = {
|
||||
},
|
||||
"brand-200": {
|
||||
cssVariable: "var(--brand-200)",
|
||||
value: "hsl(148.5, 43.5%, 82%)"
|
||||
value: "hsl(152.5, 75%, 6.3%)"
|
||||
},
|
||||
"brand-300": {
|
||||
cssVariable: "var(--brand-300)",
|
||||
value: "hsl(152.70000000000005, 48.5%, 26.7%)"
|
||||
value: "hsl(153.79999999999995, 69.6%, 9%)"
|
||||
},
|
||||
"brand-400": {
|
||||
cssVariable: "var(--brand-400)",
|
||||
value: "hsl(153.60000000000002, 41%, 12%)"
|
||||
value: "hsl(153.29999999999995, 65.2%, 13.5%)"
|
||||
},
|
||||
"brand-500": {
|
||||
cssVariable: "var(--brand-500)",
|
||||
value: "hsl(153.5, 61.8%, 21.6%)"
|
||||
},
|
||||
"brand-600": {
|
||||
cssVariable: "var(--brand-600)",
|
||||
value: "hsl(153, 59.5%, 70%)"
|
||||
},
|
||||
"brand-DEFAULT": {
|
||||
cssVariable: "var(--brand-default)",
|
||||
|
||||
@@ -21,7 +21,7 @@ export const Admonition = ({
|
||||
: type === 'info'
|
||||
? 'bg-scale-500 border-scale-800'
|
||||
: type === 'tip'
|
||||
? 'bg-brand-300 border-brand-800'
|
||||
? 'bg-brand-300 border-brand-300'
|
||||
: type === 'caution'
|
||||
? 'bg-yellow-400 border-yellow-800'
|
||||
: type === 'danger'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* .sbui-badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500 bg-opacity-10 text-brand-800;
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500 bg-opacity-10 text-brand-300;
|
||||
}
|
||||
|
||||
.sbui-badge--large {
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
.sbui-breadcrumb--item {
|
||||
@apply text-gray-500 hover:text-gray-600;
|
||||
@apply cursor-pointer;
|
||||
@apply dark:text-dark-200 dark:hover:text-brand-700;
|
||||
@apply dark:text-dark-200 dark:hover:text-brand-400;
|
||||
}
|
||||
|
||||
.sbui-breadcrumb--item__active {
|
||||
@apply text-brand-700 dark:text-brand-700;
|
||||
@apply text-brand-400 dark:text-brand-400;
|
||||
}
|
||||
|
||||
.sbui-breadcrumb--separator {
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
Button varients
|
||||
*/
|
||||
/* .sbui-btn-primary {
|
||||
@apply text-white bg-brand-800 hover:bg-brand-600;
|
||||
@apply dark:hover:bg-brand-900;
|
||||
@apply text-white bg-brand-300 hover:bg-brand-600;
|
||||
@apply dark:hover:bg-brand;
|
||||
} */
|
||||
.dark .sbui-btn-primary {
|
||||
text-shadow: 0px 0px 4px rgba(38 111 78 / 50%);
|
||||
@@ -87,7 +87,7 @@
|
||||
@apply dark:text-white dark:border-dark-400 dark:hover:text-white dark:hover:border-white;
|
||||
} */
|
||||
/* .sbui-btn-link {
|
||||
@apply bg-transparent text-brand-800 border-none hover:bg-brand-900 hover:bg-opacity-25;
|
||||
@apply bg-transparent text-brand-300 border-none hover:bg-brand hover:bg-opacity-25;
|
||||
box-shadow: none;
|
||||
} */
|
||||
/* .sbui-btn-text {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cn } from '@ui/lib/utils'
|
||||
export type ButtonVariantProps = VariantProps<typeof buttonVariants>
|
||||
const buttonVariants = cva(
|
||||
`relative
|
||||
flex iems-center justify-center
|
||||
flex items-center justify-center
|
||||
cursor-pointer
|
||||
inline-flex
|
||||
items-center
|
||||
@@ -30,9 +30,9 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
type: {
|
||||
primary: `
|
||||
bg-brand-fixed-1100 hover:bg-brand-fixed-1000
|
||||
bg-brand-600 dark:bg-brand/70 hover:bg-brand-600/80 dark:hover:bg-brand
|
||||
text-white
|
||||
border-brand-fixed-1000 hover:border-brand-fixed-900 dark:border-brand-fixed-1000 dark:hover:border-brand-fixed-1000
|
||||
border-brand dark:border-brand
|
||||
focus-visible:outline-brand-600
|
||||
shadow-sm`,
|
||||
secondary: `
|
||||
@@ -51,11 +51,11 @@ const buttonVariants = cva(
|
||||
focus-visible:outline-brand-600
|
||||
shadow-sm`,
|
||||
alternative: `
|
||||
text-brand-1100
|
||||
text-brand-600
|
||||
bg-brand-200 hover:bg-brand-400
|
||||
border-brand-600 hover:border-brand-800
|
||||
dark:border-brand-700 hover:dark:border-brand-800
|
||||
focus-visible:border-brand-800
|
||||
border-brand-600 hover:border-brand-300
|
||||
dark:border-brand-400 hover:dark:border-brand-300
|
||||
focus-visible:border-brand-300
|
||||
focus-visible:outline-brand-600
|
||||
shadow-sm`,
|
||||
outline: `
|
||||
@@ -73,7 +73,7 @@ const buttonVariants = cva(
|
||||
focus-visible:outline-scale-700
|
||||
shadow-sm`,
|
||||
link: `
|
||||
text-brand-1100
|
||||
text-brand-600
|
||||
border
|
||||
border-transparent
|
||||
hover:bg-brand-400
|
||||
|
||||