added sorting

This commit is contained in:
@jcarymuhammedow
2026-04-18 11:05:39 +05:00
parent 6ef7aa3c47
commit a2298dfa9c
11 changed files with 248 additions and 83 deletions

View File

@@ -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()}`

View File

@@ -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()}`;
},

View File

@@ -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",

View File

@@ -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: "Код товара",

View File

@@ -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",

View File

@@ -1,29 +1,97 @@
.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 {
.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;
}
.sortingSelect:focus {
border-color: #6c63ff;
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;
}
&.activePreset {
background-color: #d32824;
color: #fff;
border-color: #d32824;
font-weight: 600;
}
}
}
.mobilePhoneGrid {
display: flex !important;
flex-direction: column;
@@ -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{

View File

@@ -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>

View File

@@ -83,6 +83,7 @@ const useCategoryProducts = ({
min_price: minPrice || undefined,
max_price: maxPrice || undefined,
brands: selectedFilterBrand || undefined,
sorting: sorting || undefined,
},
{
skip: !shouldUseBaseQuery,

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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">