import { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { adminRequest } from '@/lib/adminApi'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { PaginationControls } from '@/components/ui/pagination'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Search, Crown, Users, ChevronRight, HardDrive, MailWarning, Hourglass } from 'lucide-react'; import { format } from 'date-fns'; interface Profile { id: string; name: string | null; email: string | null; avatar_url: string | null; is_premium: boolean | null; couple_id: string | null; created_at: string | null; last_sign_in_at: string | null; email_confirmed_at: string | null; storage_bytes?: number; partner_is_premium?: boolean; partner_count?: number; } function formatBytes(bytes: number): string { if (bytes === 0) return '—'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } export function UsersPage() { const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [filteredProfiles, setFilteredProfiles] = useState([]); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const fetchProfiles = useCallback(async () => { setLoading(true); try { // Fetch profiles with auth info (includes last_sign_in_at) const { users: profilesData } = await adminRequest<{ users: Profile[] }>('/v1/admin/users'); // Build a map of couple_id -> users with premium in that couple const couplePartners: Record = {}; profilesData.forEach(profile => { if (profile.couple_id) { if (!couplePartners[profile.couple_id]) { couplePartners[profile.couple_id] = []; } couplePartners[profile.couple_id].push({ id: profile.id, is_premium: profile.is_premium || false, }); } }); // Merge storage data, partner premium status, and partner count with profiles const profilesWithExtras = profilesData.map(profile => { // Check if partner has premium and count partners in couple let partnerIsPremium = false; let partnerCount = 0; if (profile.couple_id && couplePartners[profile.couple_id]) { partnerCount = couplePartners[profile.couple_id].length; const partner = couplePartners[profile.couple_id].find(p => p.id !== profile.id); partnerIsPremium = partner?.is_premium || false; } return { ...profile, storage_bytes: Number(profile.storage_bytes || 0), partner_is_premium: partnerIsPremium, partner_count: partnerCount, }; }); setProfiles(profilesWithExtras); setFilteredProfiles(profilesWithExtras); } catch (error) { console.error('Failed to load profiles:', error); } finally { setLoading(false); } }, []); // Real-time subscription for profiles table const { status: realtimeStatus } = useRealtimeSubscription({ table: 'profiles', onInsert: useCallback(() => { // Refetch to get storage and partner data fetchProfiles(); }, [fetchProfiles]), onUpdate: useCallback(() => { // Refetch to update partner premium status fetchProfiles(); }, [fetchProfiles]), onDelete: useCallback((deleted: Profile) => { setProfiles(prev => prev.filter(p => p.id !== deleted.id)); setFilteredProfiles(prev => prev.filter(p => p.id !== deleted.id)); }, []), insertToast: { enabled: true, message: (payload) => { const label = payload.name || payload.email || 'Guest'; return payload.email ? `New user registered: ${label}` : `New guest user: ${label}`; }, type: 'success', }, debounceMs: 1000, // Debounce to avoid excessive refetches }); useEffect(() => { fetchProfiles(); }, [fetchProfiles]); useEffect(() => { setPage(1); if (!search.trim()) { setFilteredProfiles(profiles); return; } const query = search.toLowerCase(); setFilteredProfiles( profiles.filter( p => p.name?.toLowerCase().includes(query) || p.email?.toLowerCase().includes(query) || p.id.toLowerCase().includes(query) ) ); }, [search, profiles]); const totalCount = filteredProfiles.length; const paginatedProfiles = filteredProfiles.slice( (page - 1) * pageSize, page * pageSize ); useEffect(() => { const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); if (page > totalPages) { setPage(totalPages); } }, [page, pageSize, totalCount]); if (loading) { return (
); } return (
{/* Header */}

Users

{profiles.length} account{profiles.length !== 1 ? 's' : ''} {(() => { const guestCount = profiles.filter(p => !p.email).length; const savedCount = profiles.length - guestCount; if (guestCount === 0) return null; return ( • {savedCount} saved • {guestCount} guest ); })()}

setSearch(e.target.value)} className="pl-9" />
{/* Users Table */}
User Email Status Couple Storage Joined Last Active {filteredProfiles.length === 0 ? ( {search ? 'No users found matching your search.' : 'No users yet.'} ) : ( paginatedProfiles.map((profile) => { const statusBadge = profile.is_premium ? ( Premium ) : profile.partner_is_premium ? ( Partner ) : ( Free ); const coupleBadge = profile.couple_id ? ( profile.partner_count && profile.partner_count >= 2 ? ( Paired ) : ( Pairing ) ) : ( Not paired ); const emailContent = profile.email ? ( {profile.email} {!profile.email_confirmed_at && ( )} ) : ( Guest ); const storageLabel = profile.storage_bytes && profile.storage_bytes > 0 ? formatBytes(profile.storage_bytes) : '—'; const joinedLabel = profile.created_at ? format(new Date(profile.created_at), 'MMM d, yyyy') : '—'; const lastActiveLabel = profile.last_sign_in_at ? format(new Date(profile.last_sign_in_at), 'MMM d, yyyy h:mm a') : 'Never'; return (
{profile.name?.charAt(0).toUpperCase() || 'U'} {profile.name || 'Unnamed User'}
{emailContent} {statusBadge} {coupleBadge} {storageLabel !== '—' && ( {storageLabel} )} Active {lastActiveLabel}
{emailContent}
{statusBadge} {profile.couple_id ? coupleBadge : —} {profile.storage_bytes && profile.storage_bytes > 0 ? (
{storageLabel}
) : ( — )}
{joinedLabel} {lastActiveLabel}
); }) )}
{filteredProfiles.length > 0 && ( { setPage(1); setPageSize(size); }} /> )}
); }