From ff6997edfeadbcb01899ea4c1504011bce2ba810 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Fri, 13 Mar 2026 00:45:45 +0200 Subject: [PATCH] Refactor FeaturedProducts and Products components to use custom hook - Replaced direct product data import with useNocoProducts hook in FeaturedProducts and Products components. - Updated logic to filter and display featured products based on badges. - Ensured product data is reactive to changes by including it in useEffect dependencies in Products component. --- src/api/nocodb.ts | 71 +++++++++++++++++++++++++++++ src/components/FeaturedProducts.tsx | 11 ++--- src/hooks/useNocoProducts.ts | 36 +++++++++++++++ src/pages/Products.tsx | 5 +- 4 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 src/api/nocodb.ts create mode 100644 src/hooks/useNocoProducts.ts diff --git a/src/api/nocodb.ts b/src/api/nocodb.ts new file mode 100644 index 0000000..772f271 --- /dev/null +++ b/src/api/nocodb.ts @@ -0,0 +1,71 @@ +const NOCO_BASE_URL = 'https://nocodb.apphub.co.za'; +const NOCO_BASE_ID = 'putj028icfsplze'; +const NOCO_TABLE_ID = 'mv3iljyh84g6ciy'; + +type NocoAttachment = { + url?: string; + signedUrl?: string; +}; + +type NocoRecord = { + fields: { + sku?: string; + image?: NocoAttachment[] | null; + }; +}; + +type NocoResponse = { + records?: NocoRecord[]; + list?: NocoRecord[]; +}; + +/** + * Fetch product attachment images from NocoDB and return a map + * of sku -> image URL (preferring signedUrl if present). + */ +export async function fetchNocoProductImages(): Promise> { + const apiToken = import.meta.env.VITE_NOCO_API_TOKEN; + if (!apiToken) { + console.warn('VITE_NOCO_API_TOKEN is not set; skipping NocoDB image fetch.'); + return {}; + } + + try { + const res = await fetch( + `${NOCO_BASE_URL}/api/v1/db/data/noco/${NOCO_BASE_ID}/${NOCO_TABLE_ID}?limit=200`, + { + headers: { + 'xc-auth': apiToken, + }, + }, + ); + + if (!res.ok) { + console.error('Failed to fetch NocoDB products', res.status, await res.text()); + return {}; + } + + const data = (await res.json()) as NocoResponse; + const rows = data.records ?? data.list ?? []; + + const map: Record = {}; + + for (const row of rows) { + const sku = row.fields.sku; + const images = row.fields.image ?? undefined; + if (!sku || !images || images.length === 0) continue; + + const first = images[0]; + const url = first.signedUrl ?? first.url; + if (url) { + map[sku] = url; + } + } + + return map; + } catch (err) { + console.error('Error fetching NocoDB product images', err); + return {}; + } +} + diff --git a/src/components/FeaturedProducts.tsx b/src/components/FeaturedProducts.tsx index ccdf4b3..0874841 100644 --- a/src/components/FeaturedProducts.tsx +++ b/src/components/FeaturedProducts.tsx @@ -1,4 +1,4 @@ -import { products } from '@/data/products'; +import { useNocoProducts } from '@/hooks/useNocoProducts'; import { Button } from '@/components/ui/button'; import { Link } from 'react-router-dom'; import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react'; @@ -11,13 +11,12 @@ import { CarouselPrevious, } from '@/components/ui/carousel'; -// Get featured products (those with badges or first 8) -const featuredProducts = products - .filter(p => p.badge === 'hot' || p.badge === 'new') - .slice(0, 8); - export function FeaturedProducts() { const { addItem } = useCart(); + const products = useNocoProducts(); + const featuredProducts = products + .filter(p => p.badge === 'hot' || p.badge === 'new') + .slice(0, 8); return (
diff --git a/src/hooks/useNocoProducts.ts b/src/hooks/useNocoProducts.ts new file mode 100644 index 0000000..406f969 --- /dev/null +++ b/src/hooks/useNocoProducts.ts @@ -0,0 +1,36 @@ +import { useEffect, useMemo, useState } from 'react'; +import { products as localProducts } from '@/data/products'; +import type { Product } from '@/types'; +import { fetchNocoProductImages } from '@/api/nocodb'; + +export function useNocoProducts(): Product[] { + const [imageMap, setImageMap] = useState>({}); + + useEffect(() => { + let cancelled = false; + + fetchNocoProductImages().then((map) => { + if (!cancelled) { + setImageMap(map); + } + }); + + return () => { + cancelled = true; + }; + }, []); + + const merged = useMemo(() => { + if (!imageMap || Object.keys(imageMap).length === 0) { + return localProducts; + } + + return localProducts.map((p) => { + const imageUrl = imageMap[p.sku]; + return imageUrl ? { ...p, imageUrl } : p; + }); + }, [imageMap]); + + return merged; +} + diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index a458e48..30e0781 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom'; import { SearchBar } from '@/components/SearchBar'; import { CategorySidebar } from '@/components/CategorySidebar'; import { ProductCard } from '@/components/ProductCard'; -import { products } from '@/data/products'; +import { useNocoProducts } from '@/hooks/useNocoProducts'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Filter } from 'lucide-react'; @@ -18,6 +18,7 @@ const Products = () => { const [searchQuery, setSearchQuery] = useState(''); const [filterOpen, setFilterOpen] = useState(false); const isMobile = useIsMobile(); + const products = useNocoProducts(); // Sync category from URL params useEffect(() => { @@ -46,7 +47,7 @@ const Products = () => { ], threshold: 0.4, includeScore: true, - }), []); + }), [products]); const displayProducts = useMemo(() => { let filtered = products;