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:
2025-12-28 22:02:46 +01:00
parent 332bfdaf02
commit 9d6a8b358f
25 changed files with 2014 additions and 495 deletions

View File

@@ -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