From 7d797aac2b770c9ee8d8664d47a925fed50815c0 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 13:15:29 -0300 Subject: [PATCH] fix: Add graceful handling for unconfigured Appwrite in messages page When Appwrite is not configured: - Shows friendly 'Service not configured' message - Displays icon and helpful description - Links to dashboard and settings - Shows technical details in collapsible section - Prevents client-side crash --- frontend/src/app/dashboard/messages/page.tsx | 214 +++++++++++++------ 1 file changed, 148 insertions(+), 66 deletions(-) diff --git a/frontend/src/app/dashboard/messages/page.tsx b/frontend/src/app/dashboard/messages/page.tsx index c292a07..ddd94b6 100644 --- a/frontend/src/app/dashboard/messages/page.tsx +++ b/frontend/src/app/dashboard/messages/page.tsx @@ -7,11 +7,19 @@ import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/ca import { Badge } from "@/components/ui/badge" import { Textarea } from "@/components/ui/textarea" import { ScrollArea } from "@/components/ui/scroll-area" -import { Search, Send, Paperclip } from "lucide-react" +import { Search, Send, Paperclip, MessageSquareOff, Settings } from "lucide-react" import { chatApi, Conversation, Message } from "@/lib/api" import { appwriteClient, APPWRITE_CONFIG } from "@/lib/appwrite" import { toast } from "sonner" import { formatDistanceToNow } from "date-fns" +import Link from "next/link" + +// Check if Appwrite is properly configured +const isAppwriteConfigured = () => { + const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID + const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID + return !!(projectId && projectId.trim() !== '' && databaseId && databaseId.trim() !== '') +} export default function AdminMessagesPage() { const [searchTerm, setSearchTerm] = useState("") @@ -20,11 +28,26 @@ export default function AdminMessagesPage() { const [messages, setMessages] = useState([]) const [messageText, setMessageText] = useState("") const [loading, setLoading] = useState(true) + const [serviceConfigured, setServiceConfigured] = useState(true) + const [error, setError] = useState(null) const processedMessageIds = useRef(new Set()) + // Check configuration on mount + useEffect(() => { + setServiceConfigured(isAppwriteConfigured()) + if (!isAppwriteConfigured()) { + setLoading(false) + } + }, []) + // Fetch Conversations const fetchConversations = async () => { + if (!serviceConfigured) { + setLoading(false) + return + } + try { const data = await chatApi.listConversations() setConversations(data) @@ -32,20 +55,28 @@ export default function AdminMessagesPage() { setSelectedConversation(data[0]) } setLoading(false) - } catch (error) { + setError(null) + } catch (error: any) { console.error("Failed to load conversations", error) - toast.error("Failed to load conversations") + // Check if it's a configuration error + if (error?.message?.includes('Project ID') || error?.code === 400) { + setServiceConfigured(false) + } else { + setError("Falha ao carregar conversas. Tente novamente.") + } setLoading(false) } } useEffect(() => { - fetchConversations() - }, []) + if (serviceConfigured) { + fetchConversations() + } + }, [serviceConfigured]) // Fetch Messages when conversation changes useEffect(() => { - if (!selectedConversation) return + if (!selectedConversation || !serviceConfigured) return setMessages([]) processedMessageIds.current.clear() @@ -65,43 +96,38 @@ export default function AdminMessagesPage() { // Appwrite Realtime Subscription // Only subscribe if config is present - if (APPWRITE_CONFIG.databaseId && APPWRITE_CONFIG.collectionId) { - const channel = `databases.${APPWRITE_CONFIG.databaseId}.collections.${APPWRITE_CONFIG.collectionId}.documents` - const unsubscribe = appwriteClient.subscribe(channel, (response) => { - if (response.events.includes("databases.*.collections.*.documents.*.create")) { - const payload = response.payload as any + if (APPWRITE_CONFIG.databaseId && APPWRITE_CONFIG.collectionId && serviceConfigured) { + try { + const channel = `databases.${APPWRITE_CONFIG.databaseId}.collections.${APPWRITE_CONFIG.collectionId}.documents` + const unsubscribe = appwriteClient.subscribe(channel, (response) => { + if (response.events.includes("databases.*.collections.*.documents.*.create")) { + const payload = response.payload as any - // Check if belongs to current conversation - if (payload.conversation_id === selectedConversation.id) { - // Check if we already have it (deduplication) - // The Payload ID is likely the Appwrite Document ID, which usually matches our Message ID if we set it. - // If backend sets ID, it matches. - const msgId = payload.$id - if (processedMessageIds.current.has(msgId)) return + // Check if belongs to current conversation + if (payload.conversation_id === selectedConversation.id) { + const msgId = payload.$id + if (processedMessageIds.current.has(msgId)) return - // We don't know "isMine" here easily without user ID. - // But if WE sent it, we likely added it optimistically or via API response which adds to processedIds. - // So assume incoming realtime events are from OTHERS? - // Not necessarily, Realtime echoes back my own messages too. - // Since I adding to processedIds on Send, I should filter my own echoes. + const newMessage: Message = { + id: msgId, + conversationId: payload.conversation_id, + senderId: payload.sender_id, + content: payload.content, + createdAt: payload.timestamp, + isMine: false + } - const newMessage: Message = { - id: msgId, - conversationId: payload.conversation_id, - senderId: payload.sender_id, - content: payload.content, - createdAt: payload.timestamp, - isMine: false // Default to false, assuming 'mine' are handled by UI state update on send + setMessages(prev => [...prev, newMessage]) + processedMessageIds.current.add(msgId) } - - setMessages(prev => [...prev, newMessage]) - processedMessageIds.current.add(msgId) } - } - }) - return () => unsubscribe() + }) + return () => unsubscribe() + } catch (error) { + console.error("Failed to subscribe to realtime", error) + } } - }, [selectedConversation]) + }, [selectedConversation, serviceConfigured]) const filteredConversations = conversations.filter((conv) => (conv.participantName || "Unknown").toLowerCase().includes(searchTerm.toLowerCase()), @@ -118,7 +144,7 @@ export default function AdminMessagesPage() { // Update messages list setMessages(prev => [...prev, newMsg]) - processedMessageIds.current.add(newMsg.id) // Mark as processed to ignore realtime echo if ID matches + processedMessageIds.current.add(newMsg.id) // Update conversation last message setConversations(prev => prev.map(c => @@ -144,6 +170,62 @@ export default function AdminMessagesPage() { } } + // Service not configured - show friendly message + if (!serviceConfigured) { + return ( +
+ {/* Header */} +
+

