fix: Fix navigation links and restore LanguageSwitcher

- Add missing nav keys to types.ts, de.ts, en.ts (sessions, buddies, stats, wishlist)
- Add LanguageSwitcher back to authenticated header
- Create /sessions page with SessionList
- Create /buddies page with BuddyList
- Create /stats page with StatsDashboard
- Create /wishlist placeholder page
This commit is contained in:
2026-01-18 21:18:25 +01:00
parent d109dfad0e
commit 1d02079df3
12 changed files with 506 additions and 110 deletions

View File

@@ -5,22 +5,21 @@ import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
import BottleGrid from "@/components/BottleGrid";
import AuthForm from "@/components/AuthForm";
import BuddyList from "@/components/BuddyList";
import SessionList from "@/components/SessionList";
import StatsDashboard from "@/components/StatsDashboard";
import DramOfTheDay from "@/components/DramOfTheDay";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import OfflineIndicator from "@/components/OfflineIndicator";
import { useI18n } from "@/i18n/I18nContext";
import { useAuth } from "@/context/AuthContext";
import { useSession } from "@/context/SessionContext";
import TastingHub from "@/components/TastingHub";
import { Sparkles, X, Loader2 } from "lucide-react";
import { Sparkles, Loader2, Search, SlidersHorizontal } from "lucide-react";
import { BottomNavigation } from '@/components/BottomNavigation';
import ScanAndTasteFlow from '@/components/ScanAndTasteFlow';
import UserStatusBadge from '@/components/UserStatusBadge';
import { getActiveSplits } from '@/services/split-actions';
import SplitCard from '@/components/SplitCard';
import HeroBanner from '@/components/HeroBanner';
import QuickActionsGrid from '@/components/QuickActionsGrid';
import DramOfTheDay from '@/components/DramOfTheDay';
export default function Home() {
const supabase = createClient();
@@ -36,6 +35,7 @@ export default function Home() {
const [capturedFile, setCapturedFile] = useState<File | null>(null);
const [hasMounted, setHasMounted] = useState(false);
const [publicSplits, setPublicSplits] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
setHasMounted(true);
@@ -47,7 +47,6 @@ export default function Home() {
};
useEffect(() => {
// Only fetch if auth is ready and user exists
if (!isAuthLoading && user) {
fetchCollection();
} else if (!isAuthLoading && !user) {
@@ -56,14 +55,12 @@ export default function Home() {
}, [user, isAuthLoading]);
useEffect(() => {
// Fetch public splits if guest
getActiveSplits().then(res => {
if (res.success && res.splits) {
setPublicSplits(res.splits);
}
});
// Listen for collection updates (e.g., after offline sync completes)
const handleCollectionUpdated = () => {
console.log('[Home] Collection update event received, refreshing...');
fetchCollection();
@@ -78,25 +75,21 @@ export default function Home() {
const fetchCollection = async () => {
setIsInternalLoading(true);
try {
// Fetch bottles with their latest tasting date
const { data, error } = await supabase
.from('bottles')
.select(`
*,
tastings (
created_at,
rating
)
`)
*,
tastings (
created_at,
rating
)
`)
.order('created_at', { ascending: false });
if (error) {
throw error;
}
if (error) throw error;
console.log(`Fetched ${data?.length || 0} bottles from Supabase`);
// Process data to get the absolute latest tasting date for each bottle
const processedBottles = (data || []).map(bottle => {
const lastTasted = bottle.tastings && bottle.tastings.length > 0
? bottle.tastings.reduce((latest: string, current: any) =>
@@ -105,41 +98,18 @@ export default function Home() {
)
: null;
return {
...bottle,
last_tasted: lastTasted
};
return { ...bottle, last_tasted: lastTasted };
});
setBottles(processedBottles);
} catch (err: any) {
// Enhanced logging for empty-looking error objects
console.warn('[Home] Fetch collection error caught:', {
name: err?.name,
message: err?.message,
keys: err ? Object.keys(err) : [],
allProps: err ? Object.getOwnPropertyNames(err) : [],
stack: err?.stack,
online: navigator.onLine
});
// Silently skip if offline or common network failure
console.warn('[Home] Fetch collection error:', err?.message);
const isNetworkError = !navigator.onLine ||
err?.name === 'TypeError' ||
err?.message?.includes('Failed to fetch') ||
err?.message?.includes('NetworkError') ||
err?.message?.includes('ERR_INTERNET_DISCONNECTED') ||
(err && typeof err === 'object' && !err.message && Object.keys(err).length === 0);
err?.message?.includes('Failed to fetch');
if (isNetworkError) {
console.log('[fetchCollection] Skipping due to offline mode or network error');
setFetchError(null);
} else {
console.error('Detailed fetch error:', err);
// Safe stringification for Error objects
const errorMessage = err?.message ||
(err && typeof err === 'object' ? JSON.stringify(err, Object.getOwnPropertyNames(err)) : String(err));
setFetchError(errorMessage);
if (!isNetworkError) {
setFetchError(err?.message || 'Unknown error');
}
} finally {
setIsInternalLoading(false);
@@ -150,6 +120,17 @@ export default function Home() {
await supabase.auth.signOut();
};
// Filter bottles by search query
const filteredBottles = bottles.filter(bottle => {
if (!searchQuery.trim()) return true;
const query = searchQuery.toLowerCase();
return (
bottle.name?.toLowerCase().includes(query) ||
bottle.distillery?.toLowerCase().includes(query) ||
bottle.category?.toLowerCase().includes(query)
);
});
if (!hasMounted) {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-zinc-950">
@@ -158,6 +139,7 @@ export default function Home() {
);
}
// Guest / Login View
if (!user) {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-950">
@@ -174,7 +156,7 @@ export default function Home() {
</div>
<AuthForm />
{!user && publicSplits.length > 0 && (
{publicSplits.length > 0 && (
<div className="mt-16 w-full max-w-lg space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-300">
<div className="flex flex-col items-center gap-2">
<h2 className="text-[10px] font-black uppercase tracking-[0.4em] text-orange-600/60">
@@ -199,61 +181,83 @@ export default function Home() {
const isLoading = isAuthLoading || isInternalLoading;
// Authenticated Home View - New Layout
return (
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-[var(--background)] pb-32">
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-12">
<header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
<div className="flex flex-col items-center sm:items-start group">
<h1 className="text-4xl font-bold text-zinc-50 tracking-tighter">
DRAM<span className="text-orange-600">LOG</span>
</h1>
{activeSession && (
<div className="flex items-center gap-2 mt-1 animate-in fade-in slide-in-from-left-2 duration-700">
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-600 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-600"></span>
<div className="flex flex-col min-h-screen bg-[var(--background)] relative">
{/* Scrollable Content Area */}
<div className="flex-1 overflow-y-auto pb-24">
{/* 1. Header */}
<header className="px-4 pt-4 pb-2">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<h1 className="text-2xl font-bold text-zinc-50 tracking-tighter">
DRAM<span className="text-orange-600">LOG</span>
</h1>
{activeSession && (
<div className="flex items-center gap-2 mt-0.5 animate-in fade-in slide-in-from-left-2 duration-700">
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-600 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-600"></span>
</div>
<span className="text-[9px] font-bold uppercase tracking-widest text-orange-600 flex items-center gap-1">
<Sparkles size={10} className="animate-pulse" />
Live: {activeSession.name}
</span>
</div>
<span className="text-[9px] font-bold uppercase tracking-widest text-orange-600 flex items-center gap-1">
<Sparkles size={10} className="animate-pulse" />
Live: {activeSession.name}
</span>
</div>
)}
</div>
<div className="flex flex-wrap items-center justify-center sm:justify-end gap-3 md:gap-4">
<UserStatusBadge />
<OfflineIndicator />
<LanguageSwitcher />
<DramOfTheDay bottles={bottles} />
<button
onClick={handleLogout}
className="text-[10px] font-bold uppercase tracking-widest text-zinc-500 hover:text-white transition-colors"
>
{t('home.logout')}
</button>
)}
</div>
<div className="flex items-center gap-2">
<UserStatusBadge />
<OfflineIndicator />
<LanguageSwitcher />
<DramOfTheDay bottles={bottles} />
<button
onClick={handleLogout}
className="text-[9px] font-bold uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
>
{t('home.logout')}
</button>
</div>
</div>
</header>
<div className="w-full">
<StatsDashboard bottles={bottles} />
{/* 2. Hero Banner (optional) */}
<div className="px-4 mt-2 mb-4">
<HeroBanner />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 w-full max-w-5xl">
<div className="flex flex-col gap-8">
<SessionList />
</div>
<div>
<BuddyList />
{/* 3. Quick Actions Grid */}
<div className="px-4 mb-4">
<QuickActionsGrid />
</div>
{/* 4. Sticky Search Bar */}
<div className="sticky top-0 z-20 px-4 py-3 bg-zinc-950/95 backdrop-blur-md border-b border-zinc-900">
<div className="flex items-center gap-3">
<div className="flex-1 relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" />
<input
type="text"
placeholder={t('home.searchPlaceholder') || 'Search collection...'}
value={searchQuery}
onChange={(e) => 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 focus:ring-1 focus:ring-orange-600/20"
/>
</div>
<button className="p-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-500 hover:text-white hover:border-zinc-700 transition-colors">
<SlidersHorizontal size={18} />
</button>
</div>
</div>
<div className="w-full mt-4" id="collection">
<div className="flex items-end justify-between mb-8">
<h2 className="text-3xl font-bold text-zinc-50 uppercase tracking-tight">
{/* 5. Collection */}
<div className="px-4 mt-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-zinc-50">
{t('home.collection')}
</h2>
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest pb-1">
{bottles.length} {t('home.bottleCount')}
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">
{filteredBottles.length} {t('home.bottleCount')}
</span>
</div>
@@ -262,33 +266,38 @@ export default function Home() {
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-orange-600"></div>
</div>
) : fetchError ? (
<div className="p-12 bg-zinc-900 border border-zinc-800 rounded-3xl text-center">
<p className="text-zinc-50 font-bold text-xl mb-2">{t('common.error')}</p>
<p className="text-zinc-500 text-xs italic mb-8 mx-auto max-w-xs">{fetchError}</p>
<div className="p-8 bg-zinc-900 border border-zinc-800 rounded-2xl text-center">
<p className="text-zinc-50 font-bold mb-2">{t('common.error')}</p>
<p className="text-zinc-500 text-xs mb-6">{fetchError}</p>
<button
onClick={fetchCollection}
className="px-10 py-4 bg-orange-600 hover:bg-orange-700 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-orange-950/20"
className="px-6 py-3 bg-orange-600 hover:bg-orange-700 text-white rounded-xl text-xs font-bold uppercase tracking-widest transition-all"
>
{t('home.reTry')}
</button>
</div>
) : (
bottles.length > 0 && <BottleGrid bottles={bottles} />
)}
) : filteredBottles.length > 0 ? (
<BottleGrid bottles={filteredBottles} />
) : bottles.length > 0 ? (
<div className="text-center py-12 text-zinc-500">
<p className="text-sm">No bottles match your search</p>
</div>
) : null}
</div>
{/* Footer */}
<footer className="pb-28 pt-8 text-center">
<div className="flex justify-center gap-4 text-xs text-zinc-600">
<a href="/impressum" className="hover:text-orange-500 transition-colors">{t('home.imprint')}</a>
<span></span>
<a href="/privacy" className="hover:text-orange-500 transition-colors">{t('home.privacy')}</a>
<span></span>
<a href="/settings" className="hover:text-orange-500 transition-colors">{t('home.settings')}</a>
</div>
</footer>
</div>
{/* Footer */}
<footer className="pb-28 pt-8 text-center">
<div className="flex justify-center gap-4 text-xs text-zinc-600">
<a href="/impressum" className="hover:text-orange-500 transition-colors">{t('home.imprint')}</a>
<span></span>
<a href="/privacy" className="hover:text-orange-500 transition-colors">{t('home.privacy')}</a>
<span></span>
<a href="/settings" className="hover:text-orange-500 transition-colors">{t('home.settings')}</a>
</div>
</footer>
{/* Bottom Navigation with FAB */}
<BottomNavigation
onHome={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
onShelf={() => document.getElementById('collection')?.scrollIntoView({ behavior: 'smooth' })}
@@ -308,6 +317,6 @@ export default function Home() {
imageFile={capturedFile}
onBottleSaved={() => fetchCollection()}
/>
</main>
</div>
);
}