diff --git a/sql/create_app_banners.sql b/sql/create_app_banners.sql
new file mode 100644
index 0000000..15e9a26
--- /dev/null
+++ b/sql/create_app_banners.sql
@@ -0,0 +1,49 @@
+-- App Banners Table for dynamic hero content on home page
+
+CREATE TABLE IF NOT EXISTS app_banners (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ title TEXT NOT NULL,
+ image_url TEXT NOT NULL, -- 16:9 Banner Image
+ link_target TEXT, -- e.g., '/sessions'
+ cta_text TEXT DEFAULT 'Open',
+ is_active BOOLEAN DEFAULT false,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- Only one banner should be active at a time (optional constraint)
+CREATE UNIQUE INDEX IF NOT EXISTS idx_app_banners_active
+ ON app_banners (is_active)
+ WHERE is_active = true;
+
+-- RLS Policies
+ALTER TABLE app_banners ENABLE ROW LEVEL SECURITY;
+
+-- Everyone can view active banners
+CREATE POLICY "Anyone can view active banners"
+ ON app_banners FOR SELECT
+ USING (is_active = true);
+
+-- Admins can manage all banners
+CREATE POLICY "Admins can manage banners"
+ ON app_banners FOR ALL
+ USING (
+ EXISTS (
+ SELECT 1 FROM admin_users
+ WHERE admin_users.user_id = auth.uid()
+ )
+ );
+
+-- Trigger for updated_at
+CREATE OR REPLACE FUNCTION update_app_banners_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_app_banners_updated_at
+ BEFORE UPDATE ON app_banners
+ FOR EACH ROW
+ EXECUTE FUNCTION update_app_banners_updated_at();
diff --git a/src/app/buddies/page.tsx b/src/app/buddies/page.tsx
new file mode 100644
index 0000000..3003f89
--- /dev/null
+++ b/src/app/buddies/page.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { ArrowLeft, Users } from 'lucide-react';
+import BuddyList from '@/components/BuddyList';
+import { useI18n } from '@/i18n/I18nContext';
+
+export default function BuddiesPage() {
+ const router = useRouter();
+ const { t } = useI18n();
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ {t('buddy.title')}
+
+
+ Your tasting companions
+
+
+
+
+ {/* Buddy List */}
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 43ee207..150c4db 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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(null);
const [hasMounted, setHasMounted] = useState(false);
const [publicSplits, setPublicSplits] = useState([]);
+ 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 (
@@ -158,6 +139,7 @@ export default function Home() {
);
}
+ // Guest / Login View
if (!user) {
return (
@@ -174,7 +156,7 @@ export default function Home() {
- {!user && publicSplits.length > 0 && (
+ {publicSplits.length > 0 && (
@@ -199,61 +181,83 @@ export default function Home() {
const isLoading = isAuthLoading || isInternalLoading;
+ // Authenticated Home View - New Layout
return (
-
-