Merge branch 'master' into km/update-tsdocs

This commit is contained in:
Kang Ming committed 2023-05-26 11:11:47 +08:00
commit dcfef4d2f2
234 files changed
+24694 -3516

No files matched your search

+3
View File
@@ -34,5 +34,8 @@ jobs:
run: npm ci
working-directory: ./
- name: Run tests
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: "--max_old_space_size=3072"
run: npm run test:studio
working-directory: ./
+3
View File
@@ -122,5 +122,8 @@ typings/
apps/new-docs/*
# UI tokens
packages/ui/tokens/**/*.json
# For self-hosted logs: https://github.com/supabase/supabase/blob/86e3ab20abfdb9c3e666334d3d2f8efeef9ccf2c/docker/docker-compose-logging.yml#L101
gcloud.json
+1 -1
View File
@@ -1 +1 @@
^./i18n
^./i18n
+1 -1
View File
@@ -5,7 +5,7 @@ export type FooterHelpCalloutType = 'default' | 'postgres'
const content = {
default: {
title: 'Need some help?',
description: `Not to worry, our specialist engineers are here to help. Submit a support ticket through the [Dashboard](https://app.supabase.com/support/new).`,
description: `Not to worry, our specialist engineers are here to help. Submit a support ticket through the [Dashboard](https://supabase.com/dashboard/support/new).`,
},
postgres: {
title: 'Looking for Serverless Postgres?',
@@ -475,10 +475,9 @@ export const auth = {
items: [
{ name: 'Overview', url: '/guides/auth/auth-helpers' },
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui' },
{ name: 'Next.js (pages)', url: '/guides/auth/auth-helpers/nextjs' },
{
name: 'Next.js (app)',
url: '/guides/auth/auth-helpers/nextjs-server-components',
name: 'Next.js',
url: '/guides/auth/auth-helpers/nextjs',
},
{ name: 'Remix', url: '/guides/auth/auth-helpers/remix' },
{ name: 'SvelteKit', url: '/guides/auth/auth-helpers/sveltekit' },
@@ -751,6 +750,7 @@ export const functions: NavMenuConstant = {
items: [
{ name: 'Dart Edge on Supabase', url: '/guides/functions/dart-edge' },
{ name: 'Browserless.io', url: '/guides/functions/examples/screenshots' },
{ name: 'Hugging Face', url: '/guides/functions/examples/huggingface-image-captioning' },
{ name: 'OpenAI API', url: '/guides/functions/examples/openai' },
{ name: 'Upstash Redis', url: '/guides/functions/examples/upstash-redis' },
{ name: 'Type-Safe SQL with Kysely', url: '/guides/functions/kysely-postgres' },
@@ -1116,6 +1116,7 @@ export const integrations: NavMenuConstant = {
{ name: 'Directus', url: '/guides/integrations/directus' },
{ name: 'Draftbit', url: '/guides/integrations/draftbit' },
{ name: 'FlutterFlow', url: '/guides/integrations/flutterflow' },
{ name: 'Forest Admin', url: '/guides/integrations/forestadmin' },
{ name: 'Plasmic', url: '/guides/integrations/plasmic' },
{ name: 'ILLA', url: '/guides/integrations/illa' },
],
@@ -197,7 +197,7 @@ const NavigationMenuRefListItems = ({
<HeaderLink title={menu.title} url={menu.url} id={id} />
<RevVersionDropdown />
</div>
<ul className="function-link-list flex flex-col gap-2">
<ul className="function-link-list flex flex-col gap-2 pb-5">
{filteredSections.map((section) => {
return (
<Fragment key={section.title}>
@@ -129,7 +129,7 @@ const TopNavBar: FC = () => {
type="outline"
as="a"
// @ts-ignore
href="https://app.supabase.com"
href="https://supabase.com/dashboard"
className="text-scale-1100 text-sm"
target="_blank"
rel="noreferrer noopener"
@@ -119,7 +119,7 @@ const TopNavBarRef: FC = () => {
type="text"
as="a"
// @ts-ignore
href="https://app.supabase.com"
href="https://supabase.com/dashboard"
target="_blank"
rel="noreferrer noopener"
>
@@ -10,6 +10,7 @@ import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers
import RefDetailCollapse from '~/components/reference/RefDetailCollapse'
import { Fragment } from 'react'
import { IRefFunctionSection } from './Reference.types'
import components from '~/components'
const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
const item = props.spec.functions.find((x: any) => x.id === props.funcData.id)
@@ -38,16 +39,16 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
<header className={['prose'].join(' ')}>
{shortText && <ReactMarkdown className="text-sm">{shortText}</ReactMarkdown>}
</header>
{item.description && (
<div className="prose">
<ReactMarkdown className="text-sm">{item.description}</ReactMarkdown>
</div>
)}
{item.notes && (
<div className="prose">
<ReactMarkdown className="text-sm">{item.notes}</ReactMarkdown>
<ReactMarkdown className="text-sm" components={components}>
{item.notes}
</ReactMarkdown>
</div>
)}
{/* // parameters */}
+168
View File
@@ -3,6 +3,174 @@ id: release-notes
title: Release Notes
---
## 0.11.0 - 2023-05-24
- Update dependency: postgrest-csharp@3.2.0
- General codebase and QOL improvements. Exceptions are generally thrown through `PostgrestException` now instead
of `Exception`. A `FailureHint.Reason` is provided with failures if possible to parse.
- `AddDebugListener` is now available on the client to help with debugging
- Merges [#65](https://github.com/supabase-community/postgrest-csharp/pull/65) Cleanup + Add better exception handling
- Merges [#66](https://github.com/supabase-community/postgrest-csharp/pull/66) Local test Fixes
- Fixes [#67](https://github.com/supabase-community/postgrest-csharp/issues/67) Postgrest Reference attribute is
producing StackOverflow for circular references
- Update dependency: gotrue-csharp@4.0.2
- [#58](https://github.com/supabase-community/gotrue-csharp/issues/58) - Add support for the `reauthentication` endpoint which allows for secure password changes.
- Update dependency: realtime-csharp@6.0.1
- Updates publishing action for future packages, includes README and icon.
- Merges [#28](https://github.com/supabase-community/realtime-csharp/pull/28) and [#30](https://github.com/supabase-community/realtime-csharp/pull/30)
- The realtime client now takes a "fail-fast" approach. On establishing an initial connection, client will throw
a `RealtimeException` in `ConnectAsync()` if the socket server is unreachable. After an initial connection has been
established, the **client will continue attempting reconnections indefinitely until disconnected.**
- [Major, New] C# `EventHandlers` have been changed to `delegates`. This should allow for cleaner event data access over
the previous subclassed `EventArgs` setup. Events are scoped accordingly. For example, the `RealtimeSocket` error
handlers will receive events regarding socket connectivity; whereas the `RealtimeChannel` error handlers will receive
events according to `Channel` joining/leaving/etc. This is implemented with the following methods prefixed by (
Add/Remove/Clear):
- `RealtimeBroadcast.AddBroadcastEventHandler`
- `RealtimePresence.AddPresenceEventHandler`
- `RealtimeSocket.AddStateChangedHandler`
- `RealtimeSocket.AddMessageReceivedHandler`
- `RealtimeSocket.AddHeartbeatHandler`
- `RealtimeSocket.AddErrorHandler`
- `RealtimeClient.AddDebugHandler`
- `RealtimeClient.AddStateChangedHandler`
- `RealtimeChannel.AddPostgresChangeHandler`
- `RealtimeChannel.AddMessageReceivedHandler`
- `RealtimeChannel.AddErrorHandler`
- `Push.AddMessageReceivedHandler`
- [Major, new] `ClientOptions.Logger` has been removed in favor of `Client.AddDebugHandler()` which allows for
implementing custom logging solutions if desired.
- A simple logger can be set up with the following:
```c#
client.AddDebugHandler((sender, message, exception) => Debug.WriteLine(message));
```
- [Major] `Connect()` has been marked `Obsolete` in favor of `ConnectAsync()`
- Custom reconnection logic has been removed in favor of using the built-in logic from `Websocket.Client@4.6.1`.
- Exceptions that are handled within this library have been marked as `RealtimeException`s.
- The local, docker-composed test suite has been brought back (as opposed to remotely testing on live supabase servers)
to test against.
- Comments have been added throughout the entire codebase and an `XML` file is now generated on build.
## 0.10.0 - 2023-05-14
- Changes options to require `Supabase.SupabaseOptions.SessionPersistor` from using `ISupabaseSessionHandler`
to `IGotrueSessionPersistance<Session>` (these are now synchronous operations).
- Update dependency: gotrue-csharp@4.0.1
- [#60](https://github.com/supabase-community/gotrue-csharp/pull/60) - Add interfaces, bug fixes, additional error
reason detection. Thanks [@wiverson](https://github.com/wiverson)!
- [#57](https://github.com/supabase-community/gotrue-csharp/pull/57) Refactor exceptions, code cleanup, and move to
delegate auth state changes
- Huge thank you to [@wiverson](https://github.com/wiverson) for his help on this refactor and release!
- Changes
- Exceptions have been simplified to a single `GotrueException`. A `Reason` field has been added
to `GotrueException` to clarify what happened. This should also be easier to manage as the Gotrue
server API & messages evolve.
- The session delegates for `Save`/`Load`/`Destroy` have been simplified to no longer require `async`.
- Console logging in a few places (most notable the background refresh thread) has been removed
in favor of a notification method. See `Client.AddDebugListener()` and the test cases for examples.
This will allow you to implement your own logging strategy (write to temp file, console, user visible
err console, etc).
- The client now more reliably emits AuthState changes.
- There is now a single source of truth for headers in the stateful Client - the `Options` headers.
- New feature:
- Added a `Settings` request to the stateless API only - you can now query the server instance to
determine if it's got the settings you need. This might allow for things like a visual
component in a tool to verify the GoTrue settings are working correctly, or tests that run differently
depending on the server configuration.
- Implementation notes:
- Test cases have been added to help ensure reliability of auth state change notifications
and persistence.
- Persistence is now managed via the same notifications as auth state change
## 0.9.1 - 2023-04-28
- Update dependency: gotrue-csharp@3.1.1
- Implements `SignInWithIdToken` for Apple/Google signing from LW7. A HUGE thank you
to [@wiverson](https://github.com/wiverson)!
- Update dependency: realtime-csharp@5.0.5
- Re: [#27](https://github.com/supabase-community/realtime-csharp/issues/27) `PostgresChangesOptions` was not
setting `listenType` in constructor. Thanks [@Kuffs2205](https://github.com/Kuffs2205)
- Update dependency: supabase-storage-csharp@1.2.10
- Re: [#7](https://github.com/supabase-community/storage-csharp/issues/7) Implements a `DownloadPublicFile` method.
## 0.9.0 - 2023-04-12
- Update dependency: gotrue-csharp@3.1.0
- [Minor] Implements PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather
than a `string`.
- Update dependency: supabase-storage-csharp@1.2.9
- Implements storage features from LW7:
- feat: custom file size limit and mime types at bucket
level [supabase/storage-js#151](https://github.com/supabase/storage-js/pull/151) file size and mime type
limits per bucket
- feat: quality option, image
transformation [supabase/storage-js#145](https://github.com/supabase/storage-js/pull/152) quality option for
image transformations
- feat: format option for webp
support [supabase/storage-js#142](https://github.com/supabase/storage-js/pull/142) format option for image
transformation
## 0.8.8 - 2023-03-29
- Update dependency: gotrue-csharp@3.0.6
- Supports adding `SignInOptions` (i.e. `RedirectTo`) on `OAuth Provider` SignIn requests.
## 0.8.7 - 2023-03-23
- Update dependency: realtime-csharp@5.0.4
- Re: [#26](https://github.com/supabase-community/realtime-csharp/pull/26) - Fixes Connect() not returning callback
result when the socket isn't null. Thanks [@BlueWaterCrystal](https://github.com/BlueWaterCrystal)!
## 0.8.6 - 2023-03-23
- Update dependency: supabase-storage-csharp@1.2.8
- [Merge #5](https://github.com/supabase-community/storage-csharp/pull/5) Added search string as an optional search
parameter. Thanks [@ElectroKnight22](https://github.com/ElectroKnight22)!
## 0.8.5 - 2023-03-10
- Update dependency: realtime-csharp@5.0.3
- Re: [#25](https://github.com/supabase-community/realtime-csharp/issues/25) - Support Channel being resubscribed
after having been unsubscribed, fixes rejoin timer being erroneously called on channel `Unsubscribe`.
Thanks [@Kuffs2205](https://github.com/Kuffs2205)!
## 0.8.4 - 2023-03-03
- Update dependency: supabase-storage-csharp@1.2.7
- Re: [#4](https://github.com/supabase-community/storage-csharp/issues/4) Implementation for `ClientOptions` which
supports specifying Upload, Download, and Request timeouts.
- Update dependency: realtime-csharp@5.0.2
- Re: [#24](https://github.com/supabase-community/realtime-csharp/issues/24) - Fixes join failing until reconnect
happened + adds access token push on channel join. Big thank you to [@Honeyhead](https://github.com/honeyhead) for
the help debugging and identifying!
## 0.8.3 - 2023-02-26
- Update dependency: supabase-storage-csharp@1.2.5
- Provides fix
for [supabase-community/supabase-csharp#54](https://github.com/supabase-community/supabase-csharp/issues/54) -
Dynamic headers were always being overwritten by initialized token headers, so the storage client would not
receive user's access token as expected.
- Provides fix for upload progress not reporting
in [supabase-community/storage-csharp#3](https://github.com/supabase-community/storage-csharp/issues/3)
- Update dependency: gotrue-csharp@3.0.5
- Fixes [#44](https://github.com/supabase-community/gotrue-csharp/issues/44) - refresh timer should automatically
reattempt (interval of 5s) for HTTP exceptions - gracefully exits on invalid refresh and triggers
an `AuthState.Changed` event
## 0.8.2 - 2023-02-26
- Update dependency: supabase-storage-csharp@1.2.4
- `UploadOrUpdate` now appropriately throws request exceptions
## 0.8.1 - 2023-02-06
- Update dependency: realtime-csharp@5.0.1
- Re: [#22](https://github.com/supabase-community/realtime-csharp/issues/22) - `SerializerSettings` were not being
passed to `PostgresChangesResponse` - Thanks [@Shenrak](https://github.com/Shenrak) for the help debugging!
## 0.8.0 - 2023-01-31
- Update dependency: realtime-csharp@5.0.0
+8 -4
View File
@@ -1,3 +1,11 @@
import '../../../packages/ui/build/css/themes/light.css'
import '../../../packages/ui/build/css/themes/dark.css'
import '../styles/ch.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
@@ -9,10 +17,6 @@ import Favicons from '~/components/Favicons'
import SiteLayout from '~/layouts/SiteLayout'
import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { post } from '~/lib/fetchWrappers'
import '../styles/ch.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const router = useRouter()
@@ -11,10 +11,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
## Official Libraries
| `Language` | `Source Code` | `Documentation` |
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-dart](https://github.com/supabase/supabase-dart) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
| `Language` | `Source Code` | `Documentation` |
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
## Community Libraries
@@ -22,10 +22,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](https://supabase.com/docs/reference/csharp/introduction) |
| Go | [supabase-go](https://github.com/supabase-community/supabase-go) | |
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | |
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](https://supabase.com/docs/reference/kotlin/introduction) |
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -0,0 +1,847 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs-pages',
title: 'Supabase Auth with Next.js Pages Directory',
description:
'Authentication helpers for Next.js API routes, middleware, and SSR in the Pages Directory.',
sidebar_label: 'Next.js (pages)',
}
This submodule provides convenience helpers for implementing user authentication in Next.js applications using the pages directory.
> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the App Router has reached stable status. This is now the recommended path for new Next.js app. Check out our guide on using [Auth Helpers with the Next.js App Directory](/docs/guides/auth/auth-helpers/nextjs).
## Install the Next.js helper library
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm"
>
<TabPanel id="npm" label="npm">
```sh
npm install @supabase/auth-helpers-nextjs
```
This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
npm install @supabase/auth-helpers-react
```
</TabPanel>
<TabPanel id="yarn" label="Yarn">
```sh
yarn add @supabase/auth-helpers-nextjs
```
This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
yarn add @supabase/auth-helpers-react
```
</TabPanel>
</Tabs>
## Set up environment variables
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example).
```bash title=.env.local
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
```
## Basic Setup
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
```jsx title=pages/_app.js
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { useState } from 'react'
function MyApp({ Component, pageProps }) {
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createPagesBrowserClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
```tsx lines=2,8 title=pages/_app.tsx
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
import { useState } from 'react'
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session
}>) {
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createPagesBrowserClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabPanel>
</Tabs>
You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined.
## Usage with TypeScript
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
### Browser client
Creating a new supabase client object:
```tsx
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '../database.types'
const supabaseClient = createPagesBrowserClient<Database>()
```
Retrieving a supabase client object from the SessionContext:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>()
```
### Server client
```tsx
// Creating a new supabase server client object (e.g. in API route):
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { Database } from 'types_db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const supabaseServerClient = createPagesServerClient<Database>({
req,
res,
})
const {
data: { user },
} = await supabaseServerClient.auth.getUser()
res.status(200).json({ name: user?.name ?? '' })
}
```
## Client-side data fetching with RLS
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
```jsx lines=10-17
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
const LoginPage = () => {
const supabaseClient = useSupabaseClient()
const user = useUser()
const [data, setData] = useState()
useEffect(() => {
async function loadData() {
const { data } = await supabaseClient.from('test').select('*')
setData(data)
}
// Only run query once user is logged in.
if (user) loadData()
}, [user])
if (!user)
return (
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
/>
)
return (
<>
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
)
}
export default LoginPage
```
## Server-side rendering (SSR)
Create a server supabase client to retrieve the logged in user's session:
```jsx title=pages/profile.js
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
```
## Server-side data fetching with RLS
You can use the server supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function ProtectedPage({ user, data }) {
return (
<>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Run queries with RLS on the server
const { data } = await supabase.from('users').select('*')
return {
props: {
initialSession: session,
user: session.user,
data: data ?? [],
},
}
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function ProtectedPage({ user, data }: { user: User; data: any }) {
return (
<>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Run queries with RLS on the server
const { data } = await supabase.from('users').select('*')
return {
props: {
initialSession: session,
user: session.user,
data: data ?? [],
},
}
}
```
</TabPanel>
</Tabs>
## Server-side data fetching to OAuth APIs using `provider token` {`#oauth-provider-token`}
When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function ProtectedPage({ user, allRepos }) {
return (
<>
<div>Protected content for {user.email}</div>
<p>Data fetched with provider token:</p>
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Retrieve provider_token & logged in user's third-party id from metadata
const { provider_token, user } = session
const userId = user.user_metadata.user_name
const allRepos = await (
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
})
).json()
return { props: { user, allRepos } }
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) {
return (
<>
<div>Protected content for {user.email}</div>
<p>Data fetched with provider token:</p>
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Retrieve provider_token & logged in user's third-party id from metadata
const { provider_token, user } = session
const userId = user.user_metadata.user_name
const allRepos = await (
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
})
).json()
return { props: { user, allRepos } }
}
```
</TabPanel>
</Tabs>
## Protecting API routes
Create a server supabase client to retrieve the logged in user's session:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title=pages/api/protected-route.js
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title=pages/api/protected-route.ts
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute: NextApiHandler = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
</Tabs>
## Auth with Next.js Middleware
As an alternative to protecting individual pages you can use a [Next.js Middleware](https://nextjs.org/docs/middleware) to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected:
```ts title=middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// Check auth condition
if (session?.user.email?.endsWith('@gmail.com')) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: '/middleware-protected/:path*',
}
```
## Migration Guide
### Migrating to v0.5.X
To make these helpers more flexible as well as more maintainable and easier to upgrade for new versions of Next.js, we're stripping them down to the most useful part which is managing the cookies and giving you an authenticated supabase-js client in any environment (client, server, middleware/edge).
Therefore we're marking the `withApiAuth`, `withPageAuth`, and `withMiddlewareAuth` higher order functions as deprecated and they will be removed in the next **minor** release (v0.6.X).
Please follow the steps below to update your API routes, pages, and middleware handlers. Thanks!
#### `withApiAuth` deprecated!
Use `createPagesServerClient` within your `NextApiHandler`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx title=pages/api/protected-route.ts
import { withApiAuth } from '@supabase/auth-helpers-nextjs'
export default withApiAuth(async function ProtectedRoute(req, res, supabase) {
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
})
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx title=pages/api/protected-route.ts
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute: NextApiHandler = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
</Tabs>
#### `withPageAuth` deprecated!
Use `createPagesServerClient` within `getServerSideProps`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx title=pages/profile.tsx
import { withPageAuth, User } from '@supabase/auth-helpers-nextjs'
export default function Profile({ user }: { user: User }) {
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
export const getServerSideProps = withPageAuth({ redirectTo: '/' })
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx title=pages/profile.js
import { createPagesServerClient, User } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function Profile({ user }: { user: User }) {
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
```
</TabPanel>
</Tabs>
#### `withMiddlewareAuth` deprecated!
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx title=middleware.ts
import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs'
export const middleware = withMiddlewareAuth({
redirectTo: '/',
authGuard: {
isPermitted: async (user) => {
return user.email?.endsWith('@gmail.com') ?? false
},
redirectTo: '/insufficient-permissions',
},
})
export const config = {
matcher: '/middleware-protected',
}
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx title=middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// Check auth condition
if (session?.user.email?.endsWith('@gmail.com')) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: '/middleware-protected',
}
```
</TabPanel>
</Tabs>
### Migrating to v0.4.X and supabase-js v2
With the update to `supabase-js` v2 the `auth` API routes are no longer required, therefore you can go ahead and delete your `auth` directory under the `/pages/api/` directory. Please refer to the [v2 migration guide](/docs/reference/javascript/v1/upgrade-guide) for the full set of changes within supabase-js.
The `/api/auth/logout` API route has been removed, please use the `signout` method instead:
```jsx
<button
onClick={async () => {
await supabaseClient.auth.signOut()
router.push('/')
}}
>
Logout
</button>
```
The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createPagesBrowserClient` and `createPagesServerClient` methods. This allows you to provide the CLI-generated types to the client:
```tsx
// client-side
import type { Database } from 'types_db'
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
// server-side API route
import type { NextApiRequest, NextApiResponse } from 'next'
import type { Database } from 'types_db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const supabaseServerClient = createPagesServerClient<Database>({
req,
res,
})
const {
data: { user },
} = await supabaseServerClient.auth.getUser()
res.status(200).json({ name: user?.name ?? '' })
}
```
- The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` componenent with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`.
- The `useUser` hook now returns the `user` object or `null`.
- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
Creating a new supabase client object:
```tsx
import { Database } from '../database.types'
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
```
Retrieving a supabase client object from the SessionContext:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>()
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,516 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs-server-components',
title: 'Supabase Auth with Next.js app directory',
description:
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components and Route Handlers.',
sidebar_label: 'Next.js (app)',
}
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
> Note: If you are using the `pages` directory, check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Bh1TOpOcGJQ"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
> To learn more about Supabase and the Next.js 13 app directory, check out [this playlist](https://youtube.com/playlist?list=PL5S4mPUpp4OtwG-qCxm8gA_hjaBq0OPdz).
## Install the Next.js helper library
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm"
>
<TabPanel id="npm" label="npm">
```sh
npm install @supabase/auth-helpers-nextjs
```
</TabPanel>
<TabPanel id="yarn" label="Yarn">
```sh
yarn add @supabase/auth-helpers-nextjs
```
</TabPanel>
</Tabs>
## Set up environment variables
Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api) in the dashboard, and create a `.env.local` file with the following environment variables:
```bash title=".env.local"
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
## Configure Middleware
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.js` in order to use a Supabase client in Server Components or Route Handlers.
Create a new `middleware.js` file in the root of your project and populate with the following:
```jsx title="middleware.js"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
export async function middleware(req) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient({ req, res })
await supabase.auth.getSession()
return res
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.ts` in order to use a Supabase client in Server Components or Route Handlers.
Create a new `middleware.ts` file in the root of your project and populate with the following:
```tsx title="middleware.ts"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import type { Database } from '@/lib/database.types'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient<Database>({ req, res })
await supabase.auth.getSession()
return res
}
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createMiddlewareSupabaseClient` to add type support to the Supabase client.
</TabPanel>
</Tabs>
## Supabase Provider
All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `<SupabaseProvider />` and use React Context to create a global Supabase instance.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Create a new file at `/app/supabase-provider.jsx` and populate with the following:
```jsx title="app/supabase-provider.jsx"
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
const Context = createContext(undefined)
export default function SupabaseProvider({ children, session }) {
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase, session }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `/app/supabase-provider.tsx` and populate with the following:
```tsx title="app/supabase-provider.tsx"
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { Session, createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '@/lib/database.types'
type MaybeSession = Session | null
type SupabaseContext = {
supabase: SupabaseClient<Database>
session: MaybeSession
}
const Context = createContext<SupabaseContext | undefined>(undefined)
export default function SupabaseProvider({
children,
session,
}: {
children: React.ReactNode
session: MaybeSession
}) {
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase, session }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
</TabPanel>
</Tabs>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
```jsx title="app/layout.jsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import './globals.css'
import SupabaseProvider from './supabase-provider'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default async function RootLayout({ children }) {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const {
data: { session },
} = await supabase.auth.getSession()
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
```tsx title="app/layout.tsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import './globals.css'
import SupabaseProvider from './supabase-provider'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const {
data: { session },
} = await supabase.auth.getSession()
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
}
```
</TabPanel>
</Tabs>
Now any of our Client Components can use the `useSupabase` hook to ensure they are using the same instance of a Supabase client.
## Creating a Supabase Client
### Client Components
While Server Components are great for data fetching, we still need to use Supabase client-side for [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx) and [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
As mentioned above, it is important that all Client Components share a single instance of the Supabase client. We can use the `useSupabase` hook we created above to ensure this is the case.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/new-post.jsx"
'use client'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```jsx title="app/new-post.tsx"
'use client'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
```
</TabPanel>
</Tabs>
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/supabase-provider.tsx) for making the user's session available to all Client Components.
### Server Components
In order to use Supabase in Server Components, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/page.jsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="app/page.tsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
</TabPanel>
</Tabs>
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx) for redirecting unauthenticated users - protected pages.
### Route Handlers
In order to use Supabase in Route Handlers, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/api/posts/route.jsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="app/api/posts/route.tsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
</Tabs>
> Check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for a full example including [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx), [realtime](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) and [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
File diff suppressed because it is too large. Load diff
@@ -441,7 +441,7 @@ useEffect(() => {
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, supabase, fetcher])
}, [serverAccessToken, supabase, revalidate])
```
</TabPanel>
@@ -471,7 +471,7 @@ useEffect(() => {
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, supabase, fetcher])
}, [serverAccessToken, supabase, revalidate])
```
</TabPanel>
+5
View File
@@ -113,6 +113,11 @@ export const examples = [
description: `Request Routing with Oak server middleware.`,
href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server',
},
{
name: 'Hugging Face',
description: `Access 100,000+ Machine Learning models.`,
href: '/guides/functions/examples/huggingface-image-captioning',
},
{
name: 'OpenAI',
description: `Using OpenAI in Edge Functions.`,
@@ -0,0 +1,104 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Hugging Face',
description:
'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.',
video: 'https://www.youtube.com/v/OgnYxRkxEUw',
tocVideo: 'OgnYxRkxEUw',
}
[Hugging Face](https://huggingface.co/) is the collaboration platform for the machine learning community.
[Huggingface.js](https://huggingface.co/docs/huggingface.js/index) provides a convenient way to make calls to 100,000+ Machine Learning models, making it easy to incorporate AI functionality into your [Supabase Edge Functions](https://supabase.com/edge-functions).
Putting this together with [Supabase Storage](https://supabase.com/storage) and [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) we can easily put together a service that automatically generates captions for any image we upload to a storage bucket.
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/OgnYxRkxEUw"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
## Setup
- Open your Supabase project dashboard or [create a new project](https://app.supabase.com/projects).
- [Create a new bucket](https://app.supabase.com/project/_/storage/buckets) called `images`.
- Generate TypeScript types from remote Database.
- Create a new Database table called `image_caption`.
- Create `id` column of type `uuid` which references `storage.objects.id`.
- Create a `caption` column of type `text`.
- Regenerate TypeScript types to include new `image_caption` table.
- Deploy the function to Supabase: `supabase functions deploy huggingface-image-captioning`.
- Create the Database Webhook in the [Supabase Dashboard](https://app.supabase.com/project/_/database/hooks) to trigger the `huggingface-image-captioning` function anytime a record is added to the `storage.objects` table.
## Generate TypeScript Types
To generate the types.ts file for the storage and public schemas, run the following command in the terminal:
```bash
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
```
## Code
Find the complete code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/huggingface-image-captioning).
```ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1'
import { Database } from './types.ts'
console.log('Hello from `huggingface-image-captioning` function!')
const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))
type SoRecord = Database['storage']['Tables']['objects']['Row']
interface WebhookPayload {
type: 'INSERT' | 'UPDATE' | 'DELETE'
table: string
record: SoRecord
schema: 'public'
old_record: null | SoRecord
}
serve(async (req) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
const supabaseAdminClient = createClient<Database>(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Construct image url from storage
const { data, error } = await supabaseAdminClient.storage
.from(soRecord.bucket_id!)
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
if (error) throw error
const { signedUrl } = data
// Run image captioning with Huggingface
const imgDesc = await hf.imageToText({
data: await (await fetch(signedUrl)).blob(),
model: 'nlpconnect/vit-gpt2-image-captioning',
})
// Store image caption in Database table
await supabaseAdminClient
.from('image_caption')
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
.throwOnError()
return new Response('ok')
})
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,53 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'forestadmin',
title: 'ForestAdmin',
description:
'Get started with Supabase and Forest Admin, a tool for automatically generating an Admin Panel without having to build it.',
}
This guide outlines how to instantly generate an Admin Panel on top of your Supabase backend.
[Forest Admin](https://www.forestadmin.com/) offers an off-the-shelf Admin Panel system that can reduce the amount of time and effort needed to create, maintain, and manage internal tools.
It automatically builds a backend API and provides a user-friendly interface to Create, Read, Update and Delete, Search, Segment your data, trigger custom actions, control permissions, and set up workflows on top of your app's data.
![forest-admin-collections](/docs/img/guides/integrations/forestadmin/forest-admin-collections.png)
If you don’t have a Forest Admin account, you can create one in a few minutes [here](https://app.forestadmin.com/signup).
Let’s get started!
## Step 1: Configure your Supabase Backend
If you already have a Supabase project set up, simply go to the Project Settings / Database tab to access the Database Settings and retrieve the connection string (URI tab). This is the only information you will need to connect your Supabase account to Forest Admin.
![supabase-db-credentials](/docs/img/guides/integrations/forestadmin/supabase-db-credentials.png)
If you don't have anything set up on Supabase yet, you can [create a Project](https://app.supabase.com/new/_) in just a few seconds. Once done, just go to the Database tab and create your first table.
![supabase-db-create-table](/docs/img/guides/integrations/forestadmin/supabase-db-create-table.png)
## Step 2: Connect the database to Forest Admin
First, you have to create a new project on Forest Admin:
![forestadmin-create-project](/docs/img/guides/integrations/forestadmin/forestadmin-create-project.png)
Then, you can use Forest Admin's Instant Setup for the Cloud mode to quickly get started. Alternatively, you can host the generated backend admin API on your own (Advanced setup), giving you full control of the backend code. In this guide, we will use the Cloud mode and the Instant Setup.
![forestadmin-hosting](/docs/img/guides/integrations/forestadmin/forestadmin-hosting.png)
Finally, you can enter the database credentials you obtained in Step 1 and set them in Forest Admin by using the Connection URI mode. Don't forget to replace the password in the connection string with the database password you set in Supabase. Note that if you forget it, you can always go to your Supabase Database settings and reset your database password.
![forestadmin-db-credentials](/docs/img/guides/integrations/forestadmin/forestadmin-db-credentials.png)
## You're all done!
There it is, the configuration of Forest Admin is now complete and your admin panel is now ready-to-use with all the features of an admin panel provided out of the box. You can now browse or manipulate all your data in a structured way, use search with support for complex filters, build dashboards, invite your team mates and start collaborating around your business operations and much more.
## Resources
- [Forest Admin](https://www.forestadmin.com/) official website.
- [Forest Admin GitHub](https://github.com/ForestAdmin).
- [Forest Admin](https://docs.forestadmin.com/documentation-portal/) documentation.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -34,9 +34,6 @@ Follow along in the video below as the founder of Snaplet, Peter Pistorius, take
> To create a read-only role across all schemas you can checkout the Snaplet [docs](https://docs.snaplet.dev/guides/postgresql/#create-a-read-only-role)
4. **Superuser access** for the development project. This can be done in Supabase via `Organization > SQL Editor > + New Query` and running this statement:
`ALTER USER postgres WITH superuser;`
## Step 2: Copying your production database
### 2.1. Connect your data source
@@ -84,23 +81,14 @@ Your data target is where you want Snaplet to restore the captured snapshot of y
> Remember the password you use when creating the project. You’ll need this password to connect your database to Snaplet later.
### 3.2. Make your `postgres` user a superuser
Snaplet requires the ability to drop the database schemas whilst restoring a snapshot. In order to do that, Snaplet requires `superuser` privileges.
1. Navigate to the `SQL Editor` in your Supabase console
2. Click on `new query`
3. Paste `alter user postgres with superuser;` into the SQL editor
4. Run the query
### 3.3. Install the Snaplet CLI
### 3.2. Install the Snaplet CLI
1. Open your terminal and run `curl -sL https://app.snaplet.dev/get-cli/ | bash`
2. Run `snaplet auth`
3. Navigate to [https://app.snaplet.dev/access-token/cli](https://app.snaplet.dev/access-token/cli) to get your access token
4. Paste your access token in the terminal
### 3.4. Restore to the data target
### 3.3. Restore to the data target
You're now ready to restore your production snapshot into your Supabase development project.
+1
View File
@@ -16,6 +16,7 @@ Chase Granberry
Chris Copplestone
Dave Wilson
Div Arora
Divit D
Egor Romanov
Fabrizio Fenoglio
Francesco Sansalvadore
Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+2 -2
View File
@@ -835,13 +835,13 @@
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components</loc>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs</loc>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs-pages</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
+1 -1
View File
@@ -25,7 +25,7 @@
}
body {
@apply bg-scale-200 dark:bg-[#1c1c1c] #{!important};
@apply bg-scale-200;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+24 -1
View File
@@ -1 +1,24 @@
module.exports = require('config/tailwind.config')
const config = require('config/tailwind.config')
module.exports = config({
content: [
'./../../packages/common/**/*.{ts,tsx}',
'./../../packages/ui/**/*.{tsx,ts,js}',
'./pages/**/*.{tsx,mdx}',
'./components/**/*.tsx',
'./layouts/**/*.tsx',
'./src/**/*.{ts,tsx,mdx}',
'./docs/**/*.{tsx,mdx}',
],
plugins: [
function ({ addUtilities, addVariant }) {
addUtilities({
// prose (tailwind typography) helpers
// useful for removing margins in prose styled sections
'.prose--remove-p-margin p': {
margin: '0',
},
})
},
],
})
@@ -105,14 +105,14 @@ Today the Community are releasing both the Flutter and Dart libraries in Beta (w
- Check out the [Flutter Quickstart Guide](/docs/guides/with-flutter)
- Check out the code:
- Supabase Flutter: [GitHub](https://github.com/supabase/supabase-flutter/) | [Release](https://pub.dev/packages/supabase_flutter)
- Supabase Dart: [GitHub](https://github.com/supabase/supabase-dart/) | [Release](https://pub.dev/packages/supabase)
- Supabase Flutter: [GitHub](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Release](https://pub.dev/packages/supabase_flutter)
- Supabase Dart: [GitHub](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase) | [Release](https://pub.dev/packages/supabase)
### Get involved
- Help [write](/supasquad#author) the Flutter Docs.
- Help [maintain](/supasquad#maintainer) the [Flutter](https://github.com/supabase/supabase-flutter/)
and [Dart](https://github.com/supabase/supabase-dart/) libraries.
- Help [maintain](/supasquad#maintainer) the [Flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter)
and [Dart](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase) libraries.
## Supabase Discord
@@ -25,7 +25,7 @@ Resumable uploads divides the file into chunks before uploading them, emitting p
/>
</video>
With this release, you can now upload files as large as 50GB! (Previously the limit was 5GB).
With this release, users on the Pro tier or higher can now upload files as large as 50GB! This substantial upgrade from the previous limit of 5GB offers even more flexibility for your file uploads.
To build this feature, we implemented Postgres Advisory locks which solved some gnarly concurrency problems. We can now handle edge-cases, like two clients uploading to the same location. We’ll deep dive into how we implemented Advisory locks later in the post.
@@ -0,0 +1,268 @@
---
title: 'ChatGPT plugins now support Postgres & Supabase'
description: Supabase recently contributed to the OpenAI Retrieval Plugin repo with a Postgres and a Supabase implementation to help developers build ChatGPT plugins using pgvector.
tags:
- AI
- tutorial
- postgres
date: '2023-05-25'
toc_depth: 2
author: egor_romanov,paul_copplestone
image: 2023-05-25-chatgpt-plugins-support-postgres/chatgpt-plugins-support-postgres.jpeg
thumb: 2023-05-25-chatgpt-plugins-support-postgres/chatgpt-plugins-support-postgres.jpeg
---
One of the challenges that ChatGPT faces is being able to answer questions from a private dataset. We can solve this with “retrieval plugins”, which allow ChatGPT to access information from a database.
Supabase recently contributed to the OpenAI repo with a [Postgres](https://github.com/openai/chatgpt-retrieval-plugin#postgres) and a [Supabase](https://github.com/openai/chatgpt-retrieval-plugin#supabase) implementation to help developers build plugins using pgvector.
Let’s dig into the specifics of Retrieval plugins, then we can implement an example - we’ll ingest all of the Postgres docs into a Supabase database, then get ChatGPT to answer questions. It’s a contrived example since ChatGPT already knows about Postgres, but what other data source would Supabase want to use?
## What is ChatGPT Retrieval Plugin?
ChatGPT recently released [Plugins](https://openai.com/blog/chatgpt-plugins) which help ChatGPT access up-to-date information, run computations, or use third-party services.
A [Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) is a Python project designed to inject external data into the ChatGPT. It allows ChatGPT to dynamically pull relevant information into conversations from your data sources. This could be PDF documents, Confluence, or Notion knowledge bases.
A retrieval plugin does a few things:
1. Turn documents into smaller chunks.
2. Converts chunks into embeddings using OpenAI's `text-embedding-ada-002` model.
3. Stores the embeddings into a vector database.
4. Queries the vector database for relevant documents when a question is asked.
You can choose your preferred vector database provider from a list of [supported options](https://github.com/openai/chatgpt-retrieval-plugin#choosing-a-vector-database).
## Adding Supabase and Postgres as Datastore options for ChatGPT Retrieval Plugin
We've implemented two vector provider options: one for Postgres and one for Supabase. The main differences are:
- The Postgres version uses the `psycopg2` python library to directly connect to the database.
- The Supabase version interacts with the database via PostgREST. This is helpful if you want to use [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) or if you are planning to use the data in the Retrieval store beyond ChatGPT.
The Postgres implementation is great to start with because there are now a [large number of providers](https://github.com/pgvector/pgvector/issues/54) supporting pgvector.
Both have the same schema so you can easily switch between them:
```sql
create table if not exists documents (
id text primary key default gen_random_uuid()::text,
source text,
source_id text,
content text,
document_id text,
author text,
url text,
created_at timestamptz default now(),
embedding vector(1536)
);
```
When you create the retrieval store inside your database, a stored function is implemented to query and find relevant information for your question to ChatGPT:
```sql
create or replace function match_page_sections(
in_embedding vector(1536),
in_match_count int default 3,
in_document_id text default '%%',
in_source_id text default '%%',
in_source text default '%%',
in_author text default '%%',
in_start_date timestamptz default '-infinity',
in_end_date timestamptz default 'infinity'
)
returns table (
id text,
source text,
source_id text,
document_id text,
url text,
created_at timestamptz,
author text,
content text,
embedding vector(1536),
similarity float
)
language plpgsql
as $$
#variable_conflict use_variable
begin
return query
select
documents.id,
documents.source,
documents.source_id,
documents.document_id,
documents.url,
documents.created_at,
documents.author,
documents.content,
documents.embedding,
(documents.embedding <#> in_embedding) * -1 as similarity
from
documents
where
in_start_date <= documents.created_at and
documents.created_at <= in_end_date and
(documents.source_id like in_source_id or documents.source_id is null) and
(documents.source like in_source or documents.source is null) and
(documents.author like in_author or documents.author is null) and
(documents.document_id like in_document_id or documents.document_id is null)
order by
documents.embedding <#> in_embedding
limit
in_match_count;
end;
$$;
```
We apply filters based on the source, author, document, and date, and find the closest embeddings using the inner product distance function. This function offers the best performance when the embeddings are normalized, which is the case for OpenAI embeddings. The similarity is calculated as: `(documents.embedding <#> in_embedding) * -1 as similarity`. And that’s it, you can now seamlessly use the Retrieval Plugin with a Postgres Database underneath, eliminating the need for any manual implementation on your end.
## Example: Chat with Postgres Docs
Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation.
This will require several steps:
1. Download all the [Postgres docs as a PDF](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf)
2. Convert the docs into chunks of embedded text and store them in Supabase
3. Run our plugin locally so that we can ask questions about the Postgres docs.
<img
className="dark:hidden !m-0"
alt="diagram reference"
src="/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-plugin-scheme--light.png"
/>
<img
className="hidden dark:block !m-0"
alt="diagram reference"
src="/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-plugin-scheme--dark.png"
/>
### Step 1: Fork the ChatGPT Retrieval Plugin repository
Fork the ChatGPT Retrieval Plugin repository to your GitHub account and clone it to your local machine. Read through the `README.md` file to understand the project structure.
### Step 2: Install dependencies
Choose your desired datastore provider and remove unused dependencies from `pyproject.toml`. For this example, we'll use Supabase. And install dependencies with Poetry:
```bash
poetry install
```
### Step 3: Create a Supabase project
Create a [Supabase project](https://supabase.com/dashboard) and database by following the instructions [here](https://supabase.com/docs/guides/platform). Export the environment variables required for the retrieval plugin to work:
```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=supabase
export SUPABASE_URL=<supabase_url>
export SUPABASE_SERVICE_ROLE_KEY=<supabase_key>
```
For Postgres datastore, you'll need to export these environment variables instead:
```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=postgres
export PG_HOST=<postgres_host_url>
export PG_PASSWORD=<postgres_password>
```
### Step 4: Run Postgres Locally
To start quicker you may use Supabase CLI to spin everything up locally as it already includes pgvector from the start. Install `supabase-cli`, go to the `examples/providers` folder in the repo and run:
```bash
supabase start
```
This will pull all docker images and run supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
Using `supabase-cli` is not required and you can use any other docker image or hosted version of PostgresDB that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
### Step 5: Obtain OpenAI API key
To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, you just need to go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.
![OpenAI Secret Keys](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/openai-secret-keys.png)
### Step 6: Run the plugin!
Execute the following command to run the plugin:
```bash
poetry run dev
# output
INFO: Will watch for changes in these directories: ['./chatgpt-retrieval-plugin']
INFO: Uvicorn running on http://localhost:3333 (Press CTRL+C to quit)
INFO: Started reloader process [87843] using WatchFiles
INFO: Started server process [87849]
INFO: Waiting for application startup.
INFO: Application startup complete.
```
The plugin will start on your localhost - port :3333 by default.
### Step 6: Populating data in the datastore
For this example, we'll upload Postgres documentation to the datastore. Download the [Postgres documentation](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf) and use the `/upsert-file` endpoint to upload it:
```bash
curl -X POST -F \\"file=@./postgresql-15-US.pdf\\" <http://localhost:3333/upsert-file>
```
The plugin will split your data and documents into smaller chunks automatically. You can view the chunks using the Supabase dashboard or any other SQL client you prefer. For the whole Postgres Documentation I got 7,904 records in my documents table, which is not a lot, but we can try to add index for `embedding` column to speed things up by a little. To do so, you should run the following SQL command:
```sql
create index on documents
using ivfflat (embedding vector_ip_ops)
with (lists = 10);
```
This will create an index for the inner product distance function. Important to note that it is an approximate index. It will change the logic from performing the exact nearest neighbor search to the approximate nearest neighbor search.
We are using `lists = 10`, because as a general guideline, you should start looking for optimal lists constant value with the formula: `rows / 1000` when you have less than 1 million records in your table.
Now, it is time to add our plugin to ChatGPT.
### Empowering ChatGPT with Postgres knowledge
To integrate our plugin with ChatGPT, register it in the ChatGPT dashboard. Assuming you have access to ChatGPT Plugins and plugin development, select the Plugins model in a new chat, then choose "Plugin store" and "Develop your own plugin." Enter `localhost:3333` into the domain input, and your plugin is now part of ChatGPT.
![ChatGPT Change Model](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-change-model.png)
![ChatGPT Plugin Store](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-plugin-store.png)
![ChatGPT Local Plugin](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-local-plugin.png)
You can now ask questions about Postgres and receive answers derived from the documentation!
Let's try it out: ask ChatGPT to find out when to use `check` and when to use `using`. You will be able to see what queries were sent to our plugin and what it responded to.
![Ask ChatGPT](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/ask-chatgpt.png)
![ChatGPT Query](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-query.png)
And after ChatGPT receives a response from the plugin it will answer your question with the data from the documentation.
![ChatGPT Reply](/images/blog/2023-05-25-chatgpt-plugins-support-postgres/chatgpt-reply.png)
## Wrap up
It's easy to bring any context into the datastore and utilize it with ChatGPT. Simply export your knowledge base from platforms like Notion or Confluence, upload it to the datastore, and you're good to go. You can also use any other datastore provider you prefer.
And the good news is that you’re not limited by using it with ChatGPT, you can embed it in your website or documentation, and build a Slack bot or telegram bot to answer questions about your company or product. For that, you will only need to add a single call to OpenAI API to create a summary of data retrieved from the Plugin. You can find some inspiration on how to do that in our blog post about building [Supabase Clippy assistant](https://supabase.com/blog/chatgpt-supabase-docs).
Let us know on [Twitter](https://twitter.com/Supabase) if you are building ChatGPT Plugins. We can’t wait to see what you will build!
## More AI resources
- [OpenAI ChatGPT Plugin docs](https://platform.openai.com/docs/plugins/introduction)
- [ChatGPT Retrieval Plugin Repo](https://github.com/openai/chatgpt-retrieval-plugin)
- [How to build ChatGPT Plugin from scratch with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template).
- [Docs pgvector: Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
+1 -1
View File
@@ -55,7 +55,7 @@ Markprompt were also impressed with the ease of use and scalability of Supabase.
## What They Built
Markprompt is a set of API endpoints that allow you to train your content and create a prompt to ask questions to it, for instance for a docs site. Markprompt also designed a user-friendly web dashboard that simplifies content training, synchronization with GitHub repos or websites, access key management, and provides insights into user query statistics. Furthermore, Markprompt's UI components, available as React and Web Components, allow integration of prompts at all touch points of a company, from public websites websites to internal knowledge bases with just a few lines of code.
Markprompt is a set of API endpoints that allow you to train your content and create a prompt to ask questions to it, for instance for a docs site. Markprompt also designed a user-friendly web dashboard that simplifies content training, synchronization with GitHub repos or websites, access key management, and provides insights into user query statistics. Furthermore, Markprompt's UI components, available as React and Web Components, allow integration of prompts at all touch points of a company, from public websites to internal knowledge bases with just a few lines of code.
Like Supabase, Markprompt is an open-source platform, allowing customers the freedom to host the dashboard and model backend on their own premises. Markprompt warmly welcomes contributions from the community, fostering collaboration and continuous improvement of the platform to meet the evolving needs of developers and technical users.
+1 -1
View File
@@ -16,7 +16,7 @@ const CTABanner = (props: any) => {
</h2>
</div>
<div className="col-span-12 mt-4">
<a href="https://app.supabase.com/">
<a href="https://supabase.com/dashboard">
<Button size="medium" className="text-white">
Start your project
</Button>
+5 -3
View File
@@ -5,6 +5,7 @@ import ReactTooltip from 'react-tooltip'
import Telemetry from '~/lib/telemetry'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import { useRouter } from 'next/router'
import { useBreakpoint } from 'common'
const frameworks = [
{
@@ -54,6 +55,7 @@ const frameworks = [
const HeroFrameworks = ({ className }: { className?: string }) => {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const isXs = useBreakpoint(640)
const sendTelemetryEvent = async (gaEvent: string) => {
return await Telemetry.sendEvent(
@@ -71,7 +73,7 @@ const HeroFrameworks = ({ className }: { className?: string }) => {
<div className={['flex text-center flex-col items-center', className].join(' ')}>
<small className="small !text-scale-1100">Works seamlessly with 20+ frameworks</small>
<div className="w-full sm:max-w-lg mt-4 md:mt-3 lg:ml-0">
<div className="flex flex-wrap items-center justify-center gap-2 sm:flex-nowrap">
<div className="flex flex-wrap items-center justify-center gap-1 xs:gap-2 sm:flex-nowrap">
{frameworks.map((framework) => (
<Link href={framework.docs} key={framework.name}>
<a
@@ -85,8 +87,8 @@ const HeroFrameworks = ({ className }: { className?: string }) => {
className="opacity-100 hover:opacity-80 transition-opacity"
src={framework.icon}
alt={framework.name}
width={45}
height={45}
width={isXs ? 35 : 45}
height={isXs ? 35 : 45}
/>
</a>
</Link>
+3 -3
View File
@@ -331,14 +331,14 @@ const Nav = () => {
</Link>
) : (
<>
<Link href="https://app.supabase.com/">
<Link href="https://supabase.com/dashboard">
<a>
<Button type="default" className="hidden lg:block">
Sign in
</Button>
</a>
</Link>
<Link href="https://app.supabase.com/">
<Link href="https://supabase.com/dashboard">
<a>
<Button className="hidden text-white lg:block">Start your project</Button>
</a>
@@ -395,7 +395,7 @@ const Nav = () => {
{/* </div> */}
<div className="mt-6 mb-12">
<div className="pt-2 pb-4 space-y-1">
<Link href="https://app.supabase.com/">
<Link href="https://supabase.com/dashboard">
<a className="block pl-3 pr-4 text-base font-medium text-scale-900 dark:text-white">
Sign in
</a>
@@ -43,7 +43,7 @@ const ProductHeader = (props: Types) => (
})}
</div>
<div className="flex flex-row md:flex-row md:items-center">
<Link href="https://app.supabase.com/" as="https://app.supabase.com/">
<Link href="https://supabase.com/dashboard" as="https://supabase.com/dashboard">
<a>
<Button size="medium" className="text-white">
Start a project
+7
View File
@@ -328,5 +328,12 @@
"position": "Product Management Leader @ AWS",
"author_url": "https://github.com/daltjoh-aws",
"author_image_url": "https://github.com/daltjoh-aws.png"
},
{
"author_id": "egor_romanov",
"author": "Egor Romanov",
"position": "Engineering",
"author_url": "https://github.com/egor-romanov",
"author_image_url": "https://github.com/egor-romanov.png"
}
]
+10
View File
@@ -1971,4 +1971,14 @@ module.exports = [
source: '/blog/how-supabase-accelerates-development-of-all-pull-together',
destination: '/customers',
},
{
permanent: true,
source: '/blog/how-supabase-accelerates-development-of-all-pull-together',
destination: '/customers',
},
{
permanent: true,
source: '/docs/guides/auth/auth-helpers/nextjs-server-components',
destination: '/docs/guides/auth/auth-helpers/nextjs',
},
]
+5
View File
@@ -1,3 +1,8 @@
import '../../../packages/ui/build/css/themes/light.css'
import '../../../packages/ui/build/css/themes/dark.css'
import '../styles/index.css'
import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants'
import { DefaultSeo } from 'next-seo'
import { AppProps } from 'next/app'
+4 -4
View File
@@ -98,7 +98,7 @@ function CaseStudyPage(props: any) {
"
>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 mb-2 lg:col-span-2">
<div className="col-span-12 mb-2 xl:col-span-2">
{/* Back button */}
<p>
<a
@@ -113,7 +113,7 @@ function CaseStudyPage(props: any) {
<div
className="col-span-12 lg:col-span-8
"
>
<div className="">
@@ -146,8 +146,8 @@ function CaseStudyPage(props: any) {
objectPosition="left"
className="
bg-no-repeat
dark:brightness-200
dark:brightness-200
dark:contrast-0
dark:filter
"
+13 -7
View File
@@ -56,7 +56,7 @@ export default function IndexPage() {
name: 'Free',
nameBadge: '',
costUnit: 'per month per project',
href: 'https://app.supabase.com/new/new-project',
href: 'https://supabase.com/dashboard/new/new-project',
priceLabel: 'Starting from',
priceMonthly: 0,
warning: 'Limit of 2 free projects',
@@ -82,7 +82,7 @@ export default function IndexPage() {
name: 'Pro',
nameBadge: '',
costUnit: 'per month per project',
href: 'https://app.supabase.com/new/new-project',
href: 'https://supabase.com/dashboard/new/new-project',
from: true,
priceLabel: 'Starting from',
warning: '+ usage',
@@ -201,7 +201,7 @@ export default function IndexPage() {
<p className="p">{priceDescription}</p>
</div>
<p className="p">{description}</p>
<Link href="https://app.supabase.com" passHref>
<Link href="https://supabase.com/dashboard" passHref>
<a>
<Button size="medium" block>
Get started
@@ -849,7 +849,10 @@ export default function IndexPage() {
</th>
<td className="px-6 pt-5">
<Link href="https://app.supabase.com" as="https://app.supabase.com">
<Link
href="https://supabase.com/dashboard"
as="https://supabase.com/dashboard"
>
<a>
<Button size="tiny" type="primary" block>
Get started
@@ -859,7 +862,10 @@ export default function IndexPage() {
</td>
<td className="px-6 pt-5">
<Link href="https://app.supabase.com" as="https://app.supabase.com">
<Link
href="https://supabase.com/dashboard"
as="https://supabase.com/dashboard"
>
<a>
<Button size="tiny" type="primary" block>
Get started
@@ -916,7 +922,7 @@ export default function IndexPage() {
Can&apos;t find the answer to your question, you can{' '}
<a
target="_blank"
href="https://app.supabase.com/support/new"
href="https://supabase.com/dashboard/support/new"
className="transition text-brand-900 hover:text-brand-1000"
>
open a support ticket
@@ -927,7 +933,7 @@ export default function IndexPage() {
For enterprise enquries,{' '}
<a
target="_blank"
href="https://app.supabase.com/support/new"
href="https://supabase.com/dashboard/support/new"
className="transition text-brand-900 hover:text-brand-1000"
>
you can contact the team here
+1 -1
View File
@@ -199,7 +199,7 @@ const Index = ({}: Props) => {
border-gray-100 bg-white p-5
pt-14 dark:border-gray-600"
>
<a href="https://app.supabase.com/support/new">
<a href="https://supabase.com/dashboard/support/new">
<Button size="medium" type="default" iconRight={<IconMail />}>
Email Support
</Button>
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

+27 -1
View File
@@ -1 +1,27 @@
module.exports = require('config/tailwind.config')
const config = require('config/tailwind.config')
module.exports = config({
content: [
'../../packages/common/**/*.{ts,tsx}',
'../../packages/ui/**/*.{tsx,ts,js}',
'./pages/**/*.{tsx,mdx}',
'./components/**/*.tsx',
'./layouts/**/*.tsx',
'./src/**/*.{ts,tsx,mdx}',
'./_blog/*.mdx',
],
theme: {
extend: {
keyframes: {
'flash-code': {
'0%': { backgroundColor: 'rgba(63, 207, 142, 0.1)' },
'100%': { backgroundColor: 'transparent' },
},
},
animation: {
'flash-code': 'flash-code 1s forwards',
'flash-code-slow': 'flash-code 2s forwards',
},
},
},
})
+1 -1
View File
@@ -247,7 +247,7 @@ services:
restart: unless-stopped
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000"
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@{POSTGRES_DB}:${POSTGRES_PORT}/${POSTGRES_DB}"
+1 -1
View File
@@ -1,4 +1,4 @@
# Get these from your API settings: https://app.supabase.com/project/_/settings/api
# Get these from your API settings: https://supabase.com/dashboard/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
+22 -23
View File
@@ -1,30 +1,29 @@
import { Database } from "@/lib/database.types";
import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { notFound } from "next/navigation";
import RealtimePost from "./realtime-post";
import { headers, cookies } from "next/headers";
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { notFound, redirect } from 'next/navigation'
import RealtimePost from './realtime-post'
import { cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0;
import type { Database } from '@/lib/database.types'
export default async function Post({
params: { id },
}: {
params: { id: string };
}) {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
export default async function Post({ params: { id } }: { params: { id: string } }) {
const supabase = createServerComponentClient<Database>({
cookies,
});
const { data: post } = await supabase
.from("posts")
.select()
.match({ id })
.single();
})
if (!post) {
notFound();
const {
data: { session },
} = await supabase.auth.getSession()
if (!session) {
// this is a protected route - only users who are signed in can view this route
redirect('/')
}
return <RealtimePost serverPost={post} />;
const { data: post } = await supabase.from('posts').select().match({ id }).single()
if (!post) {
notFound()
}
return <RealtimePost serverPost={post} />
}
+21 -20
View File
@@ -1,40 +1,41 @@
"use client";
'use client'
import { Database } from "@/lib/database.types";
import { useEffect, useState } from "react";
import { useSupabase } from "../supabase-provider";
import { useEffect, useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
type Post = Database["public"]["Tables"]["posts"]["Row"];
import type { Database } from '@/lib/database.types'
type Post = Database['public']['Tables']['posts']['Row']
export default function RealtimePost({ serverPost }: { serverPost: Post }) {
const { supabase } = useSupabase();
const [post, setPost] = useState(serverPost);
const supabase = createClientComponentClient<Database>()
const [post, setPost] = useState(serverPost)
useEffect(() => {
setPost(serverPost);
}, [serverPost]);
setPost(serverPost)
}, [serverPost])
useEffect(() => {
const channel = supabase
.channel("realtime post")
.channel('realtime post')
.on(
"postgres_changes",
'postgres_changes',
{
event: "UPDATE",
schema: "public",
table: "posts",
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: `id=eq.${post.id}`,
},
(payload) => {
setPost(payload.new as Post);
setPost(payload.new as Post)
}
)
.subscribe();
.subscribe()
return () => {
supabase.removeChannel(channel);
};
}, [supabase, post, setPost]);
supabase.removeChannel(channel)
}
}, [supabase, post, setPost])
return <pre>{JSON.stringify(post, null, 2)}</pre>;
return <pre>{JSON.stringify(post, null, 2)}</pre>
}
@@ -1,35 +0,0 @@
import { createRouteHandlerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import { headers, cookies } from "next/headers";
import type { Database } from "@/lib/database.types";
export async function POST(request: Request) {
const { content } = await request.json();
const supabase = createRouteHandlerSupabaseClient<Database>({
headers,
cookies,
});
const { data } = await supabase.from("posts").insert({ content }).select();
return NextResponse.json(data);
}
export async function PUT(request: Request) {
const { id, content } = await request.json();
const supabase = createRouteHandlerSupabaseClient<Database>({
headers,
cookies,
});
const { data } = await supabase
.from("posts")
.update({ content })
.match({ id })
.select();
return NextResponse.json(data);
}
@@ -0,0 +1,18 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import type { Database } from '@/lib/database.types'
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createRouteHandlerClient<Database>({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(requestUrl.origin)
}
+10 -12
View File
@@ -1,21 +1,19 @@
import "./globals.css";
import SupabaseProvider from "./supabase-provider";
import './globals.css'
import Login from './login'
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SupabaseProvider>{children}</SupabaseProvider>
{/* @ts-expect-error next version of TS will fix this */}
<Login />
{children}
</body>
</html>
);
)
}
+56
View File
@@ -0,0 +1,56 @@
'use client'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import type { Session } from '@supabase/auth-helpers-nextjs'
export default function LoginForm({ session }: { session: Session | null }) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const supabase = createClientComponentClient()
const handleSignUp = async () => {
await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
})
router.refresh()
}
const handleSignIn = async () => {
await supabase.auth.signInWithPassword({
email,
password,
})
router.refresh()
}
const handleSignOut = async () => {
await supabase.auth.signOut()
router.refresh()
}
// for the `session` to be available on first SSR render, it must be
// fetched in a Server Component and passed down as a prop
return session ? (
<button onClick={handleSignOut}>Sign out</button>
) : (
<>
<input name="email" onChange={(e) => setEmail(e.target.value)} value={email} />
<input
type="password"
name="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<button onClick={handleSignUp}>Sign up</button>
<button onClick={handleSignIn}>Sign in</button>
</>
)
}
+10 -28
View File
@@ -1,33 +1,15 @@
"use client";
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import LoginForm from './login-form'
import { useSupabase } from "./supabase-provider";
import type { Database } from '@/lib/database.types'
export default function Login() {
const { supabase } = useSupabase();
export default async function Login() {
const supabase = createServerComponentClient<Database>({ cookies })
const handleSignUp = async () => {
await supabase.auth.signUp({
email: "jon@supabase.com",
password: "sup3rs3cur3",
});
};
const {
data: { session },
} = await supabase.auth.getSession()
const handleLogin = async () => {
await supabase.auth.signInWithPassword({
email: "jon@supabase.com",
password: "sup3rs3cur3",
});
};
const handleLogout = async () => {
await supabase.auth.signOut();
};
return (
<div>
<button onClick={handleSignUp}>Sign Up</button>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
return <LoginForm session={session} />
}
+13 -14
View File
@@ -1,22 +1,21 @@
"use client";
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { FormEvent } from "react";
import type { Database } from '@/lib/database.types'
export default function NewPost() {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { content } = Object.fromEntries(new FormData(e.currentTarget));
await fetch("http://localhost:3000/api/posts", {
method: "post",
body: JSON.stringify({ content }),
});
};
const addPost = async (formData: FormData) => {
'use server'
const content = String(formData.get('content'))
const supabase = createServerActionClient<Database>({ cookies })
await supabase.from('posts').insert({ content }).select()
revalidatePath('/')
}
return (
<form onSubmit={handleSubmit}>
<form action={addPost}>
<input name="content" />
</form>
);
)
}
+9 -15
View File
@@ -1,26 +1,20 @@
import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { headers, cookies } from "next/headers";
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import RealtimePosts from './realtime-posts'
import NewPost from './new-post'
import type { Database } from "@/lib/database.types";
import RealtimePosts from "./realtime-posts";
import Login from "./login";
import NewPost from "./new-post";
// do not cache this page
export const revalidate = 0;
import type { Database } from '@/lib/database.types'
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
const supabase = createServerComponentClient<Database>({
cookies,
});
const { data } = await supabase.from("posts").select("*");
})
const { data } = await supabase.from('posts').select('*')
return (
<>
<Login />
<NewPost />
<RealtimePosts serverPosts={data ?? []} />
</>
);
)
}
+27 -25
View File
@@ -1,38 +1,40 @@
"use client";
'use client'
import { useEffect, useState } from "react";
import { useSupabase } from "./supabase-provider";
import { useEffect, useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from "@/lib/database.types";
import type { Database } from '@/lib/database.types'
import Link from 'next/link'
type Post = Database['public']['Tables']['posts']['Row']
type Post = Database["public"]["Tables"]["posts"]["Row"];
export default function RealtimePosts({
serverPosts,
}: {
serverPosts: Post[];
}) {
const [posts, setPosts] = useState(serverPosts);
const { supabase } = useSupabase();
export default function RealtimePosts({ serverPosts }: { serverPosts: Post[] }) {
const [posts, setPosts] = useState(serverPosts)
const supabase = createClientComponentClient<Database>()
useEffect(() => {
setPosts(serverPosts);
}, [serverPosts]);
setPosts(serverPosts)
}, [serverPosts])
useEffect(() => {
const channel = supabase
.channel("*")
.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "posts" },
(payload) => setPosts((posts) => [...posts, payload.new as Post])
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
setPosts((posts) => [...posts, payload.new as Post])
)
.subscribe();
.subscribe()
return () => {
supabase.removeChannel(channel);
};
}, [supabase, setPosts, posts]);
supabase.removeChannel(channel)
}
}, [supabase, setPosts, posts])
return <pre>{JSON.stringify(posts, null, 2)}</pre>;
return (
<>
{posts.map((post) => (
<div key={post.id}>
<Link href={`/${post.id}`}>{post.content}</Link>
</div>
))}
</>
)
}
@@ -1,51 +0,0 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { useRouter } from "next/navigation";
import type { SupabaseClient } from "@supabase/auth-helpers-nextjs";
import type { Database } from "../lib/database.types";
type SupabaseContext = {
supabase: SupabaseClient<Database>;
};
const Context = createContext<SupabaseContext | undefined>(undefined);
export default function SupabaseProvider({
children,
}: {
children: React.ReactNode;
}) {
const [supabase] = useState(() => createBrowserSupabaseClient());
const router = useRouter();
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh();
});
return () => {
subscription.unsubscribe();
};
}, [router, supabase]);
return (
<Context.Provider value={{ supabase }}>
<>{children}</>
</Context.Provider>
);
}
export const useSupabase = () => {
let context = useContext(Context);
if (context === undefined) {
throw new Error("useSupabase must be used inside SupabaseProvider");
}
return context;
};
+8 -17
View File
@@ -1,21 +1,12 @@
import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from "next/server";
import type { Database } from "./lib/database.types";
import type { NextRequest } from 'next/server'
import type { Database } from '@/lib/database.types'
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareSupabaseClient<Database>({ req, res });
const { data: { session } } = await supabase.auth.getSession(); // destructure the data object to obtain the session object
if (session === null) return NextResponse.redirect(new URL("/login", req.nextUrl));
// instead of "/login" you can redirect to any other route in case the user is not logged in
return res;
const res = NextResponse.next()
const supabase = createMiddlewareClient<Database>({ req, res })
await supabase.auth.getSession()
return res
}
export const config = {
matcher: [], // add the routes you wish the middleware to run in. You can also use regex
};
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
serverActions: true,
},
}
+753 -454
View File
File diff suppressed because it is too large. Load diff
+12 -10
View File
@@ -9,15 +9,17 @@
"lint": "next lint"
},
"dependencies": {
"@supabase/auth-helpers-nextjs": "^0.5.6",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"next": "13.2.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
"@supabase/auth-helpers-nextjs": "^0.7.0-next.8",
"@types/node": "^20.2.3",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"eslint": "^8.41.0",
"next": "^13.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.4"
},
"devDependencies": {
"eslint-config-next": "^13.4.3"
}
}
@@ -1,5 +1,5 @@
# these values can be found in your project's API settings
# https://app.supabase.com/project/_/settings/api
# https://supabase.com/dashboard/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
@@ -17,7 +17,7 @@ export default function RealtimePosts({ serverPosts }: { serverPosts: any }) {
useEffect(() => {
// ensure you have enabled replication on the `posts` table
// https://app.supabase.com/project/_/database/replication
// https://supabase.com/dashboard/project/_/database/replication
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
+1 -1
View File
@@ -1,3 +1,3 @@
# Get these from your Dashboard: https://app.supabase.com/project/_/settings/api
# Get these from your Dashboard: https://supabase.com/dashboard/project/_/settings/api
REACT_APP_SUPABASE_URL=
REACT_APP_SUPABASE_ANON_KEY=
@@ -15,7 +15,7 @@ DB_HOSTNAME=
DB_PASSWORD=
DB_SSL_CERT="-----BEGIN CERTIFICATE-----
GET YOUR CERT FROM YOUR PROJECT DASHBOARD
https://app.supabase.com/project/_/settings/database
https://supabase.com/dashboard/project/_/settings/database
-----END CERTIFICATE-----"
# puppeteer
@@ -0,0 +1,29 @@
# Image Captioning with Supabase Storage and Huggingface.js
[Hugging Face](https://huggingface.co/) is the collaboration platform for the machine learning community.
[Huggingface.js](https://huggingface.co/docs/huggingface.js/index) provides a convenient way to make calls to 100,000+ Machine Learning models, making it easy to incorporate AI functionality into your [Supabase Edge Functions](https://supabase.com/edge-functions).
Putting this together with [Supabase Storage](https://supabase.com/storage) and [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) we can easily put together a service that automatically generates captions for any image we upload to a storage bucket.
## Setup
1. Open your Supabase project dashboard or [create a new project](https://app.supabase.com/projects).
2. [Create a new bucket](https://app.supabase.com/project/_/storage/buckets) called `images`.
3. Generate TypeScript types from remote Database.
4. Create a new Database table called `image_caption`.
- Create `id` column of type `uuid` which references `storage.objects.id`.
- Create a `caption` column of type `text`.
5. Regenerate TypeScript types to include new `image_caption` table.
6. Deploy the function to Supabase: `supabase functions deploy huggingface-image-captioning`.
7. Create the Database Webhook in the [Supabase Dashboard](https://app.supabase.com/project/_/database/hooks) to trigger the `huggingface-image-captioning` function anytime a record is added to the `storage.objects` table.
## Generate TypeScript Types
To generate the types.ts file for the storage and public schemas, run the following command in the terminal:
```bash
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
```
@@ -0,0 +1,49 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
import { createClient } from '@supabase/supabase-js'
import { Database } from './types.ts'
console.log('Hello from `huggingface-image-captioning` function!')
const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))
type SoRecord = Database['storage']['Tables']['objects']['Row']
interface WebhookPayload {
type: 'INSERT' | 'UPDATE' | 'DELETE'
table: string
record: SoRecord
schema: 'public'
old_record: null | SoRecord
}
serve(async (req) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
const supabaseAdminClient = createClient<Database>(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Construct image url from storage
const { data, error } = await supabaseAdminClient.storage
.from(soRecord.bucket_id!)
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
if (error) throw error
const { signedUrl } = data
// Run image captioning with Huggingface
const imgDesc = await hf.imageToText({
data: await (await fetch(signedUrl)).blob(),
model: 'nlpconnect/vit-gpt2-image-captioning',
})
// Store image caption in Database table
await supabaseAdminClient
.from('image_caption')
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
.throwOnError()
return new Response('ok')
})
@@ -0,0 +1,202 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json }
| Json[];
export interface Database {
public: {
Tables: {
image_caption: {
Row: {
caption: string;
id: string;
};
Insert: {
caption: string;
id: string;
};
Update: {
caption?: string;
id?: string;
};
};
};
Views: {
[_ in never]: never;
};
Functions: {
[_ in never]: never;
};
Enums: {
[_ in never]: never;
};
CompositeTypes: {
[_ in never]: never;
};
};
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null;
avif_autodetection: boolean | null;
created_at: string | null;
file_size_limit: number | null;
id: string;
name: string;
owner: string | null;
public: boolean | null;
updated_at: string | null;
};
Insert: {
allowed_mime_types?: string[] | null;
avif_autodetection?: boolean | null;
created_at?: string | null;
file_size_limit?: number | null;
id: string;
name: string;
owner?: string | null;
public?: boolean | null;
updated_at?: string | null;
};
Update: {
allowed_mime_types?: string[] | null;
avif_autodetection?: boolean | null;
created_at?: string | null;
file_size_limit?: number | null;
id?: string;
name?: string;
owner?: string | null;
public?: boolean | null;
updated_at?: string | null;
};
};
migrations: {
Row: {
executed_at: string | null;
hash: string;
id: number;
name: string;
};
Insert: {
executed_at?: string | null;
hash: string;
id: number;
name: string;
};
Update: {
executed_at?: string | null;
hash?: string;
id?: number;
name?: string;
};
};
objects: {
Row: {
bucket_id: string | null;
created_at: string | null;
id: string;
last_accessed_at: string | null;
metadata: Json | null;
name: string | null;
owner: string | null;
path_tokens: string[] | null;
updated_at: string | null;
version: string | null;
};
Insert: {
bucket_id?: string | null;
created_at?: string | null;
id?: string;
last_accessed_at?: string | null;
metadata?: Json | null;
name?: string | null;
owner?: string | null;
path_tokens?: string[] | null;
updated_at?: string | null;
version?: string | null;
};
Update: {
bucket_id?: string | null;
created_at?: string | null;
id?: string;
last_accessed_at?: string | null;
metadata?: Json | null;
name?: string | null;
owner?: string | null;
path_tokens?: string[] | null;
updated_at?: string | null;
version?: string | null;
};
};
};
Views: {
[_ in never]: never;
};
Functions: {
can_insert_object: {
Args: {
bucketid: string;
name: string;
owner: string;
metadata: Json;
};
Returns: undefined;
};
extension: {
Args: {
name: string;
};
Returns: string;
};
filename: {
Args: {
name: string;
};
Returns: string;
};
foldername: {
Args: {
name: string;
};
Returns: unknown;
};
get_size_by_bucket: {
Args: Record<PropertyKey, never>;
Returns: {
size: number;
bucket_id: string;
}[];
};
search: {
Args: {
prefix: string;
bucketname: string;
limits?: number;
levels?: number;
offsets?: number;
search?: string;
sortcolumn?: string;
sortorder?: string;
};
Returns: {
name: string;
id: string;
updated_at: string;
created_at: string;
last_accessed_at: string;
metadata: Json;
}[];
};
};
Enums: {
[_ in never]: never;
};
CompositeTypes: {
[_ in never]: never;
};
};
}
@@ -10619,9 +10619,9 @@
"dev": true
},
"node_modules/socket.io-parser": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz",
"integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
@@ -19733,9 +19733,9 @@
"dev": true
},
"socket.io-parser": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz",
"integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
+12641 -58
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -14,7 +14,6 @@
"eslint-config-next": "^12.0.8",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "7.28.0",
"mini-svg-data-uri": "^1.4.4",
"tailwindcss-radix": "^2.0.0",
"@tailwindcss/typography": "^0.5.2"
}
+162 -47
View File
@@ -1,30 +1,115 @@
const ui = require('./ui.config.js')
const svgToDataUri = require('mini-svg-data-uri')
const deepMerge = require('deepmerge')
module.exports = ui({
const color = require('./../ui/build/css/tw-extend/color')
/**
* Generates Tailwind colors for the theme
* adds <alpha-value> as part of the hsl value
*/
function generateTwColorClasses(globalKey, twAttributes) {
let classes = {}
Object.values(twAttributes).map((attr, i) => {
const attrKey = Object.keys(twAttributes)[i]
if (attrKey.includes(globalKey)) {
classes = {
...classes,
[attrKey.split('-').splice(1).join('-')]: `hsl(${attr.cssVariable} / <alpha-value>)`,
}
}
})
/**
* mutate object into nested object for tailwind theme structure
*/
const nestedClasses = kebabToNested(classes)
// return, but nest the keys if they are kebab case named
return nestedClasses
}
/**
* Helper to convert kebab named keys in object to nested nodes
*/
function kebabToNested(obj) {
const result = {}
for (const [key, value] of Object.entries(obj)) {
const parts = key.split('-')
let currentObj = result
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (!currentObj[part]) {
currentObj[part] = {}
}
if (i === parts.length - 1) {
if (typeof value === 'object') {
currentObj[part] = nestObject(value)
} else {
currentObj[part] = value
}
} else {
currentObj = currentObj[part]
}
}
}
return result
}
/**
* Main theme config
*/
const uiConfig = ui({
mode: 'JIT',
content: [
'../../packages/common/**/*.{ts,tsx}',
'../../packages/ui/**/*.{tsx,ts,js}',
'./src/**/*.{ts,tsx,mdx}',
'./components/**/*.tsx',
'./layouts/**/*.tsx',
'./pages/**/*.{tsx,mdx}',
'./docs/**/*.{tsx,mdx}',
'./_blog/*.mdx',
// purge styles from supabase ui theme
],
darkMode: 'class',
theme: {
/**
* Spread all theme colors and custom generated colors into theme
*/
textColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('textColor', color),
}),
accentColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('accentColor', color),
}),
backgroundColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('backgroundColor', color),
}),
borderColor: (theme) => ({
...theme('colors'),
DEFAULT: theme('colors.scale.300', 'currentColor'),
dark: theme('colors.scale.1200', 'currentColor'),
...generateTwColorClasses('borderColor', color),
}),
boxShadowColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('boxShadowColor', color),
}),
caretColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('caretColor', color),
}),
divideColor: (theme) => ({
...theme('colors'),
DEFAULT: theme('colors.scale.300', 'currentColor'),
dark: theme('colors.scale.600', 'currentColor'),
...generateTwColorClasses('divideColor', color),
}),
outlineColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('outlineColor', color),
}),
placeholderColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('placeholderColor', color),
}),
ringColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('ringColor', color),
}),
ringOffsetColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('ringOffsetColor', color),
}),
textDecorationColor: (theme) => ({
...theme('colors'),
...generateTwColorClasses('textDecorationColor', color),
}),
extend: {
typography: ({ theme }) => ({
@@ -210,43 +295,73 @@ module.exports = ui({
},
},
},
// used in auto docs
docs: {
css: {
'--tw-prose-body': theme('colors.scale[1100]'),
'--tw-prose-headings': theme('colors.scale[1200]'),
'--tw-prose-lead': theme('colors.scale[1100]'),
'--tw-prose-links': theme('colors.brand[900]'),
'--tw-prose-bold': theme('colors.scale[1100]'),
'--tw-prose-counters': theme('colors.scale[1100]'),
'--tw-prose-bullets': theme('colors.scale[900]'),
'--tw-prose-hr': theme('colors.scale[500]'),
'--tw-prose-quotes': theme('colors.scale[1100]'),
'--tw-prose-quote-borders': theme('colors.scale[500]'),
'--tw-prose-captions': theme('colors.scale[700]'),
'--tw-prose-code': theme('colors.scale[1200]'),
'--tw-prose-pre-code': theme('colors.scale[900]'),
'--tw-prose-pre-bg': theme('colors.scale[400]'),
'--tw-prose-th-borders': theme('colors.scale[500]'),
'--tw-prose-td-borders': theme('colors.scale[200]'),
'--tw-prose-invert-body': theme('colors.scale[200]'),
'--tw-prose-invert-headings': theme('colors.white'),
'--tw-prose-invert-lead': theme('colors.scale[500]'),
'--tw-prose-invert-links': theme('colors.white'),
'--tw-prose-invert-bold': theme('colors.white'),
'--tw-prose-invert-counters': theme('colors.scale[400]'),
'--tw-prose-invert-bullets': theme('colors.scale[600]'),
'--tw-prose-invert-hr': theme('colors.scale[700]'),
'--tw-prose-invert-quotes': theme('colors.scale[100]'),
'--tw-prose-invert-quote-borders': theme('colors.scale[700]'),
'--tw-prose-invert-captions': theme('colors.scale[400]'),
// the following are typography overrides
// examples can be seen here —> https://github.com/tailwindlabs/tailwindcss-typography/blob/master/src/styles.js
// reset all header font weights
'h1, h2, h3, h4, h5': {
fontWeight: '400',
},
},
},
}),
screens: {
xs: '480px',
},
fontFamily: {
sans: ['custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
sans: ['Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
mono: ['Office Code Pro', 'Source Code Pro', 'Menlo', 'monospace'],
},
backgroundImage: (theme) => ({
squiggle: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6 3" enable-background="new 0 0 6 3" width="6" height="3" fill="${theme(
'colors.yellow.400'
)}"><polygon points="5.5,0 2.5,3 1.1,3 4.1,0"/><polygon points="4,0 6,2 6,0.6 5.4,0"/><polygon points="0,2 1,3 2.4,3 0,0.6"/></svg>`
)}")`,
}),
keyframes: {
'flash-code': {
'0%': { backgroundColor: 'rgba(63, 207, 142, 0.1)' },
'100%': { backgroundColor: 'transparent' },
},
},
animation: {
'flash-code': 'flash-code 1s forwards',
'flash-code-slow': 'flash-code 2s forwards',
},
},
},
plugins: [
require('@tailwindcss/typography'),
function ({ addUtilities, addVariant }) {
addUtilities({
// prose (tailwind typography) helpers
// useful for removing margins in prose styled sections
'.prose--remove-p-margin p': {
margin: '0',
},
})
},
],
plugins: [require('@tailwindcss/typography')],
})
function arrayMergeFn(destinationArray, sourceArray) {
return destinationArray.concat(sourceArray).reduce((acc, cur) => {
if (acc.includes(cur)) return acc
return [...acc, cur]
}, [])
}
/**
* Merge Supabase UI and Tailwind CSS configurations
* @param {object} tailwindConfig - Tailwind config object
* @return {object} new config object
*/
function wrapper(tailwindConfig) {
return deepMerge({ ...tailwindConfig }, uiConfig, {
arrayMerge: arrayMergeFn,
})
}
module.exports = wrapper
+16
View File
@@ -0,0 +1,16 @@
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 100
}
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": []
}
+21 -39
View File
@@ -1,12 +1,10 @@
const path = require('path')
module.exports = {
// stories: [
// '../src/components/**/*.stories.mdx',
// '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
// ],
// TODO: Temporally Import one by one story and fix build issue.
// After all stories are cleaned, revert to wildcard import.
// // stories: [
// // '../src/components/**/*.stories.mdx',
// // '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
// // ],
// // TODO: Temporally Import one by one story and fix build issue.
// // After all stories are cleaned, revert to wildcard import.
stories: [
'../src/components/Accordion/Accordion.stories.tsx',
'../src/components/Alert/Alert.stories.tsx',
@@ -18,43 +16,27 @@ module.exports = {
'../src/components/Dropdown/Dropdown.stories.tsx',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
// '@storybook/addon-links',
// '@storybook/addon-essentials',
// '@storybook/addon-interactions',
// '@storybook/addon-actions',
'storybook-dark-mode',
{
name: '@storybook/addon-postcss',
name: '@storybook/addon-styling',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
// Check out https://github.com/storybookjs/addon-styling/blob/main/docs/api.md
// For more details on this addon's options.
postCss: true,
},
},
'@storybook/addon-mdx-gfm',
],
framework: '@storybook/react',
webpackFinal: async (config, { configType }) => {
const cssRuleIdx = config.module.rules.findIndex(({ test }) => test.toString() === '/\\.css$/')
const cssRule = {
test: /\.css$/,
include: path.resolve(__dirname, '../'),
exclude: /\.module\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { importLoaders: 1, sourceMap: true } },
{ loader: 'postcss-loader', options: {} },
],
}
const cssModulesRule = {
test: /\.module.css$/,
include: path.resolve(__dirname, '../'),
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { importLoaders: 1, modules: true, sourceMap: true } },
{ loader: 'postcss-loader', options: {} },
],
}
config.module.rules.splice(cssRuleIdx, 1, cssModulesRule, cssRule)
return config
docs: {
autodocs: true,
},
framework: {
name: '@storybook/react-webpack5',
options: {},
},
}
+5 -1
View File
@@ -2,9 +2,13 @@
@tailwind components;
@tailwind utilities;
@import './../build/css/themes/dark.css';
@import './../build/css/themes/light.css';
@import './../build/css/themes/darker-dark.css';
.dark,
.light,
.light .docs-story,
.dark .docs-story {
@apply bg-scale-200;
@apply bg-body;
}
+38
View File
@@ -0,0 +1,38 @@
# Supabase UI Package
## Figma-tokens setup
Tokens setup is based partly on blogpost on the [Mirahi Digital Garden](https://garden.mirahi.io/how-to-use-the-color-tokens-from-your-design-system-directly-in-tailwind-css/).
Transforms tokens stored on [Figma Tokens](https://github.com/six7/figma-tokens) using [token-transformer](https://github.com/six7/figma-tokens/tree/main/token-transformer) and [Style Dictionary](https://github.com/amzn/style-dictionary) to css-variables with references, and use them in your [TailwindCSS](https://github.com/tailwindlabs/tailwindcss) environment with multiple themes.
### Build all the styles files (css-variables and tailwind config)
```bash
npm run build-styles
```
This creates a directory called `styles` with the tokens and also CSS required.
### Other examples
You can find [other examples here](https://github.com/six7/figma-tokens-examples) by [Jan Six](https://twitter.com/six7)
### Adding new themes/sets
Add new sets by extending the scripts in package.json. Currently theme sets derive from `exported/[set name]` but can come from anywhere.
Adding a new set can be done with the following:
```json package.json
// package.json
//..
"build-transform-light": "npx token-transformer tokens.json styles/tokens/02_themes/light.json global,exported/light,theme global",
// add a new line like this:
"build-transform-new": "npx token-transformer tokens.json styles/tokens/02_themes/new.json global,new,theme global",
//..
// append the new theme/set command to end of the build-transform command
"build-transform": "npm run build-transform-global && npm run build-transform-typography && npm run build-transform-dark && npm run build-transform-light && npm run build-transform-new",
```
+57
View File
@@ -0,0 +1,57 @@
/**
* Do not edit directly
* Generated on Tue, 09 May 2023 15:35:29 GMT
*/
.dark {
--utility-mix-negative: 0deg 0% 0%;
--utility-mix-positive: 0deg 0% 100%;
--darken-percentage: 0.2;
--shading-hardest-xll: 0.85;
--shading-hardest-xl: 0.75;
--shading-hardest: 0.65;
--shading-harder: 0.5;
--shading-hard: 0.3;
--shading-soft: 0.05;
--shading-default: 0.1;
--text-override-lighten-strength: 0;
--theme-variables-text-body: 0deg 0% 92.9%; /* og color is colors.Gray.Dark.12 */
--theme-variables-success: 123.8deg 100% 66.3%;
--theme-variables-danger: 0deg 100% 64.7%;
--theme-variables-warning: 26.9deg 93.8% 56.1%;
--theme-variables-background-body-alt: 0deg 0% 8.6%; /* og color is colors.Gray.Dark.1 */
--theme-variables-background-body: 0deg 0% 11%; /* og color is colors.Gray.Dark.2 */
--theme-variables-background: 0deg 0% 13.7%; /* og color is colors.Gray.Dark.3 */
--theme-variables-brand: 153.1deg 60.2% 52.7%;
--text-color-disabled-default: 0deg 0% 46.5%;
--text-color-danger-default: var(--theme-variables-danger);
--text-color-warning-default: var(--theme-variables-warning);
--text-color-link-default: var(--theme-variables-brand);
--text-color-success-default: var(--theme-variables-success);
--text-color-body-default: var(--theme-variables-text-body);
--background-color-danger-default: var(--theme-variables-danger);
--background-color-danger-300: 6.9deg 29% 16.2%;
--background-color-danger-200: 3.6deg 46.8% 35%;
--background-color-danger-100: 2.1deg 50.9% 46.5%;
--background-color-warning-default: var(--theme-variables-warning);
--background-color-warning-300: 24.5deg 32.6% 15.4%;
--background-color-warning-200: 25.7deg 57.5% 31.7%;
--background-color-warning-100: 26.2deg 64.9% 41.4%;
--background-color-success-default: var(--theme-variables-success);
--background-color-success-300: 114deg 21.2% 16.6%;
--background-color-success-200: 119deg 37.3% 36.8%;
--background-color-success-100: 121deg 43% 48.6%;
--background-color-body-alt: var(--theme-variables-background-body-alt);
--background-color-body: var(--theme-variables-background-body); /* Background color used for the main page body. */
--background-color-default: var(--theme-variables-background); /* Default background color for any container. */
--text-color-body-lightest: 91.8deg 0% 30.2%;
--text-color-body-lighter: 91.8deg 0% 43.5%;
--text-color-body-light: 91.8deg 0% 62.5%;
--border-color-selection: 91.8deg 0% 53.5%;
--border-color-body-alt: 91.8deg 0% 12.2%;
--border-color-body: 91.8deg 0% 14.6%;
--border-color-default: 91.8deg 0% 17.3%; /* Default border color */
--background-color-control: 91.8deg 0% 10.4%;
--background-color-selection: 91.8deg 0% 17.3%;
--border-color-control: 91.8deg 0% 20.8%;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Do not edit directly
* Generated on Tue, 09 May 2023 15:35:29 GMT
*/
.light {
--utility-github-background: 0deg 0% 100%;
--utility-mix-negative: 0deg 0% 100%;
--utility-mix-positive: 0deg 0% 0%;
--darken-percentage: 0.2;
--shading-hardest-xll: 0.85;
--shading-hardest-xl: 0.75;
--shading-hardest: 0.65;
--shading-harder: 0.5;
--shading-hard: 0.3;
--shading-soft: 0.035;
--shading-default: 0.1;
--text-override-lighten-strength: 5;
--theme-variables-text-body: 195deg 7.1% 11%; /* og color is colors.Slate.Dark.2 */
--theme-variables-success: 123.8deg 100% 66.3%;
--theme-variables-danger: 0deg 100% 64.7%;
--theme-variables-warning: 26.9deg 93.8% 56.1%;
--theme-variables-background-body-alt: 0deg 0% 100%; /* og color is colors.Gray.Dark.1 */
--theme-variables-background-body: 210deg 16.7% 97.6%; /* og color is colors.Slate.Light.2 */
--theme-variables-background: 210deg 33.3% 98.8%; /* og color is colors.Slate.Light.1 */
--theme-variables-brand: 153.1deg 60.2% 52.7%;
--text-color-disabled-default: 195deg 7.1% 5.5%;
--text-color-danger-default: var(--theme-variables-danger);
--text-color-warning-default: var(--theme-variables-warning);
--text-color-link-default: var(--theme-variables-brand);
--text-color-success-default: var(--theme-variables-success);
--text-color-body-default: var(--theme-variables-text-body);
--background-color-danger-default: var(--theme-variables-danger);
--background-color-danger-300: 8deg 100% 95.2%;
--background-color-danger-200: 6.9deg 100% 83.3%;
--background-color-danger-100: 5.3deg 100% 76.2%;
--background-color-warning-default: var(--theme-variables-warning);
--background-color-warning-300: 23.5deg 100% 94.4%;
--background-color-warning-200: 24.6deg 100% 80.1%;
--background-color-warning-100: 25.2deg 100% 71.2%;
--background-color-success-default: var(--theme-variables-success);
--background-color-success-300: 112deg 100% 95.7%;
--background-color-success-200: 114deg 100% 85.3%;
--background-color-success-100: 117deg 100% 78.9%;
--background-color-body-alt: var(--theme-variables-background-body-alt);
--background-color-body: var(--theme-variables-background-body); /* Background color used for the main page body. */
--background-color-default: var(--theme-variables-background); /* Default background color for any container. */
--text-color-body-lightest: 195deg 1% 65.6%;
--text-color-body-lighter: 195deg 1% 51.8%;
--text-color-body-light: 195deg 1.9% 34.3%;
--border-color-selection: 210deg 0.4% 46.1%;
--border-color-body-alt: 91.8deg 0% 96%;
--border-color-body: 210deg 6.1% 93.7%;
--border-color-default: 210deg 7.5% 94.9%; /* Default border color */
--background-color-control: 210deg 33.3% 99.2%;
--background-color-selection: 210deg 7.5% 94.9%;
--border-color-control: 210deg 4.1% 91.2%;
}
+166
View File
@@ -0,0 +1,166 @@
module.exports = {
"theme-variables-brand": {
cssVariable: "var(--theme-variables-brand)",
value: "hsl(153.10000000000002, 60.2%, 52.7%)"
},
"theme-variables-background": {
cssVariable: "var(--theme-variables-background)",
value: "hsl(0, 0%, 13.7%)"
},
"theme-variables-background-body": {
cssVariable: "var(--theme-variables-background-body)",
value: "hsl(0, 0%, 11%)"
},
"theme-variables-background-body-alt": {
cssVariable: "var(--theme-variables-background-body-alt)",
value: "hsl(0, 0%, 8.6%)"
},
"theme-variables-warning": {
cssVariable: "var(--theme-variables-warning)",
value: "hsl(26.899999999999977, 93.8%, 56.1%)"
},
"theme-variables-danger": {
cssVariable: "var(--theme-variables-danger)",
value: "hsl(0, 100%, 64.7%)"
},
"theme-variables-success": {
cssVariable: "var(--theme-variables-success)",
value: "hsl(123.80000000000001, 100%, 66.3%)"
},
"theme-variables-text-body": {
cssVariable: "var(--theme-variables-text-body)",
value: "hsl(0, 0%, 92.9%)"
},
"backgroundColor-DEFAULT": {
cssVariable: "var(--background-color-default)",
value: "hsl(0, 0%, 13.7%)"
},
"backgroundColor-body": {
cssVariable: "var(--background-color-body)",
value: "hsl(0, 0%, 11%)"
},
"backgroundColor-body-alt": {
cssVariable: "var(--background-color-body-alt)",
value: "hsl(0, 0%, 8.6%)"
},
"backgroundColor-selection": {
cssVariable: "var(--background-color-selection)",
value: "hsl(91.8 0% 17.3%)"
},
"backgroundColor-control": {
cssVariable: "var(--background-color-control)",
value: "hsl(91.8 0% 10.4%)"
},
"backgroundColor-success-100": {
cssVariable: "var(--background-color-success-100)",
value: "hsl(121 43% 48.6%)"
},
"backgroundColor-success-200": {
cssVariable: "var(--background-color-success-200)",
value: "hsl(119 37.3% 36.8%)"
},
"backgroundColor-success-300": {
cssVariable: "var(--background-color-success-300)",
value: "hsl(114 21.2% 16.6%)"
},
"backgroundColor-success-DEFAULT": {
cssVariable: "var(--background-color-success-default)",
value: "hsl(123.80000000000001, 100%, 66.3%)"
},
"backgroundColor-warning-100": {
cssVariable: "var(--background-color-warning-100)",
value: "hsl(26.2 64.9% 41.4%)"
},
"backgroundColor-warning-200": {
cssVariable: "var(--background-color-warning-200)",
value: "hsl(25.7 57.5% 31.7%)"
},
"backgroundColor-warning-300": {
cssVariable: "var(--background-color-warning-300)",
value: "hsl(24.5 32.6% 15.4%)"
},
"backgroundColor-warning-DEFAULT": {
cssVariable: "var(--background-color-warning-default)",
value: "hsl(26.899999999999977, 93.8%, 56.1%)"
},
"backgroundColor-danger-100": {
cssVariable: "var(--background-color-danger-100)",
value: "hsl(2.07 50.9% 46.5%)"
},
"backgroundColor-danger-200": {
cssVariable: "var(--background-color-danger-200)",
value: "hsl(3.65 46.8% 35%)"
},
"backgroundColor-danger-300": {
cssVariable: "var(--background-color-danger-300)",
value: "hsl(6.9 29% 16.2%)"
},
"backgroundColor-danger-DEFAULT": {
cssVariable: "var(--background-color-danger-default)",
value: "hsl(0, 100%, 64.7%)"
},
"borderColor-DEFAULT": {
cssVariable: "var(--border-color-default)",
value: "hsl(91.8 0% 17.3%)"
},
"borderColor-body": {
cssVariable: "var(--border-color-body)",
value: "hsl(91.8 0% 14.6%)"
},
"borderColor-body-alt": {
cssVariable: "var(--border-color-body-alt)",
value: "hsl(91.8 0% 12.2%)"
},
"borderColor-selection": {
cssVariable: "var(--border-color-selection)",
value: "hsl(91.8 0% 53.5%)"
},
"borderColor-control": {
cssVariable: "var(--border-color-control)",
value: "hsl(91.8 0% 20.8%)"
},
"textColor-body-DEFAULT": {
cssVariable: "var(--text-color-body-default)",
value: "hsl(0, 0%, 92.9%)"
},
"textColor-body-light": {
cssVariable: "var(--text-color-body-light)",
value: "hsl(91.8 0% 62.5%)"
},
"textColor-body-lighter": {
cssVariable: "var(--text-color-body-lighter)",
value: "hsl(91.8 0% 43.5%)"
},
"textColor-body-lightest": {
cssVariable: "var(--text-color-body-lightest)",
value: "hsl(91.8 0% 30.2%)"
},
"textColor-success-DEFAULT": {
cssVariable: "var(--text-color-success-default)",
value: "hsl(123.80000000000001, 100%, 66.3%)"
},
"textColor-link-DEFAULT": {
cssVariable: "var(--text-color-link-default)",
value: "hsl(153.10000000000002, 60.2%, 52.7%)"
},
"textColor-warning-DEFAULT": {
cssVariable: "var(--text-color-warning-default)",
value: "hsl(26.899999999999977, 93.8%, 56.1%)"
},
"textColor-danger-DEFAULT": {
cssVariable: "var(--text-color-danger-default)",
value: "hsl(0, 100%, 64.7%)"
},
"textColor-disabled-DEFAULT": {
cssVariable: "var(--text-color-disabled-default)",
value: "hsl(0 0% 46.5%)"
},
"utility-mix-positive": {
cssVariable: "var(--utility-mix-positive)",
value: "hsl(0, 0%, 100%)"
},
"utility-mix-negative": {
cssVariable: "var(--utility-mix-negative)",
value: "hsl(0, 0%, 0%)"
}
}
-67
View File
@@ -1,67 +0,0 @@
const deepMerge = require('deepmerge')
const customFormsPlugin = require('@tailwindcss/forms')
const backgroundOpacity = (theme) => ({
10: '0.1',
...theme('opacity'),
})
const maxHeight = (theme) => ({
0: '0',
xl: '36rem',
...theme('spacing'),
})
const windmillConfig = {
darkMode: 'class',
purge: {
content: [
//'node_modules/@supabase/ui/theme/defaultTheme.js',
// 'node_modules/@supabase/ui/dist/index.js',
],
},
theme: {
backgroundOpacity,
maxHeight,
},
variants: {
backgroundOpacity: ['responsive', 'hover', 'focus', 'dark'],
backgroundColor: ['responsive', 'hover', 'focus', 'active', 'odd', 'dark'],
display: ['responsive', 'dark'],
textColor: ['responsive', 'focus', 'focus-within', 'hover', 'active', 'dark'],
placeholderColor: ['responsive', 'focus', 'dark'],
borderColor: ['responsive', 'hover', 'focus', 'dark'],
divideColor: ['responsive', 'dark'],
boxShadow: ['responsive', 'hover', 'focus', 'dark'],
margin: ['responsive', 'last'],
},
plugins: [customFormsPlugin],
}
function arrayMergeFn(destinationArray, sourceArray) {
return destinationArray.concat(sourceArray).reduce((acc, cur) => {
if (acc.includes(cur)) return acc
return [...acc, cur]
}, [])
}
/**
* Merge Windmill and Tailwind CSS configurations
* @param {object} tailwindConfig - Tailwind config object
* @return {object} new config object
*/
export function wrapper(tailwindConfig) {
let purge
if (Array.isArray(tailwindConfig.purge)) {
purge = {
content: tailwindConfig.purge,
}
} else {
purge = tailwindConfig.purge
}
return deepMerge({ ...tailwindConfig, purge }, windmillConfig, {
arrayMerge: arrayMergeFn,
})
}
// module.exports = wrapper
@@ -0,0 +1,248 @@
const glob = require('glob')
const configFiles = glob.sync(`tokens/config/**/*.json`)
const sourceFiles = glob.sync(`tokens/source/**/*.json`)
const themeFiles = glob.sync(`tokens/themes/**/*.json`)
const semanticFiles = glob.sync(`tokens/semantic/**/*.json`)
const { registerTransforms } = require('@tokens-studio/sd-transforms')
const StyleDictionary = require('style-dictionary')
const Color = require('color')
/**
* uncomment tailwind types as they become available
*/
const supportedTokenTypeList = [
// 'spacing',
// 'sizing',
// 'borderRadius',
// 'borderWidth',
'color',
// 'opacity',
// 'fontFamilies',
// 'lineHeights',
// 'letterSpacing',
// 'paragraphSpacing',
// 'fontWeights',
// 'fontSizes',
// 'textCase',
// 'textDecoration',
]
registerTransforms(StyleDictionary)
const formatTailwindValue = (tokenType, value) => {
let formattedValue
switch (tokenType) {
case 'color':
default:
formattedValue = value
}
return formattedValue
}
/**
* Custom format that generates tailwind friendly colors so we can add <alpha-value>
* in the config later
* https://tailwindcss.com/docs/customizing-colors#using-css-variables
*/
StyleDictionary.registerTransform({
name: 'color/rgb',
type: 'value',
matcher: (prop) => prop.type === 'color',
transformer: (prop) => {
const color = Color(prop.original.value).hsl()
return color.string()
},
})
/**
* Custom format that generate tailwind color config based on css variables
*/
StyleDictionary.registerFormat({
name: 'tw/css-variables',
formatter({ dictionary }) {
// console.log('dictionary', dictionary)
return (
'module.exports = ' +
`{\n${dictionary.allProperties
.map((token) => {
const value = formatTailwindValue(token.type, token.value)
return `"${token.path.slice(0).join('-')}": {
cssVariable: "var(--${token.name})",
value: "${value}"
}`
})
.join(',\n')}\n}`
)
},
})
/**
* Returns the files configuration
* for generating separated tailwind files.
*/
function getConfigTailwindFilesByType(typeList) {
console.log('\n')
console.log('tailwind typeList', typeList)
return typeList.map((typeName) => {
return {
destination: `tw-extend/${typeName}.js`,
format: 'tw/css-variables',
filter: {
type: typeName,
},
}
})
}
function getStyleDictionaryConfig(
sourceFiles,
fileName,
type,
buildTailwindFiles = false,
rootTheme = false
) {
console.log(sourceFiles)
let configTailwindFilesByType = []
if (buildTailwindFiles) {
console.log(`\n`)
console.log(`Generating Tailwind Props ✨`)
configTailwindFilesByType = getConfigTailwindFilesByType(supportedTokenTypeList)
}
return {
source: sourceFiles,
platforms: {
css: {
// transformGroup: 'tokens-studio',
transforms: [
'ts/descriptionToComment',
'ts/size/px',
'ts/opacity',
'ts/size/lineheight',
'ts/type/fontWeight',
'ts/resolveMath',
'ts/size/css/letterspacing',
'ts/typography/css/shorthand',
'ts/border/css/shorthand',
'ts/shadow/css/shorthand',
'ts/color/css/hexrgba',
'ts/color/modifiers',
'name/cti/kebab',
'color/rgb',
],
buildPath: 'build/css/',
files: [
{
destination: `${type}/${fileName}.css`,
format: 'css/variables',
options: {
selector: rootTheme ? ':root' : `.${fileName}`,
outputReferences: true,
},
// filter: (token) =>
// [sourceFiles.slice(-1)[0], ...semanticFiles, ...sourceFiles].includes(token.filePath),
},
...configTailwindFilesByType,
],
},
},
}
}
function fileNameCleaner(fileName) {
return fileName.split('/').pop().replace('.json', '')
}
/**
* BUILD SOURCE FILES
*/
sourceFiles.map(function (filePath) {
const fileName = fileNameCleaner(filePath)
const SD = StyleDictionary.extend(
getStyleDictionaryConfig([filePath], fileName, 'source', true, true)
)
SD.buildAllPlatforms()
})
/**
* BUILD THEME FILE
*/
themeFiles.map(function (filePath, i) {
const buildTailwindFiles = filePath.includes('root') // i === 0
const fileName = fileNameCleaner(filePath)
const rootTheme = filePath.includes('root')
const SD = StyleDictionary.extend(
getStyleDictionaryConfig(
// determine whether to include the root theme properties with each theme
rootTheme ? [filePath] : ['tokens/themes/root.json', filePath],
rootTheme ? 'dark' : fileName,
'themes',
buildTailwindFiles,
false
)
)
SD.buildAllPlatforms()
})
function convertToVariableIfNeeded(value) {
if (value.startsWith('{') && value.endsWith('}')) {
return `var(--${value.slice(1, -1).split('.').join('-')})`
}
return value
}
/**
* BUILD TYPOGRAPHY FILES
*/
/**
* Format for css typography classes
* This generates theme-independent css classes so we're fine with just using css variables here
*/
StyleDictionary.registerFormat({
name: 'css/typographyClasses',
formatter: (dictionary, config) =>
dictionary.allProperties
.map((prop) => {
return `
.${prop.name} {
font: var(--${prop.name});
letter-spacing: ${convertToVariableIfNeeded(prop.original.value.letterSpacing)};
text-transform: ${convertToVariableIfNeeded(prop.original.value.textCase)};
text-decoration: ${convertToVariableIfNeeded(prop.original.value.textDecoration)};
}`
})
.join('\n'),
})
function getTypographyConfig() {
console.log('Building: typography')
return {
source: [...semanticFiles, ...sourceFiles],
platforms: {
css: {
transforms: ['name/cti/kebab'],
transformGroup: 'tokens-studio',
buildPath: 'build/css/',
files: [
{
destination: `source/typography-classes.css`,
format: 'css/typographyClasses',
selector: ':root',
filter: (token) => token.type === 'typography',
},
],
},
},
}
}
const typographyBuild = StyleDictionary.extend(getTypographyConfig())
typographyBuild.buildAllPlatforms()
@@ -0,0 +1,35 @@
const glob = require('glob')
const fs = require('fs')
const cssFiles = glob.sync('build/css/**/*.css')
function transformHSLValues(precision) {
cssFiles.forEach((file) => {
const css = fs.readFileSync(file, 'utf8')
const transformedCss = css.replace(
/hsl\(([\d.]+)[,\s]+([\d.]+)%[,\s]+([\d.]+)%\)/g,
(match, hue, saturation, lightness) => {
hue = `${roundNumber(hue, precision)}deg`
saturation = `${roundNumber(saturation, precision)}%`
lightness = `${roundNumber(lightness, precision)}%`
return `${hue} ${saturation} ${lightness}`
}
)
fs.writeFileSync(file, transformedCss)
})
function roundNumber(number, precision) {
const rounded = parseFloat(number).toFixed(precision)
const str = rounded.toString()
const match = str.match(/^(\d+)\.(\d+)$/)
if (match && match[2] === '0'.repeat(match[2].length)) {
return match[1]
}
return rounded
}
}
// Example usage:
transformHSLValues(1)
@@ -0,0 +1,78 @@
const https = require('https')
const fs = require('fs')
/*
* Branch to target in GitHub repo
*/
const branch = 'feat/new-components'
// using Math.random() in a param as GitHub caches raw content
// const url = `https://raw.githubusercontent.com/MildTomato/supabase-design-tokens/${branch}/tokens.json?e=${Math.random()}`
const baseUrl = `https://raw.githubusercontent.com/MildTomato/supabase-design-tokens/${branch}/tokens/`
/*
* Files that need to be copied over from Tokens repo
*/
const TOKEN_FILES_METADATA = [
// figma tokens misc config files (comment out for now)
// { fileName: '$metadata.json', type: 'config' },
// { fileName: '$themes.json', type: 'config' },
// source files (comment out for now)
// { fileName: 'global.json', type: 'source' },
// { fileName: 'global-two.json', type: 'source' },
// semantic (comment out for now)
// { fileName: 'typography.json', type: 'semantic' },
// themes
{ fileName: 'root.json', type: 'theme', subdirectory: 'new/' }, // root theme
{ fileName: 'light.json', type: 'theme', subdirectory: 'new/' },
// { fileName: 'darker-dark.json', type: 'theme', subdirectory: 'new/' },
]
const PATHS = {
config: 'config/',
source: 'source/',
theme: 'themes/',
semantic: 'semantic/',
}
async function getTokensFile() {
const promises = TOKEN_FILES_METADATA.map((tokenMetadata, i) => {
return new Promise((resolve, reject) => {
https
.get(
baseUrl + tokenMetadata.subdirectory + tokenMetadata.fileName + `?e=${Math.random()}`,
(res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
fs.writeFile(
`./tokens/${PATHS[tokenMetadata.type]}${tokenMetadata.fileName}`,
data,
'utf8',
() => {}
)
resolve(data)
})
}
)
.on('error', (err) => {
reject(err)
})
})
})
await Promise.all(promises)
}
async function copyFiles() {
console.log('Copying token files... \n')
console.log('branch to pull is:', branch)
try {
await getTokensFile()
} catch (error) {
console.error(error)
}
}
copyFiles()
+25 -7
View File
@@ -6,9 +6,13 @@
"license": "MIT",
"sideEffects": false,
"scripts": {
"build-storybook": "build-storybook",
"build:icons": "node --experimental-modules internals/populate-icons.js",
"storybook": "start-storybook -p 6006",
"build-tokens": "node internals/tokens/build-tokens.js",
"copy-tokens": "node internals/tokens/copy-tokens.js",
"cleanse-css-for-tailwind": "node internals/tokens/cleanse-css-for-tailwind.js",
"generate-styles": "npm run copy-tokens && npm run build-tokens && npm run cleanse-css-for-tailwind",
"build-storybook": "storybook build",
"storybook": "storybook dev -p 6006",
"test": "jest"
},
"dependencies": {
@@ -25,16 +29,17 @@
"@supabase/auth-helpers-react": "^0.3.1",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.0",
"autoprefixer": "^10.4.2",
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"cmdk-supabase": "^0.2.2",
"color": "^4.2.3",
"colorjs.io": "^0.4.3",
"common-tags": "^1.8.2",
"deepmerge": "^4.2.2",
"formik": "^2.2.9",
"lodash": "^4.17.20",
"lucide-react": "^0.124.0",
"openai": "^3.2.1",
"postcss": "^8.4.5",
"postcss": "^8.4.22",
"prop-types": "^15.7.2",
"react-copy-to-clipboard": "^5.1.0",
"react-countdown": "^2.3.5",
@@ -48,15 +53,28 @@
"tailwindcss-radix": "^1.6.0"
},
"devDependencies": {
"@storybook/react": "^6.5.16",
"config": "*",
"@babel/preset-env": "^7.21.4",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.4",
"@storybook/addon-actions": "^7.0.5",
"@storybook/addon-mdx-gfm": "^7.0.5",
"@storybook/addon-styling": "^1.0.1",
"@storybook/react": "^7.0.5",
"@storybook/react-webpack5": "^7.0.5",
"@tokens-studio/sd-transforms": "^0.8.5",
"@testing-library/jest-dom": "^5.16.5",
"@types/common-tags": "^1.8.1",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^17.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"copyfiles": "^2.4.1",
"glob": "^8.1.0",
"sql-formatter": "^12.2.0",
"storybook-dark-mode": "^2.1.1",
"storybook": "^7.0.5",
"storybook-dark-mode": "^3.0.0",
"style-dictionary": "^3.7.1",
"typescript": "4.3.4"
}
}
@@ -14,6 +14,7 @@ export const Collapsible = ({
}: CollapsibleProps) => {
return (
<RadixCollapsible.Root
asChild={props.asChild}
defaultOpen={props.defaultOpen}
open={open}
onOpenChange={props.onOpenChange}
@@ -30,15 +31,17 @@ export function Trigger({ children, asChild }: { children: React.ReactNode; asCh
}
export function Content({
asChild,
children,
className,
}: {
asChild?: boolean
children: React.ReactNode
className?: string
}) {
const __styles = styleHandler('collapsible')
return (
<RadixCollapsible.Content className={[__styles.content, className].join(' ')}>
<RadixCollapsible.Content asChild={asChild} className={[__styles.content, className].join(' ')}>
{children}
</RadixCollapsible.Content>
)
@@ -207,7 +207,7 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => {
{sharedItems.tools.map((item) => {
const itemUrl = (
projectRef ? item.url.replace('_', projectRef) : item.url
).split('https://app.supabase.com')[1]
).split('https://supabase.com/dashboard')[1]
return (
<CommandItem
@@ -2,171 +2,171 @@
"tools": [
{
"label": "Table Editor",
"url": "https://app.supabase.com/project/_/editor"
"url": "https://supabase.com/dashboard/project/_/editor"
},
{
"label": "SQL Editor",
"url": "https://app.supabase.com/project/_/sql"
"url": "https://supabase.com/dashboard/project/_/sql"
},
{
"label": "Database",
"url": "https://app.supabase.com/project/_/database/tables",
"url": "https://supabase.com/dashboard/project/_/database/tables",
"subItems": [
{
"label": "Tables",
"url": "https://app.supabase.com/project/_/database/tables"
"url": "https://supabase.com/dashboard/project/_/database/tables"
},
{
"label": "Triggers",
"url": "https://app.supabase.com/project/_/database/triggers"
"url": "https://supabase.com/dashboard/project/_/database/triggers"
},
{
"label": "Functions",
"url": "https://app.supabase.com/project/_/database/functions"
"url": "https://supabase.com/dashboard/project/_/database/functions"
},
{
"label": "Extensions",
"url": "https://app.supabase.com/project/_/database/extensions"
"url": "https://supabase.com/dashboard/project/_/database/extensions"
},
{
"label": "Roles",
"url": "https://app.supabase.com/project/_/database/roles"
"url": "https://supabase.com/dashboard/project/_/database/roles"
},
{
"label": "Replication",
"url": "https://app.supabase.com/project/_/database/replication"
"url": "https://supabase.com/dashboard/project/_/database/replication"
},
{
"label": "Webhooks",
"url": "https://app.supabase.com/project/_/database/webhooks"
"url": "https://supabase.com/dashboard/project/_/database/webhooks"
},
{
"label": "Backups",
"url": "https://app.supabase.com/project/_/database/backups/scheduled"
"url": "https://supabase.com/dashboard/project/_/database/backups/scheduled"
}
]
},
{
"label": "Auth",
"url": "https://app.supabase.com/project/_/auth/users",
"url": "https://supabase.com/dashboard/project/_/auth/users",
"subItems": [
{
"label": "Users",
"url": "https://app.supabase.com/project/_/auth/users"
"url": "https://supabase.com/dashboard/project/_/auth/users"
},
{
"label": "Policies",
"url": "https://app.supabase.com/project/_/auth/policies"
"url": "https://supabase.com/dashboard/project/_/auth/policies"
},
{
"label": "Providers",
"url": "https://app.supabase.com/project/_/auth/providers"
"url": "https://supabase.com/dashboard/project/_/auth/providers"
},
{
"label": "Email Templates",
"url": "https://app.supabase.com/project/_/auth/templates"
"url": "https://supabase.com/dashboard/project/_/auth/templates"
},
{
"label": "URL Configuration",
"url": "https://app.supabase.com/project/_/auth/url-configuration"
"url": "https://supabase.com/dashboard/project/_/auth/url-configuration"
}
]
},
{
"label": "Storage",
"url": "https://app.supabase.com/project/_/storage"
"url": "https://supabase.com/dashboard/project/_/storage"
},
{
"label": "Edge Functions",
"url": "https://app.supabase.com/project/_/functions"
"url": "https://supabase.com/dashboard/project/_/functions"
},
{
"label": "Logs",
"url": "https://app.supabase.com/project/_/logs/explorer",
"url": "https://supabase.com/dashboard/project/_/logs/explorer",
"subItems": [
{
"label": "Postgres logs",
"url": "https://app.supabase.com/project/_/logs/postgres-logs"
"url": "https://supabase.com/dashboard/project/_/logs/postgres-logs"
},
{
"label": "PostgREST logs",
"url": "https://app.supabase.com/project/_/logs/postgrest-logs"
"url": "https://supabase.com/dashboard/project/_/logs/postgrest-logs"
},
{
"label": "Postgres logs",
"url": "https://app.supabase.com/project/_/logs/pgbouncer-logs"
"url": "https://supabase.com/dashboard/project/_/logs/pgbouncer-logs"
},
{
"label": "Auth logs logs",
"url": "https://app.supabase.com/project/_/logs/auth-logs"
"url": "https://supabase.com/dashboard/project/_/logs/auth-logs"
},
{
"label": "Storage logs",
"url": "https://app.supabase.com/project/_/logs/storage-logs"
"url": "https://supabase.com/dashboard/project/_/logs/storage-logs"
},
{
"label": "realtime logs",
"url": "https://app.supabase.com/project/_/logs/realtime-logs"
"url": "https://supabase.com/dashboard/project/_/logs/realtime-logs"
}
]
},
{
"label": "API Docs",
"url": "https://app.supabase.com/project/_/api",
"url": "https://supabase.com/dashboard/project/_/api",
"subItems": [
{
"label": "Auth docs",
"url": "https://app.supabase.com/project/_/api?page=auth"
"url": "https://supabase.com/dashboard/project/_/api?page=auth"
},
{
"label": "User management docs",
"url": "https://app.supabase.com/project/_/api?page=users"
"url": "https://supabase.com/dashboard/project/_/api?page=users"
},
{
"label": "Graphql",
"url": "https://app.supabase.com/project/_/api/graphiql"
"url": "https://supabase.com/dashboard/project/_/api/graphiql"
}
]
},
{
"label": "Project Settings",
"url": "https://app.supabase.com/project/_/settings/general",
"url": "https://supabase.com/dashboard/project/_/settings/general",
"subItems": [
{
"label": "General settings",
"url": "https://app.supabase.com/project/_/settings/general"
"url": "https://supabase.com/dashboard/project/_/settings/general"
},
{
"label": "Database settings",
"url": "https://app.supabase.com/project/_/settings/database"
"url": "https://supabase.com/dashboard/project/_/settings/database"
},
{
"label": "API settings",
"url": "https://app.supabase.com/project/_/settings/api"
"url": "https://supabase.com/dashboard/project/_/settings/api"
},
{
"label": "Auth settings",
"url": "https://app.supabase.com/project/_/settings/auth"
"url": "https://supabase.com/dashboard/project/_/settings/auth"
},
{
"label": "Storage settings",
"url": "https://app.supabase.com/project/_/settings/storage"
"url": "https://supabase.com/dashboard/project/_/settings/storage"
},
{
"label": "Subscription settings",
"url": "https://app.supabase.com/project/_/settings/billing/subscription"
"url": "https://supabase.com/dashboard/project/_/settings/billing/subscription"
},
{
"label": "Project Usage",
"url": "https://app.supabase.com/project/_/settings/billing/usage"
"url": "https://supabase.com/dashboard/project/_/settings/billing/usage"
},
{
"label": "Project Invoices",
"url": "https://app.supabase.com/project/_/settings/billing/invoices"
"url": "https://supabase.com/dashboard/project/_/settings/billing/invoices"
},
{
"label": "Project Invoices",
"url": "https://app.supabase.com/project/_/settings/billing/invoices"
"url": "https://supabase.com/dashboard/project/_/settings/billing/invoices"
}
]
}
@@ -174,11 +174,11 @@
"projectTools": [
{
"label": "Your Projects",
"url": "https://app.supabase.com/projects"
"url": "https://supabase.com/dashboard/projects"
},
{
"label": "Project API Keys",
"url": "https://app.supabase.com/project/_/settings/api"
"url": "https://supabase.com/dashboard/project/_/settings/api"
}
],
"support": [
@@ -25,8 +25,13 @@ const DemoContainer = ({ children }: { children: React.ReactNode }) => (
>
<div
className="
bg border
p-3 px-5
bg-scale-300 border border-scale-600 rounded flex gap-4
shadow-sm
rounded-md
flex
gap-4
justify-between
items-center
w-3/4
@@ -53,9 +58,9 @@ export const DefaultFull = (args: any) => {
overlay={[
<Dropdown.Misc>
<div>
<span className="block">Signed in as </span>
<span className="block text-body">Signed in as </span>
<span>tom@example.com </span>
<span className="text-body-light">tom@example.com </span>
</div>
</Dropdown.Misc>,
<Dropdown.Separator />,
+2
View File
@@ -88,6 +88,8 @@ export default function Form({ validate, ...props }: Props) {
handleReset: formik.handleReset,
/** Resets the form with custom values */
resetForm: formik.resetForm,
/** Manually sets a fields value */
setFieldValue: formik.setFieldValue,
})}
</FormContextProvider>
</form>
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more