feat: Extra components for UI Library (#34363)

* Fix the v0 button. Add some more docs. (+13 squashed commits)
Squashed commits:
[cc5954779e] Add success state to the forgot-password form.
[258bfb1015] Simplify the tanstack auth block.
[1ba5c223d9] Add missing pages to the nextjs auth.
[b842e4acec] Fix the env vars in the React client.
[2a2bcc5356] Fix the command URL.
[a26a2d36c2] Add a tanstack block for password-based auth.
[d68881f0d5] Fix the tanstack client.
[1fd2e16d96] Add missing deps to satisfy TS build.
[9797d745df] Various fixes.
[3e9b676e99] Fix the registryBlock component.
[540a5d600b] Set the supabase project for testing.
[3eba892c92] Regenerate the llms.txt file.
[bf526a0ecb] Regenerate the registry files.

* Add current user avatar.

* Add RealtimeAvatarStack.

* Use the fields which are populated by Auth.

* Regenerate the registry files.

* Fix the imports.

* Rebuild the registry files. Add a github login to the supabase config.

* Minor fixes for the components.

* Minor fix to the avatar stack.

* Remove peekCode, show showCode prop to component preview.

* Add examples for avatar stack and current user avatar.

* Use the new generatenames function in the cursors.

* Add documentation for avatar stack and avatar.

* Switch the profile images.

* Fix a type error.

* More fixes.
This commit is contained in:
Ivan Vasilov
2025-03-27 18:41:31 +02:00
committed by GitHub
parent 7228bba94b
commit a81d056d79
48 changed files with 1586 additions and 128 deletions

View File

@@ -139,6 +139,102 @@ export const Index: Record<string, any> = {
chunks: []
}
,
"current-user-avatar-nextjs": {
name: "current-user-avatar-nextjs",
type: "registry:component",
registryDependencies: ["avatar"],
component: React.lazy(() => import("@/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx")),
source: "",
files: ["registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.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"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"current-user-avatar-react": {
name: "current-user-avatar-react",
type: "registry:component",
registryDependencies: ["avatar"],
component: React.lazy(() => import("@/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx")),
source: "",
files: ["registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","registry/default/clients/react/lib/supabase/client.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"current-user-avatar-react-router": {
name: "current-user-avatar-react-router",
type: "registry:component",
registryDependencies: ["avatar"],
component: React.lazy(() => import("@/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx")),
source: "",
files: ["registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","registry/default/clients/react-router/lib/supabase.client.ts","registry/default/clients/react-router/lib/supabase.server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"current-user-avatar-tanstack": {
name: "current-user-avatar-tanstack",
type: "registry:component",
registryDependencies: ["avatar"],
component: React.lazy(() => import("@/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx")),
source: "",
files: ["registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","registry/default/clients/tanstack/lib/supabase/client.ts","registry/default/clients/tanstack/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-avatar-stack-nextjs": {
name: "realtime-avatar-stack-nextjs",
type: "registry:component",
registryDependencies: ["avatar","tooltip"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx")),
source: "",
files: ["registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.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"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-avatar-stack-react": {
name: "realtime-avatar-stack-react",
type: "registry:component",
registryDependencies: ["avatar","tooltip"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx")),
source: "",
files: ["registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","registry/default/clients/react/lib/supabase/client.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-avatar-stack-react-router": {
name: "realtime-avatar-stack-react-router",
type: "registry:component",
registryDependencies: ["avatar","tooltip"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx")),
source: "",
files: ["registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","registry/default/clients/react-router/lib/supabase.client.ts","registry/default/clients/react-router/lib/supabase.server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-avatar-stack-tanstack": {
name: "realtime-avatar-stack-tanstack",
type: "registry:component",
registryDependencies: ["avatar","tooltip"],
component: React.lazy(() => import("@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx")),
source: "",
files: ["registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx","registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts","registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts","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",
@@ -234,6 +330,30 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
}
,
"current-user-avatar-demo": {
name: "current-user-avatar-demo",
type: "registry:example",
registryDependencies: [],
component: React.lazy(() => import("@/registry/default/examples/current-user-avatar-demo.tsx")),
source: "",
files: ["registry/default/examples/current-user-avatar-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"realtime-avatar-stack-demo": {
name: "realtime-avatar-stack-demo",
type: "registry:example",
registryDependencies: [],
component: React.lazy(() => import("@/registry/default/examples/realtime-avatar-stack-demo.tsx")),
source: "",
files: ["registry/default/examples/realtime-avatar-stack-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
},
}

View File

@@ -1,13 +1,9 @@
'use client'
import { RegistryNode } from '@/lib/process-registry'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@ui/components/shadcn/ui/tabs'
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'
import { useState } from 'react'
import supabaseTheme from '../lib/themes/supabase-2.json' assert { type: 'json' }
import { BlockItemPreview } from './block-item'
import { CodeBlock, CodeBlockLang, TreeView, TreeViewItem, flattenTree } from 'ui'
import { File } from 'lucide-react'
import { useState } from 'react'
import { CodeBlock, TreeView, TreeViewItem, flattenTree } from 'ui'
interface BlockItemCodeProps {
files: RegistryNode[]
@@ -107,7 +103,7 @@ export function BlockItemCode({ files }: BlockItemCodeProps) {
<CodeBlock
wrapperClassName="w-full"
className="h-full max-w-none !w-full flex-1 font-mono text-xs rounded-none border-none"
language={selectedFile.name.split('.').pop() as CodeBlockLang}
language="ts"
>
{selectedFile?.content}
</CodeBlock>

View File

@@ -13,7 +13,6 @@ interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
extractClassname?: boolean
extractedClassNames?: string
align?: 'center' | 'start' | 'end'
peekCode?: boolean
showGrid?: boolean
showDottedGrid?: boolean
wide?: boolean
@@ -26,7 +25,6 @@ export function CodeFragment({
extractClassname,
extractedClassNames,
align = 'center',
peekCode = false,
showGrid = false,
showDottedGrid = true,
wide = false,

View File

@@ -4,25 +4,19 @@ import { Index } from '@/__registry__'
import * as React from 'react'
import { useConfig } from '@/hooks/use-config'
import {
Button,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
cn,
} from 'ui'
import { CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, Collapsible_Shadcn_, cn } from 'ui'
import { styles } from '@/registry/styles'
import { ChevronRight, Expand } from 'lucide-react'
import { ChevronRight } from 'lucide-react'
interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
name: string
extractClassname?: boolean
extractedClassNames?: string
align?: 'center' | 'start' | 'end'
peekCode?: boolean
showGrid?: boolean
showDottedGrid?: boolean
showCode?: boolean
wide?: boolean
}
@@ -33,9 +27,9 @@ export function ComponentPreview({
extractClassname,
extractedClassNames,
align = 'center',
peekCode = false,
showGrid = false,
showDottedGrid = true,
showCode = true,
wide = false,
...props
}: ComponentPreviewProps) {
@@ -89,55 +83,13 @@ export function ComponentPreview({
const wideClasses = wide ? '2xl:-ml-12 2xl:-mr-12' : ''
if (peekCode) {
return (
<div className={cn('mt-4 mb-12', wideClasses)}>
<div
className={cn(
'relative rounded-tl-md rounded-tr-md border-t border-l border-r bg-studio'
)}
>
{showGrid && (
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
)}
{showDottedGrid && (
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
)}
<div className="z-10 relative">{ComponentPreview}</div>
{/* <div className="preview-grid-background"></div> */}
</div>
<div className="flex flex-col space-y-4">
<div
className={cn(
'relative',
'w-full rounded-md [&_pre]:my-0',
expand
? '[&_pre]:overflow-auto'
: 'inset-0 [&_pre]:max-h-[196px] [&_pre]:overflow-hidden',
'[&_pre]:rounded-tr-none [&_pre]:rounded-tl-none'
)}
>
{Code}
<div className="absolute bottom-0 w-full flex justify-center mb-4">
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
type="default"
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
</Button>
</div>
</div>
</div>
</div>
)
}
return (
<div className={cn('mt-4 mb-12', wideClasses)}>
<div
className={cn('relative rounded-tl-md rounded-tr-md border-t border-l border-r bg-studio')}
className={cn(
'relative bg-studio',
showCode ? 'rounded-tl-md rounded-tr-md border-t border-l border-r' : 'rounded-md border'
)}
>
{showGrid && (
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
@@ -146,11 +98,11 @@ export function ComponentPreview({
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
)}
<div className="z-10 relative">{ComponentPreview}</div>
{/* <div className="preview-grid-background"></div> */}
</div>
<Collapsible_Shadcn_>
<CollapsibleTrigger_Shadcn_
className={`
{showCode && (
<Collapsible_Shadcn_>
<CollapsibleTrigger_Shadcn_
className={`
flex
gap-3 items-center
w-full
@@ -163,27 +115,28 @@ export function ComponentPreview({
data-[state=closed]:rounded-bl-md data-[state=closed]:rounded-br-md
`}
>
<ChevronRight
className="transition-all group-data-[state=open]:rotate-90 text-foreground-lighter"
size={14}
/>
View code
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="transition-all">
<div
className={cn(
'relative',
'w-full rounded-md [&_pre]:my-0',
'[&_pre]:overflow-auto',
'[&_pre]:max-h-[320px]',
'[&_pre]:rounded-tr-none [&_pre]:rounded-tl-none [&_pre]:border-t-transparent'
)}
>
{Code}
</div>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
<ChevronRight
className="transition-all group-data-[state=open]:rotate-90 text-foreground-lighter"
size={14}
/>
View code
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="transition-all">
<div
className={cn(
'relative',
'w-full rounded-md [&_pre]:my-0',
'[&_pre]:overflow-auto',
'[&_pre]:max-h-[320px]',
'[&_pre]:rounded-tr-none [&_pre]:rounded-tl-none [&_pre]:border-t-transparent'
)}
>
{Code}
</div>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
)}
</div>
)
}

View File

@@ -83,6 +83,18 @@ export const componentPages: Record<
commandItemLabel: 'Realtime Cursor',
href: '/docs/realtime-cursor',
},
'current-user-avatar': {
title: 'Current User Avatar',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Current User Avatar',
href: '/docs/current-user-avatar',
},
'realtime-avatar-stack': {
title: 'Realtime Avatar Stack',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Realtime Avatar Stack',
href: '/docs/realtime-avatar-stack',
},
}
export const COMMAND_ITEMS = [

View File

@@ -0,0 +1,24 @@
---
title: Current User Avatar
description: Supabase Auth-aware avatar
---
<ComponentPreview name="current-user-avatar-demo" showCode={false} />
## Installation
<BlockItem
name="current-user-avatar-nextjs"
description="Renders the avatar of the current user."
/>
## Folder structure
<RegistryBlock itemName="current-user-avatar-nextjs" />
## Usage
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 `?`.
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.

View File

@@ -12,6 +12,8 @@ description: Password-based Auth block for Next.js app
description="All needed components for the password based auth flow"
/>
This block includes the [Supabase client](/ui/docs/nextjs/client). When installing, you can skip overwriting it.
## Folder structure
This block includes the [Supabase client](/ui/docs/nextjs/client). When installing, you can skip overwriting it.

View File

@@ -0,0 +1,21 @@
---
title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
## Installation
<BlockItem
name="realtime-avatar-stack-nextjs"
description="Renders a stack of avatars which are connected via Supabase Realtime"
/>
## Folder structure
<RegistryBlock itemName="realtime-avatar-stack-nextjs" />
## 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.

View File

@@ -0,0 +1,24 @@
---
title: Current User Avatar
description: Supabase Auth-aware avatar
---
<ComponentPreview name="current-user-avatar-demo" showCode={false} />
## Installation
<BlockItem
name="current-user-avatar-react-router"
description="Renders the avatar of the current user."
/>
## Folder structure
<RegistryBlock itemName="current-user-avatar-react-router" />
## Usage
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 `?`.
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.

View File

@@ -0,0 +1,21 @@
---
title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
## Installation
<BlockItem
name="realtime-avatar-stack-react-router"
description="Renders a stack of avatars which are connected via Supabase Realtime"
/>
## Folder structure
<RegistryBlock itemName="realtime-avatar-stack-react-router" />
## 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.

View File

@@ -0,0 +1,21 @@
---
title: Current User Avatar
description: Supabase Auth-aware avatar
---
<ComponentPreview name="current-user-avatar-demo" showCode={false} />
## Installation
<BlockItem name="current-user-avatar-react" description="Renders the avatar of the current user." />
## Folder structure
<RegistryBlock itemName="current-user-avatar-react" />
## Usage
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 `?`.
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.

View File

@@ -0,0 +1,21 @@
---
title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
## Installation
<BlockItem
name="realtime-avatar-stack-react"
description="Renders a stack of avatars which are connected via Supabase Realtime"
/>
## Folder structure
<RegistryBlock itemName="realtime-avatar-stack-react" />
## 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.

View File

@@ -0,0 +1,24 @@
---
title: Current User Avatar
description: Supabase Auth-aware avatar
---
<ComponentPreview name="current-user-avatar-demo" showCode={false} />
## Installation
<BlockItem
name="current-user-avatar-tanstack"
description="Renders the avatar of the current user."
/>
## Folder structure
<RegistryBlock itemName="current-user-avatar-tanstack" />
## Usage
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 `?`.
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.

View File

@@ -0,0 +1,21 @@
---
title: Realtime Avatar Stack
description: Avatar stack in realtime
---
<ComponentPreview name="realtime-avatar-stack-demo" showCode={false} />
## Installation
<BlockItem
name="realtime-avatar-stack-tanstack"
description="Renders a stack of avatars which are connected via Supabase Realtime"
/>
## Folder structure
<RegistryBlock itemName="realtime-avatar-stack-tanstack" />
## 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.

View File

@@ -16,11 +16,13 @@
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.6.0",
"common": "workspace:*",
"contentlayer2": "0.4.6",
"eslint-config-supabase": "workspace:*",
"framer-motion": "^11.0.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

View File

@@ -1,5 +1,5 @@
# Supabase UI Library
Last updated: 2025-03-27T07:03:14.930Z
Last updated: 2025-03-27T11:29:28.542Z
## Overview
Library of components for your project. The components integrate with Supabase and are shadcn compatible.

View File

@@ -0,0 +1,45 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-nextjs",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"dependencies": [
"@supabase/ssr@latest"
],
"registryDependencies": [
"avatar"
],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\n\nexport const CurrentUserAvatar = () => {\n const profileImage = useCurrentUserImage()\n const name = useCurrentUserName()\n const initials = name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()\n\n return (\n <Avatar>\n {profileImage && <AvatarImage src={profileImage} alt={initials} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getUser(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: DO NOT REMOVE auth.getUser()\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-react-router",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"dependencies": [
"@supabase/ssr@latest"
],
"registryDependencies": [
"avatar"
],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\n\nexport const CurrentUserAvatar = () => {\n const profileImage = useCurrentUserImage()\n const name = useCurrentUserName()\n const initials = name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()\n\n return (\n <Avatar>\n {profileImage && <AvatarImage src={profileImage} alt={initials} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react-router/lib/supabase.client.ts",
"content": "import { createBrowserClient } from '@supabase/ssr'\n\n// Use this function to create a client for the browser. You should pass the env variables through a loader and use them\n// to instantiate the client.\nexport function createClient(supabaseUrl: string, supabaseAnonKey: string) {\n return createBrowserClient(supabaseUrl, supabaseAnonKey)\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase.server.ts",
"content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n })\n\n return { supabase, headers }\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-react",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"dependencies": [
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"avatar"
],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\n\nexport const CurrentUserAvatar = () => {\n const profileImage = useCurrentUserImage()\n const name = useCurrentUserName()\n const initials = name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()\n\n return (\n <Avatar>\n {profileImage && <AvatarImage src={profileImage} alt={initials} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-tanstack",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"avatar"
],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\n\nexport const CurrentUserAvatar = () => {\n const profileImage = useCurrentUserImage()\n const name = useCurrentUserName()\n const initials = name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()\n\n return (\n <Avatar>\n {profileImage && <AvatarImage src={profileImage} alt={initials} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { parseCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!, {\n cookies: {\n getAll() {\n return Object.entries(parseCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n })\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,56 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-nextjs",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"dependencies": [
"@supabase/ssr@latest"
],
"registryDependencies": [
"avatar",
"tooltip"
],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n variants: {\n orientation: {\n vertical: 'flex-row',\n horizontal: 'flex-col',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n})\n\nexport interface AvatarStackProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof avatarStackVariants> {\n avatars: { name: string; image: string }[]\n maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n className,\n orientation,\n avatars,\n maxAvatarsAmount = 3,\n ...props\n}: AvatarStackProps) => {\n const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n return (\n <div\n className={cn(\n avatarStackVariants({ orientation }),\n className,\n orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'\n )}\n {...props}\n >\n {shownAvatars.map(({ name, image }, index) => (\n <Tooltip key={`${name}-${image}-${index}`}>\n <TooltipTrigger asChild>\n <Avatar className={'hover:z-10'}>\n <AvatarImage src={image} />\n <AvatarFallback>\n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n </AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n <p>{name}</p>\n </TooltipContent>\n </Tooltip>\n ))}\n\n {hiddenAvatars.length ? (\n <Tooltip key=\"hidden-avatars\">\n <TooltipTrigger asChild>\n <Avatar>\n <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent className=\"flex flex-col gap-2 bg-gray-100 p-2 rounded border\">\n {hiddenAvatars.map(({ name }, index) => (\n <p key={`${name}-${index}`}>{name}</p>\n ))}\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"content": "'use client'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\nimport { useMemo } from 'react'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n const { users: usersMap } = useRealtimePresenceRoom(roomName)\n const avatars = useMemo(() => {\n return Object.values(usersMap).map((user) => ({\n name: user.name,\n image: user.image,\n }))\n }, [usersMap])\n\n return <AvatarStack avatars={avatars} />\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n id: string\n name: string\n image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n const currentUserImage = useCurrentUserImage()\n const currentUserName = useCurrentUserName()\n\n const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n useEffect(() => {\n const room = supabase.channel(roomName)\n\n room\n .on('presence', { event: 'sync' }, () => {\n const newState = room.presenceState<{ image: string; name: string }>()\n\n const newUsers = Object.fromEntries(\n Object.entries(newState).map(([key, values]) => [\n key,\n { name: values[0].name, image: values[0].image },\n ])\n ) as Record<string, RealtimeUser>\n setUsers(newUsers)\n })\n .subscribe(async (status) => {\n if (status !== 'SUBSCRIBED') {\n return\n }\n\n await room.track({\n name: currentUserName,\n image: currentUserImage,\n })\n })\n\n return () => {\n room.unsubscribe()\n }\n }, [roomName, currentUserName, currentUserImage])\n\n return { users }\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getUser(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: DO NOT REMOVE auth.getUser()\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,51 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-react-router",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"dependencies": [
"@supabase/ssr@latest"
],
"registryDependencies": [
"avatar",
"tooltip"
],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n variants: {\n orientation: {\n vertical: 'flex-row',\n horizontal: 'flex-col',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n})\n\nexport interface AvatarStackProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof avatarStackVariants> {\n avatars: { name: string; image: string }[]\n maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n className,\n orientation,\n avatars,\n maxAvatarsAmount = 3,\n ...props\n}: AvatarStackProps) => {\n const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n return (\n <div\n className={cn(\n avatarStackVariants({ orientation }),\n className,\n orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'\n )}\n {...props}\n >\n {shownAvatars.map(({ name, image }, index) => (\n <Tooltip key={`${name}-${image}-${index}`}>\n <TooltipTrigger asChild>\n <Avatar className={'hover:z-10'}>\n <AvatarImage src={image} />\n <AvatarFallback>\n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n </AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n <p>{name}</p>\n </TooltipContent>\n </Tooltip>\n ))}\n\n {hiddenAvatars.length ? (\n <Tooltip key=\"hidden-avatars\">\n <TooltipTrigger asChild>\n <Avatar>\n <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent className=\"flex flex-col gap-2 bg-gray-100 p-2 rounded border\">\n {hiddenAvatars.map(({ name }, index) => (\n <p key={`${name}-${index}`}>{name}</p>\n ))}\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"content": "'use client'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\nimport { useMemo } from 'react'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n const { users: usersMap } = useRealtimePresenceRoom(roomName)\n const avatars = useMemo(() => {\n return Object.values(usersMap).map((user) => ({\n name: user.name,\n image: user.image,\n }))\n }, [usersMap])\n\n return <AvatarStack avatars={avatars} />\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n id: string\n name: string\n image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n const currentUserImage = useCurrentUserImage()\n const currentUserName = useCurrentUserName()\n\n const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n useEffect(() => {\n const room = supabase.channel(roomName)\n\n room\n .on('presence', { event: 'sync' }, () => {\n const newState = room.presenceState<{ image: string; name: string }>()\n\n const newUsers = Object.fromEntries(\n Object.entries(newState).map(([key, values]) => [\n key,\n { name: values[0].name, image: values[0].image },\n ])\n ) as Record<string, RealtimeUser>\n setUsers(newUsers)\n })\n .subscribe(async (status) => {\n if (status !== 'SUBSCRIBED') {\n return\n }\n\n await room.track({\n name: currentUserName,\n image: currentUserImage,\n })\n })\n\n return () => {\n room.unsubscribe()\n }\n }, [roomName, currentUserName, currentUserImage])\n\n return { users }\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react-router/lib/supabase.client.ts",
"content": "import { createBrowserClient } from '@supabase/ssr'\n\n// Use this function to create a client for the browser. You should pass the env variables through a loader and use them\n// to instantiate the client.\nexport function createClient(supabaseUrl: string, supabaseAnonKey: string) {\n return createBrowserClient(supabaseUrl, supabaseAnonKey)\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase.server.ts",
"content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n })\n\n return { supabase, headers }\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,46 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-react",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"dependencies": [
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"avatar",
"tooltip"
],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n variants: {\n orientation: {\n vertical: 'flex-row',\n horizontal: 'flex-col',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n})\n\nexport interface AvatarStackProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof avatarStackVariants> {\n avatars: { name: string; image: string }[]\n maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n className,\n orientation,\n avatars,\n maxAvatarsAmount = 3,\n ...props\n}: AvatarStackProps) => {\n const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n return (\n <div\n className={cn(\n avatarStackVariants({ orientation }),\n className,\n orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'\n )}\n {...props}\n >\n {shownAvatars.map(({ name, image }, index) => (\n <Tooltip key={`${name}-${image}-${index}`}>\n <TooltipTrigger asChild>\n <Avatar className={'hover:z-10'}>\n <AvatarImage src={image} />\n <AvatarFallback>\n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n </AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n <p>{name}</p>\n </TooltipContent>\n </Tooltip>\n ))}\n\n {hiddenAvatars.length ? (\n <Tooltip key=\"hidden-avatars\">\n <TooltipTrigger asChild>\n <Avatar>\n <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent className=\"flex flex-col gap-2 bg-gray-100 p-2 rounded border\">\n {hiddenAvatars.map(({ name }, index) => (\n <p key={`${name}-${index}`}>{name}</p>\n ))}\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"content": "'use client'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\nimport { useMemo } from 'react'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n const { users: usersMap } = useRealtimePresenceRoom(roomName)\n const avatars = useMemo(() => {\n return Object.values(usersMap).map((user) => ({\n name: user.name,\n image: user.image,\n }))\n }, [usersMap])\n\n return <AvatarStack avatars={avatars} />\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n id: string\n name: string\n image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n const currentUserImage = useCurrentUserImage()\n const currentUserName = useCurrentUserName()\n\n const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n useEffect(() => {\n const room = supabase.channel(roomName)\n\n room\n .on('presence', { event: 'sync' }, () => {\n const newState = room.presenceState<{ image: string; name: string }>()\n\n const newUsers = Object.fromEntries(\n Object.entries(newState).map(([key, values]) => [\n key,\n { name: values[0].name, image: values[0].image },\n ])\n ) as Record<string, RealtimeUser>\n setUsers(newUsers)\n })\n .subscribe(async (status) => {\n if (status !== 'SUBSCRIBED') {\n return\n }\n\n await room.track({\n name: currentUserName,\n image: currentUserImage,\n })\n })\n\n return () => {\n room.unsubscribe()\n }\n }, [roomName, currentUserName, currentUserImage])\n\n return { users }\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -0,0 +1,52 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-tanstack",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"avatar",
"tooltip"
],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n variants: {\n orientation: {\n vertical: 'flex-row',\n horizontal: 'flex-col',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n})\n\nexport interface AvatarStackProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof avatarStackVariants> {\n avatars: { name: string; image: string }[]\n maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n className,\n orientation,\n avatars,\n maxAvatarsAmount = 3,\n ...props\n}: AvatarStackProps) => {\n const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n return (\n <div\n className={cn(\n avatarStackVariants({ orientation }),\n className,\n orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'\n )}\n {...props}\n >\n {shownAvatars.map(({ name, image }, index) => (\n <Tooltip key={`${name}-${image}-${index}`}>\n <TooltipTrigger asChild>\n <Avatar className={'hover:z-10'}>\n <AvatarImage src={image} />\n <AvatarFallback>\n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n </AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n <p>{name}</p>\n </TooltipContent>\n </Tooltip>\n ))}\n\n {hiddenAvatars.length ? (\n <Tooltip key=\"hidden-avatars\">\n <TooltipTrigger asChild>\n <Avatar>\n <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent className=\"flex flex-col gap-2 bg-gray-100 p-2 rounded border\">\n {hiddenAvatars.map(({ name }, index) => (\n <p key={`${name}-${index}`}>{name}</p>\n ))}\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"content": "'use client'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\nimport { useMemo } from 'react'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n const { users: usersMap } = useRealtimePresenceRoom(roomName)\n const avatars = useMemo(() => {\n return Object.values(usersMap).map((user) => ({\n name: user.name,\n image: user.image,\n }))\n }, [usersMap])\n\n return <AvatarStack avatars={avatars} />\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n id: string\n name: string\n image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n const currentUserImage = useCurrentUserImage()\n const currentUserName = useCurrentUserName()\n\n const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n useEffect(() => {\n const room = supabase.channel(roomName)\n\n room\n .on('presence', { event: 'sync' }, () => {\n const newState = room.presenceState<{ image: string; name: string }>()\n\n const newUsers = Object.fromEntries(\n Object.entries(newState).map(([key, values]) => [\n key,\n { name: values[0].name, image: values[0].image },\n ])\n ) as Record<string, RealtimeUser>\n setUsers(newUsers)\n })\n .subscribe(async (status) => {\n if (status !== 'SUBSCRIBED') {\n return\n }\n\n await room.track({\n name: currentUserName,\n image: currentUserImage,\n })\n })\n\n return () => {\n room.unsubscribe()\n }\n }, [roomName, currentUserName, currentUserImage])\n\n return { users }\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { parseCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!, {\n cookies: {\n getAll() {\n return Object.entries(parseCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n })\n}\n",
"type": "registry:lib"
}
]
}

View File

@@ -436,6 +436,286 @@
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-nextjs",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"registryDependencies": ["avatar"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-react",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"registryDependencies": ["avatar"],
"dependencies": ["@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-react-router",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"registryDependencies": ["avatar"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react-router/lib/supabase.client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase.server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar-tanstack",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"registryDependencies": ["avatar"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-nextjs",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"registryDependencies": ["avatar", "tooltip"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-react",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"registryDependencies": ["avatar", "tooltip"],
"dependencies": ["@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-react-router",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"registryDependencies": ["avatar", "tooltip"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/react-router/lib/supabase.client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase.server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack-tanstack",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"registryDependencies": ["avatar", "tooltip"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-nextjs",

View File

@@ -1,9 +1,11 @@
import { type Registry, type RegistryItem } from 'shadcn/registry'
import { clients } from './clients'
import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' assert { type: 'json' }
import dropzone from './default/blocks/dropzone/registry-item.json' assert { type: 'json' }
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' assert { type: 'json' }
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' assert { type: 'json' }
import passwordBasedAuthTanstack from './default/blocks/password-based-auth-tanstack/registry-item.json' assert { type: 'json' }
import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' assert { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' assert { type: 'json' }
import { registryItemAppend } from './utils'
@@ -30,4 +32,6 @@ export const blocks = [
registryItemAppend(passwordBasedAuthTanstack as RegistryItem, [tanstackClient!]),
...combine(dropzone as RegistryItem),
...combine(realtimeCursor as RegistryItem),
...combine(currentUserAvatar as RegistryItem),
...combine(realtimeAvatarStack as RegistryItem),
] as Registry['items']

View File

@@ -0,0 +1,22 @@
'use client'
import { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'
import { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'
export const CurrentUserAvatar = () => {
const profileImage = useCurrentUserImage()
const name = useCurrentUserName()
const initials = name
?.split(' ')
?.map((word) => word[0])
?.join('')
?.toUpperCase()
return (
<Avatar>
{profileImage && <AvatarImage src={profileImage} alt={initials} />}
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
)
}

View File

@@ -0,0 +1,20 @@
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useEffect, useState } from 'react'
export const useCurrentUserImage = () => {
const [image, setImage] = useState<string | null>(null)
useEffect(() => {
const fetchUserImage = async () => {
const { data, error } = await createClient().auth.getSession()
if (error) {
console.error(error)
}
setImage(data.session?.user.user_metadata.avatar_url ?? null)
}
fetchUserImage()
}, [])
return image
}

View File

@@ -0,0 +1,21 @@
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useEffect, useState } from 'react'
export const useCurrentUserName = () => {
const [name, setName] = useState<string | null>(null)
useEffect(() => {
const fetchProfileName = async () => {
const { data, error } = await createClient().auth.getSession()
if (error) {
console.error(error)
}
setName(data.session?.user.user_metadata.full_name ?? '?')
}
fetchProfileName()
}, [])
return name || '?'
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "current-user-avatar",
"type": "registry:component",
"title": "Current User Avatar",
"description": "Component which renders the current user's avatar.",
"registryDependencies": ["avatar"],
"dependencies": [],
"files": [
{
"path": "registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
}
]
}

View File

@@ -0,0 +1,83 @@
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
const avatarStackVariants = cva('flex -space-x-4 -space-y-4', {
variants: {
orientation: {
vertical: 'flex-row',
horizontal: 'flex-col',
},
},
defaultVariants: {
orientation: 'vertical',
},
})
export interface AvatarStackProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof avatarStackVariants> {
avatars: { name: string; image: string }[]
maxAvatarsAmount?: number
}
const AvatarStack = ({
className,
orientation,
avatars,
maxAvatarsAmount = 3,
...props
}: AvatarStackProps) => {
const shownAvatars = avatars.slice(0, maxAvatarsAmount)
const hiddenAvatars = avatars.slice(maxAvatarsAmount)
return (
<div
className={cn(
avatarStackVariants({ orientation }),
className,
orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'
)}
{...props}
>
{shownAvatars.map(({ name, image }, index) => (
<Tooltip key={`${name}-${image}-${index}`}>
<TooltipTrigger asChild>
<Avatar className={'hover:z-10'}>
<AvatarImage src={image} />
<AvatarFallback>
{name
?.split(' ')
?.map((word) => word[0])
?.join('')
?.toUpperCase()}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent>
<p>{name}</p>
</TooltipContent>
</Tooltip>
))}
{hiddenAvatars.length ? (
<Tooltip key="hidden-avatars">
<TooltipTrigger asChild>
<Avatar>
<AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent className="flex flex-col gap-2 bg-gray-100 p-2 rounded border">
{hiddenAvatars.map(({ name }, index) => (
<p key={`${name}-${index}`}>{name}</p>
))}
</TooltipContent>
</Tooltip>
) : null}
</div>
)
}
export { AvatarStack, avatarStackVariants }

View File

@@ -0,0 +1,17 @@
'use client'
import { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'
import { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'
import { useMemo } from 'react'
export const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {
const { users: usersMap } = useRealtimePresenceRoom(roomName)
const avatars = useMemo(() => {
return Object.values(usersMap).map((user) => ({
name: user.name,
image: user.image,
}))
}, [usersMap])
return <AvatarStack avatars={avatars} />
}

View File

@@ -0,0 +1,54 @@
'use client'
import { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'
import { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useEffect, useState } from 'react'
const supabase = createClient()
export type RealtimeUser = {
id: string
name: string
image: string
}
export const useRealtimePresenceRoom = (roomName: string) => {
const currentUserImage = useCurrentUserImage()
const currentUserName = useCurrentUserName()
const [users, setUsers] = useState<Record<string, RealtimeUser>>({})
useEffect(() => {
const room = supabase.channel(roomName)
room
.on('presence', { event: 'sync' }, () => {
const newState = room.presenceState<{ image: string; name: string }>()
const newUsers = Object.fromEntries(
Object.entries(newState).map(([key, values]) => [
key,
{ name: values[0].name, image: values[0].image },
])
) as Record<string, RealtimeUser>
setUsers(newUsers)
})
.subscribe(async (status) => {
if (status !== 'SUBSCRIBED') {
return
}
await room.track({
name: currentUserName,
image: currentUserImage,
})
})
return () => {
room.unsubscribe()
}
}, [roomName, currentUserName, currentUserImage])
return { users }
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-avatar-stack",
"type": "registry:component",
"title": "Avatar Stack with Realtime Presence",
"description": "Component which stack of avatars, tracked by realtime presence.",
"registryDependencies": ["avatar", "tooltip"],
"dependencies": [],
"files": [
{
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
"type": "registry:hook"
},
{
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
"type": "registry:hook"
}
]
}

View File

@@ -0,0 +1,47 @@
'use client'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from 'react'
import { cn } from '@/lib/utils'
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarFallback, AvatarImage }

View File

@@ -0,0 +1,41 @@
'use client'
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'
import { useUser } from 'common'
const CurrentUserAvatarDemo = () => {
// this demo only works on supabase.com because all apps are on the same domain and share cookies
const user = useUser()
const profileImage = user?.user_metadata.avatar_url ?? null
const name = (user?.user_metadata.full_name as string) ?? '?'
const initials = name
?.split(' ')
?.map((word) => word[0])
?.join('')
?.toUpperCase()
return (
<div className="flex flex-col gap-4 items-center justify-center">
<Avatar>
{profileImage && <AvatarImage src={profileImage} alt={initials} />}
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
{!user && (
<span className="text-sm text-foreground-light">
It seems like you&apos;re not logged in. Login via the{' '}
<a
href="https://supabase.com/dashboard/sign-in"
className="text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2"
>
Dashboard
</a>{' '}
to see your avatar.
</span>
)}
</div>
)
}
export default CurrentUserAvatarDemo

View File

@@ -0,0 +1,116 @@
'use client'
import { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'
import { RealtimeUser } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useUser } from 'common'
import { useEffect, useMemo, useState } from 'react'
import { Label_Shadcn_, Switch } from 'ui'
import { getRandomUser } from './utils'
const supabase = createClient()
const roomName = 'realtime-avatar-stack-demo'
const randomUser = getRandomUser()
// This demo is using the supabase.com account to broadcast its data to a realtime channel from a normal Supabase project.
// This is a workaround to make the more interactive. Don't use it this way in production (it only works on supabase.com)
const RealtimeAvatarStackDemo = () => {
// this demo only works on supabase.com because all apps are on the same domain and share cookies
const user = useUser()
const [dashboardUser, setDashboardUser] = useState(false)
// generate a random name for the current user or use his supabase.com name
const currentUserName = useMemo(() => {
let name = randomUser.name
if (dashboardUser) {
name = user?.user_metadata.full_name as string
}
return name ?? '?'
}, [user, dashboardUser, user?.user_metadata.full_name])
// generate a random image for the current user or use his supabase.com avatar
const currentUserImage = useMemo(() => {
let image = randomUser.image
if (dashboardUser) {
image = (user?.user_metadata.avatar_url as string) ?? null
}
return image
}, [user, dashboardUser, user?.user_metadata.avatar_url])
const [usersMap, setUsersMap] = useState<Record<string, RealtimeUser> | null>(null)
useEffect(() => {
const room = supabase.channel(roomName)
room
.on('presence', { event: 'sync' }, () => {
const newState = room.presenceState<{ image: string; name: string }>()
const newUsers = Object.fromEntries(
Object.entries(newState).map(([key, values]) => [
key,
{ name: values[0].name, image: values[0].image },
])
) as Record<string, RealtimeUser>
setUsersMap(newUsers)
})
.subscribe(async (status) => {
if (status !== 'SUBSCRIBED') {
return
}
await room.track({
name: currentUserName,
image: currentUserImage,
})
})
return () => {
room.unsubscribe()
}
}, [currentUserName, currentUserImage, roomName])
const avatars = useMemo(() => {
return Object.values(usersMap || {}).map((user) => ({
name: user.name,
image: user.image,
}))
}, [usersMap])
if (usersMap === null) {
return null
}
return (
<div className="flex flex-col gap-4 items-center justify-center">
<AvatarStack avatars={avatars} />
{avatars.length < 2 ? (
<div className="flex flex-col text-sm text-foreground-light">
<span>It seems like you're the only person viewing this page.</span>
<span>Open this page in another browser tab to see it in action.</span>
</div>
) : user ? (
<div className="flex items-center space-x-2">
<Switch id="current-user" checked={dashboardUser} onCheckedChange={setDashboardUser} />
<Label_Shadcn_ htmlFor="current-user">Use my supabase.com account instead</Label_Shadcn_>
</div>
) : (
<span className="text-sm text-foreground-light">
It seems like you&apos;re not logged in. Login via the{' '}
<a
href="https://supabase.com/dashboard/sign-in"
className="text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2"
>
Dashboard
</a>{' '}
to see your avatar.
</span>
)}
</div>
)
}
export default RealtimeAvatarStackDemo

View File

@@ -4,14 +4,13 @@ import { RealtimeCursors } from '@/registry/default/blocks/realtime-cursor/compo
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useEffect, useState } from 'react'
const names = ['Eren', 'Armin', 'Mikasa', 'Reiner', 'Levi', 'Bertholdt']
import { generateFullName } from './utils'
const RealtimeCursorDemo = () => {
const [username, setUsername] = useState('')
useEffect(() => {
setUsername(names[Math.floor(Math.random() * names.length)])
setUsername(generateFullName())
}, [])
return (

View File

@@ -0,0 +1,56 @@
import { sample } from 'lodash'
const users = [
{
name: 'Gemma Scout',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-0.png`,
},
{
name: 'Miss Casey',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-0.png`,
},
{
name: 'Mark S.',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-1.png`,
},
{
name: 'Mark Scout',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-1.png`,
},
{
name: 'Seth Milchik',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-2.png`,
},
{
name: 'Helly R.',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-3.png`,
},
{
name: 'Helena Eagan',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-3.png`,
},
{
name: 'Dylan G.',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-4.png`,
},
{
name: 'Dylan George',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-4.png`,
},
{
name: 'Irving B.',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-5.png`,
},
{
name: 'Irving Bailiff',
image: `${process.env.NEXT_PUBLIC_BASE_PATH}/img/profile-images/profile-5.png`,
},
]
export function getRandomUser() {
return sample(users)!
}
export function generateFullName(): string {
return sample(users)?.name!
}

View File

@@ -34,4 +34,26 @@ export const examples: Registry['items'] = [
},
],
},
{
name: 'current-user-avatar-demo',
type: 'registry:example',
registryDependencies: [],
files: [
{
path: 'registry/default/examples/current-user-avatar-demo.tsx',
type: 'registry:example',
},
],
},
{
name: 'realtime-avatar-stack-demo',
type: 'registry:example',
registryDependencies: [],
files: [
{
path: 'registry/default/examples/realtime-avatar-stack-demo.tsx',
type: 'registry:example',
},
],
},
]

View File

@@ -253,6 +253,19 @@ url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
[auth.external.github]
enabled = true
client_id = "env(SUPABASE_AUTH_GITHUB_CLIENT_ID)"
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_GITHUB_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = "http://localhost:54321/auth/v1/callback"
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false

52
pnpm-lock.yaml generated
View File

@@ -859,7 +859,7 @@ importers:
version: 9.3.4
'@testing-library/jest-dom':
specifier: ^6.4.6
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4)
'@testing-library/react':
specifier: ^14.0.0
version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -992,6 +992,9 @@ importers:
apps/ui-library:
dependencies:
'@radix-ui/react-avatar':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1007,6 +1010,9 @@ importers:
class-variance-authority:
specifier: ^0.6.0
version: 0.6.1
common:
specifier: workspace:*
version: link:../../packages/common
contentlayer2:
specifier: 0.4.6
version: 0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1)
@@ -1855,7 +1861,7 @@ importers:
version: 3.6.1
'@testing-library/jest-dom':
specifier: ^6.1.3
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)
'@testing-library/react':
specifier: ^14.0.0
version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -2030,7 +2036,7 @@ importers:
version: 10.1.0
'@testing-library/jest-dom':
specifier: ^6.4.6
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)
'@testing-library/react':
specifier: ^16.0.0
version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -9758,9 +9764,6 @@ packages:
hast-util-to-estree@2.3.3:
resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==}
hast-util-to-estree@3.1.0:
resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==}
hast-util-to-estree@3.1.3:
resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
@@ -17370,8 +17373,8 @@ snapshots:
estree-util-is-identifier-name: 3.0.0
estree-util-to-js: 2.0.0
estree-walker: 3.0.3
hast-util-to-estree: 3.1.0(supports-color@8.1.1)
hast-util-to-jsx-runtime: 2.3.0(supports-color@8.1.1)
hast-util-to-estree: 3.1.3(supports-color@8.1.1)
hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1)
markdown-extensions: 2.0.0
periscopic: 3.1.0
remark-mdx: 3.0.1(supports-color@8.1.1)
@@ -20980,7 +20983,7 @@ snapshots:
lz-string: 1.5.0
pretty-format: 27.5.1
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))':
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4)':
dependencies:
'@adobe/css-tools': 4.4.0
'@babel/runtime': 7.24.7
@@ -20996,7 +20999,7 @@ snapshots:
jest: 29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2))
vitest: 3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5)
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))':
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)':
dependencies:
'@adobe/css-tools': 4.4.0
'@babel/runtime': 7.24.7
@@ -23929,7 +23932,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
enhanced-resolve: 5.17.1
eslint: 8.57.0(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
@@ -23941,7 +23944,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
@@ -23968,7 +23971,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0(supports-color@8.1.1)
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -25038,27 +25041,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
hast-util-to-estree@3.1.0(supports-color@8.1.1):
dependencies:
'@types/estree': 1.0.5
'@types/estree-jsx': 1.0.1
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
devlop: 1.1.0
estree-util-attach-comments: 3.0.0
estree-util-is-identifier-name: 3.0.0
hast-util-whitespace: 3.0.0
mdast-util-mdx-expression: 2.0.0(supports-color@8.1.1)
mdast-util-mdx-jsx: 3.1.2(supports-color@8.1.1)
mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1)
property-information: 6.3.0
space-separated-tokens: 2.0.2
style-to-object: 0.4.2
unist-util-position: 5.0.0
zwitch: 2.0.4
transitivePeerDependencies:
- supports-color
hast-util-to-estree@3.1.3(supports-color@8.1.1):
dependencies:
'@types/estree': 1.0.5
@@ -30888,7 +30870,7 @@ snapshots:
dependencies:
client-only: 0.0.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
use-sync-external-store: 1.4.0(react@18.2.0)
swrev@4.0.0: {}