diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx index 49f05446..cb7676c4 100644 --- a/packages/web/app/page.tsx +++ b/packages/web/app/page.tsx @@ -181,6 +181,48 @@ export default async function Home({ } } + // Enrich hero posts with spots/solutions for item overlay + try { + const supabase = await createSupabaseServerClient(); + const heroPostIds = heroPosts.map((hp) => hp.id); + const { data: heroSpots } = await supabase + .from("spots") + .select("id, post_id, position_top, position_left, solutions(id, title, thumbnail_url, metadata)") + .in("post_id", heroPostIds); + + if (heroSpots) { + const spotsByPost = new Map(); + for (const spot of heroSpots) { + const existing = spotsByPost.get(spot.post_id) ?? []; + existing.push(spot); + spotsByPost.set(spot.post_id, existing); + } + for (const hp of heroPosts) { + const spots = spotsByPost.get(hp.id) ?? []; + hp.items = spots.flatMap((spot: any) => + (spot.solutions || []) + .filter((sol: any) => sol.thumbnail_url) + .map((sol: any) => ({ + id: sol.id, + label: sol.title, + name: sol.title, + brand: (sol.metadata as any)?.brand || "", + imageUrl: proxyImg(sol.thumbnail_url), + })) + ).slice(0, 4); + if (hp.items.length > 0) { + const post = recentPosts.find((p) => p.id === hp.id) ?? popularPosts.find((p) => p.id === hp.id); + if (post) { + hp.heroData = buildHeroFromApiPost( + post, + buildSpots(hp.items.map((it) => ({ ...it, label: it.name }))) + ); + } + } + } + } + } catch { /* hero spots enrichment failed — show without items */ } + // --- Trending on Decoded --- (#88: temporarily disabled) // const trendingPostCards: LatestPostCardData[] = popularPosts.slice(0, 16).map((p) => { // const { displayName } = enrichArtistName(p.artist_name || p.group_name); diff --git a/packages/web/lib/hooks/useSolutions.ts b/packages/web/lib/hooks/useSolutions.ts index 755c9de7..69d9de3b 100644 --- a/packages/web/lib/hooks/useSolutions.ts +++ b/packages/web/lib/hooks/useSolutions.ts @@ -29,6 +29,7 @@ import type { MetadataResponse, AffiliateLinkResponse, } from "@/lib/api/generated/models"; +import { supabaseBrowserClient } from "@/lib/supabase/client"; /** * Cache Invalidation Boundaries (MIG-09) @@ -82,11 +83,38 @@ export function useSolutions( // ============================================================ /** Solutions grouped by spot ID */ +/** Fetch solutions for a spot via Supabase (no Rust backend needed) */ +async function fetchSolutionsFromSupabase(spotId: string): Promise { + const { data, error } = await supabaseBrowserClient + .from("solutions") + .select("*, profiles:user_id(id, username, avatar_url)") + .eq("spot_id", spotId) + .order("created_at", { ascending: false }); + + if (error) throw new Error(error.message); + return (data ?? []).map((row: any) => ({ + id: row.id, + title: row.title ?? "", + thumbnail_url: row.thumbnail_url, + original_url: row.original_url, + affiliate_url: row.affiliate_url, + link_type: row.link_type, + metadata: row.metadata, + brand_id: row.brand_id, + match_type: row.match_type, + is_verified: row.is_verified ?? false, + is_adopted: row.is_adopted ?? false, + created_at: row.created_at, + vote_stats: { accurate: 0, different: 0 }, + user: { id: row.user_id, username: "", email: "", rank: "Member", total_points: 0, followers_count: 0, following_count: 0, is_admin: false }, + } as GeneratedSolutionListItem)); +} + export function useAllSolutionsForSpots(spotIds: string[]) { const results = useQueries({ queries: spotIds.map((spotId) => ({ queryKey: solutionKeys.list(spotId), - queryFn: () => listSolutions(spotId), + queryFn: () => fetchSolutionsFromSupabase(spotId), enabled: !!spotId, staleTime: 1000 * 60, })), diff --git a/packages/web/lib/server-env.ts b/packages/web/lib/server-env.ts index 1739d00f..34ab57e8 100644 --- a/packages/web/lib/server-env.ts +++ b/packages/web/lib/server-env.ts @@ -1,8 +1,9 @@ -function requireEnv(name: string): string { - const val = process.env[name]; - if (!val) throw new Error(`Missing required env var: ${name}`); - return val; +function getEnvOrEmpty(name: string): string { + return process.env[name] ?? ""; } -/** Evaluated at module load — fails fast if missing */ -export const API_BASE_URL = requireEnv("API_BASE_URL"); +/** + * Backend API base URL. Empty when no Rust backend is configured — + * API routes will return 502 and clients fall back to Supabase. + */ +export const API_BASE_URL = getEnvOrEmpty("API_BASE_URL");