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:
@@ -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: []
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
24
apps/ui-library/content/docs/nextjs/current-user-avatar.mdx
Normal file
24
apps/ui-library/content/docs/nextjs/current-user-avatar.mdx
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
21
apps/ui-library/content/docs/react/current-user-avatar.mdx
Normal file
21
apps/ui-library/content/docs/react/current-user-avatar.mdx
Normal 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.
|
||||
21
apps/ui-library/content/docs/react/realtime-avatar-stack.mdx
Normal file
21
apps/ui-library/content/docs/react/realtime-avatar-stack.mdx
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
BIN
apps/ui-library/public/img/profile-images/profile-0.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 874 KiB |
BIN
apps/ui-library/public/img/profile-images/profile-1.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
apps/ui-library/public/img/profile-images/profile-2.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 883 KiB |
BIN
apps/ui-library/public/img/profile-images/profile-3.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 949 KiB |
BIN
apps/ui-library/public/img/profile-images/profile-4.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 811 KiB |
BIN
apps/ui-library/public/img/profile-images/profile-5.png
Normal file
BIN
apps/ui-library/public/img/profile-images/profile-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 833 KiB |
@@ -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.
|
||||
|
||||
45
apps/ui-library/public/r/current-user-avatar-nextjs.json
Normal file
45
apps/ui-library/public/r/current-user-avatar-nextjs.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
apps/ui-library/public/r/current-user-avatar-react.json
Normal file
35
apps/ui-library/public/r/current-user-avatar-react.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
apps/ui-library/public/r/current-user-avatar-tanstack.json
Normal file
41
apps/ui-library/public/r/current-user-avatar-tanstack.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
apps/ui-library/public/r/realtime-avatar-stack-nextjs.json
Normal file
56
apps/ui-library/public/r/realtime-avatar-stack-nextjs.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
apps/ui-library/public/r/realtime-avatar-stack-react.json
Normal file
46
apps/ui-library/public/r/realtime-avatar-stack-react.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
apps/ui-library/public/r/realtime-avatar-stack-tanstack.json
Normal file
52
apps/ui-library/public/r/realtime-avatar-stack-tanstack.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 || '?'
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
apps/ui-library/registry/default/components/ui/avatar.tsx
Normal file
47
apps/ui-library/registry/default/components/ui/avatar.tsx
Normal 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 }
|
||||
@@ -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'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
|
||||
@@ -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'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
|
||||
@@ -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 (
|
||||
|
||||
56
apps/ui-library/registry/default/examples/utils.ts
Normal file
56
apps/ui-library/registry/default/examples/utils.ts
Normal 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!
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
52
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user