Spaces:
Sleeping
Sleeping
| 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; | |