Messages

+

Communicate with candidates and companies

+
+ + {/* Not Configured Card */} + +
+
+ +
+
+

Serviço de Mensagens não Configurado

+

+ O serviço de mensagens em tempo real (Appwrite) ainda não foi configurado para este ambiente. + Entre em contato com o administrador do sistema para ativar esta funcionalidade. +

+
+
+ + + + + + +
+ + {/* Technical Details (collapsed by default) */} +
+ + Detalhes técnicos + +
+

Variáveis necessárias:

+
    +
  • NEXT_PUBLIC_APPWRITE_ENDPOINT
  • +
  • NEXT_PUBLIC_APPWRITE_PROJECT_ID
  • +
  • NEXT_PUBLIC_APPWRITE_DATABASE_ID
  • +
  • NEXT_PUBLIC_APPWRITE_COLLECTION_ID
  • +
+
+
+
+
+
+ ) + } + return (
{/* Header */} @@ -152,7 +234,7 @@ export default function AdminMessagesPage() {

Communicate with candidates and companies

- {/* Stats (Calculated from conversations for now, or fetch API stats later) */} + {/* Stats */}
@@ -168,7 +250,6 @@ export default function AdminMessagesPage() { - {/* Placeholders */} Replied today @@ -202,33 +283,34 @@ export default function AdminMessagesPage() {
{loading ?

Loading...

: - filteredConversations.length === 0 ?

No conversations found

: - filteredConversations.map((conversation) => ( -
- - ))} + + ))}