Spaces:
Running
Running
import React, { useState, useEffect } from 'react'; | |
import { Button } from '@/components/ui/button'; | |
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; | |
import { Input } from '@/components/ui/input'; | |
import { Users, Link, Copy, CheckCircle, Send } from 'lucide-react'; | |
import { useToast } from '@/hooks/use-toast'; | |
interface WatchTogetherProps { | |
title: string; | |
currentTime: number; | |
duration: number; | |
onSeek?: (time: number) => void; | |
} | |
interface Message { | |
id: string; | |
name: string; | |
text: string; | |
timestamp: number; | |
type: 'chat' | 'system' | 'timestamp'; | |
} | |
const WatchTogether: React.FC<WatchTogetherProps> = ({ title, currentTime, duration, onSeek }) => { | |
const [isOpen, setIsOpen] = useState(false); | |
const [roomId, setRoomId] = useState<string>(''); | |
const [userName, setUserName] = useState<string>(''); | |
const [message, setMessage] = useState<string>(''); | |
const [messages, setMessages] = useState<Message[]>([]); | |
const [userCount, setUserCount] = useState(1); | |
const [isHost, setIsHost] = useState(true); | |
const [linkCopied, setLinkCopied] = useState(false); | |
const { toast } = useToast(); | |
// Generate room ID on mount | |
useEffect(() => { | |
const id = `room-${Math.random().toString(36).substring(2, 8)}`; | |
setRoomId(id); | |
// If no username set, use a default | |
if (!userName) { | |
setUserName(`User${Math.floor(Math.random() * 10000)}`); | |
} | |
// Initial system message | |
addSystemMessage(`Watch Party started for "${title}"`); | |
}, [title]); | |
// Function to add system message | |
const addSystemMessage = (text: string) => { | |
const newMessage: Message = { | |
id: `sys-${Date.now()}`, | |
name: 'System', | |
text, | |
timestamp: Date.now(), | |
type: 'system' | |
}; | |
setMessages(prev => [...prev, newMessage]); | |
}; | |
// Function to add user message | |
const addUserMessage = () => { | |
if (!message.trim()) return; | |
// If message starts with '/seek ', treat as seek command | |
if (message.startsWith('/seek ')) { | |
const seekTime = parseInt(message.replace('/seek ', '')); | |
if (!isNaN(seekTime) && seekTime >= 0 && seekTime <= duration) { | |
handleSeek(seekTime); | |
setMessage(''); | |
return; | |
} | |
} | |
// Regular message | |
const newMessage: Message = { | |
id: `msg-${Date.now()}`, | |
name: userName, | |
text: message, | |
timestamp: Date.now(), | |
type: 'chat' | |
}; | |
setMessages(prev => [...prev, newMessage]); | |
setMessage(''); | |
}; | |
// Function to handle seeking | |
const handleSeek = (time: number) => { | |
if (onSeek) { | |
onSeek(time); | |
// Add timestamp message | |
const newMessage: Message = { | |
id: `time-${Date.now()}`, | |
name: userName, | |
text: `Seeked to ${formatTime(time)}`, | |
timestamp: Date.now(), | |
type: 'timestamp' | |
}; | |
setMessages(prev => [...prev, newMessage]); | |
} | |
}; | |
// Function to share current timestamp | |
const shareCurrentTime = () => { | |
const newMessage: Message = { | |
id: `time-${Date.now()}`, | |
name: userName, | |
text: `Current position: ${formatTime(currentTime)}`, | |
timestamp: Date.now(), | |
type: 'timestamp' | |
}; | |
setMessages(prev => [...prev, newMessage]); | |
}; | |
// Function to format time | |
const formatTime = (timeInSeconds: number) => { | |
const minutes = Math.floor(timeInSeconds / 60); | |
const seconds = Math.floor(timeInSeconds % 60); | |
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; | |
}; | |
// Function to copy invite link | |
const copyInviteLink = () => { | |
const inviteLink = `${window.location.href}?room=${roomId}&host=false`; | |
navigator.clipboard.writeText(inviteLink); | |
setLinkCopied(true); | |
toast({ | |
title: "Link Copied", | |
description: "Share this link with friends to watch together", | |
}); | |
setTimeout(() => setLinkCopied(false), 2000); | |
}; | |
// Simulate someone joining after a delay | |
useEffect(() => { | |
if (isOpen && isHost) { | |
const timer = setTimeout(() => { | |
setUserCount(2); | |
addSystemMessage("Alice has joined the watch party"); | |
}, 5000); | |
return () => clearTimeout(timer); | |
} | |
}, [isOpen, isHost]); | |
// Add mock message after some delays | |
useEffect(() => { | |
if (isOpen && userCount > 1) { | |
const timer1 = setTimeout(() => { | |
setMessages(prev => [ | |
...prev, | |
{ | |
id: `msg-alice-1`, | |
name: "Alice", | |
text: "Hey, thanks for inviting me!", | |
timestamp: Date.now(), | |
type: 'chat' | |
} | |
]); | |
}, 3000); | |
const timer2 = setTimeout(() => { | |
setMessages(prev => [ | |
...prev, | |
{ | |
id: `msg-alice-2`, | |
name: "Alice", | |
text: "I love this part coming up!", | |
timestamp: Date.now(), | |
type: 'chat' | |
} | |
]); | |
}, 15000); | |
return () => { | |
clearTimeout(timer1); | |
clearTimeout(timer2); | |
}; | |
} | |
}, [isOpen, userCount]); | |
return ( | |
<Dialog open={isOpen} onOpenChange={setIsOpen}> | |
<DialogTrigger asChild> | |
<Button | |
variant="outline" | |
size="sm" | |
className="fixed top-4 right-36 z-50 bg-gray-800/80 hover:bg-gray-700/80 text-white border-gray-600" | |
onClick={() => setIsOpen(true)} | |
> | |
<Users className="mr-2 h-4 w-4" /> | |
Watch Together | |
</Button> | |
</DialogTrigger> | |
<DialogContent className="sm:max-w-[425px] bg-gray-900 text-white border-gray-700"> | |
<DialogHeader> | |
<DialogTitle>Watch Together</DialogTitle> | |
</DialogHeader> | |
<div className="flex items-center justify-between py-2 px-4 bg-gray-800 rounded-lg"> | |
<div className="flex items-center"> | |
<Users className="h-5 w-5 mr-2 text-theme-primary" /> | |
<span>{userCount} {userCount === 1 ? 'viewer' : 'viewers'}</span> | |
</div> | |
<div className="flex items-center space-x-2"> | |
<Link className="h-4 w-4 text-gray-400" /> | |
<button | |
onClick={copyInviteLink} | |
className="text-sm text-theme-primary hover:text-theme-primary-light flex items-center" | |
> | |
{linkCopied ? ( | |
<><CheckCircle className="h-4 w-4 mr-1" /> Copied</> | |
) : ( | |
<><Copy className="h-4 w-4 mr-1" /> Copy Invite Link</> | |
)} | |
</button> | |
</div> | |
</div> | |
{/* Chat messages */} | |
<div className="flex flex-col space-y-4 h-[250px] overflow-y-auto py-2 px-1"> | |
{messages.map((msg) => ( | |
<div | |
key={msg.id} | |
className={`flex flex-col ${msg.name === userName ? 'items-end' : 'items-start'}`} | |
> | |
{msg.type === 'system' ? ( | |
<div className="bg-gray-800/50 text-gray-300 py-1 px-3 rounded-md text-xs w-full text-center"> | |
{msg.text} | |
</div> | |
) : msg.type === 'timestamp' ? ( | |
<div | |
className={`bg-theme-primary/20 text-theme-primary py-1 px-3 rounded-md text-xs cursor-pointer hover:bg-theme-primary/30 ${ | |
msg.name === userName ? 'self-end' : 'self-start' | |
}`} | |
onClick={() => { | |
const timeMatch = msg.text.match(/(\d+):(\d+)/); | |
if (timeMatch) { | |
const minutes = parseInt(timeMatch[1]); | |
const seconds = parseInt(timeMatch[2]); | |
const totalSeconds = minutes * 60 + seconds; | |
onSeek?.(totalSeconds); | |
} | |
}} | |
> | |
{msg.text} | |
</div> | |
) : ( | |
<> | |
<span className="text-xs text-gray-400 mb-1"> | |
{msg.name === userName ? 'You' : msg.name} | |
</span> | |
<div | |
className={`py-2 px-3 rounded-lg max-w-[80%] ${ | |
msg.name === userName | |
? 'bg-theme-primary text-white' | |
: 'bg-gray-800 text-gray-200' | |
}`} | |
> | |
<p className="text-sm">{msg.text}</p> | |
</div> | |
</> | |
)} | |
</div> | |
))} | |
</div> | |
{/* Share current timestamp button */} | |
<button | |
onClick={shareCurrentTime} | |
className="text-sm text-theme-primary hover:text-theme-primary-light flex items-center self-center" | |
> | |
Share current timestamp ({formatTime(currentTime)}) | |
</button> | |
{/* Chat input */} | |
<div className="flex space-x-2 mt-2"> | |
<Input | |
placeholder="Type a message..." | |
value={message} | |
onChange={(e) => setMessage(e.target.value)} | |
className="bg-gray-800 border-gray-700 text-white" | |
onKeyDown={(e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
addUserMessage(); | |
} | |
}} | |
/> | |
<Button | |
size="icon" | |
onClick={addUserMessage} | |
className="bg-theme-primary hover:bg-theme-primary-hover" | |
> | |
<Send className="h-4 w-4" /> | |
</Button> | |
</div> | |
<p className="text-xs text-gray-400 mt-2"> | |
Pro tip: Type '/seek 10' to jump to 10 seconds, or click on any shared timestamp to seek. | |
</p> | |
</DialogContent> | |
</Dialog> | |
); | |
}; | |
export default WatchTogether; | |