{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "realtime-chat-react-router", "type": "registry:component", "title": "Realtime Chat", "description": "Component which renders realtime chat messages from other users in a room.", "dependencies": [ "lucide-react", "@supabase/ssr@latest", "@supabase/supabase-js@latest" ], "registryDependencies": [ "input", "button" ], "files": [ { "path": "registry/default/blocks/realtime-chat/components/chat-message.tsx", "content": "import { cn } from '@/lib/utils'\nimport type { ChatMessage } from '@/registry/default/blocks/realtime-chat/hooks/use-realtime-chat'\n\ninterface ChatMessageItemProps {\n message: ChatMessage\n isOwnMessage: boolean\n showHeader: boolean\n}\n\nexport const ChatMessageItem = ({ message, isOwnMessage, showHeader }: ChatMessageItemProps) => {\n return (\n
\n \n {showHeader && (\n \n {message.user.name}\n \n {new Date(message.createdAt).toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: true,\n })}\n \n
\n )}\n \n {message.content}\n \n \n \n )\n}\n", "type": "registry:component" }, { "path": "registry/default/blocks/realtime-chat/components/realtime-chat.tsx", "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { ChatMessageItem } from '@/registry/default/blocks/realtime-chat/components/chat-message'\nimport { useChatScroll } from '@/registry/default/blocks/realtime-chat/hooks/use-chat-scroll'\nimport {\n type ChatMessage,\n useRealtimeChat,\n} from '@/registry/default/blocks/realtime-chat/hooks/use-realtime-chat'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Send } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\n\ninterface RealtimeChatProps {\n roomName: string\n username: string\n onMessage?: (messages: ChatMessage[]) => void\n messages?: ChatMessage[]\n}\n\n/**\n * Realtime chat component\n * @param roomName - The name of the room to join. Each room is a unique chat.\n * @param username - The username of the user\n * @param onMessage - The callback function to handle the messages. Useful if you want to store the messages in a database.\n * @param messages - The messages to display in the chat. Useful if you want to display messages from a database.\n * @returns The chat component\n */\nexport const RealtimeChat = ({\n roomName,\n username,\n onMessage,\n messages: initialMessages = [],\n}: RealtimeChatProps) => {\n const { containerRef, scrollToBottom } = useChatScroll()\n\n const {\n messages: realtimeMessages,\n sendMessage,\n isConnected,\n } = useRealtimeChat({\n roomName,\n username,\n })\n const [newMessage, setNewMessage] = useState('')\n\n // Merge realtime messages with initial messages\n const allMessages = useMemo(() => {\n const mergedMessages = [...initialMessages, ...realtimeMessages]\n // Remove duplicates based on message id\n const uniqueMessages = mergedMessages.filter(\n (message, index, self) => index === self.findIndex((m) => m.id === message.id)\n )\n // Sort by creation date\n const sortedMessages = uniqueMessages.sort((a, b) => a.createdAt.localeCompare(b.createdAt))\n\n return sortedMessages\n }, [initialMessages, realtimeMessages])\n\n useEffect(() => {\n if (onMessage) {\n onMessage(allMessages)\n }\n }, [allMessages, onMessage])\n\n useEffect(() => {\n // Scroll to bottom whenever messages change\n scrollToBottom()\n }, [allMessages, scrollToBottom])\n\n const handleSendMessage = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault()\n if (!newMessage.trim() || !isConnected) return\n\n sendMessage(newMessage)\n setNewMessage('')\n },\n [newMessage, isConnected, sendMessage]\n )\n\n return (\n
\n {/* Messages */}\n
\n {allMessages.length === 0 ? (\n
\n No messages yet. Start the conversation!\n
\n ) : null}\n
\n {allMessages.map((message, index) => {\n const prevMessage = index > 0 ? allMessages[index - 1] : null\n const showHeader = !prevMessage || prevMessage.user.name !== message.user.name\n\n return (\n \n \n
\n )\n })}\n
\n
\n\n
\n setNewMessage(e.target.value)}\n placeholder=\"Type a message...\"\n disabled={!isConnected}\n />\n {isConnected && newMessage.trim() && (\n \n \n \n )}\n \n \n )\n}\n", "type": "registry:component" }, { "path": "registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx", "content": "'use client'\n\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useState } from 'react'\n\ninterface UseRealtimeChatProps {\n roomName: string\n username: string\n}\n\nexport interface ChatMessage {\n id: string\n content: string\n user: {\n name: string\n }\n createdAt: string\n}\n\nconst EVENT_MESSAGE_TYPE = 'message'\n\nexport function useRealtimeChat({ roomName, username }: UseRealtimeChatProps) {\n const supabase = createClient()\n const [messages, setMessages] = useState([])\n const [channel, setChannel] = useState | null>(null)\n const [isConnected, setIsConnected] = useState(false)\n\n useEffect(() => {\n const newChannel = supabase.channel(roomName)\n\n newChannel\n .on('broadcast', { event: EVENT_MESSAGE_TYPE }, (payload) => {\n setMessages((current) => [...current, payload.payload as ChatMessage])\n })\n .subscribe(async (status) => {\n if (status === 'SUBSCRIBED') {\n setIsConnected(true)\n } else {\n setIsConnected(false)\n }\n })\n\n setChannel(newChannel)\n\n return () => {\n supabase.removeChannel(newChannel)\n }\n }, [roomName, username, supabase])\n\n const sendMessage = useCallback(\n async (content: string) => {\n if (!channel || !isConnected) return\n\n const message: ChatMessage = {\n id: crypto.randomUUID(),\n content,\n user: {\n name: username,\n },\n createdAt: new Date().toISOString(),\n }\n\n // Update local state immediately for the sender\n setMessages((current) => [...current, message])\n\n await channel.send({\n type: 'broadcast',\n event: EVENT_MESSAGE_TYPE,\n payload: message,\n })\n },\n [channel, isConnected, username]\n )\n\n return { messages, sendMessage, isConnected }\n}\n", "type": "registry:hook" }, { "path": "registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx", "content": "import { useCallback, useRef } from 'react'\n\nexport function useChatScroll() {\n const containerRef = useRef(null)\n\n const scrollToBottom = useCallback(() => {\n if (!containerRef.current) return\n\n const container = containerRef.current\n container.scrollTo({\n top: container.scrollHeight,\n behavior: 'smooth',\n })\n }, [])\n\n return { containerRef, scrollToBottom }\n}\n", "type": "registry:hook" }, { "path": "registry/default/clients/react-router/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_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`." }