diff --git a/app/(product)/components/globalchat.tsx b/app/(product)/components/globalchat.tsx index 334f81d..e50906e 100644 --- a/app/(product)/components/globalchat.tsx +++ b/app/(product)/components/globalchat.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; -import { Send, Trash2 } from "lucide-react"; +import { Send, Trash2, Smile, MoreHorizontal, Users, Clock } from "lucide-react"; import { useSession } from "next-auth/react"; type GlobalMessage = { @@ -12,6 +12,7 @@ type GlobalMessage = { created_at: string; deleted?: boolean; deleted_at?: string; + reactions?: { [emoji: string]: string[] }; // emoji -> array of user_ids }; export default function GlobalChat() { @@ -24,8 +25,13 @@ export default function GlobalChat() { const [isLoading, setIsLoading] = useState(true); const [deletingIds, setDeletingIds] = useState>(new Set()); const [deleteConfirmId, setDeleteConfirmId] = useState(null); + const [onlineUsers] = useState(0); + const [isTyping, setIsTyping] = useState>(new Set()); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const [showScrollButton, setShowScrollButton] = useState(false); const bottomRef = useRef(null); const inputRef = useRef(null); + const typingTimeoutRef = useRef(null); const load = useMemo( () => @@ -63,9 +69,34 @@ export default function GlobalChat() { }, [load]); useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + // Smooth scroll to bottom when new messages arrive + const scrollToBottom = () => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + // Use requestAnimationFrame to ensure DOM is updated + requestAnimationFrame(scrollToBottom); }, [messages.length]); + // Auto-scroll to bottom when user is near the bottom + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; + + // Show/hide scroll to bottom button + setShowScrollButton(scrollTop > 200); + + if (isNearBottom) { + setTimeout(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }; + + const scrollToBottom = () => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + const send = async () => { const content = text.trim(); if (!content || isSending) return; @@ -99,6 +130,30 @@ export default function GlobalChat() { } }; + const handleInputChange = (e: React.ChangeEvent) => { + setText(e.target.value); + + // Clear existing timeout + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + // Set typing indicator + if (e.target.value.trim() && currentUserId) { + // In a real app, you'd emit typing events here + // For now, we'll simulate it + } + + // Clear typing indicator after 3 seconds of inactivity + typingTimeoutRef.current = setTimeout(() => { + setIsTyping(prev => { + const newSet = new Set(prev); + newSet.delete(currentUserId || ''); + return newSet; + }); + }, 3000); + }; + const deleteMessage = async (messageId: string) => { if (deletingIds.has(messageId)) return; @@ -126,26 +181,112 @@ export default function GlobalChat() { const formatTime = (dateString: string) => { const date = new Date(dateString); const now = new Date(); - const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); + const diffInMinutes = (now.getTime() - date.getTime()) / (1000 * 60); + const diffInHours = diffInMinutes / 60; + const diffInDays = diffInHours / 24; - if (diffInHours < 24) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + if (diffInMinutes < 1) { + return 'Just now'; + } else if (diffInMinutes < 60) { + return `${Math.floor(diffInMinutes)}m ago`; + } else if (diffInHours < 24) { + return `${Math.floor(diffInHours)}h ago`; + } else if (diffInDays < 7) { + return `${Math.floor(diffInDays)}d ago`; + } else { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); } - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + }; + + const addEmoji = (emoji: string) => { + setText(prev => prev + emoji); + setShowEmojiPicker(false); + inputRef.current?.focus(); + }; + + const commonEmojis = ['😀', '😂', '❤️', '👍', '👎', '🎉', '🔥', '💯', '🤔', '😮']; + + const addReaction = async (messageId: string, emoji: string) => { + if (!currentUserId) return; + + try { + const res = await fetch(`/api/global-chat/${messageId}/reaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ emoji }), + }); + + if (res.ok) { + await load(); + } + } catch (e) { + console.error('Failed to add reaction:', e); + } + }; + + const hasUserReacted = (message: GlobalMessage, emoji: string) => { + return message.reactions?.[emoji]?.includes(currentUserId || '') || false; }; return ( -
- {/* Header */} -
-

- Global Chat -

-

- {messages.length} {messages.length === 1 ? 'message' : 'messages'} -

+
+ {/* Enhanced Header */} +
+
+
+
+ # +
+
+

+ Global Chat +

+
+ + + {onlineUsers} online + {onlineUsers} + + + + {messages.length} {messages.length === 1 ? 'message' : 'messages'} + {messages.length} + +
+
+
+
+ + +
+
+ {/* Emoji Picker */} + {showEmojiPicker && ( +
+
+ {commonEmojis.map((emoji, index) => ( + + ))} +
+
+ )} + {/* Error Message */} {error && (
@@ -190,7 +331,10 @@ export default function GlobalChat() { )} {/* Messages Area */} -
+
{isLoading ? (
@@ -202,10 +346,28 @@ export default function GlobalChat() {
) : messages.length === 0 ? (
-
-

- No messages yet. Start the conversation! +

+
+ 💬 +
+

+ Welcome to Global Chat! +

+

+ This is a space for everyone to connect, share ideas, and have meaningful conversations. + Be respectful and kind to others.

+
+ + 💡 Share ideas + + + 🤝 Connect + + + 🎉 Have fun + +
) : ( @@ -238,44 +400,111 @@ export default function GlobalChat() {
)} -
-
-
- +
+
+
+ {(m.user_name || m.user_id).charAt(0).toUpperCase()}
-
- +
+ {m.user_name || m.user_id} {formatTime(m.created_at)} + {isOwnMessage && ( +
+ + You + + +
+ )}
{m.deleted ? ( -

- This message was deleted -

+
+

+ This message was deleted +

+
) : ( -

- {m.content} -

+
+

+ {m.content} +

+ + {/* Message Reactions */} + {m.reactions && Object.keys(m.reactions).length > 0 && ( +
+ {Object.entries(m.reactions).map(([emoji, userIds]) => ( + + ))} +
+ )} +
)}
- {!m.deleted && isOwnMessage && ( + {!m.deleted && (
- +
+ {/* Quick Reaction Buttons */} +
+ {['👍', '❤️', '😂', '🎉'].map((emoji) => ( + + ))} +
+ + {/* Delete Button (only for own messages) */} + {isOwnMessage && ( + + )} +
)}
@@ -285,31 +514,89 @@ export default function GlobalChat() {
)} + + {/* Typing Indicators */} + {isTyping.size > 0 && ( +
+
+
+
+
+
+ + {Array.from(isTyping).length === 1 + ? 'Someone is typing...' + : `${Array.from(isTyping).length} people are typing...` + } + +
+ )} + + {/* Scroll to Bottom Button */} + {showScrollButton && ( + + )}
- {/* Input Area */} -
-
- setText(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Type a message..." - disabled={isSending} - className="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 dark:focus:ring-green-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" - /> + {/* Enhanced Input Area */} +
+
+
+ + +
+
+
+ Press Enter to send, Shift+Enter for new line + Enter to send + {text.length > 0 && ( + + {text.length} characters + + )} +
+
+ 💬 + Global Chat +
+
);