diff --git a/oreel-web/app/(shop)/search/page.tsx b/oreel-web/app/(shop)/search/page.tsx index 1b42a41..0739249 100644 --- a/oreel-web/app/(shop)/search/page.tsx +++ b/oreel-web/app/(shop)/search/page.tsx @@ -1,3 +1,68 @@ -export default function SearchPage() { - return null +import React, { Suspense } from 'react' +import ProductList from '../../../components/products/ProductList' +import Filters from '../../../components/filters/Filters' + +function uniqueBrands(list: any[]) { return Array.from(new Set(list.map((p: any) => p.brand))) } + +export default async function SearchPage({ searchParams }: { searchParams?: Record }){ + // If an external API base is configured, call it. Otherwise use local mock data. + let data: any + if (process.env.NEXT_PUBLIC_API_URL) { + const apiBase = process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '') + const url = new URL(apiBase + '/api/products') + if (searchParams) Object.entries(searchParams).forEach(([k,v])=>{ if(v) url.searchParams.set(k,v) }) + const res = await fetch(url.toString(), { next: { revalidate: 60 } }) + data = await res.json() + } else { + const list = (await import('../../../data/mock-products')).default as any[] + let filtered = list.slice() + const category = null + const search = searchParams?.q || searchParams?.search || '' + const minPrice = searchParams?.minPrice ? parseFloat(searchParams.minPrice) : null + const maxPrice = searchParams?.maxPrice ? parseFloat(searchParams.maxPrice) : null + const brands = searchParams?.brands ? searchParams.brands.split(',') : null + const rating = searchParams?.rating ? parseFloat(searchParams.rating) : null + const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1 + const limit = searchParams?.limit ? parseInt(searchParams.limit, 10) : 12 + + if (category) filtered = filtered.filter((p: any) => p.category === category) + if (search) filtered = filtered.filter((p: any) => p.title.toLowerCase().includes((search as string).toLowerCase()) || p.brand.toLowerCase().includes((search as string).toLowerCase())) + if (minPrice !== null) filtered = filtered.filter((p: any) => p.price >= minPrice) + if (maxPrice !== null) filtered = filtered.filter((p: any) => p.price <= maxPrice) + if (brands && brands.length) filtered = filtered.filter((p: any) => brands.includes(p.brand)) + if (rating !== null) filtered = filtered.filter((p: any) => p.rating >= rating) + + const total = filtered.length + const start = (page - 1) * limit + const pageItems = filtered.slice(start, start + limit) + + data = { products: pageItems, total, page, limit, brands: uniqueBrands(list) } + } + + return ( +
+

Search results

+
+
+ Loading filters…
}> + + +
+
+ {data.total === 0 ? ( +
No products found. Try clearing filters.
+ ) : ( + <> + +
+ {Array.from({length: Math.ceil(data.total / data.limit)}, (_,i)=> i+1).map(p => ( + {p} + ))} +
+ + )} +
+
+ + ) } diff --git a/oreel-web/app/api/products/route.ts b/oreel-web/app/api/products/route.ts new file mode 100644 index 0000000..c4f4832 --- /dev/null +++ b/oreel-web/app/api/products/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server' +import products from '../../../data/mock-products' + +function uniqueBrands(list: any[]): string[] { + return Array.from(new Set(list.map((p: any) => p.brand))) +} + +export async function GET(request: Request) { + const url = new URL(request.url) + const params = url.searchParams + + const category = params.get('category') + const search = params.get('search') + const minPrice = params.get('minPrice') ? parseFloat(params.get('minPrice') as string) : null + const maxPrice = params.get('maxPrice') ? parseFloat(params.get('maxPrice') as string) : null + const brands = params.get('brands') ? params.get('brands')!.split(',') : null + const rating = params.get('rating') ? parseFloat(params.get('rating') as string) : null + const page = params.get('page') ? parseInt(params.get('page') as string, 10) : 1 + const limit = params.get('limit') ? parseInt(params.get('limit') as string, 10) : 12 + + let filtered = (products as any[]).slice() + + if (category) filtered = filtered.filter((p: any) => p.category === category) + if (search) filtered = filtered.filter((p: any) => p.title.toLowerCase().includes(search.toLowerCase()) || p.brand.toLowerCase().includes(search.toLowerCase())) + if (minPrice !== null) filtered = filtered.filter((p: any) => p.price >= minPrice) + if (maxPrice !== null) filtered = filtered.filter((p: any) => p.price <= maxPrice) + if (brands && brands.length) filtered = filtered.filter((p: any) => brands.includes(p.brand)) + if (rating !== null) filtered = filtered.filter((p: any) => p.rating >= rating) + + const total = filtered.length + const start = (page - 1) * limit + const end = start + limit + const pageItems = filtered.slice(start, end) + + const res = { + products: pageItems, + total, + page, + limit, + brands: uniqueBrands(products), + } + + return NextResponse.json(res, { status: 200 }) +} diff --git a/oreel-web/app/category/[slug]/page.tsx b/oreel-web/app/category/[slug]/page.tsx new file mode 100644 index 0000000..e012b08 --- /dev/null +++ b/oreel-web/app/category/[slug]/page.tsx @@ -0,0 +1,73 @@ +import React, { Suspense } from 'react' +import ProductList from '../../../components/products/ProductList' +import Filters from '../../../components/filters/Filters' + +type Props = { params: { slug: string }, searchParams?: Record } + +function uniqueBrands(list: any[]) { return Array.from(new Set(list.map((p: any) => p.brand))) } + +export default async function CategoryPage({ params, searchParams }: Props){ + const slug = params.slug + let data: any + + if (process.env.NEXT_PUBLIC_API_URL) { + const apiBase = process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '') + const url = new URL(apiBase + '/api/products') + url.searchParams.set('category', slug) + if(searchParams){ + Object.entries(searchParams).forEach(([k,v])=>{ if(v) url.searchParams.set(k,v) }) + } + const res = await fetch(url.toString(), { next: { revalidate: 60 } }) + data = await res.json() + } else { + const list = (await import('../../../data/mock-products')).default as any[] + let filtered = list.slice() + if (slug) filtered = filtered.filter((p: any) => p.category === slug) + const search = searchParams?.q || searchParams?.search || '' + const minPrice = searchParams?.minPrice ? parseFloat(searchParams.minPrice) : null + const maxPrice = searchParams?.maxPrice ? parseFloat(searchParams.maxPrice) : null + const brands = searchParams?.brands ? searchParams.brands.split(',') : null + const rating = searchParams?.rating ? parseFloat(searchParams.rating) : null + const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1 + const limit = searchParams?.limit ? parseInt(searchParams.limit, 10) : 12 + + if (search) filtered = filtered.filter((p: any) => p.title.toLowerCase().includes((search as string).toLowerCase()) || p.brand.toLowerCase().includes((search as string).toLowerCase())) + if (minPrice !== null) filtered = filtered.filter((p: any) => p.price >= minPrice) + if (maxPrice !== null) filtered = filtered.filter((p: any) => p.price <= maxPrice) + if (brands && brands.length) filtered = filtered.filter((p: any) => brands.includes(p.brand)) + if (rating !== null) filtered = filtered.filter((p: any) => p.rating >= rating) + + const total = filtered.length + const start = (page - 1) * limit + const pageItems = filtered.slice(start, start + limit) + + data = { products: pageItems, total, page, limit, brands: uniqueBrands(list) } + } + + return ( +
+

