Merge branch 'feat/mamba-d1' of github.com:supabase/supabase into feat/mamba-d1

This commit is contained in:
Terry Sutton committed 2025-03-31 08:37:52 -02:30
commit 468f60be26
201 files changed
+6894 -1895

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
engine-strict=true
update-notifier=false
@jsr:registry=https://npm.jsr.io
@@ -26,7 +26,7 @@ export default function TreeViewDemo() {
<TreeView
data={flattenTree(data)}
aria-label="directory tree"
className="w-[420px]"
className="w-[420px] border bg py-2"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
<TreeViewItem
isExpanded={isExpanded}
@@ -5,34 +5,85 @@ export default function TreeViewDemo() {
name: '',
children: [
{
name: 'Current batch',
children: [{ name: 'index.js' }, { name: 'styles.css' }],
name: 'Active Projects',
children: [{ name: 'main.js' }, { name: 'styles.css' }],
},
{
name: 'Older queries',
name: 'Archived Queries',
children: [
{
name: 'all countries',
name: 'Historical Data',
children: [
{
name: 'Country Statistics',
},
{
name: 'Add New Countries',
},
{
name: 'Regional Insights',
},
{
name: 'Customer-Specific Regions',
},
],
},
{
name: 'add new countries',
name: 'Previous Queries',
children: [
{
name: 'Country Statistics',
children: [
{
name: 'Country Overview',
},
{
name: 'Add New Countries',
},
{
name: 'Regional Insights',
},
{
name: 'Customer-Specific Regions',
},
],
},
{
name: 'Country Overview',
},
{
name: 'Add New Countries',
},
{
name: 'Regional Insights',
},
{
name: 'Customer-Specific Regions',
},
],
},
{
name: 'regions',
name: 'Country Overview',
},
{
name: 'regions by customer',
name: 'Add New Countries',
},
{
name: 'Regional Insights',
},
{
name: 'Customer-Specific Regions',
},
],
},
{
name: 'query all users',
name: 'User Query Logs',
},
{
name: 'users in last day',
name: 'Recent User Activity',
},
{
name: 'new users over time',
name: 'User Growth Trends',
},
],
}
@@ -41,14 +92,13 @@ export default function TreeViewDemo() {
<TreeView
data={flattenTree(data)}
aria-label="directory tree"
className="w-[420px]"
className="w-[420px] border bg py-2"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
<TreeViewItem
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={isSelected}
level={level}
xPadding={16}
name={element.name}
{...getNodeProps()}
/>
@@ -80,6 +80,7 @@ export default function TreeViewDemo() {
return (
<TreeView
data={flattenTree(data)}
className="w-[420px] border bg py-2"
aria-label="directory tree"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
return (
@@ -84,6 +84,7 @@ export default function TreeViewDemo() {
togglableSelect
clickAction="EXCLUSIVE_SELECT"
multiSelect
className="w-[420px] border bg py-2"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
return (
<ContextMenu_Shadcn_ modal={false}>
@@ -652,6 +652,7 @@ export const auth = {
name: 'Third-party auth',
items: [
{ name: 'Overview', url: '/guides/auth/third-party/overview' },
{ name: 'Clerk', url: '/guides/auth/third-party/clerk' },
{ name: 'Firebase Auth', url: '/guides/auth/third-party/firebase-auth' },
{ name: 'Auth0', url: '/guides/auth/third-party/auth0' },
{ name: 'AWS Cognito (Amplify)', url: '/guides/auth/third-party/aws-cognito' },
@@ -33,8 +33,9 @@ import {
import { useCopy } from '~/hooks/useCopy'
import { useBranchesQuery } from '~/lib/fetch/branches'
import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
import { useProjectApiQuery } from '~/lib/fetch/projectApi'
import { useProjectsQuery } from '~/lib/fetch/projects'
import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects'
import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
@@ -59,6 +60,7 @@ type VariableDataState =
| 'userLoading'
| 'loggedOut'
| 'loggedIn.noSelectedProject'
| 'loggedIn.selectedProject.projectPaused'
| 'loggedIn.selectedProject.dataPending'
| 'loggedIn.selectedProject.dataError'
| 'loggedIn.selectedProject.dataSuccess'
@@ -187,18 +189,19 @@ function BranchSelector() {
const { selectedProject, selectedBranch, setSelectedBranch } = useSnapshot(projectsStore)
const projectPaused = isProjectPaused(selectedProject)
const hasBranches = selectedProject?.is_branch_enabled ?? false
const { data, isPending, isError } = useBranchesQuery(
{ projectRef: selectedProject?.ref },
{ enabled: isLoggedIn && hasBranches }
{ enabled: isLoggedIn && !projectPaused && hasBranches }
)
const stateSummary: BranchesDataState = userLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !hasBranches
: !hasBranches || projectPaused
? 'loggedIn.noBranches'
: isPending
? 'loggedIn.branches.dataPending'
@@ -265,22 +268,36 @@ function VariableView({ variable, className }: { variable: Variable; className?:
const isLoggedIn = useIsLoggedIn()
const { selectedProject, selectedBranch } = useSnapshot(projectsStore)
const projectPaused = isProjectPaused(selectedProject)
const hasBranches = selectedProject?.is_branch_enabled ?? false
const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref
const needsApiQuery = variable === 'anonKey' || variable === 'url'
const needsSupavisorQuery = variable === 'sessionPooler'
const {
data: apiData,
isPending,
isError,
isPending: isApiPending,
isError: isApiError,
} = useProjectApiQuery(
{
projectRef: ref,
},
{ enabled: isLoggedIn && !!ref }
{ enabled: isLoggedIn && !!ref && !projectPaused && needsApiQuery }
)
function isInvalid(apiData: ProjectApiData) {
const {
data: supavisorConfig,
isPending: isSupavisorPending,
isError: isSupavisorError,
} = useSupavisorConfigQuery(
{
projectRef: ref,
},
{ enabled: isLoggedIn && !!ref && !projectPaused && needsSupavisorQuery }
)
function isInvalidApiData(apiData: ProjectApiData) {
switch (variable) {
case 'url':
return !apiData.app_config?.endpoint
@@ -289,17 +306,27 @@ function VariableView({ variable, className }: { variable: Variable; className?:
}
}
function isInvalidSupavisorData(supavisorData: SupavisorConfigData) {
return supavisorData.length === 0
}
const stateSummary: VariableDataState = isUserLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !ref
? 'loggedIn.noSelectedProject'
: isPending
? 'loggedIn.selectedProject.dataPending'
: isError || isInvalid(apiData)
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
: projectPaused
? 'loggedIn.selectedProject.projectPaused'
: (needsApiQuery ? isApiPending : isSupavisorPending)
? 'loggedIn.selectedProject.dataPending'
: (
needsApiQuery
? isApiError || isInvalidApiData(apiData)
: isSupavisorError || isInvalidSupavisorData(supavisorConfig)
)
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
let variableValue: string = null
if (stateSummary === 'loggedIn.selectedProject.dataSuccess') {
@@ -310,6 +337,8 @@ function VariableView({ variable, className }: { variable: Variable; className?:
case 'anonKey':
variableValue = apiData.service_api_keys!.find((key) => key.tags === 'anon')!.api_key
break
case 'sessionPooler':
variableValue = supavisorConfig[0].connection_string
}
}
@@ -326,9 +355,11 @@ function VariableView({ variable, className }: { variable: Variable; className?:
stateSummary === 'userLoading' ||
stateSummary === 'loggedIn.selectedProject.dataPending'
? 'Loading...'
: stateSummary === 'loggedIn.selectedProject.dataSuccess'
? variableValue
: `YOUR ${prettyFormatVariable[variable].toUpperCase()}`
: stateSummary === 'loggedIn.selectedProject.projectPaused'
? 'PROJECT PAUSED'
: stateSummary === 'loggedIn.selectedProject.dataSuccess'
? variableValue
: `YOUR ${prettyFormatVariable[variable].toUpperCase()}`
}
/>
<CopyToClipboard text={variableValue ?? ''}>
@@ -6,7 +6,7 @@ export type Org = OrganizationsData[number]
export type Project = ProjectsData[number]
export type Branch = BranchesData[number]
export type Variable = 'url' | 'anonKey'
export type Variable = 'url' | 'anonKey' | 'sessionPooler'
function removeDoubleQuotes(str: string) {
return str.replaceAll('"', '')
@@ -31,6 +31,7 @@ function unescapeDoubleQuotes(str: string) {
export const prettyFormatVariable: Record<Variable, string> = {
url: 'Project URL',
anonKey: 'Anon key',
sessionPooler: 'Connection string (pooler session mode)',
}
type DeepReadonly<T> = {
@@ -68,7 +68,7 @@ Below is information about your project's SAML 2.0 configuration which you can s
| SLO URL | `https://<project>.supabase.co/auth/v1/sso/slo` |
| `NameID` | Required `emailAddress` or `persistent` |
Note that SLO (Single Logout) is not supported at this time with Supabase Auth as it is a rarely supported feature by identity providers. However, the URL is registered and advertised for when this does become available.
Note that SLO (Single Logout) is not supported at this time with Supabase Auth as it is a rarely supported feature by identity providers. However, the URL is registered and advertised for when this does become available. SLO is a best-effort service, so we recommend considering [Session Timebox or Session Inactivity Timeout](/docs/guides/auth/sessions#limiting-session-lifetime-and-number-of-allowed-sessions-per-user) instead to force your end-users to authenticate regularly.
Append `?download=true` to the Metadata URL to download the Metadata XML file. This is useful in cases where the identity provider requires a file.
+143
View File
@@ -0,0 +1,143 @@
---
id: 'auth-third-party-clerk'
title: 'Clerk'
subtitle: 'Use Clerk with your Supabase project'
---
Clerk can be used as a third-party authentication provider alongside Supabase Auth, or standalone, with your Supabase project.
## Getting started
Getting started is incredibly easy. Start off by visiting [Clerk's Connect with Supabase page](https://dashboard.clerk.com/setup/supabase) to configure your Clerk instance for Supabase compatibility.
Finally add a [new Third-Party Auth integration with Clerk](/dashboard/project/_/auth/third-party) in the Supabase dashboard.
### Configure for local development or self-hosting
When developing locally or self-hosting with the Supabase CLI, add the following config to your `supabase/config.toml` file:
```toml
[auth.third_party.clerk]
enabled = true
domain = "example.clerk.accounts.dev"
```
You will still need to configure your Clerk instance for Supabase compatibility.
### Manually configuring your Clerk instance
If you are not able to use [Clerk's Connect with Supabase page](https://dashboard.clerk.com/setup/supabase) to configure your Clerk instance for working with Supabase, follow these steps.
1. Add the `role` claim to [Clerk session tokens](https://clerk.com/docs/backend-requests/resources/session-tokens) by [customizing them](https://clerk.com/docs/backend-requests/custom-session-token). End-users who are authenticated should have the `authenticated` value for the claim. If you have an advanced Postgres setup where authenticated end-users use different Postgres roles to access the database, adjust the value to use the correct role name.
2. Once all Clerk session tokens for your instance contain the `role` claim, add a [new Third-Party Auth integration with Clerk](/dashboard/project/_/auth/third-party) in the Supabase dashboard or register it in the CLI as instructed above.
## Setup the Supabase client library
<Tabs type="underlined" queryGroup="language">
<TabPanel id="ts" label="TypeScript">
```typescript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://<supabase-project>.supabase.co', 'SUPABASE_ANON_KEY', {
accessToken: () => {
return Clerk.session?.getToken()
},
})
```
</TabPanel>
<TabPanel id="dart" label="Flutter">
```dart
import 'package:clerk_flutter/clerk_flutter.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
...
await Supabase.initialize(
url: 'SUPABASE_URL',
anonKey: 'SUPABASE_ANON_KEY',
accessToken: () async {
final token = await ClerkAuth.of(context).sessionToken();
return token.jwt;
},
);
```
</TabPanel>
<TabPanel id="swift" label="Swift (iOS)">
```swift
import Clerk
import Supabase
let supabase = SupabaseClient(
supabaseURL: URL(string: "https://project-ref.supabase.io")!,
supabaseKey: "supabase.anon.key",
options: SupabaseClientOptions(
auth: SupabaseClientOptions.AuthOptions(
accessToken: {
try await Clerk.shared.session?.getToken()?.jwt
}
)
)
)
```
</TabPanel>
</Tabs>
## Using RLS policies
Once you've configured the Supabase client library to use Clerk session tokens, you can use RLS policies to secure access to your project's database, Storage objects and Realtime channels.
The recommended way to design RLS policies with Clerk is to use claims present in your Clerk session token to allow or reject access to your project's data. Check [Clerk's docs](https://clerk.com/docs/backend-requests/resources/session-tokens) on the available JWT claims and their values.
### Example: Check user organization role
```sql
create policy "Only organization admins can insert in table"
on table_name
for insert
to authenticated
with check (
((select auth.jwt()->>'org_role') = 'org:admin')
and
(organization_id = (select auth.jwt()->>'org_id'))
);
```
This RLS policy checks that the newly inserted row in the table has the user's declared organization ID (from the `org_id` session token claim) in the `organization_id` column. Additionally it ensures that they're an `org:admin`.
This way only organization admins can add rows to the table, for organizations they're a member of.
### Example: Check user has passed second factor verification
```sql
create policy "Only users that have passed second factor verification can read from table"
on table_name
for select
as restrictive
to authenticated
using (
((select auth.jwt()->'fva'->>1) != '-1')
);
```
This example uses a restrictive RLS policy checks that the [second factor verification](https://clerk.com/docs/guides/reverification) age element in the `fva` claim is not `'-1'` indicating the user has passed through second factor verification.
## Deprecated integration with JWT templates
As of 1st April 2025 the previously available [Clerk Integration with Supabase](https://supabase.com/partners/integrations/clerk) is considered deprecated and is no longer recommended for use. All projects using the deprecated integration will be excluded from Third-Party Monthly Active User (TP-MAU) charges until at least 1st January 2026.
This integration used low-level primitives that are still available in Supabase and Clerk, such as a [configurable JWT secret](/dashboard/project/_/settings/api) and [JWT templates from Clerk](https://clerk.com/docs/backend-requests/jwt-templates). This enables you to keep using it in an unofficial manner, though only limited support will be provided from Supabase.
Deprecation is done for the following reasons:
- Sharing your project's JWT secret with a third-party is a problematic security practice
- Rotating the project's JWT secret in this case almost always results in significant downtime for your application
- Additional latency to [generate a new JWT](https://clerk.com/docs/backend-requests/jwt-templates#generate-a-jwt) for use with Supabase, instead of using the Clerk [session tokens](https://clerk.com/docs/backend-requests/resources/session-tokens)
@@ -27,7 +27,7 @@ import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://<supabase-project>.supabase.co', 'SUPABASE_ANON_KEY', {
accessToken: async () => {
return await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null
return (await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null
},
})
```
@@ -6,6 +6,7 @@ subtitle: 'First-class support for authentication providers'
Supabase has first-class support for these third-party authentication providers:
- [Clerk](/docs/guides/auth/third-party/clerk)
- [Firebase Auth](/docs/guides/auth/third-party/firebase-auth)
- [Auth0](/docs/guides/auth/third-party/auth0)
- [AWS Cognito (with or without AWS Amplify)](/docs/guides/auth/third-party/aws-cognito)
@@ -50,7 +50,7 @@ After developing your project and deciding it's Production Ready, you should run
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Settings > Auth)
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. You can refer to our [SMTP guide](/docs/guides/auth/auth-smtp) for more details.
- The default rate limit for auth emails when using a custom SMTP provider is 30 new users per hour, if doing a major public announcement you will likely require more than this.
- If your application is on the Free Plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources.
- Applications on the Free Plan that exhibit extremely low activity in a 7 day period may be paused by Supabase to save on server resources.
- You can restore paused projects from the Supabase dashboard.
- Upgrade to Pro to guarantee that your project will not be paused for inactivity.
- Database backups are not available for download on the Free Plan.
@@ -27,11 +27,19 @@ To get started, you will need to retrieve your database connection string. These
#### For a hosted Supabase instance
When running a hosted instance of Supabase, you can find your connection string by:
<ProjectConfigVariables variable="sessionPooler" />
Log in and choose a project above to find your connection string, or find it in the Dashboard:
1. Navigating to your project's [Connection settings](/dashboard/project/_/settings/database?showConnect=true)
1. Copying the connection string found under **Session pooler**.
<Admonition type="note">
Be sure to replace `[YOUR-PASSWORD]` with your database password.
</Admonition>
#### For a local Supabase instance
When running a local instance of Supabase via the [CLI](/docs/reference/cli/introduction), you can find your connection string by running:
@@ -236,6 +236,24 @@ supabase db dump > supabase/schemas/prod.sql
From there, you can start breaking down your schema into smaller files and generate migrations. You can do this all at once, or incrementally as you make changes to your schema.
### Rolling back a schema change
During development, you may want to rollback a migration to keep your new schema changes in a single migration file. This can be done by resetting your local database to a previous version.
```bash Terminal
supabase db reset --version 20241005112233
```
After a reset, you can [edit the schema](#updating-your-schema) and regenerate a new migration file. Note that you should not reset a version that's already deployed to production.
If you need to rollback a migration that's already deployed, you should first revert changes to the schema files. Then you can generate a new migration file containing the down migration. This ensures your production migrations are always rolling forward.
<Admonition type="danger">
SQL statements generated in a down migration are usually destructive. You must review them carefully to avoid unintentional data loss.
</Admonition>
## Known caveats
The `migra` diff tool used for generating schema diff is capable of tracking most database changes. However, there are edge cases where it can fail.
+42
View File
@@ -0,0 +1,42 @@
import { type UseQueryOptions, useQuery } from '@tanstack/react-query'
import type { ResponseError } from '~/types/fetch'
import { get } from './fetchWrappers'
const poolerKeys = {
supavisorConfig: (projectRef: string | undefined) => ['supavisor', 'config', projectRef] as const,
}
export interface SupavisorConfigVariables {
projectRef?: string
}
async function getSupavisorConfig({ projectRef }: SupavisorConfigVariables, signal?: AbortSignal) {
if (!projectRef) throw Error('projectRef is required')
const { data, error } = await get(`/platform/projects/{ref}/config/supavisor`, {
params: { path: { ref: projectRef } },
signal,
})
if (error) throw error
return data
}
export type SupavisorConfigData = Awaited<ReturnType<typeof getSupavisorConfig>>
type SupavisorConfigError = ResponseError
export function useSupavisorConfigQuery<TData = SupavisorConfigData>(
{ projectRef }: SupavisorConfigVariables,
{
enabled = true,
...options
}: Omit<UseQueryOptions<SupavisorConfigData, SupavisorConfigError, TData>, 'queryKey'>
) {
return useQuery<SupavisorConfigData, SupavisorConfigError, TData>({
queryKey: poolerKeys.supavisorConfig(projectRef),
queryFn: ({ signal }) => getSupavisorConfig({ projectRef }, signal),
enabled,
...options,
})
}
+7
View File
@@ -1,6 +1,7 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { ResponseError } from '~/types/fetch'
import { get } from './fetchWrappers'
import { type ReadonlyRecursive } from '~/types/utils'
const projectKeys = {
list: () => ['all-projects'] as const,
@@ -26,3 +27,9 @@ export function useProjectsQuery<TData = ProjectsData>({
...options,
})
}
export function isProjectPaused(
project: ReadonlyRecursive<ProjectsData[number]> | null
): boolean | undefined {
return !project ? undefined : project.status === 'INACTIVE'
}
+3
View File
@@ -0,0 +1,3 @@
export type ReadonlyRecursive<T> = {
readonly [P in keyof T]: ReadonlyRecursive<T[P]>
}
@@ -1,8 +1,6 @@
import { ChevronDown } from 'lucide-react'
import Image from 'next/image'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useFlag } from 'hooks/ui/useFlag'
import {
Button,
cn,
@@ -49,17 +47,6 @@ const ProviderDropdownItem = ({
export const AddIntegrationDropdown = ({
onSelectIntegrationType,
}: AddIntegrationDropdownProps) => {
const selectedProject = useSelectedProject()
const isClerkTPAEnabledFlag = useFlag<string>('isClerkTPAEnabledOnProjects')
const isClerkTPAEnabled =
selectedProject?.ref &&
isClerkTPAEnabledFlag &&
isClerkTPAEnabledFlag
.split(',')
.map((it) => it.trim())
.includes(selectedProject.ref)
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
@@ -72,10 +59,7 @@ export const AddIntegrationDropdown = ({
<DropdownMenuSeparator />
<ProviderDropdownItem type="firebase" onSelectIntegrationType={onSelectIntegrationType} />
{isClerkTPAEnabled && (
<ProviderDropdownItem type="clerk" onSelectIntegrationType={onSelectIntegrationType} />
)}
<ProviderDropdownItem type="clerk" onSelectIntegrationType={onSelectIntegrationType} />
<ProviderDropdownItem type="auth0" onSelectIntegrationType={onSelectIntegrationType} />
<ProviderDropdownItem type="awsCognito" onSelectIntegrationType={onSelectIntegrationType} />
</DropdownMenuContent>
@@ -106,8 +106,11 @@ export const CreateClerkAuthIntegrationDialog = ({
<Form_Shadcn_ {...form}>
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<p className="text-sm text-foreground-light">
This will enable a Clerk session token (JWT) from a specific access data from this
Supabase project.{' '}
Register your Clerk domain. Visit{' '}
<a href="https://dashboard.clerk.com/setup/supabase" target="_blank" rel="noopener">
Clerk's Connect with Supabase page
</a>{' '}
to configure your Clerk instance.
</p>
<FormField_Shadcn_
key="domain"
@@ -119,7 +122,7 @@ export const CreateClerkAuthIntegrationDialog = ({
<Input_Shadcn_
{...field}
placeholder={
'https://clerk.example.com / https://example.clerk.accounts.dev'
'https://clerk.example.com or https://example.clerk.accounts.dev'
}
/>
</FormControl_Shadcn_>
@@ -340,7 +340,7 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
<div className={cn('flex flex-col -space-y-1 !pt-0', PANEL_PADDING)}>
<RowAction
title="Remove MFA factors"
description="This will log the user out of all active sessions"
description="Removes all MFA factors associated with the user"
button={{
icon: <ShieldOff />,
text: 'Remove MFA factors',
@@ -409,12 +409,13 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
onConfirm={() => handleDeleteFactors()}
alert={{
base: { variant: 'warning' },
title: 'Removing MFA factors is irreversible',
description: 'This will log the user out of all active sessions.',
title:
"Removing MFA factors will drop the user's authentication assurance level (AAL) to AAL1",
description: 'Note that this does not sign the user out',
}}
>
<p className="text-sm text-foreground-light">
This is permanent! Are you sure you want to remove the MFA factors for the user{' '}
Are you sure you want to remove the MFA factors for the user{' '}
<span className="text-foreground">{user.email ?? user.phone ?? 'this user'}</span>?
</p>
</ConfirmationModal>
@@ -104,8 +104,8 @@ export const calculateIOPSPrice = ({
0,
newProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops
)
const oldPrice = oldChargeableIOPS * DISK_PRICING[oldStorageType]?.iops ?? 0
const newPrice = newChargeableIOPS * DISK_PRICING[newStorageType]?.iops ?? 0
const oldPrice = oldChargeableIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
const newPrice = newChargeableIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
return {
oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
@@ -116,8 +116,8 @@ export const calculateIOPSPrice = ({
oldStorageType === 'gp3'
? (oldProvisionedIOPS - DISK_LIMITS[oldStorageType].includedIops) *
DISK_PRICING[oldStorageType].iops
: oldProvisionedIOPS * DISK_PRICING[oldStorageType]?.iops ?? 0
const newPrice = newProvisionedIOPS * DISK_PRICING[newStorageType]?.iops ?? 0
: oldProvisionedIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
const newPrice = newProvisionedIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
return {
oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
@@ -242,7 +242,10 @@ export function DiskManagementForm() {
) {
willUpdateDiskConfiguration = true
if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(payload.computeSize)) {
if (
payload.storageType === 'gp3' &&
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(payload.computeSize)
) {
payload.provisionedIOPS = IOPS_RANGE[DiskType.GP3].min
}
@@ -382,6 +382,12 @@ export const DiskManagementReviewAndSubmitDialog = ({
</TableBody>
</Table>
{numReplicas > 0 && (
<div className="border-t px-4 py-2 text-sm text-foreground-lighter">
{replicaTooltipText}
</div>
)}
<DialogFooter>
<Button block size="large" type="default" onClick={() => setIsDialogOpen(false)}>
Cancel
@@ -0,0 +1,30 @@
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
interface DeployEdgeFunctionWarningModalProps {
visible: boolean
onCancel: () => void
onConfirm: () => void
}
export const DeployEdgeFunctionWarningModal = ({
visible,
onCancel,
onConfirm,
}: DeployEdgeFunctionWarningModalProps) => {
return (
<ConfirmationModal
visible={visible}
size="medium"
title="Confirm deploying updates"
confirmLabel="Deploy updates"
variant="warning"
onCancel={onCancel}
onConfirm={onConfirm}
>
<p className="text-sm text-foreground-light">
Deploying will immediately update your live Edge Function for this project and cannot be
rolled back automatically. Are you sure you want to deploy the changes?
</p>
</ConfirmationModal>
)
}
@@ -14,7 +14,7 @@ console.info('server started');
Deno.serve(async (req: Request) => {
const { name }: reqPayload = await req.json();
const data = {
message: \`Hello \${name} from foo!\`,
message: \`Hello \${name}!\`,
};
return new Response(
@@ -39,6 +39,7 @@ Deno.serve(async (req) => {
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } }
)
// TODO: Change the table_name to your table
const { data, error } = await supabase.from('table_name').select('*')
if (error) {
@@ -50,7 +51,10 @@ Deno.serve(async (req) => {
status: 200,
})
} catch (err) {
return new Response(String(err?.message ?? err), { status: 500 })
return new Response(JSON.stringify({ message: err?.message ?? err }), {
headers: { 'Content-Type': 'application/json' },
status: 500
})
}
})`,
},
@@ -61,6 +65,7 @@ Deno.serve(async (req) => {
content: `// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { randomUUID } from 'node:crypto'
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
@@ -70,11 +75,13 @@ const supabase = createClient(
Deno.serve(async (req) => {
const formData = await req.formData()
const file = formData.get('file')
// TODO: update your-bucket to the bucket you want to write files
const { data, error } = await supabase
.storage
.from('your-bucket')
.upload(
\`files/\${crypto.randomUUID()}\`,
\`\${file.name}-\${randomUUID()}\`,
file,
{ contentType: file.type }
)
@@ -120,7 +127,9 @@ import express from "npm:express@4.18.2";
const app = express();
app.get(/(.*)/, (req, res) => {
// TODO: replace slug with Function's slug
// https://supabase.com/docs/guides/functions/routing?queryGroups=framework&framework=expressjs
app.get(/slug/(.*)/, (req, res) => {
res.send("Welcome to Supabase");
});
@@ -161,32 +170,38 @@ Deno.serve(async (req) => {
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import Stripe from 'npm:stripe@12.0.0'
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY')!)
const endpointSecret = Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string, {
// This is needed to use the Fetch API rather than relying on the Node http
// package.
apiVersion: '2024-11-20'
})
Deno.serve(async (req) => {
const signature = req.headers.get('stripe-signature')
// This is needed in order to use the Web Crypto API in Deno.
const cryptoProvider = Stripe.createSubtleCryptoProvider()
console.log('Stripe Webhook Function booted!')
Deno.serve(async (request) => {
const signature = request.headers.get('Stripe-Signature')
// First step is to verify the event. The .text() method must be used as the
// verification relies on the raw request body rather than the parsed JSON.
const body = await request.text()
let receivedEvent
try {
const event = stripe.webhooks.constructEvent(
await req.text(),
receivedEvent = await stripe.webhooks.constructEventAsync(
body,
signature!,
endpointSecret
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
undefined,
cryptoProvider
)
switch (event.type) {
case 'payment_intent.succeeded':
// Handle successful payment
break
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' },
})
} catch (err) {
return new Response(
JSON.stringify({ error: err.message }),
{ status: 400 }
)
return new Response(err.message, { status: 400 })
}
})`,
console.log(\`🔔 Event received: \${receivedEvent.id}\`)
return new Response(JSON.stringify({ ok: true }), { status: 200 })
});`,
},
{
value: 'resend-email',
@@ -244,35 +259,6 @@ Deno.serve(async (req) => {
result,
{ headers: { 'Content-Type': 'image/png' }}
)
})`,
},
{
value: 'discord-bot',
name: 'Discord Bot Example',
description: 'Build a Slash Command Discord Bot',
content: `// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { verifyDiscordRequest } from './_shared/discord.ts'
Deno.serve(async (req) => {
const { valid } = await verifyDiscordRequest(req)
if (!valid) {
return new Response('Invalid request', { status: 401 })
}
const message = await req.json()
if (message.type === 1) {
return new Response(
JSON.stringify({ type: 1 }),
{ headers: { 'Content-Type': 'application/json' }}
)
}
return new Response(
JSON.stringify({
type: 4,
data: { content: 'Hello from Supabase Edge Functions!' }
}),
{ headers: { 'Content-Type': 'application/json' }}
)
})`,
},
{
@@ -35,6 +35,7 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio
const method = log?.metadata?.[0]?.request?.[0]?.method
const path = log?.metadata?.[0]?.request?.[0]?.path
const user_agent = log?.metadata?.[0]?.request?.[0]?.headers[0].user_agent
const error_code = log?.metadata?.[0]?.response?.[0]?.headers?.[0]?.x_sb_error_code
const { id, metadata, timestamp, event_message, ...rest } = log
const apiLog = {
@@ -46,6 +47,7 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio
timestamp,
event_message,
metadata,
...(error_code ? { error_code } : null),
...rest,
}
@@ -1,9 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Send } from 'lucide-react'
import { Download, FileArchive, Send } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet'
@@ -11,13 +12,21 @@ import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import APIDocsButton from 'components/ui/APIDocsButton'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { withAuth } from 'hooks/misc/withAuth'
import { useFlag } from 'hooks/ui/useFlag'
import { Button } from 'ui'
import {
Button,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Separator,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
@@ -49,6 +58,11 @@ const EdgeFunctionDetailsLayout = ({
isError,
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const { data: functionFiles = [], error: filesError } = useEdgeFunctionBodyQuery({
projectRef: ref,
slug: functionSlug,
})
const name = selectedFunction?.name || ''
const breadcrumbItems = [
@@ -87,6 +101,29 @@ const EdgeFunctionDetailsLayout = ({
]
: []
const downloadFunction = async () => {
if (filesError) return toast.error('Failed to retrieve edge function files')
const zipFileWriter = new BlobWriter('application/zip')
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
functionFiles.forEach((file) => {
const nameSections = file.name.split('/')
const slugIndex = nameSections.indexOf(functionSlug ?? '')
const fileName = nameSections.slice(slugIndex + 1).join('/')
const fileBlob = new Blob([file.content])
zipWriter.add(fileName, new BlobReader(fileBlob))
})
const blobURL = URL.createObjectURL(await zipWriter.close())
const link = document.createElement('a')
link.href = blobURL
link.setAttribute('download', `${functionSlug}.zip`)
document.body.appendChild(link)
link.click()
link.parentNode?.removeChild(link)
}
useEffect(() => {
let cancel = false
@@ -126,6 +163,37 @@ const EdgeFunctionDetailsLayout = ({
/>
)}
<DocsButton href="https://supabase.com/docs/guides/functions" />
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button type="default" icon={<Download />}>
Download
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ align="end" className="p-0">
<div className="p-3 flex flex-col gap-y-2">
<p className="text-xs text-foreground-light">Download via CLI</p>
<Input
copy
showCopyOnHover
readOnly
containerClassName=""
className="text-xs font-mono tracking-tighter"
value={`supabase functions download ${functionSlug}`}
/>
</div>
<Separator className="!bg-border-overlay" />
<div className="py-2 px-1">
<Button
type="text"
className="w-min hover:bg-transparent"
icon={<FileArchive />}
onClick={downloadFunction}
>
Download as ZIP
</Button>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
{edgeFunctionCreate && !!functionSlug && (
<Button
type="default"
@@ -1,6 +1,6 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { handleError, constructHeaders } from 'data/fetchers'
import { IS_PLATFORM, BASE_PATH } from 'lib/constants'
import { constructHeaders, handleError } from 'data/fetchers'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { ResponseError } from 'types'
import { edgeFunctionsKeys } from './keys'
@@ -46,7 +46,7 @@ export async function getEdgeFunctionBody(
}
const { files } = await parseResponse.json()
return files
return files as EdgeFunctionFile[]
} catch (error) {
console.error('Failed to parse edge function code:', error)
throw new Error(
@@ -64,7 +64,7 @@ export const useEdgeFunctionDeployMutation = ({
async onSuccess(data, variables, context) {
const { projectRef, slug } = variables
await Promise.all([
queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef)),
queryClient.invalidateQueries(edgeFunctionsKeys.detail(projectRef, slug)),
queryClient.invalidateQueries(edgeFunctionsKeys.body(projectRef, slug)),
])
await onSuccess?.(data, variables, context)
+1 -1
View File
@@ -1,7 +1,7 @@
export const edgeFunctionsKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
detail: (projectRef: string | undefined, slug: string | undefined) =>
['projects', projectRef, 'edge-function', slug] as const,
['projects', projectRef, 'edge-function', slug, 'detail'] as const,
body: (projectRef: string | undefined, slug: string | undefined) =>
['projects', projectRef, 'edge-function', slug, 'body'] as const,
}
+1 -22
View File
@@ -4,28 +4,7 @@ import path from 'path'
function url2path(url: string) {
try {
// Parse the URL
const parsedUrl = new URL(url)
// Get the pathname and split it
const pathParts = parsedUrl.pathname.split('/').filter(Boolean)
// For user function paths like "user_fn_ukekbxwcwofeosgskskm_66727868-6415-49d7-b518-2a057c8377cb_1/data.json"
// Just return the filename (data.json)
if (pathParts.length > 0) {
// Check if it's a user function path with UUID
if (pathParts[0].startsWith('user_fn_')) {
// Get just the filename (last part)
return pathParts[pathParts.length - 1]
}
// If it starts with 'tmp/' or similar, remove that prefix
if (pathParts[0] === 'tmp' || pathParts[0] === 'deno') {
// Get just the filename (last part)
return pathParts[pathParts.length - 1]
}
}
// Otherwise, return the full path without leading/trailing slashes
return pathParts.join('/')
return new URL(url).pathname
} catch (error) {
// If URL parsing fails, fallback to extracting just the filename
console.warn('Failed to parse URL:', url)
+1
View File
@@ -40,6 +40,7 @@
"@next/bundle-analyzer": "^14.2.3",
"@number-flow/react": "^0.3.2",
"@sentry/nextjs": "^8.52.1",
"@std/path": "npm:@jsr/std__path@^1.0.8",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.5.0",
"@supabase/auth-js": "catalog:",
@@ -1,23 +1,27 @@
import { common, dirname, relative } from '@std/path/posix'
import { AlertCircle, CornerDownLeft, Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import LogoLoader from '@ui/components/LogoLoader'
import { useParams } from 'common'
import { DeployEdgeFunctionWarningModal } from 'components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal'
import DefaultLayout from 'components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import FileExplorerAndEditor from 'components/ui/FileExplorerAndEditor/FileExplorerAndEditor'
import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useFlag } from 'hooks/ui/useFlag'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { Button } from 'ui'
const CodePage = () => {
const router = useRouter()
@@ -28,24 +32,36 @@ const CodePage = () => {
const edgeFunctionCreate = useFlag('edgeFunctionCreate')
const { mutate: sendEvent } = useSendEventMutation()
const org = useSelectedOrganization()
const [showDeployWarning, setShowDeployWarning] = useState(false)
const canDeployFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const {
data: functionFiles,
isLoading: isLoadingFiles,
isError: isErrorLoadingFiles,
isSuccess: isSuccessLoadingFiles,
error: filesError,
} = useEdgeFunctionBodyQuery({
projectRef: ref,
slug: functionSlug,
})
} = useEdgeFunctionBodyQuery(
{
projectRef: ref,
slug: functionSlug,
},
{
retry: false,
refetchOnWindowFocus: false,
retryOnMount: false,
}
)
const [files, setFiles] = useState<
{ id: number; name: string; content: string; selected?: boolean }[]
>([])
const { mutateAsync: deployFunction, isLoading: isDeploying } = useEdgeFunctionDeployMutation({
const { mutate: deployFunction, isLoading: isDeploying } = useEdgeFunctionDeployMutation({
onSuccess: () => {
toast.success('Successfully updated edge function')
setShowDeployWarning(false)
},
})
@@ -53,21 +69,46 @@ const CodePage = () => {
if (isDeploying || !ref || !functionSlug || !selectedFunction || files.length === 0) return
try {
const parsedEntrypointPath =
selectedFunction.entrypoint_path?.replace(/\/tmp\/user_fn_[^/]+\//, '') || 'index.ts'
const existingFile = files.find((file) => file.name === parsedEntrypointPath) ||
files.find((file) => file.name.endsWith('.ts') || file.name.endsWith('.js')) || {
name: 'index.ts',
}
const newEntrypointPath = selectedFunction.entrypoint_path?.split('/').pop()
const newImportMapPath = selectedFunction.import_map_path?.split('/').pop()
await deployFunction({
const fallbackEntrypointPath = () => {
// when there's no matching entrypoint path is set,
// we use few heuristics to find an entrypoint file
// 1. If the function has only a single TS / JS file, if so set it as entrypoint
const jsFiles = files.filter(({ name }) => name.endsWith('.js') || name.endsWith('.ts'))
if (jsFiles.length === 1) {
return jsFiles[0].name
} else if (jsFiles.length) {
// 2. If function has a `index` or `main` file use it as the entrypoint
const regex = /^.*?(index|main).*$/i
const matchingFile = jsFiles.find(({ name }) => regex.test(name))
// 3. if no valid index / main file found, we set the entrypoint expliclty to first JS file
return matchingFile ? matchingFile.name : jsFiles[0].name
} else {
// no potential entrypoint files found, this will most likely result in an error on deploy
return 'index.ts'
}
}
const fallbackImportMapPath = () => {
// try to find a deno.json or import_map.json file
const regex = /^.*?(deno|import_map).json*$/i
return files.find(({ name }) => regex.test(name))?.name
}
deployFunction({
projectRef: ref,
slug: selectedFunction.slug,
metadata: {
name: selectedFunction.name,
verify_jwt: selectedFunction.verify_jwt,
entrypoint_path: existingFile.name,
import_map_path: selectedFunction.import_map_path,
entrypoint_path: files.some(({ name }) => name === newEntrypointPath)
? (newEntrypointPath as string)
: fallbackEntrypointPath(),
import_map_path: files.some(({ name }) => name === newImportMapPath)
? newImportMapPath
: fallbackImportMapPath(),
},
files: files.map(({ name, content }) => ({ name, content })),
})
@@ -78,17 +119,93 @@ const CodePage = () => {
}
}
const renderContent = () => {
if (isLoadingFiles) {
return (
function getBasePath(entrypoint: string | undefined): string {
if (!entrypoint) {
return '/'
}
try {
return dirname(new URL(entrypoint).pathname)
} catch (e) {
console.error('Failed to parse entrypoint', entrypoint)
return '/'
}
}
const handleDeployClick = () => {
if (files.length === 0 || isLoadingFiles) return
setShowDeployWarning(true)
sendEvent({
action: 'edge_function_deploy_updates_button_clicked',
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}
const handleDeployConfirm = () => {
sendEvent({
action: 'edge_function_deploy_updates_confirm_clicked',
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
onUpdate()
}
// TODO (Saxon): Remove this once the flag is fully launched
useEffect(() => {
if (edgeFunctionCreate !== undefined && !edgeFunctionCreate) {
router.push(`/project/${ref}/functions`)
}
}, [edgeFunctionCreate])
useEffect(() => {
// Set files from API response when available
if (selectedFunction?.entrypoint_path && functionFiles) {
const base_path = getBasePath(selectedFunction?.entrypoint_path)
const filesWithRelPath = functionFiles
// ignore empty files
.filter((file: { name: string; content: string }) => !!file.content.length)
// set file paths relative to entrypoint
.map((file: { name: string; content: string }) => {
try {
// if the current file and base path doesn't share a common path,
// return unmodified file
const common_path = common([base_path, file.name])
if (common_path === '' || common_path === '/tmp/') {
return file
}
file.name = relative(base_path, file.name)
return file
} catch (e) {
console.error(e)
// return unmodified file
return file
}
})
setFiles((prev) => {
return filesWithRelPath.map((file: { name: string; content: string }, index: number) => {
const prevState = prev.find((x) => x.name === file.name)
return {
id: index + 1,
name: file.name,
content: file.content,
selected: prevState?.selected ?? index === 0,
}
})
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [functionFiles])
return (
<div className="flex flex-col h-full">
{isLoadingFiles && (
<div className="flex flex-col items-center justify-center h-full bg-surface-200">
<LogoLoader />
</div>
)
}
)}
if (isErrorLoadingFiles) {
return (
{isErrorLoadingFiles && (
<div className="flex flex-col items-center justify-center h-full bg-surface-200">
<div className="flex flex-col items-center text-center gap-2 max-w-md">
<AlertCircle size={24} strokeWidth={1.5} className="text-amber-900" />
@@ -99,75 +216,55 @@ const CodePage = () => {
</p>
</div>
</div>
)
}
return (
<FileExplorerAndEditor
files={files}
onFilesChange={setFiles}
aiEndpoint={`${BASE_PATH}/api/ai/edge-function/complete`}
aiMetadata={{
projectRef: project?.ref,
connectionString: project?.connectionString,
includeSchemaMetadata,
}}
/>
)
}
// TODO (Saxon): Remove this once the flag is fully launched
useEffect(() => {
if (!edgeFunctionCreate) {
router.push(`/project/${ref}/functions`)
}
}, [edgeFunctionCreate, ref, router])
useEffect(() => {
// Set files from API response when available
if (functionFiles) {
setFiles(
functionFiles.map((file: { name: string; content: string }, index: number) => ({
id: index + 1,
name: file.name,
content: file.content,
selected: index === 0,
}))
)
}
}, [functionFiles])
return (
<div className="flex flex-col h-full">
{renderContent()}
{!isErrorLoadingFiles && (
<div className="flex items-center bg-background-muted justify-end p-4 border-t bg-surface-100 shrink-0">
<Button
loading={isDeploying}
size="medium"
disabled={files.length === 0 || isLoadingFiles}
onClick={() => {
onUpdate()
sendEvent({
action: 'edge_function_deploy_updates_button_clicked',
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
iconRight={
isDeploying ? (
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
) : (
<div className="flex items-center space-x-1">
<CornerDownLeft size={10} strokeWidth={1.5} />
</div>
)
}
>
Deploy updates
</Button>
</div>
)}
{isSuccessLoadingFiles && (
<>
<FileExplorerAndEditor
files={files}
onFilesChange={setFiles}
aiEndpoint={`${BASE_PATH}/api/ai/edge-function/complete`}
aiMetadata={{
projectRef: project?.ref,
connectionString: project?.connectionString,
includeSchemaMetadata,
}}
/>
<div className="flex items-center bg-background-muted justify-end p-4 border-t bg-surface-100 shrink-0">
<ButtonTooltip
loading={isDeploying}
size="medium"
disabled={!canDeployFunction || files.length === 0 || isLoadingFiles}
onClick={handleDeployClick}
iconRight={
isDeploying ? (
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
) : (
<div className="flex items-center space-x-1">
<CornerDownLeft size={10} strokeWidth={1.5} />
</div>
)
}
tooltip={{
content: {
side: 'top',
text: !canDeployFunction
? 'You need additional permissions to update edge functions'
: undefined,
},
}}
>
Deploy updates
</ButtonTooltip>
</div>
</>
)}
<DeployEdgeFunctionWarningModal
visible={showDeployWarning}
onCancel={() => setShowDeployWarning(false)}
onConfirm={handleDeployConfirm}
/>
</div>
)
}
+54 -6
View File
@@ -11,9 +11,9 @@ export const Index: Record<string, any> = {
name: "password-based-auth-nextjs",
type: "registry:block",
registryDependencies: ["button","card","input","label"],
component: React.lazy(() => import("@/registry/default/blocks/password-based-auth-nextjs/app/login/page.tsx")),
component: React.lazy(() => import("@/registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx")),
source: "",
files: ["registry/default/blocks/password-based-auth-nextjs/app/login/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/error/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/confirm/route.ts","registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx","registry/default/blocks/password-based-auth-nextjs/middleware.ts","registry/default/blocks/password-based-auth-nextjs/app/sign-up/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/sign-up-success/page.tsx","registry/default/blocks/password-based-auth-nextjs/components/sign-up-form.tsx","registry/default/blocks/password-based-auth-nextjs/app/forgot-password/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/update-password/page.tsx","registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx","registry/default/blocks/password-based-auth-nextjs/components/update-password-form.tsx","registry/default/clients/nextjs/lib/supabase/client.ts","registry/default/clients/nextjs/lib/supabase/middleware.ts","registry/default/clients/nextjs/lib/supabase/server.ts"],
files: ["registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/auth/error/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts","registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx","registry/default/blocks/password-based-auth-nextjs/middleware.ts","registry/default/blocks/password-based-auth-nextjs/app/auth/sign-up/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/auth/sign-up-success/page.tsx","registry/default/blocks/password-based-auth-nextjs/components/sign-up-form.tsx","registry/default/blocks/password-based-auth-nextjs/app/auth/forgot-password/page.tsx","registry/default/blocks/password-based-auth-nextjs/app/auth/update-password/page.tsx","registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx","registry/default/blocks/password-based-auth-nextjs/components/update-password-form.tsx","registry/default/blocks/password-based-auth-nextjs/components/logout-button.tsx","registry/default/clients/nextjs/lib/supabase/client.ts","registry/default/clients/nextjs/lib/supabase/middleware.ts","registry/default/clients/nextjs/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
@@ -58,7 +58,7 @@ export const Index: Record<string, any> = {
"dropzone-nextjs": {
name: "dropzone-nextjs",
type: "registry:component",
registryDependencies: ["button","tooltip","progress"],
registryDependencies: ["button"],
component: React.lazy(() => import("@/registry/default/blocks/dropzone/components/dropzone.tsx")),
source: "",
files: ["registry/default/blocks/dropzone/components/dropzone.tsx","registry/default/blocks/dropzone/hooks/use-supabase-upload.ts","registry/default/clients/nextjs/lib/supabase/client.ts","registry/default/clients/nextjs/lib/supabase/middleware.ts","registry/default/clients/nextjs/lib/supabase/server.ts"],
@@ -70,7 +70,7 @@ export const Index: Record<string, any> = {
"dropzone-react": {
name: "dropzone-react",
type: "registry:component",
registryDependencies: ["button","tooltip","progress"],
registryDependencies: ["button"],
component: React.lazy(() => import("@/registry/default/blocks/dropzone/components/dropzone.tsx")),
source: "",
files: ["registry/default/blocks/dropzone/components/dropzone.tsx","registry/default/blocks/dropzone/hooks/use-supabase-upload.ts","registry/default/clients/react/lib/supabase/client.ts"],
@@ -82,7 +82,7 @@ export const Index: Record<string, any> = {
"dropzone-react-router": {
name: "dropzone-react-router",
type: "registry:component",
registryDependencies: ["button","tooltip","progress"],
registryDependencies: ["button"],
component: React.lazy(() => import("@/registry/default/blocks/dropzone/components/dropzone.tsx")),
source: "",
files: ["registry/default/blocks/dropzone/components/dropzone.tsx","registry/default/blocks/dropzone/hooks/use-supabase-upload.ts","registry/default/clients/react-router/lib/supabase/client.ts","registry/default/clients/react-router/lib/supabase/server.ts"],
@@ -94,7 +94,7 @@ export const Index: Record<string, any> = {
"dropzone-tanstack": {
name: "dropzone-tanstack",
type: "registry:component",
registryDependencies: ["button","tooltip","progress"],
registryDependencies: ["button"],
component: React.lazy(() => import("@/registry/default/blocks/dropzone/components/dropzone.tsx")),
source: "",
files: ["registry/default/blocks/dropzone/components/dropzone.tsx","registry/default/blocks/dropzone/hooks/use-supabase-upload.ts","registry/default/clients/tanstack/lib/supabase/client.ts","registry/default/clients/tanstack/lib/supabase/server.ts"],
@@ -247,6 +247,54 @@ export const Index: Record<string, any> = {
chunks: []
}
,
"realtime-chat-nextjs": {
name: "realtime-chat-nextjs",
type: "registry:component",
registryDependencies: ["input","button"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-chat/components/chat-message.tsx")),
source: "",
files: ["registry/default/blocks/realtime-chat/components/chat-message.tsx","registry/default/blocks/realtime-chat/components/realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx","registry/default/clients/nextjs/lib/supabase/client.ts","registry/default/clients/nextjs/lib/supabase/middleware.ts","registry/default/clients/nextjs/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-chat-react": {
name: "realtime-chat-react",
type: "registry:component",
registryDependencies: ["input","button"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-chat/components/chat-message.tsx")),
source: "",
files: ["registry/default/blocks/realtime-chat/components/chat-message.tsx","registry/default/blocks/realtime-chat/components/realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx","registry/default/clients/react/lib/supabase/client.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-chat-react-router": {
name: "realtime-chat-react-router",
type: "registry:component",
registryDependencies: ["input","button"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-chat/components/chat-message.tsx")),
source: "",
files: ["registry/default/blocks/realtime-chat/components/chat-message.tsx","registry/default/blocks/realtime-chat/components/realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx","registry/default/clients/react-router/lib/supabase/client.ts","registry/default/clients/react-router/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-chat-tanstack": {
name: "realtime-chat-tanstack",
type: "registry:component",
registryDependencies: ["input","button"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-chat/components/chat-message.tsx")),
source: "",
files: ["registry/default/blocks/realtime-chat/components/chat-message.tsx","registry/default/blocks/realtime-chat/components/realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx","registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx","registry/default/clients/tanstack/lib/supabase/client.ts","registry/default/clients/tanstack/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"supabase-client-nextjs": {
name: "supabase-client-nextjs",
type: "registry:lib",
@@ -81,18 +81,18 @@ export default async function DocPage({ params }: DocPageProps) {
const toc = await getTableOfContents(doc.body.raw)
return (
<main className="relative lg:gap-10 xl:grid xl:grid-cols-[1fr_200px] pr-6 lg:py-20">
<main className="relative lg:gap-10 xl:grid xl:grid-cols-[1fr_200px] px-8 md:px-16 py-20">
<div className="mx-auto w-full min-w-0 max-w-4xl">
<div className="mb-4 flex items-center space-x-1 text-sm text-foreground-muted">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Docs</div>
<ChevronRight className="h-4 w-4 text-foreground-muted" />
<div className="text-foreground-lighter">{doc.title}</div>
</div>
<div className="flex items-end justify-between mb-5">
<div className="flex flex-col lg:flex-row lg:items-end justify-between mb-5">
<div className="space-y-2">
<h1 className={cn('scroll-m-20 text-4xl tracking-tight')}>{doc.title}</h1>
<h1 className={cn('scroll-m-20 text-2xl lg:text-4xl tracking-tight')}>{doc.title}</h1>
{doc.description && (
<p className="text-lg text-foreground-light">
<p className="text-base lg:text-lg text-foreground-light">
<Balancer>{doc.description}</Balancer>
</p>
)}
+5 -13
View File
@@ -1,8 +1,5 @@
import SideNavigation from '@/components/side-navigation'
import { Sidebar } from '@/components/sidebar'
import { SiteFooter } from '@/components/site-footer'
// import ThemeSettings from '@/components/theme-settings'
import TopNavigation from '@/components/top-navigation'
import { ScrollArea } from 'ui'
interface AppLayoutProps {
children: React.ReactNode
@@ -11,19 +8,14 @@ interface AppLayoutProps {
export default function AppLayout({ children }: AppLayoutProps) {
return (
<>
{/* <TopNavigation /> */}
{/* main container */}
<div className="">
<div className="pt-10 md:pt-0">
{/* main content */}
<main className="flex-1 max-w-site mx-auto w-full">
<main className="flex-1 max-w-site mx-auto w-full p-0">
{/* {children} */}
<div className="border-b">
<div className="flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
<aside className="fixed z-30 top-0 hidden h-screen w-full shrink-0 md:sticky md:block bg-black/10 border-r border-muted/50">
<ScrollArea className="h-full">
<SideNavigation />
</ScrollArea>
</aside>
<div className="flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] lg:grid-cols-[240px_minmax(0,1fr)]">
<Sidebar />
<div vaul-drawer-wrapper="">
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
</div>
+27 -10
View File
@@ -1,7 +1,7 @@
import { BlockPreview } from '@/components/block-preview'
import { ComponentPreview } from '@/components/component-preview'
import { Button } from '@/registry/default/components/ui/button'
import Link from 'next/link'
import { Button_Shadcn_ } from 'ui'
// Horizontal grid line component
const HorizontalGridLine = () => <div className="col-span-12 h-px bg-border/30" />
@@ -33,19 +33,18 @@ export default function Home() {
data-grid-item
className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-8 pb-8"
>
<div className="flex flex-col gap-8 justify-start pt-32">
<div className="flex flex-col gap-8 justify-start pt-16 md:pt-32">
<div className="max-w-2xl">
<h1 className="text-4xl text-foreground mb-3 font-medium tracking-tight">
UI Blocks for Supabase Projects
</h1>
<h2 className="text-lg text-foreground-light mb-4">
A set of beautifully-designed, accessible components that connect your
Supabase back-end to your front-end. Works with your favorite frameworks. Open
Source. Open Code.
A collection of components and blocks built on the shadcn/ui library that
connect your front-end to your Supabase back-end via a single command.
</h2>
<Button variant="secondary" size="lg" className="mt-4">
<Button_Shadcn_ variant="secondary" size="lg" className="mt-4">
<Link href="/docs/getting-started/quickstart">Get Started</Link>
</Button>
</Button_Shadcn_>
</div>
</div>
</div>
@@ -63,7 +62,7 @@ export default function Home() {
className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 relative"
>
<div className="-mt-4">
<BlockPreview name="password-based-auth/sign-up" />
<BlockPreview name="password-based-auth/auth/sign-up" />
</div>
</div>
<HorizontalGridLine />
@@ -81,8 +80,8 @@ export default function Home() {
className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 relative"
>
<div className="-mt-4 flex rounded-lg overflow-hidden">
<BlockPreview name="realtime-cursor" isPair />
<BlockPreview name="realtime-cursor" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
</div>
</div>
<HorizontalGridLine />
@@ -140,6 +139,24 @@ export default function Home() {
</div>
</div>
<HorizontalGridLine />
{/* Realtime Chat */}
<div
data-grid-item
className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-16 pb-6 text-xs uppercase font-mono text-foreground-light tracking-wider relative"
>
Realtime Chat
</div>
<HorizontalGridLine />
<div
data-grid-item
className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 relative"
>
<div className="-mt-4 flex rounded-lg overflow-hidden">
<BlockPreview name="realtime-chat-demo" isPair />
<BlockPreview name="realtime-chat-demo" isPair />
</div>
</div>
</div>
</div>
</div>
+6 -3
View File
@@ -6,6 +6,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { FrameworkProvider } from '@/context/framework-context'
import { MobileMenuProvider } from '@/context/mobile-menu-context'
import { TooltipProvider } from 'ui'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
@@ -13,9 +14,11 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
<AuthProvider>
<JotaiProvider>
<NextThemesProvider {...props}>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
<MobileMenuProvider>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
</MobileMenuProvider>
</NextThemesProvider>
</JotaiProvider>
</AuthProvider>
@@ -13,6 +13,12 @@ export function BaseInjector() {
base.href = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/example/password-based-auth/`
document.head.prepend(base)
// Add preview class to body to use shadcn theme vars
const bodyElement = document.querySelector('body')
if (bodyElement) {
bodyElement.classList.add('preview')
}
return () => {
base.remove()
}
@@ -1,22 +0,0 @@
'use client'
import { Button } from '@/registry/default/components/ui/button'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useRouter } from 'next/navigation'
export function SignOutButton() {
const supabase = createClient()
const router = useRouter()
return (
<Button
variant="outline"
onClick={() => {
supabase.auth.signOut()
router.push('/example/password-based-auth/login')
}}
>
Sign out
</Button>
)
}
@@ -1,20 +1,19 @@
import { redirect } from 'next/navigation'
import { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
import { SignOutButton } from './SignOutButton'
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
redirect('/login')
redirect('/example/password-based-auth/auth/login')
}
return (
<>
<p>Hello {data.user.email}</p>
<SignOutButton />
<LogoutButton />
</>
)
}
@@ -0,0 +1,19 @@
import { Metadata } from 'next'
import { BaseInjector } from './../base-injector'
export const metadata: Metadata = {
title: 'Realtime Avatar Stack Example',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<>
<BaseInjector />
<div className="flex w-full items-center justify-center p-6 md:p-10 preview bg-surface-100 h-screen">
<div className="w-full max-w-sm flex flex-col gap-4 justify-center items-center">
{children}
</div>
</div>
</>
)
}
@@ -0,0 +1,9 @@
'use client'
import Demo from '@/registry/default/examples/realtime-avatar-stack-demo'
const RealtimeAvatarStackDemoPage = () => {
return <Demo />
}
export default RealtimeAvatarStackDemoPage
@@ -0,0 +1,19 @@
import { Metadata } from 'next'
import { BaseInjector } from './../base-injector'
export const metadata: Metadata = {
title: 'Realtime Chat Example',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<>
<BaseInjector />
<div className="flex w-full items-center justify-center p-6 md:p-10 preview bg-surface-100 h-screen">
<div className="w-full max-w-sm flex flex-col gap-4 justify-center items-center">
{children}
</div>
</div>
</>
)
}
@@ -0,0 +1,9 @@
'use client'
import Demo from '@/registry/default/examples/realtime-chat-demo'
const RealtimeChatDemoPage = () => {
return <Demo />
}
export default RealtimeChatDemoPage
@@ -2,8 +2,8 @@
import Demo from '@/registry/default/examples/realtime-cursor-demo'
const RealtimeCursorPage = () => {
const RealtimeCursorDemoPage = () => {
return <Demo />
}
export default RealtimeCursorPage
export default RealtimeCursorDemoPage
+2 -2
View File
@@ -1,5 +1,6 @@
import '@/styles/globals.css'
import type { Metadata } from 'next'
import '@/styles/globals.css'
import { Inter } from 'next/font/google'
import { ThemeProvider } from './Providers'
import { SonnerToaster } from './SonnerToast'
@@ -26,7 +27,6 @@ export default async function Layout({ children }: RootLayoutProps) {
enableSystem
>
{children}
<SonnerToaster />
</ThemeProvider>
</body>
@@ -91,8 +91,8 @@ export function BlockItemCode({ files }: BlockItemCodeProps) {
isSelected={isSelected}
level={level}
icon={<File strokeWidth={1.5} size={16} className="shrink-0" />}
xPadding={16}
name={element.name}
className="gap-1.5"
/>
)}
/>
+5 -1
View File
@@ -1,6 +1,10 @@
import { Command } from '@/components/command'
import { OpenInV0Button } from '@/components/open-in-v0-button'
import dynamic from 'next/dynamic'
//The dynamic import is to prevent the command component from being rendered on the server and cause hydration errors
const Command = dynamic(() => import('./command').then((mod) => mod.Command), { ssr: false })
interface BlockItemProps {
name: string
}
+1 -1
View File
@@ -19,7 +19,7 @@ export function BlockPreview({ name, wide = false, isPair = false }: ComponentPr
}
>
<iframe
src={`/ui/example/${name}`}
src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/example/${name}`}
style={{
border: 'none',
width: '100%',
-155
View File
@@ -1,155 +0,0 @@
import { cn } from 'ui/src/lib/utils/cn'
// import { Card } from '@ui/components/shadcn/ui/card'
import color from 'ui/src/lib/tailwind-demo-classes'
import { Grid, GridItem } from './grid'
const Colors = ({
definition,
}: {
definition: 'background' | 'border' | 'text' | 'colors' | 'palletes'
}) => {
const Example = ({ x }: { x: string }) => {
switch (definition) {
case 'background':
return (
<div className={cn(x, 'relative w-full h-12 border border-overlay rounded-full')}></div>
)
break
case 'border':
return <div className={cn(x, 'relative w-full h-12 border-4 rounded-full')}></div>
break
case 'text':
return (
<span className={cn(x, 'relative w-full h-12 flex items-center justify-center')}>
Postgres
</span>
)
break
case 'colors':
return (
<div className={cn(x, 'relative w-full h-12 border border-overlay rounded-full')}></div>
)
break
case 'palletes':
return (
<div className={cn(x, 'relative w-full h-12 border border-overlay rounded-full')}></div>
)
break
default:
break
}
}
return (
<>
<Grid>
{color[definition].map((x: string, i) => {
return (
<GridItem
key={i}
className={cn(x.includes('contrast') && 'bg-foreground hover:bg-foreground-light')}
>
<Example x={x} />
<span className="bg-surface-100 rounded-full border px-2 font-mono text-xs text-foreground-lighter group-data-[state=open]:text-foreground">
{x}
</span>
</GridItem>
)
})}
</Grid>
</>
)
}
{
/* <div className="flex flex-col gap-12 py-20">
<div className="flex flex-row gap-12">
<div>
<h5 className="mb-3">Background</h5>
<div className="flex flex-col gap-3">
{color.background.map((x: string, i) => {
return (
<div className="flex gap-3 items-center" key={i}>
<div className={cn(x, 'relative w-12 h-12 border border-overlay shadow')}></div>
<div className="font-mono text-sm bg-surface-100 px-2 py-0.5 rounded-full">
{x}
</div>
</div>
)
})}
</div>
</div>
<div>
<h5 className="mb-3">Border</h5>
<div className="flex flex-col gap-3">
{color.border.map((x, i) => {
return (
<div className="flex gap-3 items-center" key={i}>
<div className={cn(x, 'relative w-12 h-12 border-4')}></div>
<div className="font-mono text-sm bg-surface-100 px-2 py-0.5 rounded-full">
{x}
</div>
</div>
)
})}
</div>
</div>
<div>
<h5 className="mb-3">texts</h5>
<div className="flex flex-col gap-3">
{color.text.map((x, i) => {
return (
<div className="flex gap-3 items-center" key={i}>
<span className={cn(x, 'relative w-12 h-12 flex items-center justify-center')}>
###
</span>
<div className="font-mono text-sm bg-surface-100 px-2 py-0.5 rounded-full">
{x}
</div>
</div>
)
})}
</div>
</div>
</div>
<div className="flex flex-row gap-12">
<div>
<h5 className="mb-3">Colors</h5>
<div className="flex flex-col gap-3">
{color.colors.map((x: string, i) => {
return (
<div className="flex gap-3 items-center" key={i}>
<div className={cn(x, 'relative w-12 h-12 border border-overlay shadow')}></div>
<div className="font-mono text-sm bg-surface-100 px-2 py-0.5 rounded-full">
{x}
</div>
</div>
)
})}
</div>
</div>
<div>
<h5 className="mb-3">Palletes</h5>
<div className="flex flex-col gap-3">
{color.palletes.map((x, i) => {
return (
<div className="flex gap-3 items-center" key={i}>
<div className={cn(x, 'relative w-12 h-12 border border-overlay shadow')}></div>
<div className="font-mono text-sm bg-surface-100 px-2 py-0.5 rounded-full">
{x}
</div>
</div>
)
})}
</div>
</div>
</div>
</div> */
}
export { Colors }
+53 -16
View File
@@ -1,25 +1,45 @@
'use client'
import { motion } from 'framer-motion'
import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { CommandCopyButton } from './command-copy-button'
import { useLocalStorage } from './use-local-storage'
interface CommandCopyProps {
name: string
highlight?: boolean
}
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
const LOCAL_STORAGE_KEY = 'package-manager-copy-command'
export function Command({ name, highlight }: CommandCopyProps) {
const command = `npx shadcn@latest add ${
process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production'
? `https://supabase.com`
: process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'preview'
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
: 'http://localhost:3004'
}${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/r/${name}.json`
const [value, setValue] = useLocalStorage(LOCAL_STORAGE_KEY, 'npm')
const getBaseUrl = () => {
if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') {
return `https://supabase.com`
} else if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'preview') {
return `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
} else {
return 'http://localhost:3004'
}
}
const baseUrl = getBaseUrl()
const componentPath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/r/${name}.json`
const commands: Record<PackageManager, string> = {
npm: `npx shadcn@latest add ${baseUrl}${componentPath}`,
pnpm: `pnpm dlx shadcn@latest add ${baseUrl}${componentPath}`,
yarn: `npx shadcn@latest add ${baseUrl}${componentPath}`,
bun: `bunx --bun shadcn@latest add ${baseUrl}${componentPath}`,
}
return (
<>
<div className="w-full group relative flex items-center rounded-lg bg-surface-100 px-4 py-2 overflow-hidden">
<Tabs_Shadcn_ value={value} onValueChange={setValue} className="w-full">
<div className="w-full group relative rounded-lg bg-surface-100 px-4 py-2 overflow-hidden">
{highlight && (
<motion.div
className="absolute inset-0 bg-gradient-to-l from-transparent via-white to-transparent opacity-10 z-0"
@@ -33,14 +53,31 @@ export function Command({ name, highlight }: CommandCopyProps) {
}}
/>
)}
<div className="flex-1 font-mono text-sm text-foreground relative z-10">
<span className="mr-2 text-[#888]">$</span>
{command}
</div>
<div className="relative z-10">
<CommandCopyButton command={command} />
<div className="flex flex-col">
<TabsList_Shadcn_ className="gap-2 relative mb-2 z-10">
{(Object.keys(commands) as PackageManager[]).map((manager) => (
<TabsTrigger_Shadcn_ key={manager} value={manager} className="text-xs">
{manager}
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
{(Object.keys(commands) as PackageManager[]).map((manager) => (
<TabsContent_Shadcn_ key={manager} value={manager} className="m-0">
<div className="flex items-center">
<div className="flex-1 font-mono text-sm text-foreground relative z-10">
<span className="mr-2 text-[#888]">$</span>
{commands[manager]}
</div>
<div className="relative z-10">
<CommandCopyButton command={commands[manager]} />
</div>
</div>
</TabsContent_Shadcn_>
))}
</div>
</div>
</>
</Tabs_Shadcn_>
)
}
@@ -4,13 +4,13 @@ import { usePathname, useRouter } from 'next/navigation'
import { componentPages, frameworkTitles } from '@/config/docs'
import { useFramework } from '@/context/framework-context'
import { SelectValue } from '@ui/components/shadcn/ui/select'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
const frameworks = Object.keys(frameworkTitles)
@@ -33,10 +33,18 @@ export function FrameworkSelector() {
}
// Get the supported frameworks for this component
const supportedFrameworks = componentPages[docTitle]?.supportedFrameworks || []
const componentItem = componentPages.items.find(
(item) => item.href?.split('/').pop() === docTitle.toLowerCase()
)
// Use the supportedFrameworks property if it exists, otherwise default to all frameworks
const supportedFrameworks = componentItem?.supportedFrameworks
if (!supportedFrameworks) {
return null
}
// Don't show selector if component only supports one framework
if (supportedFrameworks.length <= 1) {
if (supportedFrameworks && supportedFrameworks.length <= 1) {
return null
}
@@ -54,15 +62,16 @@ export function FrameworkSelector() {
setPreferredFramework(value as keyof typeof frameworkTitles)
}
const options = supportedFrameworks.map((f) => ({
label: frameworkTitles[f],
value: f,
}))
const options =
supportedFrameworks?.map((f) => ({
label: frameworkTitles[f],
value: f,
})) || []
return (
<Select_Shadcn_ value={selectedFramework} onValueChange={onSelect}>
<SelectTrigger_Shadcn_ className="w-[180px]">
<SelectValue />
<SelectTrigger_Shadcn_ className="w-[180px] mt-4 lg:mt-0">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
@@ -13,12 +13,10 @@ import { CopyButton } from '@/components/copy-button'
import { cn } from 'ui'
// import { FrameworkDocs } from '@/components/framework-docs'
import { CodeFragment } from '@/components/code-fragment'
import { Colors } from '@/components/colors'
import { Icons } from '@/components/icons'
import { RegistryBlock } from '@/components/registry-block'
import { ThemeSettings } from '@/components/theme-settings'
import { Style } from '@/registry/styles'
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'
import {
Accordion_Shadcn_ as Accordion,
AccordionContent_Shadcn_ as AccordionContent,
@@ -42,7 +40,6 @@ import { SonnerPositionConfig } from './sonner-expand-position'
import { StyleWrapper } from './style-wrapper'
const components = {
DynamicCodeBlock,
RegistryBlock,
Accordion,
AccordionContent,
@@ -97,10 +94,10 @@ const components = {
/>
),
ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
<ul className={cn('my-6 ml-6 list-disc', className)} {...props} />
<ul className={cn('my-6 ml-6 list-disc text-foreground-light', className)} {...props} />
),
ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
<ol className={cn('my-6 ml-6 list-decimal', className)} {...props} />
<ol className={cn('my-6 ml-6 list-decimal text-foreground-light', className)} {...props} />
),
li: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<li className={cn('mt-2', className)} {...props} />
@@ -260,13 +257,12 @@ const components = {
LinkedCard: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
<Link
className={cn(
'flex w-full flex-col items-center rounded-xl border bg-card p-6 text-card-foreground shadow transition-colors hover:bg-muted/50 sm:p-10',
'flex w-full flex-col items-center rounded-xl border bg-surface-100 text-card-background p-6 shadow transition-colors hover:bg-muted/50 sm:p-10',
className
)}
{...props}
/>
),
Colors,
Icons,
ThemeSettings,
CodeFragment,
@@ -0,0 +1,18 @@
'use client'
import { useMobileMenu } from '@/hooks/use-mobile-menu'
import { Sheet } from 'ui'
interface MobileMenuSheetProps {
children: React.ReactNode
}
export function MobileMenuSheet({ children }: MobileMenuSheetProps) {
const { open, setOpen } = useMobileMenu()
return (
<Sheet open={open} onOpenChange={setOpen}>
{children}
</Sheet>
)
}
@@ -1,34 +1,91 @@
'use client'
import { SidebarNavItem } from '@/types/nav'
import { cn } from 'ui/src/lib/utils/cn'
import Link, { LinkProps } from 'next/link'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import React from 'react'
const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item }) => {
import { useFramework } from '@/context/framework-context'
import { useMobileMenu } from '@/hooks/use-mobile-menu'
import { SidebarNavItem } from '@/types/nav'
import { cn } from 'ui'
// We extend:
// 1. LinkProps - for Next.js Link component props (prefetch, etc)
// 2. AnchorHTMLAttributes - for standard HTML anchor props (className, etc)
// We omit href from both since we compute it internally from item.href
interface NavigationItemProps
extends Omit<LinkProps, 'href'>,
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
item: SidebarNavItem
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void
}
const NavigationItem: React.FC<NavigationItemProps> = ({ item, onClick, ...props }) => {
const { setOpen } = useMobileMenu()
const { framework } = useFramework()
const pathname = usePathname()
const pathParts = pathname.split('/')
const slug = pathParts[pathParts.length - 1]
const framework = pathParts[pathParts.length - 2]
// Build URL with framework param if provided
// Helper function to check if a framework is supported for a navigation item
const isFrameworkSupported = (item: SidebarNavItem, framework: string) => {
const supportedFrameworks = item.supportedFrameworks || []
const hasFrameworkRestrictions = supportedFrameworks.length > 0
// An item supports the current framework if either:
// 1. It has no framework restrictions (supports all frameworks)
// 2. The current framework is in its list of supported frameworks
return !hasFrameworkRestrictions || supportedFrameworks.includes(framework as any)
}
// Build URL with priority:
// 1. item.href if available (replacing any existing framework with current one)
// 2. Computed from current path considering framework support
let href = item.href
// Handle component items with slug and framework
if (!href && slug) {
if (framework) {
// Only modify URLs for items that explicitly support frameworks
if (item.supportedFrameworks) {
if (href && href.startsWith('/docs/')) {
const hrefParts = href.split('/')
if (hrefParts.length >= 3) {
if (framework && isFrameworkSupported(item, framework)) {
if (hrefParts.length >= 4) {
href = `/docs/${framework}/${hrefParts[3]}`
}
}
}
}
}
// Handle component items with slug but no href
else if (!href && slug) {
// Build the URL using the current framework if it's supported
if (framework && isFrameworkSupported(item, framework)) {
href = `/docs/${framework}/${slug}`
} else {
// Fall back to framework-agnostic URL when framework not supported
href = `/docs/${slug}`
}
}
// Determine if this link represents the current page
const isActive = pathname === href
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Close the mobile menu when navigating
setOpen(false)
// Call the onClick prop if it exists
if (onClick) {
onClick(e)
}
}
return (
<Link
href={href || '#'}
{...props}
onClick={handleClick}
className={cn(
'relative',
'flex',
@@ -38,9 +95,11 @@ const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item })
'text-foreground-lighter px-6',
!isActive && 'hover:bg-surface-100 hover:text-foreground',
isActive && 'bg-surface-200 text-foreground',
'transition-all'
'transition-all',
props.className
)}
>
{/* Active indicator bar */}
<div
className={cn(
'transition',
@@ -51,7 +110,7 @@ const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item })
{item.title}
</Link>
)
})
}
NavigationItem.displayName = 'NavigationItem'
+14 -40
View File
@@ -1,44 +1,15 @@
'use client'
import Link from 'next/link'
import NavigationItem from '@/components/side-navigation-item'
import { aiEditorsRules, componentPages, gettingStarted } from '@/config/docs'
import { useFramework } from '@/context/framework-context'
import Link from 'next/link'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
import { CommandMenu } from './command-menu'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
function SideNavigation() {
const { framework: preferredFramework } = useFramework()
// Create a function to build URLs
const buildUrl = (slug: string, framework?: string) => {
if (!framework) return `/docs/${slug}`
return `/docs/${framework}/${slug}`
}
// Get all component pages
const componentItems = Object.entries(componentPages).map(([slug, component]) => {
let frameworkToUse = undefined
// Add framework param only if needed
if (preferredFramework && component.supportedFrameworks.includes(preferredFramework)) {
frameworkToUse = preferredFramework
} else if (component.supportedFrameworks.length > 0) {
frameworkToUse = component.supportedFrameworks[0]
}
return {
title: component.title,
href: buildUrl(slug, frameworkToUse),
items: [],
commandItemLabel: component.commandItemLabel,
}
})
return (
<nav className="flex flex-col h-full min-w-[220px]">
<div className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start justify-between mb-4">
<Link href="/">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -46,7 +17,7 @@ function SideNavigation() {
height="113"
viewBox="0 0 109 113"
fill="none"
className="w-6 h-6 mb-4"
className="w-6 h-6"
>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z"
@@ -55,7 +26,7 @@ function SideNavigation() {
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z"
fill="url(#paint1_linear)"
fill-opacity="0.2"
fillOpacity="0.2"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
@@ -70,8 +41,8 @@ function SideNavigation() {
y2="71.8293"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#249361" />
<stop offset="1" stop-color="#3ECF8E" />
<stop stopColor="#249361" />
<stop offset="1" stopColor="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear"
@@ -82,7 +53,7 @@ function SideNavigation() {
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stop-opacity="0" />
<stop offset="1" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
@@ -95,7 +66,7 @@ function SideNavigation() {
{/* <TopNavigationSearch /> */}
<CommandMenu />
</div>
<div className="pb-6">
<div className="pb-6 space-y-0.5">
<div className="font-mono uppercase text-xs text-foreground-lighter/75 mb-2 px-6 tracking-widest">
{gettingStarted.title}
</div>
@@ -109,8 +80,11 @@ function SideNavigation() {
</div>
<div className="space-y-0.5">
{/* Render items based on component definitions */}
{componentItems.map((item, i) => (
<NavigationItem item={item} key={`${item.href?.toString() || item.title}-${i}`} />
{componentPages.items.map((component, i) => (
<NavigationItem
item={component}
key={`${component.href?.toString() || component.title}-${i}`}
/>
))}
</div>
</div>
+32
View File
@@ -0,0 +1,32 @@
import { Menu } from 'lucide-react'
import SideNavigation from '@/components/side-navigation'
import { Button, ScrollArea, SheetContent, SheetTrigger } from 'ui'
import { MobileMenuSheet } from './mobile-menu-sheet'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
export function Sidebar() {
return (
<>
<div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-background justify-between flex items-center px-8 py-3 border-b">
<MobileMenuSheet>
<SheetTrigger asChild>
<Button type="outline" icon={<Menu />} />
</SheetTrigger>
<SheetContent side="left" className="p-0 w-80" showClose={false}>
<ScrollArea className="h-full">
<SideNavigation />
</ScrollArea>
</SheetContent>
</MobileMenuSheet>
<ThemeSwitcherDropdown />
</div>
<aside className="fixed z-30 top-0 hidden h-screen w-full shrink-0 md:sticky md:block bg-200 border-r border-muted/50">
<ScrollArea className="h-full">
<SideNavigation />
</ScrollArea>
</aside>
</>
)
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { siteConfig } from '@/config/site'
export function SiteFooter() {
return (
<footer className="py-6 md:px-8 md:py-0 mx-auto w-full max-w-site">
<footer className="py-6 px-4 md:px-8 md:py-0 mx-auto w-full max-w-site">
<div className="flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
<p className="text-balance text-center text-sm leading-loose text-foreground-muted md:text-left">
Built by{' '}
+1 -2
View File
@@ -3,9 +3,8 @@ import Link from 'next/link'
import { forwardRef } from 'react'
import { ExternalLink } from 'lucide-react'
import { Button } from 'ui'
import { cn } from 'ui/src/lib/utils/cn'
import Image from 'next/image'
import { Button, cn } from 'ui'
const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> & { doc: Doc }>(
({ doc, children, ...props }, ref) => {
@@ -0,0 +1,47 @@
// Reference: https://usehooks.com/useLocalStorage/
import { useCallback, useState } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
console.log(error)
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
},
[key, storedValue]
)
return [storedValue, setValue] as const
}
+62 -59
View File
@@ -1,9 +1,4 @@
import { MainNavItem, SidebarNavGroup, SidebarNavItem } from 'types/nav'
interface DocsConfig {
mainNav?: MainNavItem[]
sidebarNav: SidebarNavItem[]
}
import { SidebarNavGroup } from 'types/nav'
export const gettingStarted: SidebarNavGroup = {
title: 'Getting Started',
@@ -41,60 +36,60 @@ export const aiEditorsRules: SidebarNavGroup = {
],
}
// Framework titles for display
export const frameworkTitles: Record<string, string> = {
nextjs: 'Next.js',
'react-router': 'React Router',
tanstack: 'Tanstack Start',
react: 'React SPA',
}
// Component definitions with supported frameworks
export const componentPages: Record<
string,
{
title: string
supportedFrameworks: string[]
commandItemLabel: string
href: string
}
> = {
client: {
title: 'Client',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Supabase Client',
href: '/docs/nextjs/client',
},
'password-based-auth': {
title: 'Password-Based Auth',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Password-Based Auth',
href: '/docs/nextjs/password-based-auth',
},
dropzone: {
title: 'Dropzone',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Dropzone (File Upload)',
href: '/docs/nextjs/dropzone',
},
'realtime-cursor': {
title: 'Realtime Cursor',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Realtime Cursor',
href: '/docs/nextjs/realtime-cursor',
},
'current-user-avatar': {
title: 'Current User Avatar',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Current User Avatar',
href: '/docs/nextjs/current-user-avatar',
},
'realtime-avatar-stack': {
title: 'Realtime Avatar Stack',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Realtime Avatar Stack',
href: '/docs/nextjs/realtime-avatar-stack',
},
export const componentPages: SidebarNavGroup = {
title: 'Components',
items: [
{
title: 'Client',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/client',
items: [],
commandItemLabel: 'Supabase Client',
},
{
title: 'Password-Based Auth',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/password-based-auth',
items: [],
commandItemLabel: 'Password-Based Auth',
},
{
title: 'Dropzone',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/dropzone',
items: [],
commandItemLabel: 'Dropzone (File Upload)',
},
{
title: 'Realtime Cursor',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/realtime-cursor',
items: [],
commandItemLabel: 'Realtime Cursor',
},
{
title: 'Current User Avatar',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/current-user-avatar',
items: [],
commandItemLabel: 'Current User Avatar',
},
{
title: 'Realtime Avatar Stack',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/realtime-avatar-stack',
items: [],
commandItemLabel: 'Realtime Avatar Stack',
},
{
title: 'Realtime Chat',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/realtime-chat',
items: [],
commandItemLabel: 'Realtime Chat',
},
],
}
export const COMMAND_ITEMS = [
@@ -111,3 +106,11 @@ export const COMMAND_ITEMS = [
href: component.href,
})),
]
// Framework titles for display
export const frameworkTitles: Record<string, string> = {
nextjs: 'Next.js',
'react-router': 'React Router',
tanstack: 'TanStack Start',
react: 'React SPA',
}
@@ -16,7 +16,7 @@ description: Frequently asked questions
</AccordionContent>
</AccordionItem>
<AccordionItem value="overwrite">
<AccordionTrigger>Will it overwrite my existing files?</AccordionTrigger>
<AccordionTrigger>Will it overwrite existing files?</AccordionTrigger>
<AccordionContent>
No. If you run a command in an existing project, it will <u>not</u> overwrite your existing
files. If there conflicting files, you will be asked what you want to do (leave them or
@@ -24,7 +24,7 @@ description: Frequently asked questions
</AccordionContent>
</AccordionItem>
<AccordionItem value="existing-project">
<AccordionTrigger>I've already started my a project, what should I do?</AccordionTrigger>
<AccordionTrigger>I've already started a project, what should I do?</AccordionTrigger>
<AccordionContent>
You can still use these components, even if you've already started your project! The commands
won't overwrite your existing files, but you should still be careful with the changes to makes
@@ -45,12 +45,21 @@ description: Frequently asked questions
</AccordionTrigger>
<AccordionContent>
Our Next.js quickstart is a great starting point for building a full app with Supabase and
Next.js. If you've already started with this template, you can use add command to add more
components to your project.
Next.js. If you've already started with this template, you can just add the components in this library to your project.
</AccordionContent>
</AccordionItem>
<AccordionItem value="quickstart-needed">
<AccordionTrigger>
Do I need to install one of the framework quickstarts before using this library?
</AccordionTrigger>
<AccordionContent>
No. You don't need to start with one of our framework quickstarts.
If you already have an existing project, you can just add the components in this library to your project. If you're starting from scratch, you should use one of the shadcn/ui setup guides for your framework of choice via the [Quickstart](/ui/docs/getting-started/quickstart) page.
</AccordionContent>
</AccordionItem>
<AccordionItem value="ai-builder">
<AccordionTrigger>I used an AI builder to build my app, what should I do?</AccordionTrigger>
<AccordionTrigger>I used an AI builder to build an app, what should I do?</AccordionTrigger>
<AccordionContent>
If your app is already built, you can use any of the commands to drop components into your
project. Your app likely already has a Supabase client set up, so you can just continue using
@@ -25,10 +25,10 @@ Our components are designed to be easily extensible. You can modify the code to
Our components are designed to be easily composable. You can combine them to create new components that are tailored to your needs. If you want to change the design or behavior of a component, you can easily do so in your own codebase.
### Supabase Clients
### Supabase clients
The Supabase Clients are the backbone of your apps. They are designed to be easily integrated with your existing Supabase project. We provide both client-side and server-side clients for several major frameworks, and the code is designed to be easily integrated with your existing codebase.
The Supabase clients are the backbone of your apps. They are designed to be easily integrated with your existing Supabase project. We provide both client-side and server-side clients for several major frameworks, and the code is designed to be easily integrated with your existing codebase.
### Scaffolding for Hard Problems
### Scaffolding for hard problems
Setting up something like Auth should be easy, so we've done the hard work for you. Our components are designed to be easily integrated with your new or existing Supabase projects. Installing them drops the files into place so you can get the hard parts out of the way quickly.
@@ -1,10 +1,69 @@
---
title: Quick Start
description: Components
description: Install shadcn/ui and use the components in your project
---
## Code Block
## Set up shadcn/ui
```js
console.log('Hello World')
```
Start by installing [shadcn/ui](https://ui.shadcn.com/) in your project. Use the appropriate setup guide linked below for your framework of choice.
If you're already using a recent version of shadcn/ui in your existing project, you can skip this step—this library should work with your current setup. However, if you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup.
<div className="grid grid-cols-2 gap-4 mt-8 sm:gap-6">
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/next">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>Next.js</title>
<path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" />
</svg>
<p className="font-medium mt-2">Next.js</p>
</LinkedCard>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/react-router">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="w-10 h-10"
fill="currentColor"
>
<path d="M12.118 5.466a2.306 2.306 0 0 0-.623.08c-.278.067-.702.332-.953.583-.41.423-.49.609-.662 1.469-.08.423.41 1.43.847 1.734.45.317 1.085.502 2.065.608 1.429.16 1.84.636 1.84 2.197 0 1.377-.385 1.747-1.96 1.906-1.707.172-2.58.834-2.765 2.117-.106.781.41 1.76 1.125 2.091 1.627.768 3.15-.198 3.467-2.196.211-1.284.622-1.642 1.998-1.747 1.588-.133 2.409-.675 2.713-1.787.278-1.02-.304-2.157-1.297-2.554-.264-.106-.873-.238-1.35-.291-1.495-.16-1.879-.424-2.038-1.39-.225-1.337-.317-1.562-.794-2.09a2.174 2.174 0 0 0-1.613-.73zm-4.785 4.36a2.145 2.145 0 0 0-.497.048c-1.469.318-2.17 2.051-1.35 3.295 1.178 1.774 3.944.953 3.97-1.177.012-1.193-.98-2.143-2.123-2.166zM2.089 14.19a2.22 2.22 0 0 0-.427.052c-2.158.476-2.237 3.626-.106 4.182.53.145.582.145 1.111.013 1.191-.318 1.866-1.456 1.549-2.607-.278-1.02-1.144-1.664-2.127-1.64zm19.824.008c-.233.002-.477.058-.784.162-1.39.477-1.866 2.092-.98 3.336.557.794 1.96 1.058 2.82.516 1.416-.874 1.363-3.057-.093-3.746-.38-.186-.663-.271-.963-.268z" />
</svg>
<p className="font-medium mt-2">React Router</p>
</LinkedCard>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/tanstack">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="w-10 h-10"
fill="currentColor"
>
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
</svg>
<p className="font-medium mt-2">TanStack Start</p>
</LinkedCard>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/manual">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>React SPA</title>
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
<p className="font-medium mt-2">React SPA</p>
</LinkedCard>
</div>
When you've finished setting up shadcn/ui, you can start exploring the components in the sidebar
## Troubleshooting
If you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup.
For issues and questions about specific frameworks, see the [FAQs](/ui/docs/getting-started/faq)
@@ -1,5 +1,5 @@
---
title: Next.js
title: Supabase Client Libraries
description: Supabase client for Next.js
---
@@ -28,10 +28,14 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
<Callout type="warning" className="mt-4">
{' '}
This Supabase client is built for SSR with the Next.js App Router. If you're building a React SPA,
use the [React SPA client](/ui/docs/react-router/client) instead.{' '}
</Callout>
## Further reading
- [Generating TypeScript types for your client](https://supabase.com/docs/guides/api/rest/generating-types)
@@ -16,9 +16,36 @@ description: Supabase Auth-aware avatar
<RegistryBlock itemName="current-user-avatar-nextjs" />
## Usage
## Introduction
The `CurrentUserAvatar` component connects to Supabase Auth to fetch the user data and show an avatar. It uses the `user_metadata`
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?`.
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?` as a fallback, which you can change.
The `CurrentUserAvatar` component integrates with Supabase Auth to display user avatars dynamically. It automatically retrieves the profile image from the `user_metadata` field, which Supabase Auth populates when using provider-based authentication. The component also fallbacks to `?` if the user is unauthenticated.
## Usage
The `CurrentUserAvatar` component is designed to be used anywhere in your app. Add the `<CurrentUserAvatar />` component to your page and it will render the avatar of the current user, with a fallback.
```tsx
'use client'
import { CurrentUserAvatar } from '@/components/current-user-avatar'
const CurrentUserAvatarDemo = () => {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<CurrentUserAvatar />
</Header>
)
}
export default CurrentUserAvatarDemo
```
## Props
This component doesn't accept any props. If you wish to change the fallback, you can do so by changing the `CurrentUserAvatar` component directly.
## Further reading
- [Auth users](https://supabase.com/docs/guides/auth/users)
@@ -25,7 +25,7 @@ Uploading files should be easy—this component handles the tricky parts for you
The File Upload component makes it easy to add file uploads to your app, with built-in support for drag-and-drop, file type restrictions, image previews, and configurable limits on file size and number of files. All the essentials, ready to go.
## Features
**Features**
- Drag-and-drop support
- Multiple file uploads
@@ -40,6 +40,38 @@ The File Upload component makes it easy to add file uploads to your app, with bu
- Simply add this `<Dropzone />` component to your page and it will handle the rest.
- For control over file upload, you can pass in a `props` object to the component.
```tsx
'use client'
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
const props = useSupabaseUpload({
bucketName: 'test',
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
maxFileSize: 1000 * 1000 * 10, // 10MB,
})
return (
<div className="w-[500px]">
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
)
}
export FileUploadDemo
```
## Props
| Prop | Type | Default | Description |
@@ -49,3 +81,9 @@ The File Upload component makes it easy to add file uploads to your app, with bu
| `allowedMimeTypes` | `string[]` | `[]` | The MIME types to allow for upload |
| `maxFiles` | `number` | `1` | Maximum number of files to upload |
| `maxFileSize` | `number` | `1000` | Maximum file size in bytes |
## Further reading
- [Creating buckets](https://supabase.com/docs/guides/storage/buckets/creating-buckets)
- [Access control](https://supabase.com/docs/guides/storage/security/access-control)
- [Standard uploads](https://supabase.com/docs/guides/storage/uploads/standard-uploads)
@@ -1,9 +1,9 @@
---
title: Password-based Auth (Next.js)
description: Password-based Auth block for Next.js app
title: Password-based Authentication
description: Password-based authentication block for Next.js
---
<BlockPreview name="password-based-auth/sign-up" />
<BlockPreview name="password-based-auth/auth/sign-up" />
## Installation
@@ -33,7 +33,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Adding email templates
@@ -70,7 +70,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the Next.js route users will visit to reset or update their password. First, update the `forgot-password-form.tsx` component with your `forgot-password` route. You'll need to tell Supabase to allow this route as the redirect URL. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add that route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Set up the Next.js route that users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login-form.tsx` and `update-password-form.tsx` components to point to the logged-in routes in your app.
@@ -79,3 +79,11 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
You can use this block with the Pages router by simply moving the routes from the `app` folder into the `pages` folder and renaming them. Example instead of `app/sign-up/page.tsx`, you'd create a `pages/sign-up.tsx` file.
</Callout>
## Further reading
- [Password-based authentication (PKCE flow)](https://supabase.com/docs/guides/auth/passwords?queryGroups=flow&flow=pkce)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
- [Email templates for local development](https://supabase.com/docs/guides/local-development/customizing-email-templates)
- [Custom SMTP](https://supabase.com/docs/guides/auth/auth-smtp)
@@ -3,7 +3,7 @@ title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
<BlockPreview name="realtime-avatar-stack-demo" />
## Installation
@@ -18,4 +18,28 @@ description: Avatar stack in realtime
## Usage
The `RealtimeAvatarStack` renders stacked avatars which are connected to Supabase Realtime. Specifically, it uses the Presence feature. You can use this to show currently online users in a chatroom, game session or collaborative app.
The `RealtimeAvatarStack` component renders stacked avatars which are connected to Supabase Realtime. It uses the Presence feature of Supabase Realtime. You can use this to show currently online users in a chatroom, game session or collaborative app.
```tsx
import { RealtimeAvatarStack } from '@/components/realtime-avatar-stack'
export default function Page() {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<RealtimeAvatarStack roomName="break_room" />
</Header>
)
}
```
## Props
| Prop | Type | Default | Description |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `roomName` | `string` | `null` | The name of the Supabase Realtime room to connect to |
## Further reading
- [Realtime Presence](https://supabase.com/docs/guides/realtime/presence)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -0,0 +1,103 @@
---
title: Realtime Chat
description: Real-time chat component for collaborative applications
---
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-chat-demo" isPair />
<BlockPreview name="realtime-chat-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-chat-nextjs"
description="Renders a real-time chat interface for users in a shared room."
/>
## Folder structure
<RegistryBlock itemName="realtime-chat-nextjs" />
## Introduction
The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
## Usage
### Basic usage
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
export default function ChatPage() {
return <RealtimeChat roomName="my-chat-room" username="john_doe" />
}
```
### With initial messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
return <RealtimeChat roomName="my-chat-room" username="john_doe" messages={messages} />
}
```
### Storing messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
import { storeMessages } from '@/lib/store-messages'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
const handleMessage = (messages: ChatMessage[]) => {
// Store messages in your database
await storeMessages(messages)
}
return <RealtimeChat roomName="my-chat-room" username="john_doe" onMessage={handleMessage} />
}
```
## Features
- Real-time message synchronization
- Message persistence support with `onMessage` and `messages` props
- Customizable message appearance
- Automatic scroll-to-bottom on new messages
- Room-based isolation for scoped conversations
- Low-latency updates using Supabase Realtime
## Props
| Prop | Type | Description |
| ------------ | ----------------------------------- | ------------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared chat room. |
| `username` | `string` | Name of the current user; used to identify message senders. |
| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
### ChatMessage type
```typescript
interface ChatMessage {
id: string
content: string
user: {
name: string
}
createdAt: string
}
```
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -4,10 +4,8 @@ description: Real-time cursor sharing for collaborative applications
---
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-cursor" isPair />
<BlockPreview name="realtime-cursor" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
</div>
## Installation
@@ -25,11 +23,7 @@ description: Real-time cursor sharing for collaborative applications
The Realtime Cursors component lets users share their cursor position with others in the same room—perfect for real-time collaboration. It handles all the setup and boilerplate for you, so you can add it to your app with minimal effort.
## Usage
The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
## Features
**Features**
- Broadcast cursor position to other users in the same room
- Customizable cursor appearance
@@ -37,9 +31,31 @@ The Realtime Cursor component is designed to be used in a room. It can be used t
- Low-latency updates using Supabase Realtime
- Room-based isolation for scoped collaboration
## Usage
The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
```tsx
'use client'
import { RealtimeCursors } from '@/components/realtime-cursors'
export default function Page() {
return (
<div className="w-full min-h-screen">
<RealtimeCursors roomName="macrodata_refinement_office" username="Mark Scout" />
</div>
)
}
```
## Props
| Prop | Type | Description |
| ---------- | -------- | ---------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared room or session. |
| `username` | `string` | Name of the current user; used to track and label cursors. |
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -1,5 +1,5 @@
---
title: Supabase Client for React Router
title: Supabase Client Libraries
description: Supabase client for React Router
---
@@ -11,5 +11,25 @@ description: Supabase client for React Router
<RegistryBlock itemName="supabase-client-react-router" />
This block provides Supabase clients designed for use with SSR by storing JWT tokens in cookies. If you're
developing a React SPA, use the [React SPA client](/ui/docs/react-router/client) instead.
## Usage
This block installs Supabase clients for connecting your React Router project to Supabase. They're designed for use in both server-side loaders and actions, as well as client-side components.
If you've already set up Supabase clients in your project, you can just continue using them.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
## Further reading
- [Generating TypeScript types for your client](https://supabase.com/docs/guides/api/rest/generating-types)
@@ -16,9 +16,34 @@ description: Supabase Auth-aware avatar
<RegistryBlock itemName="current-user-avatar-react-router" />
## Usage
## Introduction
The `CurrentUserAvatar` component connects to Supabase Auth to fetch the user data and show an avatar. It uses the `user_metadata`
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?`.
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?` as a fallback, which you can change.
The `CurrentUserAvatar` component integrates with Supabase Auth to display user avatars dynamically. It automatically retrieves the profile image from the `user_metadata` field, which Supabase Auth populates when using provider-based authentication. The component also fallbacks to `?` if the user is unauthenticated.
## Usage
The `CurrentUserAvatar` component is designed to be used anywhere in your app. Add the `<CurrentUserAvatar />` component to your page and it will render the avatar of the current user, with a fallback.
```tsx
import { CurrentUserAvatar } from '@/components/current-user-avatar'
const CurrentUserAvatarDemo = () => {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<CurrentUserAvatar />
</Header>
)
}
export default CurrentUserAvatarDemo
```
## Props
This component doesn't accept any props. If you wish to change the fallback, you can do so by changing the `CurrentUserAvatar` component directly.
## Further reading
- [Auth users](https://supabase.com/docs/guides/auth/users)
@@ -21,24 +21,67 @@ description: Displays a control for easier uploading of files directly to Supaba
## Introduction
The File Upload component is designed to allow users to upload files with specific features and restrictions. It supports drag-and-drop functionality, MIME type restrictions, image previews, and configurable limits on file size and number of files.
Uploading files should be easy—this component handles the tricky parts for you.
## Features
The File Upload component makes it easy to add file uploads to your app, with built-in support for drag-and-drop, file type restrictions, image previews, and configurable limits on file size and number of files. All the essentials, ready to go.
1. **Drag-and-Drop**. Support drag-and-drop functionality for adding files.
**Features**
1. **MIME Type Restriction**. Only allow specific MIME types.
1. **Invalid File Handling**. Allow users to remove invalid files from the list. Display a tooltip with error information for invalid files.
1. **Image Previews**. Display previews for image files.
1. **Error Handling**. Show error messages if the upload fails due to network or server errors.
1. **Success Handling**. Display a success message and clear the file list upon successful upload.
1. **Configurable Limits**. Allow configuration of file size limits and the maximum number of files.
- Drag-and-drop support
- Multiple file uploads
- File size and count limits
- Image previews for supported file types
- MIME type restrictions
- Invalid file handling
- Success and error states with clear feedback
## Usage
## Examples
- Simply add this `<Dropzone />` component to your page and it will handle the rest.
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
const props = useSupabaseUpload({
bucketName: 'test',
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
maxFileSize: 1000 * 1000 * 10, // 10MB,
})
return (
<div className="w-[500px]">
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
)
}
export FileUploadDemo
```
## Props
| Prop | Type | Default | Description |
| ------------------ | ---------- | ------- | ---------------------------------------------------- |
| `bucketName` | `string` | `null` | The name of the Supabase Storage bucket to upload to |
| `path` | `string` | `null` | The path or subfolder to upload the file to |
| `allowedMimeTypes` | `string[]` | `[]` | The MIME types to allow for upload |
| `maxFiles` | `number` | `1` | Maximum number of files to upload |
| `maxFileSize` | `number` | `1000` | Maximum file size in bytes |
## Further reading
- [Creating buckets](https://supabase.com/docs/guides/storage/buckets/creating-buckets)
- [Access control](https://supabase.com/docs/guides/storage/security/access-control)
- [Standard uploads](https://supabase.com/docs/guides/storage/uploads/standard-uploads)
@@ -1,14 +1,14 @@
---
title: Password-based Auth (Next.js)
description: Password-based Auth block for Next.js app
title: Password-based Authentication
description: Password-based authentication block for React Router
---
<BlockPreview name="password-based-auth/sign-up" />
<BlockPreview name="password-based-auth/auth/sign-up" />
## Installation
<BlockItem
name="password-based-auth-nextjs"
name="password-based-auth-react-router"
description="All needed components for the password based auth flow"
/>
@@ -33,7 +33,7 @@ VITE_SUPABASE_ANON_KEY=
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Adding email templates
@@ -70,6 +70,14 @@ VITE_SUPABASE_ANON_KEY=
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. First, update the `forgot-password.tsx` component with your `update-password` route. You'll need to tell Supabase to allow this route as the redirect URL. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add that route to the list of Redirect URLs. It should look something like: `http://example.com/update-password`.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
## Further reading
- [Password-based authentication (PKCE flow)](https://supabase.com/docs/guides/auth/passwords?queryGroups=flow&flow=pkce)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
- [Email templates for local development](https://supabase.com/docs/guides/local-development/customizing-email-templates)
- [Custom SMTP](https://supabase.com/docs/guides/auth/auth-smtp)
@@ -3,7 +3,7 @@ title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
<BlockPreview name="realtime-avatar-stack-demo" />
## Installation
@@ -18,4 +18,28 @@ description: Avatar stack in realtime
## Usage
The `RealtimeAvatarStack` renders stacked avatars which are connected to Supabase Realtime. Specifically, it uses the Presence feature. You can use this to show currently online users in a chatroom, game session or collaborative app.
The `RealtimeAvatarStack` component renders stacked avatars which are connected to Supabase Realtime. It uses the Presence feature of Supabase Realtime. You can use this to show currently online users in a chatroom, game session or collaborative app.
```tsx
import { RealtimeAvatarStack } from '@/components/realtime-avatar-stack'
export default function Page() {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<RealtimeAvatarStack roomName="break_room" />
</Header>
)
}
```
## Props
| Prop | Type | Default | Description |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `roomName` | `string` | `null` | The name of the Supabase Realtime room to connect to |
## Further reading
- [Realtime Presence](https://supabase.com/docs/guides/realtime/presence)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -0,0 +1,103 @@
---
title: Realtime Chat
description: Real-time chat component for collaborative applications
---
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-chat-demo" isPair />
<BlockPreview name="realtime-chat-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-chat-react-router"
description="Renders a real-time chat interface for users in a shared room."
/>
## Folder structure
<RegistryBlock itemName="realtime-chat-react-router" />
## Introduction
The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
## Usage
### Basic usage
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
export default function ChatPage() {
return <RealtimeChat roomName="my-chat-room" username="john_doe" />
}
```
### With initial messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
return <RealtimeChat roomName="my-chat-room" username="john_doe" messages={messages} />
}
```
### Storing messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
import { storeMessages } from '@/lib/store-messages'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
const handleMessage = (messages: ChatMessage[]) => {
// Store messages in your database
await storeMessages(messages)
}
return <RealtimeChat roomName="my-chat-room" username="john_doe" onMessage={handleMessage} />
}
```
## Features
- Real-time message synchronization
- Message persistence support with `onMessage` and `messages` props
- Customizable message appearance
- Automatic scroll-to-bottom on new messages
- Room-based isolation for scoped conversations
- Low-latency updates using Supabase Realtime
## Props
| Prop | Type | Description |
| ------------ | ----------------------------------- | ------------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared chat room. |
| `username` | `string` | Name of the current user; used to identify message senders. |
| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
### ChatMessage type
```typescript
interface ChatMessage {
id: string
content: string
user: {
name: string
}
createdAt: string
}
```
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -3,16 +3,16 @@ title: Realtime Cursor
description: Real-time cursor sharing for collaborative applications
---
<ComponentPreview
name="realtime-cursor-demo"
description="An alert with an icon, title and description. The title says 'Heads up!' and the description is 'You can add components to your app using the cli.'."
/>
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-cursor-demo" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-cursor-react-router"
description="Component which renders realtime cursors from other users in a room."
description="Renders realtime cursors from other users in a room."
/>
## Folder structure
@@ -20,3 +20,41 @@ description: Real-time cursor sharing for collaborative applications
<RegistryBlock itemName="realtime-cursor-react-router" />
## Introduction
The Realtime Cursors component lets users share their cursor position with others in the same room—perfect for real-time collaboration. It handles all the setup and boilerplate for you, so you can add it to your app with minimal effort.
**Features**
- Broadcast cursor position to other users in the same room
- Customizable cursor appearance
- Presence detection (automatically joins/leaves users)
- Low-latency updates using Supabase Realtime
- Room-based isolation for scoped collaboration
## Usage
The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
```tsx
import { RealtimeCursors } from '@/components/realtime-cursors'
export default function Page() {
return (
<div className="w-full min-h-screen">
<RealtimeCursors roomName="macrodata_refinement_office" username="Mark Scout" />
</div>
)
}
```
## Props
| Prop | Type | Description |
| ---------- | -------- | ---------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared room or session. |
| `username` | `string` | Name of the current user; used to track and label cursors. |
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
+24 -1
View File
@@ -1,5 +1,5 @@
---
title: React Single Page Applications
title: Supabase Client Libraries
description: Supabase client for React Single Page Applications
---
@@ -10,3 +10,26 @@ description: Supabase client for React Single Page Applications
## Folder structure
<RegistryBlock itemName="supabase-client-react" />
## Usage
This block installs a Supabase client for connecting your React project to Supabase. It's designed for use in client-side components.
If you've already set up a Supabase client in your project, you can just continue using that existing setup.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
## Further reading
- [Generating TypeScript types for your client](https://supabase.com/docs/guides/api/rest/generating-types)
@@ -13,9 +13,34 @@ description: Supabase Auth-aware avatar
<RegistryBlock itemName="current-user-avatar-react" />
## Usage
## Introduction
The `CurrentUserAvatar` component connects to Supabase Auth to fetch the user data and show an avatar. It uses the `user_metadata`
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?`.
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?` as a fallback, which you can change.
The `CurrentUserAvatar` component integrates with Supabase Auth to display user avatars dynamically. It automatically retrieves the profile image from the `user_metadata` field, which Supabase Auth populates when using provider-based authentication. The component also fallbacks to `?` if the user is unauthenticated.
## Usage
The `CurrentUserAvatar` component is designed to be used anywhere in your app. Add the `<CurrentUserAvatar />` component to your page and it will render the avatar of the current user, with a fallback.
```tsx
import { CurrentUserAvatar } from '@/components/current-user-avatar'
const CurrentUserAvatarDemo = () => {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<CurrentUserAvatar />
</Header>
)
}
export default CurrentUserAvatarDemo
```
## Props
This component doesn't accept any props. If you wish to change the fallback, you can do so by changing the `CurrentUserAvatar` component directly.
## Further reading
- [Auth users](https://supabase.com/docs/guides/auth/users)
+58 -15
View File
@@ -21,24 +21,67 @@ description: Displays a control for easier uploading of files directly to Supaba
## Introduction
The File Upload component is designed to allow users to upload files with specific features and restrictions. It supports drag-and-drop functionality, MIME type restrictions, image previews, and configurable limits on file size and number of files.
Uploading files should be easy—this component handles the tricky parts for you.
## Features
The File Upload component makes it easy to add file uploads to your app, with built-in support for drag-and-drop, file type restrictions, image previews, and configurable limits on file size and number of files. All the essentials, ready to go.
1. **Drag-and-Drop**. Support drag-and-drop functionality for adding files.
**Features**
1. **MIME Type Restriction**. Only allow specific MIME types.
1. **Invalid File Handling**. Allow users to remove invalid files from the list. Display a tooltip with error information for invalid files.
1. **Image Previews**. Display previews for image files.
1. **Error Handling**. Show error messages if the upload fails due to network or server errors.
1. **Success Handling**. Display a success message and clear the file list upon successful upload.
1. **Configurable Limits**. Allow configuration of file size limits and the maximum number of files.
- Drag-and-drop support
- Multiple file uploads
- File size and count limits
- Image previews for supported file types
- MIME type restrictions
- Invalid file handling
- Success and error states with clear feedback
## Usage
## Examples
- Simply add this `<Dropzone />` component to your page and it will handle the rest.
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
const props = useSupabaseUpload({
bucketName: 'test',
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
maxFileSize: 1000 * 1000 * 10, // 10MB,
})
return (
<div className="w-[500px]">
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
)
}
export FileUploadDemo
```
## Props
| Prop | Type | Default | Description |
| ------------------ | ---------- | ------- | ---------------------------------------------------- |
| `bucketName` | `string` | `null` | The name of the Supabase Storage bucket to upload to |
| `path` | `string` | `null` | The path or subfolder to upload the file to |
| `allowedMimeTypes` | `string[]` | `[]` | The MIME types to allow for upload |
| `maxFiles` | `number` | `1` | Maximum number of files to upload |
| `maxFileSize` | `number` | `1000` | Maximum file size in bytes |
## Further reading
- [Creating buckets](https://supabase.com/docs/guides/storage/buckets/creating-buckets)
- [Access control](https://supabase.com/docs/guides/storage/security/access-control)
- [Standard uploads](https://supabase.com/docs/guides/storage/uploads/standard-uploads)
@@ -3,6 +3,8 @@ title: Password-based Authentication
description: Password-based authentication block for React Single Page Applications
---
<BlockPreview name="password-based-auth/auth/sign-up" />
## Installation
<BlockItem
@@ -16,51 +18,54 @@ description: Password-based authentication block for React Single Page Applicati
## Usage
This block inserts components for login, sign-up, forgot-password and update-password flows in your projects. In general, you
would do the following steps:
Once you install the block in your React project, you'll get all the necessary pages and components to set up a password-based authentication flow.
1. Create routes using your framework of choice for the 4 flows
2. Import the block components into the routes
3. Add a authenticated route /info which should render the current user info.
4. Add the following code in the authenticated route to redirect to login if the user is unauthenticated.
### Getting started
````ts
useEffect(() => {
const checkAuth = async () => {
const client = createClient();
const { error } = await client.auth.getUser();
First, add a `.env` file to your project with the following environment variables:
if (error) {
location.href = "/login";
}
};
checkAuth();
}, []);
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
4. Update the redirect upon successful login in `login-form.tsx` and `update-password.tsx` to `/info`.
5. Test the rest of the flows.
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
There few more steps to finish the installation:
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
1. Set the `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` environment variables in your `.env` file in your project.
1. Add an [email template for sign-up](https://supabase.com/dashboard/project/uznvnxvfhkixgqetwekc/auth/templates) to the Supabase project. For detailed instructions on how to configure your email
templates, including the use of variables like `{{ .SiteURL }}`,`{{ .TokenHash }}`, and `{{ .RedirectTo }}`, refer to our [Email Templates guide](/docs/email-templates).
### Setting up routes and redirect URLs
Your signup email template should contain the following HTML:
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
```html
<h2>Confirm your signup</h2>
1. Add the following code in the authenticated route to redirect to login if the user is unauthenticated.
<p>Follow this link to confirm your user:</p>
<p>
<a
href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next={{ .RedirectTo }}"
>Confirm your email</a
>
</p>
````
```ts
import { useEffect } from "react"
import { createClient } from "@supabase/supabase-js"
1. Set the site URL in the [Supabase dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration).
1. Set the forgot-password route in `forgot-password-form.tsx` component. The route needs to be configured as a redirect URL in the [Supabase dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration).
1. Update the redirect paths in the `login-form.tsx` and `update-password-form.tsx` components to point to the logged-in routes in your app.
export default function AuthenticatedRoute() {
useEffect(() => {
const checkAuth = async () => {
const client = createClient()
const { error } = await client.auth.getUser()
if (error) {
location.href = "/login"
}
}
checkAuth()
}, [])
return <div>Authenticated page</div>;
}
```
## Further reading
- [Password-based authentication (implicit flow)](https://supabase.com/docs/guides/auth/passwords?queryGroups=flow&flow=implicit)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
- [Email templates for local development](https://supabase.com/docs/guides/local-development/customizing-email-templates)
- [Custom SMTP](https://supabase.com/docs/guides/auth/auth-smtp)
@@ -3,7 +3,7 @@ title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
<BlockPreview name="realtime-avatar-stack-demo" />
## Installation
@@ -18,4 +18,28 @@ description: Avatar stack in realtime
## Usage
The `RealtimeAvatarStack` renders stacked avatars which are connected to Supabase Realtime. Specifically, it uses the Presence feature. You can use this to show currently online users in a chatroom, game session or collaborative app.
The `RealtimeAvatarStack` component renders stacked avatars which are connected to Supabase Realtime. It uses the Presence feature of Supabase Realtime. You can use this to show currently online users in a chatroom, game session or collaborative app.
```tsx
import { RealtimeAvatarStack } from '@/components/realtime-avatar-stack'
export default function Page() {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<RealtimeAvatarStack roomName="break_room" />
</Header>
)
}
```
## Props
| Prop | Type | Default | Description |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `roomName` | `string` | `null` | The name of the Supabase Realtime room to connect to |
## Further reading
- [Realtime Presence](https://supabase.com/docs/guides/realtime/presence)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -0,0 +1,103 @@
---
title: Realtime Chat
description: Real-time chat component for collaborative applications
---
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-chat-demo" isPair />
<BlockPreview name="realtime-chat-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-chat-react"
description="Renders a real-time chat interface for users in a shared room."
/>
## Folder structure
<RegistryBlock itemName="realtime-chat-react" />
## Introduction
The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
## Usage
### Basic usage
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
export default function ChatPage() {
return <RealtimeChat roomName="my-chat-room" username="john_doe" />
}
```
### With initial messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
return <RealtimeChat roomName="my-chat-room" username="john_doe" messages={messages} />
}
```
### Storing messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
import { storeMessages } from '@/lib/store-messages'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
const handleMessage = (messages: ChatMessage[]) => {
// Store messages in your database
await storeMessages(messages)
}
return <RealtimeChat roomName="my-chat-room" username="john_doe" onMessage={handleMessage} />
}
```
## Features
- Real-time message synchronization
- Message persistence support with `onMessage` and `messages` props
- Customizable message appearance
- Automatic scroll-to-bottom on new messages
- Room-based isolation for scoped conversations
- Low-latency updates using Supabase Realtime
## Props
| Prop | Type | Description |
| ------------ | ----------------------------------- | ------------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared chat room. |
| `username` | `string` | Name of the current user; used to identify message senders. |
| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
### ChatMessage type
```typescript
interface ChatMessage {
id: string
content: string
user: {
name: string
}
createdAt: string
}
```
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -3,16 +3,16 @@ title: Realtime Cursor
description: Real-time cursor sharing for collaborative applications
---
<ComponentPreview
name="realtime-cursor-demo"
description="An alert with an icon, title and description. The title says 'Heads up!' and the description is 'You can add components to your app using the cli.'."
/>
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-cursor-demo" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-cursor-react"
description="Component which renders realtime cursors from other users in a room."
description="Renders realtime cursors from other users in a room."
/>
## Folder structure
@@ -20,3 +20,41 @@ description: Real-time cursor sharing for collaborative applications
<RegistryBlock itemName="realtime-cursor-react" />
## Introduction
The Realtime Cursors component lets users share their cursor position with others in the same room—perfect for real-time collaboration. It handles all the setup and boilerplate for you, so you can add it to your app with minimal effort.
**Features**
- Broadcast cursor position to other users in the same room
- Customizable cursor appearance
- Presence detection (automatically joins/leaves users)
- Low-latency updates using Supabase Realtime
- Room-based isolation for scoped collaboration
## Usage
The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
```tsx
import { RealtimeCursors } from '@/components/realtime-cursors'
export default function Page() {
return (
<div className="w-full min-h-screen">
<RealtimeCursors roomName="macrodata_refinement_office" username="Mark Scout" />
</div>
)
}
```
## Props
| Prop | Type | Description |
| ---------- | -------- | ---------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared room or session. |
| `username` | `string` | Name of the current user; used to track and label cursors. |
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -1,21 +1,35 @@
---
title: Tanstack Start
description: Supabase client for Tanstack Start
title: Supabase Client Libraries
description: Supabase client for TanStack Start
---
## Installation
<BlockItem name="supabase-client-tanstack" description="Supabase Client for Tanstack Start" />
The easiest way to use Tanstack Start with Supabase is to follow the following steps:
1. Follow the [official guide](https://ui.shadcn.com/docs/installation/tanstack) for adding shadcn/ui to Tanstack Start
2. Install this block to get the Supabase clients
3. Add VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY to your .env file
<BlockItem name="supabase-client-tanstack" description="Supabase Client for TanStack Start" />
## Folder structure
<RegistryBlock itemName="supabase-client-tanstack" />
This block provides Supabase clients designed for use with SSR by storing JWT tokens in cookies. If you're
developing a React SPA, use the [React SPA client](/ui/docs/react-router/client) instead.
## Usage
This block installs Supabase clients for connecting your TanStack Start project to Supabase. They're designed for use in both server-side loaders and actions, as well as client-side components.
If you've already set up a Supabase client in your project, you can just continue using that existing setup.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
## Further reading
- [Generating TypeScript types for your client](https://supabase.com/docs/guides/api/rest/generating-types)
@@ -16,9 +16,34 @@ description: Supabase Auth-aware avatar
<RegistryBlock itemName="current-user-avatar-tanstack" />
## Usage
## Introduction
The `CurrentUserAvatar` component connects to Supabase Auth to fetch the user data and show an avatar. It uses the `user_metadata`
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?`.
property which gets populated automatically by Supabase Auth if the user logged in via a provider. If the user doesn't have a profile image, it renders their initials. If the user is logged out, it renders a `?` as a fallback, which you can change.
The `CurrentUserAvatar` component integrates with Supabase Auth to display user avatars dynamically. It automatically retrieves the profile image from the `user_metadata` field, which Supabase Auth populates when using provider-based authentication. The component also fallbacks to `?` if the user is unauthenticated.
## Usage
The `CurrentUserAvatar` component is designed to be used anywhere in your app. Add the `<CurrentUserAvatar />` component to your page and it will render the avatar of the current user, with a fallback.
```tsx
import { CurrentUserAvatar } from '@/components/current-user-avatar'
const CurrentUserAvatarDemo = () => {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<CurrentUserAvatar />
</Header>
)
}
export default CurrentUserAvatarDemo
```
## Props
This component doesn't accept any props. If you wish to change the fallback, you can do so by changing the `CurrentUserAvatar` component directly.
## Further reading
- [Auth users](https://supabase.com/docs/guides/auth/users)
@@ -21,24 +21,67 @@ description: Displays a control for easier uploading of files directly to Supaba
## Introduction
The File Upload component is designed to allow users to upload files with specific features and restrictions. It supports drag-and-drop functionality, MIME type restrictions, image previews, and configurable limits on file size and number of files.
Uploading files should be easy—this component handles the tricky parts for you.
## Features
The File Upload component makes it easy to add file uploads to your app, with built-in support for drag-and-drop, file type restrictions, image previews, and configurable limits on file size and number of files. All the essentials, ready to go.
1. **Drag-and-Drop**. Support drag-and-drop functionality for adding files.
**Features**
1. **MIME Type Restriction**. Only allow specific MIME types.
1. **Invalid File Handling**. Allow users to remove invalid files from the list. Display a tooltip with error information for invalid files.
1. **Image Previews**. Display previews for image files.
1. **Error Handling**. Show error messages if the upload fails due to network or server errors.
1. **Success Handling**. Display a success message and clear the file list upon successful upload.
1. **Configurable Limits**. Allow configuration of file size limits and the maximum number of files.
- Drag-and-drop support
- Multiple file uploads
- File size and count limits
- Image previews for supported file types
- MIME type restrictions
- Invalid file handling
- Success and error states with clear feedback
## Usage
## Examples
- Simply add this `<Dropzone />` component to your page and it will handle the rest.
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
const props = useSupabaseUpload({
bucketName: 'test',
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
maxFileSize: 1000 * 1000 * 10, // 10MB,
})
return (
<div className="w-[500px]">
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
)
}
export FileUploadDemo
```
## Props
| Prop | Type | Default | Description |
| ------------------ | ---------- | ------- | ---------------------------------------------------- |
| `bucketName` | `string` | `null` | The name of the Supabase Storage bucket to upload to |
| `path` | `string` | `null` | The path or subfolder to upload the file to |
| `allowedMimeTypes` | `string[]` | `[]` | The MIME types to allow for upload |
| `maxFiles` | `number` | `1` | Maximum number of files to upload |
| `maxFileSize` | `number` | `1000` | Maximum file size in bytes |
## Further reading
- [Creating buckets](https://supabase.com/docs/guides/storage/buckets/creating-buckets)
- [Access control](https://supabase.com/docs/guides/storage/security/access-control)
- [Standard uploads](https://supabase.com/docs/guides/storage/uploads/standard-uploads)
@@ -1,16 +1,78 @@
---
title: Tanstack Start
description: Supabase client for Tanstack Start
title: Password-based Authentication
description: Password-based authentication block for TanStack Start
---
<BlockPreview name="password-based-auth/auth/sign-up" />
## Installation
<BlockItem name="password-based-auth-tanstack" description="Supabase Client for Tanstack Start" />
1. Follow the steps in /ui/docs/tanstack/password-based-auth to setup your repo.
2. Try to access the /info route, you should be redirected to login screen. If you create an account and login and try accessing the /info page, you should see your email.
<BlockItem name="password-based-auth-tanstack" description="Supabase Client for TanStack Start" />
## Folder structure
<RegistryBlock itemName="password-based-auth-tanstack" />
## Usage
Once you install the block in your TanStack Start project, you'll get all the necessary pages and components to set up a password-based authentication flow.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Adding email templates
1. Add an [email template for sign-up](https://supabase.com/dashboard/project/_/auth/templates) to the Supabase project. Your signup email template should contain at least the following HTML:
```html
<h2>Confirm your signup</h2>
<p>Follow this link to confirm your user:</p>
<p>
<a
href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next={{ .RedirectTo }}"
>Confirm your email</a
>
</p>
```
For detailed instructions on how to configure your email templates, including the use of variables like `{{ .SiteURL }}`,`{{ .TokenHash }}`, and `{{ .RedirectTo }}`, refer to our [Email Templates guide](https://supabase.com/docs/email-templates).
1. Add an [email template for reset password](https://supabase.com/dashboard/project/_/auth/templates) to the Supabase project. Your reset password email template should contain at least the following HTML:
```html
<h2>Reset Password</h2>
<p>Follow this link to reset the password for your user:</p>
<p>
<a
href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recovery&next={{ .RedirectTo }}"
>Reset Password</a
>
</p>
```
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
## Further reading
- [Password-based authentication (PKCE flow)](https://supabase.com/docs/guides/auth/passwords?queryGroups=flow&flow=pkce)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
- [Email templates for local development](https://supabase.com/docs/guides/local-development/customizing-email-templates)
- [Custom SMTP](https://supabase.com/docs/guides/auth/auth-smtp)
@@ -3,7 +3,7 @@ title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
<BlockPreview name="realtime-avatar-stack-demo" />
## Installation
@@ -18,4 +18,28 @@ description: Avatar stack in realtime
## Usage
The `RealtimeAvatarStack` renders stacked avatars which are connected to Supabase Realtime. Specifically, it uses the Presence feature. You can use this to show currently online users in a chatroom, game session or collaborative app.
The `RealtimeAvatarStack` component renders stacked avatars which are connected to Supabase Realtime. It uses the Presence feature of Supabase Realtime. You can use this to show currently online users in a chatroom, game session or collaborative app.
```tsx
import { RealtimeAvatarStack } from '@/components/realtime-avatar-stack'
export default function Page() {
return (
<Header className="flex items-center justify-between">
<h1>Lumon Industries</h1>
<RealtimeAvatarStack roomName="break_room" />
</Header>
)
}
```
## Props
| Prop | Type | Default | Description |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `roomName` | `string` | `null` | The name of the Supabase Realtime room to connect to |
## Further reading
- [Realtime Presence](https://supabase.com/docs/guides/realtime/presence)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -0,0 +1,103 @@
---
title: Realtime Chat
description: Real-time chat component for collaborative applications
---
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-chat-demo" isPair />
<BlockPreview name="realtime-chat-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-chat-tanstack"
description="Renders a real-time chat interface for users in a shared room."
/>
## Folder structure
<RegistryBlock itemName="realtime-chat-tanstack" />
## Introduction
The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
## Usage
### Basic usage
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
export default function ChatPage() {
return <RealtimeChat roomName="my-chat-room" username="john_doe" />
}
```
### With initial messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
return <RealtimeChat roomName="my-chat-room" username="john_doe" messages={messages} />
}
```
### Storing messages
```tsx
import { RealtimeChat } from '@/components/realtime-chat'
import { useMessagesQuery } from '@/hooks/use-messages-query'
import { storeMessages } from '@/lib/store-messages'
export default function ChatPage() {
const { data: messages } = useMessagesQuery()
const handleMessage = (messages: ChatMessage[]) => {
// Store messages in your database
await storeMessages(messages)
}
return <RealtimeChat roomName="my-chat-room" username="john_doe" onMessage={handleMessage} />
}
```
## Features
- Real-time message synchronization
- Message persistence support with `onMessage` and `messages` props
- Customizable message appearance
- Automatic scroll-to-bottom on new messages
- Room-based isolation for scoped conversations
- Low-latency updates using Supabase Realtime
## Props
| Prop | Type | Description |
| ------------ | ----------------------------------- | ------------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared chat room. |
| `username` | `string` | Name of the current user; used to identify message senders. |
| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
### ChatMessage type
```typescript
interface ChatMessage {
id: string
content: string
user: {
name: string
}
createdAt: string
}
```
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
@@ -3,16 +3,16 @@ title: Realtime Cursor
description: Real-time cursor sharing for collaborative applications
---
<ComponentPreview
name="realtime-cursor-demo"
description="An alert with an icon, title and description. The title says 'Heads up!' and the description is 'You can add components to your app using the cli.'."
/>
<div className="flex flex-row -space-x-px w-full">
<BlockPreview name="realtime-cursor-demo" isPair />
<BlockPreview name="realtime-cursor-demo" isPair />
</div>
## Installation
<BlockItem
name="realtime-cursor-tanstack"
description="Component which renders realtime cursors from other users in a room."
description="Renders realtime cursors from other users in a room."
/>
## Folder structure
@@ -20,3 +20,41 @@ description: Real-time cursor sharing for collaborative applications
<RegistryBlock itemName="realtime-cursor-tanstack" />
## Introduction
The Realtime Cursors component lets users share their cursor position with others in the same room—perfect for real-time collaboration. It handles all the setup and boilerplate for you, so you can add it to your app with minimal effort.
**Features**
- Broadcast cursor position to other users in the same room
- Customizable cursor appearance
- Presence detection (automatically joins/leaves users)
- Low-latency updates using Supabase Realtime
- Room-based isolation for scoped collaboration
## Usage
The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
```tsx
import { RealtimeCursors } from '@/components/realtime-cursors'
export default function Page() {
return (
<div className="w-full min-h-screen">
<RealtimeCursors roomName="macrodata_refinement_office" username="Mark Scout" />
</div>
)
}
```
## Props
| Prop | Type | Description |
| ---------- | -------- | ---------------------------------------------------------- |
| `roomName` | `string` | Unique identifier for the shared room or session. |
| `username` | `string` | Name of the current user; used to track and label cursors. |
## Further reading
- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
Loaded 100 of 201 files, more files were not shown because too many files have changed in this diff. Show more