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
This commit is contained in:
Tiago Yamamoto 2025-12-26 13:15:29 -03:00
parent bf41570cae
commit 7d797aac2b

View file

@ -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<Message[]>([])
const [messageText, setMessageText] = useState("")
const [loading, setLoading] = useState(true)
const [serviceConfigured, setServiceConfigured] = useState(true)
const [error, setError] = useState<string | null>(null)
const processedMessageIds = useRef(new Set<string>())
// 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 (
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Messages</h1>
<p className="text-muted-foreground mt-1">Communicate with candidates and companies</p>
</div>
{/* Not Configured Card */}
<Card className="p-8">
<div className="flex flex-col items-center justify-center text-center space-y-4">
<div className="p-4 bg-muted rounded-full">
<MessageSquareOff className="h-12 w-12 text-muted-foreground" />
</div>
<div className="space-y-2">
<h2 className="text-xl font-semibold">Serviço de Mensagens não Configurado</h2>
<p className="text-muted-foreground max-w-md">
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.
</p>
</div>
<div className="flex gap-4 mt-4">
<Link href="/dashboard">
<Button variant="outline">Voltar ao Dashboard</Button>
</Link>
<Link href="/dashboard/settings">
<Button>
<Settings className="h-4 w-4 mr-2" />
Configurações
</Button>
</Link>
</div>
{/* Technical Details (collapsed by default) */}
<details className="mt-6 text-left w-full max-w-md">
<summary className="text-sm text-muted-foreground cursor-pointer hover:text-foreground">
Detalhes técnicos
</summary>
<div className="mt-2 p-4 bg-muted rounded-lg text-sm font-mono">
<p>Variáveis necessárias:</p>
<ul className="list-disc list-inside mt-2 space-y-1 text-muted-foreground">
<li>NEXT_PUBLIC_APPWRITE_ENDPOINT</li>
<li>NEXT_PUBLIC_APPWRITE_PROJECT_ID</li>
<li>NEXT_PUBLIC_APPWRITE_DATABASE_ID</li>
<li>NEXT_PUBLIC_APPWRITE_COLLECTION_ID</li>
</ul>
</div>
</details>
</div>
</Card>
</div>
)
}
return (
<div className="space-y-8">
{/* Header */}
@ -152,7 +234,7 @@ export default function AdminMessagesPage() {
<p className="text-muted-foreground mt-1">Communicate with candidates and companies</p>
</div>
{/* Stats (Calculated from conversations for now, or fetch API stats later) */}
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
@ -168,7 +250,6 @@ export default function AdminMessagesPage() {
</CardTitle>
</CardHeader>
</Card>
{/* Placeholders */}
<Card>
<CardHeader className="pb-3">
<CardDescription>Replied today</CardDescription>
@ -202,33 +283,34 @@ export default function AdminMessagesPage() {
<ScrollArea className="h-[calc(600px-80px)]">
<div className="p-2">
{loading ? <p className="p-4 text-center text-muted-foreground">Loading...</p> :
filteredConversations.length === 0 ? <p className="p-4 text-center text-muted-foreground">No conversations found</p> :
filteredConversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => setSelectedConversation(conversation)}
className={`w-full flex items-start gap-3 p-3 rounded-lg hover:bg-muted transition-colors ${selectedConversation?.id === conversation.id ? "bg-muted" : ""
}`}
>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate">{conversation.participantName || "Unknown User"}</span>
<span className="text-xs text-muted-foreground">{formatTime(conversation.lastMessageAt)}</span>
error ? <p className="p-4 text-center text-destructive">{error}</p> :
filteredConversations.length === 0 ? <p className="p-4 text-center text-muted-foreground">No conversations found</p> :
filteredConversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => setSelectedConversation(conversation)}
className={`w-full flex items-start gap-3 p-3 rounded-lg hover:bg-muted transition-colors ${selectedConversation?.id === conversation.id ? "bg-muted" : ""
}`}
>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate">{conversation.participantName || "Unknown User"}</span>
<span className="text-xs text-muted-foreground">{formatTime(conversation.lastMessageAt)}</span>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground truncate">{conversation.lastMessage || "No messages yet"}</p>
{(conversation.unreadCount || 0) > 0 && (
<Badge
variant="default"
className="ml-2 h-5 w-5 p-0 flex items-center justify-center rounded-full"
>
{conversation.unreadCount}
</Badge>
)}
</div>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground truncate">{conversation.lastMessage || "No messages yet"}</p>
{(conversation.unreadCount || 0) > 0 && (
<Badge
variant="default"
className="ml-2 h-5 w-5 p-0 flex items-center justify-center rounded-full"
>
{conversation.unreadCount}
</Badge>
)}
</div>
</div>
</button>
))}
</button>
))}
</div>
</ScrollArea>
</div>