'use client' import { ExampleLayoutProps } from '../example-layout' const instanceId = Math.random().toString(36).substring(2, 9) const appJsCode = `import { useEffect, useState, useRef } from 'react'; import './styles.css'; import { createClient } from '@supabase/supabase-js'; import { X } from 'lucide-react'; // Initialize Supabase client const supabaseUrl = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_URL}'; const supabaseKey = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_ANON_KEY}'; const supabase = createClient(supabaseUrl, supabaseKey); // Channel name - using a unique ID to ensure both instances connect to the same channel const CHANNEL = 'image-annotation-example-${instanceId}'; export default function App() { const [username, setUsername] = useState(''); const [isConnected, setIsConnected] = useState(false); const [activeUsers, setActiveUsers] = useState([]); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [commentPosition, setCommentPosition] = useState(null); const [imageLoaded, setImageLoaded] = useState(false); const userId = useRef(Math.random().toString(36).substring(2, 15)); const imageRef = useRef(null); const containerRef = useRef(null); const channelRef = useRef(null); // Specific Unsplash image URL const imageUrl = "https://images.unsplash.com/photo-1600695580162-cb0fa319067a?q=80&w=3764&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; // Initialize connection useEffect(() => { // Generate a random username const adjectives = ['Happy', 'Clever', 'Brave', 'Bright', 'Kind']; const nouns = ['Panda', 'Tiger', 'Eagle', 'Dolphin', 'Fox']; const randomName = \`\${adjectives[Math.floor(Math.random() * adjectives.length)]}\${ nouns[Math.floor(Math.random() * nouns.length)] }\${Math.floor(Math.random() * 100)}\`; setUsername(randomName); // Set up Supabase channel const channel = supabase.channel(CHANNEL); channelRef.current = channel; // Handle presence for user list channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); const presenceList = []; // Convert presence state to array Object.keys(state).forEach(key => { const presences = state[key]; presenceList.push(...presences); }); setActiveUsers(presenceList); }); // Handle new comments channel.on('broadcast', { event: 'new_comment' }, (payload) => { const newComment = payload.payload; setComments(prevComments => { // Check if comment already exists (avoid duplicates) const exists = prevComments.some(comment => comment.id === newComment.id); if (exists) return prevComments; return [...prevComments, newComment]; }); }); // Handle comment deletion channel.on('broadcast', { event: 'delete_comment' }, (payload) => { const { commentId } = payload.payload; setComments(prevComments => prevComments.filter(comment => comment.id !== commentId) ); }); // Subscribe to channel channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { // Track presence await channel.track({ user_id: userId.current, username: randomName, online_at: new Date().getTime(), }); setIsConnected(true); } }); // Clean up return () => { if (channel) { channel.unsubscribe(); } }; }, []); // Handle image load const handleImageLoad = () => { setImageLoaded(true); }; // Handle click on the image const handleImageClick = (e) => { if (!isConnected || !imageRef.current) return; // Don't process clicks on existing comments or the input form if ( e.target.closest('[data-comment]') || e.target.closest('[data-comment-bubble]') || e.target.closest('[data-comment-input]') ) { return; } // Calculate relative position (%) const rect = imageRef.current.getBoundingClientRect(); const xPercent = ((e.clientX - rect.left) / rect.width) * 100; const yPercent = ((e.clientY - rect.top) / rect.height) * 100; // Set position for new comment setCommentPosition({ x: xPercent, y: yPercent }); // Focus on input (will be created in render) setTimeout(() => { const input = document.getElementById('new-comment-input'); if (input) input.focus(); }, 10); }; // Handle comment submission const handleCommentSubmit = (e) => { e.preventDefault(); if (!newComment.trim() || !commentPosition || !isConnected) { // Reset state if invalid setCommentPosition(null); setNewComment(''); return; } // Create new comment const comment = { id: \`\${userId.current}-\${Date.now()}\`, text: newComment.trim(), position: commentPosition, user_id: userId.current, username, created_at: Date.now(), }; // Add to local state setComments(prevComments => [...prevComments, comment]); // Broadcast to other clients channelRef.current.send({ type: 'broadcast', event: 'new_comment', payload: comment, }); // Reset state setCommentPosition(null); setNewComment(''); }; // Handle comment deletion const handleDeleteComment = (commentId) => { // Remove from local state setComments(prevComments => prevComments.filter(comment => comment.id !== commentId) ); // Broadcast deletion channelRef.current.send({ type: 'broadcast', event: 'delete_comment', payload: { commentId }, }); }; // Cancel new comment const handleCancelComment = () => { setCommentPosition(null); setNewComment(''); }; // Handle input change const handleInputChange = (e) => { setNewComment(e.target.value); }; // Handle keydown in input const handleInputKeyDown = (e) => { if (e.key === 'Escape') { handleCancelComment(); } }; return (
Loading image...