web / frontend /src /components /WatchTogether.tsx
Chandima Prabhath
Track bun.lockb with Git LFS
cc2caf9
raw
history blame
9.9 kB
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;