Merge branch 'master' into km/update-tsdocs
No files matched your search
@@ -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: ./
|
||||
@@ -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 @@
|
||||
^./i18n
|
||||
^./i18n
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## Step 2: Connect the database to Forest Admin
|
||||
|
||||
First, you have to create a new project on Forest Admin:
|
||||

|
||||
|
||||
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.
|
||||

|
||||
|
||||
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.
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Chase Granberry
|
||||
Chris Copplestone
|
||||
Dave Wilson
|
||||
Div Arora
|
||||
Divit D
|
||||
Egor Romanov
|
||||
Fabrizio Fenoglio
|
||||
Francesco Sansalvadore
|
||||
|
||||
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 130 KiB |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
And after ChatGPT receives a response from the plugin it will answer your question with the data from the documentation.
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
"
|
||||
|
||||
@@ -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'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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 193 KiB |
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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,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
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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 ?? []} />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,7 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
serverActions: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sourceType": "unambiguous",
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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: {},
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
```
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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%)"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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>
|
||||
|
||||