// Generic Realtime subscription hook for Supabase tables // Fetches initial data then listens for INSERT/UPDATE/DELETE events import { useEffect, useState, useCallback, useRef } from 'react'; import { supabase } from '../lib/supabase'; import type { RealtimeChannel } from '@supabase/supabase-js'; type Row = { [key: string]: unknown }; interface UseRealtimeQueryOptions { table: string; /** Supabase filter string, e.g. 'user_id=eq.abc123' */ filter?: string; /** Column to order by (default: created_at desc) */ orderBy?: string; ascending?: boolean; limit?: number; /** Map a raw DB row to the UI type */ mapRow: (row: Row) => T; /** Unique key extractor (default: row.id) */ getKey?: (item: T) => string; enabled?: boolean; } interface UseRealtimeQueryResult { data: T[]; loading: boolean; error: string | null; refetch: () => Promise; } export function useRealtimeQuery( options: UseRealtimeQueryOptions, ): UseRealtimeQueryResult { const { table, filter, orderBy = 'created_at', ascending = false, limit, mapRow, getKey = (item: T) => (item as Row).id as string, enabled = true, } = options; const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const channelRef = useRef(null); const fetchData = useCallback(async () => { if (!enabled) return; setLoading(true); setError(null); let query = supabase.from(table).select('*').order(orderBy, { ascending }); if (limit) query = query.limit(limit); // Apply simple eq filters from the filter string if (filter) { const parts = filter.split('=eq.'); if (parts.length === 2) { query = query.eq(parts[0], parts[1]); } } const { data: rows, error: err } = await query; if (err) { setError(err.message); setLoading(false); return; } setData((rows ?? []).map(mapRow)); setLoading(false); }, [table, filter, orderBy, ascending, limit, mapRow, enabled]); useEffect(() => { if (!enabled) return; fetchData(); // Realtime subscription const channelName = `realtime-${table}-${filter ?? 'all'}`; const channel = supabase .channel(channelName) .on( 'postgres_changes', { event: '*', schema: 'public', table, ...(filter ? { filter } : {}), }, (payload) => { const eventType = payload.eventType; if (eventType === 'INSERT') { const newItem = mapRow(payload.new as Row); setData((prev) => ascending ? [...prev, newItem] : [newItem, ...prev], ); } else if (eventType === 'UPDATE') { const updatedItem = mapRow(payload.new as Row); const key = getKey(updatedItem); setData((prev) => prev.map((item) => (getKey(item) === key ? updatedItem : item)), ); } else if (eventType === 'DELETE') { const oldId = (payload.old as Row).id as string; if (oldId) { setData((prev) => prev.filter((item) => getKey(item) !== oldId), ); } } }, ) .subscribe(); channelRef.current = channel; return () => { if (channelRef.current) { supabase.removeChannel(channelRef.current); channelRef.current = null; } }; }, [table, filter, enabled, ascending, fetchData, mapRow, getKey]); return { data, loading, error, refetch: fetchData }; }