diff --git a/src/app/api/brandsApi.js b/src/app/api/brandsApi.js index c067862..1ab6866 100644 --- a/src/app/api/brandsApi.js +++ b/src/app/api/brandsApi.js @@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({ getBrands: builder.query({ query: (params = {}) => { const queryParams = new URLSearchParams(); - if (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.type) queryParams.append("type", params.type); + if (params.page) queryParams.append("page", params.page); + if (params.limit) queryParams.append("limit", params.limit); const queryString = queryParams.toString(); return `/brands${queryString ? `?${queryString}` : ""}`; }, @@ -28,30 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({ getBrandProducts: builder.query({ query: (params) => { - if (typeof params === 'string' || typeof params === 'number') { + if (typeof params === "string" || typeof params === "number") { return `/brands/${params}/products`; } - const { id, page = 1, limit, sorting, min_price, max_price, brands } = params; - let url = `/brands/${id}/products?page=${page}`; + const { id, page = 1, limit = 24, sorting, min_price, max_price } = params; + 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) { - 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; + return `/brands/${id}/products?${urlParams.toString()}`; }, transformResponse: (response) => ({ data: response.data || response, diff --git a/src/app/api/categories.js b/src/app/api/categories.js index c13401c..4cc07ba 100644 --- a/src/app/api/categories.js +++ b/src/app/api/categories.js @@ -5,17 +5,16 @@ export const categoriesApi = baseApi.injectEndpoints({ getCategories: builder.query({ query: (type = "tree") => `/categories?type=${type}`, }), - + 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(); - 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("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 `categories/${categoryId}/products?${params.toString()}`; }, transformResponse: (response) => ({ @@ -23,80 +22,105 @@ export const categoriesApi = baseApi.injectEndpoints({ pagination: response.pagination || {}, }), }), - + getAllCategoryProducts: builder.query({ - async queryFn(category, queryApi, extraOptions, baseQuery) { + async queryFn(category, _queryApi, _extraOptions, baseQuery) { const fetchProducts = async (categoryId) => { const result = await baseQuery(`categories/${categoryId}/products`); return result.data ? result.data.data : []; }; let allProducts = await fetchProducts(category.id); - for (const child of category.children) { const childProducts = await fetchProducts(child.id); allProducts = [...allProducts, ...childProducts]; } - return { data: allProducts }; }, }), getAllCategoryProductsPaginated: builder.query({ async queryFn( - { category, page = 1, limit = 6, brands, min_price, max_price, sorting }, - queryApi, - extraOptions, + { category, page = 1, limit = 24, brands, min_price, max_price, sorting }, + _queryApi, + _extraOptions, baseQuery ) { - if (!category) return { data: [] }; + if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } }; 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]; - if (category.children && category.children.length > 0) { + if (category.children?.length > 0) { 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( - (hasMore) => hasMore - ); + const result = await baseQuery( + `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 { data: { - data: productsForPage, + data: allProducts, pagination: { currentPage: page, - hasMorePages: hasMorePages, + hasMorePages, }, }, }; @@ -105,11 +129,11 @@ export const categoriesApi = baseApi.injectEndpoints({ } }, }), - + getProductById: builder.query({ query: (productId) => `/products/${productId}`, }), - + getRelatedProducts: builder.query({ query: (productId) => `/products/${productId}/related`, }), diff --git a/src/app/api/collectionsApi.js b/src/app/api/collectionsApi.js index 7de32b2..9019aab 100644 --- a/src/app/api/collectionsApi.js +++ b/src/app/api/collectionsApi.js @@ -5,40 +5,35 @@ export const collectionsApi = baseApi.injectEndpoints({ getCollections: builder.query({ query: () => `/collections`, }), - + getCollectionById: builder.query({ query: (collectionId) => `/collections/${collectionId}`, }), - + getCollectionProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products`, - transformResponse: (response) => { - return { - data: response.data || [], - isEmpty: !response.data || response.data.length === 0, - }; - }, + transformResponse: (response) => ({ + data: response.data || [], + isEmpty: !response.data || response.data.length === 0, + }), }), - + checkCollectionHasProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products?limit=1`, - transformResponse: (response) => { - return { - hasProducts: response.data && response.data.length > 0, - }; - }, + transformResponse: (response) => ({ + hasProducts: response.data && response.data.length > 0, + }), }), - + 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(); - 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); - params.append('sorting', sorting); - + 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); // undefined gelirse gönderme return `/collections/${collectionId}/products?${params.toString()}`; }, transformResponse: (response) => ({ diff --git a/src/components/CategoryDropdown/DropdownMenu.module.scss b/src/components/CategoryDropdown/DropdownMenu.module.scss index 84380f2..2532815 100644 --- a/src/components/CategoryDropdown/DropdownMenu.module.scss +++ b/src/components/CategoryDropdown/DropdownMenu.module.scss @@ -1,235 +1,272 @@ +// DropdownMenu.module.scss + .dropdownContainer { + position: relative; + @media screen and (max-width: 1023px) { display: none; } } +// ---- TRIGGER BUTTON ---- .navButton { 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; + gap: 6px; + border: none; + padding: 0.25rem 0.875rem; border-radius: 0.5rem; height: 2.5rem; - font-size: 0.875rem; + font-size: 16px; + font-weight: 600; color: #4b5563; background-color: transparent; - font-weight: 600; - font-size: 16px; cursor: pointer; + transition: background-color 0.15s, color 0.15s; + position: relative; + z-index: 999; &:hover { 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 { - position: relative; -} - -.dropdownPanel { position: absolute; - top: 100%; - margin-top: 8px; - z-index: 50; - display: flex; - 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; + top: calc(100% + 8px); + left: 0; + z-index: 999; + animation: slideDown 0.18s ease; } -.categoriesList { - flex: 1; - max-height: 500px; - overflow-y: auto; - border-right: 1px solid #ebe7eb; - padding: 20px; +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// ---- PANEL SHELL ---- +.dropdownPanel { display: flex; - flex-direction: column; - gap: 5px; + background: #fff; + 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 { - // width: 6px; - // } +// ---- LEFT LIST ---- +.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 { - background: #e5e7eb; + &::-webkit-scrollbar { + width: 4px; } - &::-webkit-scrollbar-thumb { - background: #d1d5db; - } - .title { - &:hover { - color: #888888; - } - &:active { - color: #888888; - } + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; } } .categoryItem { display: flex; align-items: center; - gap: 8px; - padding: 6px; + justify-content: space-between; + padding: 9px 16px; + font-size: 16px; cursor: pointer; - transition: background-color 0.2s; - border: 1px solid #3615371a; - border-radius: 6px; + color: #000; + transition: background-color 0.12s, color 0.12s; &:hover { - background-color: #f9fafb; + background-color: rgba(255, 255, 255, 0.08); + color: #000; } &.active { - background-color: #f3f4f6; - } + background-color: rgba(255, 255, 255, 0.15); + color: #000; - .icon { - font-size: 14px; - } - - .title { - font-size: 14px; - &:hover { - color: #888888; + .title { + font-weight: 600; } } } +.title { + font-size: 14px; +} + +.chevron { + color: rgba(255, 255, 255, 0.4); + flex-shrink: 0; +} + +// ---- RIGHT CONTENT PANEL ---- .contentPanel { - flex: 3; - padding: 16px; - max-height: 400px; - overflow-y: hidden; + flex: 1; + padding: 20px 24px; + max-height: 520px; + overflow-y: auto; + background: #ffffff; - // &::-webkit-scrollbar { - // width: 6px; - // } - - &::-webkit-scrollbar-track { - background: #e5e7eb; + &::-webkit-scrollbar { + width: 4px; } - &::-webkit-scrollbar-thumb { background: #d1d5db; - // border-radius: 3px; - } - .title { - cursor: pointer; - color: #361517; - font-size: 24px; - font-weight: 600; - &:hover { - color: #888888; - } + border-radius: 2px; } } -.column { - display: flex; - flex-direction: column; - flex: 2; - text-align: left; + +.panelTitle { + font-size: 20px; + font-weight: 700; + 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 { - 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; - color: #361517; - padding: 4px 0; + font-weight: 800; + color: #111827; + margin-bottom: 6px; cursor: pointer; - transition: color 0.2s; &:hover { - color: #888888; + color: #e63946; } } -.subCategoriesContainer { - display: flex; - flex-direction: column; - max-height: 360px; - overflow-y: auto; -} - -.nestedCategoryContainer:last-child { - margin-bottom: 16px; -} - -.nestedCategoryContainer { - margin-bottom: 4px; -} - -.nestedCategoryItem { - display: flex; - align-items: center; - padding: 8px 12px; - border-radius: 4px; +.leafItem { + display: block; + font-size: 14px; + color: #4b5563; + padding: 3px 0; 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 { background-color: #f3f4f6; - } - - .categoryLabel { - flex: 1; - display: flex; - align-items: center; - } - - .title { - font-size: 14px; + color: #111827; } } -.expandButton, -.navigateButton { +.navButtonLoading { + opacity: 0.7; + cursor: wait; + + .categoryIcon { + opacity: 0.4; + } +} + +.loadingDots { display: flex; + gap: 3px; align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 4px; - background-color: transparent; - border: none; - cursor: pointer; - &:hover { - background-color: #e5e7eb; + span { + 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 { - margin-top: 4px; -} - -.noSubcategories { - color: #6b7280; - font-style: italic; -} +@keyframes dotPulse { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} \ No newline at end of file diff --git a/src/components/CategoryDropdown/index.jsx b/src/components/CategoryDropdown/index.jsx index d73309e..0e339ab 100644 --- a/src/components/CategoryDropdown/index.jsx +++ b/src/components/CategoryDropdown/index.jsx @@ -1,83 +1,62 @@ +// DropdownMenu.jsx import React, { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import { ChevronRight } from "lucide-react"; import styles from "./DropdownMenu.module.scss"; import { useGetCategoriesQuery } from "../../app/api/categories"; import { CategoryIcon } from "../Icons"; -import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar -const NestedCategory = ({ - category, - level = 0, - handleCategorySelect, - closeDropdown, -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const hasChildren = category.children && category.children.length > 0; +const ContentPanel = ({ category, onSelect, onClose }) => { + if (!category) return null; - const handleClick = (e) => { - e.stopPropagation(); - if (hasChildren) { - setIsExpanded(!isExpanded); - } else { - handleCategorySelect(category); - closeDropdown(); - } - }; + const children = category.children || []; + const withChildren = children.filter((c) => c.children?.length > 0); + const withoutChildren = children.filter((c) => !c.children?.length); - const handleDirectNavigation = (e) => { - e.stopPropagation(); - handleCategorySelect(category); - closeDropdown(); - }; + const allColumns = [ + ...withChildren, + ...withoutChildren.map((c) => ({ ...c, children: [] })), + ]; return ( -
-
-
- {category.name} -
+
+

{ + onSelect(category); + onClose(); + }} + > + {category.name} +

- {hasChildren && ( - - )} - - {hasChildren && ( - - )} -
- - {hasChildren && isExpanded && ( -
- {category.children.map((child) => ( - + {allColumns.length > 0 && ( +
+ {allColumns.map((sub) => ( +
+
{ + onSelect(sub); + onClose(); + }} + > + {sub.name} +
+ {sub.children?.map((leaf) => ( + { + onSelect(leaf); + onClose(); + }} + > + {leaf.name} + + ))} +
))}
)} @@ -89,113 +68,85 @@ const DropdownMenu = () => { const { t } = useTranslation(); const navigate = useNavigate(); const dropdownRef = useRef(null); + const { data: categoriesData, isLoading, error, } = useGetCategoriesQuery("tree"); - const categories = categoriesData?.data || []; + const [isOpen, setIsOpen] = useState(false); - const [activeMainCategory, setActiveMainCategory] = useState(null); + const [activeCategory, setActiveCategory] = useState(null); useEffect(() => { - if (categories.length > 0) { - const defaultCategory = - categories.find((cat) => cat.name === "Aýallar üçin") || categories[0]; - setActiveMainCategory(defaultCategory); + if (categories.length > 0 && !activeCategory) { + setActiveCategory(categories[0]); } }, [categories]); - const handleToggle = () => { - setIsOpen(!isOpen); - }; + useEffect(() => { + const handler = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); - const handleMouseLeave = () => { - if (categories.length > 0) { - const defaultCategory = - categories.find((cat) => cat.name === "Aýallar üçin") || categories[0]; - setActiveMainCategory(defaultCategory); - } - }; - - const handleCategorySelect = (category) => { + const handleSelect = (category) => { navigate(`/category/${category.id}`, { state: { category } }); 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
Loading...
; - if (error) return
Error loading categories
; return (
- {isOpen && ( -
-
-
- {categories.map((category) => ( -
setActiveMainCategory(category)} - onClick={() => handleCategorySelect(category)} - > - {category.name} -
- ))} -
+ <> +
setIsOpen(false)} /> - {activeMainCategory && ( -
-

handleCategorySelect(activeMainCategory)} - className={styles.title} - > - {activeMainCategory.name} -

- -
- {activeMainCategory.children && - activeMainCategory.children.length > 0 ? ( - activeMainCategory.children.map((subcategory) => ( - setIsOpen(false)} - /> - )) - ) : ( -
- {/* No subcategories available */} -
- )} -
+
+
+
+ {categories.map((cat) => ( +
setActiveCategory(cat)} + onClick={() => handleSelect(cat)} + > + {cat.name} + {cat.children?.length > 0 && ( + + )} +
+ ))}
- )} + + setIsOpen(false)} + /> +
-
+ )}
); diff --git a/src/components/HomeBrands/HomeBrands.module.scss b/src/components/HomeBrands/HomeBrands.module.scss new file mode 100644 index 0000000..71c05e9 --- /dev/null +++ b/src/components/HomeBrands/HomeBrands.module.scss @@ -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; + } +} diff --git a/src/components/HomeBrands/index.jsx b/src/components/HomeBrands/index.jsx new file mode 100644 index 0000000..b7ff7d2 --- /dev/null +++ b/src/components/HomeBrands/index.jsx @@ -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 ( +
+
+ {randomBrands.map((brand) => ( +
navigate(`/brands/${brand.id}`)} + > + {brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? ( + {brand.name} { + e.target.style.display = "none"; + e.target.nextSibling.style.display = "flex"; + }} + /> + ) : ( +
+ +
+ )} +
+ +
+
+ ))} +
navigate('/brands')} + > + {getMoreText()} + +
+
+
+ ); +}; + +export default HomeBrands; diff --git a/src/components/Icons/index.jsx b/src/components/Icons/index.jsx index 7b12c4b..c8f84da 100644 --- a/src/components/Icons/index.jsx +++ b/src/components/Icons/index.jsx @@ -221,7 +221,7 @@ export const CategoryIcon = () => ( height={20} > diff --git a/src/components/Navbar/Navbar.module.scss b/src/components/Navbar/Navbar.module.scss index eb1cef4..cfc5fff 100644 --- a/src/components/Navbar/Navbar.module.scss +++ b/src/components/Navbar/Navbar.module.scss @@ -39,7 +39,7 @@ } &__satyjy { - @media screen and (max-width: 500px) { + @media screen and (max-width: 785px) { display: none; } } @@ -288,7 +288,7 @@ align-items: center; gap: 8px; margin-left: auto; - @media screen and (max-width: 500px) { + @media screen and (max-width: 708px) { display: none; } } diff --git a/src/components/PendingPriceBadge/PendingPriceBadge.module.scss b/src/components/PendingPriceBadge/PendingPriceBadge.module.scss new file mode 100644 index 0000000..5363c8a --- /dev/null +++ b/src/components/PendingPriceBadge/PendingPriceBadge.module.scss @@ -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; + } + } + } + } +} diff --git a/src/components/PendingPriceBadge/index.jsx b/src/components/PendingPriceBadge/index.jsx new file mode 100644 index 0000000..17624b5 --- /dev/null +++ b/src/components/PendingPriceBadge/index.jsx @@ -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 }) => ( + +

+ {t("cart.pendingPriceDesc") || + "Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer."} +

+
+); + +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 ( + + setTooltipVisible(true)} + onMouseLeave={() => setTooltipVisible(false)} + onClick={handleClick} + onTouchEnd={(e) => { + e.stopPropagation(); + }} + > + ! + + {tooltipVisible && ( + + {t("cart.pendingPriceTitle") || "Bahasyny anyklamaly"} + {t("cart.pendingPriceTooltipDesc") || + "Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."} + + )} + + + setModalVisible(false)} + t={t} + /> + + ); +}; + +export default PendingPriceBadge; diff --git a/src/components/ProductCard/index.jsx b/src/components/ProductCard/index.jsx index 5c81269..2e8a2d1 100644 --- a/src/components/ProductCard/index.jsx +++ b/src/components/ProductCard/index.jsx @@ -201,9 +201,9 @@ const ProductCard = ({ onMouseLeave={() => setIsHovered(false)} >
- {(product.discount || calculatedDiscount) && ( + {(product.discount > 0 || calculatedDiscount > 0) && ( - -{product.discount ?? calculatedDiscount}% + -{product.discount || calculatedDiscount}% )} {product.stock === 0 && ( @@ -221,7 +221,7 @@ const ProductCard = ({
{isPriceZero(price_amount) ? ( - Bahasyny anyklamaly + {t("cart.pendingPriceTitle")} ) : ( <> {price_amount} m. diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index bc5ff33..5818676 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -38,6 +38,9 @@ export default { emptyCartTitle: "Your cart is empty", emptyCartMessage: "Looks like you haven't added any items to your cart yet", 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: { paymentMethod: "Payment Method", diff --git a/src/i18n/locales/ru.js b/src/i18n/locales/ru.js index 6ab997a..4c45e24 100644 --- a/src/i18n/locales/ru.js +++ b/src/i18n/locales/ru.js @@ -38,6 +38,9 @@ export default { emptyCartTitle: "Ваша корзина пуста", emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину", continueShopping: "Продолжить покупки", + pendingPriceTitle: "Цена уточняется", + pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.", + pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию." }, checkout: { paymentMethod: "Способ оплаты", diff --git a/src/i18n/locales/tm.js b/src/i18n/locales/tm.js index bba079b..ef26fae 100644 --- a/src/i18n/locales/tm.js +++ b/src/i18n/locales/tm.js @@ -38,6 +38,9 @@ export default { emptyCartTitle: "Sebediňiz boş", emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.", 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: { paymentMethod: "Töleg görnüşi", diff --git a/src/pages/Cart/CartPage.module.scss b/src/pages/Cart/CartPage.module.scss index 3b9d384..538c7c2 100644 --- a/src/pages/Cart/CartPage.module.scss +++ b/src/pages/Cart/CartPage.module.scss @@ -31,7 +31,6 @@ } } } - .cartProducts { display: flex; justify-content: space-between; @@ -152,6 +151,7 @@ @media screen and (max-width: 720px) { flex-direction: row-reverse; justify-content: space-between; + gap:10px; } .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; + } + } + } + } +} diff --git a/src/pages/Cart/index.jsx b/src/pages/Cart/index.jsx index 1778aeb..3f3e72a 100644 --- a/src/pages/Cart/index.jsx +++ b/src/pages/Cart/index.jsx @@ -13,6 +13,7 @@ import { import { useCart } from "../../app/api/useCart"; import { DecreaseIcon, IncreaseIcon } from "../../components/Icons"; import Loader from "../../components/Loader/index"; +import PendingPriceBadge from "../../components/PendingPriceBadge"; const isPriceZero = (price) => !price || parseFloat(price) === 0; @@ -320,7 +321,7 @@ const CartPage = () => {
{isPriceZero(item.product.price_amount) - ? "Bahasyny anyklamaly" + ? t("cart.pendingPriceTitle") : `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
@@ -375,20 +376,12 @@ const CartPage = () => { {store.name} - {t("cart.basket")}: {hasZeroPrice ? ( - <> -
- {t("cart.price")}: - Bahasyny anyklamaly -
-
- {t("cart.delivery")}: - Bahasyny anyklamaly -
-
- {t("cart.total")}: - Bahasyny anyklamaly -
- +
+ {t("cart.total")}: + + {t("cart.pendingPriceTitle")} + +
) : ( <>
diff --git a/src/pages/Category/CategoryPage.module.scss b/src/pages/Category/CategoryPage.module.scss index 020d91a..84dfeba 100644 --- a/src/pages/Category/CategoryPage.module.scss +++ b/src/pages/Category/CategoryPage.module.scss @@ -128,6 +128,7 @@ gap: 6px; flex: 1; min-width: 0; + } .priceLabel { @@ -144,7 +145,7 @@ font-size: 14px; background: #fff; transition: all 0.2s ease; - +width: 85%; &::placeholder { color: #bbb; } diff --git a/src/pages/Category/hooks/useCategoryProducts.js b/src/pages/Category/hooks/useCategoryProducts.js index 74e3f86..91810d7 100644 --- a/src/pages/Category/hooks/useCategoryProducts.js +++ b/src/pages/Category/hooks/useCategoryProducts.js @@ -1,7 +1,7 @@ -import { useState, useEffect, useMemo, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { - useGetCategoryProductsQuery, useLazyGetAllCategoryProductsPaginatedQuery, + useGetCategoryProductsQuery, } from "../../../app/api/categories"; import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi"; import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi"; @@ -19,50 +19,16 @@ const useCategoryProducts = ({ maxPrice, sorting, searchQuery, + initialProducts = [], + initialHasMore = true, }) => { - const [products, setProducts] = useState([]); - const [hasMore, setHasMore] = useState(true); + const [products, setProducts] = useState(initialProducts); + const [hasMore, setHasMore] = useState(initialHasMore); + const [isFetching, setIsFetching] = useState(false); - const isFetchingRef = useRef(false); - const lastFetchKeyRef = useRef(null); - const abortControllerRef = useRef(null); - - 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 activeRequestId = useRef(0); + // Tüm parametreleri ref'te tut — stale closure'ı tamamen engelle + const paramsRef = useRef({}); const shouldUseBaseQuery = categoryId && @@ -72,238 +38,146 @@ const useCategoryProducts = ({ !brandId && !collectionId; - const { - data: paginatedCategoryProducts, - isLoading: categoryLoading, - isFetching: categoryFetching, - } = useGetCategoryProductsQuery( - { - categoryId: categoryId, - page: currentPage, - min_price: minPrice || undefined, - max_price: maxPrice || undefined, - brands: selectedFilterBrand || undefined, - sorting: sorting || undefined, - }, - { - skip: !shouldUseBaseQuery, - }, - ); + const { data: baseQueryData, isFetching: baseQueryFetching } = + useGetCategoryProductsQuery( + { + categoryId, + page: currentPage, + min_price: minPrice || undefined, + max_price: maxPrice || undefined, + brands: selectedFilterBrand || undefined, + sorting: sorting || undefined, + }, + { skip: !shouldUseBaseQuery } + ); - const [ - fetchCategoryPaginated, - { - 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(); + const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery(); + const [fetchBrandPaginated] = useLazyGetBrandProductsQuery(); + const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery(); + // Base query handler useEffect(() => { - setProducts([]); - setHasMore(true); - - resetCategoryPaginated?.(); - resetBrandPaginated?.(); - resetCollectionPaginated?.(); - - lastFetchKeyRef.current = null; - isFetchingRef.current = false; - - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; - } - }, [ - contextId, - resetCategoryPaginated, - resetBrandPaginated, - resetCollectionPaginated, - ]); + if (!shouldUseBaseQuery || !baseQueryData) return; + const data = baseQueryData.data || []; + const hasNextPage = !!baseQueryData.pagination?.next_page_url; + setProducts((prev) => { + if (currentPage === 1) return data; + const existingIds = new Set(prev.map((p) => p.id)); + const newItems = data.filter((p) => !existingIds.has(p.id)); + return newItems.length > 0 ? [...prev, ...newItems] : prev; + }); + setHasMore(hasNextPage); + }, [baseQueryData, currentPage, shouldUseBaseQuery]); + // Her fetch çağrısını doğrudan effect içinde yap — useCallback kaldırıldı useEffect(() => { - if (searchQuery) return; + if (shouldUseBaseQuery || searchQuery) return; - if (lastFetchKeyRef.current === fetchKey) { - return; - } + // Parametreleri snapshot al + const snapshot = { + currentPage, + selectedFilterCategory, + categoryId, + isSubCategory, + brandId, + collectionId, + selectedFilterBrand, + minPrice, + maxPrice, + sorting, + }; - if (isFetchingRef.current) { - return; - } + const requestId = ++activeRequestId.current; + setIsFetching(true); - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - - abortControllerRef.current = new AbortController(); - isFetchingRef.current = true; - lastFetchKeyRef.current = fetchKey; - - const executeFetch = async () => { + const run = async () => { try { - if (selectedFilterCategory) { - await fetchCategoryPaginated({ - category: { - id: selectedFilterCategory, - children: [], - }, - ...fetchParams, - }); + const params = { + page: snapshot.currentPage, + limit: 24, + brands: snapshot.selectedFilterBrand || undefined, + min_price: snapshot.minPrice || undefined, + max_price: snapshot.maxPrice || undefined, + 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; } - if (categoryId && isSubCategory) { - await fetchCategoryPaginated({ - category: { - id: parseInt(categoryId), - children: [], - }, - ...fetchParams, - }); - return; - } + const data = result.data || []; + const hasNextPage = + result.pagination?.hasMorePages || + !!result.pagination?.next_page_url || + false; - if (brandId) { - await fetchBrandPaginated({ - id: brandId, - ...fetchParams, - }); - return; - } + setProducts((prev) => { + if (snapshot.currentPage === 1) return data; + const existingIds = new Set(prev.map((p) => p.id)); + const newItems = data.filter((p) => !existingIds.has(p.id)); + return newItems.length > 0 ? [...prev, ...newItems] : prev; + }); - if (collectionId) { - await fetchCollectionPaginated({ - collectionId, - ...fetchParams, - }); - return; - } - } catch (error) { - if (error.name !== "AbortError") { - console.error("Fetch error:", error); - } + setHasMore(data.length > 0 ? hasNextPage : false); + } catch (err) { + if (requestId !== activeRequestId.current) return; + console.error("Fetch error:", err); + setHasMore(false); } finally { - isFetchingRef.current = false; + if (requestId === activeRequestId.current) { + setIsFetching(false); + } } }; - executeFetch(); - - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - }; + run(); }, [ - fetchKey, + // useCallback YOK — her dependency değişince effect direkt çalışır + shouldUseBaseQuery, searchQuery, - selectedFilterBrand, + currentPage, selectedFilterCategory, categoryId, isSubCategory, brandId, collectionId, - fetchParams, + selectedFilterBrand, + minPrice, + maxPrice, + sorting, fetchCategoryPaginated, fetchBrandPaginated, fetchCollectionPaginated, ]); - useEffect(() => { - 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; + const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching; return { products, @@ -314,4 +188,4 @@ const useCategoryProducts = ({ }; }; -export default useCategoryProducts; +export default useCategoryProducts; \ No newline at end of file diff --git a/src/pages/Category/index.jsx b/src/pages/Category/index.jsx index 7f5648a..b11c0be 100644 --- a/src/pages/Category/index.jsx +++ b/src/pages/Category/index.jsx @@ -1,5 +1,3 @@ -"use client"; - import { useEffect, useState, useMemo, useRef } from "react"; import { useParams, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -25,18 +23,43 @@ const CategoryPage = () => { const location = useLocation(); 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, minPrice: "", maxPrice: "", sorting: "", - }); + })); - const [filterState, setFilterState] = useState({ + const [filterState, setFilterState] = useState(() => getSavedState("filterState", { selectedFilterCategory: null, selectedFilterBrand: null, brandSearchQuery: "", - }); + })); const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const [windowWidth, setWindowWidth] = useState(window.innerWidth); @@ -47,11 +70,6 @@ const CategoryPage = () => { return () => window.removeEventListener("resize", handleResize); }, []); - const routeKey = useMemo( - () => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`, - [categoryId, collectionId, brandId], - ); - const prevRouteRef = useRef(routeKey); const isInitialMount = useRef(true); @@ -97,6 +115,8 @@ const CategoryPage = () => { maxPrice: pageState.maxPrice, sorting: pageState.sorting, searchQuery, + initialProducts: getSavedState("products", []), + initialHasMore: getSavedState("hasMore", true), }); const isMobilePhoneView = (Number(categoryId) === 531 || @@ -106,6 +126,10 @@ const CategoryPage = () => { if (isInitialMount.current) { isInitialMount.current = false; prevRouteRef.current = routeKey; + const savedScroll = getSavedState("scroll", 0); + if (savedScroll > 0) { + setTimeout(() => window.scrollTo(0, savedScroll), 100); + } return; } @@ -113,14 +137,36 @@ const CategoryPage = () => { prevRouteRef.current = routeKey; - setAllProducts([]); - setHasMore(true); - setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); - setFilterState({ - selectedFilterCategory: null, - selectedFilterBrand: null, - brandSearchQuery: "", - }); + const savedPageState = getSavedStateByKey(routeKey, "pageState"); + const savedFilterState = getSavedStateByKey(routeKey, "filterState"); + const savedProducts = getSavedStateByKey(routeKey, "products"); + const savedHasMore = getSavedStateByKey(routeKey, "hasMore"); + + if (savedPageState && savedFilterState && savedProducts) { + 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) { navigate(location.pathname, { replace: true, state: {} }); @@ -134,6 +180,46 @@ const CategoryPage = () => { 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(() => { let list = searchQuery ? searchResults : allProducts; @@ -189,7 +275,12 @@ const CategoryPage = () => { selectedFilterBrand: null, })); - setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting })); + setPageState((prev) => ({ + currentPage: 1, + minPrice: "", + maxPrice: "", + sorting: prev.sorting, + })); setAllProducts([]); setHasMore(true); @@ -197,15 +288,15 @@ const CategoryPage = () => { }; const handleFilterCategoryDeselect = () => { - setFilterState((prev) => ({ + setFilterState((prev) => ({ ...prev, selectedFilterCategory: null })); + setPageState((prev) => ({ ...prev, - selectedFilterCategory: null, + currentPage: 1, + minPrice: "", + maxPrice: "", })); - - setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); setAllProducts([]); setHasMore(true); - if (categoryId) fetchFilters({ category_id: categoryId }); }; @@ -215,32 +306,29 @@ const CategoryPage = () => { selectedFilterBrand: brandId, })); - setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting })); + setPageState((prev) => ({ + currentPage: 1, + minPrice: "", + maxPrice: "", + sorting: prev.sorting, + })); setAllProducts([]); setHasMore(true); }; const handleFilterBrandDeselect = () => { - setFilterState((prev) => ({ + setFilterState((prev) => ({ ...prev, selectedFilterBrand: null })); + setPageState((prev) => ({ ...prev, - selectedFilterBrand: null, + currentPage: 1, + minPrice: "", + maxPrice: "", })); - - setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); setAllProducts([]); setHasMore(true); }; const handleCategoryClick = (targetId) => { - setFilterState({ - selectedFilterCategory: null, - selectedFilterBrand: null, - brandSearchQuery: "", - }); - setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); - setAllProducts([]); - setHasMore(true); - navigate(`/category/${targetId}`, { replace: false, state: { clearFilters: true, timestamp: Date.now() }, @@ -314,31 +402,22 @@ const CategoryPage = () => { minPrice={pageState.minPrice} maxPrice={pageState.maxPrice} onMinPriceChange={(value) => { - setPageState((prev) => { - // Sadece aktif bir değer girilirse ürünleri sıfırla - if (value !== "") { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - minPrice: value, - currentPage: 1, - }; - }); + setAllProducts([]); + setHasMore(true); + setPageState((prev) => ({ + ...prev, + minPrice: value, + currentPage: 1, + })); }} onMaxPriceChange={(value) => { - setPageState((prev) => { - if (value !== "") { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - maxPrice: value, - currentPage: 1, - }; - }); + setAllProducts([]); + setHasMore(true); + setPageState((prev) => ({ + ...prev, + maxPrice: value, + currentPage: 1, + })); }} onCategorySelect={handleFilterCategorySelect} onCategoryDeselect={handleFilterCategoryDeselect} @@ -351,16 +430,9 @@ const CategoryPage = () => { onSortingChange={(value) => { setPageState((prev) => { const newSorting = prev.sorting === value ? "" : value; - // Sadece aktif bir sort seçilirse ürünleri sıfırla - if (newSorting !== "") { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - sorting: newSorting, - currentPage: 1, - }; + setAllProducts([]); // her zaman sıfırla + setHasMore(true); + return { ...prev, sorting: newSorting, currentPage: 1 }; }); }} /> @@ -377,30 +449,22 @@ const CategoryPage = () => { minPrice={pageState.minPrice} maxPrice={pageState.maxPrice} onMinPriceChange={(value) => { - setPageState((prev) => { - if (value !== "") { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - minPrice: value, - currentPage: 1, - }; - }); + setAllProducts([]); + setHasMore(true); + setPageState((prev) => ({ + ...prev, + minPrice: value, + currentPage: 1, + })); }} onMaxPriceChange={(value) => { - setPageState((prev) => { - if (value !== "") { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - maxPrice: value, - currentPage: 1, - }; - }); + setAllProducts([]); + setHasMore(true); + setPageState((prev) => ({ + ...prev, + maxPrice: value, + currentPage: 1, + })); }} onCategorySelect={handleFilterCategorySelect} onCategoryDeselect={handleFilterCategoryDeselect} @@ -413,15 +477,9 @@ const CategoryPage = () => { onSortingChange={(value) => { setPageState((prev) => { const newSorting = prev.sorting === value ? "" : value; - if (newSorting) { - setAllProducts([]); - setHasMore(true); - } - return { - ...prev, - sorting: newSorting, - currentPage: 1, - }; + setAllProducts([]); // her zaman sıfırla + setHasMore(true); + return { ...prev, sorting: newSorting, currentPage: 1 }; }); }} /> @@ -441,12 +499,10 @@ const CategoryPage = () => { next={loadMoreData} hasMore={hasMore} scrollThreshold={0.8} - scrollableTarget={null} - style={{ overflow: "hidden" }} + scrollableTarget={null} + style={{ overflow: "hidden" }} loader={ -
+
} diff --git a/src/pages/OrderDetail/index.jsx b/src/pages/OrderDetail/index.jsx index 70d1ad8..51d3fa6 100644 --- a/src/pages/OrderDetail/index.jsx +++ b/src/pages/OrderDetail/index.jsx @@ -1,79 +1,14 @@ import { useParams, useNavigate } from "react-router-dom"; -import { useState } from "react"; import styles from "./OrderDetail.module.scss"; import { useTranslation } from "react-i18next"; import { useGetOrderByIdQuery } from "../../app/api/orderApi"; import track from "../../assets/track.jpg"; 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 PendingPriceModal = ({ open, onClose, t }) => ( - -

- Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. - Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer. -

-
-); - -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 ( - - !isMobile && setTooltipVisible(true)} - onMouseLeave={() => setTooltipVisible(false)} - onClick={handleClick} - onTouchEnd={(e) => { - e.stopPropagation(); - }} - > - ! - - {tooltipVisible && ( - - Bahasyny anyklamaly - Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip - goşmaça maglumat berer. - - )} - - - setModalVisible(false)} - t={t} - /> - - ); -}; - const OrderDetail = () => { const { t } = useTranslation(); const { id } = useParams(); @@ -242,7 +177,7 @@ const OrderDetail = () => { gap: 6, }} > - + ) : ( `${item.unit_price_amount} m.` @@ -258,7 +193,7 @@ const OrderDetail = () => { gap: 6, }} > - + ) : ( `${itemTotal} m.` @@ -318,7 +253,7 @@ const OrderDetail = () => { gap: 6, }} > - + ) : ( `${item.unit_price_amount} m.` diff --git a/src/pages/Orders/index.jsx b/src/pages/Orders/index.jsx index 4fa697e..bd01549 100644 --- a/src/pages/Orders/index.jsx +++ b/src/pages/Orders/index.jsx @@ -5,79 +5,15 @@ import { useTranslation } from "react-i18next"; import { useGetOrdersQuery } from "../../app/api/orderApi"; import EmptyOrderState from "./emptyOrder"; import Loader from "../../components/Loader/index"; -import { Result, Button, Modal } from "antd"; +import { Result, Button } from "antd"; import { useNavigate } from "react-router-dom"; +import PendingPriceBadge from "../../components/PendingPriceBadge"; const isPriceZero = (price) => !price || parseFloat(price) === 0; const orderHasZeroPrice = (orderItems) => orderItems?.some((item) => isPriceZero(item.unit_price_amount)); -const PendingPriceModal = ({ open, onClose, t }) => ( - -

- Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. - Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer. -

-
-); - -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 ( - - !isMobile && setTooltipVisible(true)} - onMouseLeave={() => setTooltipVisible(false)} - onClick={handleClick} - onTouchEnd={(e) => { - e.stopPropagation(); - }} - > - ! - - {tooltipVisible && ( - - Bahasyny anyklamaly - Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip - goşmaça maglumat berer. - - )} - - - setModalVisible(false)} - t={t} - /> - - ); -}; - const Orders = () => { const { t } = useTranslation(); const { data: orders, isLoading, error } = useGetOrdersQuery(); @@ -154,7 +90,7 @@ const Orders = () => { gap: 6, }} > - Bahasyny anyklamaly + {t("cart.pendingPriceTitle")} ) : ( `${totalAmount.toFixed(2)} m.` @@ -224,7 +160,7 @@ const Orders = () => { gap: 6, }} > - Bahasyny anyklamaly + {t("cart.pendingPriceTitle")} ) : ( `${totalAmount.toFixed(2)} m.` diff --git a/src/pages/ProductDetail/index.jsx b/src/pages/ProductDetail/index.jsx index 6c7c128..49a110d 100644 --- a/src/pages/ProductDetail/index.jsx +++ b/src/pages/ProductDetail/index.jsx @@ -27,6 +27,9 @@ import ImageCarousel from "../../components/ProductCard/imageCarousel/index"; import Loader from "../../components/Loader/index"; import { Result, Button } from "antd"; import { div } from "framer-motion/client"; +import PendingPriceBadge from "../../components/PendingPriceBadge"; + +const isPriceZero = (price) => !price || parseFloat(price) === 0; const ProductPage = ({ productProp, @@ -370,6 +373,15 @@ const ProductPage = ({
)} + + {product.properties?.length > 0 && ( + product.properties.map((prop, index) => ( +
+ {prop.name} + {prop.value} +
+ )) + )}
{/* Description card */} @@ -426,11 +438,19 @@ const ProductPage = ({
{t("product.price")}:
- {product.price_amount} m. - {product.old_price_amount && ( - - {product.old_price_amount} m. + {isPriceZero(product.price_amount) ? ( + + {t("cart.pendingPriceTitle")} + ) : ( + <> + {product.price_amount} m. + {product.old_price_amount && ( + + {product.old_price_amount} m. + + )} + )}
@@ -444,11 +464,19 @@ const ProductPage = ({ {/* ── Mobile sticky bar ── */}
- {product.price_amount} m. - {product.old_price_amount && ( - - {product.old_price_amount} m. + {isPriceZero(product.price_amount) ? ( + + {t("cart.pendingPriceTitle")} + ) : ( + <> + {product.price_amount} m. + {product.old_price_amount && ( + + {product.old_price_amount} m. + + )} + )}
diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx index 97c3fa1..3caa8b4 100644 --- a/src/pages/home/index.jsx +++ b/src/pages/home/index.jsx @@ -3,6 +3,7 @@ import InfiniteScroll from "react-infinite-scroll-component"; import CategorySection from "../../components/CategorySection/index"; import Carousel from "../../components/Banner/index"; import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel"; +import HomeBrands from "../../components/HomeBrands/index"; import FlashSales from "../../components/FlashSales"; import styles from "./Home.module.scss"; import { useGetCollectionsQuery } from "../../app/api/collectionsApi"; @@ -98,6 +99,7 @@ const Home = () => {
+