added baha anyklmak, attributes

This commit is contained in:
Jelaletdin12
2026-04-30 10:44:08 +05:00
parent cc89455967
commit 9419ec0af0
24 changed files with 1207 additions and 939 deletions

View File

@@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({
getBrands: builder.query({ getBrands: builder.query({
query: (params = {}) => { query: (params = {}) => {
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();
if (params.type) { if (params.type) queryParams.append("type", params.type);
queryParams.append("type", params.type); if (params.page) queryParams.append("page", params.page);
} if (params.limit) queryParams.append("limit", params.limit);
if (params.page) {
queryParams.append("page", params.page);
}
if (params.limit) {
queryParams.append("limit", params.limit);
}
const queryString = queryParams.toString(); const queryString = queryParams.toString();
return `/brands${queryString ? `?${queryString}` : ""}`; return `/brands${queryString ? `?${queryString}` : ""}`;
}, },
@@ -28,30 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({
getBrandProducts: builder.query({ getBrandProducts: builder.query({
query: (params) => { query: (params) => {
if (typeof params === 'string' || typeof params === 'number') { if (typeof params === "string" || typeof params === "number") {
return `/brands/${params}/products`; return `/brands/${params}/products`;
} }
const { id, page = 1, limit, sorting, min_price, max_price, brands } = params; const { id, page = 1, limit = 24, sorting, min_price, max_price } = params;
let url = `/brands/${id}/products?page=${page}`; const urlParams = new URLSearchParams();
urlParams.append("page", page);
urlParams.append("limit", limit);
if (sorting) urlParams.append("sorting", sorting);
if (min_price) urlParams.append("min_price", min_price);
if (max_price) urlParams.append("max_price", max_price);
if (limit) { return `/brands/${id}/products?${urlParams.toString()}`;
url += `&limit=${limit}`;
}
if (sorting) {
url += `&sorting=${encodeURIComponent(sorting)}`;
}
if (min_price) {
url += `&min_price=${encodeURIComponent(min_price)}`;
}
if (max_price) {
url += `&max_price=${encodeURIComponent(max_price)}`;
}
if (brands) {
url += `&brands=${encodeURIComponent(brands)}`;
}
return url;
}, },
transformResponse: (response) => ({ transformResponse: (response) => ({
data: response.data || response, data: response.data || response,

View File

@@ -7,15 +7,14 @@ export const categoriesApi = baseApi.injectEndpoints({
}), }),
getCategoryProducts: builder.query({ getCategoryProducts: builder.query({
query: ({ categoryId, page = 1, limit, brands, min_price, max_price, sorting }) => { query: ({ categoryId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('page', page); params.append("page", page);
if (limit) params.append('limit', 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); if (sorting) params.append("sorting", sorting);
return `categories/${categoryId}/products?${params.toString()}`; return `categories/${categoryId}/products?${params.toString()}`;
}, },
transformResponse: (response) => ({ transformResponse: (response) => ({
@@ -25,78 +24,103 @@ export const categoriesApi = baseApi.injectEndpoints({
}), }),
getAllCategoryProducts: builder.query({ getAllCategoryProducts: builder.query({
async queryFn(category, queryApi, extraOptions, baseQuery) { async queryFn(category, _queryApi, _extraOptions, baseQuery) {
const fetchProducts = async (categoryId) => { const fetchProducts = async (categoryId) => {
const result = await baseQuery(`categories/${categoryId}/products`); const result = await baseQuery(`categories/${categoryId}/products`);
return result.data ? result.data.data : []; return result.data ? result.data.data : [];
}; };
let allProducts = await fetchProducts(category.id); let allProducts = await fetchProducts(category.id);
for (const child of category.children) { for (const child of category.children) {
const childProducts = await fetchProducts(child.id); const childProducts = await fetchProducts(child.id);
allProducts = [...allProducts, ...childProducts]; allProducts = [...allProducts, ...childProducts];
} }
return { data: allProducts }; return { data: allProducts };
}, },
}), }),
getAllCategoryProductsPaginated: builder.query({ getAllCategoryProductsPaginated: builder.query({
async queryFn( async queryFn(
{ category, page = 1, limit = 6, brands, min_price, max_price, sorting }, { category, page = 1, limit = 24, brands, min_price, max_price, sorting },
queryApi, _queryApi,
extraOptions, _extraOptions,
baseQuery baseQuery
) { ) {
if (!category) return { data: [] }; if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } };
try { try {
const hasMoreByCategory = {};
const fetchProductsForPage = async (categoryIds, currentPage) => {
let allPageProducts = [];
const perCategoryLimit = Math.ceil(limit / categoryIds.length);
for (const categoryId of categoryIds) {
const params = new URLSearchParams();
params.append('page', currentPage);
params.append('limit', perCategoryLimit);
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()}`
);
if (result.data && result.data.data) {
allPageProducts = [...allPageProducts, ...result.data.data];
hasMoreByCategory[categoryId] = !!result.data.pagination.next_page_url;
}
}
return allPageProducts;
};
const categoryIds = [category.id]; const categoryIds = [category.id];
if (category.children && category.children.length > 0) { if (category.children?.length > 0) {
category.children.forEach((child) => categoryIds.push(child.id)); category.children.forEach((child) => categoryIds.push(child.id));
} }
const productsForPage = await fetchProductsForPage(categoryIds, page); // Tek category — direkt fetch, limit tam uygulanır
if (categoryIds.length === 1) {
const params = new URLSearchParams();
params.append("page", page);
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);
const hasMorePages = Object.values(hasMoreByCategory).some( const result = await baseQuery(
(hasMore) => hasMore `categories/${categoryIds[0]}/products?${params.toString()}`
); );
if (result.error) return { error: result.error };
return {
data: {
data: result.data?.data || [],
pagination: {
currentPage: page,
hasMorePages: !!result.data?.pagination?.next_page_url,
},
},
};
}
// Birden fazla category — paralel fetch, her biri tam limit ile
// Sonra client-side deduplicate + slice
const requests = categoryIds.map((categoryId) => {
const params = new URLSearchParams();
params.append("page", page);
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);
return baseQuery(`categories/${categoryId}/products?${params.toString()}`);
});
const results = await Promise.all(requests);
let allProducts = [];
let hasMorePages = false;
const seenIds = new Set();
for (const result of results) {
if (result.error) continue;
const items = result.data?.data || [];
for (const item of items) {
if (!seenIds.has(item.id)) {
seenIds.add(item.id);
allProducts.push(item);
}
}
if (result.data?.pagination?.next_page_url) {
hasMorePages = true;
}
}
return { return {
data: { data: {
data: productsForPage, data: allProducts,
pagination: { pagination: {
currentPage: page, currentPage: page,
hasMorePages: hasMorePages, hasMorePages,
}, },
}, },
}; };

View File

@@ -12,33 +12,28 @@ export const collectionsApi = baseApi.injectEndpoints({
getCollectionProducts: builder.query({ getCollectionProducts: builder.query({
query: (collectionId) => `/collections/${collectionId}/products`, query: (collectionId) => `/collections/${collectionId}/products`,
transformResponse: (response) => { transformResponse: (response) => ({
return { data: response.data || [],
data: response.data || [], isEmpty: !response.data || response.data.length === 0,
isEmpty: !response.data || response.data.length === 0, }),
};
},
}), }),
checkCollectionHasProducts: builder.query({ checkCollectionHasProducts: builder.query({
query: (collectionId) => `/collections/${collectionId}/products?limit=1`, query: (collectionId) => `/collections/${collectionId}/products?limit=1`,
transformResponse: (response) => { transformResponse: (response) => ({
return { hasProducts: response.data && response.data.length > 0,
hasProducts: response.data && response.data.length > 0, }),
};
},
}), }),
getCollectionProductsPaginated: builder.query({ getCollectionProductsPaginated: builder.query({
query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price, sorting = "price_amount-ascending" }) => { query: ({ collectionId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('page', page); params.append("page", page);
if (limit) params.append('limit', 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);
params.append('sorting', sorting); if (sorting) params.append("sorting", sorting); // undefined gelirse gönderme
return `/collections/${collectionId}/products?${params.toString()}`; return `/collections/${collectionId}/products?${params.toString()}`;
}, },
transformResponse: (response) => ({ transformResponse: (response) => ({

View File

@@ -1,235 +1,272 @@
// DropdownMenu.module.scss
.dropdownContainer { .dropdownContainer {
position: relative;
@media screen and (max-width: 1023px) { @media screen and (max-width: 1023px) {
display: none; display: none;
} }
} }
// ---- TRIGGER BUTTON ----
.navButton { .navButton {
display: flex; display: flex;
gap: 5px;
border: none;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.875rem;
padding-right: 0.875rem;
justify-content: center;
align-items: center; align-items: center;
gap: 6px;
border: none;
padding: 0.25rem 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
height: 2.5rem; height: 2.5rem;
font-size: 0.875rem; font-size: 16px;
font-weight: 600;
color: #4b5563; color: #4b5563;
background-color: transparent; background-color: transparent;
font-weight: 600;
font-size: 16px;
cursor: pointer; cursor: pointer;
transition: background-color 0.15s, color 0.15s;
position: relative;
z-index: 999;
&:hover { &:hover {
background-color: #f3f4f6; background-color: #f3f4f6;
} }
&.navButtonActive {
background-color: #e63946;
color: #ffffff;
svg {
color: #ffffff;
}
}
} }
// ---- OVERLAY ----
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 998;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// ---- WRAPPER + ANIMATION ----
.dropdownWrapper { .dropdownWrapper {
position: relative;
}
.dropdownPanel {
position: absolute; position: absolute;
top: 100%; top: calc(100% + 8px);
margin-top: 8px; left: 0;
z-index: 50; z-index: 999;
display: flex; animation: slideDown 0.18s ease;
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
width: 1366px;
padding: 0 1.375rem;
} }
.categoriesList { @keyframes slideDown {
flex: 1; from {
max-height: 500px; opacity: 0;
overflow-y: auto; transform: translateY(-6px);
border-right: 1px solid #ebe7eb; }
padding: 20px; to {
opacity: 1;
transform: translateY(0);
}
}
// ---- PANEL SHELL ----
.dropdownPanel {
display: flex; display: flex;
flex-direction: column; background: #fff;
gap: 5px; border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
width: 1336px;
max-height: 520px;
// max-width: calc(100vw - 32px);
}
// &::-webkit-scrollbar { // ---- LEFT LIST ----
// width: 6px; .categoriesList {
// } width: 270px;
flex-shrink: 0;
border-right: 1px solid rgba(255, 255, 255, 0.12);
padding: 10px 0;
max-height: 520px;
overflow-y: auto;
&::-webkit-scrollbar-track { &::-webkit-scrollbar {
background: #e5e7eb; width: 4px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #d1d5db; background: rgba(255, 255, 255, 0.2);
} border-radius: 2px;
.title {
&:hover {
color: #888888;
}
&:active {
color: #888888;
}
} }
} }
.categoryItem { .categoryItem {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: space-between;
padding: 6px; padding: 9px 16px;
font-size: 16px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; color: #000;
border: 1px solid #3615371a; transition: background-color 0.12s, color 0.12s;
border-radius: 6px;
&:hover { &:hover {
background-color: #f9fafb; background-color: rgba(255, 255, 255, 0.08);
color: #000;
} }
&.active { &.active {
background-color: #f3f4f6; background-color: rgba(255, 255, 255, 0.15);
} color: #000;
.icon { .title {
font-size: 14px; font-weight: 600;
}
.title {
font-size: 14px;
&:hover {
color: #888888;
} }
} }
} }
.title {
font-size: 14px;
}
.chevron {
color: rgba(255, 255, 255, 0.4);
flex-shrink: 0;
}
// ---- RIGHT CONTENT PANEL ----
.contentPanel { .contentPanel {
flex: 3; flex: 1;
padding: 16px; padding: 20px 24px;
max-height: 400px; max-height: 520px;
overflow-y: hidden; overflow-y: auto;
background: #ffffff;
// &::-webkit-scrollbar { &::-webkit-scrollbar {
// width: 6px; width: 4px;
// }
&::-webkit-scrollbar-track {
background: #e5e7eb;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #d1d5db; background: #d1d5db;
// border-radius: 3px; border-radius: 2px;
}
.title {
cursor: pointer;
color: #361517;
font-size: 24px;
font-weight: 600;
&:hover {
color: #888888;
}
} }
} }
.column {
display: flex; .panelTitle {
flex-direction: column; font-size: 20px;
flex: 2; font-weight: 700;
text-align: left; color: #111827;
margin-bottom: 16px;
cursor: pointer;
display: inline-block;
&:hover {
color: #e63946;
}
}
// COLUMN GRID MODE
// SONRA — column layout (iyi, masonry gibi akar)
.columnsGrid {
columns: 250px auto;
column-gap: 24px;
}
.columnSection {
break-inside: avoid;
margin-bottom: 20px;
display: inline-block; // break-inside'ın çalışması için zorunlu
width: 100%;
} }
.sectionTitle { .sectionTitle {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;
color: #361517;
cursor: pointer;
&:hover {
color: #888888;
}
}
.subcategoryList {
margin-bottom: 24px;
display: flex;
}
.subcategoryItem {
font-size: 14px; font-size: 14px;
color: #361517; font-weight: 800;
padding: 4px 0; color: #111827;
margin-bottom: 6px;
cursor: pointer; cursor: pointer;
transition: color 0.2s;
&:hover { &:hover {
color: #888888; color: #e63946;
} }
} }
.subCategoriesContainer { .leafItem {
display: flex; display: block;
flex-direction: column; font-size: 14px;
max-height: 360px; color: #4b5563;
overflow-y: auto; padding: 3px 0;
}
.nestedCategoryContainer:last-child {
margin-bottom: 16px;
}
.nestedCategoryContainer {
margin-bottom: 4px;
}
.nestedCategoryItem {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: color 0.12s;
&:hover {
color: #e63946;
}
}
// FLAT LIST MODE
.flatList {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-content: flex-start;
}
.flatListBordered {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
.flatItem {
font-size: 14px;
color: #4b5563;
cursor: pointer;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid #e5e7eb;
transition: background-color 0.12s, color 0.12s;
&:hover { &:hover {
background-color: #f3f4f6; background-color: #f3f4f6;
} color: #111827;
.categoryLabel {
flex: 1;
display: flex;
align-items: center;
}
.title {
font-size: 14px;
} }
} }
.expandButton, .navButtonLoading {
.navigateButton { opacity: 0.7;
cursor: wait;
.categoryIcon {
opacity: 0.4;
}
}
.loadingDots {
display: flex; display: flex;
gap: 3px;
align-items: center; align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
background-color: transparent;
border: none;
cursor: pointer;
&:hover { span {
background-color: #e5e7eb; width: 4px;
height: 4px;
border-radius: 50%;
background: currentColor;
animation: dotPulse 1.2s infinite ease-in-out;
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.4s; }
} }
} }
.nestedChildren { @keyframes dotPulse {
margin-top: 4px; 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
} 40% { transform: scale(1); opacity: 1; }
.noSubcategories {
color: #6b7280;
font-style: italic;
} }

View File

@@ -1,83 +1,62 @@
// DropdownMenu.jsx
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react";
import styles from "./DropdownMenu.module.scss"; import styles from "./DropdownMenu.module.scss";
import { useGetCategoriesQuery } from "../../app/api/categories"; import { useGetCategoriesQuery } from "../../app/api/categories";
import { CategoryIcon } from "../Icons"; import { CategoryIcon } from "../Icons";
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
const NestedCategory = ({ const ContentPanel = ({ category, onSelect, onClose }) => {
category, if (!category) return null;
level = 0,
handleCategorySelect,
closeDropdown,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const hasChildren = category.children && category.children.length > 0;
const handleClick = (e) => { const children = category.children || [];
e.stopPropagation(); const withChildren = children.filter((c) => c.children?.length > 0);
if (hasChildren) { const withoutChildren = children.filter((c) => !c.children?.length);
setIsExpanded(!isExpanded);
} else {
handleCategorySelect(category);
closeDropdown();
}
};
const handleDirectNavigation = (e) => { const allColumns = [
e.stopPropagation(); ...withChildren,
handleCategorySelect(category); ...withoutChildren.map((c) => ({ ...c, children: [] })),
closeDropdown(); ];
};
return ( return (
<div <div className={styles.contentPanel}>
className={styles.nestedCategoryContainer} <h2
style={{ paddingLeft: `${level * 16}px` }} className={styles.panelTitle}
> onClick={() => {
<div className={styles.nestedCategoryItem} onClick={handleClick}> onSelect(category);
<div className={styles.categoryLabel}> onClose();
<span className={styles.title}>{category.name}</span> }}
</div> >
{category.name}
</h2>
{hasChildren && ( {allColumns.length > 0 && (
<button <div className={styles.columnsGrid}>
className={styles.expandButton} {allColumns.map((sub) => (
onClick={(e) => { <div key={sub.id} className={styles.columnSection}>
e.stopPropagation(); <div
setIsExpanded(!isExpanded); className={styles.sectionTitle}
}} onClick={() => {
> onSelect(sub);
{isExpanded ? ( onClose();
<ChevronDown size={16} /> }}
) : ( >
<ChevronRight size={16} /> {sub.name}
)} </div>
</button> {sub.children?.map((leaf) => (
)} <span
key={leaf.id}
{hasChildren && ( className={styles.leafItem}
<button onClick={() => {
className={styles.navigateButton} onSelect(leaf);
onClick={handleDirectNavigation} onClose();
title="Go to category" }}
> >
{leaf.name}
</button> </span>
)} ))}
</div> </div>
{hasChildren && isExpanded && (
<div className={styles.nestedChildren}>
{category.children.map((child) => (
<NestedCategory
key={child.id}
category={child}
level={level + 1}
handleCategorySelect={handleCategorySelect}
closeDropdown={closeDropdown}
/>
))} ))}
</div> </div>
)} )}
@@ -89,113 +68,85 @@ const DropdownMenu = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
const { const {
data: categoriesData, data: categoriesData,
isLoading, isLoading,
error, error,
} = useGetCategoriesQuery("tree"); } = useGetCategoriesQuery("tree");
const categories = categoriesData?.data || []; const categories = categoriesData?.data || [];
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [activeMainCategory, setActiveMainCategory] = useState(null); const [activeCategory, setActiveCategory] = useState(null);
useEffect(() => { useEffect(() => {
if (categories.length > 0) { if (categories.length > 0 && !activeCategory) {
const defaultCategory = setActiveCategory(categories[0]);
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
setActiveMainCategory(defaultCategory);
} }
}, [categories]); }, [categories]);
const handleToggle = () => { useEffect(() => {
setIsOpen(!isOpen); const handler = (e) => {
}; if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
const handleMouseLeave = () => { const handleSelect = (category) => {
if (categories.length > 0) {
const defaultCategory =
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
setActiveMainCategory(defaultCategory);
}
};
const handleCategorySelect = (category) => {
navigate(`/category/${category.id}`, { state: { category } }); navigate(`/category/${category.id}`, { state: { category } });
setIsOpen(false); setIsOpen(false);
}; };
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading categories</div>;
return ( return (
<div className={styles.dropdownContainer} ref={dropdownRef}> <div className={styles.dropdownContainer} ref={dropdownRef}>
<button onClick={handleToggle} className={styles.navButton}> <button
onClick={() => setIsOpen((p) => !p)}
className={`${styles.navButton} ${isOpen ? styles.navButtonActive : ""}`}
aria-expanded={isOpen}
>
<CategoryIcon /> <CategoryIcon />
{t("navbar.category")} {isLoading
? <div className={styles.loadingDots}><span/><span/><span/></div>
: t("navbar.category")
}
</button> </button>
{isOpen && ( {isOpen && (
<div className={styles.dropdownWrapper}> <>
<div className={styles.dropdownPanel} onMouseLeave={handleMouseLeave}> <div className={styles.overlay} onClick={() => setIsOpen(false)} />
<div className={styles.categoriesList}>
{categories.map((category) => (
<div
key={category.id}
className={`${styles.categoryItem} ${
activeMainCategory?.id === category.id ? styles.active : ""
}`}
onMouseEnter={() => setActiveMainCategory(category)}
onClick={() => handleCategorySelect(category)}
>
<span className={styles.title}>{category.name}</span>
</div>
))}
</div>
{activeMainCategory && ( <div className={styles.dropdownWrapper}>
<div className={styles.contentPanel}> <div className={styles.dropdownPanel}>
<h2 <div className={styles.categoriesList}>
onClick={() => handleCategorySelect(activeMainCategory)} {categories.map((cat) => (
className={styles.title} <div
> key={cat.id}
{activeMainCategory.name} className={`${styles.categoryItem} ${
</h2> activeCategory?.id === cat.id ? styles.active : ""
}`}
<div className={styles.subCategoriesContainer}> onMouseEnter={() => setActiveCategory(cat)}
{activeMainCategory.children && onClick={() => handleSelect(cat)}
activeMainCategory.children.length > 0 ? ( >
activeMainCategory.children.map((subcategory) => ( <span className={styles.title}>{cat.name}</span>
<NestedCategory {cat.children?.length > 0 && (
key={subcategory.id} <ChevronRight size={14} className={styles.chevron} />
category={subcategory} )}
handleCategorySelect={handleCategorySelect} </div>
closeDropdown={() => setIsOpen(false)} ))}
/>
))
) : (
<div className={styles.noSubcategories}>
{/* No subcategories available */}
</div>
)}
</div>
</div> </div>
)}
<ContentPanel
category={activeCategory}
onSelect={handleSelect}
onClose={() => setIsOpen(false)}
/>
</div>
</div> </div>
</div> </>
)} )}
</div> </div>
); );

View File

@@ -0,0 +1,92 @@
.container {
max-width: 1336px;
margin: 20px auto;
width: 100%;
@media screen and (max-width: 1023px) {
margin: 10px auto;
}
}
.brandsScroll {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 8px;
/* Hide scrollbar for Webkit */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for Firefox, IE, Edge */
-ms-overflow-style: none;
scrollbar-width: none;
}
.brandCard {
flex: 0 0 auto;
width: 122px;
height: 50px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 8px;
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.logoFallback {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.allButton {
flex: 0 0 auto;
width: 122px;
height: 67.6px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
gap: 4px;
color: #111827;
font-weight: 600;
font-size: 14px;
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
color: #aaaaaa;
}
svg {
font-size: 16px;
}
}
@media screen and (max-width: 768px) {
.brandCard, .allButton {
width: 100px;
// height: 79.2px;
}
}

View File

@@ -0,0 +1,77 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useGetBrandsQuery } from '../../app/api/brandsApi';
import styles from './HomeBrands.module.scss';
import { Logo } from '../Icons';
import { IoIosArrowForward } from 'react-icons/io';
const HomeBrands = () => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
// We fetch a larger amount so we have enough to shuffle.
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 50 });
const randomBrands = useMemo(() => {
if (!brandsData) return [];
// Create a shallow copy and shuffle it
const shuffled = [...brandsData].sort(() => 0.5 - Math.random());
// Pick the first 9 brands
return shuffled.slice(0, 8);
}, [brandsData]);
if (isLoading || !brandsData || brandsData.length === 0) return null;
// "Еще" in ru, "Hemmesi" in tm, "More" in en
const getMoreText = () => {
const lang = i18n.language;
if (lang === 'ru') return 'Еще';
if (lang === 'en') return 'More';
return 'Hemmesi';
};
return (
<div className={styles.container}>
<div className={styles.brandsScroll}>
{randomBrands.map((brand) => (
<div
key={brand.id}
className={styles.brandCard}
onClick={() => navigate(`/brands/${brand.id}`)}
>
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
<img
src={
brand.media?.[0]?.thumbnail ||
brand.media?.[0]?.images_800x800 ||
brand.logo
}
alt={brand.name}
onError={(e) => {
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
) : (
<div className={styles.logoFallback}>
<Logo width={40} height={40} />
</div>
)}
<div className={styles.logoFallback} style={{ display: "none" }}>
<Logo width={40} height={40} />
</div>
</div>
))}
<div
className={styles.allButton}
onClick={() => navigate('/brands')}
>
<span>{getMoreText()}</span>
<IoIosArrowForward />
</div>
</div>
</div>
);
};
export default HomeBrands;

View File

@@ -221,7 +221,7 @@ export const CategoryIcon = () => (
height={20} height={20}
> >
<path <path
fill="#4b5563" fill="currentColor"
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z" d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
></path> ></path>
</svg> </svg>

View File

@@ -39,7 +39,7 @@
} }
&__satyjy { &__satyjy {
@media screen and (max-width: 500px) { @media screen and (max-width: 785px) {
display: none; display: none;
} }
} }
@@ -288,7 +288,7 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-left: auto; margin-left: auto;
@media screen and (max-width: 500px) { @media screen and (max-width: 708px) {
display: none; display: none;
} }
} }

View File

@@ -0,0 +1,102 @@
.pendingPriceBadgeWrapper {
position: relative;
display: inline-flex;
align-items: center;
}
.pendingPriceBadge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: #faeeda;
border: 0.5px solid #ef9f27;
color: #854f0b;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
}
.pendingPriceTooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: var(--color-background-primary, #ffffff);
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
border-radius: var(--border-radius-md, 6px);
padding: 8px 12px;
width: 220px;
font-size: 13px;
color: var(--color-text-primary, #333333);
line-height: 1.5;
z-index: 100;
white-space: normal;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@media (max-width: 767px) {
display: none;
}
strong {
display: block;
margin-bottom: 4px;
color: var(--color-text-primary, #000000);
}
}
:global {
.pending-price-modal {
.ant-modal-content {
border-radius: 12px;
padding: 24px;
@media (max-width: 767px) {
padding: 20px;
}
}
.ant-modal-header {
margin-bottom: 12px;
.ant-modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
@media (max-width: 767px) {
font-size: 16px;
}
}
}
.ant-modal-body {
p {
font-size: 14px;
line-height: 1.6;
color: #555;
margin: 0;
@media (max-width: 767px) {
font-size: 13px;
}
}
}
.ant-modal-footer {
margin-top: 20px;
.ant-btn-primary {
background-color: #888888;
border-color: #888888;
border-radius: 6px;
height: 36px;
padding: 0 20px;
font-weight: 500;
&:hover {
background-color: #666666;
border-color: #666666;
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
import React, { useState } from "react";
import { Modal } from "antd";
import { useTranslation } from "react-i18next";
import styles from "./PendingPriceBadge.module.scss";
const PendingPriceModal = ({ open, onClose, t }) => (
<Modal
open={open}
onOk={onClose}
onCancel={onClose}
okText={t("common.ok") || "Ok"}
cancelButtonProps={{ style: { display: "none" } }}
centered
title={t("cart.pendingPriceTitle") || "Bahasy anyklamaly"}
className="pending-price-modal"
width={400}
>
<p>
{t("cart.pendingPriceDesc") ||
"Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer."}
</p>
</Modal>
);
const PendingPriceBadge = () => {
const { t } = useTranslation();
const [tooltipVisible, setTooltipVisible] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
setModalVisible(true);
};
const stopPropagation = (e) => {
e.stopPropagation();
};
return (
<span onClick={stopPropagation}>
<span
className={styles.pendingPriceBadgeWrapper}
onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
onClick={handleClick}
onTouchEnd={(e) => {
e.stopPropagation();
}}
>
<span className={styles.pendingPriceBadge}>!</span>
{tooltipVisible && (
<span className={styles.pendingPriceTooltip}>
<strong>{t("cart.pendingPriceTitle") || "Bahasyny anyklamaly"}</strong>
{t("cart.pendingPriceTooltipDesc") ||
"Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."}
</span>
)}
</span>
<PendingPriceModal
open={modalVisible}
onClose={() => setModalVisible(false)}
t={t}
/>
</span>
);
};
export default PendingPriceBadge;

View File

@@ -201,9 +201,9 @@ const ProductCard = ({
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
{(product.discount || calculatedDiscount) && ( {(product.discount > 0 || calculatedDiscount > 0) && (
<span className={styles.discountBadge}> <span className={styles.discountBadge}>
-{product.discount ?? calculatedDiscount}% -{product.discount || calculatedDiscount}%
</span> </span>
)} )}
{product.stock === 0 && ( {product.stock === 0 && (
@@ -221,7 +221,7 @@ const ProductCard = ({
<div className={styles.priceContainer}> <div className={styles.priceContainer}>
<div> <div>
{isPriceZero(price_amount) ? ( {isPriceZero(price_amount) ? (
<span className={styles.currentPrice}>Bahasyny anyklamaly</span> <span className={styles.currentPrice}> {t("cart.pendingPriceTitle")}</span>
) : ( ) : (
<> <>
<span className={styles.currentPrice}>{price_amount} m.</span> <span className={styles.currentPrice}>{price_amount} m.</span>

View File

@@ -38,6 +38,9 @@ export default {
emptyCartTitle: "Your cart is empty", emptyCartTitle: "Your cart is empty",
emptyCartMessage: "Looks like you haven't added any items to your cart yet", emptyCartMessage: "Looks like you haven't added any items to your cart yet",
continueShopping: "Continue Shopping", continueShopping: "Continue Shopping",
pendingPriceTitle: "Price pending",
pendingPriceDesc: "The price of one or more items in this order has not yet been determined. Our operator will contact you to provide additional information.",
pendingPriceTooltipDesc: "The price of this item in the order has not been determined. The operator will call you and provide additional information."
}, },
checkout: { checkout: {
paymentMethod: "Payment Method", paymentMethod: "Payment Method",

View File

@@ -38,6 +38,9 @@ export default {
emptyCartTitle: "Ваша корзина пуста", emptyCartTitle: "Ваша корзина пуста",
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину", emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
continueShopping: "Продолжить покупки", continueShopping: "Продолжить покупки",
pendingPriceTitle: "Цена уточняется",
pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.",
pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию."
}, },
checkout: { checkout: {
paymentMethod: "Способ оплаты", paymentMethod: "Способ оплаты",

View File

@@ -38,6 +38,9 @@ export default {
emptyCartTitle: "Sebediňiz boş", emptyCartTitle: "Sebediňiz boş",
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.", emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
continueShopping: "Söwda etmegi dowam etdiriň", continueShopping: "Söwda etmegi dowam etdiriň",
pendingPriceTitle: "Bahasyny anyklamaly",
pendingPriceDesc: "Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.",
pendingPriceTooltipDesc: "Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."
}, },
checkout: { checkout: {
paymentMethod: "Töleg görnüşi", paymentMethod: "Töleg görnüşi",

View File

@@ -31,7 +31,6 @@
} }
} }
} }
.cartProducts { .cartProducts {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -152,6 +151,7 @@
@media screen and (max-width: 720px) { @media screen and (max-width: 720px) {
flex-direction: row-reverse; flex-direction: row-reverse;
justify-content: space-between; justify-content: space-between;
gap:10px;
} }
.price { .price {
@@ -524,3 +524,106 @@
} }
} }
} }
.pendingPriceBadgeWrapper {
position: relative;
display: inline-flex;
align-items: center;
}
.pendingPriceBadge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: #faeeda;
border: 0.5px solid #ef9f27;
color: #854f0b;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
}
.pendingPriceTooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: var(--color-background-primary, #ffffff);
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
border-radius: var(--border-radius-md, 6px);
padding: 8px 12px;
width: 220px;
font-size: 13px;
color: var(--color-text-primary, #333333);
line-height: 1.5;
z-index: 100;
white-space: normal;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@media (max-width: 767px) {
display: none;
}
strong {
display: block;
margin-bottom: 4px;
color: var(--color-text-primary, #000000);
}
}
:global {
.pending-price-modal {
.ant-modal-content {
border-radius: 12px;
padding: 24px;
@media (max-width: 767px) {
padding: 20px;
}
}
.ant-modal-header {
margin-bottom: 12px;
.ant-modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
@media (max-width: 767px) {
font-size: 16px;
}
}
}
.ant-modal-body {
p {
font-size: 14px;
line-height: 1.6;
color: #555;
margin: 0;
@media (max-width: 767px) {
font-size: 13px;
}
}
}
.ant-modal-footer {
margin-top: 20px;
.ant-btn-primary {
background-color: #888888;
border-color: #888888;
border-radius: 6px;
height: 36px;
padding: 0 20px;
font-weight: 500;
&:hover {
background-color: #666666;
border-color: #666666;
}
}
}
}
}

View File

@@ -13,6 +13,7 @@ import {
import { useCart } from "../../app/api/useCart"; import { useCart } from "../../app/api/useCart";
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons"; import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
import PendingPriceBadge from "../../components/PendingPriceBadge";
const isPriceZero = (price) => !price || parseFloat(price) === 0; const isPriceZero = (price) => !price || parseFloat(price) === 0;
@@ -320,7 +321,7 @@ const CartPage = () => {
<div className={styles.priceQuantity}> <div className={styles.priceQuantity}>
<span className={styles.price}> <span className={styles.price}>
{isPriceZero(item.product.price_amount) {isPriceZero(item.product.price_amount)
? "Bahasyny anyklamaly" ? t("cart.pendingPriceTitle")
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`} : `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
</span> </span>
<div className={styles.quantityControls}> <div className={styles.quantityControls}>
@@ -375,20 +376,12 @@ const CartPage = () => {
{store.name} - {t("cart.basket")}: {store.name} - {t("cart.basket")}:
</h3> </h3>
{hasZeroPrice ? ( {hasZeroPrice ? (
<> <div className={styles.summaryRow}>
<div className={styles.summaryRow}> <span>{t("cart.total")}:</span>
<span>{t("cart.price")}:</span> <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
<span>Bahasyny anyklamaly</span> {t("cart.pendingPriceTitle")} <PendingPriceBadge />
</div> </span>
<div className={styles.summaryRow}> </div>
<span>{t("cart.delivery")}:</span>
<span>Bahasyny anyklamaly</span>
</div>
<div className={styles.summaryRow}>
<span>{t("cart.total")}:</span>
<span>Bahasyny anyklamaly</span>
</div>
</>
) : ( ) : (
<> <>
<div className={styles.summaryRow}> <div className={styles.summaryRow}>

View File

@@ -128,6 +128,7 @@
gap: 6px; gap: 6px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.priceLabel { .priceLabel {
@@ -144,7 +145,7 @@
font-size: 14px; font-size: 14px;
background: #fff; background: #fff;
transition: all 0.2s ease; transition: all 0.2s ease;
width: 85%;
&::placeholder { &::placeholder {
color: #bbb; color: #bbb;
} }

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useRef } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import { import {
useGetCategoryProductsQuery,
useLazyGetAllCategoryProductsPaginatedQuery, useLazyGetAllCategoryProductsPaginatedQuery,
useGetCategoryProductsQuery,
} from "../../../app/api/categories"; } from "../../../app/api/categories";
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi"; import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi"; import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
@@ -19,50 +19,16 @@ const useCategoryProducts = ({
maxPrice, maxPrice,
sorting, sorting,
searchQuery, searchQuery,
initialProducts = [],
initialHasMore = true,
}) => { }) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState(initialProducts);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(initialHasMore);
const [isFetching, setIsFetching] = useState(false);
const isFetchingRef = useRef(false); const activeRequestId = useRef(0);
const lastFetchKeyRef = useRef(null); // Tüm parametreleri ref'te tut — stale closure'ı tamamen engelle
const abortControllerRef = useRef(null); const paramsRef = useRef({});
const contextId = useMemo(() => {
const parts = [
selectedFilterCategory && `fcat-${selectedFilterCategory}`,
categoryId && `cat-${categoryId}`,
brandId && `brand-${brandId}`,
collectionId && `col-${collectionId}`,
selectedFilterBrand && `fbrand-${selectedFilterBrand}`,
minPrice && `min-${minPrice}`,
maxPrice && `max-${maxPrice}`,
sorting && `sort-${sorting}`,
].filter(Boolean);
return parts.join("|") || "none";
}, [
selectedFilterCategory,
categoryId,
brandId,
collectionId,
selectedFilterBrand,
minPrice,
maxPrice,
sorting,
]);
const fetchParams = useMemo(
() => ({
page: currentPage,
limit: 24,
brands: selectedFilterBrand || undefined,
min_price: minPrice || undefined,
max_price: maxPrice || undefined,
sorting: sorting || undefined,
}),
[currentPage, selectedFilterBrand, minPrice, maxPrice, sorting],
);
const fetchKey = `${contextId}-p${currentPage}`;
const shouldUseBaseQuery = const shouldUseBaseQuery =
categoryId && categoryId &&
@@ -72,238 +38,146 @@ const useCategoryProducts = ({
!brandId && !brandId &&
!collectionId; !collectionId;
const { const { data: baseQueryData, isFetching: baseQueryFetching } =
data: paginatedCategoryProducts, useGetCategoryProductsQuery(
isLoading: categoryLoading, {
isFetching: categoryFetching, categoryId,
} = useGetCategoryProductsQuery( page: currentPage,
{ min_price: minPrice || undefined,
categoryId: categoryId, max_price: maxPrice || undefined,
page: currentPage, brands: selectedFilterBrand || undefined,
min_price: minPrice || undefined, sorting: sorting || undefined,
max_price: maxPrice || undefined, },
brands: selectedFilterBrand || undefined, { skip: !shouldUseBaseQuery }
sorting: sorting || undefined, );
},
{
skip: !shouldUseBaseQuery,
},
);
const [ const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
fetchCategoryPaginated, const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
{ const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
data: lazyCategoryProducts,
isLoading: lazyCategoryLoading,
isFetching: lazyCategoryFetching,
reset: resetCategoryPaginated,
},
] = useLazyGetAllCategoryProductsPaginatedQuery();
const [
fetchBrandPaginated,
{
data: paginatedBrandProducts,
isLoading: brandPaginatedLoading,
isFetching: brandFetching,
reset: resetBrandPaginated,
},
] = useLazyGetBrandProductsQuery();
const [
fetchCollectionPaginated,
{
data: paginatedCollectionProducts,
isLoading: collectionPaginatedLoading,
isFetching: collectionFetching,
reset: resetCollectionPaginated,
},
] = useLazyGetCollectionProductsPaginatedQuery();
// Base query handler
useEffect(() => { useEffect(() => {
setProducts([]); if (!shouldUseBaseQuery || !baseQueryData) return;
setHasMore(true); const data = baseQueryData.data || [];
const hasNextPage = !!baseQueryData.pagination?.next_page_url;
resetCategoryPaginated?.(); setProducts((prev) => {
resetBrandPaginated?.(); if (currentPage === 1) return data;
resetCollectionPaginated?.(); const existingIds = new Set(prev.map((p) => p.id));
const newItems = data.filter((p) => !existingIds.has(p.id));
lastFetchKeyRef.current = null; return newItems.length > 0 ? [...prev, ...newItems] : prev;
isFetchingRef.current = false; });
setHasMore(hasNextPage);
if (abortControllerRef.current) { }, [baseQueryData, currentPage, shouldUseBaseQuery]);
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}, [
contextId,
resetCategoryPaginated,
resetBrandPaginated,
resetCollectionPaginated,
]);
// Her fetch çağrısını doğrudan effect içinde yap — useCallback kaldırıldı
useEffect(() => { useEffect(() => {
if (searchQuery) return; if (shouldUseBaseQuery || searchQuery) return;
if (lastFetchKeyRef.current === fetchKey) { // Parametreleri snapshot al
return; const snapshot = {
} currentPage,
selectedFilterCategory,
categoryId,
isSubCategory,
brandId,
collectionId,
selectedFilterBrand,
minPrice,
maxPrice,
sorting,
};
if (isFetchingRef.current) { const requestId = ++activeRequestId.current;
return; setIsFetching(true);
}
if (abortControllerRef.current) { const run = async () => {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
isFetchingRef.current = true;
lastFetchKeyRef.current = fetchKey;
const executeFetch = async () => {
try { try {
if (selectedFilterCategory) { const params = {
await fetchCategoryPaginated({ page: snapshot.currentPage,
category: { limit: 24,
id: selectedFilterCategory, brands: snapshot.selectedFilterBrand || undefined,
children: [], min_price: snapshot.minPrice || undefined,
}, max_price: snapshot.maxPrice || undefined,
...fetchParams, sorting: snapshot.sorting || undefined,
}); };
let result = null;
if (snapshot.selectedFilterCategory) {
result = await fetchCategoryPaginated({
category: { id: snapshot.selectedFilterCategory, children: [] },
...params,
}).unwrap();
} else if (snapshot.categoryId && snapshot.isSubCategory) {
result = await fetchCategoryPaginated({
category: { id: parseInt(snapshot.categoryId), children: [] },
...params,
}).unwrap();
} else if (snapshot.brandId) {
result = await fetchBrandPaginated({
id: snapshot.brandId,
...params,
}).unwrap();
} else if (snapshot.collectionId) {
result = await fetchCollectionPaginated({
collectionId: snapshot.collectionId,
...params,
}).unwrap();
}
if (requestId !== activeRequestId.current) return;
if (!result) {
setHasMore(false);
return; return;
} }
if (categoryId && isSubCategory) { const data = result.data || [];
await fetchCategoryPaginated({ const hasNextPage =
category: { result.pagination?.hasMorePages ||
id: parseInt(categoryId), !!result.pagination?.next_page_url ||
children: [], false;
},
...fetchParams,
});
return;
}
if (brandId) { setProducts((prev) => {
await fetchBrandPaginated({ if (snapshot.currentPage === 1) return data;
id: brandId, const existingIds = new Set(prev.map((p) => p.id));
...fetchParams, const newItems = data.filter((p) => !existingIds.has(p.id));
}); return newItems.length > 0 ? [...prev, ...newItems] : prev;
return; });
}
if (collectionId) { setHasMore(data.length > 0 ? hasNextPage : false);
await fetchCollectionPaginated({ } catch (err) {
collectionId, if (requestId !== activeRequestId.current) return;
...fetchParams, console.error("Fetch error:", err);
}); setHasMore(false);
return;
}
} catch (error) {
if (error.name !== "AbortError") {
console.error("Fetch error:", error);
}
} finally { } finally {
isFetchingRef.current = false; if (requestId === activeRequestId.current) {
setIsFetching(false);
}
} }
}; };
executeFetch(); run();
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [ }, [
fetchKey, // useCallback YOK — her dependency değişince effect direkt çalışır
shouldUseBaseQuery,
searchQuery, searchQuery,
selectedFilterBrand, currentPage,
selectedFilterCategory, selectedFilterCategory,
categoryId, categoryId,
isSubCategory, isSubCategory,
brandId, brandId,
collectionId, collectionId,
fetchParams, selectedFilterBrand,
minPrice,
maxPrice,
sorting,
fetchCategoryPaginated, fetchCategoryPaginated,
fetchBrandPaginated, fetchBrandPaginated,
fetchCollectionPaginated, fetchCollectionPaginated,
]); ]);
useEffect(() => { const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
const updateProducts = (newData, hasNextPage) => {
if (!newData || newData.length === 0) {
if (currentPage === 1) {
setProducts([]);
setHasMore(false);
}
return;
}
setProducts((prev) => {
if (currentPage === 1) {
return newData;
}
const existingIds = new Set(prev.map((p) => p.id));
const newProducts = newData.filter((p) => !existingIds.has(p.id));
return newProducts.length > 0 ? [...prev, ...newProducts] : prev;
});
setHasMore(hasNextPage);
};
if (paginatedCategoryProducts && shouldUseBaseQuery) {
updateProducts(
paginatedCategoryProducts.data || [],
!!paginatedCategoryProducts.pagination?.next_page_url,
);
return;
}
if (lazyCategoryProducts) {
updateProducts(
lazyCategoryProducts.data || [],
lazyCategoryProducts.pagination?.hasMorePages || false,
);
return;
}
// Brand products
if (paginatedBrandProducts) {
updateProducts(
paginatedBrandProducts.data || [],
!!paginatedBrandProducts.pagination?.next_page_url,
);
return;
}
if (paginatedCollectionProducts) {
updateProducts(
paginatedCollectionProducts.data || [],
!!paginatedCollectionProducts.pagination?.next_page_url,
);
}
}, [
paginatedCategoryProducts,
lazyCategoryProducts,
paginatedBrandProducts,
paginatedCollectionProducts,
currentPage,
shouldUseBaseQuery,
]);
const isLoading =
categoryLoading ||
lazyCategoryLoading ||
brandPaginatedLoading ||
collectionPaginatedLoading ||
categoryFetching ||
lazyCategoryFetching ||
brandFetching ||
collectionFetching;
return { return {
products, products,

View File

@@ -1,5 +1,3 @@
"use client";
import { useEffect, useState, useMemo, useRef } from "react"; import { useEffect, useState, useMemo, useRef } from "react";
import { useParams, useLocation, useNavigate } from "react-router-dom"; import { useParams, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -25,18 +23,43 @@ const CategoryPage = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [pageState, setPageState] = useState({ const routeKey = useMemo(
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
[categoryId, collectionId, brandId],
);
const getSavedState = (key, defaultVal) => {
try {
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
if (saved) return JSON.parse(saved);
} catch (e) {
console.error(e);
}
return defaultVal;
};
const getSavedStateByKey = (route, key) => {
try {
const saved = sessionStorage.getItem(`category_${key}_${route}`);
if (saved) return JSON.parse(saved);
} catch (e) {
console.error(e);
}
return null;
};
const [pageState, setPageState] = useState(() => getSavedState("pageState", {
currentPage: 1, currentPage: 1,
minPrice: "", minPrice: "",
maxPrice: "", maxPrice: "",
sorting: "", sorting: "",
}); }));
const [filterState, setFilterState] = useState({ const [filterState, setFilterState] = useState(() => getSavedState("filterState", {
selectedFilterCategory: null, selectedFilterCategory: null,
selectedFilterBrand: null, selectedFilterBrand: null,
brandSearchQuery: "", brandSearchQuery: "",
}); }));
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [windowWidth, setWindowWidth] = useState(window.innerWidth);
@@ -47,11 +70,6 @@ const CategoryPage = () => {
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
const routeKey = useMemo(
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
[categoryId, collectionId, brandId],
);
const prevRouteRef = useRef(routeKey); const prevRouteRef = useRef(routeKey);
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
@@ -97,6 +115,8 @@ const CategoryPage = () => {
maxPrice: pageState.maxPrice, maxPrice: pageState.maxPrice,
sorting: pageState.sorting, sorting: pageState.sorting,
searchQuery, searchQuery,
initialProducts: getSavedState("products", []),
initialHasMore: getSavedState("hasMore", true),
}); });
const isMobilePhoneView = const isMobilePhoneView =
(Number(categoryId) === 531 || (Number(categoryId) === 531 ||
@@ -106,6 +126,10 @@ const CategoryPage = () => {
if (isInitialMount.current) { if (isInitialMount.current) {
isInitialMount.current = false; isInitialMount.current = false;
prevRouteRef.current = routeKey; prevRouteRef.current = routeKey;
const savedScroll = getSavedState("scroll", 0);
if (savedScroll > 0) {
setTimeout(() => window.scrollTo(0, savedScroll), 100);
}
return; return;
} }
@@ -113,14 +137,36 @@ const CategoryPage = () => {
prevRouteRef.current = routeKey; prevRouteRef.current = routeKey;
setAllProducts([]); const savedPageState = getSavedStateByKey(routeKey, "pageState");
setHasMore(true); const savedFilterState = getSavedStateByKey(routeKey, "filterState");
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); const savedProducts = getSavedStateByKey(routeKey, "products");
setFilterState({ const savedHasMore = getSavedStateByKey(routeKey, "hasMore");
selectedFilterCategory: null,
selectedFilterBrand: null, if (savedPageState && savedFilterState && savedProducts) {
brandSearchQuery: "", setPageState(savedPageState);
}); setFilterState(savedFilterState);
setAllProducts(savedProducts);
setHasMore(savedHasMore ?? true);
const savedScroll = getSavedStateByKey(routeKey, "scroll");
if (savedScroll !== null) {
setTimeout(() => window.scrollTo(0, savedScroll), 100);
}
} else {
setAllProducts([]);
setHasMore(true);
setPageState({
currentPage: 1,
minPrice: "",
maxPrice: "",
sorting: "",
});
setFilterState({
selectedFilterCategory: null,
selectedFilterBrand: null,
brandSearchQuery: "",
});
window.scrollTo(0, 0);
}
if (location.state?.clearFilters) { if (location.state?.clearFilters) {
navigate(location.pathname, { replace: true, state: {} }); navigate(location.pathname, { replace: true, state: {} });
@@ -134,6 +180,46 @@ const CategoryPage = () => {
setHasMore, setHasMore,
]); ]);
const stateRef = useRef();
useEffect(() => {
stateRef.current = { routeKey, pageState, filterState, allProducts, hasMore };
}, [routeKey, pageState, filterState, allProducts, hasMore]);
useEffect(() => {
if (stateRef.current) {
try {
const { routeKey: key, pageState: ps, filterState: fs, allProducts: ap, hasMore: hm } = stateRef.current;
sessionStorage.setItem(`category_pageState_${key}`, JSON.stringify(ps));
sessionStorage.setItem(`category_filterState_${key}`, JSON.stringify(fs));
sessionStorage.setItem(`category_products_${key}`, JSON.stringify(ap));
sessionStorage.setItem(`category_hasMore_${key}`, JSON.stringify(hm));
} catch (error) {
console.warn("Could not save category state to sessionStorage", error);
}
}
}, [pageState, filterState, allProducts, hasMore, routeKey]);
useEffect(() => {
let scrollTimeout;
const handleScroll = () => {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (stateRef.current) {
try {
sessionStorage.setItem(`category_scroll_${stateRef.current.routeKey}`, JSON.stringify(window.scrollY));
} catch (e) {
// ignore
}
}
}, 100);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
if (scrollTimeout) clearTimeout(scrollTimeout);
};
}, []);
const filteredProducts = useMemo(() => { const filteredProducts = useMemo(() => {
let list = searchQuery ? searchResults : allProducts; let list = searchQuery ? searchResults : allProducts;
@@ -189,7 +275,12 @@ const CategoryPage = () => {
selectedFilterBrand: null, selectedFilterBrand: null,
})); }));
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting })); setPageState((prev) => ({
currentPage: 1,
minPrice: "",
maxPrice: "",
sorting: prev.sorting,
}));
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
@@ -197,15 +288,15 @@ const CategoryPage = () => {
}; };
const handleFilterCategoryDeselect = () => { const handleFilterCategoryDeselect = () => {
setFilterState((prev) => ({ setFilterState((prev) => ({ ...prev, selectedFilterCategory: null }));
setPageState((prev) => ({
...prev, ...prev,
selectedFilterCategory: null, currentPage: 1,
minPrice: "",
maxPrice: "",
})); }));
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
if (categoryId) fetchFilters({ category_id: categoryId }); if (categoryId) fetchFilters({ category_id: categoryId });
}; };
@@ -215,32 +306,29 @@ const CategoryPage = () => {
selectedFilterBrand: brandId, selectedFilterBrand: brandId,
})); }));
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting })); setPageState((prev) => ({
currentPage: 1,
minPrice: "",
maxPrice: "",
sorting: prev.sorting,
}));
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
}; };
const handleFilterBrandDeselect = () => { const handleFilterBrandDeselect = () => {
setFilterState((prev) => ({ setFilterState((prev) => ({ ...prev, selectedFilterBrand: null }));
setPageState((prev) => ({
...prev, ...prev,
selectedFilterBrand: null, currentPage: 1,
minPrice: "",
maxPrice: "",
})); }));
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
setAllProducts([]); setAllProducts([]);
setHasMore(true); setHasMore(true);
}; };
const handleCategoryClick = (targetId) => { const handleCategoryClick = (targetId) => {
setFilterState({
selectedFilterCategory: null,
selectedFilterBrand: null,
brandSearchQuery: "",
});
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
setAllProducts([]);
setHasMore(true);
navigate(`/category/${targetId}`, { navigate(`/category/${targetId}`, {
replace: false, replace: false,
state: { clearFilters: true, timestamp: Date.now() }, state: { clearFilters: true, timestamp: Date.now() },
@@ -314,31 +402,22 @@ const CategoryPage = () => {
minPrice={pageState.minPrice} minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice} maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => { onMinPriceChange={(value) => {
setPageState((prev) => { setAllProducts([]);
// Sadece aktif bir değer girilirse ürünleri sıfırla setHasMore(true);
if (value !== "") { setPageState((prev) => ({
setAllProducts([]); ...prev,
setHasMore(true); minPrice: value,
} currentPage: 1,
return { }));
...prev,
minPrice: value,
currentPage: 1,
};
});
}} }}
onMaxPriceChange={(value) => { onMaxPriceChange={(value) => {
setPageState((prev) => { setAllProducts([]);
if (value !== "") { setHasMore(true);
setAllProducts([]); setPageState((prev) => ({
setHasMore(true); ...prev,
} maxPrice: value,
return { currentPage: 1,
...prev, }));
maxPrice: value,
currentPage: 1,
};
});
}} }}
onCategorySelect={handleFilterCategorySelect} onCategorySelect={handleFilterCategorySelect}
onCategoryDeselect={handleFilterCategoryDeselect} onCategoryDeselect={handleFilterCategoryDeselect}
@@ -351,16 +430,9 @@ const CategoryPage = () => {
onSortingChange={(value) => { onSortingChange={(value) => {
setPageState((prev) => { setPageState((prev) => {
const newSorting = prev.sorting === value ? "" : value; const newSorting = prev.sorting === value ? "" : value;
// Sadece aktif bir sort seçilirse ürünleri sıfırla setAllProducts([]); // her zaman sıfırla
if (newSorting !== "") { setHasMore(true);
setAllProducts([]); return { ...prev, sorting: newSorting, currentPage: 1 };
setHasMore(true);
}
return {
...prev,
sorting: newSorting,
currentPage: 1,
};
}); });
}} }}
/> />
@@ -377,30 +449,22 @@ const CategoryPage = () => {
minPrice={pageState.minPrice} minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice} maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => { onMinPriceChange={(value) => {
setPageState((prev) => { setAllProducts([]);
if (value !== "") { setHasMore(true);
setAllProducts([]); setPageState((prev) => ({
setHasMore(true); ...prev,
} minPrice: value,
return { currentPage: 1,
...prev, }));
minPrice: value,
currentPage: 1,
};
});
}} }}
onMaxPriceChange={(value) => { onMaxPriceChange={(value) => {
setPageState((prev) => { setAllProducts([]);
if (value !== "") { setHasMore(true);
setAllProducts([]); setPageState((prev) => ({
setHasMore(true); ...prev,
} maxPrice: value,
return { currentPage: 1,
...prev, }));
maxPrice: value,
currentPage: 1,
};
});
}} }}
onCategorySelect={handleFilterCategorySelect} onCategorySelect={handleFilterCategorySelect}
onCategoryDeselect={handleFilterCategoryDeselect} onCategoryDeselect={handleFilterCategoryDeselect}
@@ -413,15 +477,9 @@ const CategoryPage = () => {
onSortingChange={(value) => { onSortingChange={(value) => {
setPageState((prev) => { setPageState((prev) => {
const newSorting = prev.sorting === value ? "" : value; const newSorting = prev.sorting === value ? "" : value;
if (newSorting) { setAllProducts([]); // her zaman sıfırla
setAllProducts([]); setHasMore(true);
setHasMore(true); return { ...prev, sorting: newSorting, currentPage: 1 };
}
return {
...prev,
sorting: newSorting,
currentPage: 1,
};
}); });
}} }}
/> />
@@ -444,9 +502,7 @@ const CategoryPage = () => {
scrollableTarget={null} scrollableTarget={null}
style={{ overflow: "hidden" }} style={{ overflow: "hidden" }}
loader={ loader={
<div <div className={`${styles.loaderContainer} `}>
className={`${styles.loaderContainer} `}
>
<Loader /> <Loader />
</div> </div>
} }

View File

@@ -1,79 +1,14 @@
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { useState } from "react";
import styles from "./OrderDetail.module.scss"; import styles from "./OrderDetail.module.scss";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useGetOrderByIdQuery } from "../../app/api/orderApi"; import { useGetOrderByIdQuery } from "../../app/api/orderApi";
import track from "../../assets/track.jpg"; import track from "../../assets/track.jpg";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
import { Result, Button, Modal } from "antd"; import { Result, Button } from "antd";
import PendingPriceBadge from "../../components/PendingPriceBadge";
const isPriceZero = (price) => !price || parseFloat(price) === 0; const isPriceZero = (price) => !price || parseFloat(price) === 0;
const PendingPriceModal = ({ open, onClose, t }) => (
<Modal
open={open}
onOk={onClose}
onCancel={onClose}
okText={t ? t("common.ok") : "OK"}
cancelButtonProps={{ style: { display: "none" } }}
centered
title="Bahasy anyklamaly"
className="pending-price-modal"
width={400}
>
<p>
Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik.
Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.
</p>
</Modal>
);
const PendingPriceBadge = ({ t }) => {
const [tooltipVisible, setTooltipVisible] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
if (isMobile) setModalVisible(true);
};
const stopPropagation = (e) => {
e.stopPropagation();
};
return (
<span onClick={stopPropagation}>
<span
className={styles.pendingPriceBadgeWrapper}
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
onClick={handleClick}
onTouchEnd={(e) => {
e.stopPropagation();
}}
>
<span className={styles.pendingPriceBadge}>!</span>
{tooltipVisible && (
<span className={styles.pendingPriceTooltip}>
<strong>Bahasyny anyklamaly</strong>
Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip
goşmaça maglumat berer.
</span>
)}
</span>
<PendingPriceModal
open={modalVisible}
onClose={() => setModalVisible(false)}
t={t}
/>
</span>
);
};
const OrderDetail = () => { const OrderDetail = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams(); const { id } = useParams();
@@ -242,7 +177,7 @@ const OrderDetail = () => {
gap: 6, gap: 6,
}} }}
> >
<PendingPriceBadge t={t} /> <PendingPriceBadge />
</span> </span>
) : ( ) : (
`${item.unit_price_amount} m.` `${item.unit_price_amount} m.`
@@ -258,7 +193,7 @@ const OrderDetail = () => {
gap: 6, gap: 6,
}} }}
> >
<PendingPriceBadge t={t} /> <PendingPriceBadge />
</span> </span>
) : ( ) : (
`${itemTotal} m.` `${itemTotal} m.`
@@ -318,7 +253,7 @@ const OrderDetail = () => {
gap: 6, gap: 6,
}} }}
> >
<PendingPriceBadge t={t} /> <PendingPriceBadge />
</span> </span>
) : ( ) : (
`${item.unit_price_amount} m.` `${item.unit_price_amount} m.`

View File

@@ -5,79 +5,15 @@ import { useTranslation } from "react-i18next";
import { useGetOrdersQuery } from "../../app/api/orderApi"; import { useGetOrdersQuery } from "../../app/api/orderApi";
import EmptyOrderState from "./emptyOrder"; import EmptyOrderState from "./emptyOrder";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
import { Result, Button, Modal } from "antd"; import { Result, Button } from "antd";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import PendingPriceBadge from "../../components/PendingPriceBadge";
const isPriceZero = (price) => !price || parseFloat(price) === 0; const isPriceZero = (price) => !price || parseFloat(price) === 0;
const orderHasZeroPrice = (orderItems) => const orderHasZeroPrice = (orderItems) =>
orderItems?.some((item) => isPriceZero(item.unit_price_amount)); orderItems?.some((item) => isPriceZero(item.unit_price_amount));
const PendingPriceModal = ({ open, onClose, t }) => (
<Modal
open={open}
onOk={onClose}
onCancel={onClose}
okText={t("common.ok")}
cancelButtonProps={{ style: { display: "none" } }}
centered
title="Bahasy anyklamaly"
className="pending-price-modal"
width={400}
>
<p>
Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik.
Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.
</p>
</Modal>
);
const PendingPriceBadge = ({ t }) => {
const [tooltipVisible, setTooltipVisible] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
if (isMobile) setModalVisible(true);
};
const stopPropagation = (e) => {
e.stopPropagation();
};
return (
<span onClick={stopPropagation}>
<span
className={styles.pendingPriceBadgeWrapper}
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
onClick={handleClick}
onTouchEnd={(e) => {
e.stopPropagation();
}}
>
<span className={styles.pendingPriceBadge}>!</span>
{tooltipVisible && (
<span className={styles.pendingPriceTooltip}>
<strong>Bahasyny anyklamaly</strong>
Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip
goşmaça maglumat berer.
</span>
)}
</span>
<PendingPriceModal
open={modalVisible}
onClose={() => setModalVisible(false)}
t={t}
/>
</span>
);
};
const Orders = () => { const Orders = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: orders, isLoading, error } = useGetOrdersQuery(); const { data: orders, isLoading, error } = useGetOrdersQuery();
@@ -154,7 +90,7 @@ const Orders = () => {
gap: 6, gap: 6,
}} }}
> >
Bahasyny anyklamaly <PendingPriceBadge t={t} /> {t("cart.pendingPriceTitle")} <PendingPriceBadge />
</span> </span>
) : ( ) : (
`${totalAmount.toFixed(2)} m.` `${totalAmount.toFixed(2)} m.`
@@ -224,7 +160,7 @@ const Orders = () => {
gap: 6, gap: 6,
}} }}
> >
Bahasyny anyklamaly <PendingPriceBadge t={t} /> {t("cart.pendingPriceTitle")} <PendingPriceBadge />
</span> </span>
) : ( ) : (
`${totalAmount.toFixed(2)} m.` `${totalAmount.toFixed(2)} m.`

View File

@@ -27,6 +27,9 @@ import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
import { Result, Button } from "antd"; import { Result, Button } from "antd";
import { div } from "framer-motion/client"; import { div } from "framer-motion/client";
import PendingPriceBadge from "../../components/PendingPriceBadge";
const isPriceZero = (price) => !price || parseFloat(price) === 0;
const ProductPage = ({ const ProductPage = ({
productProp, productProp,
@@ -370,6 +373,15 @@ const ProductPage = ({
</span> </span>
</div> </div>
)} )}
{product.properties?.length > 0 && (
product.properties.map((prop, index) => (
<div key={`${prop.attribute_id}-${index}`} className={styles.metaItem}>
<span className={styles.metaLabel}>{prop.name}</span>
<span className={styles.metaValue}>{prop.value}</span>
</div>
))
)}
</div> </div>
{/* Description card */} {/* Description card */}
@@ -426,11 +438,19 @@ const ProductPage = ({
<div className={styles.priceRow}> <div className={styles.priceRow}>
<span className={styles.priceLabel}>{t("product.price")}:</span> <span className={styles.priceLabel}>{t("product.price")}:</span>
<div className={styles.priceRight}> <div className={styles.priceRight}>
<span className={styles.price}>{product.price_amount} m.</span> {isPriceZero(product.price_amount) ? (
{product.old_price_amount && ( <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
<span className={styles.oldPrice}> {t("cart.pendingPriceTitle")} <PendingPriceBadge />
{product.old_price_amount} m.
</span> </span>
) : (
<>
<span className={styles.price}>{product.price_amount} m.</span>
{product.old_price_amount && (
<span className={styles.oldPrice}>
{product.old_price_amount} m.
</span>
)}
</>
)} )}
</div> </div>
</div> </div>
@@ -444,11 +464,19 @@ const ProductPage = ({
{/* ── Mobile sticky bar ── */} {/* ── Mobile sticky bar ── */}
<div className={styles.productActionsMobile}> <div className={styles.productActionsMobile}>
<div className={styles.mobilePriceContainer}> <div className={styles.mobilePriceContainer}>
<span className={styles.price}>{product.price_amount} m.</span> {isPriceZero(product.price_amount) ? (
{product.old_price_amount && ( <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
<span className={styles.oldPrice}> {t("cart.pendingPriceTitle")} <PendingPriceBadge />
{product.old_price_amount} m.
</span> </span>
) : (
<>
<span className={styles.price}>{product.price_amount} m.</span>
{product.old_price_amount && (
<span className={styles.oldPrice}>
{product.old_price_amount} m.
</span>
)}
</>
)} )}
</div> </div>
<div className={styles.mobileBtnContainer}> <div className={styles.mobileBtnContainer}>

View File

@@ -3,6 +3,7 @@ import InfiniteScroll from "react-infinite-scroll-component";
import CategorySection from "../../components/CategorySection/index"; import CategorySection from "../../components/CategorySection/index";
import Carousel from "../../components/Banner/index"; import Carousel from "../../components/Banner/index";
import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel"; import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel";
import HomeBrands from "../../components/HomeBrands/index";
import FlashSales from "../../components/FlashSales"; import FlashSales from "../../components/FlashSales";
import styles from "./Home.module.scss"; import styles from "./Home.module.scss";
import { useGetCollectionsQuery } from "../../app/api/collectionsApi"; import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
@@ -98,6 +99,7 @@ const Home = () => {
<div className={styles.home}> <div className={styles.home}>
<Carousel /> <Carousel />
<CategoryCarousel /> <CategoryCarousel />
<HomeBrands />
<FlashSales /> <FlashSales />
<div className={styles.sections}> <div className={styles.sections}>
<InfiniteScroll <InfiniteScroll