diff --git a/apps/ui-library/__registry__/index.tsx b/apps/ui-library/__registry__/index.tsx index 14e3098441..ea26a79d8b 100644 --- a/apps/ui-library/__registry__/index.tsx +++ b/apps/ui-library/__registry__/index.tsx @@ -139,6 +139,102 @@ export const Index: Record = { 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 = { 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: [] + } }, } diff --git a/apps/ui-library/components/block-item-code.tsx b/apps/ui-library/components/block-item-code.tsx index 1187f340cb..aedd1d4e56 100644 --- a/apps/ui-library/components/block-item-code.tsx +++ b/apps/ui-library/components/block-item-code.tsx @@ -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) { {selectedFile?.content} diff --git a/apps/ui-library/components/code-fragment.tsx b/apps/ui-library/components/code-fragment.tsx index afe821331d..c3ef648478 100644 --- a/apps/ui-library/components/code-fragment.tsx +++ b/apps/ui-library/components/code-fragment.tsx @@ -13,7 +13,6 @@ interface ComponentPreviewProps extends React.HTMLAttributes { 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, diff --git a/apps/ui-library/components/component-preview.tsx b/apps/ui-library/components/component-preview.tsx index 0b02b88b3a..07a3e01182 100644 --- a/apps/ui-library/components/component-preview.tsx +++ b/apps/ui-library/components/component-preview.tsx @@ -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 { 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 ( -
-
- {showGrid && ( -
- )} - {showDottedGrid && ( -
- )} -
{ComponentPreview}
- {/*
*/} -
-
-
- {Code} -
- -
-
-
-
- ) - } - return (
{showGrid && (
@@ -146,11 +98,11 @@ export function ComponentPreview({
)}
{ComponentPreview}
- {/*
*/}
- - + - - View code - - -
- {Code} -
-
-
+ + View code + + +
+ {Code} +
+
+ + )}
) } diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts index 14ba68d4a4..bc860467f5 100644 --- a/apps/ui-library/config/docs.ts +++ b/apps/ui-library/config/docs.ts @@ -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 = [ diff --git a/apps/ui-library/content/docs/nextjs/current-user-avatar.mdx b/apps/ui-library/content/docs/nextjs/current-user-avatar.mdx new file mode 100644 index 0000000000..e02f5e6a4a --- /dev/null +++ b/apps/ui-library/content/docs/nextjs/current-user-avatar.mdx @@ -0,0 +1,24 @@ +--- +title: Current User Avatar +description: Supabase Auth-aware avatar +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx index 9cd6452fd3..9957ee2f86 100644 --- a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx +++ b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx @@ -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. diff --git a/apps/ui-library/content/docs/nextjs/realtime-avatar-stack.mdx b/apps/ui-library/content/docs/nextjs/realtime-avatar-stack.mdx new file mode 100644 index 0000000000..3d2d9375f5 --- /dev/null +++ b/apps/ui-library/content/docs/nextjs/realtime-avatar-stack.mdx @@ -0,0 +1,21 @@ +--- +title: Realtime Avatar Stack +description: Avatar stack in realtime +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/react-router/current-user-avatar.mdx b/apps/ui-library/content/docs/react-router/current-user-avatar.mdx new file mode 100644 index 0000000000..d962179a6b --- /dev/null +++ b/apps/ui-library/content/docs/react-router/current-user-avatar.mdx @@ -0,0 +1,24 @@ +--- +title: Current User Avatar +description: Supabase Auth-aware avatar +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/react-router/realtime-avatar-stack.mdx b/apps/ui-library/content/docs/react-router/realtime-avatar-stack.mdx new file mode 100644 index 0000000000..dd48141fda --- /dev/null +++ b/apps/ui-library/content/docs/react-router/realtime-avatar-stack.mdx @@ -0,0 +1,21 @@ +--- +title: Realtime Avatar Stack +description: Avatar stack in realtime +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/react/current-user-avatar.mdx b/apps/ui-library/content/docs/react/current-user-avatar.mdx new file mode 100644 index 0000000000..d5a5dcd46a --- /dev/null +++ b/apps/ui-library/content/docs/react/current-user-avatar.mdx @@ -0,0 +1,21 @@ +--- +title: Current User Avatar +description: Supabase Auth-aware avatar +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/react/realtime-avatar-stack.mdx b/apps/ui-library/content/docs/react/realtime-avatar-stack.mdx new file mode 100644 index 0000000000..33ced96f95 --- /dev/null +++ b/apps/ui-library/content/docs/react/realtime-avatar-stack.mdx @@ -0,0 +1,21 @@ +--- +title: Realtime Avatar Stack +description: Avatar stack in realtime +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/tanstack/current-user-avatar.mdx b/apps/ui-library/content/docs/tanstack/current-user-avatar.mdx new file mode 100644 index 0000000000..746b06d5cb --- /dev/null +++ b/apps/ui-library/content/docs/tanstack/current-user-avatar.mdx @@ -0,0 +1,24 @@ +--- +title: Current User Avatar +description: Supabase Auth-aware avatar +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/content/docs/tanstack/realtime-avatar-stack.mdx b/apps/ui-library/content/docs/tanstack/realtime-avatar-stack.mdx new file mode 100644 index 0000000000..a7d588531c --- /dev/null +++ b/apps/ui-library/content/docs/tanstack/realtime-avatar-stack.mdx @@ -0,0 +1,21 @@ +--- +title: Realtime Avatar Stack +description: Avatar stack in realtime +--- + + + +## Installation + + + +## Folder structure + + + +## 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. diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index ae3ed88ec6..5e542c01ce 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -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", diff --git a/apps/ui-library/public/img/profile-images/profile-0.png b/apps/ui-library/public/img/profile-images/profile-0.png new file mode 100644 index 0000000000..7507a159fd Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-0.png differ diff --git a/apps/ui-library/public/img/profile-images/profile-1.png b/apps/ui-library/public/img/profile-images/profile-1.png new file mode 100644 index 0000000000..8a1befd6a9 Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-1.png differ diff --git a/apps/ui-library/public/img/profile-images/profile-2.png b/apps/ui-library/public/img/profile-images/profile-2.png new file mode 100644 index 0000000000..4d0c632893 Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-2.png differ diff --git a/apps/ui-library/public/img/profile-images/profile-3.png b/apps/ui-library/public/img/profile-images/profile-3.png new file mode 100644 index 0000000000..6a96ed536d Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-3.png differ diff --git a/apps/ui-library/public/img/profile-images/profile-4.png b/apps/ui-library/public/img/profile-images/profile-4.png new file mode 100644 index 0000000000..cc4579abc4 Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-4.png differ diff --git a/apps/ui-library/public/img/profile-images/profile-5.png b/apps/ui-library/public/img/profile-images/profile-5.png new file mode 100644 index 0000000000..8b7afa42b6 Binary files /dev/null and b/apps/ui-library/public/img/profile-images/profile-5.png differ diff --git a/apps/ui-library/public/llms.txt b/apps/ui-library/public/llms.txt index 58da4410fd..04337c1622 100644 --- a/apps/ui-library/public/llms.txt +++ b/apps/ui-library/public/llms.txt @@ -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. diff --git a/apps/ui-library/public/r/current-user-avatar-nextjs.json b/apps/ui-library/public/r/current-user-avatar-nextjs.json new file mode 100644 index 0000000000..0d6c9afb86 --- /dev/null +++ b/apps/ui-library/public/r/current-user-avatar-nextjs.json @@ -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 \n {profileImage && }\n {initials}\n \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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-react-router.json b/apps/ui-library/public/r/current-user-avatar-react-router.json new file mode 100644 index 0000000000..a8ee5dbd74 --- /dev/null +++ b/apps/ui-library/public/r/current-user-avatar-react-router.json @@ -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 \n {profileImage && }\n {initials}\n \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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-react.json b/apps/ui-library/public/r/current-user-avatar-react.json new file mode 100644 index 0000000000..f506572f7e --- /dev/null +++ b/apps/ui-library/public/r/current-user-avatar-react.json @@ -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 \n {profileImage && }\n {initials}\n \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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-tanstack.json b/apps/ui-library/public/r/current-user-avatar-tanstack.json new file mode 100644 index 0000000000..8038da79e5 --- /dev/null +++ b/apps/ui-library/public/r/current-user-avatar-tanstack.json @@ -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 \n {profileImage && }\n {initials}\n \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(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(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": "/// \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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json new file mode 100644 index 0000000000..4b988897e1 --- /dev/null +++ b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json @@ -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,\n VariantProps {\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 \n {shownAvatars.map(({ name, image }, index) => (\n \n \n \n \n \n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n \n \n \n \n

{name}

\n
\n
\n ))}\n\n {hiddenAvatars.length ? (\n \n \n \n +{avatars.length - shownAvatars.length}\n \n \n \n {hiddenAvatars.map(({ name }, index) => (\n

{name}

\n ))}\n
\n
\n ) : null}\n \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 \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>({})\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\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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-react-router.json b/apps/ui-library/public/r/realtime-avatar-stack-react-router.json new file mode 100644 index 0000000000..8fc5a2f778 --- /dev/null +++ b/apps/ui-library/public/r/realtime-avatar-stack-react-router.json @@ -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,\n VariantProps {\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 \n {shownAvatars.map(({ name, image }, index) => (\n \n \n \n \n \n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n \n \n \n \n

{name}

\n
\n
\n ))}\n\n {hiddenAvatars.length ? (\n \n \n \n +{avatars.length - shownAvatars.length}\n \n \n \n {hiddenAvatars.map(({ name }, index) => (\n

{name}

\n ))}\n
\n
\n ) : null}\n \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 \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>({})\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\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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-react.json b/apps/ui-library/public/r/realtime-avatar-stack-react.json new file mode 100644 index 0000000000..6f4825557a --- /dev/null +++ b/apps/ui-library/public/r/realtime-avatar-stack-react.json @@ -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,\n VariantProps {\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 \n {shownAvatars.map(({ name, image }, index) => (\n \n \n \n \n \n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n \n \n \n \n

{name}

\n
\n
\n ))}\n\n {hiddenAvatars.length ? (\n \n \n \n +{avatars.length - shownAvatars.length}\n \n \n \n {hiddenAvatars.map(({ name }, index) => (\n

{name}

\n ))}\n
\n
\n ) : null}\n \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 \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>({})\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\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(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(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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json b/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json new file mode 100644 index 0000000000..61dce22414 --- /dev/null +++ b/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json @@ -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,\n VariantProps {\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 \n {shownAvatars.map(({ name, image }, index) => (\n \n \n \n \n \n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n \n \n \n \n

{name}

\n
\n
\n ))}\n\n {hiddenAvatars.length ? (\n \n \n \n +{avatars.length - shownAvatars.length}\n \n \n \n {hiddenAvatars.map(({ name }, index) => (\n

{name}

\n ))}\n
\n
\n ) : null}\n \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 \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>({})\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\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(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(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": "/// \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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/registry.json b/apps/ui-library/registry.json index 911e292cf1..772924348a 100644 --- a/apps/ui-library/registry.json +++ b/apps/ui-library/registry.json @@ -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", diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts index 7db43270b5..f3ee03532f 100644 --- a/apps/ui-library/registry/blocks.ts +++ b/apps/ui-library/registry/blocks.ts @@ -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'] diff --git a/apps/ui-library/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx b/apps/ui-library/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx new file mode 100644 index 0000000000..dd33755a20 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/current-user-avatar/components/current-user-avatar.tsx @@ -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 ( + + {profileImage && } + {initials} + + ) +} diff --git a/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts b/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts new file mode 100644 index 0000000000..6592edc905 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts @@ -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(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 +} diff --git a/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts b/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts new file mode 100644 index 0000000000..80d2c3801d --- /dev/null +++ b/apps/ui-library/registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts @@ -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(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 || '?' +} diff --git a/apps/ui-library/registry/default/blocks/current-user-avatar/registry-item.json b/apps/ui-library/registry/default/blocks/current-user-avatar/registry-item.json new file mode 100644 index 0000000000..d2b14de199 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/current-user-avatar/registry-item.json @@ -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" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx new file mode 100644 index 0000000000..a28692009e --- /dev/null +++ b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx @@ -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, + VariantProps { + 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 ( +
+ {shownAvatars.map(({ name, image }, index) => ( + + + + + + {name + ?.split(' ') + ?.map((word) => word[0]) + ?.join('') + ?.toUpperCase()} + + + + +

{name}

+
+
+ ))} + + {hiddenAvatars.length ? ( + + + + +{avatars.length - shownAvatars.length} + + + + {hiddenAvatars.map(({ name }, index) => ( +

{name}

+ ))} +
+
+ ) : null} +
+ ) +} + +export { AvatarStack, avatarStackVariants } diff --git a/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx new file mode 100644 index 0000000000..6137163a28 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx @@ -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 +} diff --git a/apps/ui-library/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts new file mode 100644 index 0000000000..e49b4e820f --- /dev/null +++ b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts @@ -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>({}) + + 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 + setUsers(newUsers) + }) + .subscribe(async (status) => { + if (status !== 'SUBSCRIBED') { + return + } + + await room.track({ + name: currentUserName, + image: currentUserImage, + }) + }) + + return () => { + room.unsubscribe() + } + }, [roomName, currentUserName, currentUserImage]) + + return { users } +} diff --git a/apps/ui-library/registry/default/blocks/realtime-avatar-stack/registry-item.json b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/registry-item.json new file mode 100644 index 0000000000..ee3754b71c --- /dev/null +++ b/apps/ui-library/registry/default/blocks/realtime-avatar-stack/registry-item.json @@ -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" + } + ] +} diff --git a/apps/ui-library/registry/default/components/ui/avatar.tsx b/apps/ui-library/registry/default/components/ui/avatar.tsx new file mode 100644 index 0000000000..0553e3aa99 --- /dev/null +++ b/apps/ui-library/registry/default/components/ui/avatar.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarFallback, AvatarImage } diff --git a/apps/ui-library/registry/default/examples/current-user-avatar-demo.tsx b/apps/ui-library/registry/default/examples/current-user-avatar-demo.tsx new file mode 100644 index 0000000000..ed847e94be --- /dev/null +++ b/apps/ui-library/registry/default/examples/current-user-avatar-demo.tsx @@ -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 ( +
+ + {profileImage && } + {initials} + + + {!user && ( + + It seems like you're not logged in. Login via the{' '} + + Dashboard + {' '} + to see your avatar. + + )} +
+ ) +} + +export default CurrentUserAvatarDemo diff --git a/apps/ui-library/registry/default/examples/realtime-avatar-stack-demo.tsx b/apps/ui-library/registry/default/examples/realtime-avatar-stack-demo.tsx new file mode 100644 index 0000000000..073b4c364b --- /dev/null +++ b/apps/ui-library/registry/default/examples/realtime-avatar-stack-demo.tsx @@ -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 | 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 + 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 ( +
+ + + {avatars.length < 2 ? ( +
+ It seems like you're the only person viewing this page. + Open this page in another browser tab to see it in action. +
+ ) : user ? ( +
+ + Use my supabase.com account instead +
+ ) : ( + + It seems like you're not logged in. Login via the{' '} + + Dashboard + {' '} + to see your avatar. + + )} +
+ ) +} + +export default RealtimeAvatarStackDemo diff --git a/apps/ui-library/registry/default/examples/realtime-cursor-demo.tsx b/apps/ui-library/registry/default/examples/realtime-cursor-demo.tsx index 74f086bdba..9e4d6c148d 100644 --- a/apps/ui-library/registry/default/examples/realtime-cursor-demo.tsx +++ b/apps/ui-library/registry/default/examples/realtime-cursor-demo.tsx @@ -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 ( diff --git a/apps/ui-library/registry/default/examples/utils.ts b/apps/ui-library/registry/default/examples/utils.ts new file mode 100644 index 0000000000..80dbd6c3b7 --- /dev/null +++ b/apps/ui-library/registry/default/examples/utils.ts @@ -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! +} diff --git a/apps/ui-library/registry/examples.ts b/apps/ui-library/registry/examples.ts index a4105724d3..e444e3b156 100644 --- a/apps/ui-library/registry/examples.ts +++ b/apps/ui-library/registry/examples.ts @@ -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', + }, + ], + }, ] diff --git a/apps/ui-library/supabase/config.toml b/apps/ui-library/supabase/config.toml index 73e1ef8f07..372fd92e8e 100644 --- a/apps/ui-library/supabase/config.toml +++ b/apps/ui-library/supabase/config.toml @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6a5955a1d..a2bc4a0243 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}