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({ getAllCategoryProductsPaginated: builder.query({
async queryFn( async queryFn(
{ category, page = 1, limit = 6, brands, min_price, max_price }, { category, page = 1, limit = 6, brands, min_price, max_price, sorting },
queryApi, queryApi,
extraOptions, extraOptions,
baseQuery baseQuery
@@ -65,6 +65,7 @@ export const categoriesApi = baseApi.injectEndpoints({
if (brands) params.append('brands', brands); if (brands) params.append('brands', brands);
if (min_price) params.append('min_price', min_price); if (min_price) params.append('min_price', min_price);
if (max_price) params.append('max_price', max_price); if (max_price) params.append('max_price', max_price);
if (sorting) params.append('sorting', sorting);
const result = await baseQuery( const result = await baseQuery(
`categories/${categoryId}/products?${params.toString()}` `categories/${categoryId}/products?${params.toString()}`

View File

@@ -30,14 +30,14 @@ export const collectionsApi = baseApi.injectEndpoints({
}), }),
getCollectionProductsPaginated: builder.query({ 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(); const params = new URLSearchParams();
params.append('page', page); params.append('page', page);
if (limit) params.append('limit', limit); if (limit) params.append('limit', limit);
if (brands) params.append('brands', brands); if (brands) params.append('brands', brands);
if (min_price) params.append('min_price', min_price); if (min_price) params.append('min_price', min_price);
if (max_price) params.append('max_price', max_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()}`; return `/collections/${collectionId}/products?${params.toString()}`;
}, },

View File

@@ -172,7 +172,17 @@ export default {
price: "Price", price: "Price",
minPrice: "Min Price", minPrice: "Min Price",
maxPrice: "Max 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: { product: {
productCode: "Product code", productCode: "Product code",
barCode: "Barcode", barCode: "Barcode",

View File

@@ -167,9 +167,19 @@ export default {
From_expensive_to_cheap: "От дорогих к дешевым", From_expensive_to_cheap: "От дорогих к дешевым",
From_cheap_to_expensive: "От дешевых к дорогим", From_cheap_to_expensive: "От дешевых к дорогим",
price: "Цена", price: "Цена",
minPrice: "Минимальная цена", minPrice: "Мин цена",
maxPrice: "Максимальная цена", maxPrice: "Макс цена",
}, priceHighToLow: "От дорогих к дешевым",
priceLowToHigh: "От дешевых к дорогим",
priceRange: "Диапазон цен",
under50: "До 50m",
under100: "До 100m",
from50to200: "50 - 200",
from200to500: "200 - 500",
from500to1000: "500 - 1000",
over1000: "Более 1000m",
sortBy: "Сортировать по",
},
product: { product: {
productCode: "Код товара", productCode: "Код товара",
barCode: "Штрих-код", barCode: "Штрих-код",

View File

@@ -170,9 +170,19 @@ export default {
From_expensive_to_cheap: "Gymmatdan arzana", From_expensive_to_cheap: "Gymmatdan arzana",
From_cheap_to_expensive: "Arzandan gymmada", From_cheap_to_expensive: "Arzandan gymmada",
price: "Bahasy", price: "Bahasy",
maxPrice: "Maksimum baha", maxPrice: "Maks baha",
minPrice: "Minimum 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: { product: {
productCode: "Haryt kody", productCode: "Haryt kody",
barCode: "Çyzgyç kod", barCode: "Çyzgyç kod",

View File

@@ -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 { .sortingContainer {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-left: 16px; margin-left: 16px;
.sortingLabel {
font-size: 14px;
color: #888;
}
} }
.sortingLabel {
font-size: 14px;
color: #888; .pricePresetsContainer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 12px;
.pricePresetBtn {
padding: 7px 10px;
border: 1px solid #d1d5db;
border-radius: 5px;
background: #fff;
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;
}
}
} }
.sortingSelect {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #d1d5db;
font-size: 15px;
background: #fff;
color: #222;
outline: none;
transition: border-color 0.2s;
}
.sortingSelect:focus {
border-color: #6c63ff;
}
.mobilePhoneGrid { .mobilePhoneGrid {
display: flex !important; display: flex !important;
@@ -32,41 +100,64 @@
// Price Filter Styles // Price Filter Styles
.priceFilterContainer { .priceFilterContainer {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 8px; gap: 12px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 16px; 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 { .priceInputGroup {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 6px;
flex: 1;
min-width: 0;
} }
.priceLabel { .priceLabel {
font-size: 13px; font-size: 12px;
color: #888; color: #666;
margin-bottom: 2px; font-weight: 600;
letter-spacing: 0.3px;
} }
.priceInput { .priceInput {
width: 90px; padding: 8px 10px;
padding: 6px 10px;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 6px; border-radius: 6px;
font-size: 15px; font-size: 14px;
background: #fff; background: #fff;
transition: border-color 0.2s; transition: all 0.2s ease;
&::placeholder {
color: #bbb;
}
} }
.priceInput:focus { .priceInput:focus {
border-color: #6c63ff; border-color: #d32824;
box-shadow: 0 0 0 3px rgba(211, 40, 36, 0.1);
outline: none; outline: none;
} }
.priceDivider { .priceDivider {
font-size: 18px; display: none;
color: #aaa;
font-weight: bold;
margin: 0 6px;
} }
.filtersContainer{ .filtersContainer{

View File

@@ -1,6 +1,7 @@
import React from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TiTick } from "react-icons/ti"; import { TiTick } from "react-icons/ti";
import { Divider } from "antd";
import styles from "../CategoryPage.module.scss"; import styles from "../CategoryPage.module.scss";
const CategoryFilters = ({ const CategoryFilters = ({
@@ -24,6 +25,25 @@ const CategoryFilters = ({
}) => { }) => {
const { t } = useTranslation(); 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; if (searchQuery) return null;
return ( return (
@@ -100,6 +120,23 @@ const CategoryFilters = ({
)} )}
<div className={styles.filterSection}> <div className={styles.filterSection}>
<h3>{t("category.price")}</h3> <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.priceFilterContainer}>
<div className={styles.priceInputGroup}> <div className={styles.priceInputGroup}>
<span className={styles.priceLabel}>{t("category.minPrice")}</span> <span className={styles.priceLabel}>{t("category.minPrice")}</span>
@@ -125,18 +162,22 @@ const CategoryFilters = ({
/> />
</div> </div>
</div> </div>
{/* Sıralama dropdown'u */}
<div className={styles.sortingContainer} style={{ marginTop: 12 }}> <Divider style={{ margin: "12px 0" }} />
<label htmlFor="sortingSidebar" className={styles.sortingLabel}>{t("category.sortBy")}: </label>
<select <div className={styles.sortingSection}>
id="sortingSidebar" <h4 className={styles.sortingTitle}>{t("category.sortBy")}</h4>
className={styles.sortingSelect} <div className={styles.sortingButtonsContainer}>
value={sorting} {sortOptions.map((option) => (
onChange={e => onSortingChange(e.target.value)} <button
> key={option.value}
<option value="price_amount-descending">{t("category.priceHighToLow")}</option> className={`${styles.sortingBtn} ${sorting === option.value ? styles.activeSorting : ""}`}
<option value="price_amount-ascending">{t("category.priceLowToHigh")}</option> onClick={() => onSortingChange(option.value)}
</select> >
{option.label}
</button>
))}
</div>
</div> </div>
</div> </div>
</aside> </aside>

View File

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

View File

@@ -29,7 +29,7 @@ const CategoryPage = () => {
currentPage: 1, currentPage: 1,
minPrice: "", minPrice: "",
maxPrice: "", maxPrice: "",
sorting: "price_amount-descending", sorting: "price_amount-ascending",
}); });
const [filterState, setFilterState] = useState({ const [filterState, setFilterState] = useState({
@@ -128,6 +128,7 @@ const CategoryPage = () => {
}, [ }, [
routeKey, routeKey,
location.state?.clearFilters, location.state?.clearFilters,
location.pathname,
navigate, navigate,
setAllProducts, setAllProducts,
setHasMore, setHasMore,
@@ -188,7 +189,7 @@ const CategoryPage = () => {
selectedFilterBrand: null, selectedFilterBrand: null,
})); }));
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
@@ -214,7 +215,7 @@ const CategoryPage = () => {
selectedFilterBrand: brandId, selectedFilterBrand: brandId,
})); }));
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
}; };
@@ -295,22 +296,6 @@ const CategoryPage = () => {
> >
{t("category.filter")} <LuFilter /> {t("category.filter")} <LuFilter />
</button> </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> </div>
<Drawer <Drawer

View File

@@ -323,11 +323,22 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: 60px; height: 125px;
pointer-events: none; pointer-events: none;
background: linear-gradient(to top, #111 0%, #fff 100%); background: linear-gradient(
opacity: 0.7; 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; z-index: 2;
opacity: 0;
transition: opacity 300ms ease;
}
.descriptionCardCollapsed::after {
opacity: 1;
} }
.productDescriptionCollapsed + div { .productDescriptionCollapsed + div {
@@ -391,18 +402,23 @@
background: none; background: none;
border: none; border: none;
color: #fff; color: #fff;
font-weight: bold; font-weight: 700;
cursor: pointer; cursor: pointer;
padding: 8px 0 0 0; padding: 8px 0 0 0;
font-size: 16px; font-size: 16px;
display: inline-block; display: inline-block;
text-shadow: 0 1px 8px #000, 0 1px 1px #000;
letter-spacing: 0.5px; letter-spacing: 0.5px;
margin-top: 8px; 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 { &:hover {
text-decoration: underline; 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 */} {/* Description card */}
{product.description && ( {product.description && (
<div className={styles.descriptionCard}> <div className={`${styles.descriptionCard} ${!isDescExpanded ? styles.descriptionCardCollapsed : ''}`}>
<div className={styles.descriptionHeader}> <div className={styles.descriptionHeader}>
<div className={styles.descriptionIcon}> <div className={styles.descriptionIcon}>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">