From 489b975911b5cdd1d5ea5ef1294a0f64f9dd2453 Mon Sep 17 00:00:00 2001 From: robin Date: Sun, 18 Jan 2026 21:24:53 +0100 Subject: [PATCH] feat: Improve Sessions and Buddies pages with modern UI - Rename 'Events' to 'Tastings' in nav - Sessions page: Create, list, delete sessions with sticky header - Buddies page: Add, search, delete buddies with linked/unlinked sections - Both pages match new home view design language --- src/app/buddies/page.tsx | 247 ++++++++++++++++++++++++++++++++-- src/app/sessions/page.tsx | 276 ++++++++++++++++++++++++++++++++++++-- src/i18n/de.ts | 2 +- src/i18n/en.ts | 2 +- 4 files changed, 499 insertions(+), 28 deletions(-) diff --git a/src/app/buddies/page.tsx b/src/app/buddies/page.tsx index 3003f89..dd8e51e 100644 --- a/src/app/buddies/page.tsx +++ b/src/app/buddies/page.tsx @@ -1,38 +1,259 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { ArrowLeft, Users } from 'lucide-react'; -import BuddyList from '@/components/BuddyList'; +import { ArrowLeft, Users, UserPlus, Loader2, Trash2, Link2, Search } from 'lucide-react'; import { useI18n } from '@/i18n/I18nContext'; +import { useEffect, useState } from 'react'; +import { createClient } from '@/lib/supabase/client'; +import { useAuth } from '@/context/AuthContext'; +import { addBuddy, deleteBuddy } from '@/services/buddy'; +import BuddyHandshake from '@/components/BuddyHandshake'; + +interface Buddy { + id: string; + name: string; + buddy_profile_id: string | null; +} export default function BuddiesPage() { const router = useRouter(); const { t } = useI18n(); + const supabase = createClient(); + const { user, isLoading: isAuthLoading } = useAuth(); + + const [buddies, setBuddies] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isAdding, setIsAdding] = useState(false); + const [newName, setNewName] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [isHandshakeOpen, setIsHandshakeOpen] = useState(false); + + useEffect(() => { + if (!isAuthLoading && user) { + fetchBuddies(); + } + }, [user, isAuthLoading]); + + const fetchBuddies = async () => { + setIsLoading(true); + const { data, error } = await supabase + .from('buddies') + .select('*') + .order('name'); + + if (!error) { + setBuddies(data || []); + } + setIsLoading(false); + }; + + const handleAddBuddy = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newName.trim()) return; + + setIsAdding(true); + const result = await addBuddy({ name: newName.trim() }); + + if (result.success && result.data) { + setBuddies(prev => [...[result.data], ...prev].sort((a, b) => a.name.localeCompare(b.name))); + setNewName(''); + } + setIsAdding(false); + }; + + const handleDeleteBuddy = async (id: string) => { + const result = await deleteBuddy(id); + if (result.success) { + setBuddies(prev => prev.filter(b => b.id !== id)); + } + }; + + const filteredBuddies = buddies.filter(b => + b.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const linkedBuddies = filteredBuddies.filter(b => b.buddy_profile_id); + const unlinkedBuddies = filteredBuddies.filter(b => !b.buddy_profile_id); return ( -
-
- {/* Header */} -
+
+ {/* Header */} +
+
-
-

+
+

{t('buddy.title')}

-

- Your tasting companions +

+ {buddies.length} Buddies

+

- - {/* Buddy List */} -
+ +
+ {/* Add Buddy Form */} +
+ setNewName(e.target.value)} + placeholder={t('buddy.placeholder')} + className="flex-1 bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600 transition-colors" + /> + +
+ + {/* Search */} + {buddies.length > 5 && ( +
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600/50" + /> +
+ )} + + {/* Buddies List */} + {isLoading ? ( +
+ +
+ ) : buddies.length === 0 ? ( +
+
+ +
+

{t('buddy.noBuddies')}

+

+ Add your tasting friends to share sessions and compare notes. +

+
+ ) : ( +
+ {/* Linked Buddies */} + {linkedBuddies.length > 0 && ( +
+

+ + Linked Accounts +

+
+ {linkedBuddies.map(buddy => ( + + ))} +
+
+ )} + + {/* Unlinked Buddies */} + {unlinkedBuddies.length > 0 && ( +
+ {linkedBuddies.length > 0 && ( +

+ Other Buddies +

+ )} +
+ {unlinkedBuddies.map(buddy => ( + + ))} +
+
+ )} +
+ )} +
+ + {/* Buddy Handshake Dialog */} + setIsHandshakeOpen(false)} + onSuccess={() => { + setIsHandshakeOpen(false); + fetchBuddies(); + }} + />
); } + +function BuddyCard({ buddy, onDelete }: { buddy: Buddy; onDelete: (id: string) => void }) { + const { t } = useI18n(); + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + setIsDeleting(true); + await onDelete(buddy.id); + setIsDeleting(false); + }; + + return ( +
+
+
+ {buddy.name[0].toUpperCase()} +
+
+

{buddy.name}

+ {buddy.buddy_profile_id && ( +

+ {t('common.link')} +

+ )} +
+
+ +
+ ); +} diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx index 432ad83..0649496 100644 --- a/src/app/sessions/page.tsx +++ b/src/app/sessions/page.tsx @@ -1,37 +1,287 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { ArrowLeft, Calendar, Plus } from 'lucide-react'; -import SessionList from '@/components/SessionList'; +import { ArrowLeft, Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Sparkles, Clock, Trash2 } from 'lucide-react'; import { useI18n } from '@/i18n/I18nContext'; +import { useEffect, useState } from 'react'; +import { createClient } from '@/lib/supabase/client'; +import Link from 'next/link'; +import { useSession } from '@/context/SessionContext'; +import { useAuth } from '@/context/AuthContext'; +import { deleteSession } from '@/services/delete-session'; +import AvatarStack from '@/components/AvatarStack'; + +interface Session { + id: string; + name: string; + scheduled_at: string; + ended_at?: string; + participant_count?: number; + whisky_count?: number; + participants?: string[]; +} export default function SessionsPage() { const router = useRouter(); - const { t } = useI18n(); + const { t, locale } = useI18n(); + const supabase = createClient(); + const { activeSession, setActiveSession } = useSession(); + const { user, isLoading: isAuthLoading } = useAuth(); + + const [sessions, setSessions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isCreating, setIsCreating] = useState(false); + const [isDeleting, setIsDeleting] = useState(null); + const [newName, setNewName] = useState(''); + const [showCreateForm, setShowCreateForm] = useState(false); + + useEffect(() => { + if (!isAuthLoading && user) { + fetchSessions(); + } + }, [user, isAuthLoading]); + + const fetchSessions = async () => { + setIsLoading(true); + const { data, error } = await supabase + .from('tasting_sessions') + .select(` + *, + session_participants (buddies(name)), + tastings (count) + `) + .order('scheduled_at', { ascending: false }); + + if (error) { + console.error('Error fetching sessions:', error); + // Fallback without tastings join + const { data: fallbackData } = await supabase + .from('tasting_sessions') + .select(`*, session_participants (buddies(name))`) + .order('scheduled_at', { ascending: false }); + + if (fallbackData) { + setSessions(fallbackData.map(s => { + const participants = (s.session_participants as any[])?.filter(p => p.buddies).map(p => p.buddies.name) || []; + return { ...s, participant_count: participants.length, participants, whisky_count: 0 }; + })); + } + } else { + setSessions(data?.map(s => { + const participants = (s.session_participants as any[])?.filter(p => p.buddies).map(p => p.buddies.name) || []; + return { + ...s, + participant_count: participants.length, + participants, + whisky_count: (s.tastings as any[])?.[0]?.count || 0 + }; + }) || []); + } + setIsLoading(false); + }; + + const handleCreateSession = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newName.trim()) return; + + setIsCreating(true); + const { data, error } = await supabase + .from('tasting_sessions') + .insert({ name: newName.trim(), scheduled_at: new Date().toISOString() }) + .select() + .single(); + + if (!error && data) { + setSessions(prev => [{ ...data, participant_count: 0, whisky_count: 0 }, ...prev]); + setNewName(''); + setShowCreateForm(false); + setActiveSession({ id: data.id, name: data.name }); + } + setIsCreating(false); + }; + + const handleDeleteSession = async (id: string) => { + setIsDeleting(id); + const result = await deleteSession(id); + if (result.success) { + setSessions(prev => prev.filter(s => s.id !== id)); + if (activeSession?.id === id) { + setActiveSession(null); + } + } + setIsDeleting(null); + }; + + const formatDate = (date: string) => { + return new Date(date).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', { + day: 'numeric', + month: 'short', + year: 'numeric' + }); + }; + + const activeSessions = sessions.filter(s => !s.ended_at); + const pastSessions = sessions.filter(s => s.ended_at); return ( -
-
- {/* Header */} -
+
+ {/* Header */} +
+
-
-

+
+

{t('session.title')}

-

- Manage your tasting events +

+ {sessions.length} Sessions

+

+
- {/* Session List */} - +
+ {/* Create Form */} + {showCreateForm && ( +
+ setNewName(e.target.value)} + placeholder={t('session.sessionName')} + className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600 mb-3" + autoFocus + /> +
+ + +
+
+ )} + + {/* Active Session Banner */} + {activeSession && ( + +
+
+
+ +
+ +
+
+

+ Live Session +

+

+ {activeSession.name} +

+
+ +
+ + )} + + {/* Sessions List */} + {isLoading ? ( +
+ +
+ ) : sessions.length === 0 ? ( +
+
+ +
+

No Sessions Yet

+

+ Start your first tasting session to track what you're drinking. +

+
+ ) : ( +
+ {sessions.map(session => ( +
+
+
+ +
+
+ +

+ {session.name} +

+ +
+ + + {formatDate(session.scheduled_at)} + + {session.whisky_count ? ( + {session.whisky_count} Whiskys + ) : null} +
+
+ + {session.participants && session.participants.length > 0 && ( + + )} + +
+ + + + +
+
+
+ ))} +
+ )}
); diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 1f16efa..6e30547 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -199,7 +199,7 @@ export const de: TranslationKeys = { activity: 'Aktivität', search: 'Suchen', profile: 'Profil', - sessions: 'Events', + sessions: 'Tastings', buddies: 'Buddies', stats: 'Statistik', wishlist: 'Wunschliste', diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 541643a..9c075c4 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -199,7 +199,7 @@ export const en: TranslationKeys = { activity: 'Activity', search: 'Search', profile: 'Profile', - sessions: 'Events', + sessions: 'Tastings', buddies: 'Buddies', stats: 'Stats', wishlist: 'Wishlist',