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
69 changes: 67 additions & 2 deletions oreel-web/app/(shop)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string,string> }){
// 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 (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-4">Search results</h1>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-1">
<Suspense fallback={<div className="p-4">Loading filters…</div>}>
<Filters brands={data.brands} />
</Suspense>
</div>
<div className="lg:col-span-3">
{data.total === 0 ? (
<div className="p-6 text-center">No products found. Try clearing filters.</div>
) : (
<>
<ProductList products={data.products} />
<div className="mt-6 flex items-center justify-center gap-2">
{Array.from({length: Math.ceil(data.total / data.limit)}, (_,i)=> i+1).map(p => (
<a key={p} href={`?page=${p}`} className="px-3 py-1 border rounded">{p}</a>
))}
</div>
</>
)}
</div>
</div>
</div>
)
}
44 changes: 44 additions & 0 deletions oreel-web/app/api/products/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
73 changes: 73 additions & 0 deletions oreel-web/app/category/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string,string> }

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 (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-4">Category: {slug}</h1>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-1">
<Suspense fallback={<div className="p-4">Loading filters…</div>}>
<Filters brands={data.brands} />
</Suspense>
</div>
<div className="lg:col-span-3">
{data.total === 0 ? (
<div className="p-6 text-center">No products found. Try resetting filters.</div>
) : (
<>
<ProductList products={data.products} />
<div className="mt-6 flex items-center justify-center gap-2">
{Array.from({length: Math.ceil(data.total / data.limit)}, (_,i)=> i+1).map(p => (
<a key={p} href={`?page=${p}`} className="px-3 py-1 border rounded">{p}</a>
))}
</div>
</>
)}
</div>
</div>
</div>
)
}
82 changes: 82 additions & 0 deletions oreel-web/components/filters/Filters.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>((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 (
<aside className="p-4 border rounded bg-white">
<h4 className="font-semibold mb-2">Filters</h4>
<div className="mb-3">
<label className="block text-xs text-gray-700">Min price</label>
<input value={minPrice} onChange={e=>setMinPrice(e.target.value)} className="w-full px-2 py-1 border rounded" />
</div>
<div className="mb-3">
<label className="block text-xs text-gray-700">Max price</label>
<input value={maxPrice} onChange={e=>setMaxPrice(e.target.value)} className="w-full px-2 py-1 border rounded" />
</div>

<div className="mb-3">
<div className="text-xs text-gray-700 mb-1">Brand</div>
{brands.map(b => (
<label key={b} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={selectedBrands.includes(b)} onChange={()=>toggleBrand(b)} /> {b}</label>
))}
</div>

<div className="mb-3">
<div className="text-xs text-gray-700 mb-1">Rating</div>
<select value={rating} onChange={e=>setRating(e.target.value)} className="w-full border rounded px-2 py-1">
<option value="">Any</option>
<option value="4">4+ stars</option>
<option value="3">3+ stars</option>
</select>
</div>

<div className="flex gap-2">
<button onClick={apply} className="px-3 py-1 bg-primary text-white rounded">Apply</button>
<button onClick={reset} className="px-3 py-1 border rounded">Reset</button>
</div>
</aside>
)
}
18 changes: 18 additions & 0 deletions oreel-web/components/products/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from 'next/link'

export default function ProductCard({ product }: { product: any }){
return (
<article className="border rounded overflow-hidden bg-white">
<div className="w-full h-44 bg-gray-100 flex items-center justify-center">{product.image || <span className="text-gray-400">Image</span>}</div>
<div className="p-3">
<h3 className="text-sm font-semibold text-gray-900">{product.title}</h3>
<div className="text-xs text-gray-600">{product.brand}</div>
<div className="mt-2 flex items-center justify-between">
<div className="font-bold text-gray-900">${product.price.toFixed(2)}</div>
<Link href={`/products/${product.slug}`} className="text-primary text-sm">View</Link>
</div>
<div className="mt-2 text-sm text-yellow-600">⭐ {product.rating} <span className="text-xs text-gray-500">({product.reviews})</span></div>
</div>
</article>
)
}
11 changes: 11 additions & 0 deletions oreel-web/components/products/ProductList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ProductCard from './ProductCard'

export default function ProductList({ products }: { products: any[] }){
return (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
)
}
14 changes: 8 additions & 6 deletions oreel-web/data/mock-products.ts
Original file line number Diff line number Diff line change
@@ -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
Loading