mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 12:55:06 +03:00
* fix(ui library): fix presence room to clear stack on subscription error * fix(ui library): fix realtime chat on subscription error * feat(ui library): improve realtime cursors joining, leaving and disconnecting Presence is used now to react to cursors joining and leaving. * When a cursor joins all the other cursors broadcast their last position (if available). This avoids an empty page when joining if cursors are not moving * When a cursor leaves the room their respective cursor is removed And finally the cursor position is broadcasted only the channel is successfully subscribed. Otherwise it might try to send POST requests to broadcast outside the websocket * chore(ui library): update registry & llms * chore: prettier * fix(ui-library): fix missing username on cursor * chore(ui-library): npm run build
57 lines
8.5 KiB
JSON
57 lines
8.5 KiB
JSON
{
|
|
"$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",
|
|
"@supabase/supabase-js@latest"
|
|
],
|
|
"registryDependencies": [
|
|
"avatar",
|
|
"tooltip"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
|
|
"content": "import { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n variants: {\n orientation: {\n vertical: 'flex-row',\n horizontal: 'flex-col',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n})\n\nexport interface AvatarStackProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof avatarStackVariants> {\n avatars: { name: string; image: string }[]\n maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n className,\n orientation,\n avatars,\n maxAvatarsAmount = 3,\n ...props\n}: AvatarStackProps) => {\n const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n return (\n <div\n className={cn(\n avatarStackVariants({ orientation }),\n className,\n orientation === 'horizontal' ? '-space-x-0' : '-space-y-0'\n )}\n {...props}\n >\n {shownAvatars.map(({ name, image }, index) => (\n <Tooltip key={`${name}-${image}-${index}`}>\n <TooltipTrigger asChild>\n <Avatar className=\"hover:z-10\">\n <AvatarImage src={image} />\n <AvatarFallback>\n {name\n ?.split(' ')\n ?.map((word) => word[0])\n ?.join('')\n ?.toUpperCase()}\n </AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n <p>{name}</p>\n </TooltipContent>\n </Tooltip>\n ))}\n\n {hiddenAvatars.length ? (\n <Tooltip key=\"hidden-avatars\">\n <TooltipTrigger asChild>\n <Avatar>\n <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n </Avatar>\n </TooltipTrigger>\n <TooltipContent>\n {hiddenAvatars.map(({ name }, index) => (\n <p key={`${name}-${index}`}>{name}</p>\n ))}\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
|
|
"type": "registry:component"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
|
|
"content": "'use client'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\nimport { useMemo } from 'react'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n const { users: usersMap } = useRealtimePresenceRoom(roomName)\n const avatars = useMemo(() => {\n return Object.values(usersMap).map((user) => ({\n name: user.name,\n image: user.image,\n }))\n }, [usersMap])\n\n return <AvatarStack avatars={avatars} />\n}\n",
|
|
"type": "registry:component"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
|
|
"content": "'use client'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js'\nimport { useEffect, useState } from 'react'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n id: string\n name: string\n image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n const currentUserImage = useCurrentUserImage()\n const currentUserName = useCurrentUserName()\n\n const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n useEffect(() => {\n const room = supabase.channel(roomName)\n\n room\n .on('presence', { event: 'sync' }, () => {\n const newState = room.presenceState<{ image: string; name: string }>()\n\n const newUsers = Object.fromEntries(\n Object.entries(newState).map(([key, values]) => [\n key,\n { name: values[0].name, image: values[0].image },\n ])\n ) as Record<string, RealtimeUser>\n setUsers(newUsers)\n })\n .subscribe(async (status) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n await room.track({\n name: currentUserName,\n image: currentUserImage,\n })\n } else {\n setUsers({})\n }\n })\n\n return () => {\n room.unsubscribe()\n }\n }, [roomName, currentUserName, currentUserImage])\n\n return { users }\n}\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
|
|
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserName = () => {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchProfileName = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setName(data.session?.user.user_metadata.full_name ?? '?')\n }\n\n fetchProfileName()\n }, [])\n\n return name || '?'\n}\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
|
|
"content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useEffect, useState } from 'react'\n\nexport const useCurrentUserImage = () => {\n const [image, setImage] = useState<string | null>(null)\n\n useEffect(() => {\n const fetchUserImage = async () => {\n const { data, error } = await createClient().auth.getSession()\n if (error) {\n console.error(error)\n }\n\n setImage(data.session?.user.user_metadata.avatar_url ?? null)\n }\n fetchUserImage()\n }, [])\n\n return image\n}\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react-router/lib/supabase/client.ts",
|
|
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\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(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\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\n return { supabase, headers }\n}\n",
|
|
"type": "registry:lib"
|
|
}
|
|
],
|
|
"envVars": {
|
|
"VITE_SUPABASE_URL": "",
|
|
"VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
|
|
},
|
|
"docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
|
|
} |