223 lines
8.9 KiB
TypeScript
223 lines
8.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
|
import CameraCapture from "@/components/CameraCapture";
|
|
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 { useI18n } from "@/i18n/I18nContext";
|
|
|
|
export default function Home() {
|
|
const supabase = createClientComponentClient();
|
|
const [bottles, setBottles] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [user, setUser] = useState<any>(null);
|
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
|
const { t } = useI18n();
|
|
|
|
useEffect(() => {
|
|
// Check session
|
|
const checkUser = async () => {
|
|
try {
|
|
// Proactively get session - this will trigger a refresh if needed
|
|
const { data: { session }, error } = await supabase.auth.getSession();
|
|
|
|
if (session) {
|
|
console.log('[Auth] Valid session found:', {
|
|
userId: session.user.id,
|
|
expiry: new Date(session.expires_at! * 1000).toLocaleString()
|
|
});
|
|
} else {
|
|
console.log('[Auth] No active session found.');
|
|
}
|
|
|
|
if (error) {
|
|
console.error('[Auth] Session check error:', error);
|
|
}
|
|
|
|
setUser(session?.user ?? null);
|
|
if (session?.user) {
|
|
fetchCollection();
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Unexpected error during session check:', err);
|
|
setUser(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
checkUser();
|
|
|
|
// Listen for visibility change (wake up from sleep)
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'visible') {
|
|
console.log('[Auth] App became visible, refreshing session...');
|
|
checkUser();
|
|
}
|
|
};
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
// Listen for auth changes
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
|
console.log('[Auth] State change event:', event, {
|
|
hasSession: !!session,
|
|
userId: session?.user?.id,
|
|
email: session?.user?.email
|
|
});
|
|
|
|
setUser(session?.user ?? null);
|
|
if (session?.user) {
|
|
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION' || event === 'TOKEN_REFRESHED') {
|
|
fetchCollection();
|
|
}
|
|
} else {
|
|
setBottles([]);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
};
|
|
}, []);
|
|
|
|
const fetchCollection = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
// Fetch bottles with their latest tasting date
|
|
const { data, error } = await supabase
|
|
.from('bottles')
|
|
.select(`
|
|
*,
|
|
tastings (
|
|
created_at,
|
|
rating
|
|
)
|
|
`)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
console.error('Supabase fetch error:', 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) =>
|
|
new Date(current.created_at) > new Date(latest) ? current.created_at : latest,
|
|
bottle.tastings[0].created_at
|
|
)
|
|
: null;
|
|
|
|
return {
|
|
...bottle,
|
|
last_tasted: lastTasted
|
|
};
|
|
});
|
|
|
|
setBottles(processedBottles);
|
|
} catch (err: any) {
|
|
console.error('Detailed fetch error:', err);
|
|
setFetchError(err.message || JSON.stringify(err));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await supabase.auth.signOut();
|
|
};
|
|
|
|
if (!user) {
|
|
return (
|
|
<main className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-50 dark:bg-black">
|
|
<div className="mb-12 text-center">
|
|
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter mb-4">
|
|
WHISKY<span className="text-amber-600">VAULT</span>
|
|
</h1>
|
|
<p className="text-zinc-500 max-w-sm mx-auto">
|
|
{t('home.searchPlaceholder').replace('...', '')}
|
|
</p>
|
|
<div className="mt-8">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</div>
|
|
<AuthForm />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-zinc-50 dark:bg-black">
|
|
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-8">
|
|
<header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
|
|
<h1 className="text-4xl font-black text-zinc-900 dark:text-white tracking-tighter">
|
|
WHISKY<span className="text-amber-600">VAULT</span>
|
|
</h1>
|
|
<div className="flex flex-wrap items-center justify-center sm:justify-end gap-3 md:gap-4">
|
|
<LanguageSwitcher />
|
|
<DramOfTheDay bottles={bottles} />
|
|
<button
|
|
onClick={handleLogout}
|
|
className="text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors"
|
|
>
|
|
{t('home.logout')}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="w-full">
|
|
<StatsDashboard bottles={bottles} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-5xl">
|
|
<div className="flex flex-col gap-8">
|
|
<CameraCapture onSaveComplete={fetchCollection} />
|
|
<SessionList />
|
|
</div>
|
|
<div>
|
|
<BuddyList />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full mt-12">
|
|
<h2 className="text-2xl font-bold mb-6 text-zinc-800 dark:text-zinc-100 flex items-center gap-3">
|
|
{t('home.collection')}
|
|
<span className="text-sm font-normal text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-3 py-1 rounded-full">
|
|
{bottles.length}
|
|
</span>
|
|
</h2>
|
|
|
|
{isLoading ? (
|
|
<div className="flex justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div>
|
|
</div>
|
|
) : fetchError ? (
|
|
<div className="p-8 bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-3xl text-center">
|
|
<p className="text-zinc-800 dark:text-zinc-200 font-bold mb-2">{t('common.error')}</p>
|
|
<p className="text-zinc-500 text-sm italic mb-4">{fetchError}</p>
|
|
<button
|
|
onClick={fetchCollection}
|
|
className="px-6 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl text-xs font-bold uppercase tracking-widest transition-all"
|
|
>
|
|
{t('home.reTry')}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<BottleGrid bottles={bottles} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|