diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7455c0f..97bc406 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = 'force-dynamic'; import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/src/app/admin/plans/page.tsx b/src/app/admin/plans/page.tsx index dd1c238..dce93ff 100644 --- a/src/app/admin/plans/page.tsx +++ b/src/app/admin/plans/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = 'force-dynamic'; import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index be1b5d5..ff66a76 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = 'force-dynamic'; import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index 610aa16..083e039 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square } from 'lucide-react'; import Link from 'next/link'; +import AvatarStack from '@/components/AvatarStack'; import { deleteSession } from '@/services/delete-session'; import { useSession } from '@/context/SessionContext'; import { useParams, useRouter } from 'next/navigation'; @@ -193,14 +194,20 @@ export default function SessionDetailPage() {

{session.name}

-
- - +
+ + {new Date(session.scheduled_at).toLocaleDateString('de-DE')} + {participants.length > 0 && ( +
+ + p.buddies.name)} limit={5} /> +
+ )} {tastings.length > 0 && ( - - + + {tastings.length} {tastings.length === 1 ? 'Whisky' : 'Whiskys'} )} diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx new file mode 100644 index 0000000..85e4040 --- /dev/null +++ b/src/components/AvatarStack.tsx @@ -0,0 +1,56 @@ +'use client'; + +import React from 'react'; + +interface AvatarStackProps { + names: string[]; + limit?: number; + size?: 'sm' | 'md'; +} + +export default function AvatarStack({ names, limit = 3, size = 'sm' }: AvatarStackProps) { + if (!names || names.length === 0) return null; + + const visibleNames = names.slice(0, limit); + const extraCount = names.length - limit; + + const sizeClasses = size === 'sm' ? 'w-6 h-6 text-[10px]' : 'w-8 h-8 text-xs'; + + const getInitials = (name: string) => { + return name + .split(' ') + .map(n => n[0]) + .slice(0, 2) + .join('') + .toUpperCase(); + }; + + return ( +
+ {visibleNames.map((name, i) => ( +
+ {getInitials(name)} + {/* Tooltip on hover */} +
+ {name} +
+
+ ))} + {extraCount > 0 && ( +
+ +{extraCount} +
+ {names.slice(limit).join(', ')} +
+
+ )} +
+ ); +} diff --git a/src/components/BuddyList.tsx b/src/components/BuddyList.tsx index 8614adf..22feeee 100644 --- a/src/components/BuddyList.tsx +++ b/src/components/BuddyList.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; -import { Users, UserPlus, Trash2, User, Loader2 } from 'lucide-react'; +import { Users, UserPlus, Trash2, User, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; import { useI18n } from '@/i18n/I18nContext'; interface Buddy { @@ -18,6 +18,12 @@ export default function BuddyList() { const [newName, setNewName] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isAdding, setIsAdding] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('whisky-buddies-collapsed') === 'true'; + } + return false; + }); useEffect(() => { fetchBuddies(); @@ -40,6 +46,12 @@ export default function BuddyList() { setIsLoading(false); }; + const handleToggleCollapse = () => { + const nextState = !isCollapsed; + setIsCollapsed(nextState); + localStorage.setItem('whisky-buddies-collapsed', String(nextState)); + }; + const handleAddBuddy = async (e: React.FormEvent) => { e.preventDefault(); if (!newName.trim()) return; @@ -76,63 +88,97 @@ export default function BuddyList() { }; return ( -
-

- - {t('buddy.title')} -

- -
- setNewName(e.target.value)} - placeholder={t('buddy.placeholder')} - className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" - /> +
+
+

+ + {t('buddy.title')} + {!isCollapsed && buddies.length > 0 && ( + ({buddies.length}) + )} +

- +
- {isLoading ? ( -
- -
- ) : buddies.length === 0 ? ( -
- {t('buddy.noBuddies')} -
- ) : ( -
- {buddies.map((buddy) => ( -
+
+ setNewName(e.target.value)} + placeholder={t('buddy.placeholder')} + className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> + + {isAdding ? : } + + + + {isLoading ? ( +
+
- ))} + ) : buddies.length === 0 ? ( +
+ {t('buddy.noBuddies')} +
+ ) : ( +
+ {buddies.map((buddy) => ( +
+
+
+ {buddy.name[0].toUpperCase()} +
+
+ {buddy.name} + {buddy.buddy_profile_id && ( + {t('common.link')} + )} +
+
+ +
+ ))} +
+ )} + + )} + + {isCollapsed && buddies.length > 0 && ( +
+
+ {buddies.slice(0, 5).map((b, i) => ( +
+ {b.name[0].toUpperCase()} +
+ ))} + {buddies.length > 5 && ( +
+ +{buddies.length - 5} +
+ )} +
+ {buddies.length} Buddies
)}
diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 222577f..cf56d9d 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -2,8 +2,9 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; -import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2 } from 'lucide-react'; +import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2, ChevronDown, ChevronUp } from 'lucide-react'; import Link from 'next/link'; +import AvatarStack from './AvatarStack'; import { deleteSession } from '@/services/delete-session'; import { useI18n } from '@/i18n/I18nContext'; import { useSession } from '@/context/SessionContext'; @@ -14,6 +15,7 @@ interface Session { scheduled_at: string; participant_count?: number; whisky_count?: number; + participants?: string[]; } export default function SessionList() { @@ -23,6 +25,12 @@ export default function SessionList() { const [isLoading, setIsLoading] = useState(true); const [isCreating, setIsCreating] = useState(false); const [isDeleting, setIsDeleting] = useState(null); + const [isCollapsed, setIsCollapsed] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('whisky-sessions-collapsed') === 'true'; + } + return false; + }); const [newName, setNewName] = useState(''); const { activeSession, setActiveSession } = useSession(); @@ -35,7 +43,10 @@ export default function SessionList() { .from('tasting_sessions') .select(` *, - session_participants (count), + session_participants ( + count, + buddies (name) + ), tastings (count) `) .order('scheduled_at', { ascending: false }); @@ -57,6 +68,7 @@ export default function SessionList() { setSessions(fallbackData.map(s => ({ ...s, participant_count: s.session_participants[0]?.count || 0, + participants: (s.session_participants as any[])?.filter(p => p.buddies).map(p => p.buddies.name) || [], whisky_count: 0 })) || []); } @@ -64,12 +76,19 @@ export default function SessionList() { setSessions(data.map(s => ({ ...s, participant_count: s.session_participants[0]?.count || 0, + participants: (s.session_participants as any[])?.filter(p => p.buddies).map(p => p.buddies.name) || [], whisky_count: s.tastings[0]?.count || 0 })) || []); } setIsLoading(false); }; + const handleToggleCollapse = () => { + const nextState = !isCollapsed; + setIsCollapsed(nextState); + localStorage.setItem('whisky-sessions-collapsed', String(nextState)); + }; + const handleCreateSession = async (e: React.FormEvent) => { e.preventDefault(); if (!newName.trim()) return; @@ -115,106 +134,138 @@ export default function SessionList() { }; return ( -
-

- - {t('session.title')} -

- -
- setNewName(e.target.value)} - placeholder={t('session.sessionName')} - className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" - /> +
+
+

+ + {t('session.title')} + {!isCollapsed && sessions.length > 0 && ( + ({sessions.length}) + )} +

- +
- {isLoading ? ( -
- -
- ) : sessions.length === 0 ? ( -
- {t('session.noSessions')} -
- ) : ( -
- {sessions.map((session) => ( -
+
+ setNewName(e.target.value)} + placeholder={t('session.sessionName')} + className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> + - ) : ( -
- -
- )} - - -
+ {isCreating ? : } + + + + {isLoading ? ( +
+
- )) - } -
+ ) : sessions.length === 0 ? ( +
+ {t('session.noSessions')} +
+ ) : ( +
+ {sessions.map((session) => ( +
+ +
+ {session.name} +
+
+ + + {new Date(session.scheduled_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')} + + {session.whisky_count! > 0 && ( + + + {session.whisky_count} Whiskys + + )} +
+ {session.participants && session.participants.length > 0 && ( +
+ +
+ )} + +
+ {activeSession?.id !== session.id ? ( + + ) : ( +
+ +
+ )} + + +
+
+ ))} +
+ )} + )} -
+ + {isCollapsed && sessions.length > 0 && ( +
+
+ {sessions.slice(0, 3).map((s, i) => ( +
+ {s.name[0].toUpperCase()} +
+ ))} + {sessions.length > 3 && ( +
+ +{sessions.length - 3} +
+ )} +
+ {sessions.length} Sessions +
+ )} +
); } diff --git a/src/components/TastingList.tsx b/src/components/TastingList.tsx index faaeba3..53af076 100644 --- a/src/components/TastingList.tsx +++ b/src/components/TastingList.tsx @@ -3,6 +3,8 @@ import React, { useState, useMemo } from 'react'; import { Calendar, Star, ArrowUpDown, Clock, Trash2, Loader2, Users, GlassWater } from 'lucide-react'; import Link from 'next/link'; +import AvatarStack from './AvatarStack'; +import { useI18n } from '@/i18n/I18nContext'; import { deleteTasting } from '@/services/delete-tasting'; interface Tasting { @@ -33,6 +35,7 @@ interface TastingListProps { } export default function TastingList({ initialTastings, currentUserId }: TastingListProps) { + const { t } = useI18n(); const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'rating-desc' | 'rating-asc'>('date-desc'); const [isDeleting, setIsDeleting] = useState(null); @@ -185,16 +188,14 @@ export default function TastingList({ initialTastings, currentUserId }: TastingL
{note.tasting_tags && note.tasting_tags.length > 0 && ( -
- - - Gekostet mit: - - {note.tasting_tags.map((tag) => ( - - {tag.buddies.name} +
+
+ + + {t('tasting.with') || 'Mit'}: - ))} + tag.buddies.name)} /> +
)}