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.
This commit is contained in:
SJCoetz 2026-03-13 00:45:45 +02:00
parent 7c239b648f
commit ff6997edfe
4 changed files with 115 additions and 8 deletions

71
src/api/nocodb.ts Normal file
View file

@ -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<Record<string, string>> {
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<string, string> = {};
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 {};
}
}

View file

@ -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 (
<div>

View file

@ -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<Record<string, string>>({});
useEffect(() => {
let cancelled = false;
fetchNocoProductImages().then((map) => {
if (!cancelled) {
setImageMap(map);
}
});
return () => {
cancelled = true;
};
}, []);
const merged = useMemo<Product[]>(() => {
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;
}

View file

@ -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;