Category: {slug}

+
+
+ Loading filters…
}> + + +
+
+ {data.total === 0 ? ( +
No products found. Try resetting filters.
+ ) : ( + <> + +
+ {Array.from({length: Math.ceil(data.total / data.limit)}, (_,i)=> i+1).map(p => ( + {p} + ))} +
+ + )} +
+
+ + ) +} diff --git a/oreel-web/components/filters/Filters.tsx b/oreel-web/components/filters/Filters.tsx new file mode 100644 index 0000000..f6f332d --- /dev/null +++ b/oreel-web/components/filters/Filters.tsx @@ -0,0 +1,82 @@ +"use client" +import React, { useState, useEffect } from 'react' +import { useRouter, usePathname, useSearchParams } from 'next/navigation' + +export default function Filters({ brands = [] }: { brands?: string[] }){ + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + + const getParam = (k: string) => searchParams?.get(k) ?? '' + const [minPrice, setMinPrice] = useState(getParam('minPrice') || '') + const [maxPrice, setMaxPrice] = useState(getParam('maxPrice') || '') + const [selectedBrands, setSelectedBrands] = useState((getParam('brands') || '').split(',').filter(Boolean)) + const [rating, setRating] = useState(getParam('rating') || '') + + useEffect(()=>{ + setSelectedBrands((getParam('brands') || '').split(',').filter(Boolean)) + },[searchParams]) + + function apply(){ + const params = new URLSearchParams(Array.from(searchParams || [] as any)) + if(minPrice) params.set('minPrice', minPrice) + else params.delete('minPrice') + if(maxPrice) params.set('maxPrice', maxPrice) + else params.delete('maxPrice') + if(selectedBrands.length) params.set('brands', selectedBrands.join(',')) + else params.delete('brands') + if(rating) params.set('rating', rating) + else params.delete('rating') + params.delete('page') + router.push(pathname + '?' + params.toString()) + } + + function reset(){ + const params = new URLSearchParams(Array.from(searchParams || [] as any)) + params.delete('minPrice') + params.delete('maxPrice') + params.delete('brands') + params.delete('rating') + params.delete('page') + router.push(pathname + '?' + params.toString()) + } + + function toggleBrand(b: string){ + setSelectedBrands(prev => prev.includes(b) ? prev.filter(x=>x!==b) : [...prev, b]) + } + + return ( + + ) +} diff --git a/oreel-web/components/products/ProductCard.tsx b/oreel-web/components/products/ProductCard.tsx new file mode 100644 index 0000000..3885359 --- /dev/null +++ b/oreel-web/components/products/ProductCard.tsx @@ -0,0 +1,18 @@ +import Link from 'next/link' + +export default function ProductCard({ product }: { product: any }){ + return ( +
+
{product.image || Image}
+
+

{product.title}

+
{product.brand}
+
+
${product.price.toFixed(2)}
+ View +
+
⭐ {product.rating} ({product.reviews})
+
+
+ ) +} diff --git a/oreel-web/components/products/ProductList.tsx b/oreel-web/components/products/ProductList.tsx new file mode 100644 index 0000000..144a28c --- /dev/null +++ b/oreel-web/components/products/ProductList.tsx @@ -0,0 +1,11 @@ +import ProductCard from './ProductCard' + +export default function ProductList({ products }: { products: any[] }){ + return ( +
+ {products.map(p => ( + + ))} +
+ ) +} diff --git a/oreel-web/data/mock-products.ts b/oreel-web/data/mock-products.ts index a5554cd..1e343b6 100644 --- a/oreel-web/data/mock-products.ts +++ b/oreel-web/data/mock-products.ts @@ -1,10 +1,12 @@ const products = [ - { id: 'p1', title: 'Radiant Lipstick', brand: 'Oreel Beauty', price: '12.00', slug: 'radiant-lipstick' }, - { id: 'p2', title: 'Glow Foundation', brand: 'Oreel Beauty', price: '28.00', slug: 'glow-foundation' }, - { id: 'p3', title: 'Velvet Eyeshadow', brand: 'Oreel Beauty', price: '18.00', slug: 'velvet-eyeshadow' }, - { id: 'p4', title: 'Silk Scarf', brand: 'Oreel Fashion', price: '35.00', slug: 'silk-scarf' }, - { id: 'p5', title: 'Classic Perfume', brand: 'Oreel Fragrance', price: '45.00', slug: 'classic-perfume' }, - { id: 'p6', title: 'Hydrating Serum', brand: 'Oreel Beauty', price: '22.00', slug: 'hydrating-serum' }, + { id: 'p1', title: 'Radiant Lipstick', brand: 'Oreel Beauty', price: 12.0, slug: 'radiant-lipstick', category: 'makeup', rating: 4.5, reviews: 24, image: '' }, + { id: 'p2', title: 'Glow Foundation', brand: 'Oreel Beauty', price: 28.0, slug: 'glow-foundation', category: 'makeup', rating: 4.2, reviews: 12, image: '' }, + { id: 'p3', title: 'Velvet Eyeshadow', brand: 'Oreel Beauty', price: 18.0, slug: 'velvet-eyeshadow', category: 'makeup', rating: 4.7, reviews: 40, image: '' }, + { id: 'p4', title: 'Silk Scarf', brand: 'Oreel Fashion', price: 35.0, slug: 'silk-scarf', category: 'accessories', rating: 4.1, reviews: 8, image: '' }, + { id: 'p5', title: 'Classic Perfume', brand: 'Oreel Fragrance', price: 45.0, slug: 'classic-perfume', category: 'fragrance', rating: 4.8, reviews: 66, image: '' }, + { id: 'p6', title: 'Hydrating Serum', brand: 'Oreel Beauty', price: 22.0, slug: 'hydrating-serum', category: 'skincare', rating: 4.3, reviews: 19, image: '' }, + { id: 'p7', title: 'Matte Lipstick', brand: 'Oreel Beauty', price: 14.0, slug: 'matte-lipstick', category: 'makeup', rating: 4.0, reviews: 5, image: '' }, + { id: 'p8', title: 'Night Cream', brand: 'Oreel Skin', price: 30.0, slug: 'night-cream', category: 'skincare', rating: 3.9, reviews: 3, image: '' }, ] export default products