|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const id = `room-${Math.random().toString(36).substring(2, 8)}`; |
|
|
setRoomId(id); |
|
|
|
|
|
|
|
|
if (!userName) { |
|
|
setUserName(`User${Math.floor(Math.random() * 10000)}`); |
|
|
} |
|
|
|
|
|
|
|
|
addSystemMessage(`Watch Party started for "${title}"`); |
|
|
}, [title]); |
|
|
|
|
|
|
|
|
const addSystemMessage = (text: string) => { |
|
|
const newMessage: Message = { |
|
|
id: `sys-${Date.now()}`, |
|
|
name: 'System', |
|
|
text, |
|
|
timestamp: Date.now(), |
|
|
type: 'system' |
|
|
}; |
|
|
setMessages(prev => [...prev, newMessage]); |
|
|
}; |
|
|
|
|
|
|
|
|
const addUserMessage = () => { |
|
|
if (!message.trim()) return; |
|
|
|
|
|
|
|
|
if (message.startsWith('/seek ')) { |
|
|
const seekTime = parseInt(message.replace('/seek ', '')); |
|
|
if (!isNaN(seekTime) && seekTime >= 0 && seekTime <= duration) { |
|
|
handleSeek(seekTime); |
|
|
setMessage(''); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const newMessage: Message = { |
|
|
id: `msg-${Date.now()}`, |
|
|
name: userName, |
|
|
text: message, |
|
|
timestamp: Date.now(), |
|
|
type: 'chat' |
|
|
}; |
|
|
|
|
|
setMessages(prev => [...prev, newMessage]); |
|
|
setMessage(''); |
|
|
}; |
|
|
|
|
|
|
|
|
const handleSeek = (time: number) => { |
|
|
if (onSeek) { |
|
|
onSeek(time); |
|
|
|
|
|
|
|
|
const newMessage: Message = { |
|
|
id: `time-${Date.now()}`, |
|
|
name: userName, |
|
|
text: `Seeked to ${formatTime(time)}`, |
|
|
timestamp: Date.now(), |
|
|
type: 'timestamp' |
|
|
}; |
|
|
setMessages(prev => [...prev, newMessage]); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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]); |
|
|
}; |
|
|
|
|
|
|
|
|
const formatTime = (timeInSeconds: number) => { |
|
|
const minutes = Math.floor(timeInSeconds / 60); |
|
|
const seconds = Math.floor(timeInSeconds % 60); |
|
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; |
|
|
}; |
|
|
|
|
|
|
|
|
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); |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (isOpen && isHost) { |
|
|
const timer = setTimeout(() => { |
|
|
setUserCount(2); |
|
|
addSystemMessage("Alice has joined the watch party"); |
|
|
}, 5000); |
|
|
|
|
|
return () => clearTimeout(timer); |
|
|
} |
|
|
}, [isOpen, isHost]); |
|
|
|
|
|
|
|
|
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> |
|
|
|
|
|
{} |
|
|
<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> |
|
|
|
|
|
{} |
|
|
<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> |
|
|
|
|
|
{} |
|
|
<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; |
|
|
|