import { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { supabase } from '@/config'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { PaginationControls } from '@/components/ui/pagination'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@/components/ui/tabs'; import { formatDistanceToNow, format } from 'date-fns'; import { toast } from 'sonner'; import { Activity, ThumbsUp, ThumbsDown, Minus, Heart, MessageCircle, UserPlus, ExternalLink, } from 'lucide-react'; // Types interface ResponseActivity { id: string; user_id: string; answer: 'yes' | 'no' | 'maybe'; created_at: string; profile: { id: string; name: string | null; email: string | null; } | null; question: { id: string; text: string; pack: { id: string; name: string; } | null; } | null; } interface MatchActivity { id: string; couple_id: string; match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe'; is_new: boolean; created_at: string; question: { id: string; text: string; } | null; coupleProfiles?: Array<{ id: string; name: string | null; email: string | null; }>; } interface MessageActivity { id: string; user_id: string; content: string | null; media_path: string | null; media_type: 'image' | 'video' | null; created_at: string; profile: { id: string; name: string | null; email: string | null; } | null; match: { id: string; question: { text: string; } | null; } | null; } interface SignupActivity { id: string; name: string | null; email: string | null; avatar_url: string | null; created_at: string; onboarding_completed: boolean | null; couple_id: string | null; } type ActivityTab = 'responses' | 'matches' | 'messages' | 'signups'; // Answer icons const answerConfig = { yes: { icon: , color: 'text-green-500', bg: 'bg-green-500/10' }, no: { icon: , color: 'text-red-500', bg: 'bg-red-500/10' }, maybe: { icon: , color: 'text-yellow-500', bg: 'bg-yellow-500/10' }, }; const matchTypeLabels = { yes_yes: { label: 'Both Yes!', variant: 'default' as const }, yes_maybe: { label: 'Yes + Maybe', variant: 'secondary' as const }, maybe_maybe: { label: 'Both Maybe', variant: 'outline' as const }, }; export function UserActivityPage() { const [activeTab, setActiveTab] = useState('responses'); const [responses, setResponses] = useState([]); const [matches, setMatches] = useState([]); const [messages, setMessages] = useState([]); const [signups, setSignups] = useState([]); const [responsesPage, setResponsesPage] = useState(1); const [responsesPageSize, setResponsesPageSize] = useState(25); const [responsesTotal, setResponsesTotal] = useState(0); const [matchesPage, setMatchesPage] = useState(1); const [matchesPageSize, setMatchesPageSize] = useState(25); const [matchesTotal, setMatchesTotal] = useState(0); const [messagesPage, setMessagesPage] = useState(1); const [messagesPageSize, setMessagesPageSize] = useState(25); const [messagesTotal, setMessagesTotal] = useState(0); const [signupsPage, setSignupsPage] = useState(1); const [signupsPageSize, setSignupsPageSize] = useState(25); const [signupsTotal, setSignupsTotal] = useState(0); const [loading, setLoading] = useState({ responses: true, matches: true, messages: true, signups: true, }); // Fetch responses const fetchResponses = useCallback(async () => { setLoading(prev => ({ ...prev, responses: true })); try { const from = (responsesPage - 1) * responsesPageSize; const to = from + responsesPageSize - 1; const { data, error, count } = await supabase .from('responses') .select(` id, user_id, answer, created_at, profile:profiles!responses_user_id_fkey(id, name, email), question:questions( id, text, pack:question_packs(id, name) ) `, { count: 'exact' }) .order('created_at', { ascending: false }) .range(from, to); if (error) throw error; setResponsesTotal(count || 0); // Transform data to match our types (Supabase returns nested objects, not arrays for 1:1 relations) const transformed: ResponseActivity[] = (data || []).map((item: any) => ({ id: item.id, user_id: item.user_id, answer: item.answer, created_at: item.created_at, profile: item.profile, question: item.question ? { id: item.question.id, text: item.question.text, pack: item.question.pack, } : null, })); setResponses(transformed); } catch (error) { console.error('Failed to load responses:', error); toast.error("Failed to load responses"); } finally { setLoading(prev => ({ ...prev, responses: false })); } }, [responsesPage, responsesPageSize]); // Fetch matches with couple profiles const fetchMatches = useCallback(async () => { setLoading(prev => ({ ...prev, matches: true })); try { const from = (matchesPage - 1) * matchesPageSize; const to = from + matchesPageSize - 1; const { data: matchesData, error: matchesError, count } = await supabase .from('matches') .select(` id, couple_id, match_type, is_new, created_at, question:questions(id, text) `, { count: 'exact' }) .order('created_at', { ascending: false }) .range(from, to); if (matchesError) throw matchesError; setMatchesTotal(count || 0); // Get unique couple IDs and fetch profiles const coupleIds = [...new Set((matchesData || []).map((m: any) => m.couple_id))]; if (coupleIds.length > 0) { const { data: profiles, error: profilesError } = await supabase .from('profiles') .select('id, name, email, couple_id') .in('couple_id', coupleIds); if (profilesError) throw profilesError; // Attach profiles to matches const matchesWithProfiles: MatchActivity[] = (matchesData || []).map((match: any) => ({ id: match.id, couple_id: match.couple_id, match_type: match.match_type, is_new: match.is_new, created_at: match.created_at, question: match.question, coupleProfiles: (profiles || []).filter((p: any) => p.couple_id === match.couple_id), })); setMatches(matchesWithProfiles); } else { const transformed: MatchActivity[] = (matchesData || []).map((match: any) => ({ id: match.id, couple_id: match.couple_id, match_type: match.match_type, is_new: match.is_new, created_at: match.created_at, question: match.question, coupleProfiles: [], })); setMatches(transformed); } } catch (error) { console.error('Failed to load matches:', error); toast.error("Failed to load matches"); } finally { setLoading(prev => ({ ...prev, matches: false })); } }, [matchesPage, matchesPageSize]); // Fetch messages const fetchMessages = useCallback(async () => { setLoading(prev => ({ ...prev, messages: true })); try { const from = (messagesPage - 1) * messagesPageSize; const to = from + messagesPageSize - 1; // Fetch messages with match data const { data: messagesData, error: messagesError, count } = await supabase .from('messages') .select(` id, user_id, content, media_path, media_type, created_at, match:matches( id, question:questions(text) ) `, { count: 'exact' }) .order('created_at', { ascending: false }) .range(from, to); if (messagesError) throw messagesError; setMessagesTotal(count || 0); // Get unique user IDs and fetch profiles const userIds = [...new Set((messagesData || []).map((m: any) => m.user_id))]; let profilesMap: Record = {}; if (userIds.length > 0) { const { data: profiles, error: profilesError } = await supabase .from('profiles') .select('id, name, email') .in('id', userIds); if (profilesError) throw profilesError; profilesMap = (profiles || []).reduce((acc: any, p: any) => { acc[p.id] = p; return acc; }, {}); } // Transform data to match our types const transformed: MessageActivity[] = (messagesData || []).map((item: any) => ({ id: item.id, user_id: item.user_id, content: item.content, media_path: item.media_path, media_type: item.media_type, created_at: item.created_at, profile: profilesMap[item.user_id] || null, match: item.match ? { id: item.match.id, question: item.match.question, } : null, })); setMessages(transformed); } catch (error) { console.error('Failed to load messages:', error); toast.error("Failed to load messages"); } finally { setLoading(prev => ({ ...prev, messages: false })); } }, [messagesPage, messagesPageSize]); // Fetch signups const fetchSignups = useCallback(async () => { setLoading(prev => ({ ...prev, signups: true })); try { // Use RPC to get profiles with auth info (includes email from auth.users) const { data: allData, error } = await supabase .rpc('get_profiles_with_auth_info'); if (error) throw error; // Sort by created_at descending and apply pagination const sorted = (allData || []).sort((a: SignupActivity, b: SignupActivity) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); const from = (signupsPage - 1) * signupsPageSize; const to = from + signupsPageSize; setSignups(sorted.slice(from, to)); setSignupsTotal(sorted.length); } catch (error) { console.error('Failed to load signups:', error); toast.error("Failed to load signups"); } finally { setLoading(prev => ({ ...prev, signups: false })); } }, [signupsPage, signupsPageSize]); // Real-time subscriptions const { status: responsesStatus } = useRealtimeSubscription({ table: 'responses', onInsert: useCallback(() => fetchResponses(), [fetchResponses]), insertToast: { enabled: true, message: () => 'New response', type: 'info' }, }); const { status: matchesStatus } = useRealtimeSubscription({ table: 'matches', onInsert: useCallback(() => fetchMatches(), [fetchMatches]), insertToast: { enabled: true, message: () => 'New match!', type: 'success' }, }); const { status: messagesStatus } = useRealtimeSubscription({ table: 'messages', onInsert: useCallback(() => fetchMessages(), [fetchMessages]), insertToast: { enabled: true, message: () => 'New message', type: 'info' }, }); const { status: signupsStatus } = useRealtimeSubscription({ table: 'profiles', onInsert: useCallback(() => fetchSignups(), [fetchSignups]), insertToast: { enabled: true, message: () => 'New user signed up!', type: 'success' }, }); // Fetch all data on mount useEffect(() => { fetchResponses(); fetchMatches(); fetchMessages(); fetchSignups(); }, [fetchResponses, fetchMatches, fetchMessages, fetchSignups]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(responsesTotal / responsesPageSize)); if (responsesPage > totalPages) { setResponsesPage(totalPages); } }, [responsesPage, responsesPageSize, responsesTotal]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(matchesTotal / matchesPageSize)); if (matchesPage > totalPages) { setMatchesPage(totalPages); } }, [matchesPage, matchesPageSize, matchesTotal]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(messagesTotal / messagesPageSize)); if (messagesPage > totalPages) { setMessagesPage(totalPages); } }, [messagesPage, messagesPageSize, messagesTotal]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(signupsTotal / signupsPageSize)); if (signupsPage > totalPages) { setSignupsPage(totalPages); } }, [signupsPage, signupsPageSize, signupsTotal]); // Get current realtime status based on active tab const getCurrentRealtimeStatus = () => { switch (activeTab) { case 'responses': return responsesStatus; case 'matches': return matchesStatus; case 'messages': return messagesStatus; case 'signups': return signupsStatus; } }; // Render user link const renderUserLink = (profile: { id: string; name: string | null; email: string | null } | null) => { if (!profile) return Unknown User; return ( {profile.name || profile.email || 'Unknown'} ); }; // Render answer badge const renderAnswerBadge = (answer: 'yes' | 'no' | 'maybe') => { const config = answerConfig[answer]; return ( {config.icon} {answer} ); }; return ( {/* Header */} User Activity Monitor recent user activity across the platform {/* Tabs */} setActiveTab(v as ActivityTab)}> Responses Matches Messages Signups {/* Responses Tab */} User Question Pack Answer Time {loading.responses ? ( ) : responses.length === 0 ? ( No responses yet. ) : ( responses.map((item) => ( {renderUserLink(item.profile)} {item.question?.pack?.id ? ( {item.question?.text || 'Unknown question'} ) : ( {item.question?.text || 'Unknown question'} )} {item.question?.pack?.id ? ( {item.question.pack.name} ) : ( {item.question?.pack?.name || 'Unknown'} )} {renderAnswerBadge(item.answer)} {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} )) )} {responsesTotal > 0 && ( { setResponsesPage(1); setResponsesPageSize(size); }} /> )} {/* Matches Tab */} Couple Question Match Type Status Time {loading.matches ? ( ) : matches.length === 0 ? ( No matches yet. ) : ( matches.map((item) => ( {item.coupleProfiles && item.coupleProfiles.length > 0 ? ( <> {renderUserLink(item.coupleProfiles[0])} {item.coupleProfiles.length > 1 && ( <> & {renderUserLink(item.coupleProfiles[1])} > )} > ) : ( Unknown couple )} {item.question?.text || 'Unknown question'} {matchTypeLabels[item.match_type]?.label || item.match_type} {item.is_new ? ( New ) : ( Viewed )} {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} )) )} {matchesTotal > 0 && ( { setMatchesPage(1); setMatchesPageSize(size); }} /> )} {/* Messages Tab */} User Context Time Actions {loading.messages ? ( ) : messages.length === 0 ? ( No messages yet. ) : ( messages.map((item) => ( {renderUserLink(item.profile)} {item.match?.question?.text || 'Unknown match'} {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} {item.match && item.profile ? ( View Chat ) : ( - )} )) )} {messagesTotal > 0 && ( { setMessagesPage(1); setMessagesPageSize(size); }} /> )} {/* Signups Tab */} User Email Status Paired Joined {loading.signups ? ( ) : signups.length === 0 ? ( No signups yet. ) : ( signups.map((item) => ( {(item.name || item.email || '?').charAt(0).toUpperCase()} {item.name || 'No name'} {item.email || 'No email'} {item.onboarding_completed ? ( Onboarded ) : ( Pending )} {item.couple_id ? ( Paired ) : ( Single )} {format(new Date(item.created_at), 'MMM d, yyyy')} )) )} {signupsTotal > 0 && ( { setSignupsPage(1); setSignupsPageSize(size); }} /> )} ); }
Monitor recent user activity across the platform