Merge branch 'master' into feat/auth-config-refactor

This commit is contained in:
Jonathan Summers-Muir committed 2022-06-15 22:24:56 +08:00
commit ad8dc06e95
272 files changed
+23330 -20277

No files matched your search

+8
View File
@@ -14,6 +14,8 @@
- [Shared components](#shared-components)
- [Installing packages](#installing-packages)
- [Development](#development)
- [Common Tasks](#common-tasks)
- [Adding redirects](#adding-redirects)
- [Finally](#finally)
- [Community Channels](#community-channels)
@@ -211,6 +213,12 @@ You do not need to install `devDependencies` in each workspace. These can all be
`npm run dev`
## Common Tasks
### Adding Redirects
To add a redirect, simple create a new entry in the [`next.config.js`](https://github.com/supabase/supabase/blob/master/apps/www/next.config.js) file in our main site.
## Finally
After making your changes to the file(s) you'd like to update, it's time to open a pull request. Once you submit your pull request, others from the Supabase team/community will review it with you.
+1
View File
@@ -1032,6 +1032,7 @@ h4.method-list-item-label {
h4.method-list-item-label .method-list-item-label-name {
font-family: var(--ifm-font-family-monospace);
font-size: 0.8rem;
margin-right: 4px;
padding: var(--ifm-code-padding-vertical) 0;
}
h4.method-list-item-label .method-list-item-label-badge {
+1 -1
View File
@@ -7,7 +7,7 @@ export default function AuthProviders() {
<div className="grid gap-4 md:grid-cols-2">
{authProviders.map((x: any) => (
<div key={x.name}>
<Link href={x.href}>
<Link passHref href={x.href}>
<Card>
<div className="flex justify-between gap-10">
<p>{x.name}</p>
@@ -1,24 +0,0 @@
.code-block::before {
content: '';
background: #1e1e1e;
height: 10px;
width: 48px;
display: block;
}
.code-block::after {
content: '';
background: #1e1e1e;
height: 12px;
width: 48px;
display: block;
}
.code-block code .linenumber {
margin-right: 4px;
}
.code-block code::after,
.code-block code::before {
content: none !important;
}
@@ -1,12 +1,12 @@
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
import monokaiCustomTheme from './CodeBlock.utils'
import CodeBlockStyles from './CodeBlock.module.css'
import { Button, IconCopy } from '@supabase/ui'
import { Button, IconCheck, IconCopy } from '@supabase/ui'
import CopyToClipboard from 'react-copy-to-clipboard'
import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'
import py from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'
import { useState } from 'react'
interface Props {
lang: 'js' | 'sql' | 'py'
@@ -18,6 +18,15 @@ interface Props {
}
function CodeBlock(props: Props) {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1000)
}
let lang = props.lang
? props.lang
: props.className
@@ -38,7 +47,7 @@ function CodeBlock(props: Props) {
<SyntaxHighlighter
language={lang}
style={monokaiCustomTheme}
className={'code-block dark:border-dark rounded-lg border border-gray-600'}
className={'code-block border-scale-600 rounded-lg border'}
customStyle={{
padding: 0,
fontSize: large ? 18 : 12,
@@ -51,23 +60,29 @@ function CodeBlock(props: Props) {
paddingTop: '128px',
}}
lineNumberStyle={{
minWidth: '48px',
minWidth: '44px',
background: '#1e1e1e',
paddingLeft: '21px',
paddingLeft: '4px',
paddingRight: '4px',
marginRight: '12px',
color: '#828282',
textAlign: 'center',
fontSize: large ? 14 : 12,
paddingTop: '4px',
paddingBottom: '4px',
}}
>
{props.children}
{props.children?.trimEnd()}
</SyntaxHighlighter>
{!props.hideCopy && props.children ? (
<div className="dark absolute right-2 top-2">
<CopyToClipboard text={props.children}>
<Button type="outline" className="dark:bg-dark-800" icon={<IconCopy />}>
Copy
<Button
type="default"
icon={copied ? <IconCheck /> : <IconCopy />}
onClick={() => handleCopy()}
>
{copied ? 'Copied' : 'Copy'}
</Button>
</CopyToClipboard>
</div>
+37 -21
View File
@@ -1,4 +1,4 @@
import { Typography } from '@supabase/ui'
import Image from 'next/image'
import Link from 'next/link'
import footer from '../data/footer.json'
import { useTheme } from './Providers'
@@ -16,24 +16,35 @@ const Footer = () => {
</h2>
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
<div className="space-y-8 xl:col-span-1">
<Link href="#">
<Link href="/">
<a>
<img
className="w-40"
<Image
src={isDarkMode ? `/supabase-dark.svg` : `/supabase-light.svg`}
alt="Supabase"
width={160}
height={22}
/>
</a>
</Link>
<div className="flex space-x-5">
<a href="https://twitter.com/supabase" className="text-scale-900 hover:text-scale-1100">
<a
href="https://twitter.com/supabase"
target="_blank"
rel="noreferrer"
className="text-scale-900 hover:text-scale-1100"
>
<span className="sr-only">Twitter</span>
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
</svg>
</a>
<a href="https://github.com/supabase" className="text-scale-900 hover:text-scale-1100">
<a
href="https://github.com/supabase"
target="_blank"
rel="noreferrer"
className="text-scale-900 hover:text-scale-1100"
>
<span className="sr-only">GitHub</span>
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
@@ -46,6 +57,8 @@ const Footer = () => {
<a
href="https://discord.supabase.com/"
target="_blank"
rel="noreferrer"
className="text-scale-900 hover:text-scale-1100"
>
<span className="sr-only">Discord</span>
@@ -60,6 +73,8 @@ const Footer = () => {
<a
href="https://youtube.com/c/supabase"
target="_blank"
rel="noreferrer"
className="text-scale-900 hover:text-scale-1100"
>
<span className="sr-only">Youtube</span>
@@ -78,21 +93,22 @@ const Footer = () => {
<ul className="mt-4 space-y-2">
{segment.links.map((link: any, idx: number) => (
<li key={`${segment.title}_link_${idx}`}>
<a
href={link.url}
className={`text-sm ${
link.url
? 'text-scale-1000 dark:text-scale-1000'
: 'text-scale-300 dark:text-scale-300'
} hover:text-scale-900 dark:hover:text-scale-500`}
>
{link.text}
{!link.url && (
<span className="dark:text-dark-300 block text-sm text-gray-300">
Coming soon
</span>
)}
</a>
<Link href={link.url}>
<a
className={`text-sm ${
link.url
? 'text-scale-1000 dark:text-scale-1000'
: 'text-scale-300 dark:text-scale-300'
} hover:text-brand-900 dark:hover:text-brand-1100 transition`}
>
{link.text}
{!link.url && (
<span className="dark:text-dark-300 block text-sm text-gray-300">
Coming soon
</span>
)}
</a>
</Link>
</li>
))}
</ul>
+18 -13
View File
@@ -1,27 +1,32 @@
import Link from 'next/link'
import { ReactElement } from 'react'
export default function LinkCard({
title,
description,
icon,
link,
}: {
title: string
description: string
icon: ReactElement
link: string
}) {
return (
<div className="border-scale-200 hover:border-brand-900 dark:bg-scale-400 m-4 w-full cursor-pointer rounded-md border px-4 shadow-sm hover:shadow-2xl lg:w-2/5">
{description ? (
<>
<h4 className="my-0 p-0">{title}</h4>
<p>{description}</p>
</>
) : (
<div className="flex">
{icon}
<p>{title}</p>
</div>
)}
</div>
<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">
{description ? (
<>
<h4 className="m-0 mb-4 p-0 text-base font-semibold">{title}</h4>
<p className="text-scale-1100 m-0 p-0">{description}</p>
</>
) : (
<div className="flex">
{icon}
<p className="text-scale-1200 m-0 p-0">{title}</p>
</div>
)}
</a>
</Link>
)
}
+4 -2
View File
@@ -2,10 +2,12 @@ import Image from 'next/image'
export default function Sponsor({ imageUrl, handle }: { imageUrl: string; handle: string }) {
return (
<div className="my-4 flex h-14 w-44 flex-row items-center">
<img
<Image
className="mr-2 rounded-full"
src={`https://github.com/${handle}.png`}
className="mr-2 w-10 rounded-full"
alt={handle}
width={32}
height={32}
/>
<h5>{handle}</h5>
</div>
+1 -3
View File
@@ -1,7 +1,5 @@
import { ReactElement } from 'react'
import { Tabs } from '@supabase/ui'
export default function TabPanel(props) {
console.log({ props })
export default function TabPanel(props: { id: string; label: string | undefined; children: any }) {
return (
<Tabs.Panel id={props.id} label={props.label}>
{props.children}
+10 -5
View File
@@ -1,10 +1,15 @@
import { Tabs } from '@supabase/ui'
const TabWrapper = (props) => {
console.log('Tab props: ', props)
const { children, ...otherProps } = props
return <Tabs {...otherProps}>{props.children}</Tabs>
const TabWrapper = (props: any) => {
return (
<div className="dashboard-tabs">
<Tabs size="xlarge" type="underlined">
<Tabs.Panel id={props.id} label={props.label}>
{props.children}
</Tabs.Panel>
</Tabs>
</div>
)
}
export default TabWrapper
+6 -3
View File
@@ -13,9 +13,12 @@ function DarkModeToggle() {
}
return (
<button className="flex w-full justify-between" onClick={() => toggleDarkMode()}>
{isDarkMode ? <span>Dark Mode</span> : <span>Light Mode</span>}
{isDarkMode ? <IconMoon /> : <IconSun />}
<button onClick={() => toggleDarkMode()}>
{isDarkMode ? (
<IconMoon className="text-scale-1100 stroke-2" />
) : (
<IconSun className="text-scale-1100 stroke-2" />
)}
</button>
)
}
+21 -17
View File
@@ -1,4 +1,4 @@
import { ReactElement, useState } from 'react'
import { ReactElement } from 'react'
import Head from 'next/head'
import NavBar from '../nav/NavBar'
import SideBar from '../nav/SideBar'
@@ -12,8 +12,8 @@ const DocsLayout = ({
currentPage,
}: {
meta: { title: string; description: string }
children: string
toc: any
children: ReactElement
toc?: any
menuItems: any
currentPage: string
}) => {
@@ -25,7 +25,7 @@ const DocsLayout = ({
<title>{meta?.title} | Supabase</title>
<meta name="description" content={meta?.description} />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/docs/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={meta?.title} />
<meta property="og:description" content={meta?.description} />
@@ -40,24 +40,28 @@ const DocsLayout = ({
<main>
<NavBar currentPage={currentPage} />
<div className="flex flex-row">
<div className="flex w-full flex-row">
<SideBar menuItems={menuItems} />
<div className="mx-auto grid w-full max-w-7xl grid-cols-12">
<article className="prose dark:prose-dark dark:bg-scale-200 col-span-8 max-w-none p-8">
{children}
</article>
<div className="prose dark:bg-scale-200 col-span-4 max-w-none p-8">
<h5>On this page</h5>
{toc
? toc.json.map((item: any) => {
<div className="docs-width flex w-full grid-cols-12 justify-between p-4 pb-8">
<div className="w-full p-4">
<article className="prose dark:prose-dark dark:bg-scale-200 width-full">
{children}
</article>
</div>
{toc && (
<div className="prose border-scale-400 dark:bg-scale-200 thin-scrollbar table-of-contents-height w-1/4 border-l px-4">
<h5>On this page</h5>
<ul className="list-none pl-2 text-[0.8rem]">
{toc.json.map((item: any, i: number) => {
return (
<li>
<li key={i}>
<a href={`#${item.slug}`}>{item.content}</a>
</li>
)
})
: 'Please note: No TOC on this page'}
</div>
})}
</ul>
</div>
)}
</div>
</div>
<Footer />
+20 -19
View File
@@ -10,8 +10,8 @@ import {
} from '@supabase/ui'
import Image from 'next/image'
import Link from 'next/link'
import styles from '../../styles/Home.module.css'
import { useTheme } from '../Providers'
import ThemeToggle from '../ThemeToggle'
const NavBar = ({ currentPage }: { currentPage: string }) => {
const [mounted, setMounted] = useState(false)
@@ -32,16 +32,14 @@ const NavBar = ({ currentPage }: { currentPage: string }) => {
]
return (
<nav
className={`dark:border-scale-400 dark:bg-scale-200 sticky top-0 z-10 flex h-[72px] justify-between border-b bg-white p-4 backdrop-blur backdrop-filter`}
>
<nav className="dark:border-scale-400 dark:bg-scale-200 sticky top-0 z-10 flex h-[72px] items-center justify-between border-b bg-white p-4 backdrop-blur backdrop-filter">
<div className="flex items-center">
<button className="mr-2 block lg:hidden">
<IconMenu />
<button className="mr-4 block stroke-2 lg:hidden">
<IconMenu className="text-scale-1100" />
</button>
{mounted && (
<Link href="/">
<a>
<a className="flex items-center">
<Image
className="cursor-pointer"
src={isDarkMode ? `/supabase-dark.svg` : `/supabase-light.svg`}
@@ -53,7 +51,7 @@ const NavBar = ({ currentPage }: { currentPage: string }) => {
</Link>
)}
<ul className={`${styles.navLinks} flex`}>
<ul className="ml-8 hidden space-x-8 lg:flex">
{pageLinks.map((p) => (
<li key={`${p.text}-${p.link}`}>
<Link href={p.link}>
@@ -70,17 +68,20 @@ const NavBar = ({ currentPage }: { currentPage: string }) => {
</li>
</ul>
</div>
<div className={`${styles.navRight} flex items-center`}>
<Link href="https://github.com/supabase/supabase">
<a>
<IconGitHub className="text-scale-1100" />
</a>
</Link>
<Link href="https://twitter.com/supabase">
<a>
<IconTwitter className="text-scale-1100" />
</a>
</Link>
<div className="flex items-center space-x-6">
<div className="hidden items-center space-x-6 md:flex">
<Link href="https://github.com/supabase/supabase">
<a target="_blank">
<IconGitHub className="text-scale-1100 stroke-2" />
</a>
</Link>
<Link href="https://twitter.com/supabase">
<a target="_blank">
<IconTwitter className="text-scale-1100 stroke-2" />
</a>
</Link>
<ThemeToggle />
</div>
<Input
placeholder="Search"
icon={<IconSearch />}
+35 -40
View File
@@ -1,60 +1,55 @@
import { Menu } from '@supabase/ui'
import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../../styles/Home.module.css'
import ThemeToggle from '../ThemeToggle'
const SideBar = ({ menuItems }: { menuItems: any }) => {
const { asPath } = useRouter()
return (
<div
className={`${styles.sidebar} dark:bg-scale-200 dark:border-scale-400 relative h-full border-r py-4`}
>
<Menu>
<div className="dark:bg-scale-200 dark:border-scale-400 thin-scrollbar sidebar-width sticky top-0 flex h-screen overflow-y-scroll border-r py-4">
<Menu className="w-full">
<div>
{Object.keys(menuItems).map((key) => {
return (
<div key={key} className="mb-4">
<Menu.Group title={key} />
{menuItems[key].map((item: { link: string; text: string; sections: Array }) => (
<span key={item.link}>
{Array.isArray(item.sections) ? (
<>
<Menu.Group title={item.text} />
{item.sections.map((section) => (
<Link href={section.link} key={`${item.text}-${section.text}`}>
<Menu.Item
active={asPath == section.link}
showActiveBar={asPath === section.link}
>
{section.text}
{menuItems[key].map(
(item: { link: string; text: string; sections: Array<any> }) => (
<span key={item.link}>
{Array.isArray(item.sections) ? (
<>
<Menu.Group title={item.text} />
{item.sections.map((section) => (
<Link href={section.link} key={`${item.text}-${section.text}`}>
<a>
<Menu.Item active={asPath == section.link}>
{section.text}
</Menu.Item>
</a>
</Link>
))}
</>
) : (
<Link href={item.link}>
<a>
<Menu.Item active={asPath === item.link}>
<div
className={
asPath === item.link ? 'text-brand-900' : 'text-scale-1000'
}
>
{item.text}
</div>
</Menu.Item>
</Link>
))}
</>
) : (
<Link href={item.link} passHref>
<a>
<Menu.Item
active={asPath === item.link}
showActiveBar={asPath === item.link}
>
{item.text}
</Menu.Item>
</a>
</Link>
)}
</span>
))}
</a>
</Link>
)}
</span>
)
)}
</div>
)
})}
</div>
<div className="mt-16 w-full">
<Menu.Item>
<ThemeToggle />
</Menu.Item>
</div>
</Menu>
</div>
)
+4 -4
View File
@@ -4,7 +4,7 @@
"links": [
{
"text": "Blog",
"url": "https://supabase.com/blog"
"url": "/blog"
},
{
"text": "Open Source",
@@ -25,7 +25,7 @@
"links": [
{
"text": "Brand Assets",
"url": "https://supabase.com/brand-assets"
"url": "/brand-assets"
},
{
"text": "Docs",
@@ -33,7 +33,7 @@
},
{
"text": "Pricing",
"url": "https://supabase.com/pricing"
"url": "/pricing"
},
{
"text": "Support",
@@ -62,7 +62,7 @@
},
{
"text": "RSS",
"url": "https://supabase.com/rss.xml"
"url": "/rss.xml"
},
{
"text": "Discord",
+1 -1
View File
@@ -54,7 +54,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -47,7 +47,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -65,7 +65,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="Dashboard UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -62,7 +62,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="Dashboard UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
@@ -114,7 +114,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
4. Click "Run".
```
+1 -1
View File
@@ -50,7 +50,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -47,7 +47,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
@@ -194,7 +194,7 @@ const { user, session, error } = await supabase.auth.signIn({
### Sign in with redirect.
Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for
any URL path on your website (the base domain must be the same as the domain in your Auth settings).
any URL path on your website (the URL must either be on the same domain as your Site URL [see Auth>Settings in dashboard], or must match one of the Additional Redirect URLs [also in Auth>Settings]).
<Tabs
defaultActiveId="js"
@@ -194,7 +194,7 @@ const { user, session, error } = await supabase.auth.signIn({
### Sign in with redirect.
Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for
any URL path on your website (the base domain must be the same as the domain in your Auth settings).
any URL path on your website (the URL must either be on the same domain as your Site URL [see Auth>Settings in dashboard], or must match one of the Additional Redirect URLs [also in Auth>Settings]).
<Tabs
defaultActiveId="js"
+8 -8
View File
@@ -32,14 +32,14 @@ function walk(dir: string) {
const list = fs.readdirSync(dir)
list.forEach(function (file) {
file = dir + '/' + file
const stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(walk(file))
} else {
/* Is a file */
results.push(file)
}
let slugs = []
fs.readdirSync(dir).forEach((file) => {
let absolute = join(dir, file)
if (fs.statSync(absolute).isDirectory()) {
fs.readdirSync(absolute).forEach((subFile) => slugs.push(file.concat('/' + subFile)))
}
})
})
return results
}
+2 -2
View File
@@ -1,4 +1,3 @@
import { ReactElement } from 'react'
import { getAllDocs, getDocsBySlug } from '../lib/docs'
import Layout from '../components/layouts/Layout'
import { serialize } from 'next-mdx-remote/serialize'
@@ -17,7 +16,7 @@ export default function Doc({
toc,
}: {
meta: { title: string; description: string }
content: ReactElement
content: any
toc: any
}) {
const { asPath } = useRouter()
@@ -37,6 +36,7 @@ export default function Doc({
}
return (
// @ts-ignore
<Layout meta={meta} toc={toc} menuItems={menuItems[page]} currentPage={page}>
<MDXProvider components={components}>
<MDXRemote {...content} components={components} />
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react'
import type { AppProps } from 'next/app'
import { ThemeProvider } from '../components/Providers'
import '../styles/tailwind.css'
import '../styles/main.css'
function MyApp({ Component, pageProps }: AppProps) {
return (
-15
View File
@@ -124,18 +124,3 @@
flex-direction: column;
}
}
.navbar {
height: 60px;
}
.sidebar {
width: 300px;
}
.navLinks li {
margin-left: 2.5em;
}
.navRight > * {
margin-left: 1em;
margin-right: 1em;
}
+51
View File
@@ -0,0 +1,51 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--sidebar-width: 300px;
}
body {
@apply bg-white dark:bg-[#1c1c1c] !important;
scroll-behavior: smooth;
}
::selection {
background-color: #6ee7b7;
color: #333 !important;
}
a {
text-decoration: none !important;
}
article h1 {
margin-bottom: 3rem !important;
font-size: 3rem !important;
font-weight: 400 !important;
}
.thin-scrollbar {
scrollbar-width: thin;
}
.sidebar-width {
width: var(--sidebar-width);
}
.docs-width {
max-width: calc(100% - var(--sidebar-width));
}
.table-of-contents-height {
max-height: calc(100vh - 3.75rem - 2rem);
overflow-y: auto;
position: sticky;
top: calc(3.75rem + 1rem);
}
.width-full {
width: 100% !important;
max-width: max-content !important;
}
-12
View File
@@ -1,12 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-white dark:bg-[#1c1c1c] !important;
}
::selection {
background-color: #6ee7b7;
color: #333 !important;
}
@@ -0,0 +1,71 @@
---
title: Supabase vs Firebase
description: Supabase is an Open Source Firebase Alternative with a SQL based Database, Auth, and Cloud Functions
author: ant_wilson
image: partner-gallery/partner-showcase-thumb.png
thumb: partner-gallery/partner-showcase-thumb.png
tags:
- comparison
date: '2022-05-26'
toc_depth: 3
---
## What is Firebase?
Now owned by Google, Firebase is a collection of tools aimed at mobile and web developers. At its core is the Firestore database.
Firestore allows you to store “documents”. These are collections of key:value pairs where the value can be another sub-document. Document based storage is perfect for unstructured data, since two documents in a collection do not necessarily need to have the same structure.
Firebase also offers other things that web developers find useful like an auth service for user management, and wrappers for other Google services such as Cloud Functions, and File Storage.
## What is Supabase?
Supabase is an open source firebase alternative, but instead of being built around a document-based datastore, Supabase offers a relational database management system called PostgreSQL. This comes with a few advantages:
- It’s open source, so there is zero lock in.
- You can query it with SQL, a proven and powerful query language.
- It has a long track record of being used at scale.
- It’s the database of choice for transactional workloads (think apps and websites, or other things that require near-instant responses to queries).
- It comes with decades of [useful postgres extensions and plug-ins](https://supabase.com/docs/guides/database/extensions).
At Supabase we’ve always been huge fans of Firebase - so we started adding a few things on top of PostgreSQL in an attempt to reach feature parity, including:
- Auto-generated API - [query your data straight from the client](https://supabase.com/docs/guides/api#rest-api-2).
- Realtime - [changes in your data will be streamed directly to your application](https://supabase.com/docs/reference/dart/subscribe).
- Auth - [a simple to integrate auth system and SQL based rules engine](https://supabase.com/auth).
- Functions - [javascript and typescript functions that deploy out globally](https://supabase.com/edge-functions).
- Storage - [hosting images, videos, and pdfs easily](https://supabase.com/storage).
## How are they similar?
Both Firebase and Supabase are based on the idea of bringing a superior developer experience to databases. With both platforms you can spin up a new project from directly inside the browser without the need to download any extra tools or software to your machine. Both platforms come with a useful dashboard UI for debugging your data in realtime, which is especially useful for fast iterations when in development.
Both Firebase and Supabase have invested heavily in client side libraries so you can communicate with your database directly from the client. Firebase has their [Firebase Javascript SDK](https://github.com/firebase/firebase-js-sdk) and Supabase has [supabase-js an isomorphic client](https://github.com/supabase/supabase-js/) that can be used both on the client also on the server in a node-js environment.
## How are they different?
Firebase and Supabase differ in several ways. The main one being that Firebase is a document store, whereas Supabase is based on PostgreSQL - a relational, SQL-based database management system.
There are some other important differences.
### Open Source
Supabase is open source. Along with the hosted cloud platform, you can also take the Supabase stack and host it inside your own cloud or run it locally on your machine. There is no vendor lock in.
### Pricing
[Firebase charges for reads, writes and deletes](https://firebase.google.com/pricing), which can lead to some unpredictability, especially in the early stages of a project when your application is in heavy development. Supabase [charges based on the amount of data stored](https://supabase.com/pricing), with breathing room for unlimited API requests and an unlimited number of Auth users.
### Performance
We created a benchmarking repo where you can compare the performance of both services in different scenarios. Our most recent results show that [Supabase outperforms Firebase by up to 4x](https://github.com/supabase/benchmarks/issues/8) on number of reads per second, and 3.1x on writes per second.
## How do I migrate from Firebase to Supabase?
Since Firebase is document based, migrating into a relational database requires you to map your data structure across into a SQL schema. Luckily we’ve built a [handy conversion tool to do it for you](https://github.com/supabase-community/firebase-to-supabase/tree/main/firestore).
We also have a tool for [migrating Firebase Auth to Supabase Auth](https://github.com/supabase-community/firebase-to-supabase/tree/main/auth). And one for [migrating Firebase Storage files to Supabase Storage](https://github.com/supabase-community/firebase-to-supabase/tree/main/storage).
These are by far the most complete Firebase to Postgres migration tools available anywhere on the web.
You can [try Supabase for free](https://app.supabase.io). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://supabase.com/contact/enterprise).
@@ -0,0 +1,75 @@
---
title: Supabase vs Heroku Postgres
description: Supabase is an alternative to Heroku Postgres with Enterprise Grade Support available
author: ant_wilson
image: partner-gallery/partner-showcase-thumb.png
thumb: partner-gallery/partner-showcase-thumb.png
tags:
- comparison
date: '2022-05-26'
toc_depth: 3
---
## What is Heroku Postgres?
Heroku is a cloud application platform that offers managed PostgreSQL as a service. They offer 5 levels of Postgres support from the Hobby Tier up to the Shield Tier, each with different levels of features and pricing.
## What is Supabase?
Supabase also offers managed Postgres, the main difference is that with each deployment you also get:
- Auto-generated API - [never write an API again](https://supabase.com/docs/guides/api#rest-api-2)
- Realtime - [subscribe to data changes via websockets](https://supabase.com/docs/reference/dart/subscribe)
- Auth - [users can log in and out of your application](https://supabase.com/auth)
- Functions - [deploy custom logic to the edge](https://supabase.com/edge-functions)
- Storage - [serve large files and folders](https://supabase.com/storage)
- PgBouncer - [connection pooling useful for serverless computing](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool)
## How are they similar?
Heroku Postgres and Supabase both offer:
- [A web UI for managing your instance](https://supabase.com/docs/guides/database#table-view)
- SLAs and Enterprise-grade support packages
- Direct SSL connections to Postgres
- Postgres Extensions (see [Supabase Extensions](https://supabase.com/docs/guides/database/extensions), [Heroku Extensions](https://devcenter.heroku.com/articles/heroku-postgres-extensions-postgis-full-text-search))
- [Backups and PITR](https://supabase.com/blog/2020/08/02/continuous-postgresql-backup-walg)
- [Postgres logs](https://supabase.com/docs/guides/platform/logs)
- Encryption-at-rest
## What are the differences?
### Core Features
These are some of the key differences between Heroku Postgres and Supabase in terms of features:
- Supabase is more than just the raw database, it also comes with:
- [Connection pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool) so that you won’t run out of connections in a serverless environment.
- [Auto-generated APIs](https://supabase.com/docs/guides/api#rest-api-2) based on your schema, so you can communicate with your database directly from the client.
- [Realtime API](https://supabase.com/docs/reference/dart/subscribe) is useful for when you want to subscribe to changes to your database over websockets.
- [Auth API](https://supabase.com/auth) can be used to leverage Postgres’s Row Level Security model, and control access to sensitive data on a per user, or per group level.
- [Functions](https://supabase.com/edge-functions) can be deployed out to the edge directly from the Supabase CLI, which means you can run sensitive business logic or transformations in a serverless fashion.
- You can also deploy edge functions to Heroku using their [Dynos](https://www.heroku.com/dynos) runtime in conjunction with something like [Fastly](https://www.fastly.com/).
- [File Storage](https://supabase.com/storage) is useful for when your app needs to store large files and folders that aren’t suitable for storing within Postgres itself.
- A spreadsheet-like web interface for building your schemas and inspecting data.
### Pricing
The two services price quite differently, the key differences being:
- [Supabase pricing is based around usage](https://supabase.com/pricing), so you only pay for what you use.
- Heroku prices based on a tier model with [37 plans to choose from](https://elements.heroku.com/addons/heroku-postgresql#pricing).
Supabase’s free tier also includes a dedicated Postgres instance, and the best bit is you can upgrade to pro later without any interruptions.
### Global Deployments
You may have strict data regulations that you must comply with, so choosing your region can be very important. Here’s how the deployment options stack up:
- Supabase can be deployed to any one of [12 data centers across the globe](https://github.com/supabase/supabase/discussions/4815#discussioncomment-1915129) (free tier included).
- Since Supabase is fully open source - you can also [self host wherever you like](https://supabase.com/docs/guides/hosting/overview).
- You can deploy Heroku Postgres to two data centers (US and Europe) however [6 more data centers](https://devcenter.heroku.com/articles/regions) are available on the Enterprise plan.
## How to migrate from Heroku Postgres to Supabase
Migrating is surprisingly simple. You just need to use the standard Postgres `pg_dump` and `pg_restore` tools to dump and restore from a backup. We created a handy guide for [migrating from heroku to supabase](https://github.com/supabase-community/heroku-to-supabase).
@@ -5,8 +5,8 @@ author: ant_wilson
image: 2022-march/thumb.jpg
thumb: 2022-march/thumb.jpg
tags:
- launch-week
date: '2022-04-15T11:00:00'
- release-notes
date: '2022-04-15'
toc_depth: 3
---
@@ -0,0 +1,129 @@
---
title: Supabase Beta May 2022
description: Product and community updates including wildcard auth redirects, edge functions with webhooks, and Prometheus endpoints for everybody.
author: ant_wilson
image: 2022-may/thumb.jpg
thumb: 2022-may/thumb.jpg
tags:
- release-notes
date: '2022-06-01'
toc_depth: 3
---
It's been another packed month of shipping like crazy at Supabase, here's the update on what you might have seen appearing in the product and beyond during the month of May...
## Wildcard Auth Redirects
Wildcard redirects are especially useful for Jamstack platforms with preview deployments, like Vercel and Netlify. Now you can add a wildcard domain, like `*.mydomain.com/welcome` and both `x.mydomain.com/welcome` and `y.mydomain.com/welcome` will be be accepted. [Read the Pull Request](https://github.com/supabase/gotrue/pull/334).
[![Wildcard Auth Redirects](/images/blog/2022-may/wildcard.jpg)](https://github.com/supabase/gotrue/pull/334)
## Supabase Edge functions with Webhooks
Now you can deploy functions with optional JWT verification. This makes it easier to trigger Edge Functions with webhooks. Use the -no-verify-jwt flag when deploying your functions via the CLI to enable this mode. You can test this out with our [Edge Functions Telegram Bot example](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/telegram-bot).
[![graphql](/images/blog/2022-may/stripe-webhook.jpg)](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/telegram-bot)
## Metrics for everybody
We are making another enterprise feature available for all of our users. Our Prometheus-compatible metrics endpoint gives you granular insights on the health and status of your projects, allowing for real-time monitoring, debugging, and alerting for all. [Read the docs](https://supabase.com/docs/guides/platform/metrics).
[![realtime](/images/blog/2022-may/prometheus.png)](https://supabase.com/docs/guides/platform/metrics)
## Improved Policy Editor
The Policy editor now includes a “Target roles” field. We now have a strong recommendation to use this over the auth.role() = 'authenticated' technique. In one customer’s database, this reduced a query from 46 seconds to 7 milliseconds. Sometimes even we get it wrong - luckily you can trust that Postgres has it right. [Check it out](https://app.supabase.com/project/_/auth/policies).
[![enterprise](/images/blog/2022-may/policy.jpg)](https://app.supabase.com/project/_/auth/policies)
## State of DB 2022
Basedash conducted a survey that takes the pulse of the current databases that teams, individuals, startups, enterprises and hobbyists are using.
Supabase was listed in two categories, including the first place in satisfaction for hosting providers (84% of users would use us again 🙏). [Full results](https://stateofdb.com/).
[![secret scanning](/images/blog/2022-may/databasesurvey.jpg)](https://stateofdb.com/)
## Supabase Happy Hour #5
For the fifth edition of our Happy Hour streaming, we had a chat with our CEO and co-founder, Paul Copplestone. We went through some of the history of Supabase, our culture, why PostgreSQL was the right call, and hints about the future of the product. [Watch the episode](https://www.youtube.com/watch?v=_k7GqVDSFvw).
[![community day](/images/blog/2022-may/happyhour.jpg)](https://www.youtube.com/watch?v=_k7GqVDSFvw)
## Supabase on Software Engineering Radio
I chatted with SE Radio host Jeremy Jung about all things Supabase: building an API layer with postgREST, authentication using GoTrue, row-level security, forking open source projects, using the write ahead log to implement real time updates, provisioning and monitoring databases, user support, incidents, and open source licenses. [Listen to the podcast](https://www.se-radio.net/2022/05/episode-511-ant-wilson-on-supabase-postgres-as-a-service/).
[![open source](/images/blog/2022-may/se_radio_image.jpg)](https://www.se-radio.net/2022/05/episode-511-ant-wilson-on-supabase-postgres-as-a-service/)
## Supabase is Hiring for Sales Assist
There is an opportunity to work inside the growth team at Supabase. It's still a small team so you'd be getting in at the ground floor. If you love dev tools and are passionate about talking to customers, then you're who we're looking for! [Read the role spec here](https://boards.greenhouse.io/supabase/jobs/4530789004).
[![supabrew](/images/blog/2022-may/hiring.jpg)](https://boards.greenhouse.io/supabase/jobs/4530789004)
## Partner Showcase
### Deepnote
Deepnote is a data notebook that’s built for collaboration — Jupyter compatible, works magically in the cloud, and sharing is as easy as sending a link.
You can connect your Supabase Postgres Database to your Deepnote notebooks to quickly analyze data and share your findings with others. [Check out the guide](https://docs.deepnote.com/integrations/supabase).
[![supabrew](/images/blog/2022-may/graph.jpg)](https://docs.deepnote.com/integrations/supabase)
## Made with Supabase
### Polypane
A browser that help you design, build and test better websites. Who doesn't want that? [Polypane](https://polypane.app/) helps with responsive design, accessibility, SEO, performance, design accuracy, semantic HTML, social media tags... and everything else. Built by: [@kilianvalkhof](https://twitter.com/kilianvalkhof).
[![supabrew](/images/blog/2022-may/polypane.jpg)](https://polypane.app/)
### Pawternity Hub
[Pawternity Hub](https://pawternityhub.netlify.app/) is a Pet Adoption website where people can find local pets around their area to adopt. Started in a hackathon while brainstorming ideas to help the community, it was built with React and Bootstrap and uses the PetFinder API. Built by [@NathanJoSuarez](https://twitter.com/NathanJoSuarez).
[![supabrew](/images/blog/2022-may/pawhub.jpg)](https://pawternityhub.netlify.app/)
Discover other projects: Made with Supabase
## Issue Bounties
- Fix: [crash in aws sdk](https://issuehunt.io/r/supabase/storage-api/issues/141)
## Quick Product Updates
- Fix: [Added reuse interval for token refresh](https://github.com/supabase/gotrue/pull/466)
## Community Highlights
- Morrow is looking for a full Senior Full Stack TypeScript Developer (Supabase Focus): [Job post](https://jobs.themorrow.digital/24826)
- The StackOverflow podcast with Matt and Cassidoo talking about Supabase’s Series B and open source. [Podcast](https://stackoverflow.blog/2022/05/17/open-source-is-winning-over-developers-and-investors-ep-442/)
- Migrate your MySQL database to Postgres with Supabase, Hasura & pgloader. [Blog](https://medium.com/@adi_myth/migrate-your-mysql-database-to-postgres-with-supabase-hasura-pgloader-3ef63bedd38)
- Here’s Why You Should Ditch MongoDB and Switch to Supabase. [Blog](https://javascript.plainenglish.io/heres-why-you-should-ditch-mongodb-and-switch-to-supabase-374b43f4ebc2)
- What is Supabase? [Video](https://www.youtube.com/watch?v=v2W7QJMMIA0)
- Anima converted Supabase’s Social Auth component into a native Figma component, with over 400 variants. [Link](https://unruffled-hoover-de9320.netlify.app/?path=/story/auth-auth--with-coloured-social-auth&args=socialLayout:horizontal)
- How To Build A Nuxt 3 Ionic Capacitor Starter App, Supabase Setup and Authentication. [Blog](https://dev.to/aaronksaunders/how-to-build-a-nuxt-3-ionic-capacitor-starter-app-supabase-setup-and-authentication-3gd0)
- How to create and test a GitHub Action that generates Supabase database types. [Blog](https://dev.to/lyqht/how-to-create-and-test-a-github-action-that-generates-supabase-database-types-1l6b)
## Supabase GitHub
We hit 49K stars!!: [repository.surf/supabase](https://repository.surf/supabase)
[![swag store](/images/blog/2022-may/stars.jpg)](https://repository.surf/supabase)
## Meme Zone
If you made it this far in the blog post you deserve a treat. [Follow us on Twitter](https://twitter.com/supabase) for more.
[![swag store](/images/blog/2022-may/meme.jpg)](https://twitter.com/supabase)
## Get started with Supabase
- Start using Supabase today: **[app.supabase.com](https://app.supabase.com/)**
- Make sure to **[star us on GitHub](https://github.com/supabase/supabase)**
- Follow us **[on Twitter](https://twitter.com/supabase)**
- Subscribe to our **[YouTube channel](https://www.youtube.com/c/supabase)**
- Become a **[sponsor](https://github.com/sponsors/supabase)**
- Join the **[team](https://about.supabase.com/careers)**
+1 -1
View File
@@ -64,7 +64,7 @@ function CodeBlock(props: CodeBlockProps) {
const style = shouldHighlightLine ? {} : { filter: 'grayscale(75%)', opacity: 0.5 }
return {
class: classNames(
className: classNames(
CodeBlockStyles['code-line'],
shouldHighlightLines && shouldHighlightLine && CodeBlockStyles['code-line--flash']
),
@@ -73,8 +73,8 @@ export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient
<Form initialValues={INITIAL_VALUES} validate={validate} onSubmit={handleFormSubmit}>
{({ isSubmitting }: any) => (
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
<div className="col-span-2 h-24">
<div className="flex flex-col space-y-4">
<div>
<Select
id="type"
name="type"
@@ -89,67 +89,73 @@ export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient
</Select>
</div>
<div className="h-24">
<Input
label="First Name *"
id="first"
name="first"
layout="vertical"
placeholder="Jane"
/>
<div className="flex space-x-2 sm:space-x-4">
<div className="flex-1">
<Input
label="First Name *"
id="first"
name="first"
layout="vertical"
placeholder="Jane"
/>
</div>
<div className="flex-1">
<Input
label="Last Name *"
id="last"
name="last"
layout="vertical"
placeholder="Doe"
/>
</div>
</div>
<div className="h-24">
<Input
label="Last Name *"
id="last"
name="last"
layout="vertical"
placeholder="Doe"
/>
<div className="flex space-x-2 sm:space-x-4">
<div className="flex-1">
<Input
label="Company Name"
id="company"
name="company"
layout="vertical"
placeholder="Supa Inc."
/>
</div>
<div className="flex-1">
<InputNumber
label="Company Size"
id="size"
name="size"
layout="vertical"
placeholder="1"
/>
</div>
</div>
<div className="h-24">
<Input
label="Company Name"
id="company"
name="company"
layout="vertical"
placeholder="Supa Inc."
/>
<div className="flex space-x-2 sm:space-x-4">
<div className="flex-1">
<Input
label="Job Title"
id="title"
name="title"
layout="vertical"
placeholder="CEO"
/>
</div>
<div className="flex-1">
<Input
label="Business email *"
id="email"
name="email"
layout="vertical"
placeholder="janedoe@example.sg"
/>
</div>
</div>
<div className="h-24">
<InputNumber
label="Company Size"
id="size"
name="size"
layout="vertical"
placeholder="1"
/>
</div>
<div className="h-24">
<Input
label="Job Title"
id="title"
name="title"
layout="vertical"
placeholder="CEO"
/>
</div>
<div className="h-24">
<Input
label="Business email *"
id="email"
name="email"
layout="vertical"
placeholder="janedoe@example.sg"
/>
</div>
<div className="h-24">
<div>
<Input
label="Phone Number"
id="phone"
@@ -159,7 +165,7 @@ export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient
/>
</div>
<div className="h-24">
<div>
<Select
label="Country / Main Timezone"
id="country"
+1 -1
View File
@@ -164,7 +164,7 @@
"author_url": "https://github.com/geromegrignon",
"author_img": "https://avatars.githubusercontent.com/u/32737308",
"repo_name": "angular-todo-list",
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/angular-todo-list",
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/todo-list/angular-todo-list",
"vercel_deploy_url": "",
"demo_url": ""
}
+10 -1
View File
@@ -1,6 +1,6 @@
type Investor = {
name: string
round: 'SEED' | 'A'
round: 'SEED' | 'A' | 'B'
title: string
img: string
lead: boolean
@@ -36,6 +36,15 @@ const data: Investor[] = [
twitter: null,
linkedIn: null,
},
{
name: 'Felicis',
round: 'B',
title: 'Felicis',
img: '/images/logos/felicis.png',
lead: true,
twitter: null,
linkedIn: null,
},
{
name: 'Tom Preston-Werner',
round: 'A',
+6
View File
@@ -6,6 +6,12 @@ type PressItem = {
}
const data: PressItem[] = [
{
type: 'article',
href: 'https://techcrunch.com/2022/05/10/supabase-raises-80m-series-b-for-its-open-source-firebase-alternative/',
title: 'Supabase raises $80M Series B for its open source Firebase alternative',
publisher: 'TechCrunch',
},
{
type: 'article',
href: 'https://techcrunch.com/2021/09/09/supabase-raises-30m-for-its-open-source-insta-backend',
+5
View File
@@ -15,6 +15,7 @@ async function generate() {
'pages/*/*.tsx',
'data/**/*.mdx',
'_blog/*.mdx',
'_alternatives/*.mdx',
'!pages/index.tsx',
'!data/*.mdx',
'!pages/_*.js',
@@ -33,6 +34,7 @@ async function generate() {
.replace('pages', '')
// add a `/` for blog posts
.replace('_blog', '/blog')
.replace('_alternatives', '/alternatives')
.replace('.tsx', '')
.replace('.mdx', '')
// replace the paths for nested 'index' based routes
@@ -42,6 +44,9 @@ async function generate() {
let route = path === '/index' ? '' : path
if (route === '/alternatives/[slug]') return null
if (route === '/partners/[slug]') return null
//
// blog specific urls
//
+196
View File
@@ -0,0 +1,196 @@
import authors from 'lib/authors.json'
import hydrate from 'next-mdx-remote/hydrate'
import { NextSeo } from 'next-seo'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import { Gfm } from 'remark-gfm'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import { generateReadingTime } from '~/lib/helpers'
interface Props {
components: React.ReactNode
props: any
gfm: Gfm
slug: string
}
const LayoutComparison = ({ components, props, gfm, slug }: Props) => {
// @ts-ignore
const content = hydrate(props.blog.content, { components })
const authorArray = props.blog.author.split(',')
const author = []
for (let i = 0; i < authorArray.length; i++) {
author.push(
// @ts-ignore
authors.find((authors: string) => {
// @ts-ignore
return authors.author_id === authorArray[i]
})
)
}
const { basePath } = useRouter()
const NextCard = (props: any) => {
const { post, label, className } = props
return (
<Link href={`${post.url}`} as={`${post.url}`}>
<div className={className}>
<div className="border-scale-500 hover:bg-scale-100 dark:hover:bg-scale-300 cursor-pointer rounded border p-6 transition">
<div className="space-y-4">
<div>
<p className="text-scale-900 text-sm">{label}</p>
</div>
<div>
<h4 className="text-scale-1200 text-lg">{post.title}</h4>
<p className="small">{post.date}</p>
</div>
</div>
</div>
</div>
</Link>
)
}
return (
<>
<NextSeo
title={props.blog.title}
openGraph={{
title: props.blog.title,
description: props.blog.description,
url: `https://supabase.com/blog/${props.blog.slug}`,
type: 'article',
article: {
//
// to do: add expiration and modified dates
// https://github.com/garmeeh/next-seo#article
publishedTime: props.blog.date,
//
// to do: author urls should be internal in future
// currently we have external links to github profiles
authors: [props.blog.author_url],
tags: props.blog.tags.map((cat: string) => {
return cat
}),
},
images: [
{
url: `https://supabase.com${basePath}/images/blog/${
props.blog.image ? props.blog.image : props.blog.thumb
}`,
},
],
}}
/>
<DefaultLayout>
<article className="mx-auto max-w-5xl px-8 py-16 sm:px-16 xl:px-20">
{/* Title and description */}
<div className="mb-16 max-w-5xl space-y-8">
<div className="space-y-4">
<p className="text-brand-900 text-center">Alternative</p>
<h1 className="h1 text-center">{props.blog.title}</h1>
<div className="text-scale-900 flex justify-center space-x-3 text-sm">
<p>{props.blog.date}</p>
<p>•</p>
<p>{generateReadingTime(props.blog.content.renderedOutput)}</p>
</div>
<div className="flex justify-center gap-3">
{author.map((author: any) => {
return (
<div className="mt-6 mb-8 mr-4 w-max lg:mb-0">
<Link href={author.author_url}>
<a className="cursor-pointer">
<div className="flex items-center gap-3">
{author.author_image_url && (
<div className="w-10">
<Image
src={author.author_image_url}
className="dark:border-dark rounded-full border"
width="100%"
height="100%"
layout="responsive"
/>
</div>
)}
<div className="flex flex-col">
<span className="text-scale-1200 mb-0 text-sm">{author.author}</span>
<span className="text-scale-900 mb-0 text-xs">{author.position}</span>
</div>
</div>
</a>
</Link>
</div>
)
})}
</div>
</div>
</div>
<div className="">
{/* Content */}
<div className="prose prose-docs max-w-none">{content}</div>
<div className="py-16">
<div className="text-scale-900 dark:text-scale-1000 text-sm">Share this article</div>
<div className="mt-4 flex items-center space-x-4">
<Link
passHref
href={`https://twitter.com/share?text=${props.blog.title}&url=https://supabase.com/blog/${props.blog.slug}`}
>
<a target="_blank" className="text-scale-900 hover:text-scale-1200">
<svg
height="26"
width="26"
viewBox="-89 -46.8 644 446.8"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path
d="m154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052a205.304 205.304 0 0 0 50.352-52.29c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.42 202.42 0 0 1 -64.098 24.511c-18.42-19.628-44.644-31.904-73.682-31.904-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504a100.739 100.739 0 0 0 -13.668 50.754c0 35.034 17.82 65.961 44.92 84.055a100.172 100.172 0 0 1 -45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101.036 101.036 0 0 1 -26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224a204.9 204.9 0 0 1 -24.078-1.399c44.674 28.645 97.72 45.359 154.734 45.359"
fillRule="nonzero"
/>
</svg>
</a>
</Link>
<Link
passHref
href={`https://www.linkedin.com/shareArticle?url=https://supabase.com/blog/${props.blog.slug}&title=${props.blog.title}`}
>
<a target="_blank" className="text-scale-900 hover:text-scale-1200">
<svg
width="20"
height="20"
viewBox="0 5 1036 990"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path d="M0 120c0-33.334 11.667-60.834 35-82.5C58.333 15.833 88.667 5 126 5c36.667 0 66.333 10.666 89 32 23.333 22 35 50.666 35 86 0 32-11.333 58.666-34 80-23.333 22-54 33-92 33h-1c-36.667 0-66.333-11-89-33S0 153.333 0 120zm13 875V327h222v668H13zm345 0h222V622c0-23.334 2.667-41.334 8-54 9.333-22.667 23.5-41.834 42.5-57.5 19-15.667 42.833-23.5 71.5-23.5 74.667 0 112 50.333 112 151v357h222V612c0-98.667-23.333-173.5-70-224.5S857.667 311 781 311c-86 0-153 37-201 111v2h-1l1-2v-95H358c1.333 21.333 2 87.666 2 199 0 111.333-.667 267.666-2 469z" />
</svg>
</a>
</Link>
</div>
</div>
<div className="grid gap-8 py-8 lg:grid-cols-1">
<div>
{props.prevPost && <NextCard post={props.prevPost} label="Previous comparison" />}
</div>
<div>
{props.nextPost && (
<NextCard post={props.nextPost} label="Next comparison" className="text-right" />
)}
</div>
</div>
</div>
</article>
<CTABanner />
</DefaultLayout>
</>
)
}
export default LayoutComparison
+7 -1
View File
@@ -7,12 +7,14 @@ import { generateReadingTime } from './helpers'
// based on YYYY-MM-DD format
const FILENAME_SUBSTRING = 11
type Directories = '_blog' | '_case-studies'
type Directories = '_blog' | '_case-studies' | '_alternatives'
export const getSortedPosts = (directory: Directories, limit?: number, tags?: any) => {
//Finding directory named "blog" from the current working directory of Node.
const postDirectory = path.join(process.cwd(), directory)
console.log(postDirectory)
//Reads all the files in the post directory
const fileNames = fs.readdirSync(postDirectory)
@@ -109,8 +111,12 @@ export const getPostdata = async (slug: string, directory: string) => {
//Finding directory named "blog" from the current working directory of Node.
const postDirectory = path.join(process.cwd(), directory)
console.log('postDirectory', postDirectory)
const fullPath = path.join(postDirectory, `${slug}.mdx`)
console.log('fullPath', fullPath)
const postContent = fs.readFileSync(fullPath, 'utf8')
return postContent
+5
View File
@@ -654,6 +654,11 @@ module.exports = withMDX({
source: '/learn-more',
destination: '/?utm_source=event&utm_medium=billboard&utm_campaign=aws-atlanta',
},
{
permanent: true,
source: '/docs/reference/javascript/supabase-client',
destination: '/docs/reference',
},
]
},
})
@@ -0,0 +1,46 @@
/* .toc a {
text-decoration: none !important;
font-weight: 400 !important;
}
.toc > ul > li {
padding-left: 0 !important;
}
.toc li {
@apply text-sm !important;
padding-left: 1rem !important;
}
.toc ul > li:before {
content: '';
position: absolute;
background-color: none;
border-radius: 50%;
width: 0 !important;
height: 0 !important;
top: 0.6875em;
left: 0.25em;
}
.header:hover svg,
.header svg:hover {
display: inline-block;
}
.article h1,
.article h2,
.article h3,
.article h4,
.article h5 {
font-weight: 500 !important;
}
.article {
@apply mb-16;
}
.article > div > pre {
padding: 0;
background: none;
} */
+101
View File
@@ -0,0 +1,101 @@
import matter from 'gray-matter'
import authors from 'lib/authors.json'
import hydrate from 'next-mdx-remote/hydrate'
import renderToString from 'next-mdx-remote/render-to-string'
import React from 'react'
import Avatar from '~/components/Avatar'
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import ImageGrid from '~/components/ImageGrid'
import Quote from '~/components/Quote'
import LayoutComparison from '~/layouts/comparison'
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
// import all components used in blog articles here
// for instance, if you use a button, you must add `Button` in the components object below.
const components = {
CodeBlock,
Quote,
Avatar,
code: (props: any) => {
return <CodeBlock {...props} />
},
ImageGrid,
}
// plugins for next-mdx-remote
const gfm = require('remark-gfm')
const slug = require('rehype-slug')
// table of contents extractor
const toc = require('markdown-toc')
export async function getStaticPaths() {
const paths = getAllPostSlugs('_alternatives')
return {
paths,
fallback: false,
}
}
export async function getStaticProps({ params }: any) {
const filePath = `${params.slug}`
const postContent = await getPostdata(filePath, '_alternatives')
const { data, content } = matter(postContent)
const mdxSource: any = await renderToString(content, {
components,
scope: data,
mdxOptions: {
remarkPlugins: [gfm],
rehypePlugins: [slug],
},
})
const relatedPosts = getSortedPosts('_alternatives', 5, mdxSource.scope.tags)
const allPosts = getSortedPosts('_alternatives')
const currentIndex = allPosts
.map(function (e) {
return e.slug
})
.indexOf(filePath)
const nextPost = allPosts[currentIndex + 1]
const prevPost = allPosts[currentIndex - 1]
return {
props: {
prevPost: currentIndex === 0 ? null : prevPost ? prevPost : null,
nextPost: currentIndex === allPosts.length ? null : nextPost ? nextPost : null,
relatedPosts,
blog: {
slug: `${params.year}/${params.month}/${params.day}/${params.slug}`,
content: mdxSource,
...data,
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
},
},
}
}
function BlogPostPage(props: any) {
// @ts-ignore
const content = hydrate(props.blog.content, { components })
const authorArray = props.blog.author.split(',')
const author = []
for (let i = 0; i < authorArray.length; i++) {
author.push(
// @ts-ignore
authors.find((authors: string) => {
// @ts-ignore
return authors.author_id === authorArray[i]
})
)
}
return <LayoutComparison components={components} props={props} gfm={gfm} slug={slug} />
}
export default BlogPostPage
+3 -3
View File
@@ -247,7 +247,7 @@ const Investors = () => {
<>
<p>
<p className="text-lg">
We've raised over $36 million in funding, backed by some of the world's leading
We've raised over $116 million in funding, backed by some of the world's leading
investors.
</p>
</p>
@@ -256,7 +256,7 @@ const Investors = () => {
/>
</div>
<div className="mx-auto mt-5 mb-16 grid max-w-lg gap-0.5 lg:max-w-none lg:grid-cols-3">
<div className="mx-auto mt-5 mb-16 grid max-w-lg gap-0.5 lg:max-w-none lg:grid-cols-4">
{InvestorData.filter((x) => x.lead === true).map((x) => (
<div key={x.name}>
<div
@@ -311,7 +311,7 @@ const Press = () => {
<div>
<SectionHeader title={'Press'} />
</div>
<div className="mx-auto mt-5 grid gap-5 lg:max-w-none lg:grid-cols-2">
<div className="mx-auto mt-5 grid gap-5 lg:max-w-none lg:grid-cols-3">
{PressData.filter((x) => x.type == 'article').map((x) => (
<a href={x.href} key={x.href} target="_blank">
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 bg-white">
+1 -1
View File
@@ -53,7 +53,7 @@ function Database() {
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/auth`,
url: `https://supabase.com/database`,
images: [
{
url: `https://supabase.com${basePath}/images/product/database/database-og.jpg`,
View File
Whitespace-only changes.
+40
View File
@@ -0,0 +1,40 @@
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
// NOTE: this is a placeholder for our future Next.js marketing page. For now it redirects to our Next.js guide.
function Nextjs() {
// base path for images
const router = useRouter()
const { basePath } = router
useEffect(() => {
router.push('/docs/guides/with-nextjs')
}, [])
const meta_title = 'Next.js Database with Auth, Realtime, Fiel Storage, and more.'
const meta_description =
'Supabase is the fastest way to build performant Next.js apps with Authentication & User Management, Realtime, File Storage on top of PostgreSQL Databases.'
return (
<>
<NextSeo
title={meta_title}
description={meta_description}
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/nextjs`,
images: [
{
url: `https://supabase.com${basePath}/images/product/database/database-og.jpg`,
},
],
}}
/>
</>
)
}
export default Nextjs
+3
View File
@@ -0,0 +1,3 @@
import Nextjs from './Nextjs'
export default Nextjs
+3 -1
View File
@@ -653,7 +653,9 @@ export default function IndexPage() {
{pricingFaq.map((faq, i) => {
return (
<Accordion.Item key={i} header={faq.question} id={`faq--${i.toString()}`}>
<ReactMarkdown className="text-scale-1100">{faq.answer}</ReactMarkdown>
<ReactMarkdown className="text-scale-1100 prose max-w-full">
{faq.answer}
</ReactMarkdown>
</Accordion.Item>
)
})}
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

+9 -1
View File
@@ -5,9 +5,17 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 25 May 2022 16:00:00 GMT</lastBuildDate>
<lastBuildDate>Tue, 31 May 2022 16:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/2022/06/07/beta-update-may-2022</guid>
<title>Supabase Beta May 2022</title>
<link>https://supabase.com/blog/2022/06/07/beta-update-may-2022</link>
<description>Product and community updates including wildcard auth redirects, edge functions with webhooks, and Prometheus endpoints for everybody.</description>
<pubDate>Tue, 31 May 2022 16:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/2022/05/26/how-supabase-accelerates-development-of-all-pull-together</guid>
<title>How Mike Lyndon is using Supabase to accelerate development of AllPullTogether</title>
+20 -6
View File
@@ -66,12 +66,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/[slug]</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/storage</loc>
<changefreq>weekly</changefreq>
@@ -649,4 +643,24 @@
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc
>https://supabase.com/blog/2022/05/26/how-supabase-accelerates-development-of-all-pull-together</loc
>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/alternatives/supabase-vs-firebase</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/alternatives/supabase-vs-heroku-postgres</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
</urlset>
@@ -1,12 +1,9 @@
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
const FUNCTION_SECRET = Deno.env.get('FUNCTION_SECRET')
const HUBSPOT_API_KEY = Deno.env.get('HUBSPOT_API_KEY')
const OWNER_ID = Deno.env.get('OWNER_ID')
const ENTERPRISE_PIPELINE = Deno.env.get('ENTERPRISE_PIPELINE')
const PARTNERS_PIPELINE = Deno.env.get('PARTNERS_PIPELINE')
const ENTERPRISE_DEAL_STAGE = Deno.env.get('ENTERPRISE_DEAL_STAGE')
const PARTNERS_DEAL_STAGE = Deno.env.get('PARTNERS_DEAL_STAGE')
const HUBSPOT_PORTAL = Deno.env.get('HUBSPOT_PORTAL')
const PARTNER_FORM_ID = Deno.env.get('PARTNER_FORM_ID')
const ENTERPRISE_FORM_ID = Deno.env.get('ENTERPRISE_FORM_ID')
serve(async (req) => {
const requestSecret = req.headers.get('x-function-secret')
@@ -17,14 +14,51 @@ serve(async (req) => {
const input = await req.json()
const firstName = input.record.contact_first_name || input.record.first
const lastName = input.record.contact_last_name || input.record.last
const supabase_table_id = input.record.id
const country = input.record.country
const form_note = input.record.details
const company = input.record.company_name || input.record.company
const company_size = input.record.company_size || input.record.size
const email = input.record.contact_email || input.record.email
const phone = input.record.contact_phone || input.record.phone
const lastName = input.record.contact_last_name || input.record.last
const firstName = input.record.contact_first_name || input.record.first
const jobtitle = input.record.title
const website = input.record.website
const partner_gallery_type = input.record.type
const contactResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts?hapikey=${HUBSPOT_API_KEY}`,
let formData: object = {
supabase_table_id,
country,
form_note,
company,
company_size,
email,
phone,
lastName,
firstName,
}
if (input.table === 'partner_contacts') {
formData = {
...formData,
jobtitle,
website,
partner_gallery_type,
}
}
const fields: { objectTypeId: '0-1'; name: string; value: string }[] = []
for (const [key, value] of Object.entries(formData)) {
fields.push({ objectTypeId: '0-1', name: key, value: value })
}
const formId = input.table === 'partner_contacts' ? PARTNER_FORM_ID : ENTERPRISE_FORM_ID
// Using HubSpot's submit data to a form API
// https://legacydocs.hubspot.com/docs/methods/forms/submit_form
const formResponse = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL}/${formId}`,
{
method: 'POST',
headers: {
@@ -32,143 +66,18 @@ serve(async (req) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
company,
email,
firstname: firstName,
lastname: lastName,
phone,
},
fields,
}),
}
)
const body = await contactResponse.json()
// Will create a deal using HubSpot's workflow tool instead of via API
let contactId: string | null = null
if (contactResponse.ok) {
contactId = body.id
} else {
if (body.status === 'error' && body.category === 'CONFLICT') {
// fetch existing contact
const contactAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts/${email}?idProperty=email&hapikey=${HUBSPOT_API_KEY}`
)
const body = await contactAssociateResponse.json()
if (contactAssociateResponse.ok) {
contactId = body.id
} else {
console.log(`Failed to fetch contact`, body)
return new Response(`Failed to fetch contact`, { status: 500 })
}
} else {
console.log(`Failed to create contact`, body)
return new Response(`Failed to create contact`, { status: 500 })
}
}
const noteContent = Object.entries(input.record).reduce(
(acc, [key, value]) => acc + `${key}: ${value || 'NOT_PROVIDED'}<br />`,
''
)
// Add note
const noteResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/notes?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
hs_timestamp: Date.now(),
hs_note_body: noteContent,
hubspot_owner_id: OWNER_ID,
},
}),
}
)
const noteBody = await noteResponse.json()
if (!noteResponse.ok) {
console.log(`Failed to add note`, noteBody)
return new Response(`Failed to add note`, { status: 500 })
}
const noteId = noteBody.id
// Associate note with contact
const noteAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/notes/${noteId}/associations/contacts/${contactId}/note_to_contact?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
)
if (!noteAssociateResponse.ok) {
console.log(`Failed to associate note`, await noteAssociateResponse.json())
return new Response(`Failed to associate note`, { status: 500 })
}
// Create deal
const dealCloseDate = new Date()
dealCloseDate.setDate(dealCloseDate.getDate() + 28) // 28 days
const dealResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/deals?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
closedate: dealCloseDate.toISOString(),
dealname: company || `${firstName} ${lastName}`,
dealstage:
input.table === 'enterprise_contacts' ? ENTERPRISE_DEAL_STAGE : PARTNERS_DEAL_STAGE,
hubspot_owner_id: OWNER_ID,
pipeline: input.table === 'enterprise_contacts' ? ENTERPRISE_PIPELINE : PARTNERS_PIPELINE,
},
}),
}
)
const dealBody = await dealResponse.json()
if (!dealResponse.ok) {
console.log(`Failed to create deal`, dealBody)
return new Response(`Failed to create deal: ` + dealBody.message, { status: 500 })
}
const dealId = dealBody.id
// Associate contact with a deal
const dealAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts/${contactId}/associations/deals/${dealId}/contact_to_deal?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
)
if (!dealAssociateResponse.ok) {
console.log(`Failed to associate contact with deal`, await dealAssociateResponse.json())
return new Response(`Failed to associate contact with deal`, { status: 500 })
if (!formResponse.ok) {
console.log(`Failed to submit form data to HubSpot`, await formResponse.json())
return new Response(`Failed to submit form data to HubSpot`, {
status: 500,
})
}
return new Response(JSON.stringify({ done: true }), {
+1
View File
@@ -11,6 +11,7 @@ The function examples are located in [`./supabase/functions`](./supabase/functio
- [`browser-with-cors`](./supabase/functions/browser-with-cors/index.ts): Handle CORS headers for function invocations from browser environments.
- [`select-from-table-with-auth-rls`](./supabase/functions/select-from-table-with-auth-rls/index.ts): Retrieve data from an authenticated user via RLS.
- [`send-email-smtp`](./supabase/functions/send-email-smtp/index.ts): Send an email using SMTP credentials.
- [`stripe-webhooks`](./supabase/functions/stripe-webhooks/index.ts): Handle Stripe Webhooks.
- [`telegram-bot`](./supabase/functions/telegram-bot/index.ts): Webhook handler for Telegram bots using [grammY](https://grammy.dev/).
## Develop locally
@@ -7,4 +7,8 @@ SMTP_FROM="no-reply@example.com"
# telegram-bot
TELEGRAM_BOT_TOKEN="get it from https://t.me/BotFather"
FUNCTION_SECRET="random secret"
FUNCTION_SECRET="random secret"
# stripe-webhooks
STRIPE_API_KEY="<YOUR API KEY HERE>"
STRIPE_WEBHOOK_SIGNING_SECRET="<YOUR WEBHOOK SIGNING HERE>"
@@ -0,0 +1,21 @@
# stripe-webhooks
Also check out our full Stripe Payments examples for [React Native (Expo)](https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions) and [Flutter](https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions).
## Setup env vars
- `cp supabase/.env.local.example supabase/.env.local`
## Test locally
- Terminal 1:
- `supabase functions serve --no-verify-jwt stripe-webhooks --env-file ./supabase/.env.local`
- Terminal 2:
- `stripe listen --forward-to localhost:54321/functions/v1/`
- Terminal 3 (optional):
- `stripe trigger payment_intent.succeeded`
## Deploy
- `supabase functions deploy --no-verify-jwt stripe-webhooks`
- `supabase secrets set --env-file ./supabase/.env.local`
@@ -0,0 +1,60 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
// esm.sh is used to compile stripe-node to be compatible with ES modules.
import Stripe from 'https://esm.sh/stripe@9.6.0?target=deno&no-check'
const stripe = Stripe(Deno.env.get('STRIPE_API_KEY'), {
// This is needed to use the Fetch API rather than relying on the Node http
// package.
httpClient: Stripe.createFetchHttpClient(),
})
// This is needed in order to use the Web Crypto API in Deno.
const cryptoProvider = Stripe.createSubtleCryptoProvider()
console.log(`Function "stripe-webhooks" up and running!`)
serve(async (request) => {
const signature = request.headers.get('Stripe-Signature')
// First step is to verify the event. The .text() method must be used as the
// verification relies on the raw request body rather than the parsed JSON.
const body = await request.text()
let receivedEvent
try {
receivedEvent = await stripe.webhooks.constructEventAsync(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET'),
undefined,
cryptoProvider
)
} catch (err) {
return new Response(err.message, { status: 400 })
}
console.log(`🔔 Event received: ${receivedEvent.id}`)
// Secondly, we use this event to query the Stripe API in order to avoid
// handling any forged event. If available, we use the idempotency key.
const requestOptions =
receivedEvent.request && receivedEvent.request.idempotency_key
? {
idempotencyKey: receivedEvent.request.idempotency_key,
}
: {}
let retrievedEvent
try {
retrievedEvent = await stripe.events.retrieve(receivedEvent.id, requestOptions)
} catch (err) {
return new Response(err.message, { status: 400 })
}
return new Response(JSON.stringify({ id: retrievedEvent.id, status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
})
@@ -9,12 +9,14 @@ export default function IndexPage() {
<div className="w-full h-full bg-gray-300">
{!user ? (
<div className="w-full h-full flex justify-center items-center p-4">
<Auth
supabaseClient={supabase}
providers={['google', 'github']}
socialLayout="horizontal"
socialButtonSize="xlarge"
/>
<div>
<Auth
supabaseClient={supabase}
providers={['google', 'github']}
socialLayout="horizontal"
socialButtonSize="xlarge"
/>
</div>
</div>
) : (
<div
+6 -6
View File
@@ -4214,9 +4214,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -17284,9 +17284,9 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
@@ -4495,9 +4495,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -17949,9 +17949,9 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
+1
View File
@@ -18,6 +18,7 @@ module.exports = ui({
'../../packages/common/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'./components/**/*.tsx',
'./layouts/**/*.tsx',
'./pages/**/*.tsx',
'./_blog/*.mdx',
// purge styles from supabase ui theme
@@ -1,15 +1,16 @@
import React, { FC, useState } from 'react';
import { Button, Input, IconChevronDown, IconSearch } from '@supabase/ui';
import { DropdownControl } from '../../common';
import { FilterOperatorOptions } from '../../header/filter';
import { Filter, FilterOperator } from '../../../types';
import AwesomeDebouncePromise from 'awesome-debounce-promise';
import React, { FC, useState } from 'react'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { Button, Input, IconChevronDown, IconSearch } from '@supabase/ui'
import { uuidv4 } from 'lib/helpers'
import { DropdownControl } from '../../common'
import { FilterOperatorOptions } from '../../header/filter'
import { Filter, FilterOperator } from '../../../types'
interface FilterProps {
defaultColumnName?: string;
defaultValue?: string;
foreignColumnNames: string[];
onChange: (value: Filter) => void;
defaultColumnName?: string
defaultValue?: string
foreignColumnNames: string[]
onChange: (value: Filter) => void
}
export const FilterHeader: FC<FilterProps> = ({
@@ -24,46 +25,43 @@ export const FilterHeader: FC<FilterProps> = ({
: foreignColumnNames.length > 0
? foreignColumnNames[0]
: ''
);
const [condition, setCondition] = useState(FilterOperatorOptions[0].value);
const [filterText, setFilterText] = useState(defaultValue ?? '');
)
const [condition, setCondition] = useState(FilterOperatorOptions[0].value)
const [filterText, setFilterText] = useState(defaultValue ?? '')
const columnOptions =
foreignColumnNames.map((x) => {
return { value: x, label: x };
}) || [];
return { value: x, label: x }
}) || []
function triggerOnChange(
columnName: string,
condition: string,
filterText: string
) {
function triggerOnChange(columnName: string, condition: string, filterText: string) {
onFilterChangeDebounced(
{
id: uuidv4(),
column: columnName,
operator: condition as FilterOperator,
value: filterText,
},
onChange
);
)
}
function onColumnChange(value: string | number) {
const str = value + '';
setColumnName(str);
triggerOnChange(str, condition, filterText);
const str = value + ''
setColumnName(str)
triggerOnChange(str, condition, filterText)
}
function onConditionChange(value: string | number) {
const str = value + '';
setCondition(str);
triggerOnChange(columnName, str, filterText);
const str = value + ''
setCondition(str)
triggerOnChange(columnName, str, filterText)
}
function onFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
setFilterText(value);
triggerOnChange(columnName, condition, value);
const value = event.target.value
setFilterText(value)
triggerOnChange(columnName, condition, value)
}
return (
@@ -76,12 +74,8 @@ export const FilterHeader: FC<FilterProps> = ({
>
<Button as="span" type="outline" iconRight={<IconChevronDown />}>
<span className="foreign-table-modal__filter__trigger-content">
<span className="foreign-table-modal__filter__trigger-content__label">
Column
</span>
<span className="foreign-table-modal__filter__trigger-content__name">
{columnName}
</span>
<span className="foreign-table-modal__filter__trigger-content__label">Column</span>
<span className="foreign-table-modal__filter__trigger-content__name">{columnName}</span>
</span>
</Button>
</DropdownControl>
@@ -93,12 +87,8 @@ export const FilterHeader: FC<FilterProps> = ({
>
<Button as="span" type="outline" iconRight={<IconChevronDown />}>
<span className="foreign-table-modal__filter__trigger-content">
<span className="foreign-table-modal__filter__trigger-content__label">
Filter
</span>
<span className="foreign-table-modal__filter__trigger-content__name">
{condition}
</span>
<span className="foreign-table-modal__filter__trigger-content__label">Filter</span>
<span className="foreign-table-modal__filter__trigger-content__name">{condition}</span>
</span>
</Button>
</DropdownControl>
@@ -111,12 +101,12 @@ export const FilterHeader: FC<FilterProps> = ({
icon={<IconSearch size="small" />}
/>
</div>
);
};
)
}
const onFilterChangeDebounced = AwesomeDebouncePromise(
(filter: Filter, onChange: (value: Filter) => void) => {
onChange(filter);
onChange(filter)
},
500
);
)
@@ -1,5 +1,6 @@
import * as React from 'react'
import { Button, Modal, Menu, IconExternalLink, IconTrash } from '@supabase/ui'
import { uuidv4 } from 'lib/helpers'
import { useTrackedState } from '../../../store'
import { Dictionary, Filter } from '../../../types'
import { RowItem } from './RowItem'
@@ -30,6 +31,7 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
if (defaultValue && columnDefinition) {
fetchData({
id: uuidv4(),
column: columnDefinition.targetColumnName!,
operator: '=',
value: defaultValue,
@@ -20,7 +20,7 @@ const Header: React.FC<HeaderProps> = ({ onAddColumn, onAddRow, headerActions })
const { selectedRows } = state
return (
<div className="bg-scale-100 dark:bg-scale-300 flex h-10 justify-between px-5">
<div className="bg-scale-100 dark:bg-scale-300 flex h-10 items-center justify-between px-5">
{selectedRows.size > 0 ? (
<RowHeader />
) : (
@@ -126,47 +126,39 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
}
return (
<>
<Button
type="text"
style={{ padding: '4px' }}
icon={<IconX size="tiny" strokeWidth={2} />}
onClick={deselectRows}
/>
<div>
<Typography.Text small type="secondary" className="row_header__selected-rows">
<div className="flex items-center gap-6">
<div className="flex items-center gap-3">
<Button
type="default"
style={{ padding: '3px' }}
icon={<IconX size="tiny" strokeWidth={2} />}
onClick={deselectRows}
/>
<span className="text-scale-1200 text-xs">
{selectedRows.size > 1
? `${selectedRows.size} rows selected`
: `${selectedRows.size} row selected`}
</Typography.Text>
</span>
</div>
<Button
type="primary"
size="tiny"
// style={{ padding: '4px 8px' }}
icon={<IconDownload />}
onClick={onRowsExportCsv}
>
Export to csv
</Button>
{editable && (
<>
<Divider type="vertical" className="sb-grid-header__inner__divider" light />
<Button
type="default"
size="tiny"
// style={{ padding: '4px 8px' }}
icon={<IconTrash size="tiny" />}
onClick={onRowsDelete}
>
{selectedRows.size > 1
? `Delete ${selectedRows.size} rows`
: `Delete ${selectedRows.size} row`}
</Button>
</>
)}
</>
<div className="flex items-center gap-2">
<Button type="primary" size="tiny" icon={<IconDownload />} onClick={onRowsExportCsv}>
Export to csv
</Button>
{editable && (
<>
<Button
type="default"
size="tiny"
icon={<IconTrash size="tiny" />}
onClick={onRowsDelete}
>
{selectedRows.size > 1
? `Delete ${selectedRows.size} rows`
: `Delete ${selectedRows.size} row`}
</Button>
</>
)}
</div>
</div>
)
}
@@ -1,5 +1,6 @@
import React, { FC } from 'react'
import { FC } from 'react'
import { Button, IconPlus, IconFilter, Popover } from '@supabase/ui'
import { uuidv4 } from 'lib/helpers'
import { useDispatch, useTrackedState } from '../../../store'
import FilterRow from './FilterRow'
@@ -20,7 +21,6 @@ const FilterPopover: FC = () => {
<IconFilter strokeWidth={1.5} />
</div>
}
// style={{ padding: '4px 8px' }}
>
{btnText}
</Button>
@@ -37,6 +37,7 @@ const Filter: FC = () => {
dispatch({
type: 'ADD_FILTER',
payload: {
id: uuidv4(),
column: state.table?.columns[0].name,
operator: '=',
value: '',
@@ -47,8 +48,8 @@ const Filter: FC = () => {
return (
<div className="space-y-2 py-2">
<div className="space-y-2">
{state.filters.map((_, index) => (
<FilterRow key={`filter-${index}`} filterIdx={index} now={Date.now()} />
{state.filters.map((filter, index) => (
<FilterRow key={`filter-${filter.id}`} filterIdx={index} now={Date.now()} />
))}
{state.filters.length == 0 && (
<div className="space-y-1 px-3">
@@ -1,75 +1,71 @@
import * as React from 'react';
import { Button, Input, IconChevronDown, IconX } from '@supabase/ui';
import { DropdownControl } from '../../common';
import { useDispatch, useTrackedState } from '../../../store';
import { FilterOperatorOptions } from './Filter.constants';
import { updateFilterValueDebounced } from './Filter.utils';
import { FC, memo, useState, useEffect, ChangeEvent } from 'react'
import { Button, Input, IconChevronDown, IconX } from '@supabase/ui'
import { DropdownControl } from '../../common'
import { useDispatch, useTrackedState } from '../../../store'
import { FilterOperatorOptions } from './Filter.constants'
import { updateFilterValueDebounced } from './Filter.utils'
/**
* use `now` to trigger re-render as filterIdx won't change value
* if not filterText state will not updated on delete filter
*/
type FilterRowProps = {
filterIdx: number;
now: number;
};
filterIdx: number
now: number
}
const FilterRow: React.FC<FilterRowProps> = ({ filterIdx, now }) => {
const state = useTrackedState();
const dispatch = useDispatch();
const filter = state.filters[filterIdx];
const column = state.table?.columns.find((x) => x.name === filter.column);
const FilterRow: FC<FilterRowProps> = ({ filterIdx }) => {
const state = useTrackedState()
const dispatch = useDispatch()
const filter = state.filters[filterIdx]
const column = state.table?.columns.find((x) => x.name === filter.column)
const columnOptions =
state.table?.columns?.map((x) => {
return { value: x.name, label: x.name, postLabel: x.dataType };
}) || [];
const [filterValue, setFilterValue] = React.useState(filter.value);
return { value: x.name, label: x.name, postLabel: x.dataType }
}) || []
const [filterValue, setFilterValue] = useState(filter.value)
React.useEffect(() => {
const filter = state.filters[filterIdx];
setFilterValue(filter.value);
}, [filterIdx, now]);
useEffect(() => {
const filter = state.filters[filterIdx]
setFilterValue(filter.value)
}, [filterIdx])
function onColumnChange(column: string | number) {
dispatch({
type: 'UPDATE_FILTER',
payload: { filterIdx, value: { ...filter, column } },
});
})
}
function onOperatorChange(operator: string | number) {
dispatch({
type: 'UPDATE_FILTER',
payload: { filterIdx, value: { ...filter, operator } },
});
})
}
function onFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
setFilterValue(value);
function onFilterChange(event: ChangeEvent<HTMLInputElement>) {
const value = event.target.value
setFilterValue(value)
updateFilterValueDebounced(
{
filterIdx,
value: { ...filter, value: value },
},
dispatch
);
)
}
function onRemoveFilter() {
dispatch({
type: 'REMOVE_FILTER',
payload: { index: filterIdx },
});
})
}
return (
<div className="sb-grid-filter-row px-3">
<DropdownControl
align="start"
options={columnOptions}
onSelect={onColumnChange}
>
<DropdownControl align="start" options={columnOptions} onSelect={onColumnChange}>
<Button
as="span"
type="outline"
@@ -83,11 +79,7 @@ const FilterRow: React.FC<FilterRowProps> = ({ filterIdx, now }) => {
{column?.name || ''}
</Button>
</DropdownControl>
<DropdownControl
align="start"
options={FilterOperatorOptions}
onSelect={onOperatorChange}
>
<DropdownControl align="start" options={FilterOperatorOptions} onSelect={onOperatorChange}>
<Button
as="span"
type="outline"
@@ -114,6 +106,6 @@ const FilterRow: React.FC<FilterRowProps> = ({ filterIdx, now }) => {
onClick={onRemoveFilter}
/>
</div>
);
};
export default React.memo(FilterRow);
)
}
export default memo(FilterRow)
+18 -25
View File
@@ -1,23 +1,16 @@
import { Dictionary, Filter, FilterOperator, QueryTable, Sort } from '../types';
import { IQueryModifier, QueryModifier } from './QueryModifier';
import { uuidv4 } from 'lib/helpers'
import { Dictionary, Filter, FilterOperator, QueryTable, Sort } from '../types'
import { IQueryModifier, QueryModifier } from './QueryModifier'
export interface IQueryFilter {
filter: (
column: string,
operator: FilterOperator,
value: string
) => IQueryFilter;
match: (criteria: Dictionary<any>) => IQueryFilter;
order: (
column: string,
ascending?: boolean,
nullsFirst?: boolean
) => IQueryFilter;
filter: (column: string, operator: FilterOperator, value: string) => IQueryFilter
match: (criteria: Dictionary<any>) => IQueryFilter
order: (column: string, ascending?: boolean, nullsFirst?: boolean) => IQueryFilter
}
export class QueryFilter implements IQueryFilter, IQueryModifier {
protected filters: Filter[] = [];
protected sorts: Sort[] = [];
protected filters: Filter[] = []
protected sorts: Sort[] = []
constructor(
protected table: QueryTable,
@@ -27,15 +20,15 @@ export class QueryFilter implements IQueryFilter, IQueryModifier {
) {}
filter(column: string, operator: FilterOperator, value: any) {
this.filters.push({ column, operator, value });
return this;
this.filters.push({ id: uuidv4(), column, operator, value })
return this
}
match(criteria: Dictionary<any>) {
Object.entries(criteria).map(([column, value]) => {
this.filters.push({ column, operator: '=', value });
});
return this;
this.filters.push({ id: uuidv4(), column, operator: '=', value })
})
return this
}
order(column: string, ascending = true, nullsFirst = false) {
@@ -43,16 +36,16 @@ export class QueryFilter implements IQueryFilter, IQueryModifier {
column: column,
ascending,
nullsFirst,
});
return this;
})
return this
}
range(from: number, to: number) {
return this._getQueryModifier().range(from, to);
return this._getQueryModifier().range(from, to)
}
toSql() {
return this._getQueryModifier().toSql();
return this._getQueryModifier().toSql()
}
_getQueryModifier() {
@@ -61,6 +54,6 @@ export class QueryFilter implements IQueryFilter, IQueryModifier {
actionOptions: this.actionOptions,
filters: this.filters,
sorts: this.sorts,
});
})
}
}
+53 -63
View File
@@ -1,116 +1,106 @@
import update from 'immutability-helper';
import { TOTAL_ROWS_RESET } from '../../constants';
import { Filter } from '../../types';
import { getInitialFilters } from '../../utils';
import { INIT_ACTIONTYPE } from './base';
import update from 'immutability-helper'
import { TOTAL_ROWS_RESET } from '../../constants'
import { Filter } from '../../types'
import { getInitialFilters } from '../../utils'
import { INIT_ACTIONTYPE } from './base'
export interface FilterInitialState {
filters: Filter[];
filters: Filter[]
}
export const filterInitialState: FilterInitialState = { filters: [] };
export const filterInitialState: FilterInitialState = { filters: [] }
type FILTER_ACTIONTYPE =
| INIT_ACTIONTYPE
| {
type: 'SET_FILTERS';
payload: Filter[];
type: 'SET_FILTERS'
payload: Filter[]
}
| {
type: 'ADD_FILTER';
payload: Filter;
type: 'ADD_FILTER'
payload: Filter
}
| {
type: 'REMOVE_FILTER';
payload: { index: number };
type: 'REMOVE_FILTER'
payload: { index: number }
}
| {
type: 'UPDATE_FILTER';
type: 'UPDATE_FILTER'
payload: {
filterIdx: number;
value: Filter;
};
};
filterIdx: number
value: Filter
}
}
const FilterReducer = (
state: FilterInitialState,
action: FILTER_ACTIONTYPE
) => {
const FilterReducer = (state: FilterInitialState, action: FILTER_ACTIONTYPE) => {
switch (action.type) {
case 'INIT_TABLE': {
return {
...state,
filters: getInitialFilters(
action.payload.table,
action.payload.savedState
),
};
filters: getInitialFilters(action.payload.table, action.payload.savedState),
}
}
case 'SET_FILTERS':
return {
...state,
filters: action.payload,
};
}
case 'ADD_FILTER': {
const isValid = isValidFilter(action.payload);
const isValid = isValidFilter(action.payload)
const newState: any = {
...state,
filters: update(state.filters, { $push: [action.payload] }),
};
if (isValid) {
newState.page = 1;
newState.refreshPageFlag = Date.now();
newState.totalRows = TOTAL_ROWS_RESET;
}
return newState;
if (isValid) {
newState.page = 1
newState.refreshPageFlag = Date.now()
newState.totalRows = TOTAL_ROWS_RESET
}
return newState
}
case 'REMOVE_FILTER': {
const removeIdx = action.payload.index;
const removeFilter = state.filters[removeIdx];
const isValid = isValidFilter(removeFilter);
const removeIdx = action.payload.index
const removeFilter = state.filters[removeIdx]
const isValid = isValidFilter(removeFilter)
const newState: any = {
...state,
filters: update(state.filters, {
$splice: [[removeIdx, 1]],
}),
};
if (isValid) {
newState.page = 1;
newState.refreshPageFlag = Date.now();
newState.totalRows = TOTAL_ROWS_RESET;
}
return newState;
if (isValid) {
newState.page = 1
newState.refreshPageFlag = Date.now()
newState.totalRows = TOTAL_ROWS_RESET
}
return newState
}
case 'UPDATE_FILTER': {
const updatedFilter = state.filters[action.payload.filterIdx];
const previousIsValid = isValidFilter(updatedFilter);
const afterIsValid = isValidFilter(action.payload.value);
const updatedFilter = state.filters[action.payload.filterIdx]
const previousIsValid = isValidFilter(updatedFilter)
const afterIsValid = isValidFilter(action.payload.value)
const newState: any = {
...state,
filters: update(state.filters, {
[action.payload.filterIdx]: { $set: action.payload.value },
}),
};
if (previousIsValid || afterIsValid) {
newState.page = 1;
newState.refreshPageFlag = Date.now();
newState.totalRows = TOTAL_ROWS_RESET;
}
return newState;
if (previousIsValid || afterIsValid) {
newState.page = 1
newState.refreshPageFlag = Date.now()
newState.totalRows = TOTAL_ROWS_RESET
}
return newState
}
default:
return state;
return state
}
};
}
export default FilterReducer;
export default FilterReducer
function isValidFilter(filter: Filter) {
return (
filter &&
filter.column &&
filter.column != '' &&
filter.value &&
filter.value != ''
);
return filter && filter.column && filter.column != '' && filter.value && filter.value != ''
}
+19 -21
View File
@@ -1,16 +1,13 @@
import {
CalculatedColumn,
HeaderRendererProps,
} from '@supabase/react-data-grid';
import { CalculatedColumn, HeaderRendererProps } from '@supabase/react-data-grid'
export interface Dictionary<T> {
[Key: string]: T;
[Key: string]: T
}
export interface Sort {
column: string;
ascending?: boolean;
nullsFirst?: boolean;
column: string
ascending?: boolean
nullsFirst?: boolean
}
export type FilterOperator =
@@ -25,23 +22,24 @@ export type FilterOperator =
| '!~~'
| '!~~*'
| 'in'
| 'is';
| 'is'
export interface Filter {
column: string;
operator: FilterOperator;
value: any;
id: string
column: string
operator: FilterOperator
value: any
}
export interface SavedState {
filters: Filter[];
gridColumns: CalculatedColumn<any, any>[];
sorts: Sort[];
filters: Filter[]
gridColumns: CalculatedColumn<any, any>[]
sorts: Sort[]
}
export interface DragItem {
index: number;
key: string;
index: number
key: string
}
export type ColumnType =
@@ -56,10 +54,10 @@ export type ColumnType =
| 'primary_key'
| 'text'
| 'time'
| 'unknown';
| 'unknown'
export interface ColumnHeaderProps<R> extends HeaderRendererProps<R> {
columnType: ColumnType;
isPrimaryKey: Boolean | undefined;
format: string;
columnType: ColumnType
isPrimaryKey: Boolean | undefined
format: string
}
+10 -10
View File
@@ -1,16 +1,16 @@
import { Filter, SavedState, SupaTable } from '../types';
import { uuidv4 } from 'lib/helpers'
import { Filter, SavedState, SupaTable } from '../types'
export function getInitialFilters(
table: SupaTable,
savedState?: SavedState
): Filter[] {
export function getInitialFilters(table: SupaTable, savedState?: SavedState): Filter[] {
if (savedState?.filters) {
// verify column still exists
const filters = savedState.filters.filter((x) => {
const found = table.columns.find((y) => y.name === x.column);
return found ? true : false;
});
if (filters?.length > 0) return filters;
const found = table.columns.find((y) => y.name === x.column)
return found ? true : false
})
if (filters?.length > 0) {
return filters.map((filter) => ({ ...filter, id: filter?.id || uuidv4() }))
}
}
return [];
return []
}
@@ -0,0 +1,29 @@
import { FC } from 'react'
import Link from 'next/link'
import { useFlag } from 'hooks'
import { IconExternalLink } from '@supabase/ui'
const AppBannerWrapper: FC = ({ children }) => {
const ongoingIncident = useFlag('ongoingIncident')
return (
<div className="flex h-full flex-col">
{ongoingIncident && (
<Link href="https://status.supabase.com">
<a target="_blank">
<div className="hover:bg-green-1000 flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 transition text-scale-400 dark:text-scale-1200">
<p className="text-sm font-medium">
We are currently investigating a technical issue, follow status.supabase.com for
updates
</p>
<IconExternalLink size={16} strokeWidth={2} />
</div>
</a>
</Link>
)}
{children}
</div>
)
}
export default AppBannerWrapper
+2 -1
View File
@@ -1,5 +1,6 @@
import PortalToast from './PortalToast'
import GoTrueWrapper from './GoTrueWrapper/GoTrueWrapper'
import RouteValidationWrapper from './RouteValidationWrapper'
import AppBannerWrapper from './AppBannerWrapper'
export { PortalToast, GoTrueWrapper, RouteValidationWrapper }
export { PortalToast, GoTrueWrapper, RouteValidationWrapper, AppBannerWrapper }
@@ -1,14 +1,14 @@
import { FC, useEffect } from 'react'
import { usePrevious } from 'hooks'
import SqlEditor from 'components/to-be-cleaned/SqlEditor'
import SqlEditor from 'components/ui/SqlEditor'
interface Props {
operation: string
definition: string
check: string
onUpdatePolicyUsing: (using: string | null) => void
onUpdatePolicyCheck: (check: string | null) => void
onUpdatePolicyUsing: (using: string | undefined) => void
onUpdatePolicyCheck: (check: string | undefined) => void
}
const PolicyDefinition: FC<Props> = ({
@@ -24,8 +24,8 @@ const PolicyDefinition: FC<Props> = ({
const previousOperation = usePrevious(operation) || ''
useEffect(() => {
if (showUsing(previousOperation) && !showUsing(operation)) onUpdatePolicyUsing(null)
if (showCheck(previousOperation) && !showCheck(operation)) onUpdatePolicyCheck(null)
if (showUsing(previousOperation) && !showUsing(operation)) onUpdatePolicyUsing(undefined)
if (showCheck(previousOperation) && !showCheck(operation)) onUpdatePolicyCheck(undefined)
}, [operation])
return (
@@ -68,10 +68,10 @@ const PolicyEditor: FC<Props> = ({
operation={operation}
definition={definition}
check={check}
onUpdatePolicyUsing={(definition: string | null) =>
onUpdatePolicyUsing={(definition: string | undefined) =>
onUpdatePolicyFormFields({ definition })
}
onUpdatePolicyCheck={(check: string | null) => onUpdatePolicyFormFields({ check })}
onUpdatePolicyCheck={(check: string | undefined) => onUpdatePolicyFormFields({ check })}
/>
</Modal.Content>
</div>
@@ -26,7 +26,7 @@ ON ${schema}.${table}
FOR SELECT USING (
true
);`.trim(),
name: 'Enable access to all users',
name: 'Enable read access for all users',
definition: 'true',
check: '',
command: 'SELECT',
@@ -1,7 +1,7 @@
import { Button, Modal } from '@supabase/ui'
import { isEmpty } from 'lodash'
import { FC, useState } from 'react'
import SqlEditor from 'components/to-be-cleaned/SqlEditor'
import SqlEditor from 'components/ui/SqlEditor'
import { PolicyForReview } from './Policies.types'
interface Props {
@@ -1,6 +1,6 @@
import { FC } from 'react'
import { Button, Dropdown, IconEdit, IconTrash, IconMoreVertical } from '@supabase/ui'
import Panel from 'components/to-be-cleaned/Panel'
import Panel from 'components/ui/Panel'
import { PostgresPolicy } from '@supabase/postgres-meta'
interface Props {
@@ -5,7 +5,7 @@ import { useStore } from 'hooks'
import PolicyTableRowHeader from './PolicyTableRowHeader'
import PolicyRow from './PolicyRow'
import Panel from 'components/to-be-cleaned/Panel'
import Panel from 'components/ui/Panel'
interface Props {
table: PostgresTable
@@ -1,6 +1,6 @@
import { FC } from 'react'
import { isEmpty } from 'lodash'
import SqlEditor from 'components/to-be-cleaned/SqlEditor'
import SqlEditor from 'components/ui/SqlEditor'
import { PolicyTemplate } from './PolicyTemplates.constants'
interface Props {
@@ -7,7 +7,6 @@ export const formatComputeSizes = (computeSizes: any[]) => {
const microOption = {
id: '',
name: 'Micro Add-on',
description: 'Basic set up to get started',
metadata: {
default_price_id: undefined,
supabase_prod_id: 'addon_instance_micro',
@@ -29,30 +28,23 @@ export const formatComputeSizes = (computeSizes: any[]) => {
],
}
const addonsOrder = [
'addon_instance_small',
'addon_instance_medium',
'addon_instance_large',
'addon_instance_xlarge',
'addon_instance_xxlarge',
'addon_instance_4xlarge',
'addon_instance_8xlarge',
'addon_instance_12xlarge',
'addon_instance_16xlarge',
]
return [microOption]
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_small'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_medium'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_large'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_xlarge'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_xxlarge'
)
addonsOrder.map((id: string) => {
return computeSizes.find((option: any) => option.metadata.supabase_prod_id === id)
})
)
.filter((option) => option !== undefined)
}
@@ -1,12 +1,14 @@
import { FC } from 'react'
import Link from 'next/link'
import { Badge, Button, Radio } from '@supabase/ui'
import { useStore } from 'hooks'
import { useStore, useFlag } from 'hooks'
import { getProductPrice } from '../Billing.utils'
import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident'
interface Props {
computeSizes: any[]
currentComputeSize: any
currentComputeSize?: any
selectedComputeSize: any
onSelectOption: (option: any) => void
}
@@ -17,10 +19,9 @@ const ComputeSizeSelection: FC<Props> = ({
selectedComputeSize,
onSelectOption,
}) => {
// [Joshen] We should have some indication on what's the current compute
// size of the project based on currentComputeSize
const { ui } = useStore()
const projectRef = ui.selectedProjectRef
const addonUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
return (
<div className="space-y-4">
@@ -29,49 +30,60 @@ const ComputeSizeSelection: FC<Props> = ({
<h4 className="text-lg">Database add-ons</h4>
<Badge color="green">Optional</Badge>
</div>
<p className="text-sm text-scale-1100">
<p className="text-scale-1100 text-sm">
Choose the database instance size that best fits your needs
</p>
</div>
<Radio.Group type="cards" className="billing-compute-radio">
{computeSizes.map((option: any) => {
const defaultPrice = getProductPrice(option)
return (
<Radio
hidden
key={option.id}
align="vertical"
label={option.name}
// @ts-ignore
description={
<div>
<p>{option.description}</p>
<p>{option.metadata.features}</p>
</div>
}
value={option.id}
optionalLabel={<div>${defaultPrice.unit_amount / 100} / month</div>}
checked={selectedComputeSize?.id === option.id}
onChange={() => onSelectOption(option)}
/>
)
})}
<Radio
hidden
disabled
key="compute-size-cta"
align="vertical"
label="Need a larger add on?"
description="Reach out to us - we've got you covered!"
optionalLabel={
<Link href={`/support/new?ref=${projectRef}&category=sales`}>
<a>
<Button>Contact us</Button>
</a>
</Link>
}
/>
</Radio.Group>
{addonUpdateDisabled ? (
<DisabledWarningDueToIncident title="Updating database add-ons is currently disabled" />
) : (
<Radio.Group type="cards" className="billing-compute-radio">
{computeSizes.map((option: any) => {
const defaultPrice = getProductPrice(option)
return (
<Radio
hidden
key={option.id}
align="vertical"
// @ts-ignore
label={
<div className="flex items-center space-x-4">
<p>{option.name}</p>
{currentComputeSize?.id === option.id && (
<Badge color="brand">Current selection</Badge>
)}
</div>
}
// @ts-ignore
description={
<div>
<p>{option.metadata.features}</p>
</div>
}
value={option.id}
optionalLabel={<div>${defaultPrice.unit_amount / 100} / month</div>}
checked={selectedComputeSize?.id === option.id}
onChange={() => onSelectOption(option)}
/>
)
})}
<Radio
hidden
disabled
key="compute-size-cta"
align="vertical"
label="Need a larger add on?"
description="Reach out to us - we've got you covered!"
optionalLabel={
<Link href={`/support/new?ref=${projectRef}&category=sales`}>
<a>
<Button>Contact us</Button>
</a>
</Link>
}
/>
</Radio.Group>
)}
</div>
)
}
@@ -6,3 +6,21 @@ export const getProductPrice = (product: any) => {
: product.prices[0]
return price
}
export const formSubscriptionUpdatePayload = (
selectedTier: any,
selectedComputeSize: any,
selectedPaymentMethod: string,
region: string
) => {
const defaultPrice = selectedTier ? getProductPrice(selectedTier) : undefined
const addons =
region === 'af-south-1' || !selectedComputeSize.id ? [] : [selectedComputeSize.prices[0].id]
const proration_date = Math.floor(Date.now() / 1000)
return {
...(defaultPrice && { tier: defaultPrice.id }),
addons,
proration_date,
payment_method: selectedPaymentMethod,
}
}
@@ -0,0 +1,217 @@
import { FC, useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import { Transition } from '@headlessui/react'
import { useStore } from 'hooks'
import { getURL } from 'lib/helpers'
import { post, patch } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import Divider from 'components/ui/Divider'
import { formatComputeSizes } from './AddOns/AddOns.utils'
import {
AddNewPaymentMethodModal,
ComputeSizeSelection,
StripeSubscription,
PaymentSummaryPanel,
UpdateSuccess,
} from './'
import { SubscriptionPreview } from './Billing.types'
import { formSubscriptionUpdatePayload } from './Billing.utils'
interface Props {
products: { tiers: any[]; addons: any[] }
paymentMethods?: any[]
currentSubscription: StripeSubscription
isLoadingPaymentMethods: boolean
}
const EnterpriseUpdate: FC<Props> = ({
products,
paymentMethods,
currentSubscription,
isLoadingPaymentMethods,
}) => {
const { app, ui } = useStore()
const router = useRouter()
const projectId = ui.selectedProject?.id ?? -1
const projectRef = ui.selectedProject?.ref ?? 'default'
const projectRegion = ui.selectedProject?.region ?? ''
const { addons } = products
const computeSizes = formatComputeSizes(addons)
const currentComputeSize =
computeSizes.find((option: any) => option.id === currentSubscription?.addons[0]?.prod_id) ||
computeSizes.find((option: any) => option.metadata.supabase_prod_id === 'addon_instance_micro')
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<any>()
const [selectedComputeSize, setSelectedComputeSize] = useState<any>(currentComputeSize)
const [subscriptionPreview, setSubscriptionPreview] = useState<SubscriptionPreview>()
const [isRefreshingPreview, setIsRefreshingPreview] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccessful, setIsSuccessful] = useState(false)
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false)
const isChangingComputeSize = currentComputeSize?.id !== selectedComputeSize.id
useEffect(() => {
getSubscriptionPreview()
}, [selectedComputeSize])
useEffect(() => {
if (!isLoadingPaymentMethods && paymentMethods && paymentMethods.length > 0) {
setSelectedPaymentMethod(paymentMethods[0].id)
}
}, [isLoadingPaymentMethods, paymentMethods])
const getSubscriptionPreview = async () => {
// For enterprise, only tier will be fixed based on current subscription
// Only allow add-ons changing
const payload = {
...formSubscriptionUpdatePayload(
null,
selectedComputeSize,
selectedPaymentMethod,
projectRegion
),
tier: currentSubscription.tier.price_id,
}
setIsRefreshingPreview(true)
const preview = await post(`${API_URL}/projects/${projectRef}/subscription/preview`, payload)
if (preview.error) {
ui.setNotification({
category: 'error',
message: `Failed to fetch subscription preview: ${preview.error.message}`,
})
}
setSubscriptionPreview(preview)
setIsRefreshingPreview(false)
}
// Last todo to support enterprise billing on dashboard + E2E test
const onConfirmPayment = async () => {
const payload = {
...formSubscriptionUpdatePayload(
null,
selectedComputeSize,
selectedPaymentMethod,
projectRegion
),
tier: currentSubscription.tier.price_id,
}
setIsSubmitting(true)
const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload)
if (res?.error) {
ui.setNotification({
category: 'error',
message: `Failed to update subscription: ${res?.error?.message}`,
})
} else {
if (isChangingComputeSize) {
app.onProjectStatusUpdated(projectId, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message:
'Your project has been updated and is currently restarting to update its instance size',
duration: 8000,
})
router.push(`/project/${projectRef}`)
} else {
setIsSuccessful(true)
}
}
setIsSubmitting(false)
}
if (!isSubmitting && isSuccessful) {
return (
<UpdateSuccess
projectRef={projectRef || ''}
title="Your project has been updated!"
message="Let us know if you have any feedback at sales@supabase.io."
/>
)
}
return (
<>
<Transition
show
enter="transition ease-out duration-300"
enterFrom="transform opacity-0 translate-x-10"
enterTo="transform opacity-100 translate-x-0"
className="flex w-full items-start justify-between"
>
<div className="2xl:min-w-5xl mx-auto mt-10">
<div className="relative space-y-4 px-5">
<div className="space-y-8">
<h4 className="text-scale-900 text-lg">Change your project's subscription</h4>
<div
className="space-y-8 overflow-scroll pb-8"
style={{ height: 'calc(100vh - 6.4rem - 57px)' }}
>
<h3 className="text-xl">
Managing your <span className="text-brand-900">Enterprise</span> plan
</h3>
<div
className={[
'bg-panel-body-light dark:bg-panel-body-dark border-panel-border-light border-panel-border-dark',
'flex max-w-[600px] items-center justify-between gap-16 rounded border px-6 py-4 drop-shadow-sm',
].join(' ')}
>
<p>
If you'd like to change your subscription away from enterprise, please reach out
to <span className="text-brand-900">enterprise@supabase.io</span> with your
request.
</p>
</div>
{projectRegion !== 'af-south-1' && (
<>
<Divider light />
<ComputeSizeSelection
computeSizes={computeSizes || []}
currentComputeSize={currentComputeSize}
selectedComputeSize={selectedComputeSize}
onSelectOption={setSelectedComputeSize}
/>
</>
)}
</div>
</div>
</div>
</div>
<div className="w-[32rem]">
<PaymentSummaryPanel
isSpendCapEnabled={true}
isSubmitting={isSubmitting}
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
currentPlan={currentSubscription.tier}
currentComputeSize={currentComputeSize}
selectedComputeSize={selectedComputeSize}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={setSelectedPaymentMethod}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
onConfirmPayment={onConfirmPayment}
/>
</div>
</Transition>
<AddNewPaymentMethodModal
visible={showAddPaymentMethodModal}
returnUrl={`${getURL()}/project/${projectRef}/settings/billing/update/pro`}
onCancel={() => setShowAddPaymentMethodModal(false)}
/>
</>
)
}
export default EnterpriseUpdate
@@ -10,8 +10,8 @@ import {
Alert,
} from '@supabase/ui'
import { useFlag, useStore } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { useStore } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice } from '../Billing.utils'
import PaymentTotal from './PaymentTotal'
@@ -57,21 +57,23 @@ const PaymentSummaryPanel: FC<Props> = ({
onConfirmPayment,
isSubmitting,
}) => {
/**
* Feature flags
*/
const nativeBilling = useFlag('nativeBilling')
const [showConfirmModal, setShowConfirmModal] = useState(false)
const { ui } = useStore()
const projectRegion = ui.selectedProject?.region
const [showConfirmModal, setShowConfirmModal] = useState(false)
const isEnterprise = currentPlan.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
const isChangingPlan =
(currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG && currentPlan.prod_id !== selectedPlan?.id) ||
(currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG &&
selectedPlan &&
currentPlan.prod_id !== selectedPlan.id) ||
(currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG && !isSpendCapEnabled) ||
(currentPlan.prod_id === STRIPE_PRODUCT_IDS.PAYG && isSpendCapEnabled)
const isChangingComputeSize = currentComputeSize.id !== selectedComputeSize.id
const hasChangesToPlan = subscriptionPreview?.has_changes ?? false
// If it's enterprise we only only changing of add-ons
const hasChangesToPlan = isEnterprise
? isChangingComputeSize
: subscriptionPreview?.has_changes ?? false
const getPlanName = (plan: any) => {
if (plan.prod_id === STRIPE_PRODUCT_IDS.PAYG || plan.id === STRIPE_PRODUCT_IDS.PAYG) {
@@ -82,14 +84,14 @@ const PaymentSummaryPanel: FC<Props> = ({
return (
<>
<div
className="bg-panel-body-light dark:bg-panel-body-dark w-full px-6 lg:px-12 py-10 space-y-8 border-l"
className="bg-panel-body-light dark:bg-panel-body-dark w-full space-y-8 border-l px-6 py-10 lg:px-12"
style={{ height: 'calc(100vh - 57px)' }}
>
<p>Payment Summary</p>
{/* Subscription details */}
<div className="space-y-1">
<p className="text-sm text-scale-1100">Selected subscription</p>
<p className="text-scale-1100 text-sm">Selected subscription</p>
<div className="flex items-center justify-between">
<p className={`${isChangingPlan ? 'text-scale-1100 line-through' : ''} text-sm`}>
{getPlanName(currentPlan)}
@@ -109,57 +111,53 @@ const PaymentSummaryPanel: FC<Props> = ({
</div>
{/* Add on details */}
{nativeBilling && (
<>
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
</>
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
</div>
)}
<div className="h-px w-full bg-scale-600" />
<div className="bg-scale-600 h-px w-full" />
<PaymentTotal
subscriptionPreview={subscriptionPreview}
@@ -171,13 +169,13 @@ const PaymentSummaryPanel: FC<Props> = ({
<div className="space-y-2">
<p className="text-sm">Select payment method</p>
{isLoadingPaymentMethods ? (
<div className="flex items-center rounded-md space-x-4 px-4 py-2 bg-scale-400 border border-scale-700">
<div className="bg-scale-400 border-scale-700 flex items-center space-x-4 rounded-md border px-4 py-2">
<IconLoader className="animate-spin" size={14} />
<p className="text-sm text-scale-1100">Retrieving payment methods</p>
<p className="text-scale-1100 text-sm">Retrieving payment methods</p>
</div>
) : paymentMethods.length === 0 ? (
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-100 border border-dashed">
<div className="flex items-center space-x-4 text-scale-1100">
<div className="bg-scale-100 flex items-center justify-between rounded-md border border-dashed px-4 py-2">
<div className="text-scale-1100 flex items-center space-x-4">
<IconAlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm">No saved payment methods</p>
</div>
@@ -214,11 +212,11 @@ const PaymentSummaryPanel: FC<Props> = ({
)
})}
<div
className="py-2 px-3 flex items-center space-x-2 cursor-pointer transition group hover:bg-scale-500"
className="hover:bg-scale-500 group flex cursor-pointer items-center space-x-2 py-2 px-3 transition"
onClick={onSelectAddNewPaymentMethod}
>
<IconPlus size={16} />
<p className="text-scale-1000 transition group-hover:text-scale-1200">
<p className="text-scale-1000 group-hover:text-scale-1200 transition">
Add new payment method
</p>
</div>
@@ -247,7 +245,7 @@ const PaymentSummaryPanel: FC<Props> = ({
header="Updating project database instance size"
onCancel={() => setShowConfirmModal(false)}
>
<div className="py-4 space-y-4">
<div className="space-y-4 py-4">
<Modal.Content>
<Alert
withIcon
@@ -259,7 +257,7 @@ const PaymentSummaryPanel: FC<Props> = ({
</Alert>
</Modal.Content>
<Modal.Content>
<p className="text-sm text-scale-1200">Would you like to update your project now?</p>
<p className="text-scale-1200 text-sm">Would you like to update your project now?</p>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
@@ -1,8 +1,10 @@
import { FC } from 'react'
import { Button, IconLoader } from '@supabase/ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useFlag } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { StripeProduct } from '../..'
import { StripeProduct } from 'components/interfaces/Billing'
import Link from 'next/link'
interface Props {
@@ -12,6 +14,8 @@ interface Props {
}
const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const getButtonType = (plan: any) => {
if (plan.name === 'Enterprise' || plan.id === STRIPE_PRODUCT_IDS.FREE) {
return 'default'
@@ -71,9 +75,9 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
}
return (
<div className="flex items-center justify-center">
<div className="flex flex-col items-center justify-center space-y-2">
<Button
disabled={disabled}
disabled={disabled || projectUpdateDisabled}
type={type}
onClick={() => onSelectPlan(plan)}
block
@@ -81,6 +85,11 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
>
{ctaText}
</Button>
{!disabled && projectUpdateDisabled && (
<p className="text-scale-1100 text-sm">
Subscription changes are currently disabled, our engineers are working on a fix
</p>
)}
</div>
)
}
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff. Show more