|
|
| 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; |
|
|