mirror of
https://gitlab.com/davidels1986/ore-cart-checkout.git
synced 2026-10-30 02:59:13 +00:00
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:
parent
7c239b648f
commit
ff6997edfe
4 changed files with 115 additions and 8 deletions
71
src/api/nocodb.ts
Normal file
71
src/api/nocodb.ts
Normal 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 {};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
36
src/hooks/useNocoProducts.ts
Normal file
36
src/hooks/useNocoProducts.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue