diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c559b81..f38da88 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -141,6 +141,12 @@ export default async function AdminPage() { > All Tastings + + All Sessions + (null); + const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'ended'>('all'); + + // Get unique hosts for filter + const hosts = useMemo(() => { + const hostMap = new Map(); + sessions.forEach(s => { + hostMap.set(s.user_id, s.user.display_name || s.user.username); + }); + return Array.from(hostMap.entries()).sort((a, b) => a[1].localeCompare(b[1])); + }, [sessions]); + + // Filter sessions + const filteredSessions = useMemo(() => { + let result = sessions; + + if (search) { + const searchLower = search.toLowerCase(); + result = result.filter(s => + s.name?.toLowerCase().includes(searchLower) || + s.user.username.toLowerCase().includes(searchLower) || + s.user.display_name?.toLowerCase().includes(searchLower) + ); + } + + if (filterHost) { + result = result.filter(s => s.user_id === filterHost); + } + + if (filterStatus === 'active') { + result = result.filter(s => !s.ended_at); + } else if (filterStatus === 'ended') { + result = result.filter(s => s.ended_at); + } + + return result; + }, [sessions, search, filterHost, filterStatus]); + + const formatDate = (date: string) => { + return new Date(date).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const getSessionDuration = (start: string, end: string | null) => { + const startDate = new Date(start); + const endDate = end ? new Date(end) : new Date(); + const diffMs = endDate.getTime() - startDate.getTime(); + const hours = Math.floor(diffMs / (1000 * 60 * 60)); + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); + if (hours > 0) { + return `${hours}h ${minutes}m`; + } + return `${minutes}m`; + }; + + return ( +
+ {/* Filters */} +
+
+ + setSearch(e.target.value)} + placeholder="Search sessions or hosts..." + className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600" + /> +
+ +
+ + + +
+
+ + {/* Results */} +
+ Showing {filteredSessions.length} of {sessions.length} sessions +
+ + {/* Sessions List */} +
+ {filteredSessions.map(session => ( +
+
+ {/* Icon */} +
+ +
+ + {/* Info */} +
+
+

{session.name}

+ {!session.ended_at ? ( + + + Live + + ) : ( + + + Ended + + )} +
+ +
+ + + {session.user.display_name || session.user.username} + + + + {formatDate(session.scheduled_at)} + + + + {getSessionDuration(session.scheduled_at, session.ended_at)} + +
+
+ + {/* Stats */} +
+
+
{session.participantCount}
+
Buddies
+
+
+
{session.tastingCount}
+
Tastings
+
+
+ + {/* Link */} + + + +
+
+ ))} +
+ + {/* Empty State */} + {filteredSessions.length === 0 && ( +
+
+ +
+

No Sessions Found

+

No tasting sessions match your filters.

+
+ )} +
+ ); +} diff --git a/src/app/admin/sessions/page.tsx b/src/app/admin/sessions/page.tsx new file mode 100644 index 0000000..53f7d74 --- /dev/null +++ b/src/app/admin/sessions/page.tsx @@ -0,0 +1,133 @@ +export const dynamic = 'force-dynamic'; + +import { createClient } from '@/lib/supabase/server'; +import { redirect } from 'next/navigation'; +import { checkIsAdmin } from '@/services/track-api-usage'; +import Link from 'next/link'; +import { ArrowLeft, Calendar, User, Users, GlassWater, Clock, CheckCircle } from 'lucide-react'; +import AdminSessionsList from './AdminSessionsList'; + +export default async function AdminSessionsPage() { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + redirect('/'); + } + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) { + redirect('/'); + } + + // Fetch all sessions from all users + const { data: sessionsRaw, error } = await supabase + .from('tasting_sessions') + .select(` + id, + name, + user_id, + scheduled_at, + ended_at, + created_at, + session_participants (id), + tastings (id) + `) + .order('created_at', { ascending: false }) + .limit(500); + + // Get unique user IDs + const userIds = Array.from(new Set(sessionsRaw?.map(s => s.user_id) || [])); + + // Fetch profiles for users + const { data: profiles } = userIds.length > 0 + ? await supabase.from('profiles').select('id, username, display_name').in('id', userIds) + : { data: [] }; + + // Combine sessions with user info + const sessions = sessionsRaw?.map(session => ({ + ...session, + user: profiles?.find(p => p.id === session.user_id) || { username: 'Unknown', display_name: null }, + participantCount: (session.session_participants as any[])?.length || 0, + tastingCount: (session.tastings as any[])?.length || 0, + })) || []; + + // Calculate stats + const stats = { + totalSessions: sessions.length, + activeSessions: sessions.filter(s => !s.ended_at).length, + totalHosts: userIds.length, + totalParticipants: sessions.reduce((sum, s) => sum + s.participantCount, 0), + totalTastings: sessions.reduce((sum, s) => sum + s.tastingCount, 0), + }; + + return ( +
+
+ {/* Header */} +
+ + + +
+

All Tasting Sessions

+

+ View all tasting sessions from all users +

+
+
+ + {error && ( +
+ Error loading sessions: {error.message} +
+ )} + + {/* Stats Cards */} +
+
+
+ + Total Sessions +
+
{stats.totalSessions}
+
+
+
+ + Active +
+
{stats.activeSessions}
+
+
+
+ + Hosts +
+
{stats.totalHosts}
+
+
+
+ + Participants +
+
{stats.totalParticipants}
+
+
+
+ + Tastings +
+
{stats.totalTastings}
+
+
+ + {/* Sessions List */} + +
+
+ ); +}