Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, typeof heroSpots>();
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);
Expand Down
30 changes: 29 additions & 1 deletion packages/web/lib/hooks/useSolutions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
MetadataResponse,
AffiliateLinkResponse,
} from "@/lib/api/generated/models";
import { supabaseBrowserClient } from "@/lib/supabase/client";

/**
* Cache Invalidation Boundaries (MIG-09)
Expand Down Expand Up @@ -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<GeneratedSolutionListItem[]> {
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,
})),
Expand Down
13 changes: 7 additions & 6 deletions packages/web/lib/server-env.ts
Original file line number Diff line number Diff line change
@@ -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");