Skip to content

Commit 86732b2

Browse files
authored
Merge pull request #122 from AparAgarwal/fix/backend-sorting-and-styles
Fix: Implement product sorting and improve dropdown functionality
2 parents 6949667 + 8c81c40 commit 86732b2

4 files changed

Lines changed: 91 additions & 77 deletions

File tree

src/api/productService.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,32 @@ export const getProducts = async (params = {}) => {
5353
if (params.sortBy) apiParams.sortBy = params.sortBy;
5454
if (params.sortOrder) apiParams.sortOrder = params.sortOrder;
5555

56-
const response = await axiosInstance.get('/products', { params: apiParams });
56+
let response;
57+
if (params.sort) {
58+
const sortKey = encodeURIComponent(params.sort);
59+
response = await axiosInstance.get(`/products/sort/${sortKey}`, { params: apiParams });
60+
} else {
61+
response = await axiosInstance.get('/products', { params: apiParams });
62+
}
5763

64+
const data = response.data;
65+
66+
if (Array.isArray(data)) {
67+
return {
68+
success: true,
69+
data: {
70+
products: data,
71+
total: data.length,
72+
page: 1,
73+
pages: 1,
74+
},
75+
status: response.status,
76+
};
77+
}
78+
5879
return {
5980
success: true,
60-
data: transformApiResponse(response.data),
81+
data: transformApiResponse(data),
6182
status: response.status,
6283
};
6384
} catch (error) {

src/components/Products/ProductCard.jsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,26 @@ const ProductCard = forwardRef(({ product }, ref) => {
171171
<div className="flex justify-between items-center">
172172
<div className="flex items-center gap-2">
173173
<div className="flex">
174-
{[...Array(5)].map((_, i) => (
175-
<svg key={i} className={`w-4 h-4 ${i < Math.floor(product.rating || 0) ? 'text-yellow-400' : 'text-gray-300'}`} fill="currentColor" viewBox="0 0 20 20">
176-
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
177-
</svg>
178-
))}
174+
{[0, 1, 2, 3, 4].map((i) => {
175+
const ratingValue = Number(product.rating) || 0;
176+
const fill = Math.max(0, Math.min(1, ratingValue - i)); // 0..1
177+
const pct = Math.round(fill * 100);
178+
return (
179+
<div key={i} className="relative w-4 h-4 mr-0.5">
180+
{/* Gray background star */}
181+
<svg className="w-4 h-4 text-gray-300 absolute inset-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden>
182+
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
183+
</svg>
184+
185+
{/* Yellow foreground star clipped to pct width */}
186+
<div className="absolute inset-0 overflow-hidden" style={{ width: `${pct}%` }} aria-hidden>
187+
<svg className="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
188+
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
189+
</svg>
190+
</div>
191+
</div>
192+
);
193+
})}
179194
</div>
180195
<span className="text-xs text-gray-500">({product.reviewCount || 0})</span>
181196
</div>

src/components/Products/SortDropdown.jsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,20 @@ export default function SortDropdown({ sortBy = 'featured', sortOrder = 'desc',
1818
option => option.field === sortBy && option.order === sortOrder
1919
) || SORT_OPTIONS[0];
2020

21-
// This hook now handles clicks outside AND page scrolling
21+
// Handle clicks outside to close the dropdown. Do NOT close on scroll — UX requires it remain open until user closes.
2222
useEffect(() => {
2323
const handleClickOutside = (event) => {
2424
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
2525
setIsOpen(false);
2626
}
2727
};
2828

29-
// A simple function to close the dropdown
30-
const handleScroll = () => {
31-
setIsOpen(false);
32-
};
33-
34-
// Add event listeners only when the dropdown is open
3529
if (isOpen) {
3630
document.addEventListener('mousedown', handleClickOutside);
37-
document.addEventListener('scroll', handleScroll, true); // Added scroll listener
3831
}
3932

40-
// Cleanup: remove both event listeners
4133
return () => {
4234
document.removeEventListener('mousedown', handleClickOutside);
43-
document.removeEventListener('scroll', handleScroll, true); // Removed scroll listener
4435
};
4536
}, [isOpen]);
4637

@@ -50,16 +41,16 @@ export default function SortDropdown({ sortBy = 'featured', sortOrder = 'desc',
5041
};
5142

5243
return (
53-
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
54-
<div className="relative flex-shrink-0">
44+
<div ref={dropdownRef} className={`relative flex items-center gap-2 sm:gap-4 ${className}`}>
45+
<div className="flex-shrink-0">
5546
<span className="font-semibold text-lg text-gray-800">Sort by:</span>
56-
<div className="absolute -bottom-1 left-0 w-12 h-0.5 bg-gray-800" />
47+
<div className="absolute left-0 w-12 h-0.5 bg-gray-800" />
5748
</div>
5849

59-
<div className="relative flex-1 sm:flex-initial" ref={dropdownRef}>
50+
<div className="flex-1 sm:flex-initial">
6051
<button
6152
onClick={() => setIsOpen(!isOpen)}
62-
className="w-full sm:w-[240px] flex items-center justify-between gap-2 bg-transparent text-gray-700 px-4 py-2 sm:px-6 sm:py-3 rounded-lg font-semibold border-2 border-gray-300 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300"
53+
className="w-full sm:w-[240px] flex items-center justify-between gap-2 bg-transparent text-gray-700 px-4 py-2 sm:px-6 sm:py-3 rounded-lg font-semibold border-2 border-gray-300 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 cursor-pointer"
6354
aria-haspopup="true"
6455
aria-expanded={isOpen}
6556
>
@@ -69,8 +60,26 @@ export default function SortDropdown({ sortBy = 'featured', sortOrder = 'desc',
6960
</svg>
7061
</button>
7162

72-
{isOpen && (
73-
<div className="absolute left-0 z-50 w-full sm:w-64 mt-2 origin-top-left sm:origin-top-right bg-white border border-gray-200 rounded-lg shadow-xl">
63+
<div
64+
className={`absolute top-0 left-0 right-0 z-20 w-full bg-white border border-gray-200 rounded-lg shadow-xl overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${
65+
isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
66+
}`}
67+
aria-hidden={!isOpen}
68+
>
69+
{/* Header with title and X close button */}
70+
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
71+
<div className="text-xs font-semibold text-gray-800 uppercase">Sort by</div>
72+
<button
73+
onClick={() => setIsOpen(false)}
74+
aria-label="Close sort menu"
75+
className="text-gray-500 hover:text-gray-800 focus:outline-none ml-2 cursor-pointer border rounded-full p-1"
76+
>
77+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
79+
</svg>
80+
</button>
81+
</div>
82+
7483
<div className="py-2">
7584
{SORT_OPTIONS.map((option, index) => (
7685
<button
@@ -80,7 +89,7 @@ export default function SortDropdown({ sortBy = 'featured', sortOrder = 'desc',
8089
option.field === sortBy && option.order === sortOrder
8190
? 'bg-blue-50 text-blue-700 font-semibold'
8291
: 'text-gray-700'
83-
}`}
92+
} cursor-pointer`}
8493
role="menuitem"
8594
>
8695
<span>{option.label}</span>
@@ -93,8 +102,8 @@ export default function SortDropdown({ sortBy = 'featured', sortOrder = 'desc',
93102
))}
94103
</div>
95104
</div>
96-
)}
97105
</div>
98106
</div>
99107
);
100-
}
108+
109+
}

src/pages/Products/Products.jsx

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -70,51 +70,7 @@ export default function Products() {
7070
});
7171
}, [allProducts, filters, maxProductPrice]);
7272

73-
// Apply client-side sorting to filtered products
74-
const sortedProducts = useMemo(() => {
75-
if (!filteredProducts || filteredProducts.length === 0) return [];
76-
77-
// shallow copy to avoid mutating original array
78-
const copy = [...filteredProducts];
79-
80-
const getString = (p, key) => (p[key] || p.name || p.title || '').toString().toLowerCase();
81-
const getNumber = (p, key) => {
82-
const val = p[key];
83-
if (val === undefined || val === null || val === '') return 0;
84-
const n = Number(val);
85-
return Number.isNaN(n) ? 0 : n;
86-
};
87-
88-
switch (`${sortBy}:${sortOrder}`) {
89-
case 'title:asc':
90-
copy.sort((a, b) => getString(a, 'name').localeCompare(getString(b, 'name')));
91-
break;
92-
case 'title:desc':
93-
copy.sort((a, b) => getString(b, 'name').localeCompare(getString(a, 'name')));
94-
break;
95-
case 'price:asc':
96-
copy.sort((a, b) => getNumber(a, 'price') - getNumber(b, 'price'));
97-
break;
98-
case 'price:desc':
99-
copy.sort((a, b) => getNumber(b, 'price') - getNumber(a, 'price'));
100-
break;
101-
case 'rating:asc':
102-
copy.sort((a, b) => getNumber(a, 'rating') - getNumber(b, 'rating'));
103-
break;
104-
case 'rating:desc':
105-
copy.sort((a, b) => getNumber(b, 'rating') - getNumber(a, 'rating'));
106-
break;
107-
case 'featured:desc':
108-
default:
109-
// Featured (placeholder) - keep backend/default ordering
110-
break;
111-
}
112-
113-
return copy;
114-
}, [filteredProducts, sortBy, sortOrder]);
115-
116-
// Simple display of filtered products with pagination/infinite scroll
117-
const displayedProducts = sortedProducts.slice(0, displayedCount);
73+
const displayedProducts = filteredProducts.slice(0, displayedCount);
11874
const hasMoreProducts = displayedCount < filteredProducts.length;
11975

12076
// Infinite scroll callback
@@ -135,7 +91,7 @@ export default function Products() {
13591
);
13692

13793
useEffect(() => {
138-
dispatch(fetchProducts({ page: 1, limit: 1000 }));
94+
dispatch(fetchProducts({ page: 1, limit: 1000, sort: 'feature' }));
13995
}, [dispatch]);
14096

14197
return (
@@ -174,7 +130,7 @@ export default function Products() {
174130
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
175131
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
176132
{/* Left: All Filters button (full width on mobile) */}
177-
<button onClick={() => setIsFilterOpen(true)} className="w-full sm:w-auto flex items-center justify-center gap-2 bg-transparent text-gray-700 px-4 py-2 sm:px-6 sm:py-3 rounded-lg font-medium border border-gray-300 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300">
133+
<button onClick={() => setIsFilterOpen(true)} className="w-full sm:w-auto flex items-center justify-center gap-2 bg-transparent text-gray-700 px-4 py-2 sm:px-6 sm:py-3 rounded-lg font-medium border border-gray-300 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 cursor-pointer">
178134
<span className="hidden sm:inline">All Filters</span>
179135
<span className="sm:hidden">Filters</span>
180136
<svg
@@ -197,11 +153,24 @@ export default function Products() {
197153
sortBy={sortBy}
198154
sortOrder={sortOrder}
199155
onSortChange={(field, order) => {
200-
// If user chooses Featured (placeholder), map empty or featured
201-
setSortBy(field || 'featured');
156+
// Update local sort state
157+
setSortBy(field || 'featured');
202158
setSortOrder(order || 'desc');
203-
// reset displayed count to show top results
204159
setDisplayedCount(12);
160+
161+
// Map selection to backend sort key
162+
// Supported backend keys: feature (default), a-z, z-a, price_asc, price_desc, rating_asc, rating_desc
163+
let sortKey = 'feature';
164+
if (field === 'title' && order === 'asc') sortKey = 'a-z';
165+
else if (field === 'title' && order === 'desc') sortKey = 'z-a';
166+
else if (field === 'price' && order === 'asc') sortKey = 'price_asc';
167+
else if (field === 'price' && order === 'desc') sortKey = 'price_desc';
168+
else if (field === 'rating' && order === 'asc') sortKey = 'rating_asc';
169+
else if (field === 'rating' && order === 'desc') sortKey = 'rating_desc';
170+
else sortKey = 'feature';
171+
172+
// Fetch products from backend with sort
173+
dispatch(fetchProducts({ page: 1, limit: 1000, sort: sortKey }));
205174
}}
206175
/>
207176
</div>

0 commit comments

Comments
 (0)