feat: public split visibility, RLS recursion fixes, and consolidated tasting permission management
- Added public discovery section for active splits on the landing page - Refactored split detail page for guest support and login redirects - Extracted SplitCard component for reuse - Consolidated RLS policies for bottles and tastings to resolve permission errors - Added unified SQL consolidation script for RLS and naming fixes - Enhanced service logging for better database error diagnostics
This commit is contained in:
@@ -13,10 +13,13 @@ import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import OfflineIndicator from "@/components/OfflineIndicator";
|
||||
import { useI18n } from "@/i18n/I18nContext";
|
||||
import { useSession } from "@/context/SessionContext";
|
||||
import TastingHub from "@/components/TastingHub";
|
||||
import { Sparkles, X, Loader2 } 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';
|
||||
|
||||
export default function Home() {
|
||||
const supabase = createClient();
|
||||
@@ -28,8 +31,10 @@ export default function Home() {
|
||||
const { t } = useI18n();
|
||||
const { activeSession } = useSession();
|
||||
const [isFlowOpen, setIsFlowOpen] = useState(false);
|
||||
const [isTastingHubOpen, setIsTastingHubOpen] = useState(false);
|
||||
const [capturedFile, setCapturedFile] = useState<File | null>(null);
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
const [publicSplits, setPublicSplits] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setHasMounted(true);
|
||||
@@ -74,6 +79,13 @@ export default function Home() {
|
||||
|
||||
checkUser();
|
||||
|
||||
// Fetch public splits if guest
|
||||
getActiveSplits().then(res => {
|
||||
if (res.success && res.splits) {
|
||||
setPublicSplits(res.splits);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for visibility change (wake up from sleep)
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
@@ -153,19 +165,33 @@ export default function Home() {
|
||||
|
||||
setBottles(processedBottles);
|
||||
} catch (err: any) {
|
||||
// Silently skip if offline
|
||||
// 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
|
||||
const isNetworkError = !navigator.onLine ||
|
||||
err.message?.includes('Failed to fetch') ||
|
||||
err.message?.includes('NetworkError') ||
|
||||
err.message?.includes('ERR_INTERNET_DISCONNECTED') ||
|
||||
(err && Object.keys(err).length === 0); // Empty error object from Supabase when offline
|
||||
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);
|
||||
|
||||
if (isNetworkError) {
|
||||
console.log('[fetchCollection] Skipping due to offline mode');
|
||||
console.log('[fetchCollection] Skipping due to offline mode or network error');
|
||||
setFetchError(null);
|
||||
} else {
|
||||
console.error('Detailed fetch error:', err);
|
||||
setFetchError(err.message || JSON.stringify(err));
|
||||
// Safe stringification for Error objects
|
||||
const errorMessage = err?.message ||
|
||||
(err && typeof err === 'object' ? JSON.stringify(err, Object.getOwnPropertyNames(err)) : String(err));
|
||||
setFetchError(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -192,13 +218,33 @@ export default function Home() {
|
||||
DRAM<span className="text-orange-600">LOG</span>
|
||||
</h1>
|
||||
<p className="text-zinc-500 max-w-sm mx-auto font-bold tracking-wide">
|
||||
Modern Minimalist Tasting Tool.
|
||||
{t('home.tagline')}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<AuthForm />
|
||||
|
||||
{!user && 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">
|
||||
{t('splits.publicExplore')}
|
||||
</h2>
|
||||
<div className="h-px w-8 bg-orange-600/20" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{publicSplits.map((split) => (
|
||||
<SplitCard
|
||||
key={split.id}
|
||||
split={split}
|
||||
onSelect={() => router.push(`/splits/${split.slug}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -254,10 +300,10 @@ export default function Home() {
|
||||
<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">
|
||||
Collection
|
||||
{t('home.collection')}
|
||||
</h2>
|
||||
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest pb-1">
|
||||
{bottles.length} Bottles
|
||||
{bottles.length} {t('home.bottleCount')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -285,20 +331,25 @@ export default function Home() {
|
||||
{/* 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">Impressum</a>
|
||||
<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">Datenschutz</a>
|
||||
<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">Einstellungen</a>
|
||||
<a href="/settings" className="hover:text-orange-500 transition-colors">{t('home.settings')}</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<BottomNavigation
|
||||
onScan={handleImageSelected}
|
||||
onHome={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
onShelf={() => document.getElementById('collection')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
onSearch={() => document.getElementById('search-filter')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
onTastings={() => setIsTastingHubOpen(true)}
|
||||
onProfile={() => router.push('/settings')}
|
||||
onScan={handleImageSelected}
|
||||
/>
|
||||
|
||||
<TastingHub
|
||||
isOpen={isTastingHubOpen}
|
||||
onClose={() => setIsTastingHubOpen(false)}
|
||||
/>
|
||||
|
||||
<ScanAndTasteFlow
|
||||
|
||||
Reference in New Issue
Block a user