feat: refine session workflow with global state, quick tasting, and statistics

This commit is contained in:
2025-12-18 17:19:38 +01:00
parent 7f600698e4
commit ca1621e765
14 changed files with 399 additions and 116 deletions

View File

@@ -43,6 +43,10 @@ export default async function BottlePage({
.from('tastings')
.select(`
*,
tasting_sessions (
id,
name
),
tasting_tags (
buddies (
id,
@@ -65,17 +69,6 @@ export default async function BottlePage({
Zurück zur Sammlung
</Link>
{sessionId && (
<div className="bg-amber-600/10 border border-amber-600/20 p-4 rounded-2xl flex items-center gap-4 text-amber-700 dark:text-amber-400 animate-in slide-in-from-top-4 duration-500">
<div className="bg-amber-600 text-white p-2 rounded-xl">
<PlusCircle size={20} />
</div>
<div>
<p className="font-bold">Session Aktiv!</p>
<p className="text-sm opacity-80">Jede Tasting Note, die du jetzt speicherst, wird automatisch deiner Session zugeordnet. 🥃</p>
</div>
</div>
)}
{/* Hero Section */}
<section className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
@@ -165,7 +158,7 @@ export default async function BottlePage({
{/* Form */}
<div className="lg:col-span-1 border border-zinc-200 dark:border-zinc-800 rounded-3xl p-6 bg-white dark:bg-zinc-900/50 md:sticky md:top-24">
<h3 className="text-lg font-bold mb-6 flex items-center gap-2 text-amber-600">
<Droplets size={20} /> {sessionId ? 'Session-Notiz' : 'Neu Verkosten'}
<Droplets size={20} /> Dram bewerten
</h3>
<TastingNoteForm bottleId={bottle.id} sessionId={sessionId} />
</div>

View File

@@ -5,6 +5,9 @@ import PWARegistration from "@/components/PWARegistration";
import OfflineIndicator from "@/components/OfflineIndicator";
import UploadQueue from "@/components/UploadQueue";
import { I18nProvider } from "@/i18n/I18nContext";
import { SessionProvider } from "@/context/SessionContext";
import ActiveSessionBanner from "@/components/ActiveSessionBanner";
import MainContentWrapper from "@/components/MainContentWrapper";
const inter = Inter({ subsets: ["latin"] });
@@ -42,10 +45,15 @@ export default function RootLayout({
<html lang="de">
<body className={inter.className}>
<I18nProvider>
<PWARegistration />
<OfflineIndicator />
<UploadQueue />
{children}
<SessionProvider>
<ActiveSessionBanner />
<MainContentWrapper>
<PWARegistration />
<OfflineIndicator />
<UploadQueue />
{children}
</MainContentWrapper>
</SessionProvider>
</I18nProvider>
</body>
</html>

View File

@@ -2,9 +2,11 @@
import React, { useState, useEffect } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight } from 'lucide-react';
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play } from 'lucide-react';
import Link from 'next/link';
import { useSession } from '@/context/SessionContext';
import { useParams, useRouter } from 'next/navigation';
import { useI18n } from '@/i18n/I18nContext';
interface Buddy {
id: string;
@@ -35,6 +37,7 @@ interface SessionTasting {
}
export default function SessionDetailPage() {
const { t } = useI18n();
const { id } = useParams();
const router = useRouter();
const supabase = createClientComponentClient();
@@ -43,6 +46,7 @@ export default function SessionDetailPage() {
const [tastings, setTastings] = useState<SessionTasting[]>([]);
const [allBuddies, setAllBuddies] = useState<Buddy[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { activeSession, setActiveSession } = useSession();
const [isAddingParticipant, setIsAddingParticipant] = useState(false);
useEffect(() => {
@@ -175,8 +179,31 @@ export default function SessionDetailPage() {
<Calendar size={16} className="text-zinc-400" />
{new Date(session.scheduled_at).toLocaleDateString('de-DE')}
</span>
{tastings.length > 0 && (
<span className="flex items-center gap-1.5 transition-all animate-in fade-in slide-in-from-left-2">
<GlassWater size={16} className="text-zinc-400" />
{tastings.length} {tastings.length === 1 ? 'Whisky' : 'Whiskys'}
</span>
)}
</div>
</div>
<div className="flex gap-2">
{activeSession?.id !== session.id ? (
<button
onClick={() => setActiveSession({ id: session.id, name: session.name })}
className="px-6 py-3 bg-amber-600 hover:bg-amber-700 text-white rounded-2xl text-sm font-black uppercase tracking-widest flex items-center gap-2 transition-all shadow-xl shadow-amber-600/20"
>
<Play size={18} fill="currentColor" />
Session Starten
</button>
) : (
<div className="px-6 py-3 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl text-sm font-black uppercase tracking-widest flex items-center gap-2 border border-zinc-200 dark:border-zinc-800">
<Sparkles size={18} className="text-amber-500" />
Aktiv
</div>
)}
</div>
</div>
</header>
@@ -275,8 +302,8 @@ export default function SessionDetailPage() {
</div>
</section>
</div>
</div>
</main>
</div >
</main >
);
}