mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Merge branch 'master' into docs/connect-supabase
This commit is contained in:
commit
e6cd1773a3
486 files changed
+11733
-4394
No files matched your search
@@ -235,7 +235,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Nepali / नेपाली](/i18n/README.ne.md)
|
||||
- [Indonesian / Bahasa Indonesia](/i18n/README.id.md)
|
||||
- [Italiano / Italian](/i18n/README.it.md)
|
||||
- [Japanese / 日本語](/i18n/README.ja.md)
|
||||
- [Japanese / 日本語](/i18n/README.jp.md)
|
||||
- [Korean / 한국어](/i18n/README.ko.md)
|
||||
- [Lithuanian / lietuvių](/i18n/README.lt.md)
|
||||
- [Latvian / latviski](/i18n/README.lv.md)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -28,6 +28,11 @@ const HomePageCover = (props) => {
|
||||
icon: '/docs/img/icons/flutter-icon',
|
||||
href: '/guides/getting-started/quickstarts/flutter',
|
||||
},
|
||||
{
|
||||
tooltip: 'Android Kotlin',
|
||||
icon: '/docs/img/icons/kotlin-icon',
|
||||
href: '/guides/getting-started/quickstarts/kotlin',
|
||||
},
|
||||
{
|
||||
tooltip: 'SvelteKit',
|
||||
icon: '/docs/img/icons/svelte-icon',
|
||||
@@ -69,7 +74,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>
|
||||
|
||||
@@ -72,12 +72,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
href: '/guides/platform',
|
||||
level: 'platform',
|
||||
},
|
||||
{
|
||||
label: 'Resources',
|
||||
icon: 'resources',
|
||||
href: '/guides/resources',
|
||||
level: 'resources',
|
||||
},
|
||||
{
|
||||
label: 'Self-Hosting',
|
||||
icon: 'self-hosting',
|
||||
@@ -130,7 +124,25 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Tools',
|
||||
label: 'Resources',
|
||||
},
|
||||
{
|
||||
label: 'CLI Commands',
|
||||
icon: 'reference-cli',
|
||||
href: '/reference/cli/introduction',
|
||||
level: 'reference_javascript',
|
||||
},
|
||||
{
|
||||
label: 'Management API',
|
||||
icon: 'reference-api',
|
||||
href: '/reference/api/introduction',
|
||||
level: 'reference_javascript',
|
||||
},
|
||||
{
|
||||
label: 'Guides and Examples',
|
||||
icon: 'resources',
|
||||
href: '/guides/resources',
|
||||
level: 'resources',
|
||||
},
|
||||
{
|
||||
label: 'Integrations',
|
||||
@@ -139,12 +151,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
href: 'https://supabase.com/partners/integrations',
|
||||
level: 'integrations',
|
||||
},
|
||||
{
|
||||
label: 'Management API',
|
||||
icon: 'reference-api',
|
||||
href: '/reference/api/introduction',
|
||||
level: 'reference_javascript',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -550,6 +556,10 @@ export const database: NavMenuConstant = {
|
||||
name: 'Postgres Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Analyzing efficiency and performance',
|
||||
url: '/guides/database/inspect',
|
||||
},
|
||||
{
|
||||
name: 'JSON and unstructured data',
|
||||
url: '/guides/database/json',
|
||||
@@ -663,10 +673,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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -897,6 +920,10 @@ export const ai: NavMenuConstant = {
|
||||
items: [
|
||||
{ name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' },
|
||||
{ name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' },
|
||||
{
|
||||
name: 'Generate Embeddings',
|
||||
url: '/guides/ai/quickstarts/generate-text-embeddings',
|
||||
},
|
||||
{ name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
|
||||
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
|
||||
],
|
||||
@@ -983,16 +1010,34 @@ export const supabase_cli: NavMenuConstant = {
|
||||
title: 'Local Dev / CLI',
|
||||
url: '/guides/cli',
|
||||
items: [
|
||||
{ name: 'Getting started', url: '/guides/cli' },
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development' },
|
||||
{ name: 'Managing environments', url: '/guides/cli/managing-environments' },
|
||||
{ name: 'Overview', url: '/guides/cli' },
|
||||
{
|
||||
name: 'Managing config and secrets',
|
||||
url: '/guides/cli/managing-config',
|
||||
name: 'Using the CLI',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Getting started', url: '/guides/cli/getting-started' },
|
||||
{ name: 'CLI Configuration', url: '/guides/cli/config' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Testing emails locally',
|
||||
url: '/guides/cli/testing-emails',
|
||||
name: 'Developing with Supabase',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development' },
|
||||
{ name: 'Managing environments', url: '/guides/cli/managing-environments' },
|
||||
{
|
||||
name: 'Managing config and secrets',
|
||||
url: '/guides/cli/managing-config',
|
||||
},
|
||||
{
|
||||
name: 'Seeding your database',
|
||||
url: '/guides/cli/seeding-your-database',
|
||||
},
|
||||
{
|
||||
name: 'Testing and linting',
|
||||
url: '/guides/cli/testing-and-linting',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitHub Action',
|
||||
@@ -1002,14 +1047,14 @@ export const supabase_cli: NavMenuConstant = {
|
||||
name: 'Generate types from your database',
|
||||
url: '/guides/cli/github-action/generating-types',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Reference',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Commands', url: '/reference/cli/introduction' },
|
||||
{ name: 'Configuration', url: '/reference/cli/config' },
|
||||
{
|
||||
name: 'Automated testing',
|
||||
url: '/guides/cli/github-action/testing',
|
||||
},
|
||||
{
|
||||
name: 'Backup your database',
|
||||
url: '/guides/cli/github-action/backups',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//import { Step } from 'next-seo/lib/types'
|
||||
import React, { FC } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface IStep {
|
||||
title: string
|
||||
@@ -13,7 +14,9 @@ interface IStepHikeCompactSubcomponents {
|
||||
}
|
||||
interface IDetails {
|
||||
title?: string
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
interface ICode {}
|
||||
|
||||
interface IStepHikeCompact {
|
||||
@@ -74,9 +77,9 @@ const Step: FC<IStep> = ({ children, title, step }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const Details: FC<IDetails> = ({ children, title }) => {
|
||||
const Details: FC<IDetails> = ({ children, title, fullWidth = false }) => {
|
||||
return (
|
||||
<div className="col-span-5 ml-12 lg:ml-0">
|
||||
<div className={cn(fullWidth ? 'col-span-12' : 'col-span-5', 'ml-12', 'lg:ml-0')}>
|
||||
<h3 className="mt-0 text-scale-1200 text-base">{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -12,20 +12,18 @@ hideTitle: true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's C# library,
|
||||
[supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to
|
||||
interact with your Postgres database, listen to database changes, invoke Deno Edge Functions,
|
||||
build login and user management functionality, and manage large files.
|
||||
This reference documents every object and method available in Supabase's C# library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
<p>The C# client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
|
||||
|
||||
<p>
|
||||
Huge thanks to official maintainer, [Joseph Schultz](https://github.com/acupofjose), and to [Ben
|
||||
Randall](https://github.com/veleek) and [Rhuan Barros](https://github.com/rhuanbarros) for their
|
||||
help.
|
||||
</p>
|
||||
<p>
|
||||
The C# client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Huge thanks to official maintainer, [Joseph Schultz](https://github.com/acupofjose), and to [Ben Randall](https://github.com/veleek) and [Rhuan Barros](https://github.com/rhuanbarros) for their help.
|
||||
</p>
|
||||
</div>
|
||||
@@ -12,13 +12,9 @@ hideTitle: true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
|
||||
This reference documents every object and method available in Supabase's Flutter
|
||||
library, [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can
|
||||
use supabase-flutter to interact with your Postgres database, listen to database changes, invoke
|
||||
Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Flutter projects.
|
||||
|
||||
This reference documents every object and method available in Supabase's Flutter library, [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can use supabase-flutter to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Flutter projects.
|
||||
</div>
|
||||
@@ -12,17 +12,12 @@ hideTitle: true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
|
||||
<Admonition type="caution">
|
||||
You're viewing the docs for an older version of the `supabase-flutter` library. Learn how to
|
||||
[upgrade to the latest version](/docs/reference/dart/v0/upgrade-guide).
|
||||
</Admonition>
|
||||
|
||||
This reference documents every object and method available in Supabase's Flutter library, [supabase-flutter](https://pub.dev/packages/supabase_flutter).
|
||||
You can use supabase-flutter to interact with your Postgres database, listen to database changes, invoke
|
||||
Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Flutter projects.
|
||||
|
||||
<Admonition type="caution">
|
||||
You're viewing the docs for an older version of the `supabase-flutter` library. Learn how to [upgrade to the latest version](/docs/reference/dart/v0/upgrade-guide).
|
||||
</Admonition>
|
||||
This reference documents every object and method available in Supabase's Flutter library, [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can use supabase-flutter to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Flutter projects.
|
||||
</div>
|
||||
@@ -7,14 +7,12 @@ hideTitle: true
|
||||
<div className="flex items-start gap-6 not-prose" id="introduction">
|
||||
<IconMenuJavascript width={35} height={35} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-scale-1200 m-0">Javascript Client Library</h1>
|
||||
<h1 className="text-3xl text-scale-1200 m-0">JavaScript Client Library</h1>
|
||||
<h2 className="text-base font-mono text-scale-1100">@supabase/supabase-js</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's isomorphic JavaScript
|
||||
library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to
|
||||
database changes, invoke Deno Edge Functions, build login and user management functionality, and
|
||||
manage large files.
|
||||
This reference documents every object and method available in Supabase's isomorphic JavaScript library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
</div>
|
||||
@@ -1,23 +1,26 @@
|
||||
---
|
||||
id: typescript-support
|
||||
title: Typescript Support
|
||||
title: TypeScript Support
|
||||
---
|
||||
|
||||
`supabase-js` supports Typescript.
|
||||
`supabase-js` has TypeScript support for type inference, autocompletion, type-safe queries, and more.
|
||||
|
||||
With TypeScript, `supabase-js` detects things like `not null` constraints and [generated columns](https://www.postgresql.org/docs/current/ddl-generated-columns.html). Nullable columns are typed as `T | null` when you select the column. Generated columns will show a type error when you insert to it.
|
||||
|
||||
`supabase-js` also detects relationships between tables. A foreign table with one-to-many relationship is typed as `T[]`. Likewise, a foreign table with many-to-one relationship is typed as `T | null`.
|
||||
|
||||
## Generating types
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
You can use our CLI to generate types:
|
||||
You can use the Supabase CLI to [generate the types](/docs/reference/cli/supabase-gen-types-typescript). You can also generate the types [from the dashboard](https://supabase.com/dashboard/project/_/api?page=tables-intro).
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
|
||||
```bash Terminal
|
||||
supabase start
|
||||
supabase gen types typescript --local > lib/database.types.ts
|
||||
supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.ts
|
||||
```
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
@@ -26,25 +29,41 @@ title: Typescript Support
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
These types are generated directly from your database.
|
||||
|
||||
There is a difference between `selects`, `inserts`, and `updates`, because often you will set default values in your database for specific columns.
|
||||
With default values you do not need to send any data over the network, even if that column is a "required" field. Our type system is granular
|
||||
enough to handle these situations.
|
||||
These types are generated from your database schema. Given a table `public.movies`, the generated types will look like:
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
|
||||
Given a table `public.movies`, the definition will provide the following data:
|
||||
```sql
|
||||
create table public.movies (
|
||||
id bigint generated always as identity primary key,
|
||||
name text not null,
|
||||
data jsonb null
|
||||
);
|
||||
```
|
||||
|
||||
```ts /types.ts
|
||||
interface Database {
|
||||
```ts ./database.types.ts
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
movies: {
|
||||
Row: {} // The data expected to be returned from a "select" statement.
|
||||
Insert: {} // The data expected passed to an "insert" statement.
|
||||
Update: {} // The data expected passed to an "update" statement.
|
||||
Row: { // the data expected from .select()
|
||||
id: number
|
||||
name: string
|
||||
data: Json | null
|
||||
}
|
||||
Insert: { // the data to be passed to .insert()
|
||||
id?: never // generated columns must not be supplied
|
||||
name: string // `not null` columns with no default must be supplied
|
||||
data?: Json | null // nullable columns can be omitted
|
||||
}
|
||||
Update: { // the data to be passed to .update()
|
||||
id?: never
|
||||
name?: string // `not null` columns are optional on .update()
|
||||
data?: Json | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,18 +73,18 @@ title: Typescript Support
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
|
||||
## Injecting type definitions
|
||||
## Using type definitions
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
You can enrich the supabase client with the types you generated with Supabase.
|
||||
You can supply the type definitions to `supabase-js` like so:
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
```ts ./index.tsx
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Database } from 'lib/database.types'
|
||||
import { Database } from './database.types'
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
process.env.SUPABASE_URL,
|
||||
@@ -76,52 +95,94 @@ title: Typescript Support
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
|
||||
## Type hints
|
||||
## Helper types
|
||||
|
||||
You can use the following helper types to make the TypeScript support easier to use.
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
`supabase-js` always returns a `data` object (for success), and an `error` response (for unsuccessful requests).
|
||||
|
||||
This provides a simple interface to get the relevant types returned from any function:
|
||||
Sometimes the generated types are not what you expect. For example, a view's column may show up as nullable when you expect it to be `not null`. Using [type-fest](https://github.com/sindresorhus/type-fest), you can override the types like so:
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
```ts
|
||||
export async function getMovies() {
|
||||
return await supabase.from('movies').select(`id, title`)
|
||||
```ts ./database-generated.types.ts
|
||||
export type Json = // ...
|
||||
|
||||
export interface Database {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
type MoviesResponse = Awaited<ReturnType<typeof getMovies>>
|
||||
export type MoviesResponseSuccess = MoviesResponse['data']
|
||||
export type MoviesResponseError = MoviesResponse['error']
|
||||
```ts ./database.types.ts
|
||||
import { MergeDeep } from 'type-fest'
|
||||
import { Database as DatabaseGenerated } from './database-generated.types'
|
||||
export { Json } from './database-generated.types'
|
||||
|
||||
````
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
|
||||
## Nested tables
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
For advanced queries such as nested tables, you may want to construct your own types.
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
```ts ./index.ts
|
||||
import supabase from '~/lib/supabase'
|
||||
import type { Database } from '~/lib/database.types'
|
||||
|
||||
async function getMovies() {
|
||||
return await supabase.from('movies').select('id, title, actors(\*)')
|
||||
export type Database = MergeDeep<
|
||||
DatabaseGenerated,
|
||||
{
|
||||
public: {
|
||||
Views: {
|
||||
movies_view: {
|
||||
Row: {
|
||||
// id is a primary key in public.movies, so it must be `not null`
|
||||
id: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
```
|
||||
|
||||
type Actors = Database['public']['tables']['actors']['row']
|
||||
type MoviesResponse = Awaited<ReturnType<typeof getMovies>>
|
||||
type MoviesResponseSuccess = MoviesResponse['data'] & {
|
||||
actors: Actors[]
|
||||
}
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
It's convenient to have shorthands for your most-used types.
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
```ts ./database.types.ts
|
||||
export type Tables<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Row']
|
||||
export type Enums<T extends keyof Database['public']['Enums']> = Database['public']['Enums'][T]
|
||||
// etc.
|
||||
```
|
||||
|
||||
```ts ./index.ts
|
||||
// Before 😕
|
||||
let movie: Database['public']['Tables']['movies']['Row'] = // ...
|
||||
|
||||
// After 😍
|
||||
let movie: Tables<'movies'>
|
||||
```
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
`supabase-js` always returns a `data` object (for success), and an `error` object (for unsuccessful requests).
|
||||
|
||||
These helper types provide the result types from any query:
|
||||
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
```ts
|
||||
import { PostgrestError } from '@supabase/supabase-js'
|
||||
|
||||
export type DbResult<T> = T extends PromiseLike<infer U> ? U : never
|
||||
export type DbResultOk<T> = T extends PromiseLike<{ data: infer U }> ? Exclude<U, null> : never
|
||||
export type DbResultErr = PostgrestError
|
||||
```
|
||||
|
||||
```ts
|
||||
const query = supabase.from('movies').select(`id, title`)
|
||||
const movies: DbResult<typeof query> = await query
|
||||
```
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
|
||||
@@ -7,21 +7,18 @@ hideTitle: true
|
||||
<div className="flex items-start gap-6 not-prose" id="introduction">
|
||||
<IconMenuJavascript width={35} height={35} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-scale-1200 m-0">Javascript Client Library</h1>
|
||||
<h1 className="text-3xl text-scale-1200 m-0">JavaScript Client Library</h1>
|
||||
<h2 className="text-base font-mono text-scale-1100">@supabase/supabase-js</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
<Admonition type="caution">
|
||||
You're viewing the docs for an older version of the `supabase-js` library. Learn how to [upgrade to the latest version](/docs/reference/javascript/v1/upgrade-guide).
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="caution">
|
||||
You're viewing the docs for an older version of the `supabase-js` library. Learn how to [upgrade
|
||||
to the latest version](/docs/reference/javascript/v1/upgrade-guide).
|
||||
</Admonition>
|
||||
|
||||
This reference documents every object and method available in Supabase's isomorphic JavaScript
|
||||
library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to
|
||||
database changes, invoke Deno Edge Functions, build login and user management functionality, and
|
||||
manage large files.
|
||||
|
||||
<p>
|
||||
This reference documents every object and method available in Supabase's isomorphic JavaScript library, supabase-js. You can use supabase-js to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
</p>
|
||||
</div>
|
||||
@@ -12,16 +12,19 @@ hideTitle: true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
|
||||
|
||||
<p>
|
||||
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
</p>
|
||||
<p>
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
<p>The Swift client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
|
||||
|
||||
<p>Huge thanks to official maintainer, [Maail](https://github.com/maail).</p>
|
||||
|
||||
<p>Huge thanks to official maintainer, [Maail](https://github.com/maail).</p>
|
||||
</div>
|
||||
@@ -12,9 +12,7 @@ hideTitle: true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's C#
|
||||
library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can
|
||||
use supabase-csharp to interact with your Postgres database, listen to database changes, invoke
|
||||
Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
This reference documents every object and method available in Supabase's C# library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
</div>
|
||||
@@ -17,8 +17,8 @@ You can use the `supabase-flutter` library to:
|
||||
|
||||
## For non-Flutter projects
|
||||
|
||||
We also have [supabase-dart](https://github.com/supabase-community/supabase-dart) for non-Flutter Dart projects, such as server-side Dart or Angular-Dart.
|
||||
supabase-dart shares most of the APIs with supabase-flutter without being dependent on Flutter so that you can use Supabase anywhere you can run Dart!
|
||||
{/* prettier-ignore */}
|
||||
We also have [supabase-dart](https://github.com/supabase-community/supabase-dart) for non-Flutter Dart projects, such as server-side Dart or Angular-Dart. supabase-dart shares most of the APIs with supabase-flutter without being dependent on Flutter so that you can use Supabase anywhere you can run Dart!
|
||||
|
||||
## Additional Links
|
||||
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -14,6 +14,7 @@ The toolkit includes:
|
||||
|
||||
- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector.
|
||||
- A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings.
|
||||
- An [embedding generation](/docs/guides/ai/quickstarts/generate-text-embeddings) process using open source models directly in Edge Functions.
|
||||
- [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings.
|
||||
- Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
sidebar_label: 'Concepts',
|
||||
}
|
||||
|
||||
Embeddings are a core concept when building AI and vector applications.
|
||||
Embeddings are core to many AI and vector applications. This guide covers these concepts. If you prefer to get started right away, see our guide on [Generating Embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
|
||||
## What are embeddings?
|
||||
|
||||
@@ -48,7 +48,7 @@ In this example we only have 2 dimensions: the X and Y axis. In reality, we woul
|
||||
|
||||
## Using embeddings
|
||||
|
||||
Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example OpenAI's `text-embedding-ada-002` model outputs 1536 dimensions.
|
||||
Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model outputs 384 dimensions.
|
||||
|
||||
Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A common use case for this is search. Your process might look something like this:
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ We can now create a foreign table to access the data in our secondary project.
|
||||
```sql
|
||||
create foreign table docs (
|
||||
id text not null,
|
||||
embedding vector(1536),
|
||||
embedding vector(384),
|
||||
metadata jsonb,
|
||||
url text
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ Let's prepare the database schema. We can use the "OpenAI Vector Search" quickst
|
||||
|
||||
<StepHikeCompact.Details title="Set up Supabase locally">
|
||||
|
||||
Make sure you have the latest version of the [Supabase CLI installed]().
|
||||
Make sure you have the latest version of the [Supabase CLI installed](/docs/guides/cli/getting-started).
|
||||
|
||||
Initialize Supabase in the root directory of your app.
|
||||
|
||||
|
||||
@@ -2,14 +2,18 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-hugging-face',
|
||||
title: 'Hugging Face',
|
||||
title: 'Hugging Face Inference API',
|
||||
description: 'Learn how to integrate hugging face models with Supabase',
|
||||
sidebar_label: 'Hugging Face',
|
||||
}
|
||||
|
||||
[Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application.
|
||||
|
||||
Hugging Face exposes an [Inference API](https://huggingface.co/inference-api) you can use to execute AI tasks remotely on Hugging Face servers. This opens the doors to using Hugging Face with languages like TypeScript and can be deployed using [Edge Functions](/docs/guides/functions).
|
||||
There are 3 ways to use Hugging Face models in your application:
|
||||
|
||||
1. Use the [Transformers](https://huggingface.co/docs/transformers/index) Python library to perform inference in a Python backend.
|
||||
1. [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) directly in Edge Functions using Transformers.js.
|
||||
1. Use Hugging Face's hosted [Inference API](https://huggingface.co/inference-api) to execute AI tasks remotely on Hugging Face servers. This guide will walk you through this approach.
|
||||
|
||||
## AI Tasks
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-generating-embeddings',
|
||||
title: 'Generate Embeddings',
|
||||
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required.
|
||||
|
||||
## What is Transformers.js?
|
||||
|
||||
[Transformers.js](https://huggingface.co/docs/transformers.js/index) is a library that allows you to perform inference using the JavaScript runtime. You can run Hugging Face embedding models through Transformers.js directly in Supabase [Edge Functions](/docs/guides/functions).
|
||||
|
||||
## Build the Edge Function
|
||||
|
||||
Let's build an Edge Function that will accept an input string and generate an embedding for it. Edge Functions are server-side TypeScript HTTP endpoints that run on-demand closest to your users.
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Set up Supabase locally">
|
||||
|
||||
Make sure you have the latest version of the [Supabase CLI installed](/docs/guides/cli/getting-started).
|
||||
|
||||
Initialize Supabase in the root directory of your app and start your local stack.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```shell
|
||||
supabase init
|
||||
supabase start
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Create Edge Function">
|
||||
|
||||
Create an Edge Function that we will use to generate embeddings. We'll call this `embed` (you can name this anything you like).
|
||||
|
||||
This will create a new TypeScript file called `index.ts` under `./supabase/functions/embed`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```shell
|
||||
supabase functions new embed
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Import & configure Transformer.js" fullWidth>
|
||||
|
||||
Let's modify the Edge Function to import the Transformers.js client and configure it for the Deno runtime.
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { env, pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.0'
|
||||
|
||||
// Configuration for Deno runtime
|
||||
env.useBrowserCache = false;
|
||||
env.allowLocalModels = false;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="Construct embedding pipeline">
|
||||
|
||||
Next let's construct the `pipe()` function we'll use to generate the embeddings. We use the Transformer.js `pipeline()` function to create this.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
const pipe = await pipeline(
|
||||
'feature-extraction',
|
||||
'Supabase/gte-small',
|
||||
);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two arguments we pass to `pipeline()`:
|
||||
|
||||
1. The first argument specifies the type of [inference task](https://huggingface.co/docs/transformers.js/index#tasks) to perform. `feature-extraction` is used for embedding generation.
|
||||
2. The second argument specifies which Hugging Face model to use for the embedding generation. Supabase officially supports models available on the [Supabase org](https://huggingface.co/Supabase).
|
||||
|
||||
<Admonition>
|
||||
|
||||
We intentially construct the pipeline before the `serve()` function. This allows us to reuse the pre-loaded model in future warm-start requests, significantly speeding up future queries. Note that you'll only notice this speedup after deploying your function to Supabase - requests in local development will always be a cold-start.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Implement request handler">
|
||||
|
||||
Modify our `serve()` request handler to accept an `input` string from the POST request JSON body.
|
||||
|
||||
Finally let's generate the embedding by:
|
||||
|
||||
1. Calling `pipe()` to perform the embedding generation
|
||||
2. Extracting the embedding from the output as a number array
|
||||
3. Returning it as a JSON response
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
serve(async (req) => {
|
||||
// Extract input string from JSON body
|
||||
const { input } = await req.json();
|
||||
|
||||
// Generate the embedding from the user input
|
||||
const output = await pipe(input, {
|
||||
pooling: 'mean',
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
// Extract the embedding output
|
||||
const embedding = Array.from(output.data);
|
||||
|
||||
// Return the embedding
|
||||
return new Response(
|
||||
JSON.stringify({ embedding }),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two options we pass to `pipe()`:
|
||||
|
||||
- `pooling`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings and is the method supported by Transformers.js.
|
||||
- `normalize`: The second option tells Transformers.js to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
|
||||
<StepHikeCompact.Details title="Test it!">
|
||||
|
||||
To test the Edge Function, first start a local functions server.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```shell
|
||||
supabase functions serve
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Then in a new shell, create an HTTP request using cURL and pass in your input in the JSON body.
|
||||
|
||||
```shell
|
||||
curl --request POST 'http://localhost:54321/functions/v1/embed' \
|
||||
--header 'Authorization: Bearer ANON_KEY' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "input": "hello world" }'
|
||||
```
|
||||
|
||||
Be sure to replace `ANON_KEY` with your project's anonymous key. You can get this key by running `supabase status`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Learn more about [embedding concepts](/docs/guides/ai/concepts)
|
||||
- [Store your embeddings](/docs/guides/ai/vector-columns) in a database
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -56,27 +56,37 @@ create table documents (
|
||||
id serial primary key,
|
||||
title text not null,
|
||||
body text not null,
|
||||
embedding vector(1536)
|
||||
embedding vector(384)
|
||||
);
|
||||
```
|
||||
|
||||
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 1536 dimensions. Change this to the number of dimensions used in your vector application. For example, if you are generating embeddings using OpenAI's `text-embeddings-ada-002` model, you would set this number as 1536 since that model produces 1536 dimensions.
|
||||
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 384 dimensions. Change this to the number of dimensions produced by your embedding model. For example, if you are [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, you would set this number to 384 since that model produces 384 dimensions.
|
||||
|
||||
<Admonition>
|
||||
|
||||
In general, embeddings with fewer dimensions perform best. See our [analysis on fewer dimensions in pgvector](https://supabase.com/blog/fewer-dimensions-are-better-pgvector).
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Storing a vector / embedding
|
||||
|
||||
In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase JavaScript client.
|
||||
In this example we'll generate a vector using Transformers.js, then store it in the database using the Supabase JavaScript client.
|
||||
|
||||
```js
|
||||
import { pipeline } from '@xenova/transformers'
|
||||
const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small')
|
||||
|
||||
const title = 'First post!'
|
||||
const body = 'Hello world!'
|
||||
|
||||
// Generate a vector using OpenAI
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: body,
|
||||
// Generate a vector using Transformers.js
|
||||
const output = await generateEmbedding(body, {
|
||||
pooling: 'mean',
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
// Extract the embedding output
|
||||
const embedding = Array.from(output.data)
|
||||
|
||||
// Store the vector in Postgres
|
||||
const { data, error } = await supabase.from('documents').insert({
|
||||
@@ -90,7 +100,7 @@ This example uses the JavaScript Supabase client, but you can modify it to work
|
||||
|
||||
### Querying a vector / embedding
|
||||
|
||||
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for performing similarity search:
|
||||
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for computing distance:
|
||||
|
||||
| Operator | Description |
|
||||
| -------- | ---------------------- |
|
||||
@@ -98,13 +108,13 @@ Similarity search is the most common use case for vectors. `pgvector` support 3
|
||||
| `<#>` | negative inner product |
|
||||
| `<=>` | cosine distance |
|
||||
|
||||
Choosing the right operator depends on your needs. If you are searching over OpenAI embeddings, OpenAI recommends using cosine similarity. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
Choosing the right operator depends on your needs. Dot product tends to be the fastest if your vectors are normalized. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
Supabase client libraries like `supabase-js` connect to your Postgres instance via [PostgREST](docs/guides/getting-started/architecture#postgrest-api). PostgREST does not currently support `pgvector` similarity operators, so we'll need to wrap our query in a Postgres function and call it via the `rpc()` method:
|
||||
|
||||
```sql
|
||||
create or replace function match_documents (
|
||||
query_embedding vector(1536),
|
||||
query_embedding vector(384),
|
||||
match_threshold float,
|
||||
match_count int
|
||||
)
|
||||
@@ -140,9 +150,15 @@ const { data: documents } = await supabaseClient.rpc('match_documents', {
|
||||
})
|
||||
```
|
||||
|
||||
In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself (using `openai.createEmbedding()`), then pass it into the above `rpc()` function to match.
|
||||
In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself, then pass it into the above `rpc()` function to match.
|
||||
|
||||
Vectors and embedding can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
<Admonition>
|
||||
|
||||
Be sure to use embeddings produced from the same embedding model when calculating distance. Comparing embeddings from two different models will produce no meaningful result.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Vectors and embeddings can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
### Indexes
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -329,7 +329,7 @@ In some cases, you may wish to use WhatsApp as a delivery channel instead. Here
|
||||
- You wish for a secure channel
|
||||
- Your users mostly use WhatsApp as a messaging platform
|
||||
|
||||
To make use of WhatsApp OTP, please complete the following steps:
|
||||
WhatsApp OTPs can be used with Twilio Verify without additional configuration. To make use of WhatsApp OTP with Twilio Programmable Messaging, please complete the following steps:
|
||||
|
||||
- Go through the [Twilio self sign up guide for WhatsApp](https://www.twilio.com/docs/whatsapp/self-sign-up)
|
||||
- Submit a template via the [Twilio Guide For Submitting WhatsApp templates](https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates#creating-message-templates-and-submitting-them-for-approval)
|
||||
|
||||
@@ -53,7 +53,7 @@ Now go to the Auth > Settings page in the Supabase dashboard (https://supabase.c
|
||||
|
||||
You should see an option to enable Phone Signup.
|
||||
|
||||
Toggle it on, and copy the api key, api secret and optionally phone number values over from the Vonage dashboard. Click save.
|
||||
Toggle it on, and copy the api key, api secret and phone number values over from the Vonage dashboard. Click save.
|
||||
|
||||
Now the backend should be setup, we can proceed to add our client-side code!
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Setting up Twitter logins for your application consists of 3 parts:
|
||||
- Turn `Enable 3-legged OAuth` ON.
|
||||
- Turn `Request email address from users` ON.
|
||||
- Enter your `Callback URL`.
|
||||
- Enter your `Website URL`.
|
||||
- Enter your `Website URL` (tip: try `http://127.0.0.1:port` or `http://www.localhost:port` during development)
|
||||
- Enter your `Terms of service URL`.
|
||||
- Enter your `Privacy policy URL`.
|
||||
- Click `Save`.
|
||||
|
||||
+34
-149
@@ -1,162 +1,47 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Supabase CLI',
|
||||
description:
|
||||
'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.',
|
||||
subtitle:
|
||||
'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.',
|
||||
id: 'cli',
|
||||
title: 'Local Dev with CLI',
|
||||
description: 'Developing locally using the Supabase CLI.',
|
||||
subtitle: 'Developing locally using the Supabase CLI.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` (to create a new local project) and then `supabase start`.
|
||||
You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` and then `supabase start`.
|
||||
|
||||
The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema.
|
||||
|
||||
## Installing the Supabase CLI
|
||||
## Resources
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
<TabPanel id="npm" label="npm">
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{integrations.map((x) => (
|
||||
<div className="col-span-6" key={x.href}>
|
||||
<Link href={x.href} passHref>
|
||||
<a>
|
||||
<GlassPanel icon={'/docs/img/icons/github-icon'} hasLightIcon={true} title={x.name}>
|
||||
{x.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase):
|
||||
export const integrations = [
|
||||
{
|
||||
name: 'Supabase CLI',
|
||||
description:
|
||||
'The Supabase CLI provides tools to develop manage your Supabase projects from your local machine.',
|
||||
href: 'https://github.com/supabase/cli',
|
||||
},
|
||||
{
|
||||
name: 'GitHub Action',
|
||||
description: ' A GitHub action for interacting with your Supabase projects using the CLI.',
|
||||
href: 'https://github.com/supabase/setup-cli',
|
||||
},
|
||||
]
|
||||
|
||||
```sh
|
||||
npm install supabase --save-dev
|
||||
```
|
||||
|
||||
Run the CLI by prefixing each command with `npx` (only applicable when installing through `npm`):
|
||||
|
||||
```sh
|
||||
npx supabase <command>
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="macos" label="macOS">
|
||||
|
||||
Install the CLI with [Homebrew](https://brew.sh):
|
||||
|
||||
```sh
|
||||
brew install supabase/tap/supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="windows" label="Windows">
|
||||
|
||||
Install the CLI with [Scoop](https://scoop.sh):
|
||||
|
||||
```powershell
|
||||
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
|
||||
scoop install supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="linux" label="Linux">
|
||||
|
||||
The CLI is available through [Homebrew](https://brew.sh) and Linux packages.
|
||||
|
||||
#### Homebrew
|
||||
|
||||
```sh
|
||||
brew install supabase/tap/supabase
|
||||
```
|
||||
|
||||
#### Linux packages
|
||||
|
||||
Linux packages are provided in [Releases](https://github.com/supabase/cli/releases).
|
||||
To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager
|
||||
and run one of the following:
|
||||
|
||||
- `sudo apk add --allow-untrusted <...>.apk`
|
||||
- `sudo dpkg -i <...>.deb`
|
||||
- `sudo rpm -i <...>.rpm`
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Updating the Supabase CLI
|
||||
|
||||
When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
```sh
|
||||
npm update supabase --save-dev
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="macos" label="macOS">
|
||||
|
||||
```sh
|
||||
brew upgrade supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="windows" label="Windows">
|
||||
|
||||
```powershell
|
||||
scoop update supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="linux" label="Linux">
|
||||
|
||||
```sh
|
||||
brew upgrade supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features.
|
||||
|
||||
```bash
|
||||
npx supabase stop --no-backup
|
||||
npx supabase start
|
||||
```
|
||||
|
||||
## Running Supabase locally
|
||||
|
||||
Inside the folder where you want to create your project, run:
|
||||
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
|
||||
This will create a new `supabase` folder. It's safe to commit this folder to your version control system.
|
||||
|
||||
Now, to start the Supabase stack, run:
|
||||
|
||||
```bash
|
||||
supabase start
|
||||
```
|
||||
|
||||
This takes time on your first run because the CLI needs to download the local Docker images. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool).
|
||||
|
||||
The local development environment includes Supabase Studio, a graphical interface for working with your database, running by default on [localhost:54323](http://localhost:54323).
|
||||
|
||||

|
||||
|
||||
When you are finished working on your Supabase project, you can stop the stack with:
|
||||
|
||||
```bash
|
||||
supabase stop
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Supabase CLI Reference](/docs/reference/cli/introduction)
|
||||
- [Supabase CLI Configuration](/docs/reference/cli/config)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,113 @@
|
||||
import specFile from '~/../../spec/cli_v1_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import Head from 'next/head'
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['General', 'Auth', 'API', 'Database', 'Dashboard', 'Local', 'Edge-Functions']
|
||||
|
||||
const tocList = []
|
||||
const content = TAGS.map((tag) => {
|
||||
tocList.push({ text: tag, link: `${tag.toLowerCase()}-config`, level: 2 })
|
||||
return (
|
||||
<div>
|
||||
<Heading tag="h2">{tag} Config</Heading>
|
||||
{specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag.toLowerCase())
|
||||
.map((parameter: Parameter, index) => {
|
||||
tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 })
|
||||
return <Info parameter={parameter} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Supabase CLI config</title>
|
||||
</Head>
|
||||
<div className="grid grid-cols-12 relative gap-4 px-5 max-w-7xl mx-auto py-16">
|
||||
<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-300 to-brand my-8"></div>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky top-20 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Info({ parameter }: { parameter: Parameter }) {
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<Heading tag="h3" parseAnchors={false} customAnchor={parameter.id}>
|
||||
<code>{parameter.title}</code>
|
||||
</Heading>
|
||||
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<div className="">
|
||||
<table className="table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Name</th>
|
||||
<th className="text-left">Default</th>
|
||||
<th className="text-left">Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{parameter.id}</td>
|
||||
<td>{parameter.default ? parameter.default.toString() : 'None'}</td>
|
||||
<td>{parameter.required.toString()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mb-4 scroll-mt-16 mt-0 ">
|
||||
<p className="text-sm font-bold uppercase">Description</p>
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</div>
|
||||
{parameter.usage && (
|
||||
<div className="mb-4 scroll-mt-16 mt-0 ">
|
||||
<p className="text-sm font-bold uppercase">Usage</p>
|
||||
<CodeBlock className="useless-code-block-class" language="py">
|
||||
{parameter.usage}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{parameter.links && (
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase">See also</p>
|
||||
<ul>
|
||||
{parameter.links.map((link) => (
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Supabase CLI',
|
||||
description:
|
||||
'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.',
|
||||
subtitle:
|
||||
'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.',
|
||||
}
|
||||
|
||||
You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` (to create a new local project) and then `supabase start`.
|
||||
|
||||
The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema.
|
||||
|
||||
## Installing the Supabase CLI
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
Install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase):
|
||||
|
||||
```sh
|
||||
npm install supabase --save-dev
|
||||
```
|
||||
|
||||
Run the CLI by prefixing each command with `npx` (only applicable when installing through `npm`):
|
||||
|
||||
```sh
|
||||
npx supabase <command>
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="macos" label="macOS">
|
||||
|
||||
Install the CLI with [Homebrew](https://brew.sh):
|
||||
|
||||
```sh
|
||||
brew install supabase/tap/supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="windows" label="Windows">
|
||||
|
||||
Install the CLI with [Scoop](https://scoop.sh):
|
||||
|
||||
```powershell
|
||||
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
|
||||
scoop install supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="linux" label="Linux">
|
||||
|
||||
The CLI is available through [Homebrew](https://brew.sh) and Linux packages.
|
||||
|
||||
#### Homebrew
|
||||
|
||||
```sh
|
||||
brew install supabase/tap/supabase
|
||||
```
|
||||
|
||||
#### Linux packages
|
||||
|
||||
Linux packages are provided in [Releases](https://github.com/supabase/cli/releases).
|
||||
To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager
|
||||
and run one of the following:
|
||||
|
||||
- `sudo apk add --allow-untrusted <...>.apk`
|
||||
- `sudo dpkg -i <...>.deb`
|
||||
- `sudo rpm -i <...>.rpm`
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Updating the Supabase CLI
|
||||
|
||||
When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
```sh
|
||||
npm update supabase --save-dev
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="macos" label="macOS">
|
||||
|
||||
```sh
|
||||
brew upgrade supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="windows" label="Windows">
|
||||
|
||||
```powershell
|
||||
scoop update supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="linux" label="Linux">
|
||||
|
||||
```sh
|
||||
brew upgrade supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features.
|
||||
|
||||
```bash
|
||||
npx supabase stop --no-backup
|
||||
npx supabase start
|
||||
```
|
||||
|
||||
## Running Supabase locally
|
||||
|
||||
Inside the folder where you want to create your project, run:
|
||||
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
|
||||
This will create a new `supabase` folder. It's safe to commit this folder to your version control system.
|
||||
|
||||
Now, to start the Supabase stack, run:
|
||||
|
||||
```bash
|
||||
supabase start
|
||||
```
|
||||
|
||||
This takes time on your first run because the CLI needs to download the local Docker images. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool).
|
||||
|
||||
The local development environment includes Supabase Studio, a graphical interface for working with your database, running by default on [localhost:54323](http://localhost:54323).
|
||||
|
||||

|
||||
|
||||
## Stopping local services
|
||||
|
||||
When you are finished working on your Supabase project, you can stop the stack with:
|
||||
|
||||
```bash
|
||||
supabase stop
|
||||
```
|
||||
|
||||
## Full command reference
|
||||
|
||||
The CLI provides a number of commands to help you develop your project locally and deploy to the Supabase Platform. You can find all commands inside the [CLI Reference](/docs/reference/cli/introduction) docs, including:
|
||||
|
||||
- [Project](/docs/reference/cli/supabase-projects) and [Organization](/docs/reference/cli/supabase-orgs) management
|
||||
- [Database management](/docs/reference/cli/supabase-db)
|
||||
- [Database migrations](/docs/reference/cli/supabase-migration) and [Database Branching](/docs/reference/cli/supabase-branches)
|
||||
- [Database debugigng tools](/docs/reference/cli/supabase-inspect-db-calls)
|
||||
- [Edge Function management](/docs/reference/cli/supabase-functions)
|
||||
- [Auth management](/docs/reference/cli/supabase-functions)
|
||||
- [Types Generators](/docs/reference/cli/supabase-gen)
|
||||
- [Testing](/docs/reference/cli/supabase-test)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -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'
|
||||
@@ -24,13 +25,31 @@ const pageMap = [
|
||||
{
|
||||
slug: 'generating-types',
|
||||
meta: {
|
||||
title: 'Generate types from your database',
|
||||
title: 'Generate types using GitHub Actions',
|
||||
description: 'End-to-end type safety across client, server, and database.',
|
||||
subtitle: 'End-to-end type safety across client, server, and database.',
|
||||
tocVideo: 'VSNgAIObBdw',
|
||||
},
|
||||
remoteFile: 'generating-types.md',
|
||||
},
|
||||
{
|
||||
slug: 'testing',
|
||||
meta: {
|
||||
title: 'Automated testing using GitHub Actions',
|
||||
description: 'Run your tests when you or your team make changes.',
|
||||
subtitle: 'Run your tests when you or your team make changes.',
|
||||
},
|
||||
remoteFile: 'testing.md',
|
||||
},
|
||||
{
|
||||
slug: 'backups',
|
||||
meta: {
|
||||
title: 'Automated backups using GitHub Actions',
|
||||
description: 'Backup your database on a regular basis.',
|
||||
subtitle: 'Backup your database on a regular basis.',
|
||||
},
|
||||
remoteFile: 'backups.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface ActionDocsProps {
|
||||
@@ -116,6 +135,7 @@ export const getStaticProps: GetStaticProps<ActionDocsProps> = async ({ params }
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'seeding-your-database',
|
||||
title: 'Seeding your database',
|
||||
description:
|
||||
'Populate your database with initial data for reproducible environments across local and testing.',
|
||||
subtitle:
|
||||
'Populate your database with initial data for reproducible environments across local and testing.',
|
||||
}
|
||||
|
||||
When you run `supabase init` we create an empty `seed.sql` file in the root of the `supabase` folder. You can use this to populate your database with seed data.
|
||||
|
||||
## What is seed data?
|
||||
|
||||
Seeding is the process of populating a database with initial data, typically used to provide sample or default records for testing and development purposes. You can use this to create "reproducible environments" for local development, staging, and production.
|
||||
|
||||
## Using the seed file
|
||||
|
||||
The `seed.sql` file is run every time you run `supabase start` or `supabase db reset`. Seeding is done _after_ all the database migrations have been run. As a general rule, you should not add schema statements to your seed file, only data.
|
||||
|
||||
You can add any SQL statements to this file. For example:
|
||||
|
||||
```sql supabase/seed.sql
|
||||
insert into countries
|
||||
(name, code)
|
||||
values
|
||||
('United States', 'US'),
|
||||
('Canada', 'CA'),
|
||||
('Mexico', 'MX');
|
||||
```
|
||||
|
||||
{/* prettier-ignore */}
|
||||
{/*
|
||||
|
||||
### Splitting up your seed file
|
||||
|
||||
The seed file can grow quite large, so you can split it up into multiple files. For example, you can create a `seed` folder and add a `countries.sql` file. Then you can add the following to your `seed.sql` file.
|
||||
|
||||
```sql supabase/seed.sql
|
||||
\i seed/countries.sql
|
||||
```
|
||||
|
||||
### Using CSV data to seed your database
|
||||
|
||||
co
|
||||
You can use the `COPY` command to import CSV data into your database. For example, you can create a `seed` folder and add a `countries.csv` file. Then you can add the following to your `seed.sql` file.
|
||||
|
||||
```sql supabase/seed.sql
|
||||
COPY countries (name, code) FROM 'seed/countries.csv' WITH (FORMAT csv);
|
||||
```
|
||||
|
||||
## Generating seed data from Production
|
||||
|
||||
- Use snaplet
|
||||
\*/}
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,82 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'testing-emails-locally',
|
||||
title: 'Testing and linting',
|
||||
description: 'Using the CLI to test your Supabase project.',
|
||||
subtitle: 'Using the CLI to test your Supabase project.',
|
||||
}
|
||||
|
||||
The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge` Functions.
|
||||
|
||||
## Testing your database
|
||||
|
||||
The Supabase CLI provides Postgres liniting using the `supabase test db` command.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```markdown
|
||||
supabase test db --help
|
||||
Tests local database with pgTAP
|
||||
|
||||
Usage:
|
||||
supabase test db [flags]
|
||||
```
|
||||
|
||||
This is powered by the [pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) extension. You can find a full guide to writing an running tests in the [Testing your database](/docs/guides/database/testing) section.
|
||||
|
||||
### Test helpers
|
||||
|
||||
Our friends at [Basejump](https://usebasejump.com/) have created a useful set of Database [Test Helpers](https://github.com/usebasejump/supabase-test-helpers), with an accompanying [blog post](https://usebasejump.com/blog/testing-on-supabase-with-pgtap).
|
||||
|
||||
### Running database tests in CI
|
||||
|
||||
Use our GitHub Action to [automate your database tests](docs/guides/cli/github-action/testing#testing-your-database).
|
||||
|
||||
## Testing your Edge Functions
|
||||
|
||||
Edge Functions are powered by Deno, which provides a [native set of testing tools](https://deno.land/manual@v1.35.3/basics/testing). We extend this functionality in the Supabase CLI. You can find a detailed guide in the [Edge Functions section](/docs/guides/functions/unit-test).
|
||||
|
||||
## Testing Auth emails
|
||||
|
||||
The Supabase CLI uses [Inbucket](https://github.com/inbucket/inbucket) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth.
|
||||
|
||||
### Accessing Inbucket
|
||||
|
||||
By default, Inbucket is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Simply open this URL in your browser to view the emails.
|
||||
|
||||
### Going into Production
|
||||
|
||||
The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](https://supabase.com/dashboard/project/_/settings/auth).
|
||||
|
||||
## Linting your database
|
||||
|
||||
The Supabase CLI provides Postgres liniting using the `supabase db lint` command:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```markdown
|
||||
supabase db lint --help
|
||||
Checks local database for typing error
|
||||
|
||||
Usage:
|
||||
supabase db lint [flags]
|
||||
|
||||
Flags:
|
||||
--level [ warning | error ] Error level to emit. (default warning)
|
||||
--linked Lints the linked project for schema errors.
|
||||
-s, --schema strings List of schema to include. (default all)
|
||||
```
|
||||
|
||||
This is powered by [plpgsql_check](https://github.com/okbob/plpgsql_check), which leverages the internal Postgres parser/evaluator so you see any errors that would occur at runtime. It provides the following features:
|
||||
|
||||
- validates you are using the correct types for function parameters
|
||||
- identifies unused variables and function arguments
|
||||
- detection of dead code (any code after an `RETURN` command)
|
||||
- detection of missing `RETURN` commands with your Postgres function
|
||||
- identifies unwanted hidden casts, which can be a performance issue
|
||||
- checks `EXECUTE` statements agaist SQL injection vulnerability
|
||||
|
||||
Check the Reference Docs for [more information](/docs/reference/cli/supabase-db-lint).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -1,24 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'testing-emails-locally',
|
||||
title: 'Testing emails locally',
|
||||
description: 'Testing emails for Supabase Auth on your local machine.',
|
||||
subtitle: 'Testing emails for Supabase Auth on your local machine.',
|
||||
}
|
||||
|
||||
The Supabase CLI uses [Inbucket](https://github.com/inbucket/inbucket) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth.
|
||||
|
||||
## Accessing Inbucket
|
||||
|
||||
By default, Inbucket is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Simply open this URL in your browser to view the emails.
|
||||
|
||||
## Going into Production
|
||||
|
||||
The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam.
|
||||
|
||||
Before you go into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](https://supabase.com/dashboard/project/_/settings/auth).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -72,8 +72,8 @@ API:
|
||||
begin;
|
||||
select plan( 2 );
|
||||
|
||||
select has_column( 'profiles', 'id' ); # test that the "id" column exists in the "profiles" table
|
||||
select col_is_pk( 'profiles', 'id' ); # test that the "id" column is a primary key
|
||||
select has_column( 'profiles', 'id' ); -- test that the "id" column exists in the "profiles" table
|
||||
select col_is_pk( 'profiles', 'id' ); -- test that the "id" column is a primary key
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
@@ -94,8 +94,8 @@ select policies_are(
|
||||
'public',
|
||||
'profiles',
|
||||
ARRAY [
|
||||
'Profiles are public', # Test that there is a policy called "Profiles are public" on the "profiles" table.
|
||||
'Profiles can only be updated by the owner' # Test that there is a policy called "Profiles can only be updated by the owner" on the "profiles" table.
|
||||
'Profiles are public', -- Test that there is a policy called "Profiles are public" on the "profiles" table.
|
||||
'Profiles can only be updated by the owner' -- Test that there is a policy called "Profiles can only be updated by the owner" on the "profiles" table.
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Vector similarity refers to a measure of the similarity between two related item
|
||||
|
||||
### Embeddings
|
||||
|
||||
This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](https://platform.openai.com/docs/guides/embeddings) which match the GPT model you're using.
|
||||
This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -66,25 +66,29 @@ create table posts (
|
||||
id serial primary key,
|
||||
title text not null,
|
||||
body text not null,
|
||||
embedding vector(1536)
|
||||
embedding vector(384)
|
||||
);
|
||||
```
|
||||
|
||||
### Storing a vector / embedding
|
||||
|
||||
In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase client.
|
||||
In this example we'll generate a vector using Transformer.js, then store it in the database using the Supabase client.
|
||||
|
||||
```js
|
||||
import { pipeline } from '@xenova/transformers'
|
||||
const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small')
|
||||
|
||||
const title = 'First post!'
|
||||
const body = 'Hello world!'
|
||||
|
||||
// Generate a vector using OpenAI
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: body,
|
||||
// Generate a vector using Transformers.js
|
||||
const output = await generateEmbedding(body, {
|
||||
pooling: 'mean',
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
// Extract the embedding output
|
||||
const embedding = Array.from(output.data)
|
||||
|
||||
// Store the vector in Postgres
|
||||
const { data, error } = await supabase.from('posts').insert({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -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
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'json',
|
||||
title: 'Analyzing efficiency and performance from the terminal',
|
||||
description:
|
||||
'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.',
|
||||
}
|
||||
|
||||
The Supabase CLI comes with a range of tools to help inspect your Postgres instances for potential issues. The CLI gets the information from <a href="https://www.postgresql.org/docs/current/internals.html" target="_blank">Postgres internals</a>. Therefore, most tools provided are compatible with any Postgres databases regardless if they are a Supabase project or not.
|
||||
|
||||
You can find installation instructions for the the Supabase CLI <a href="/docs/guides/cli" target="_blank">here</a>.
|
||||
|
||||
### The `inspect db` command
|
||||
|
||||
The inspection tools for your Postgres database are under then `inspect db` command. You can get a full list of available commands by running `supabase inspect db help`.
|
||||
|
||||
```
|
||||
$ supabase inspect db help
|
||||
Tools to inspect your Supabase database
|
||||
|
||||
Usage:
|
||||
supabase inspect db [command]
|
||||
|
||||
Available Commands:
|
||||
bloat Estimates space allocated to a relation that is full of dead tuples
|
||||
blocking Show queries that are holding locks and the queries that are waiting for them to be released
|
||||
cache-hit Show cache hit rates for tables and indices
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Connect to any Postgres database
|
||||
|
||||
Most inspection commands are Postgres agnostic. You can run inspection routines on any Postgres database even if it is not a Supabase project by providing a connection string via `--db-url`.
|
||||
|
||||
For example you can connect to your local Postgres instance:
|
||||
|
||||
```
|
||||
supabase --db-url postgresql://postgres:postgres@localhost:5432/postgres inspect db bloat
|
||||
```
|
||||
|
||||
### Connect to a Supabase instance
|
||||
|
||||
Working with Supabase, you can link the Supabase CLI with your project:
|
||||
|
||||
```
|
||||
supabase link --project-ref <project-id>
|
||||
```
|
||||
|
||||
Then the CLI will automatically connect to your Supabase project whenever you are in the project folder and you no longer need to provide `—db-url`.
|
||||
|
||||
## Inspection Commands
|
||||
|
||||
Below are the db inspection commands provided, grouped by different use cases.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Some commands might require `pg_stat_statements` to be enabled or a specific Postgres version to be used.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Disk Storage
|
||||
|
||||
These commands are handy if you are running low on disk storage:
|
||||
|
||||
- [bloat](/docs/reference/cli/supabase-inspect-db-bloat) - estimates the amount of wasted space
|
||||
- [vacuum-stats](/docs/reference/cli/supabase-inspect-db-vacuum-stats) - gives information on waste collection routines
|
||||
- [table-record-counts](/docs/reference/cli/supabase-inspect-db-table-record-counts) - estimates the number of records per table
|
||||
- [table-sizes](/docs/reference/cli/supabase-inspect-db-table-sizes) - shows the sizes of tables
|
||||
- [index-sizes](/docs/reference/cli/supabase-inspect-db-index-sizes) - shows the sizes of individual index
|
||||
- [table-index-sizes](/docs/reference/cli/supabase-inspect-db-table-index-sizes) - shows the sizes of indexes for each table
|
||||
|
||||
### Query Performance
|
||||
|
||||
The commands below are useful if your Postgres database consumes a lot of resources like CPU, RAM or Disk IO. You can also use them to investigate slow queries.
|
||||
|
||||
- [cache-hit](/docs/reference/cli/supabase-inspect-db-cache-hit) - shows how efficient your cache usage is overall
|
||||
- [unused-indexes](/docs/reference/cli/supabase-inspect-db-unused-indexes) - shows indexes with low index scans
|
||||
- [index-usage](/docs/reference/cli/supabase-inspect-db-index-usage) - shows information about the efficiency of indexes
|
||||
- [seq-scans](/docs/reference/cli/supabase-inspect-db-seq-scans) - show number of sequential scans recorded against all tables
|
||||
- [long-running-queries](/docs/reference/cli/supabase-inspect-db-long-running-queries) - shows long running queries that are executing right now
|
||||
- [outliers](/docs/reference/cli/supabase-inspect-db-outliers) - shows queries with high execution time but low call count and queries with high proportion of execution time spent on synchronous I/O
|
||||
|
||||
### Locks
|
||||
|
||||
- [locks](/docs/reference/cli/supabase-inspect-db-locks) - shows statements which have taken out an exclusive lock on a relation
|
||||
- [blocking](/docs/reference/cli/supabase-inspect-db-blocking) - shows statements that are waiting for locks to be released
|
||||
|
||||
### Connections
|
||||
|
||||
- [role-connections](/docs/reference/cli/supabase-inspect-db-role-connections) - shows number of active connections for all database roles (Supabase-specific command)
|
||||
- [replication-slots](/docs/reference/cli/supabase-inspect-db-replication-slots) - shows information about replication slots on the database
|
||||
|
||||
## Notes on `pg_stat_statements`
|
||||
|
||||
Following commands require `pg_stat_statements` to be enabled: calls, locks, cache-hit, blocking, unused-indexes, index-usage, bloat, outliers, table-record-counts, replication-slots, seq-scans, vacuum-stats, long-running-queries.
|
||||
|
||||
When using `pg_stat_statements` also take note that it only stores the latest 5,000 statements. Moreover, consider resetting the analysis after optimizing any queries by running `select pg_stat_statements_reset();`
|
||||
|
||||
Learn more about pg_stats [here](https://supabase.com/docs/guides/database/extensions/pg_stat_statements).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Supabase CLI's inspect commands are heavily inspired by the <a href="https://github.com/heroku/heroku-pg-extras" target="_blank">pg-extras</a> tools.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -62,8 +62,6 @@ export const meta = {
|
||||
```kotlin
|
||||
implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version"
|
||||
implementation "io.ktor:ktor-client-android:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-utils:$ktor_version"
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -102,10 +100,8 @@ export const meta = {
|
||||
supabaseUrl = "https://xyzcompany.supabase.co",
|
||||
supabaseKey = "public-anon-key"
|
||||
) {
|
||||
install(GoTrue)
|
||||
install(Postgrest)
|
||||
install(Storage)
|
||||
}
|
||||
install(Postgrest)
|
||||
}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import specFile from '~/../../spec/cli_v1_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import Head from 'next/head'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general', 'auth', 'api', 'database', 'dashboard', 'local', 'edge-functions']
|
||||
|
||||
const tocList = TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter) => {
|
||||
const text = parameter.id
|
||||
const link = `#${parameter.id}`
|
||||
const level = '2'
|
||||
return { text, link, level }
|
||||
})
|
||||
).flat()
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Supabase CLI config</title>
|
||||
</Head>
|
||||
<div className="grid grid-cols-12 relative gap-4 px-5 max-w-7xl mx-auto py-16">
|
||||
<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>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
|
||||
<code>{parameter.title}</code>
|
||||
</Heading>
|
||||
<div className="grid">
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky top-20 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ Bobbie Soedirgo
|
||||
Chandana Anumula
|
||||
Chase Granberry
|
||||
Chris Copplestone
|
||||
Chris Gwilliams
|
||||
Dave Wilson
|
||||
Div Arora
|
||||
Divit D
|
||||
|
||||
@@ -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 of 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 Supabase 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!
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Supabase Beta July 2023
|
||||
description: Launch Week 8 is coming - but we still shipped some goodies during July
|
||||
author: ant_wilson
|
||||
image: 2023-08-02-july-2023-beta-update/july-2023-beta-update.png
|
||||
thumb: 2023-08-02-july-2023-beta-update/july-2023-beta-update.png
|
||||
tags:
|
||||
- release-notes
|
||||
date: '2023-08-02'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Next Monday is Launch Week 🎱, where we’ll be announcing a tonne of surprises. In the meantime, here’s a summary of everything we shipped in July. Read on!
|
||||
|
||||
## Supabase Launch Week 8
|
||||
|
||||

|
||||
|
||||
Launch Week 8 starts next week. We’ll be announcing one new major feature every day for an entire week (with a few bonus surprises along the way). Get your ticket and tune in on Monday the 7th.
|
||||
|
||||
[Get your ticket](https://supabase.com/launch-week)
|
||||
|
||||
## New Android Kotlin Library
|
||||
|
||||

|
||||
|
||||
Are you a mobile dev? We now have client libraries for Android. This is thanks to one of our community members - [@TheRealJanGER](https://twitter.com/TheRealJanGER). Try it out, and if you want to help us maintain the library let us know.
|
||||
|
||||
[Quickstart](https://supabase.com/docs/guides/getting-started/quickstarts/kotlin)
|
||||
|
||||
[Tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-kotlin)
|
||||
|
||||
## pgvector 0.4.0 performance
|
||||
|
||||

|
||||
|
||||
There's been a lot of talk about pgvector performance, so we pushed it to the limits to figure out its strengths and limitations. The good news is that work has already [started](https://github.com/pgvector/pgvector/tree/hnsw) on HNSW, and isn’t far away.
|
||||
|
||||
[Read the benchmark](https://supabase.com/blog/pgvector-performance)
|
||||
|
||||
## Quick product announcements
|
||||
|
||||
- [Auth] There is a new OAuth provider in time: Login with Figma. [Docs](https://supabase.com/docs/guides/auth/social-login/auth-figma)
|
||||
- [Auth] You can now use Resend as a custom SMTP provider. [Docs](https://resend.com/docs/send-with-supabase-smtp)
|
||||
- [Postgres Tooling] pgsodium 3.1.8 released. [PR](https://github.com/michelp/pgsodium/releases/tag/v3.1.8)
|
||||
- [Postgres Tooling] PostgREST 11.1 is now live. [Blog post](https://supabase.com/blog/postgrest-11-1-release)
|
||||
|
||||
## Egghead course: Twitter Clone with the Next.js App Router
|
||||
|
||||

|
||||
|
||||
Should we rebrand to X Clone? Next.js App router is ushering in the next era of web development. What a best way to learn to navigate it than by building a full-stack app?
|
||||
|
||||
[Course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
|
||||
|
||||
## Twitter Space: Launch Week 8 - Kickoff 🏈
|
||||
|
||||

|
||||
|
||||
After Monday's announcement, we are kicking off LW8 with a very cool Twitter Space, featuring the DevRel team and guests. Bring your questions!
|
||||
|
||||
10:00 AM PT | 1:00 PM ET | 06:00 PM UK
|
||||
|
||||
[Set reminder](https://twitter.com/i/spaces/1DXGyvjkeEeJM)
|
||||
|
||||
## Made with Supabase
|
||||
|
||||

|
||||
|
||||
[snowChat](https://snowchat.streamlit.app/) | An intuitive and user-friendly application that allows you to interact with your Snowflake data using natural language queries. Built by [Kaarthik Andavar](https://twitter.com/kaarthikcodes) using Python, Streamlit, Open AI, and Supabase Vector.
|
||||
|
||||
## Extended Community Highlights
|
||||
|
||||

|
||||
|
||||
- Build a Simple Dropbox clone using Deno, DenoKV, Fresh and Supabase. [Video Tutorial](https://www.youtube.com/watch?v=_WerF8YMqks)
|
||||
- Safeguarding Data Integrity with pg_safeupdate in PostgreSQL and Supabase. [Blog post](https://www.notion.so/809925346af243b3a4ede8774e71472c?pvs=21)
|
||||
- Supabase for C#: Streamlining Backend Development with Database Magic. [Blog post](https://themurph.hashnode.dev/supabase-csharp)
|
||||
- Building an MVP app using Supabase. [Blog post](https://www.antstack.com/blog/building-an-mvp-app-using-supabase/)
|
||||
- Fortify Your Application With Supabase: Exploring Fine-Grained Control with Row-Level Security and OAuth Sign-in. [Article](https://techconative.com/blog/supabase-rls-security-oauth-authentication/)
|
||||
- Using Supabase authentication in NestJs. [Tutorial](https://blog.iamstarcode.com/using-supabase-authentication-in-nestjs)
|
||||
- React Native file upload with Supabase Storage. [Tutorial](https://supabase.com/blog/react-native-storage) | [Video](https://www.youtube.com/watch?v=am6w5zEDk_g)
|
||||
- Safeguarding Data Integrity with pg_safeupdate in PostgreSQL and Supabase. [Blog Post](https://blog.mansueli.com/safeguarding-data-integrity-with-pg-safeupdate-in-postgresql-and-supabase)
|
||||
- Supabase almost SHUT ME DOWN. [Video](https://www.youtube.com/watch?v=2WHTZa4ENfc)
|
||||
|
||||
## Supabase in Times Square
|
||||
|
||||

|
||||
|
||||
The Redpoint team put together InfraRed 100, a report that recognizes 100 transformative companies in cloud infrastructure. We are honoured to be included with so many amazing companies we admire 🙇🏻♂️. They even featured us on a billboard in Times Square to celebrate.
|
||||
|
||||
[Read the full report](https://www.redpoint.com/infrared/100/)
|
||||
|
||||
## We’re Hiring
|
||||
|
||||
- [Support Engineer (APAC)](https://boards.greenhouse.io/supabase/jobs/4934378004)
|
||||
- [FullStack Engineer Billing](https://www.notion.so/Beta-Update-July-Monday-31st-10386eba40934ea597ee5e5b1b81574e?pvs=21)
|
||||
|
||||
## ⚠️ Baking hot meme zone ⚠️
|
||||
|
||||
If you made it this far in the email you deserve a devilish treat.
|
||||
|
||||
NPCs represent…
|
||||
|
||||

|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: 'pgvector: Fewer dimensions are better'
|
||||
description: Increase performance in pgvector by using embedding vectors with fewer dimensions
|
||||
author: gregnr,oli_rice,egor_romanov
|
||||
tags:
|
||||
- AI
|
||||
- performance
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2023-08-03'
|
||||
toc_depth: 3
|
||||
image: 2023-08-03-fewer-dimensions-are-better-pgvector/fewer-dimensions-og.png
|
||||
thumb: 2023-08-03-fewer-dimensions-are-better-pgvector/fewer-dimensions-thumb.png
|
||||
---
|
||||
|
||||
Embeddings have become a core tool in AI applications to determine semantic similarity between pieces of content. Developers are using embeddings for various tasks, such as search, retrieval augmented generation, and clustering.
|
||||
|
||||
At Supabase, we support storing embeddings in Postgres using the [pgvector](https://github.com/pgvector/pgvector) extension. pgvector adds a new datatype `vector`, which developers use to store embeddings in regular columns within their database tables. Similarity can be calculated between these vectors using one of the three supported distance measures:
|
||||
|
||||
- Inner product
|
||||
- Cosine distance
|
||||
- Euclidean distance
|
||||
|
||||
## Challenges with pgvector
|
||||
|
||||
Without indexes, pgvector performs a full table scan when you run a similarity query. This means distance has to be computed against every row in your table. This is manageable at a small scale, but becomes problematic as your table grows.
|
||||
|
||||
To solve this, pgvector offers indexes. Indexes reorganize the data into data structures that exploit internal structure and enable approximate similarity search without referring to every record. Currently, pgvector supports an IVF index, with HNSW expected in the next release.
|
||||
|
||||
IVF [indexes](https://supabase.com/docs/guides/ai/managing-indexes) work by clustering vectors into `lists`, and then querying only vectors within the same list (or multiple nearby lists, depending on the value of `probes`).
|
||||
|
||||
### Scaling indexes
|
||||
|
||||
98% of our customers are generating text embeddings using OpenAI's `text-embedding-ada-002` model. At an initial glance, there's good reason for this - these embeddings perform quite well for information retrieval and are economical to produce. `text-embedding-ada-002` produces vectors with 1536 dimensions which is among the largest in the industry. IVF indexes help address some scaling challenges, but there are still some pitfalls.
|
||||
|
||||
First, vectors are large. A 1536 dimensional vector is ~12.3 kilobytes. Scaling that up to 1M vectors, the raw data tops 11 gigabytes. Experienced SQL users know that for best performance, indexes should fit within system memory. Moreover unlike traditional workloads, there is a significant compute component when performing vector similarity queries.
|
||||
|
||||
In the real-world it's common for an index to reduce the number of distance computations needed to estimate nearest neighbors from 100% of the dataset to 5-20%. At 1M records, that's still 50k-200k distance calculations being performed for a single query. Given how different the [resource requirements](https://supabase.com/docs/guides/ai/choosing-compute-addon) are to support heavy vector workloads, it's not surprising that one of the most common issues we see is significant under provisioning of hardware.
|
||||
|
||||
## Massive Text Embedding Benchmark (MTEB)
|
||||
|
||||
Text embeddings are one of the most common types of embeddings today. Our friends at Hugging Face have done the hard work of comparing text embedding models (including OpenAI's `text-embedding-ada-002`) by benchmarking them against common similarity tasks:
|
||||
|
||||
- Retrieval (search)
|
||||
- Semantic textual similarity (STS)
|
||||
- Classification
|
||||
- Clustering
|
||||
- Pair classification
|
||||
- Reranking
|
||||
- Summarization
|
||||
- Bitext mining
|
||||
|
||||
Models are ranked per task by taking their average score produced over each task's datasets. Each model also has an overall (general purpose) score, calculated by taking the average score produced across all datasets. You can find the results on their [MTEB leaderboard](https://huggingface.co/spaces/mteb/leaderboard).
|
||||
|
||||
It's worth pointing out that each model's dimension size has little-to-no correlation with its performance. In fact there are a number of models that perform comparably with`text-embedding-ada-002`, all of which produce embeddings with fewer dimensions than 1536.
|
||||
|
||||
| Rank | Model | Dimensions | Average | Model Size (GB) |
|
||||
| ---- | -------------------------------------------------------------------------------------------------- | ---------- | ------- | --------------- |
|
||||
| 1 | [gte-large](https://huggingface.co/thenlper/gte-large) | 1024 | 63.13 | 0.67 |
|
||||
| 2 | [gte-base](https://huggingface.co/thenlper/gte-base) | 768 | 62.39 | 0.22 |
|
||||
| 3 | [e5-large-v2](https://huggingface.co/intfloat/e5-large-v2) | 1024 | 62.25 | 1.34 |
|
||||
| 9 | [gte-small](https://huggingface.co/thenlper/gte-small) | 384 | 61.36 | 0.07 |
|
||||
| 10 | [text-embedding-ada-002](https://beta.openai.com/docs/guides/embeddings/types-of-embedding-models) | 1536 | 60.99 | - |
|
||||
| 12 | [e5-small-v2](https://huggingface.co/intfloat/e5-small-v2) | 384 | 59.93 | 0.13 |
|
||||
| 32 | [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) | 384 | 56.26 | 0.09 |
|
||||
|
||||
## Benefits of fewer dimensions
|
||||
|
||||
What specifically do we gain when we have fewer dimensions? Faster queries and less RAM: Fewer dimensions means less computation while more of the dataset or index is able to fit in memory.
|
||||
|
||||
Take a look at dot product for example:
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/dot-product-light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/dot-product-dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Dot product is the product of each vector element pair summed together into a single result. Fewer dimensions in the vector means fewer calculations for every computed distance .
|
||||
|
||||
We compared the performance of `text-embedding-ada-002` from OpenAI (1536 dimensions) with open-source `all-MiniLM-L6-v2` (384 dimensions) by measuring requests per second at a constant precision and configuration:
|
||||
|
||||
- **Database size:** with 2vCPU (ARM) and 8GB RAM - `large` add-on for Supabase project.
|
||||
- **Version:** Postgres v15 and pgvector v0.4.0
|
||||
- **Dataset:** 224,482 embeddings from [Wikipedia articles](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings).
|
||||
- **Index:** The index was generated for `inner-product` (dot product) distance function with `lists=1000`.
|
||||
- **Process:** We followed [our optimization guide and tips](https://supabase.com/docs/guides/ai/going-to-prod#performance-tips-when-using-indexes).
|
||||
|
||||
We observed pgvector with `all-MiniLM-L6-v2` outperforming `text-embedding-ada-002` by 78% when holding the precision@10 constant at 0.99. This gap increases as you lower the precision. Postgres was using just 4GB of RAM with 384d vectors generated by `all-MiniLM-L6-v2` compared to 7.5GB with `text-embedding-ada-002`.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-99-light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-99-dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-98-light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-98-dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
After that, we decided to try the recently published [gte-small](https://huggingface.co/thenlper/gte-small) (also 384 dimensions), and the results were even more astonishing. With `gte-small`, we could set `probes=10` to achieve the same level of `precision@10 = 0.99`. Consequently, we observed more than a 200% improvement in requests per second for pgvector with embeddings generated by `gte-small` compared to `all-MiniLM-L6-v2`.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-gte-small-99-light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-08-03-fewer-dimensions-are-better-pgvector/performance-comparison-gte-small-99-dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Choosing an embedding model
|
||||
|
||||
Ultimately the choice of embedding model will depend on your specific use case and requirements. You would likely want to consider:
|
||||
|
||||
- **Similarity performance:** How well do embeddings match based on the task (MTEB score)?
|
||||
- **Model size:** What are the operational costs of storing and loading this model into memory?
|
||||
- **Sequence length:** How many input tokens does the model support?
|
||||
- **Dimension size:** What is the operational cost of storing and comparing embeddings?
|
||||
- **Language:** Which written languages do you need to support? Many of the smaller models only support English.
|
||||
|
||||
The goal is to maximize similarity performance and sequence length, while minimizing model size and dimension size. Notably, if we can find a model that performs well while producing as few dimensions as possible, we can immediately improve many of the above challenges developers are facing with pgvector.
|
||||
|
||||
For example [gte-small](https://huggingface.co/thenlper/gte-small), a model recently trained by the [Alibaba DAMO Academy,](https://damo.alibaba.com/) produces only 384 dimensions while ranking higher than `text-embedding-ada-002` on the MTEB leaderboard.
|
||||
|
||||
It's worth noting that many models today are trained on English-only text (such as `gte-small` and `all-MiniLM-L6-v2`). If you need to work with other languages, be sure to choose a model that supports them, such as [multilingual-e5-small](https://huggingface.co/intfloat/multilingual-e5-small).
|
||||
|
||||
## The best of both worlds
|
||||
|
||||
It's important to keep in mind that using an alternative embedding model doesn't preclude using OpenAI for other tasks like text or chat generation. If your application uses embeddings for [retrieval augmented generation](https://supabase.com/docs/guides/ai/examples/nextjs-vector-search) (searching your knowledge base for additional context to pass to a LLM prompt), you can use a lower dimension embedding model for data fetching, then send the plain text to OpenAI's completion models. The retrieval step is completely decoupled from the LLM generation.
|
||||
|
||||
## Why not dimensionality reduction?
|
||||
|
||||
While dimensionality reduction techniques like Principal Component Analysis ([PCA](https://en.wikipedia.org/wiki/Principal_component_analysis)) or t-Distributed Stochastic Neighbor Embedding ([t-SNE](https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding)) may seem like a good alternative to trim down the size of text embedding vectors, there is some risk to consider.
|
||||
|
||||
First, these tools may not appropriately model the relationships in high dimensional space. Both PCA and t-SNE are likely to oversimplify the multivariate relationships among different dimensions, leading to loss of important semantic information. This is particularly the case with PCA, which assumes linearity and would fail to preserve nonlinear interactions found in high-dimensional text embeddings.
|
||||
|
||||
Additionally, the validity of the dimensionality reduction models could change over time if the distribution of text data shifts. If the model was trained on older data, it might not accurately reflect or accommodate these shifts. For example, if your company gets a large new customer in the legal field and your embedding dataset contained no legal documents when the dimensionality reduction model was trained, otherwise exploitable structure may be destructively compressed. Both PCA and t-SNE are not inherently adaptable to such changes, which could result in an increasingly poor fit over time. In summary, the derived low-dimensional representations may not maintain semantic coherence of the original high-dimensional vectors.
|
||||
|
||||
## What's next?
|
||||
|
||||
At Supabase we are working on ways to make it easy to generate embeddings using open source, high performance, and low dimension embedding models. Stay tuned for [Launch Week 8](/launch-week) next week!
|
||||
@@ -0,0 +1,451 @@
|
||||
---
|
||||
title: Coding the stars - an interactive constellation with Three.js and React Three Fiber
|
||||
description: How we built a constellation of stars with Three.js and React Three Fiber.
|
||||
author: fsansalvadore
|
||||
image: lw8-constellation-breakdown/lw8-constellation-breakdown-og.png
|
||||
thumb: lw8-constellation-breakdown/lw8-constellation-breakdown-og.png
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2023-08-04'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Every Launch Week is an opportunity for Supabase to experiment, try some spicy new designs, and dogfood our own technology. During our previous Launch Week we [took Generative AI for a spin](https://supabase.com/blog/designing-with-ai-midjourney). This time we decided to shoot for the stars.
|
||||
|
||||
For [Launch Week 8](https://supabase.com/launch-week/), we wanted to make the user-generated tickets a central piece of the launch week theme. To do this, we built a “constellation” of stars - an animated night sky where every user signup was represented as a star, in the form of an “8” shape.
|
||||
|
||||
<Img src="/images/blog/lw8-constellation-breakdown/lw8-early-design.png" caption="Early design" />
|
||||
|
||||
We could approach this animation in a few ways.
|
||||
|
||||
For example, animating the `stroke-dashoffset` on an SVG path, similar to <Link children="this" href="https://css-tricks.com/svg-line-animation-works/" target="_blank" /> example, was a good option, but it would have been difficult to randomize and change the shape at a later stage. Other approaches included 2D animation libraries, like <Link children="Framer Motion" href="https://www.framer.com/motion/animation/" target="_blank" />, <Link children="gsap" href="https://greensock.com/gsap/" target="_blank" /> or <Link children="PixiJS" href="https://pixijs.com/" target="_blank" />.
|
||||
|
||||
Ultimately we decided to take <Link href="https://threejs.org/" target="_blank" children="Three.js" /> for a spin using [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) (R3F) giving us a more powerful toolset to enable us to achieve the best possible result.
|
||||
|
||||
Learning Three.js is not a walk in the park but R3F abstracted many of its complexities such as cameras and renderers, to name a few. If you're new to R3F, some of the core primitives they provide for a basic scene include:
|
||||
|
||||
- `Geometries`: used to create and define shapes
|
||||
- `Materials`: manage the texture and color of objects
|
||||
- `Mesh`: used to instantiate polygonal objects by combining a Geometry with a Material
|
||||
- `Lights`: to shine bright like a diamond 💎🎵
|
||||
- `Canvas`: where you define your R3F Scene
|
||||
|
||||
If you want to dive a little deeper, here are a few good resources we found to get a solid grasp on the topic:
|
||||
|
||||
- <Link
|
||||
href="https://www.youtube.com/watch?v=DPl34H2ISsk"
|
||||
target="_blank"
|
||||
children="I wish I knew this before using React Three Fiber"
|
||||
/> - from our very own <Link href="https://twitter.com/ggrdson" target="_blank" children="Greg" />
|
||||
- <Link href="https://Ts-journey.com/" target="_blank" children="ThreeJs Journey" /> - by <Link
|
||||
href="https://bruno-simon.com/"
|
||||
target="_blank"
|
||||
children="Bruno Simon"
|
||||
/>
|
||||
|
||||
## Setting up the scene
|
||||
|
||||
In this article, we’re going to break down the steps to reproduce the Launch Week 8 animation using React Three Fiber in NextJs.
|
||||
|
||||
These are the dependencies we’ll need:
|
||||
|
||||
```bash
|
||||
npm install three @react-three/fiber
|
||||
```
|
||||
|
||||
If you’re using React 17, we’ll spare you the trouble of finding the last compatible R3F version:
|
||||
|
||||
```bash
|
||||
npm install three @react-three/fiber@7.0.29
|
||||
```
|
||||
|
||||
All we need for each particle is a circle geometry with a minimal amount of sides to minimize complexity.
|
||||
|
||||
```jsx
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const Geometry = useMemo(() => () => <circleGeometry args={[1.5, 5]} />, [])
|
||||
```
|
||||
|
||||
A basic standard material with a white color will do just fine. Using the AdditiveBlending module from `three` provides a more interesting touch when particles happen to overlap, making them shine brighter:
|
||||
|
||||
```jsx
|
||||
import { AdditiveBlending } from 'three'
|
||||
|
||||
const Material = () =>
|
||||
useMemo(() => <meshStandardMaterial color="#ffffff" blending={AdditiveBlending} />, [])
|
||||
```
|
||||
|
||||
Let’s put it together in an R3F `Canvas` element and wrap up the initial setup with an `ambientLight`, which will make objects visible, just as real light does:
|
||||
|
||||
```jsx
|
||||
import { useMemo } from 'react'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { AdditiveBlending } from 'three'
|
||||
|
||||
const Geometry = useMemo(
|
||||
() => () => <circleGeometry args={[1.5, 5]} />,
|
||||
[]
|
||||
)
|
||||
|
||||
const Material = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<meshStandardMaterial
|
||||
color="#ffffff"
|
||||
blending={AdditiveBlending}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ width: 100vw, height: 100vh, background: "#000000" }}>
|
||||
<Canvas
|
||||
dpr={[1, 2]}
|
||||
camera={{ fov: 75, position: [0, 0, 500] }}
|
||||
>
|
||||
<ambientLight intensity={0.3} />
|
||||
<group>
|
||||
{particles?.map((particle, index) => (
|
||||
<mesh
|
||||
key={particle.username}
|
||||
>
|
||||
<Geometry />
|
||||
<Material />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
For more context, the `dpr` values help with pixelation issues and the `camera` [0, 0, 500] position means that the camera is moved 500 units back in the z-axis to actually see the center [0,0,0] of the scene.
|
||||
|
||||
One thing to note is that the R3F Canvas renders a transparent background, so in order to see the white particle, we need to set the background of the parent html element to a dark color.
|
||||
|
||||
We created a separate component for the Particle, which will later encapsulate the animation logic.
|
||||
|
||||
```jsx
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
const Particle = ({ children }) => {
|
||||
const particle = useRef(null)
|
||||
|
||||
return <mesh ref={particle}>{children}</mesh>
|
||||
}
|
||||
|
||||
export default Particle
|
||||
```
|
||||
|
||||
## Load users from Supabase
|
||||
|
||||
You might have noticed we haven’t instantiated the particles yet. As we mentioned earlier, we wanted each particle to represent a ticket generated by a user and stored in the database. Let’s fetch the signups from the `tickets` table in our Supabase project (you might need to start your own Launch Week to fill your table):
|
||||
|
||||
```jsx
|
||||
const [particles, setParticles] = useState([])
|
||||
|
||||
const loadUsers = async () => {
|
||||
return await supabase.from('lw8_tickets').select('*')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { data: users } = loadUsers()
|
||||
setParticles(users)
|
||||
}, [])
|
||||
```
|
||||
|
||||
We updated the constellation in [realtime](https://supabase.com/realtime) whenever a new ticket was generated, but we’ll skip over this part to keep the article shorter. Since it’s all open-source, you can dive deeper <Link children="here" href="https://github.com/supabase/supabase/pull/16022/files#diff-e3298724d8ca571c732651658e388d70cafd90c4ceea99be8e5e7529ebb73a56R17-R52" target="_blank" /> if you wish.
|
||||
|
||||
## Animating the particles
|
||||
|
||||
To move the particle around the screen we are going to leverage a few different concepts: _useFrame_ and _trigonometry_ 🤯
|
||||
|
||||
### useFrame
|
||||
|
||||
Generally, the most optimal way to animate things in a browser viewport, using javascript, is by leveraging a method called <Link href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame" target="_blank" children="requestAnimationFrame" />, which _“tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation right before the next repaint.”_. R3F has a similar hook called <Link href="https://docs.pmnd.rs/react-three-fiber/tutorials/basic-animations" target="_blank" children="useFrame" /> that lets you execute code on every frame of Fiber's render loop. We’ll use this to change the position of the particles over time in a few moments.
|
||||
|
||||
### Using time as an animation variable
|
||||
|
||||
We can extract time information from the useFrame `clock` parameter, to know how much time has elapsed in our application, and use that time to animate a value. Updating the `x` position with Math.sin() generates a horizontal oscillating movement. Multiply it with a `widthRadius` variable to customize the amplitude of the movement.
|
||||
|
||||
```jsx
|
||||
const particle = useRef(null)
|
||||
|
||||
const widthRadius = 100
|
||||
const heightRadius = 100
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
const timer = clock.getElapsedTime()
|
||||
|
||||
particle.current.position.x = Math.sin(timer) * widthRadius
|
||||
}
|
||||
})
|
||||
|
||||
return <mesh ref={particle}>{children}</mesh>
|
||||
```
|
||||
|
||||

|
||||
|
||||
Combine the previous horizontal movement with a `Math.cos()` on the `y` position to [draw a circle](https://en.wikipedia.org/wiki/Sine_and_cosine):
|
||||
|
||||
```jsx
|
||||
particle.current.position.y = Math.cos(timer) * heightRadius
|
||||
```
|
||||
|
||||

|
||||
|
||||
Calculating the circumference we can get the time the x position takes to complete a full circle.
|
||||
|
||||
```jsx
|
||||
const circumference = (config.widthRadius * Math.PI * 2) / 100
|
||||
```
|
||||
|
||||
When that happens, we can invert the cos sign on every other loop to obtain a basic 8 shape.
|
||||
|
||||
```jsx
|
||||
const isEven = Math.floor(timer / circumference) % 2 == 0
|
||||
|
||||
particle.current.position.x = Math.sin(timer) * widthRadius
|
||||
particle.current.position.y = isEven
|
||||
? Math.cos(timer) * heightRadius - heightRadius
|
||||
: -Math.cos(timer) * heightRadius + heightRadius
|
||||
```
|
||||
|
||||

|
||||
|
||||
At this point, we played around with a number of parameters that made the animation more randomized and interesting.
|
||||
|
||||
For example, we randomized the speed and the delay of each particle:
|
||||
|
||||
```jsx
|
||||
const minSpeed = -0.3
|
||||
const maxSpeed = 0.4
|
||||
const speed = Math.random() * (minSpeed - maxSpeed) + maxSpeed
|
||||
|
||||
const delayOffsetFactor = 100
|
||||
const delayOffset = Math.random() * delayOffsetFactor
|
||||
|
||||
[...]
|
||||
|
||||
const timer = clock.getElapsedTime() *** speed + delayOffset**
|
||||
```
|
||||
|
||||
We offset the shape on the x-axis, to concentrate most of the particles in the core of the 8 shape and leave a smaller amount externally, by playing around with exponentials using Math.pow() in combination with some more randomization.
|
||||
|
||||
```jsx
|
||||
const xThickness = 7
|
||||
const xRandomnessShape = 2.2
|
||||
const xRandomness = 5
|
||||
|
||||
const pathOffset =
|
||||
Math.pow(
|
||||
Math.random() * xRandomnessShape,
|
||||
xRandomness - xRandomness / 2
|
||||
) * xThickness
|
||||
|
||||
...
|
||||
|
||||
particle.current.position.x = Math.sin(timer) * widthRadius + pathOffset
|
||||
```
|
||||
|
||||
Honestly, this was the result of a lot of playing and tweaking around, and we certainly didn’t hit the best possible result on the first try. Perhaps you want to take some time to experiment with the math - you might find even better and more configurable results.
|
||||
|
||||
## GUI playground
|
||||
|
||||
<Img src="/images/blog/lw8-constellation-breakdown/lw8-constellation-gui-demo.png" wide={true} />
|
||||
|
||||
What really helped to visualize the shape, gather feedback, and decide on a final design was adding a GUI to play around with the values. You can try for yourself by appending `#debug` to the [supabase.com/launch-week#debu](http://supabase.com/launch-week#debug)[g](http://supabase.com/launch-weekdebug) url. Go crazy with it.
|
||||
|
||||
We used the <Link href="https://github.com/dataarts/dat.gui" children="dat.gui" target="_blank"/> library:
|
||||
|
||||
```bash
|
||||
npm install dat.gui@0.7.9
|
||||
```
|
||||
|
||||
Which needs to be loaded asynchronously, otherwise it raises a `window is not defined` error.
|
||||
|
||||
```jsx
|
||||
const init = async () => {
|
||||
const dat = await import('dat.gui')
|
||||
const gui = new dat.GUI()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [])
|
||||
```
|
||||
|
||||
Then we prepared a `useParticlesConfig` hook with all the configuration wired up to the GUI. Whenever the GUI updated, we also updated react state.
|
||||
|
||||
```jsx
|
||||
import { useEffect, useState } from 'react'
|
||||
import { range } from 'lodash'
|
||||
|
||||
let defaultConfig = {
|
||||
particles: 1500,
|
||||
widthRadius: 100,
|
||||
topHeightRadius: 80,
|
||||
bottomHeightRadius: 100,
|
||||
xThickness: 7,
|
||||
xRandomnessFactor: 2.2,
|
||||
xRandomnessShape: 2.2,
|
||||
xRandomness: 5,
|
||||
yThickness: 20,
|
||||
max_speed: 0.1,
|
||||
min_speed: -0.1,
|
||||
}
|
||||
|
||||
const useParticlesConfig = (): any => {
|
||||
if (typeof window === 'undefined') return null
|
||||
const hash = window.location.hash
|
||||
const isDebugMode = hash.includes('#debug')
|
||||
const [particles, setParticles] = useState(range(0, defaultConfig.particles))
|
||||
|
||||
const [config, setConfig] = useState(defaultConfig)
|
||||
|
||||
const handleSetConfig = (name, value) => {
|
||||
setConfig((prevConfig) => ({ ...prevConfig, [name]: value }))
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (!isDebugMode) return
|
||||
const dat = await import('dat.gui')
|
||||
const gui = new dat.GUI()
|
||||
const particlesFolder = gui.addFolder('Particles')
|
||||
const shapeFolder = gui.addFolder('Shape')
|
||||
|
||||
particlesFolder
|
||||
.add(config, 'particles')
|
||||
.min(1)
|
||||
.max(5000)
|
||||
.step(1)
|
||||
.onChange((value) => {
|
||||
handleSetConfig('particles', value)
|
||||
setParticles(range(0, value))
|
||||
})
|
||||
shapeFolder
|
||||
.add(config, 'widthRadius')
|
||||
.min(1)
|
||||
.max(200)
|
||||
.step(1)
|
||||
.onChange((value) => handleSetConfig('widthRadius', value))
|
||||
|
||||
// add desired folders and parameters
|
||||
|
||||
particlesFolder.open()
|
||||
shapeFolder.open()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [])
|
||||
|
||||
return { config, handleSetConfig, particles, setParticles, isDebugMode }
|
||||
}
|
||||
|
||||
export default useParticlesConfig
|
||||
```
|
||||
|
||||
Here is the final code:
|
||||
|
||||
```jsx
|
||||
import React, { useMemo, useEffect, useState } from 'react'
|
||||
import { Canvas, useFrame } from '@react-three/fiber'
|
||||
import { AdditiveBlending } from 'three'
|
||||
import useParticlesConfig from './hooks/useParticlesConfig'
|
||||
|
||||
const ParticlesCanvas = () => {
|
||||
if (typeof window === 'undefined') return null
|
||||
const { config, particles } = useParticlesConfig()
|
||||
|
||||
const Geometry = useMemo(
|
||||
() => () => <circleGeometry args={[config.particlesSize, config.particlesSides]} />,
|
||||
[]
|
||||
)
|
||||
|
||||
const Material = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
blending={config.particlesBlending ? AdditiveBlending : undefined}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ width: 100vw, height: 100vh, background: "#000000" }}>
|
||||
<Canvas
|
||||
dpr={[1, 2]}
|
||||
camera={{ fov: 75, position: [0, 0, 500] }}
|
||||
>
|
||||
<ambientLight intensity={config.lightIntensity} />
|
||||
<group>
|
||||
{particles?.map((particle, index) => (
|
||||
<Particle
|
||||
key={particle.username}
|
||||
>
|
||||
<Geometry />
|
||||
<Material />
|
||||
</Particle>
|
||||
))}
|
||||
</group>
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Particle = ({ children }: Props) => {
|
||||
const particle = useRef(null)
|
||||
|
||||
const pathOffset =
|
||||
Math.pow(
|
||||
Math.random() * config.xRandomnessShape,
|
||||
config.xRandomness - config.xRandomness / 2
|
||||
) * config.xThickness
|
||||
|
||||
const verticalRandomness = Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2
|
||||
|
||||
const speed = Math.random() * (config.min_speed - config.max_speed) + config.max_speed
|
||||
|
||||
const circumference = (config.widthRadius * Math.PI * 2) / 100
|
||||
const delayOffsetFactor = 100
|
||||
const delayOffset = Math.random() * delayOffsetFactor
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
const timer = clock.getElapsedTime() * speed + delayOffset
|
||||
const isEven = Math.floor(timer / circumference) % 2 == 0
|
||||
|
||||
// When the loop count is even, draw bottom 8 shape
|
||||
// if odd, draw top 8 shape
|
||||
particle.current.position.x = isEven
|
||||
? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset
|
||||
: Math.sin(timer) * config.widthRadius + pathOffset
|
||||
particle.current.position.y = isEven
|
||||
? Math.cos(timer) * config.bottomHeightRadius -
|
||||
config.bottomHeightRadius +
|
||||
verticalRandomness
|
||||
: -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness
|
||||
})
|
||||
|
||||
return <mesh ref={particle}>{children}</mesh>
|
||||
}
|
||||
|
||||
export default Particle
|
||||
```
|
||||
|
||||
Now **_THAT’S_** how you create a new constellation ✨. Feel free to use the code and learnings to build your own.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In this journey, you saw how to use Three.js and harness the power of React Three Fiber and creative experimentation to craft an animation. We leveraged trigonometry, animation hooks, and GUI playgrounds to build a "8" shape formed by user-generated stars.
|
||||
|
||||
If you loved this and the new [Launch Week 8](https://supabase.com/launch-week) branding, make sure to tune in on Monday at 09 AM PT as we unveil the full landing 💥
|
||||
|
||||
## More Supabase Design
|
||||
|
||||
- [Designing with AI: Generating unique artwork for every user](https://supabase.com/blog/designing-with-ai-midjourney)
|
||||
- **[Infinite scroll with Next.js, Framer Motion, and Supabase](https://supabase.com/blog/infinite-scroll-with-nextjs-framer-motion)**
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Why we'll stay remote"
|
||||
description: Offices are making a comeback, just not at Supabase.
|
||||
author: ant_wilson
|
||||
image: remote/og.jpg
|
||||
thumb: remote/thumb.jpg
|
||||
launchweek: 8
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2023-08-05'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
There’s been a major U-turn in people’s attitudes towards remote this year. In 2020-2021 we had the ‘Remote Revelation’ where everyone and their dog realized they can do their job perfectly well from the comfort of their own home (oftentimes in the company of said dog).
|
||||
|
||||
Now in 2023, there’s been whiplash back to the office with larger companies fighting especially hard to convince their employees to return to their desks.
|
||||
|
||||
Supabase started in January 2020, and the plan was always to be fully remote. In some ways COVID gave us a competitive advantage in that we could offer a stable and supportive remote working environment to those whose employers were struggling to adapt. Even now as companies fight for people to go back to the office, we’re attracting a lot of talented people who moved out of major cities and would rather hop somewhere supportive rather than relocate or commute.
|
||||
|
||||
There are good arguments on both sides. And while you might point out that it’s probably too impractical for Supabase to switch to in-person now anyway (we’re already in >25 countries), the argument I’m making here is that if we were starting again tomorrow, we would do it all the same.
|
||||
|
||||
## No Commute
|
||||
|
||||
During my first job out of college I spent an average of 2 hours every day driving to the office and back. This was valuable thinking time, it allowed me to rediscover the entire Weezer back catalog. Being in the days before (good) podcasts it gave me time to listen to the Today programme on Radio 4 and catch up on what was happening in the world. Despite all this however, it struck me as one of the most clearly insane things we force humans to do on a daily basis - spend an average of 20% of their waking hours driving or on public transport. Many people report that the first 2 hours after waking are their most productive; are we really asking them to spend those precious hours commuting? It does make some sense from the companies perspective, because the company doesn’t pay for this time, nor do they usually directly pay for the cost of the commute (which can often take a hefty chunk out of your wage). In exchange they get the benefit of extra productivity from the portion of the population who require a lot of face time in order to get their job done. While I miss listening to Pinkerton on repeat, it turned out there are other ways to get back my thinking/music/podcast time and that is going for a long walk in the woods, or commuting to the gym.
|
||||
|
||||
## Requiring a lot of face time to get your job done
|
||||
|
||||
There’s a large portion of the population who fit this description, and without casting any negative judgement, we have to be open and honest with candidates that if they’re in this camp - they’ll likely be unhappy working at Supabase. In the early days we thought that these people could adapt, or perhaps be able to satisfy their need for face to face interactions outside of work, but on the whole it wasn’t the case. We actively try to reduce the number of meetings happening across the company (both in person and via video call) which increases friction with these folks. By not supporting this group, we’re reducing the size of the population from which we can hire, but overall our hiring pool is still very much larger than the average in-person company. Thanks to the likes of Rippling, Remote, and Deel, we can extend our talent search globally - and it turns out there are a hell of a lot of _extremely_ talented and highly motivated people living outside of New York, London, and San Francisco (don’t worry we take folks from these cities as well!). After all, the goal of hiring should be to source the absolute best person for the job. Statistically speaking it’s unlikely that this person lives within an hour drive to your office.
|
||||
|
||||
## More take home pay
|
||||
|
||||
For the major US cities, we pay somewhere in the 60-80th percentile range, but for almost everywhere else we _easily_ pay in the 95th percentile (in many cases in the 99th percentile) because we don’t do geo-adjusted pay. If you want to live off the beaten path, where the air is clean, and the taxes are low - then that’s entirely up to you. We won’t lower your pay and you’ll end up with much more cash in your pocket at the end of the month. We’re strongly of the opinion that you should get the same pay for the same job, geo-adjusted pay always seemed a bit bizarre to me. It’s undeniable that major cities have a higher concentration of technical talent, but these people are taxed accordingly. If not on their income, then most certainly on their rent and other expenses.
|
||||
|
||||
## Won’t somebody think of the young people
|
||||
|
||||
An argument I often hear for being in the office, is that young people joining the workforce can’t learn the ways of the working world as quickly when working remotely because they don’t get as much exposure to senior members of the team. This may be true. In my experience, young people seem much better adapted to working remotely than people who have been in the workforce for several decades, however it doesn’t solve for the more experienced folks being visible and available for the juniors to learn from. At Supabase, we very rarely hire juniors. That’s not to say we don’t hire young people, since there are plenty of highly skilled young people who can out-ship people twice their age. But in terms of team make-up, our team is very highly skewed towards seniors and leads. So we don’t really suffer much from this issue (if it exists).
|
||||
|
||||
## Global Coverage
|
||||
|
||||
Supabase now has team members in more than 25 countries. One benefit of this as an infrastructure provider is that we get 24 hour support coverage for free. If your database is showing signs of trouble at 3am, you don’t need to worry because we have team members on the clock and actively responding to system alerts. It also means that the shipping quite literally never stops.
|
||||
|
||||
When interviewing candidates a common question we get is around working hours, and the honest answer is that we don’t care when you work. With the number of different timezones we operate across it would be impossible for us to track anyway. Some people prefer to start their day at 5am, and others prefer to work late into the night. That’s fine by us.
|
||||
|
||||
Most of the people we hire have a background in open source and contribute to large projects like PostgreSQL where most things run asynchronously. They are well adapted to working on complex designs independently. It seems that this style of open source is becoming more prevalent thanks in part to the meteoric success of GitHub. As a consequence, the pool of talent is undoubtedly growing.
|
||||
|
||||
Another huge benefit of hiring from anywhere is that you end up with an incredibly diverse team. This helps when selling to a global audience of developers, and ensures diversity of thought and a broad range of perspectives.
|
||||
|
||||
## Won’t somebody think of the commercial real-estate investors
|
||||
|
||||
A huge and obvious advantage of not having an office… is not having to pay for an office. Dedicated offices can get pretty expensive, so I’m not surprised large companies resent sitting on acres of unused office space. But fear not, here at Supabase we’ve uncovered an ingenious solution to keeping team members happy and at the same time supporting our local real estate developers. We give everyone the option of a desk at a local co-working space. In cities where we have many employees they tend to opt for the WeWork floating desk. They can also access WeWork locations globally when they travel (other co-working spaces are available). This adds some operating costs, but it’s far more affordable than a dedicated space, and a lot of people also choose not to take us up on the offer and prefer to work from their kitted out spaces at home, a local coffee shop, or a Brazilian beach resort.
|
||||
|
||||
## The Human Connection
|
||||
|
||||
When you save so much on office space it extends the budget for whole company off-sites (on-sites?). At the end of the day we’re all human, and we know the importance of getting to know your team mates on a human level. That’s why once per year we get everyone together for 11 days. This year we went to Bali.
|
||||
|
||||
On top of whole company trips, folks often self organize co-working trips to places like Malaysia, San Francisco, or Austin, Texas which helps with getting to know folks outside of your immediate team.
|
||||
|
||||
## Productivity
|
||||
|
||||
After all is said and done, companies will still ask “but where will my team be most productive?”. To be honest I think the Supabase team would be just as productive in an office as they would be from home, but would we have been able to hire these same folks without a remote working policy? Absolutely not.
|
||||
|
||||
A lot of people’s hesitations around remote seem to stem from not having trust in their team to get things done without adult supervision. However, if I know anything about high performing teams, it’s that you need to have mutual trust (which breeds psychological safety), and a shared model of what success looks like (metrics anyone?). Without these you’re going to struggle to produce results office or no office.
|
||||
|
||||
At Supabase we do what works for Supabase, and for many companies being in a dedicated office or hybrid will undoubtably be what’s best for them.
|
||||
|
||||
The book that shaped my thinking on remote more than any other was Spolsky’s ‘Smart and Gets Things Done’ (I don’t even know if it’s in print any more?), but the summary is this:
|
||||
|
||||
Hire people who are:
|
||||
|
||||
- Smart
|
||||
- Get Things Done
|
||||
|
||||
Everything else is a Nice to Have. Including proximity.
|
||||
|
||||
Lastly, if you hate wasting time commuting and want to work with some of the best engineers in the world, [we’re hiring](https://supabase.com/careers).
|
||||
|
||||
Read more:
|
||||
|
||||
- [Who we hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [Supabase Careers](https://supabase.com/careers)
|
||||
- [See what's new in Supabase](https://supabase.com/launch-week)
|
||||
@@ -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(' ')}
|
||||
|
||||
@@ -54,13 +54,8 @@ export default function LaunchWeekPrizeSection({
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-4xl">
|
||||
Get your <span className="gradient-text-pink-500">winning ticket</span>
|
||||
Submissions <span className="gradient-text-pink-500">closed</span>
|
||||
</h2>
|
||||
<p className="radial-gradient-text-scale-500">
|
||||
Join us on April 16th for Launch Week 7's final day
|
||||
<br className="hidden md:inline-block" /> and find out if you are one of the lucky
|
||||
winners. Get sharing!
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
{ticket && <Ticket />}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,56 +3,18 @@ import { Canvas } from '@react-three/fiber'
|
||||
import { AdditiveBlending } from 'three'
|
||||
import Particle from './Particle'
|
||||
import useParticlesConfig from './hooks/useParticlesConfig'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users: any }) => {
|
||||
const ParticlesCanvas = () => {
|
||||
const isWindowUndefined = typeof window === 'undefined'
|
||||
if (isWindowUndefined) return null
|
||||
|
||||
const canvasRef = React.useRef(null)
|
||||
|
||||
const [animate, setAnimate] = useState<boolean>(true)
|
||||
const { config, handleSetConfig, particles, setParticles, isDebugMode } =
|
||||
useParticlesConfig(users)
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
(typeof supabase | any)['channel']
|
||||
> | null>(null)
|
||||
|
||||
const loadUsers = async () => {
|
||||
return await supabase!.from('lw8_tickets_golden').select('username, golden', { count: 'exact' })
|
||||
}
|
||||
|
||||
// Update particles live when new tickets are generated
|
||||
useEffect(() => {
|
||||
if (!!supabase) {
|
||||
const channel = supabase
|
||||
.channel('lw8_tickets_changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'lw8_tickets_golden',
|
||||
},
|
||||
async () => {
|
||||
const { data: users } = await loadUsers()
|
||||
|
||||
handleSetConfig('particles', users?.length)
|
||||
setParticles((prev: any) => users! ?? prev)
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
const { config, particles } = useParticlesConfig()
|
||||
|
||||
// stop animation if canvas if is not in viewport
|
||||
// to avoid unnecessary computations
|
||||
// to stop unnecessary computations
|
||||
const handleScroll = () => {
|
||||
if (canvasRef.current && typeof window !== 'undefined') {
|
||||
const rect = (canvasRef.current as HTMLDivElement)?.getBoundingClientRect()
|
||||
@@ -83,7 +45,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users
|
||||
const Material = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<meshStandardMaterial
|
||||
<meshPhysicalMaterial
|
||||
color={config.color}
|
||||
blending={config.particlesBlending ? AdditiveBlending : undefined}
|
||||
/>
|
||||
@@ -110,8 +72,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users
|
||||
className="relative z-30"
|
||||
>
|
||||
<ambientLight intensity={config.lightIntensity} />
|
||||
<group position={[0, 70, 0]} scale={[0.9, 0.9, 0.9]}>
|
||||
{/* Animated 8 shape particles */}
|
||||
<group position={[0, 70, 0]} scale={[0.75, 0.75, 0.75]}>
|
||||
{particles?.map((user: any, index: number) => (
|
||||
<Particle
|
||||
key={`particle-${user.username ?? index}`}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { range } from 'lodash'
|
||||
|
||||
let defaultConfig = {
|
||||
particles: 20,
|
||||
particles: 700,
|
||||
particlesSize: 1.2,
|
||||
goldParticlesSize: 1.5,
|
||||
particlesSides: 5,
|
||||
@@ -12,11 +12,7 @@ let defaultConfig = {
|
||||
widthRatio: 1.2,
|
||||
topHeightRadius: 80,
|
||||
bottomHeightRadius: 100,
|
||||
trails: 0,
|
||||
trailWidth: 40,
|
||||
trailLength: 100,
|
||||
trailDecay: 450,
|
||||
color: 'white',
|
||||
color: '#ffffff',
|
||||
colorGold: '#b89d18',
|
||||
xThickness: 7,
|
||||
xRandomnessFactor: 2.2,
|
||||
@@ -28,12 +24,12 @@ let defaultConfig = {
|
||||
showGold: true,
|
||||
}
|
||||
|
||||
const useParticlesConfig = (users: any): any => {
|
||||
const useParticlesConfig = (): any => {
|
||||
const isWindowUndefined = typeof window === 'undefined'
|
||||
if (isWindowUndefined) return null
|
||||
const hash = window.location.hash
|
||||
const isDebugMode = hash.includes('#debug')
|
||||
const [particles, setParticles] = useState<any[]>(users)
|
||||
const [particles, setParticles] = useState<any[]>(range(0, defaultConfig.particles))
|
||||
|
||||
const [config, setConfig] = useState(defaultConfig)
|
||||
|
||||
@@ -48,7 +44,7 @@ const useParticlesConfig = (users: any): any => {
|
||||
const particlesFolder = gui.addFolder('Particles')
|
||||
const shapeFolder = gui.addFolder('Shape')
|
||||
const speedFolder = gui.addFolder('Speed')
|
||||
// const trailFolder = gui.addFolder('Trail')
|
||||
|
||||
gui.width = 500
|
||||
particlesFolder
|
||||
.add(config, 'particles')
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import React from 'react'
|
||||
import Countdown from 'react-countdown'
|
||||
import { CountdownWidget } from 'ui'
|
||||
import { LW8_LAUNCH_DATE } from '~/lib/constants'
|
||||
|
||||
const CountdownComponent = () => {
|
||||
const CountdownComponent = ({
|
||||
date,
|
||||
showCard = true,
|
||||
}: {
|
||||
date: string | number | Date
|
||||
showCard?: boolean
|
||||
}) => {
|
||||
if (!date) return null
|
||||
|
||||
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return null
|
||||
} else {
|
||||
// Render countdown
|
||||
return <CountdownWidget days={days} hours={hours} minutes={minutes} seconds={seconds} />
|
||||
return (
|
||||
<CountdownWidget
|
||||
days={days}
|
||||
hours={hours}
|
||||
minutes={minutes}
|
||||
seconds={seconds}
|
||||
showCard={showCard}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <Countdown date={new Date(LW8_LAUNCH_DATE)} renderer={renderer} />
|
||||
return <Countdown date={new Date(date)} renderer={renderer} />
|
||||
}
|
||||
|
||||
export default CountdownComponent
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import SectionContainer from '../../Layouts/SectionContainer'
|
||||
import { SmallCard } from '../8/Releases/components'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
const LW8CalloutsSection = () => {
|
||||
return (
|
||||
<SectionContainer className="!py-0 w-full !px-0 !max-w-none">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 scroll-mt-[105px]" id="hackathon">
|
||||
<SmallCard className="hover:from-scale-900 hover:to-scale-900">
|
||||
<Link href="https://twitter.com/i/spaces/1DXGyvjkeEeJM">
|
||||
<a
|
||||
target="_blank"
|
||||
className="flex flex-row justify-between items-center w-full h-full gap-2"
|
||||
>
|
||||
<div className="relative flex-shrink flex items-center p-2 w-2/3 lg:w-1/2 md:w-auto">
|
||||
<div className="flex flex-col gap-1 sm:pl-4">
|
||||
<span className="text-white">Join us daily at Twitter Spaces </span>
|
||||
<span className="">Next up: Monday – Opening Day</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex !aspect-video h-[80px] md:h-[100px] gap-2 z-10 rounded overflow-hidden">
|
||||
<Image
|
||||
src="/images/launchweek/8/twitter-spaces-thumb.svg"
|
||||
alt="twitter spaces thumbnail"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</SmallCard>
|
||||
<SmallCard className="hover:from-scale-900 hover:to-scale-900">
|
||||
<Link href="/blog/supabase-lw8-hackathon">
|
||||
<a className="flex flex-row justify-between items-center w-full h-full gap-2">
|
||||
<div className="relative flex-shrink flex items-center p-2 w-2/3 lg:w-1/2 md:w-auto">
|
||||
<div className="flex flex-col gap-1 sm:pl-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-2 h-2 flex items-center justify-center">
|
||||
<div className="absolute w-2 h-2 rounded-full bg-white" />
|
||||
<div className="absolute animate-pulse-radar w-6 h-6 rounded-full bg-white will-change-transform" />
|
||||
</div>
|
||||
<span className="text-white">LW8 Hackathon Aug 4 – Aug 13</span>
|
||||
</div>
|
||||
<span className="">Read more about rules and prizes on the blog</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex !aspect-video h-[80px] md:h-[100px] gap-2 z-10 rounded overflow-hidden">
|
||||
<Image
|
||||
src="/images/launchweek/8/lw8-hackathon-thumb.svg"
|
||||
alt="hackathon thumbnail"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</SmallCard>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LW8CalloutsSection
|
||||
@@ -0,0 +1,105 @@
|
||||
import Link from 'next/link'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Badge } from 'ui'
|
||||
import useConfData from '../hooks/use-conf-data'
|
||||
|
||||
export interface Meetup {
|
||||
id?: any
|
||||
title: string
|
||||
isLive: boolean
|
||||
link: string
|
||||
display_info: string
|
||||
start_at: string
|
||||
}
|
||||
|
||||
const LW8Meetups = ({ meetups }: { meetups?: Meetup[] }) => {
|
||||
const { supabase } = useConfData()
|
||||
const [meets, setMeets] = useState<Meetup[]>(meetups ?? [])
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
(typeof supabase)['channel']
|
||||
> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Listen to realtime changes
|
||||
if (supabase && !realtimeChannel) {
|
||||
const channel = supabase
|
||||
.channel('changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'lw8_meetups',
|
||||
},
|
||||
async () => {
|
||||
const { data: newMeets } = await supabase.from('lw8_meetups').select('*')
|
||||
setMeets(newMeets)
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto grid grid-cols-1 xl:grid-cols-12 gap-8 xl:gap-8">
|
||||
<div className="col-span-1 xl:col-span-4 flex flex-col justify-center max-w-lg">
|
||||
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-.5px]">Join the community</h2>
|
||||
<p className="text-scale-1000 text-base py-3 xl:max-w-md">
|
||||
Celebrate LW8 at our first-ever live community meetups across various locations. Join the
|
||||
fun, grab some swag, and meet the Supabase community.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 xl:col-span-7 xl:col-start-6 w-full max-w-4xl flex flex-col justify-between items-stretch">
|
||||
{meets &&
|
||||
meets
|
||||
?.sort((a, b) => (new Date(a.start_at) > new Date(b.start_at) ? 1 : -1))
|
||||
.map(({ display_info, link, isLive, title }: Meetup) => (
|
||||
<Link href={link ?? '#'}>
|
||||
<a
|
||||
target="_blank"
|
||||
className={[
|
||||
'w-full group py-0 flex items-center gap-2 md:gap-4 text-lg sm:text-2xl xl:text-4xl border-b border-[#111718]',
|
||||
'hover:text-scale-1200',
|
||||
isLive ? 'text-scale-1100' : 'text-[#56646B]',
|
||||
!link && 'pointer-events-none',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex items-center gap-2 md:gap-4 py-2 md:py-0">
|
||||
<span>{title}</span>
|
||||
{isLive && <Badge>Live now</Badge>}
|
||||
<span className="hidden md:inline opacity-0 -translate-x-2 transition-all md:group-hover:opacity-100 group-hover:translate-x-0">
|
||||
<svg
|
||||
width="47"
|
||||
height="47"
|
||||
viewBox="0 0 47 47"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M33.0387 16.2422L40.6691 23.8727M40.6691 23.8727L33.0387 31.5031M40.6691 23.8727L6.33203 23.8727"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{display_info && (
|
||||
<span className="text-sm text-right flex-1">{display_info}</span>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LW8Meetups
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { StyledArticleBadge } from './Releases/components'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
|
||||
import 'swiper/swiper.min.css'
|
||||
|
||||
const launchweeks = [
|
||||
{ title: '7', link: '/launchweek/7' },
|
||||
{ title: '6', link: '/launchweek/6' },
|
||||
{ title: '5', link: '/blog/launch-week-5-hackathon' },
|
||||
{ title: '4', link: '/blog/supabase-launch-week-four' },
|
||||
{ title: '3', link: '/blog/supabase-launch-week-the-trilogy' },
|
||||
{ title: '2', link: '/blog/supabase-launch-week-sql' },
|
||||
{ title: '1', link: '/blog/launch-week' },
|
||||
]
|
||||
|
||||
const LWArchive = () => {
|
||||
return (
|
||||
<div className="text-center relative z-10 text-white flex flex-col gap-8 lg:gap-16">
|
||||
<div className="max-w-[38rem] mx-auto flex flex-col items-center gap-4 px-4">
|
||||
<StyledArticleBadge>Throwback</StyledArticleBadge>
|
||||
<h2 className="text-4xl">Previous Launch Weeks</h2>
|
||||
<p className="text-[#9296AA]">
|
||||
Explore what has been announced in the past and relive those moments.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-[200px] [&_.swiper-container]:h-full [&_.swiper-wrapper]:h-full [&_.swiper-slide]:h-full">
|
||||
<Swiper
|
||||
// centeredSlides={true}
|
||||
spaceBetween={10}
|
||||
slidesPerView={5.5}
|
||||
// watchOverflow
|
||||
threshold={2}
|
||||
updateOnWindowResize
|
||||
breakpoints={{
|
||||
320: {
|
||||
slidesPerView: 2.5,
|
||||
},
|
||||
720: {
|
||||
slidesPerView: 3.5,
|
||||
},
|
||||
1440: {
|
||||
slidesPerView: 5.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{launchweeks.map((launchweek, i) => (
|
||||
<SwiperSlide key={launchweek.title}>
|
||||
<Link href={launchweek.link} key={launchweek.title}>
|
||||
<a className="relative h-full rounded-md md:rounded-lg transition-transform bg-[#030A0C] flex items-center justify-center border border-[#111718] hover:border-brand-800">
|
||||
<span className="stroke-text inline-block text-transparent bg-clip-text bg-gradient-to-b from-scale-1000 to-scale-200 text-7xl">
|
||||
{launchweek.title}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LWArchive
|
||||
@@ -1,12 +1,13 @@
|
||||
import Link from 'next/link'
|
||||
import CountdownComponent from './Countdown'
|
||||
import { LW8_LAUNCH_DATE } from '../../../lib/constants'
|
||||
|
||||
export function LaunchWeekLogoHeader() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 md:gap-2 items-center justify-end">
|
||||
<div className="opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both] px-2 flex flex-col items-center text-center gap-3">
|
||||
<div className="">
|
||||
<CountdownComponent />
|
||||
<CountdownComponent date={LW8_LAUNCH_DATE} />
|
||||
</div>
|
||||
<h1 className="sr-only font-normal uppercase text-[28px] sm:text-[32px]">Launch week 8</h1>
|
||||
<p className="text-white radial-gradient-text-scale-600 text-lg sm:text-2xl">
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { Badge } from 'ui'
|
||||
|
||||
import Link from 'next/link'
|
||||
import CountdownComponent from '../../Countdown'
|
||||
|
||||
export const PencilSvg = () => (
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.21792 11.2469L11.8015 2.66333L14.0953 4.95709L5.51167 13.5407M3.21792 11.2469L2.34219 14.4164L5.51167 13.5407M3.21792 11.2469L5.51167 13.5407"
|
||||
stroke="#8F8F8F"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const DocsSvg = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.96289 9.48618H10.9629M4.96289 7.48618H10.9629M4.96289 11.4862H8.96289M3 2.00034V13.9998H12.9996V5.60113L9.38156 2.00034H3ZM12.9644 5.58432L9.38004 2L9.38004 5.58432L12.9644 5.58432Z"
|
||||
stroke="#8F8F8F"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const PlaySvg = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13.4287 8L8.74762 10.7026L4.06653 13.4053L4.06653 8L4.06653 2.59474L8.74762 5.29737L13.4287 8Z"
|
||||
stroke="#8F8F8F"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const GithubSvg = () => (
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.2125 1.75781C4.6765 1.75781 1.8125 4.62181 1.8125 8.15781C1.8125 10.9898 3.6445 13.3818 6.1885 14.2298C6.5085 14.2858 6.6285 14.0938 6.6285 13.9258C6.6285 13.7738 6.6205 13.2698 6.6205 12.7338C5.0125 13.0298 4.5965 12.3418 4.4685 11.9818C4.3965 11.7978 4.0845 11.2298 3.8125 11.0778C3.5885 10.9578 3.2685 10.6618 3.8045 10.6538C4.3085 10.6458 4.6685 11.1178 4.7885 11.3098C5.3645 12.2778 6.2845 12.0058 6.6525 11.8378C6.7085 11.4218 6.8765 11.1418 7.0605 10.9818C5.6365 10.8218 4.1485 10.2698 4.1485 7.82181C4.1485 7.12581 4.3965 6.54981 4.8045 6.10181C4.7405 5.94181 4.5165 5.28581 4.8685 4.40581C4.8685 4.40581 5.4045 4.23781 6.6285 5.06181C7.1405 4.91781 7.6845 4.84581 8.2285 4.84581C8.7725 4.84581 9.3165 4.91781 9.8285 5.06181C11.0525 4.22981 11.5885 4.40581 11.5885 4.40581C11.9405 5.28581 11.7165 5.94181 11.6525 6.10181C12.0605 6.54981 12.3085 7.11781 12.3085 7.82181C12.3085 10.2778 10.8125 10.8218 9.3885 10.9818C9.6205 11.1818 9.8205 11.5658 9.8205 12.1658C9.8205 13.0218 9.8125 13.7098 9.8125 13.9258C9.8125 14.0938 9.9325 14.2938 10.2525 14.2298C11.523 13.8009 12.627 12.9844 13.4092 11.8951C14.1913 10.8059 14.6122 9.49877 14.6125 8.15781C14.6125 4.62181 11.7485 1.75781 8.2125 1.75781Z"
|
||||
fill="#8F8F8F"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const LinkSvg = () => (
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.875 4.0013H4.20833C3.47195 4.0013 2.875 4.59826 2.875 5.33464V12.0013C2.875 12.7377 3.47195 13.3346 4.20833 13.3346H10.875C11.6114 13.3346 12.2083 12.7377 12.2083 12.0013V9.33464M9.54167 2.66797H13.5417M13.5417 2.66797V6.66797M13.5417 2.66797L6.875 9.33464"
|
||||
stroke="#8F8F8F"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const ArrowTopRightSvg = () => (
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.44531 13.846L13.1152 4.17578"
|
||||
stroke="#8F8F8F"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<path
|
||||
d="M3.44531 4.09375H13.1152V13.7637"
|
||||
stroke="#8F8F8F"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const HackernewsSvg = () => (
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.26821 7.87591L9.94194 4.47266H11.8008L8.82871 10.074V13.606H7.16205V10.0213L4.1875 4.47266H6.15549L7.86748 7.9537C7.87321 7.95954 7.87972 7.96606 7.88751 7.97386C7.95585 8.04219 7.98757 8.11202 8.00726 8.16124C8.01322 8.17614 8.01776 8.18804 8.02175 8.19849C8.02548 8.20824 8.02872 8.21672 8.03217 8.22518L8.04751 8.24052C8.11375 8.10585 8.19796 7.98119 8.26821 7.87591Z"
|
||||
fill="#8F8F8F"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const SmallCard = ({ className, children }: { className?: string; children: any }) => (
|
||||
<div
|
||||
className={[
|
||||
'relative p-[1px] bg-gradient-to-b from-[#111718] to-[#1C1C1C60] rounded-2xl overflow-hidden shadow-lg',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'rounded-2xl text-sm text-[#9296AA] p-2 flex flex-row justify-between items-center backdrop-blur-md h-full',
|
||||
'bg-[#030A0C]',
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const StyledArticleBadge = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string
|
||||
children: any
|
||||
}) => (
|
||||
<div
|
||||
className={['relative bg-transparent border border-[#F4FFFA90] rounded-full', className].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
<div className="!bg-transparent rounded-full !py-1 !px-4 w-full inset-[1px] text-sm border-none dark:from-white dark:to-[#6453C5] dark:border-none">
|
||||
<span className="text-sm text-[#F4FFFA80] bg-clip-text bg-gradient-to-r from-[#F4FFFA] to-[#7E7AAD]">
|
||||
{children}
|
||||
</span>
|
||||
<div className="absolute inset-0 w-full h-full bg-[#1C1C1C] rounded-full blur-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const AccordionHeader = ({
|
||||
date,
|
||||
day,
|
||||
weekDay,
|
||||
title,
|
||||
shipped,
|
||||
publishedAt,
|
||||
shippable = true,
|
||||
}: {
|
||||
date: string
|
||||
day: number
|
||||
weekDay: string
|
||||
title: string
|
||||
shipped?: boolean
|
||||
publishedAt: string
|
||||
shippable?: boolean
|
||||
}) => (
|
||||
<div
|
||||
className={['flex flex-1', shippable && shipped ? 'flex-col sm:flex-row' : 'flex-row'].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-4 w-full sm:w-auto sm:min-w-[240px] md:min-w-[380px] items-center">
|
||||
<span className="text-scale-900 text-sm">
|
||||
<span className="inline sm:hidden md:inline">{weekDay} </span>
|
||||
{date && (
|
||||
<span>
|
||||
<span className="inline sm:hidden md:inline">・</span> {date}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{shippable && shipped && (
|
||||
<Badge
|
||||
className={`relative inset-0 !bg-transparent !py-1 !px-4 h-fit backdrop-blur-md ${
|
||||
shipped
|
||||
? 'bg-gradient-to-br from-[#05090B] to-[#05090B] !border-[#061517]'
|
||||
: 'border-[#FFFFFF20]'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[#A0A0A0] text-sm font-normal bg-clip-text bg-gradient-to-r from-[#F4FFFA] to-[#675FA7]">
|
||||
Shipped
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{shipped && <span className="text-scale-1200 text-lg mt-1 sm:mt-0">{title}</span>}
|
||||
{shippable && !shipped && (
|
||||
<span className="text-sm font-normal text-[#A0A0A0] mt-1 sm:mt-0 flex items-center gap-1 md:gap-4">
|
||||
<svg
|
||||
width="17"
|
||||
height="17"
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.32656 7.58047V5.98047C4.32656 3.77133 6.11742 1.98047 8.32656 1.98047C10.5357 1.98047 12.3266 3.77133 12.3266 5.98047V7.58047C13.2102 7.58047 13.9266 8.29681 13.9266 9.18047V13.1805C13.9266 14.0641 13.2102 14.7805 12.3266 14.7805H4.32656C3.44291 14.7805 2.72656 14.0641 2.72656 13.1805V9.18047C2.72656 8.29681 3.44291 7.58047 4.32656 7.58047ZM10.7266 5.98047V7.58047H5.92656V5.98047C5.92656 4.65499 7.00108 3.58047 8.32656 3.58047C9.65205 3.58047 10.7266 4.65499 10.7266 5.98047Z"
|
||||
fill="#A0A0A0"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{day === 1 && <CountdownComponent date={publishedAt} showCard={false} />}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const MultistepSectionHeader = ({ title, blog }: any) => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col sm:flex-row py-8">
|
||||
<div className="flex gap-4 w-full items-center justify-between md:justify-start">
|
||||
{title && <span className="text-scale-1200 text-lg mt-3 sm:mt-0">{title}</span>}
|
||||
{!!blog && (
|
||||
<ChipLink href={blog} className="!w-auto !text-left !justify-between !flex-none">
|
||||
Blog post
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<PencilSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChipLink = ({
|
||||
href,
|
||||
uiOnly,
|
||||
className,
|
||||
target,
|
||||
children,
|
||||
}: {
|
||||
href?: string
|
||||
className?: string
|
||||
uiOnly?: boolean
|
||||
target?: '_blank' | '_self' | '_parent' | '_top' | 'framename'
|
||||
children: any
|
||||
}) =>
|
||||
uiOnly ? (
|
||||
<span
|
||||
className={[
|
||||
'flex flex-auto justify-center sm:justify-between w-full text-center sm:text-left min-h-[43px] sm:w-auto items-center border border-[#232323] bg-gradient-to-r text-white from-[#46444460] to-[#19191980] hover:from-[#4e4e4e90] hover:to-[#19191990] hover:border-scale-800 backdrop-blur-xl rounded-full text-sm py-2 px-3 sm:pr-2',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
) : !!href ? (
|
||||
<Link href={href}>
|
||||
<a
|
||||
target={target ?? '_self'}
|
||||
rel="noopener"
|
||||
className={[
|
||||
'flex flex-auto justify-center sm:justify-between w-full text-center sm:text-left min-h-[43px] sm:w-auto items-center border border-[#232323] bg-gradient-to-r text-white from-[#46444460] to-[#19191980] hover:from-[#4e4e4e90] hover:to-[#19191990] hover:border-scale-800 backdrop-blur-xl rounded-full text-sm py-2 px-3 sm:pr-2',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
) : null
|
||||
|
||||
export const SectionButtons = ({
|
||||
blog,
|
||||
docs,
|
||||
video,
|
||||
github,
|
||||
url,
|
||||
hackernews,
|
||||
mobileGrid,
|
||||
}: {
|
||||
blog?: string
|
||||
docs?: string
|
||||
video?: string
|
||||
github?: string
|
||||
url?: string
|
||||
hackernews?: string
|
||||
mobileGrid?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'flex w-full max-w-full md:w-auto justify-center gap-2 z-10',
|
||||
mobileGrid && 'grid grid-cols-2 gap-2 sm:flex',
|
||||
].join(' ')}
|
||||
>
|
||||
{!!blog && (
|
||||
<ChipLink href={blog}>
|
||||
Blog post
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<PencilSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
{!!docs && (
|
||||
<ChipLink href={docs}>
|
||||
Docs
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<DocsSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
{!!video && (
|
||||
<ChipLink href={video} target="_blank">
|
||||
Video
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<PlaySvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
{!!github && (
|
||||
<ChipLink href={github} target="_blank">
|
||||
Github
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<GithubSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
{!!url && (
|
||||
<ChipLink href={url}>
|
||||
Read
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<LinkSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
{hackernews && (
|
||||
<ChipLink href={hackernews} target="_blank">
|
||||
Hackernews
|
||||
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
|
||||
<HackernewsSvg />
|
||||
</div>
|
||||
</ChipLink>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CartTitle = ({ children, className }: { children: any; className?: string }) => (
|
||||
<span
|
||||
className={[
|
||||
'z-0 relative text-[#F4FFFA90] bg-clip-text bg-gradient-to-r from-[#F4FFFA] to-[#675FA7] tracking-[-.5px] text-xl',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
<div className="absolute -z-10 inset-0 w-full h-full bg-[#1C1C1C90] rounded-full blur-xl" />
|
||||
</span>
|
||||
)
|
||||
|
||||
export default {
|
||||
AccordionHeader,
|
||||
DocsSvg,
|
||||
PencilSvg,
|
||||
PlaySvg,
|
||||
CartTitle,
|
||||
ChipLink,
|
||||
SectionButtons,
|
||||
SmallCard,
|
||||
StyledArticleBadge,
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import Image from 'next/image'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Accordion } from 'ui'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
|
||||
import days, { WeekDayProps } from './lw8_data'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import { AccordionHeader, CartTitle, SectionButtons, StyledArticleBadge } from './components'
|
||||
|
||||
export const defaultEase = [0.25, 0.25, 0, 1]
|
||||
export const defaultDuratonIn = 0.25
|
||||
export const defaultDuratonOut = 0.1
|
||||
|
||||
export const opacityVariant = {
|
||||
default: { opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const opacityVariant2 = {
|
||||
default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
opacity: 0.2,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const opacityVariant3 = {
|
||||
default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const opacityVariant4 = {
|
||||
default: { opacity: 0.3, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const scaleOpacityVariant = {
|
||||
default: { scale: 1, opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
scale: 1.05,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const scaleOpacityVariant2 = {
|
||||
default: { scale: 1, opacity: 0.8, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
scale: 1.025,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
export const aiImageMobileVariant = {
|
||||
default: { scale: 0.9 },
|
||||
hover: {
|
||||
scale: 0.9,
|
||||
},
|
||||
}
|
||||
export const moveX10 = {
|
||||
default: { x: 0, ease: defaultEase, duration: defaultDuratonOut },
|
||||
hover: {
|
||||
x: 10,
|
||||
transition: {
|
||||
duration: defaultDuratonIn,
|
||||
ease: defaultEase,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default function LW8Releases() {
|
||||
const [preRelease, day1, day2, day3, day4, day5] = days
|
||||
const isTablet = useBreakpoint(1023)
|
||||
const isDesktop = useBreakpoint(1280)
|
||||
const showAll = false
|
||||
const publishedSections =
|
||||
days
|
||||
.filter(
|
||||
(day: WeekDayProps) => day.d === 0 || Date.parse(day.publishedAt) <= Date.now() || showAll
|
||||
)
|
||||
.map((day: WeekDayProps) => day.d.toString()) ?? []
|
||||
|
||||
const day1Shipped = Date.parse(day1.publishedAt) <= Date.now()
|
||||
const day2Shipped = Date.parse(day2.publishedAt) <= Date.now()
|
||||
const day3Shipped = Date.parse(day3.publishedAt) <= Date.now()
|
||||
const day4Shipped = Date.parse(day4.publishedAt) <= Date.now()
|
||||
const day5Shipped = Date.parse(day5.publishedAt) <= Date.now()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionContainer className="!pt-0 !w-full !px-0 !max-w-none">
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
size="large"
|
||||
className="text-white"
|
||||
justified={false}
|
||||
chevronAlign="right"
|
||||
defaultValue={publishedSections}
|
||||
>
|
||||
<div className="border-b border-[#111718]" id="currentDay">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={preRelease.date}
|
||||
day={preRelease.d}
|
||||
weekDay={preRelease.dd}
|
||||
title={preRelease.title}
|
||||
shipped={true}
|
||||
shippable={false}
|
||||
publishedAt={preRelease.publishedAt}
|
||||
/>
|
||||
}
|
||||
key={preRelease.dd}
|
||||
className="h-[79px]"
|
||||
id={preRelease.d.toString()}
|
||||
>
|
||||
{preRelease.steps.length > 0 && (
|
||||
<div className="h-[400px] flex flex-col gap-5 lg:flex-row pb-4">
|
||||
<motion.div
|
||||
className={`
|
||||
relative overflow-hidden group/3 flex-1 flex flex-col items-center lg:items-start justify-between
|
||||
basis-1/2 lg:basis-1/3 border border-[#111718] rounded-xl h-full bg-no-repeat py-10 lg:py-14 px-8 lg:px-10 text-2xl bg-contain shadow-lg
|
||||
`}
|
||||
initial="default"
|
||||
animate="default"
|
||||
whileHover="hover"
|
||||
>
|
||||
<div
|
||||
className="inset-0 absolute group-hover/3:scale-105 opacity-60 group-hover/3:opacity-100 w-full h-full -z-10 transition-all duration-500 pointer-events-none"
|
||||
style={{
|
||||
background: `radial-gradient(100% 100% at 80% 110%, #6F13A450, #030A0C)`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center lg:items-start gap-2 max-w-[400px] w-full text-center lg:text-left">
|
||||
<CartTitle>{preRelease.steps[0]?.title}</CartTitle>
|
||||
<p className="text-sm text-slate-900">{preRelease.steps[0]?.description}</p>
|
||||
</div>
|
||||
<SectionButtons blog={preRelease.steps[0].blog} />
|
||||
{preRelease.steps[0]?.bg_layers &&
|
||||
preRelease.steps[0]?.bg_layers?.map(
|
||||
(layer, i) =>
|
||||
!!layer.img && (
|
||||
<motion.div
|
||||
className="absolute opacity-90 inset-0 w-full h-full -z-10"
|
||||
variants={i === 1 ? scaleOpacityVariant : undefined}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
!!layer.mobileImg && isTablet
|
||||
? (layer.mobileImg as any)
|
||||
: layer.img
|
||||
}
|
||||
className={`
|
||||
absolute opacity-90
|
||||
w-full h-full -z-10 transition-all duration-300
|
||||
`}
|
||||
layout="fill"
|
||||
objectPosition={
|
||||
isTablet ? '50%' : isDesktop ? '200% 50%' : '100% 50%'
|
||||
}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b border-[#111718]">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={day1.date}
|
||||
day={day1.d}
|
||||
weekDay={day1.dd}
|
||||
title={day1.title}
|
||||
shipped={day1Shipped}
|
||||
publishedAt={day1.publishedAt}
|
||||
/>
|
||||
}
|
||||
disabled={!day1Shipped}
|
||||
className="h-[79px]"
|
||||
id={day1.d.toString()}
|
||||
></Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b border-[#111718]">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={day2.date}
|
||||
day={day2.d}
|
||||
weekDay={day2.dd}
|
||||
title={day2.title}
|
||||
shipped={day2Shipped}
|
||||
publishedAt={day2.publishedAt}
|
||||
/>
|
||||
}
|
||||
disabled={!day2Shipped}
|
||||
className="h-[79px]"
|
||||
id={day2.d.toString()}
|
||||
></Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b border-[#111718]">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={day3.date}
|
||||
day={day3.d}
|
||||
weekDay={day3.dd}
|
||||
title={day3.title}
|
||||
shipped={day3Shipped}
|
||||
publishedAt={day3.publishedAt}
|
||||
/>
|
||||
}
|
||||
disabled={!day3Shipped}
|
||||
className="h-[79px]"
|
||||
id={day3.d.toString()}
|
||||
></Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b border-[#111718]">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={day4.date}
|
||||
day={day4.d}
|
||||
weekDay={day4.dd}
|
||||
title={day4.title}
|
||||
shipped={day4Shipped}
|
||||
publishedAt={day4.publishedAt}
|
||||
/>
|
||||
}
|
||||
disabled={!day4Shipped}
|
||||
className="h-[79px]"
|
||||
id={day4.d.toString()}
|
||||
></Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b border-[#111718]">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
date={day5.date}
|
||||
day={day5.d}
|
||||
weekDay={day5.dd}
|
||||
title={day5.title}
|
||||
shipped={day5Shipped}
|
||||
publishedAt={day5.publishedAt}
|
||||
/>
|
||||
}
|
||||
disabled={!day5Shipped}
|
||||
className="h-[79px]"
|
||||
id={day5.d.toString()}
|
||||
></Accordion.Item>
|
||||
</div>
|
||||
</Accordion>
|
||||
</SectionContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
const images = {
|
||||
'00-constellation': '/images/launchweek/8/day0/constellation-pre.svg',
|
||||
'00-constellation-mobile': '/images/launchweek/8/day0/constellation.svg',
|
||||
}
|
||||
|
||||
export interface StepProps {
|
||||
title: string
|
||||
badge?: string
|
||||
break_thumb_title?: boolean
|
||||
blog?: string
|
||||
docs?: string
|
||||
description?: string
|
||||
github?: string
|
||||
hackernews?: string
|
||||
isNew?: boolean
|
||||
thumb?: string
|
||||
url?: string
|
||||
video?: string
|
||||
className?: string
|
||||
hideInBlog?: boolean
|
||||
bg_layers?: {
|
||||
lottie?: any
|
||||
img?: string
|
||||
mobileImg?: string
|
||||
}[]
|
||||
steps: StepProps[] | []
|
||||
}
|
||||
export interface WeekDayProps {
|
||||
title: string
|
||||
shipped: boolean
|
||||
date: string
|
||||
publishedAt: string
|
||||
description: string
|
||||
d: number
|
||||
dd: string
|
||||
youtube_id: string
|
||||
blogpost: string
|
||||
docs: string
|
||||
steps: StepProps[] | []
|
||||
}
|
||||
|
||||
export const endOfLW8 = '2023-08-11T23:59:59.999-07:00'
|
||||
|
||||
const days: WeekDayProps[] = [
|
||||
{
|
||||
title: '',
|
||||
shipped: true,
|
||||
date: '04 Aug',
|
||||
publishedAt: '2023-08-04T09:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 0,
|
||||
dd: 'Pre-release',
|
||||
youtube_id: '',
|
||||
blogpost: '',
|
||||
docs: '',
|
||||
steps: [
|
||||
{
|
||||
title:
|
||||
'Coding the stars - an interactive constellation with Three.js and React Three Fiber',
|
||||
blog: '/blog/interactive-constellation-threejs-react-three-fiber',
|
||||
bg_layers: [
|
||||
{ img: images['00-constellation'], mobileImg: images['00-constellation-mobile'] },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
shipped: false,
|
||||
date: '07 Aug',
|
||||
publishedAt: '2023-08-07T09:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 1,
|
||||
dd: 'Mon',
|
||||
youtube_id: '',
|
||||
blogpost: '',
|
||||
docs: '',
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
shipped: false,
|
||||
date: '08 Aug',
|
||||
publishedAt: '2023-08-08T09:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 2,
|
||||
dd: 'Tue',
|
||||
youtube_id: 'cPGxPl1lx4Y',
|
||||
blogpost: '',
|
||||
docs: '',
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
shipped: false,
|
||||
date: '09 Aug',
|
||||
publishedAt: '2023-08-09T09:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 3,
|
||||
dd: 'Wed',
|
||||
youtube_id: '',
|
||||
blogpost: '',
|
||||
docs: '/docs/guides/storage/uploads#resumable-upload',
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
shipped: false,
|
||||
date: '10 Aug',
|
||||
publishedAt: '2023-08-10T09:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 4,
|
||||
dd: 'Thu',
|
||||
youtube_id: '',
|
||||
blogpost: '',
|
||||
docs: '',
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
shipped: false,
|
||||
date: '11 Aug',
|
||||
publishedAt: '2023-08-11T00:00:00.000-07:00',
|
||||
description: '',
|
||||
d: 5,
|
||||
dd: 'Fri',
|
||||
youtube_id: '',
|
||||
blogpost: '',
|
||||
docs: '',
|
||||
steps: [],
|
||||
},
|
||||
]
|
||||
|
||||
export default days
|
||||
@@ -0,0 +1,29 @@
|
||||
.mask {
|
||||
mask-image: radial-gradient(black, transparent);
|
||||
}
|
||||
|
||||
.dark_community {
|
||||
background-image: radial-gradient(closest-side at 50% 50%, #132121, #132121db, #121212);
|
||||
}
|
||||
|
||||
.community {
|
||||
background-image: radial-gradient(closest-side at 50% 50%, #d9eeef, #dbeef0, white);
|
||||
}
|
||||
|
||||
.wrappers > span {
|
||||
left: -200px !important;
|
||||
}
|
||||
|
||||
.community_wrappers > span {
|
||||
left: -400px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.community_wrappers > span > img {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lw7-article-card-gradient {
|
||||
background: linear-gradient(90deg, #1c1c1c -20%, #20113d50 150%);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
.appear {
|
||||
opacity: 0;
|
||||
animation: appear 0.8s cubic-bezier(0.1, 0, 0.175, 1) forwards;
|
||||
}
|
||||
|
||||
.appear-first {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.appear-second {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.appear-third {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.appear-fourth {
|
||||
animation-delay: 1.6s;
|
||||
}
|
||||
|
||||
.appear-fifth {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.appear-sixth {
|
||||
animation-delay: 2.4s;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.hide-on-desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.show-on-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hide-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.show-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-on-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-on-tablet {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.show-on-tablet {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) and (min-width: 768px) {
|
||||
.show-on-tablet {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hide-on-tablet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ export default function TicketActions({
|
||||
ticketGenerationState,
|
||||
setTicketGenerationState,
|
||||
}: Props) {
|
||||
const [imgReady, setImgReady] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [_imgReady, setImgReady] = useState(false)
|
||||
const [_loading, setLoading] = useState(false)
|
||||
const downloadLink = useRef<HTMLAnchorElement>()
|
||||
const link = `${SITE_URL}/tickets/${username}?lw=8${golden ? `&golden=true` : ''}`
|
||||
const permalink = encodeURIComponent(link)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,10 +3,12 @@ import classNames from 'classnames'
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const SectionContainer = ({ children, className }: Props) => (
|
||||
const SectionContainer = ({ children, className, id }: Props) => (
|
||||
<div
|
||||
id={id}
|
||||
className={classNames(
|
||||
`sm:py-18 container relative mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20`,
|
||||
className
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,7 +27,7 @@ const Company = () => {
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={icon} />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={icon} />
|
||||
</svg>
|
||||
<div className="ml-4">
|
||||
<p className="text-base font-medium text-gray-900 dark:text-white">{text}</p>
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,7 @@ const Developers = () => {
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d={icon} />
|
||||
<path stroke-linecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
|
||||
</svg>
|
||||
)}
|
||||
{Svg && <Svg />}
|
||||
|
||||
Loaded 100 of 486 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user