added sorting
This commit is contained in:
@@ -44,7 +44,7 @@ export const categoriesApi = baseApi.injectEndpoints({
|
||||
|
||||
getAllCategoryProductsPaginated: builder.query({
|
||||
async queryFn(
|
||||
{ category, page = 1, limit = 6, brands, min_price, max_price },
|
||||
{ category, page = 1, limit = 6, brands, min_price, max_price, sorting },
|
||||
queryApi,
|
||||
extraOptions,
|
||||
baseQuery
|
||||
@@ -65,6 +65,7 @@ export const categoriesApi = baseApi.injectEndpoints({
|
||||
if (brands) params.append('brands', brands);
|
||||
if (min_price) params.append('min_price', min_price);
|
||||
if (max_price) params.append('max_price', max_price);
|
||||
if (sorting) params.append('sorting', sorting);
|
||||
|
||||
const result = await baseQuery(
|
||||
`categories/${categoryId}/products?${params.toString()}`
|
||||
|
||||
@@ -30,14 +30,14 @@ export const collectionsApi = baseApi.injectEndpoints({
|
||||
}),
|
||||
|
||||
getCollectionProductsPaginated: builder.query({
|
||||
query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price, sorting }) => {
|
||||
query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price, sorting = "price_amount-ascending" }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page);
|
||||
if (limit) params.append('limit', limit);
|
||||
if (brands) params.append('brands', brands);
|
||||
if (min_price) params.append('min_price', min_price);
|
||||
if (max_price) params.append('max_price', max_price);
|
||||
if (sorting) params.append('sorting', sorting);
|
||||
params.append('sorting', sorting);
|
||||
|
||||
return `/collections/${collectionId}/products?${params.toString()}`;
|
||||
},
|
||||
|
||||
@@ -172,6 +172,16 @@ export default {
|
||||
price: "Price",
|
||||
minPrice: "Min Price",
|
||||
maxPrice: "Max Price",
|
||||
priceHighToLow: "From expensive to cheap",
|
||||
priceLowToHigh: "From cheap to expensive",
|
||||
priceRange: "Price Range",
|
||||
under50: "Under 50m",
|
||||
under100: "Under 100m",
|
||||
from50to200: "50 - 200",
|
||||
from200to500: "200 - 500",
|
||||
from500to1000: "500 - 1000",
|
||||
over1000: "Over 1000m",
|
||||
sortBy: "Sort By",
|
||||
},
|
||||
product: {
|
||||
productCode: "Product code",
|
||||
|
||||
@@ -167,8 +167,18 @@ export default {
|
||||
From_expensive_to_cheap: "От дорогих к дешевым",
|
||||
From_cheap_to_expensive: "От дешевых к дорогим",
|
||||
price: "Цена",
|
||||
minPrice: "Минимальная цена",
|
||||
maxPrice: "Максимальная цена",
|
||||
minPrice: "Мин цена",
|
||||
maxPrice: "Макс цена",
|
||||
priceHighToLow: "От дорогих к дешевым",
|
||||
priceLowToHigh: "От дешевых к дорогим",
|
||||
priceRange: "Диапазон цен",
|
||||
under50: "До 50m",
|
||||
under100: "До 100m",
|
||||
from50to200: "50 - 200",
|
||||
from200to500: "200 - 500",
|
||||
from500to1000: "500 - 1000",
|
||||
over1000: "Более 1000m",
|
||||
sortBy: "Сортировать по",
|
||||
},
|
||||
product: {
|
||||
productCode: "Код товара",
|
||||
|
||||
@@ -170,8 +170,18 @@ export default {
|
||||
From_expensive_to_cheap: "Gymmatdan arzana",
|
||||
From_cheap_to_expensive: "Arzandan gymmada",
|
||||
price: "Bahasy",
|
||||
maxPrice: "Maksimum baha",
|
||||
minPrice: "Minimum baha",
|
||||
maxPrice: "Maks baha",
|
||||
minPrice: "Min baha",
|
||||
priceHighToLow: "Gymmatdan arzana",
|
||||
priceLowToHigh: "Arzandan gymmada",
|
||||
priceRange: "Baha diapazony",
|
||||
under50: "50m aşagynda",
|
||||
under100: "100m aşagynda",
|
||||
from50to200: "50 - 200",
|
||||
from200to500: "200 - 500",
|
||||
from500to1000: "500 - 1000",
|
||||
over1000: "1000m dan ýokary",
|
||||
sortBy: "Tertiplemek",
|
||||
},
|
||||
product: {
|
||||
productCode: "Haryt kody",
|
||||
|
||||
@@ -1,28 +1,96 @@
|
||||
.sortingSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.sortingTitle {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.sortingButtonsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.sortingBtn {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #d32824;
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
&.activeSorting {
|
||||
background-color: #d32824;
|
||||
color: #fff;
|
||||
border-color: #d32824;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sortingContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.sortingLabel {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.sortingSelect {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
|
||||
|
||||
.pricePresetsContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.pricePresetBtn {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
font-size: 15px;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
border-color: #d32824;
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
.sortingSelect:focus {
|
||||
border-color: #6c63ff;
|
||||
|
||||
&.activePreset {
|
||||
background-color: #d32824;
|
||||
color: #fff;
|
||||
border-color: #d32824;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mobilePhoneGrid {
|
||||
display: flex !important;
|
||||
@@ -32,41 +100,64 @@
|
||||
// Price Filter Styles
|
||||
.priceFilterContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 12px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
animation: slideDown 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.priceInputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.priceLabel {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.priceInput {
|
||||
width: 90px;
|
||||
padding: 6px 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.priceInput:focus {
|
||||
border-color: #6c63ff;
|
||||
border-color: #d32824;
|
||||
box-shadow: 0 0 0 3px rgba(211, 40, 36, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.priceDivider {
|
||||
font-size: 18px;
|
||||
color: #aaa;
|
||||
font-weight: bold;
|
||||
margin: 0 6px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filtersContainer{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TiTick } from "react-icons/ti";
|
||||
import { Divider } from "antd";
|
||||
import styles from "../CategoryPage.module.scss";
|
||||
|
||||
const CategoryFilters = ({
|
||||
@@ -24,6 +25,25 @@ const CategoryFilters = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pricePresets = [
|
||||
{ label: t("category.under50"), min: 0, max: 50 },
|
||||
{ label: t("category.under100"), min: 0, max: 100 },
|
||||
{ label: t("category.from50to200"), min: 50, max: 200 },
|
||||
{ label: t("category.from200to500"), min: 200, max: 500 },
|
||||
{ label: t("category.from500to1000"), min: 500, max: 1000 },
|
||||
{ label: t("category.over1000"), min: 1000, max: 999999 },
|
||||
];
|
||||
|
||||
const handlePricePreset = (preset) => {
|
||||
onMinPriceChange(preset.min.toString());
|
||||
onMaxPriceChange(preset.max.toString());
|
||||
};
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "price_amount-ascending", label: t("category.priceLowToHigh") },
|
||||
{ value: "price_amount-descending", label: t("category.priceHighToLow") },
|
||||
];
|
||||
|
||||
if (searchQuery) return null;
|
||||
|
||||
return (
|
||||
@@ -100,6 +120,23 @@ const CategoryFilters = ({
|
||||
)}
|
||||
<div className={styles.filterSection}>
|
||||
<h3>{t("category.price")}</h3>
|
||||
|
||||
<div className={styles.pricePresetsContainer}>
|
||||
{pricePresets.map((preset, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className={`${styles.pricePresetBtn} ${
|
||||
minPrice === preset.min.toString() && maxPrice === preset.max.toString()
|
||||
? styles.activePreset
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handlePricePreset(preset)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.priceFilterContainer}>
|
||||
<div className={styles.priceInputGroup}>
|
||||
<span className={styles.priceLabel}>{t("category.minPrice")}</span>
|
||||
@@ -125,18 +162,22 @@ const CategoryFilters = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Sıralama dropdown'u */}
|
||||
<div className={styles.sortingContainer} style={{ marginTop: 12 }}>
|
||||
<label htmlFor="sortingSidebar" className={styles.sortingLabel}>{t("category.sortBy")}: </label>
|
||||
<select
|
||||
id="sortingSidebar"
|
||||
className={styles.sortingSelect}
|
||||
value={sorting}
|
||||
onChange={e => onSortingChange(e.target.value)}
|
||||
|
||||
<Divider style={{ margin: "12px 0" }} />
|
||||
|
||||
<div className={styles.sortingSection}>
|
||||
<h4 className={styles.sortingTitle}>{t("category.sortBy")}</h4>
|
||||
<div className={styles.sortingButtonsContainer}>
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
className={`${styles.sortingBtn} ${sorting === option.value ? styles.activeSorting : ""}`}
|
||||
onClick={() => onSortingChange(option.value)}
|
||||
>
|
||||
<option value="price_amount-descending">{t("category.priceHighToLow")}</option>
|
||||
<option value="price_amount-ascending">{t("category.priceLowToHigh")}</option>
|
||||
</select>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -83,6 +83,7 @@ const useCategoryProducts = ({
|
||||
min_price: minPrice || undefined,
|
||||
max_price: maxPrice || undefined,
|
||||
brands: selectedFilterBrand || undefined,
|
||||
sorting: sorting || undefined,
|
||||
},
|
||||
{
|
||||
skip: !shouldUseBaseQuery,
|
||||
|
||||
@@ -29,7 +29,7 @@ const CategoryPage = () => {
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
sorting: "price_amount-descending",
|
||||
sorting: "price_amount-ascending",
|
||||
});
|
||||
|
||||
const [filterState, setFilterState] = useState({
|
||||
@@ -128,6 +128,7 @@ const CategoryPage = () => {
|
||||
}, [
|
||||
routeKey,
|
||||
location.state?.clearFilters,
|
||||
location.pathname,
|
||||
navigate,
|
||||
setAllProducts,
|
||||
setHasMore,
|
||||
@@ -188,7 +189,7 @@ const CategoryPage = () => {
|
||||
selectedFilterBrand: null,
|
||||
}));
|
||||
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
|
||||
@@ -214,7 +215,7 @@ const CategoryPage = () => {
|
||||
selectedFilterBrand: brandId,
|
||||
}));
|
||||
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
};
|
||||
@@ -295,22 +296,6 @@ const CategoryPage = () => {
|
||||
>
|
||||
{t("category.filter")} <LuFilter />
|
||||
</button>
|
||||
<div className={styles.sortingContainer}>
|
||||
<label htmlFor="sorting" className={styles.sortingLabel}>{t("category.sortBy")}: </label>
|
||||
<select
|
||||
id="sorting"
|
||||
className={styles.sortingSelect}
|
||||
value={pageState.sorting}
|
||||
onChange={e => {
|
||||
setPageState(prev => ({ ...prev, sorting: e.target.value, currentPage: 1 }));
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
}}
|
||||
>
|
||||
<option value="price_amount-descending">{t("category.priceHighToLow")}</option>
|
||||
<option value="price_amount-ascending">{t("category.priceLowToHigh")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
|
||||
@@ -323,11 +323,22 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
height: 125px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to top, #111 0%, #fff 100%);
|
||||
opacity: 0.7;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.95) 0%,
|
||||
rgba(0, 0, 0, 0.7) 30%,
|
||||
rgba(0, 0, 0, 0.3) 70%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
.descriptionCardCollapsed::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.productDescriptionCollapsed + div {
|
||||
@@ -391,18 +402,23 @@
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
padding: 8px 0 0 0;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
text-shadow: 0 1px 8px #000, 0 1px 1px #000;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
transition: all 150ms ease;
|
||||
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8), 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.85;
|
||||
opacity: 1;
|
||||
text-shadow: 0 3px 10px rgba(0, 0, 0, 0.95), 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ const ProductPage = ({
|
||||
|
||||
{/* Description card */}
|
||||
{product.description && (
|
||||
<div className={styles.descriptionCard}>
|
||||
<div className={`${styles.descriptionCard} ${!isDescExpanded ? styles.descriptionCardCollapsed : ''}`}>
|
||||
<div className={styles.descriptionHeader}>
|
||||
<div className={styles.descriptionIcon}>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
Reference in New Issue
Block a user