added price filter, changed mobile filter ui

This commit is contained in:
@jcarymuhammedow
2026-02-25 20:31:49 +05:00
parent 53346b5a7b
commit 4e58062899
16 changed files with 530 additions and 536 deletions

View File

@@ -72,7 +72,7 @@ const customBaseQuery = async (args, api, extraOptions) => {
"Content-Type": "application/json", "Content-Type": "application/json",
"Api-Token": import.meta.env.VITE_API_TOKEN || "hello-mf-s", "Api-Token": import.meta.env.VITE_API_TOKEN || "hello-mf-s",
}, },
} },
); );
const data = await guestTokenResponse.json(); const data = await guestTokenResponse.json();
@@ -120,5 +120,6 @@ const customBaseQuery = async (args, api, extraOptions) => {
export const baseApi = createApi({ export const baseApi = createApi({
reducerPath: "api", reducerPath: "api",
baseQuery: customBaseQuery, baseQuery: customBaseQuery,
tagTypes: ["Favorites", "cartItems", "Orders"],
endpoints: () => ({}), endpoints: () => ({}),
}); });

View File

@@ -1,29 +1,36 @@
// hooks/useCart.js - YENİ DOSYA // hooks/useCart.js
import { useMemo } from 'react'; import { useMemo } from "react";
import { useGetCartQuery } from './cartApi'; import { useGetCartQuery } from "./cartApi";
export const useCart = () => { export const useCart = () => {
const { data: cartData, ...rest } = useGetCartQuery(undefined, { const queryResult = useGetCartQuery(undefined, {
pollingInterval: 0, pollingInterval: 0,
refetchOnMountOrArgChange: false, refetchOnMountOrArgChange: false, // Cache'den kullan, gereksiz GET'i engelle
refetchOnFocus: false, refetchOnFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
}); });
const { data: response = {} } = queryResult;
const cartData = response.data || {};
const cartItems = useMemo(() => { const cartItems = useMemo(() => {
if (!cartData?.data || typeof cartData.data !== 'object') return []; if (!cartData || typeof cartData !== "object") return [];
return Object.values(cartData.data).flat(); return Object.values(cartData).flat();
}, [cartData]); }, [cartData]);
const cartCount = useMemo(() => { const cartCount = useMemo(() => {
return cartItems.reduce((total, item) => { return cartItems.reduce((total, item) => {
return total + (parseInt(item.product_quantity, 10) || 0); const qty = parseInt(item.product_quantity, 10) || 0;
return total + qty;
}, 0); }, 0);
}, [cartItems]); }, [cartItems]);
const getCartItem = (productId) => { const getCartItem = (productId) => {
if (!productId) return null;
const pid = String(productId);
return cartItems.find( return cartItems.find(
item => item.product?.id === productId || item.product_id === productId (item) =>
String(item.product?.id) === pid || String(item.product_id) === pid,
); );
}; };
@@ -32,6 +39,6 @@ export const useCart = () => {
cartItems, cartItems,
cartCount, cartCount,
getCartItem, getCartItem,
...rest ...queryResult,
}; };
}; };

View File

@@ -1,55 +1,12 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import { baseApi } from "./api/baseApi"; import { baseApi } from "./api/baseApi";
import { categoriesApi } from "./api/categories";
import { searchApi } from "./api/searchApi";
import { cartApi } from "./api/cartApi";
import { brandsApi } from "./api/brandsApi";
import { collectionsApi } from "./api/collectionsApi";
import { favoritesApi } from "./api/favoritesApi";
import { legalPagesApi } from "./api/legalPagesApi";
import { locationApi } from "./api/locationApi";
import { orderApi } from "./api/orderApi";
import { mediaApi } from "./api/bannersApi";
import { reviewsApi } from "./api/reviewApi";
import { profileApi } from "./api/myProfileApi";
import { contactApi } from "./api/contactUs";
import { filtersApi } from "./api/filtersApi";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
[baseApi.reducerPath]: baseApi.reducer, [baseApi.reducerPath]: baseApi.reducer,
[categoriesApi.reducerPath]: categoriesApi.reducer,
[searchApi.reducerPath]: searchApi.reducer,
[cartApi.reducerPath]: cartApi.reducer,
[brandsApi.reducerPath]: brandsApi.reducer,
[collectionsApi.reducerPath]: collectionsApi.reducer,
[favoritesApi.reducerPath]: favoritesApi.reducer,
[legalPagesApi.reducerPath]: legalPagesApi.reducer,
[locationApi.reducerPath]: locationApi.reducer,
[orderApi.reducerPath]: orderApi.reducer,
[mediaApi.reducerPath]: mediaApi.reducer,
[reviewsApi.reducerPath]: reviewsApi.reducer,
[profileApi.reducerPath]: profileApi.reducer,
[contactApi.reducerPath]: contactApi.reducer,
[filtersApi.reducerPath]: filtersApi.reducer,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat( getDefaultMiddleware().concat(baseApi.middleware),
baseApi.middleware,
categoriesApi.middleware,
searchApi.middleware,
brandsApi.middleware,
collectionsApi.middleware,
favoritesApi.middleware,
legalPagesApi.middleware,
locationApi.middleware,
orderApi.middleware,
reviewsApi.middleware,
mediaApi.middleware,
profileApi.middleware,
contactApi.middleware,
filtersApi.middleware
),
}); });
export default store; export default store;

View File

@@ -2,31 +2,16 @@ import React from "react";
import { Home, ShoppingBag, ShoppingCart, Heart, User } from "lucide-react"; import { Home, ShoppingBag, ShoppingCart, Heart, User } from "lucide-react";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import styles from "./FooterBar.module.scss"; import styles from "./FooterBar.module.scss";
import { useGetCartQuery } from "../../app/api/cartApi"; import { useCart } from "../../app/api/useCart";
import { useGetFavoritesQuery } from "../../app/api/favoritesApi"; import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const FooterBar = () => { const FooterBar = () => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const { data: cartData } = useGetCartQuery(); const { cartCount } = useCart();
const { data: favoriteData } = useGetFavoritesQuery(); const { data: favoriteData } = useGetFavoritesQuery();
// FIX: Object içindeki tüm channel'ların item'larını birleştir
const getCartCount = () => {
if (!cartData?.data || typeof cartData.data !== 'object') {
return 0;
}
// Object.values ile tüm channel array'lerini al
const allCartItems = Object.values(cartData.data).flat();
return allCartItems.reduce((total, item) => {
return total + (parseInt(item.product_quantity, 10) || 0);
}, 0);
};
const cartCount = getCartCount();
const favoriteCount = favoriteData?.length || 0; const favoriteCount = favoriteData?.length || 0;
const navItems = [ const navItems = [

View File

@@ -17,7 +17,7 @@ import { CiLocationOn } from "react-icons/ci";
import Sidebar from "../CategorySideBar"; import Sidebar from "../CategorySideBar";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSearchProductQuery } from "../../app/api/searchApi"; import { useSearchProductQuery } from "../../app/api/searchApi";
import { useGetCartQuery } from "../../app/api/cartApi"; import { useCart } from "../../app/api/useCart";
import { useGetOrdersQuery } from "../../app/api/orderApi"; import { useGetOrdersQuery } from "../../app/api/orderApi";
import { useGetFavoritesQuery } from "../../app/api/favoritesApi"; import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
import { useAuth } from "../../context/authContext"; import { useAuth } from "../../context/authContext";
@@ -31,27 +31,11 @@ const NavbarDown = () => {
const { data: searchData, refetch } = useSearchProductQuery(searchQuery, { const { data: searchData, refetch } = useSearchProductQuery(searchQuery, {
skip: !searchQuery, skip: !searchQuery,
}); });
const { data: cartData } = useGetCartQuery(undefined, {
refetchOnMountOrArgChange: false, const { cartCount: cartItemCount } = useCart();
});
const { isAuthenticated, logout } = useAuth(); const { isAuthenticated, logout } = useAuth();
// FIX: Object içindeki tüm channel'ların item'larını birleştir
const getCartItemCount = () => {
if (!cartData?.data || typeof cartData.data !== 'object') {
return 0;
}
// Object.values ile tüm channel array'lerini al ve flat ile birleştir
const allCartItems = Object.values(cartData.data).flat();
return allCartItems.reduce((total, item) => {
return total + (parseInt(item.product_quantity, 10) || 0);
}, 0);
};
const cartItemCount = getCartItemCount();
const { data: ordersData } = useGetOrdersQuery(); const { data: ordersData } = useGetOrdersQuery();
const ordersItemCount = ordersData?.length || 0; const ordersItemCount = ordersData?.length || 0;

View File

@@ -32,6 +32,8 @@ const truncateDescription = (htmlString, maxLength = 80) => {
return truncatedText; return truncatedText;
}; };
import { useCart } from "../../app/api/useCart";
const ProductCard = ({ const ProductCard = ({
product, product,
showAddToCart = true, showAddToCart = true,
@@ -49,64 +51,39 @@ const ProductCard = ({
const { data: favoriteProducts = [] } = useGetFavoritesQuery(); const { data: favoriteProducts = [] } = useGetFavoritesQuery();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [localIsFavorite, setLocalIsFavorite] = useState( const [localIsFavorite, setLocalIsFavorite] = useState(
favoriteProducts.some((fav) => fav.product?.id === product.id) favoriteProducts.some((fav) => fav.product?.id === product.id),
); );
const truncatedDesc = truncateDescription( const truncatedDesc = truncateDescription(
product.description, product.description,
descriptionMaxLength descriptionMaxLength,
); );
// ✅ Sadece cache'den oku, yeni request gönderme const { getCartItem } = useCart();
const { data: cartData } = useGetCartQuery(undefined, {
selectFromResult: (result) => ({
data: result.data,
}),
refetchOnMountOrArgChange: false, // ✅ Mount'ta yeniden çağırma
refetchOnFocus: false,
refetchOnReconnect: false,
});
const [addToCart] = useAddToCartMutation(); const [addToCart] = useAddToCartMutation();
const [updateCartItem] = useUpdateCartItemMutation(); const [updateCartItem] = useUpdateCartItemMutation();
const [removeFromCart] = useRemoveFromCartMutation(); const [removeFromCart] = useRemoveFromCartMutation();
// ✅ Cart data'yı düzgün parse et const cartItem = getCartItem(product.id);
const getCartItem = () => {
if (!cartData || typeof cartData !== "object") {
return null;
}
// Eğer data grouped object ise (store bazlı)
const allCartItems = Object.values(cartData).flat();
return allCartItems.find(
(item) =>
item.product?.id === product.id || item.product_id === product.id
);
};
const cartItem = getCartItem();
const [localQuantity, setLocalQuantity] = useState(0); const [localQuantity, setLocalQuantity] = useState(0);
const [pendingQuantity, setPendingQuantity] = useState(0); const [pendingQuantity, setPendingQuantity] = useState(0);
// ✅ Cart item değiştiğinde local state'i güncelle // ✅ Cart item değiştiğinde local state'i güncelle
useEffect(() => { useEffect(() => {
if (cartItem) { const qty = parseInt(
const qty = cartItem.quantity || cartItem.product_quantity || 0; cartItem?.quantity || cartItem?.product_quantity || 0,
10,
);
setLocalQuantity(qty); setLocalQuantity(qty);
setPendingQuantity(qty); setPendingQuantity(qty);
} else { }, [cartItem]);
setLocalQuantity(0);
setPendingQuantity(0);
}
}, [cartItem]); // ✅ Sadece cartItem değişince, cartData değil
// ✅ Favorite state'i güncelle // ✅ Favorite state'i güncelle
useEffect(() => { useEffect(() => {
if (Array.isArray(favoriteProducts)) { if (Array.isArray(favoriteProducts)) {
const isFav = favoriteProducts.some( const isFav = favoriteProducts.some(
(fav) => fav.product?.id === product.id (fav) => fav.product?.id === product.id,
); );
setLocalIsFavorite(isFav); setLocalIsFavorite(isFav);
} }
@@ -138,38 +115,32 @@ const ProductCard = ({
// ✅ Debounced update - sadece mutation, refetch yok // ✅ Debounced update - sadece mutation, refetch yok
useEffect(() => { useEffect(() => {
const updateCart = async () => { const serverQty = parseInt(
const currentCartQty = cartItem?.quantity || cartItem?.product_quantity || 0,
cartItem?.quantity || cartItem?.product_quantity || 0; 10,
);
if (pendingQuantity !== currentCartQty && pendingQuantity > 0) { if (pendingQuantity === serverQty || pendingQuantity <= 0) {
return;
}
const handler = setTimeout(async () => {
try { try {
setIsLoading(true); setIsLoading(true);
await updateCartItem({ await updateCartItem({
productId: product.id, productId: product.id,
quantity: pendingQuantity, quantity: pendingQuantity,
}).unwrap(); }).unwrap();
// ✅ RTK Query invalidatesTags ile otomatik güncellenecek
} catch (error) { } catch (error) {
console.error("Failed to update cart item:", error); console.error("Failed to update cart item:", error);
// ✅ Hata varsa önceki değere dön setLocalQuantity(serverQty);
setLocalQuantity(currentCartQty); setPendingQuantity(serverQty);
setPendingQuantity(currentCartQty);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} }, 500);
};
const debouncedUpdate = debounce(updateCart, 300); return () => clearTimeout(handler);
const currentCartQty =
cartItem?.quantity || cartItem?.product_quantity || 0;
if (pendingQuantity !== currentCartQty) {
debouncedUpdate();
}
return () => debouncedUpdate.cancel();
}, [pendingQuantity, cartItem, product.id, updateCartItem]); }, [pendingQuantity, cartItem, product.id, updateCartItem]);
const handleQuantityIncrease = (event) => { const handleQuantityIncrease = (event) => {

View File

@@ -156,6 +156,7 @@ export default {
category: { category: {
total: "Total", total: "Total",
items: "items", items: "items",
filter: "Filters",
subCategories: "SubCategories", subCategories: "SubCategories",
order: "Order", order: "Order",
notSelected: "Not Selected", notSelected: "Not Selected",
@@ -167,6 +168,9 @@ export default {
neverMind: "Default", neverMind: "Default",
From_expensive_to_cheap: "From expensive to cheap", From_expensive_to_cheap: "From expensive to cheap",
From_cheap_to_expensive: "From cheap to expensive", From_cheap_to_expensive: "From cheap to expensive",
price: "Price",
minPrice: "Min Price",
maxPrice: "Max Price",
}, },
product: { product: {
productCode: "Product code", productCode: "Product code",
@@ -188,10 +192,13 @@ export default {
TermsofUseandPrivacyPolicy: "Terms of Use and Privacy Policy", TermsofUseandPrivacyPolicy: "Terms of Use and Privacy Policy",
mobile_applications: "Mobile applications", mobile_applications: "Mobile applications",
copyright: " All rights reserved.", copyright: " All rights reserved.",
about_paragraph1: "Our Marketplace is a convenient online marketplace where you'll find everything in one place, from auto parts and electronics to home goods and fresh produce. We've been in business since 2019, and in that time we've collected hundreds of trusted brands so you can choose only the best. The range is constantly growing - we keep a close eye on your requests and always try to offer more.", about_paragraph1:
about_paragraph2: "Our mission is to make shopping easy and convenient. Everything you need can now be ordered in a couple of clicks from the comfort of your own home. You save time, effort and money - and we make sure that everything arrives quickly and hassle-free.", "Our Marketplace is a convenient online marketplace where you'll find everything in one place, from auto parts and electronics to home goods and fresh produce. We've been in business since 2019, and in that time we've collected hundreds of trusted brands so you can choose only the best. The range is constantly growing - we keep a close eye on your requests and always try to offer more.",
about_paragraph3: "You can pay for the order as you like: cash or bank card upon receipt.", about_paragraph2:
about_paragraph4: "We are always open to co-operation and welcome feedback. Do you have an idea, question or suggestion? Write to us - we will be happy to answer!" "Our mission is to make shopping easy and convenient. Everything you need can now be ordered in a couple of clicks from the comfort of your own home. You save time, effort and money - and we make sure that everything arrives quickly and hassle-free.",
about_paragraph3:
"You can pay for the order as you like: cash or bank card upon receipt.",
about_paragraph4:
"We are always open to co-operation and welcome feedback. Do you have an idea, question or suggestion? Write to us - we will be happy to answer!",
}, },
}; };

View File

@@ -125,7 +125,7 @@ export default {
verify: "Верификация", verify: "Верификация",
name: "Имя", name: "Имя",
address: "Address", address: "Address",
lastname:"Фамилия" lastname: "Фамилия",
}, },
order: { order: {
orderDate: "Дата заказа", orderDate: "Дата заказа",
@@ -153,6 +153,7 @@ export default {
category: { category: {
total: "Всего", total: "Всего",
items: "товаров", items: "товаров",
filter: "Фильтры",
subCategories: "Подкатегории", subCategories: "Подкатегории",
order: "Сортировка", order: "Сортировка",
notSelected: "Не выбрано", notSelected: "Не выбрано",
@@ -164,6 +165,9 @@ export default {
neverMind: "По умолчанию", neverMind: "По умолчанию",
From_expensive_to_cheap: "От дорогих к дешевым", From_expensive_to_cheap: "От дорогих к дешевым",
From_cheap_to_expensive: "От дешевых к дорогим", From_cheap_to_expensive: "От дешевых к дорогим",
price: "Цена",
minPrice: "Минимальная цена",
maxPrice: "Максимальная цена",
}, },
product: { product: {
productCode: "Код товара", productCode: "Код товара",
@@ -187,9 +191,13 @@ export default {
"Условия использования и политика конфиденциальности", "Условия использования и политика конфиденциальности",
mobile_applications: "Мобильные приложения", mobile_applications: "Мобильные приложения",
copyright: "Все права защищены.", copyright: "Все права защищены.",
about_paragraph1: "Наш маркетплейс — это удобная онлайн-площадка, где вы найдёте всё в одном месте: от автозапчастей и электроники до товаров для дома и свежих продуктов. Мы работаем с 2019 года и за это время собрали сотни надёжных брендов, чтобы вы могли выбирать только лучшее. Ассортимент постоянно растёт — мы внимательно следим за вашими запросами и всегда стараемся предложить больше.", about_paragraph1:
about_paragraph2: "Наша миссия — сделать покупки простыми и удобными. Всё, что вам нужно, теперь можно заказать в пару кликов, не выходя из дома. Вы экономите время, силы и деньги — а мы заботимся о том, чтобы всё приехало быстро и без лишних хлопот.", "Наш маркетплейс — это удобная онлайн-площадка, где вы найдёте всё в одном месте: от автозапчастей и электроники до товаров для дома и свежих продуктов. Мы работаем с 2019 года и за это время собрали сотни надёжных брендов, чтобы вы могли выбирать только лучшее. Ассортимент постоянно растёт — мы внимательно следим за вашими запросами и всегда стараемся предложить больше.",
about_paragraph3: "Оплатить заказ можно как вам удобно: наличными или банковской картой при получении.", about_paragraph2:
about_paragraph4: "Мы всегда открыты к сотрудничеству и рады обратной связи. Есть идея, вопрос или предложение? Напишите нам — мы с удовольствием ответим!" "Наша миссия — сделать покупки простыми и удобными. Всё, что вам нужно, теперь можно заказать в пару кликов, не выходя из дома. Вы экономите время, силы и деньги — а мы заботимся о том, чтобы всё приехало быстро и без лишних хлопот.",
about_paragraph3:
"Оплатить заказ можно как вам удобно: наличными или банковской картой при получении.",
about_paragraph4:
"Мы всегда открыты к сотрудничеству и рады обратной связи. Есть идея, вопрос или предложение? Напишите нам — мы с удовольствием ответим!",
}, },
}; };

View File

@@ -128,7 +128,7 @@ export default {
verify: "Tassykla", verify: "Tassykla",
name: "Ady", name: "Ady",
address: "Salgy", address: "Salgy",
lastname:"Familýaňyz" lastname: "Familýaňyz",
}, },
order: { order: {
orderDate: "Sargyt senesi", orderDate: "Sargyt senesi",
@@ -156,6 +156,7 @@ export default {
category: { category: {
total: "Jemi", total: "Jemi",
items: "haryt", items: "haryt",
filter: "Süzgüç",
subCategories: "Içki kategoriýalar", subCategories: "Içki kategoriýalar",
order: "Tertip", order: "Tertip",
notSelected: "Saýlanmadyk", notSelected: "Saýlanmadyk",
@@ -167,6 +168,9 @@ export default {
neverMind: "Sortlanmadyk", neverMind: "Sortlanmadyk",
From_expensive_to_cheap: "Gymmatdan arzana", From_expensive_to_cheap: "Gymmatdan arzana",
From_cheap_to_expensive: "Arzandan gymmada", From_cheap_to_expensive: "Arzandan gymmada",
price: "Bahasy",
maxPrice: "Maksimum baha",
minPrice: "Minimum baha",
}, },
product: { product: {
productCode: "Haryt kody", productCode: "Haryt kody",
@@ -189,10 +193,13 @@ export default {
TermsofUseandPrivacyPolicy: "Ulanyş düzgünleri we gizlinlik syýasaty", TermsofUseandPrivacyPolicy: "Ulanyş düzgünleri we gizlinlik syýasaty",
mobile_applications: "Mobile goşundylar", mobile_applications: "Mobile goşundylar",
copyright: "Ähli hukuklar goralan.", copyright: "Ähli hukuklar goralan.",
about_paragraph1: "Biziň bazarymyz amatly onlaýn platforma bolup, ol ýerde hemme zady bir ýerde tapyp bilersiňiz: awtoulag zapas şaýlaryndan we elektronikadan başlap, öý önümlerine we täze önümlere çenli. 2019-njy ýyldan bäri işleýäris we bu döwürde diňe gowularyny saýlap bilersiňiz diýip, ýüzlerçe ygtybarly marka ýygnadyk. Aralygy yzygiderli ösýär - islegleriňize ýakyndan gözegçilik edýäris we elmydama has köp zat hödürlemäge synanyşýarys..", about_paragraph1:
about_paragraph2: "Biziň wezipämiz, söwda etmegi ýönekeý we amatly etmek. Gerek zatlaryň hemmesini indi öýüňizden çykman iki gezek basyp sargyt edip bilersiňiz. Wagt, güýç we pul tygşytlaýarsyňyz - we hemme zadyň çalt we gereksiz kynçylyksyz gelýändigine göz ýetirýäris.", "Biziň bazarymyz amatly onlaýn platforma bolup, ol ýerde hemme zady bir ýerde tapyp bilersiňiz: awtoulag zapas şaýlaryndan we elektronikadan başlap, öý önümlerine we täze önümlere çenli. 2019-njy ýyldan bäri işleýäris we bu döwürde diňe gowularyny saýlap bilersiňiz diýip, ýüzlerçe ygtybarly marka ýygnadyk. Aralygy yzygiderli ösýär - islegleriňize ýakyndan gözegçilik edýäris we elmydama has köp zat hödürlemäge synanyşýarys..",
about_paragraph3: "Sargydyňyzy özüňize amatly görnüşde töläp bilersiňiz: nagt ýa-da alandan soň kredit kartoçkasy bilen.", about_paragraph2:
about_paragraph4: "Hyzmatdaşlyga elmydama açyk we pikirleri kabul edýäris. Pikiriňiz, soragyňyz ýa-da teklibiňiz barmy? Bize ýazyň - jogap bermäge şat bolarys!" "Biziň wezipämiz, söwda etmegi ýönekeý we amatly etmek. Gerek zatlaryň hemmesini indi öýüňizden çykman iki gezek basyp sargyt edip bilersiňiz. Wagt, güýç we pul tygşytlaýarsyňyz - we hemme zadyň çalt we gereksiz kynçylyksyz gelýändigine göz ýetirýäris.",
about_paragraph3:
"Sargydyňyzy özüňize amatly görnüşde töläp bilersiňiz: nagt ýa-da alandan soň kredit kartoçkasy bilen.",
about_paragraph4:
"Hyzmatdaşlyga elmydama açyk we pikirleri kabul edýäris. Pikiriňiz, soragyňyz ýa-da teklibiňiz barmy? Bize ýazyň - jogap bermäge şat bolarys!",
}, },
}; };

View File

@@ -13,8 +13,8 @@ import {
useUpdateCartItemMutation, useUpdateCartItemMutation,
useCleanCartMutation, useCleanCartMutation,
} from "../../app/api/cartApi"; } from "../../app/api/cartApi";
import { useCart } from "../../app/api/useCart";
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons"; import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
import { debounce } from "lodash";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
const TruncatedDescription = ({ description, maxLength = 100 }) => { const TruncatedDescription = ({ description, maxLength = 100 }) => {
@@ -44,24 +44,7 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
}; };
const CartPage = () => { const CartPage = () => {
const { const { cartData, cartItems, isLoading, isError, error } = useCart();
data: response = {},
error,
isError,
isLoading,
} = useGetCartQuery(undefined, {
refetchOnMountOrArgChange: 30, // ✅ Sadece 30 saniye sonra mount'ta refetch
refetchOnFocus: false,
refetchOnReconnect: false,
});
// Handle the new data structure - data is now an object grouped by store
const cartData = isError ? {} : (response.data || {});
// Convert object of arrays to flat array for backward compatibility
const cartItems = useMemo(() => {
return Object.values(cartData).flat();
}, [cartData]);
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [checkoutStores, setCheckoutStores] = useState({}); const [checkoutStores, setCheckoutStores] = useState({});
@@ -89,7 +72,8 @@ const CartPage = () => {
// Convert grouped data to stores array // Convert grouped data to stores array
const stores = useMemo(() => { const stores = useMemo(() => {
return Object.entries(cartData).map(([storeSlug, items]) => { return Object.entries(cartData)
.map(([storeSlug, items]) => {
if (!items || !items.length) return null; if (!items || !items.length) return null;
// Get store info from first item // Get store info from first item
@@ -100,9 +84,10 @@ const CartPage = () => {
name: storeInfo?.name || storeSlug, name: storeInfo?.name || storeSlug,
slug: storeSlug, slug: storeSlug,
shipping_price: storeInfo?.shipping_price, shipping_price: storeInfo?.shipping_price,
items: items items: items,
}; };
}).filter(Boolean); })
.filter(Boolean);
}, [cartData]); }, [cartData]);
// ✅ Initialize local quantities from cart items // ✅ Initialize local quantities from cart items
@@ -121,73 +106,56 @@ const CartPage = () => {
setPendingQuantities(newPendingQuantities); setPendingQuantities(newPendingQuantities);
}, [cartItems]); }, [cartItems]);
// ✅ Debounced update - tek bir useEffect // ✅ Debounced Cart Update - Her ürün için ayrı debounce
useEffect(() => { useEffect(() => {
const debouncedUpdates = {}; const timers = {};
const updateItem = async (productId) => { Object.keys(pendingQuantities).forEach((productId) => {
const serverItem = cartItems.find((item) => item.product.id === productId); const serverItem = cartItems.find(
const serverQuantity = serverItem ? parseInt(serverItem.product_quantity, 10) : 0; (item) => String(item.product.id) === String(productId),
);
const serverQuantity = serverItem
? parseInt(serverItem.product_quantity, 10)
: 0;
const pendingQuantity = pendingQuantities[productId]; const pendingQuantity = pendingQuantities[productId];
// ✅ Eğer değişiklik yoksa, güncelleme yapma // Değişiklik yoksa veya 0 ise (Delete modalı tetikler) bir şey yapma
if (pendingQuantity === undefined || pendingQuantity === serverQuantity) { if (
pendingQuantity === undefined ||
pendingQuantity === serverQuantity ||
pendingQuantity <= 0
) {
return; return;
} }
timers[productId] = setTimeout(async () => {
try { try {
setLoadingItems((prev) => ({ ...prev, [productId]: true })); setLoadingItems((prev) => ({ ...prev, [productId]: true }));
if (pendingQuantity <= 0) {
await removeFromCart({ productId }).unwrap();
} else {
await updateCartItem({ await updateCartItem({
productId, productId,
quantity: pendingQuantity, quantity: pendingQuantity,
}).unwrap(); }).unwrap();
}
// ✅ RTK Query otomatik cache'i güncelleyecek, refetch'e gerek yok
} catch (error) { } catch (error) {
console.error("Failed to update cart:", error); console.error("Failed to update cart:", error);
// Hata durumunda rollback
// ✅ Hata durumunda geri al
const originalItem = cartItems.find(
(item) => item.product.id === productId
);
if (originalItem) {
const originalQty = parseInt(originalItem.product_quantity, 10) || 0;
setLocalQuantities((prev) => ({ setLocalQuantities((prev) => ({
...prev, ...prev,
[productId]: originalQty, [productId]: serverQuantity,
})); }));
setPendingQuantities((prev) => ({ setPendingQuantities((prev) => ({
...prev, ...prev,
[productId]: originalQty, [productId]: serverQuantity,
})); }));
}
} finally { } finally {
setLoadingItems((prev) => ({ ...prev, [productId]: false })); setLoadingItems((prev) => ({ ...prev, [productId]: false }));
} }
}; }, 500);
// ✅ Her productId için debounced update oluştur
Object.keys(pendingQuantities).forEach((productId) => {
if (!debouncedUpdates[productId]) {
debouncedUpdates[productId] = debounce(
() => updateItem(productId),
500 // ✅ 500ms debounce (daha stabil)
);
}
debouncedUpdates[productId]();
}); });
return () => { return () => {
Object.values(debouncedUpdates).forEach((debouncedFn) => Object.values(timers).forEach((timer) => clearTimeout(timer));
debouncedFn.cancel()
);
}; };
}, [pendingQuantities, cartItems, updateCartItem, removeFromCart]); }, [pendingQuantities, cartItems, updateCartItem]);
const handleQuantityIncrease = (productId) => (event) => { const handleQuantityIncrease = (productId) => (event) => {
event.preventDefault(); event.preventDefault();
@@ -245,8 +213,6 @@ const CartPage = () => {
}, 0); }, 0);
}; };
const getStoreShippingPrice = (store) => { const getStoreShippingPrice = (store) => {
return store.shipping_price !== null && store.shipping_price !== undefined return store.shipping_price !== null && store.shipping_price !== undefined
? parseFloat(store.shipping_price) ? parseFloat(store.shipping_price)
@@ -254,18 +220,18 @@ const CartPage = () => {
}; };
const handleCheckout = (storeId) => { const handleCheckout = (storeId) => {
setCheckoutStores(prev => ({ ...prev, [storeId]: true })); setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
}; };
const handleBackToCart = (storeId) => { const handleBackToCart = (storeId) => {
setCheckoutStores(prev => ({ ...prev, [storeId]: false })); setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
}; };
const handleOrderSubmit = async (storeId, storeItems) => { const handleOrderSubmit = async (storeId, storeItems) => {
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) { if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
const success = await checkoutRefs.current[storeId](); const success = await checkoutRefs.current[storeId]();
if (success) { if (success) {
setCheckoutStores(prev => ({ ...prev, [storeId]: false })); setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
} }
} else { } else {
handleCheckout(storeId); handleCheckout(storeId);
@@ -319,8 +285,6 @@ const CartPage = () => {
try { try {
await cleanCart().unwrap(); await cleanCart().unwrap();
setLocalQuantities({}); setLocalQuantities({});
setPendingQuantities({}); setPendingQuantities({});
setCheckoutStores({}); setCheckoutStores({});
@@ -333,7 +297,7 @@ const CartPage = () => {
const getTotalItemCount = () => { const getTotalItemCount = () => {
return cartItems.reduce( return cartItems.reduce(
(sum, item) => sum + parseInt(item.product_quantity, 10), (sum, item) => sum + parseInt(item.product_quantity, 10),
0 0,
); );
}; };
@@ -397,7 +361,7 @@ const CartPage = () => {
<Checkout <Checkout
cartItems={store.items} cartItems={store.items}
shippingPrice={shippingPrice} shippingPrice={shippingPrice}
productIds={store.items.map(item => item.product.id)} productIds={store.items.map((item) => item.product.id)}
onBackToCart={() => handleBackToCart(store.id)} onBackToCart={() => handleBackToCart(store.id)}
onPlaceOrder={(placeOrderFn) => { onPlaceOrder={(placeOrderFn) => {
checkoutRefs.current[store.id] = placeOrderFn; checkoutRefs.current[store.id] = placeOrderFn;
@@ -427,23 +391,32 @@ const CartPage = () => {
</div> </div>
<div className={styles.priceQuantity}> <div className={styles.priceQuantity}>
<span className={styles.price}> <span className={styles.price}>
{(parseFloat(item.product.price_amount) || 0).toFixed(2)} m. {(
parseFloat(item.product.price_amount) || 0
).toFixed(2)}{" "}
m.
</span> </span>
<div className={styles.quantityControls}> <div className={styles.quantityControls}>
<button <button
onClick={handleQuantityDecrease(item.product.id)} onClick={handleQuantityDecrease(
item.product.id,
)}
className={styles.quantityBtn} className={styles.quantityBtn}
disabled={loadingItems[item.product.id]} disabled={loadingItems[item.product.id]}
> >
<DecreaseIcon /> <DecreaseIcon />
</button> </button>
<span> <span>
{localQuantities[item.product.id] !== undefined {localQuantities[item.product.id] !==
undefined
? localQuantities[item.product.id] ? localQuantities[item.product.id]
: parseInt(item.product_quantity, 10) || 0} : parseInt(item.product_quantity, 10) ||
0}
</span> </span>
<button <button
onClick={handleQuantityIncrease(item.product.id)} onClick={handleQuantityIncrease(
item.product.id,
)}
className={styles.quantityBtn} className={styles.quantityBtn}
disabled={loadingItems[item.product.id]} disabled={loadingItems[item.product.id]}
> >
@@ -454,7 +427,9 @@ const CartPage = () => {
<div className={styles.deleteBtnContainer}> <div className={styles.deleteBtnContainer}>
<button <button
className={styles.deleteBtn} className={styles.deleteBtn}
onClick={() => showDeleteConfirm(item.product.id)} onClick={() =>
showDeleteConfirm(item.product.id)
}
> >
<FaTrashAlt /> <FaTrashAlt />
</button> </button>
@@ -468,7 +443,9 @@ const CartPage = () => {
<div className={styles.storeSummary}> <div className={styles.storeSummary}>
<div className={styles.cartContent}> <div className={styles.cartContent}>
<h3>{store.name} - {t("cart.basket")}:</h3> <h3>
{store.name} - {t("cart.basket")}:
</h3>
<div className={styles.summaryRow}> <div className={styles.summaryRow}>
<span>{t("cart.price")}:</span> <span>{t("cart.price")}:</span>
<span>{storeTotal.toFixed(2)} m.</span> <span>{storeTotal.toFixed(2)} m.</span>
@@ -486,7 +463,9 @@ const CartPage = () => {
onClick={() => handleOrderSubmit(store.id, store.items)} onClick={() => handleOrderSubmit(store.id, store.items)}
className={styles.checkoutBtn} className={styles.checkoutBtn}
> >
{checkoutStores[store.id] ? t("cart.order") : t("cart.prepareOrders")} {checkoutStores[store.id]
? t("cart.order")
: t("cart.prepareOrders")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,128 +1,44 @@
.categoryPage { // Price Filter Styles
.priceFilterContainer {
display: flex; display: flex;
gap: 10px; align-items: center;
max-width: 1366px; gap: 8px;
margin: auto;
padding: 20px 1.375rem;
box-sizing: border-box;
flex-direction: column;
h2 {
font-size: 24px;
font-weight: 700;
margin: 15px 0 0px 0;
@media screen and (max-width: 1024px) {
font-size: 20px;
font-weight: 500;
}
}
p {
font-size: 14px;
margin-top: 0;
}
// .sum {
// @media screen and (min-width: 1024px) {
// display: none;
// }
// }
.breadcrumb {
color: #666;
font-size: 14px;
cursor: pointer;
.separator {
margin: 0 8px;
color: #999;
}
}
.bars {
display: flex;
gap: 10px;
justify-content: flex-end;
border-bottom: 1px solid #d1d5db;
border-top: 1px solid #d1d5db;
padding: 8px 0;
@media screen and (min-width: 1024px) {
display: none;
}
.sum {
color: #6b7280;
font-size: 12px;
text-align: left;
background-color: transparent;
border: 1px solid #6b7280;
padding: 3px 6px;
display: block;
border-radius: 0.5rem;
margin: 0;
}
// button {
// background-color: #ec6323;
// border-radius: 0.5rem;
// font-size: 14px;
// border: none;
// padding: 11px;
// font-weight: 600;
// color: #ffffffff;
// display: flex;
// align-items: center;
// gap: 5px;
// img {
// width: 16px;
// height: 16px;
// }
// }
}
.subCategories {
display: flex;
gap: 10px;
margin-bottom: 15px;
overflow-x: auto;
scrollbar-width: none;
scroll-behavior: smooth;
button {
color: #6b7280;
background-color: #fff;
padding: 4px 16px;
font-size: 14px;
border-radius: 0.5rem;
border: none;
width: max-content;
height: max-content;
white-space: nowrap;
}
@media screen and (min-width: 1024px) {
display: none;
}
}
.Container {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
aside {
width: 250px;
position: sticky;
top: 5rem;
background-color: #ffff;
padding: 20px;
border-radius: 8px; border-radius: 8px;
overflow-x: auto; margin-bottom: 16px;
height: calc(-8.25rem + 100vh); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
@media screen and (max-width: 1280px) {
width: 200px;
} }
@media screen and (max-width: 1100px) { .priceInputGroup {
width: 180px; display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
} }
@media screen and (max-width: 1023px) { .priceLabel {
display: none; font-size: 13px;
color: #888;
margin-bottom: 2px;
}
.priceInput {
width: 90px;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 15px;
background: #fff;
transition: border-color 0.2s;
}
.priceInput:focus {
border-color: #6c63ff;
outline: none;
}
.priceDivider {
font-size: 18px;
color: #aaa;
font-weight: bold;
margin: 0 6px;
} }
h2 { .filtersContainer{
margin-bottom: 10px;
font-size: 24px;
}
.filterSection { .filterSection {
margin-bottom: 20px; margin-bottom: 20px;
@@ -174,7 +90,9 @@
border-radius: 50%; border-radius: 50%;
margin-right: 8px; margin-right: 8px;
background-color: #d1d5db; background-color: #d1d5db;
transition: background-color 0.2s, border-color 0.2s; transition:
background-color 0.2s,
border-color 0.2s;
} }
input[type="radio"]:checked + .customRadio { input[type="radio"]:checked + .customRadio {
@@ -195,7 +113,9 @@
border-radius: 4px; border-radius: 4px;
background-color: #d1d5db; background-color: #d1d5db;
position: relative; position: relative;
transition: background-color 0.2s, border-color 0.2s; transition:
background-color 0.2s,
border-color 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -216,6 +136,132 @@
display: block; display: block;
} }
} }
}
.categoryPage {
display: flex;
gap: 10px;
max-width: 1366px;
margin: auto;
padding: 20px 1.375rem;
box-sizing: border-box;
flex-direction: column;
h2 {
font-size: 24px;
font-weight: 700;
margin: 15px 0 0px 0;
@media screen and (max-width: 1024px) {
font-size: 20px;
font-weight: 500;
}
}
p {
font-size: 14px;
margin-top: 0;
}
// .sum {
// @media screen and (min-width: 1024px) {
// display: none;
// }
// }
.breadcrumb {
color: #666;
font-size: 14px;
cursor: pointer;
.separator {
margin: 0 8px;
color: #999;
}
}
.bars {
@media screen and (min-width: 1024px) {
display: none;
}
.filterButton {
position: fixed;
bottom: 80px;
right: 20px;
z-index: 1000;
background-color: #d32824;
border-radius: 12px;
font-size: 16px;
border: none;
padding: 10px 24px;
font-weight: 700;
color: #ffffff;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
svg {
font-size: 20px;
}
&:active {
transform: scale(0.95);
}
}
}
.subCategories {
display: flex;
gap: 10px;
margin-bottom: 15px;
overflow-x: auto;
scrollbar-width: none;
scroll-behavior: smooth;
button {
color: #6b7280;
background-color: #fff;
padding: 4px 16px;
font-size: 14px;
border-radius: 0.5rem;
border: none;
width: max-content;
height: max-content;
white-space: nowrap;
}
@media screen and (min-width: 1024px) {
display: none;
}
}
.Container {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.sidebar {
width: 250px;
position: sticky;
top: 5rem;
background-color: #ffff;
padding: 20px;
border-radius: 8px;
overflow-y: auto;
height: calc(-8.25rem + 100vh);
@media screen and (max-width: 1280px) {
width: 200px;
}
@media screen and (max-width: 1100px) {
width: 180px;
}
@media screen and (max-width: 1023px) {
display: none;
}
h2 {
margin-bottom: 10px;
font-size: 24px;
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 6px; width: 6px;
} }

View File

@@ -9,18 +9,23 @@ const CategoryFilters = ({
selectedFilterBrand, selectedFilterBrand,
brandSearchQuery, brandSearchQuery,
searchQuery, searchQuery,
minPrice,
maxPrice,
onMinPriceChange,
onMaxPriceChange,
onCategorySelect, onCategorySelect,
onCategoryDeselect, onCategoryDeselect,
onBrandSelect, onBrandSelect,
onBrandDeselect, onBrandDeselect,
onBrandSearchChange, onBrandSearchChange,
className,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (searchQuery) return null; if (searchQuery) return null;
return ( return (
<aside className={styles.sidebar}> <aside className={`${styles.filtersContainer} ${className}`}>
{filtersData?.categories?.length > 0 && ( {filtersData?.categories?.length > 0 && (
<div className={styles.filterSection}> <div className={styles.filterSection}>
<h3>{t("category.subCategories")}</h3> <h3>{t("category.subCategories")}</h3>
@@ -65,7 +70,7 @@ const CategoryFilters = ({
.filter((brand) => .filter((brand) =>
brand.name brand.name
.toLowerCase() .toLowerCase()
.includes(brandSearchQuery.toLowerCase()) .includes(brandSearchQuery.toLowerCase()),
) )
.map((brand) => ( .map((brand) => (
<li key={brand.id}> <li key={brand.id}>
@@ -91,6 +96,34 @@ const CategoryFilters = ({
</ul> </ul>
</div> </div>
)} )}
<div className={styles.filterSection}>
<h3>{t("category.price")}</h3>
<div className={styles.priceFilterContainer}>
<div className={styles.priceInputGroup}>
<span className={styles.priceLabel}>{t("category.minPrice")}</span>
<input
type="number"
min="0"
placeholder={t("category.minPrice")}
value={minPrice}
onChange={(e) => onMinPriceChange(e.target.value)}
className={styles.priceInput}
/>
</div>
<span className={styles.priceDivider}>-</span>
<div className={styles.priceInputGroup}>
<span className={styles.priceLabel}>{t("category.maxPrice")}</span>
<input
type="number"
min="0"
placeholder={t("category.maxPrice")}
value={maxPrice}
onChange={(e) => onMaxPriceChange(e.target.value)}
className={styles.priceInput}
/>
</div>
</div>
</div>
</aside> </aside>
); );
}; };

View File

@@ -33,6 +33,8 @@ const useCategoryProducts = ({
brandId && `brand-${brandId}`, brandId && `brand-${brandId}`,
collectionId && `col-${collectionId}`, collectionId && `col-${collectionId}`,
selectedFilterBrand && `fbrand-${selectedFilterBrand}`, selectedFilterBrand && `fbrand-${selectedFilterBrand}`,
minPrice && `min-${minPrice}`,
maxPrice && `max-${maxPrice}`,
].filter(Boolean); ].filter(Boolean);
return parts.join("|") || "none"; return parts.join("|") || "none";
}, [ }, [
@@ -41,6 +43,8 @@ const useCategoryProducts = ({
brandId, brandId,
collectionId, collectionId,
selectedFilterBrand, selectedFilterBrand,
minPrice,
maxPrice,
]); ]);
const fetchParams = useMemo( const fetchParams = useMemo(
@@ -51,7 +55,7 @@ const useCategoryProducts = ({
min_price: minPrice || undefined, min_price: minPrice || undefined,
max_price: maxPrice || undefined, max_price: maxPrice || undefined,
}), }),
[currentPage, selectedFilterBrand, minPrice, maxPrice] [currentPage, selectedFilterBrand, minPrice, maxPrice],
); );
const fetchKey = `${contextId}-p${currentPage}`; const fetchKey = `${contextId}-p${currentPage}`;
@@ -78,7 +82,7 @@ const useCategoryProducts = ({
}, },
{ {
skip: !shouldUseBaseQuery, skip: !shouldUseBaseQuery,
} },
); );
const [ const [
@@ -257,7 +261,7 @@ const useCategoryProducts = ({
if (paginatedCategoryProducts && shouldUseBaseQuery) { if (paginatedCategoryProducts && shouldUseBaseQuery) {
updateProducts( updateProducts(
paginatedCategoryProducts.data || [], paginatedCategoryProducts.data || [],
!!paginatedCategoryProducts.pagination?.next_page_url !!paginatedCategoryProducts.pagination?.next_page_url,
); );
return; return;
} }
@@ -265,7 +269,7 @@ const useCategoryProducts = ({
if (lazyCategoryProducts) { if (lazyCategoryProducts) {
updateProducts( updateProducts(
lazyCategoryProducts.data || [], lazyCategoryProducts.data || [],
lazyCategoryProducts.pagination?.hasMorePages || false lazyCategoryProducts.pagination?.hasMorePages || false,
); );
return; return;
} }
@@ -274,7 +278,7 @@ const useCategoryProducts = ({
if (paginatedBrandProducts) { if (paginatedBrandProducts) {
updateProducts( updateProducts(
paginatedBrandProducts.data || [], paginatedBrandProducts.data || [],
!!paginatedBrandProducts.pagination?.next_page_url !!paginatedBrandProducts.pagination?.next_page_url,
); );
return; return;
} }
@@ -282,7 +286,7 @@ const useCategoryProducts = ({
if (paginatedCollectionProducts) { if (paginatedCollectionProducts) {
updateProducts( updateProducts(
paginatedCollectionProducts.data || [], paginatedCollectionProducts.data || [],
!!paginatedCollectionProducts.pagination?.next_page_url !!paginatedCollectionProducts.pagination?.next_page_url,
); );
} }
}, [ }, [

View File

@@ -3,13 +3,12 @@
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";
import { Result, Button } from "antd"; import { Result, Button, Drawer } from "antd";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { LuFilter } from "react-icons/lu";
import styles from "./CategoryPage.module.scss"; import styles from "./CategoryPage.module.scss";
import ProductCard from "../../components/ProductCard/index"; import ProductCard from "../../components/ProductCard/index";
import BrandSidebar from "../../components/BrandsSidebar/index";
import FilterSidebar from "../../components/FilterSideBar/index";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
import CategoryFilters from "./components/CategoryFilters"; import CategoryFilters from "./components/CategoryFilters";
@@ -35,6 +34,8 @@ const CategoryPage = () => {
brandSearchQuery: "", brandSearchQuery: "",
}); });
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
const routeKey = useMemo( const routeKey = useMemo(
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`, () => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
[categoryId, collectionId, brandId] [categoryId, collectionId, brandId]
@@ -43,7 +44,10 @@ const CategoryPage = () => {
const prevRouteRef = useRef(routeKey); const prevRouteRef = useRef(routeKey);
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
const searchResults = location.state?.searchData?.data || []; const searchResults = useMemo(
() => location.state?.searchData?.data || [],
[location.state?.searchData?.data]
);
const searchQuery = location.state?.searchQuery || null; const searchQuery = location.state?.searchQuery || null;
const { const {
@@ -270,36 +274,70 @@ const CategoryPage = () => {
</p> </p>
<div className={styles.bars}> <div className={styles.bars}>
<button className={styles.sum}> <button
<strong>{t("category.total")}:</strong> <br /> className={styles.filterButton}
{totalItems} {t("category.items")} onClick={() => setIsFilterDrawerOpen(true)}
>
{t("category.filter")} <LuFilter />
</button> </button>
<BrandSidebar
brands={filtersData?.brands || []}
selectedBrand={filterState.selectedFilterBrand}
onBrandSelect={handleFilterBrandSelect}
onBrandDeselect={handleFilterBrandDeselect}
/>
{/* <FilterSidebar onPriceSortChange={() => {}} currentPriceSort="none" /> */}
</div> </div>
{selectedCategory?.children && !searchQuery && ( <Drawer
<div className={styles.subCategories}> title={t("category.filter")}
{selectedCategory.children.map((sub) => ( placement="right"
<button key={sub.id} onClick={() => handleCategoryClick(sub.id)}> onClose={() => setIsFilterDrawerOpen(false)}
{sub.name} open={isFilterDrawerOpen}
</button> width="80%"
))} >
</div>
)}
<div className={styles.Container}>
<CategoryFilters <CategoryFilters
filtersData={filtersData} filtersData={filtersData}
selectedFilterCategory={filterState.selectedFilterCategory} selectedFilterCategory={filterState.selectedFilterCategory}
selectedFilterBrand={filterState.selectedFilterBrand} selectedFilterBrand={filterState.selectedFilterBrand}
brandSearchQuery={filterState.brandSearchQuery} brandSearchQuery={filterState.brandSearchQuery}
searchQuery={searchQuery} searchQuery={searchQuery}
minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => {
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
setAllProducts([]);
setHasMore(true);
}}
onMaxPriceChange={(value) => {
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
setAllProducts([]);
setHasMore(true);
}}
onCategorySelect={handleFilterCategorySelect}
onCategoryDeselect={handleFilterCategoryDeselect}
onBrandSelect={handleFilterBrandSelect}
onBrandDeselect={handleFilterBrandDeselect}
onBrandSearchChange={(query) =>
setFilterState((prev) => ({ ...prev, brandSearchQuery: query }))
}
/>
</Drawer>
<div className={styles.Container}>
<CategoryFilters
className={styles.sidebar}
filtersData={filtersData}
selectedFilterCategory={filterState.selectedFilterCategory}
selectedFilterBrand={filterState.selectedFilterBrand}
brandSearchQuery={filterState.brandSearchQuery}
searchQuery={searchQuery}
minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => {
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
setAllProducts([]);
setHasMore(true);
}}
onMaxPriceChange={(value) => {
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
setAllProducts([]);
setHasMore(true);
}}
onCategorySelect={handleFilterCategorySelect} onCategorySelect={handleFilterCategorySelect}
onCategoryDeselect={handleFilterCategoryDeselect} onCategoryDeselect={handleFilterCategoryDeselect}
onBrandSelect={handleFilterBrandSelect} onBrandSelect={handleFilterBrandSelect}

View File

@@ -12,17 +12,16 @@ import {
import ReviewSection from "../../components/Review/index"; import ReviewSection from "../../components/Review/index";
import { Modal } from "antd"; import { Modal } from "antd";
import { debounce } from "lodash";
import { import {
useAddFavoriteMutation, useAddFavoriteMutation,
useRemoveFavoriteMutation, useRemoveFavoriteMutation,
} from "../../app/api/favoritesApi"; } from "../../app/api/favoritesApi";
import { useGetFavoritesQuery } from "../../app/api/favoritesApi"; import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
import { useCart } from "../../app/api/useCart";
import { import {
useAddToCartMutation, useAddToCartMutation,
useUpdateCartItemMutation, useUpdateCartItemMutation,
useRemoveFromCartMutation, useRemoveFromCartMutation,
useGetCartQuery,
} from "../../app/api/cartApi"; } from "../../app/api/cartApi";
import ImageCarousel from "../../components/ProductCard/imageCarousel/index"; import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
import Loader from "../../components/Loader/index"; import Loader from "../../components/Loader/index";
@@ -49,52 +48,38 @@ const ProductPage = ({
error: similarProductsError, error: similarProductsError,
isLoading: similarProductsLoading, isLoading: similarProductsLoading,
} = useGetRelatedProductsQuery(productId); } = useGetRelatedProductsQuery(productId);
const [quantity, setQuantity] = useState(0);
const product = productResponse?.data; const product = productResponse?.data;
const similarProducts = similarProductsResponse?.data; const similarProducts = similarProductsResponse?.data;
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false); const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
const [addFavorite] = useAddFavoriteMutation(); const [addFavorite] = useAddFavoriteMutation();
const [removeFavorite] = useRemoveFavoriteMutation(); const [removeFavorite] = useRemoveFavoriteMutation();
const { data: favoriteProducts = [], refetch } = useGetFavoritesQuery(); const { data: favoriteProducts = [] } = useGetFavoritesQuery();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [localIsFavorite, setLocalIsFavorite] = useState( const [localIsFavorite, setLocalIsFavorite] = useState(
favoriteProducts.some((fav) => fav.product?.id === product?.id), favoriteProducts.some((fav) => fav.product?.id === product?.id),
); );
const { data: cartData } = useGetCartQuery(undefined, {
selectFromResult: (result) => ({ const { getCartItem } = useCart();
data: result.data,
}),
});
const [addToCart] = useAddToCartMutation(); const [addToCart] = useAddToCartMutation();
const [updateCartItem] = useUpdateCartItemMutation(); const [updateCartItem] = useUpdateCartItemMutation();
const [removeFromCart] = useRemoveFromCartMutation(); const [removeFromCart] = useRemoveFromCartMutation();
const [localQuantity, setLocalQuantity] = useState(0); const [localQuantity, setLocalQuantity] = useState(0);
const getCartItem = () => {
if (!cartData?.data || typeof cartData.data !== "object") {
return null;
}
const allCartItems = Object.values(cartData.data).flat();
return allCartItems.find(
(item) =>
item.product?.id === product?.id || item.product_id === product?.id,
);
};
const cartItem = getCartItem();
const [pendingQuantity, setPendingQuantity] = useState(0); const [pendingQuantity, setPendingQuantity] = useState(0);
useEffect(() => { const cartItem = getCartItem(product?.id || productId);
if (cartItem) {
setLocalQuantity(cartItem.quantity || cartItem.product_quantity || 0);
setPendingQuantity(cartItem.quantity || cartItem.product_quantity || 0);
} else {
setLocalQuantity(0);
setPendingQuantity(0);
}
}, [cartData, cartItem]);
// ✅ Sync local state with server cart
useEffect(() => {
const qty = parseInt(
cartItem?.quantity || cartItem?.product_quantity || 0,
10,
);
setLocalQuantity(qty);
setPendingQuantity(qty);
}, [cartItem]);
// ✅ Sync favorite status
useEffect(() => { useEffect(() => {
if (Array.isArray(favoriteProducts)) { if (Array.isArray(favoriteProducts)) {
const isFav = favoriteProducts.some( const isFav = favoriteProducts.some(
@@ -104,78 +89,55 @@ const ProductPage = ({
} }
}, [favoriteProducts, product?.id]); }, [favoriteProducts, product?.id]);
// ✅ Toggle Favorite
const handleToggleFavorite = async (event) => { const handleToggleFavorite = async (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (isLoading) return; if (isLoading) return;
setIsLoading(true); setIsLoading(true);
const originalState = localIsFavorite;
setLocalIsFavorite(!originalState); // Optimistic Update
try { try {
if (localIsFavorite) { if (originalState) {
const result = await removeFavorite(product.id).unwrap(); await removeFavorite(product.id).unwrap();
if (result === "Removed" || result?.status === "success") {
setLocalIsFavorite(false);
}
} else { } else {
const result = await addFavorite(product.id).unwrap(); await addFavorite(product.id).unwrap();
if (result === "Added" || result?.status === "success") {
setLocalIsFavorite(true);
} }
}
// Refetch after changing favorite status
await refetch();
} catch (error) { } catch (error) {
console.error("Failed to toggle favorite:", error); console.error("Failed to toggle favorite:", error);
setLocalIsFavorite(originalState); // Rollback
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
// ✅ Add to Cart (Initial)
const handleAddToCart = async (event) => { const handleAddToCart = async (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
// Check if stock is available
if (product.stock <= 0) { if (product.stock <= 0) {
setStockErrorModalVisible(true); setStockErrorModalVisible(true);
return; return;
} }
setLocalQuantity((prev) => prev + 1); setLocalQuantity(1);
setPendingQuantity((prev) => prev + 1); setPendingQuantity(1);
try { try {
await addToCart({ productId: product.id, quantity: 1 }).unwrap(); await addToCart({ productId: product.id, quantity: 1 }).unwrap();
} catch (error) { } catch (error) {
console.error("Failed to add to cart:", error); console.error("Failed to add to cart:", error);
setLocalQuantity((prev) => prev - 1); setLocalQuantity(0);
setPendingQuantity((prev) => prev - 1); setPendingQuantity(0);
} }
}; };
useEffect(() => {
const updateCart = debounce(async () => {
if (pendingQuantity !== localQuantity) {
try {
await updateCartItem({
productId: product.id,
quantity: pendingQuantity,
}).unwrap();
} catch (error) {
console.error("Failed to update cart item:", error);
}
}
}, 500);
updateCart();
return () => updateCart.cancel();
}, [pendingQuantity]);
const handleQuantityIncrease = (event) => { const handleQuantityIncrease = (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (isLoading) return; if (isLoading) return;
if (localQuantity >= product.stock) { if (localQuantity >= product.stock) {
@@ -187,11 +149,9 @@ const ProductPage = ({
setPendingQuantity((prev) => prev + 1); setPendingQuantity((prev) => prev + 1);
}; };
// Update quantity decrease handler
const handleQuantityDecrease = (event) => { const handleQuantityDecrease = (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (isLoading) return; if (isLoading) return;
if (pendingQuantity <= 1) { if (pendingQuantity <= 1) {
@@ -200,6 +160,9 @@ const ProductPage = ({
setIsLoading(true); setIsLoading(true);
removeFromCart({ productId: product.id }) removeFromCart({ productId: product.id })
.unwrap() .unwrap()
.then(() => {
// Success handled by hook
})
.catch(() => { .catch(() => {
setLocalQuantity(1); setLocalQuantity(1);
setPendingQuantity(1); setPendingQuantity(1);
@@ -213,9 +176,19 @@ const ProductPage = ({
} }
}; };
// ✅ Debounced Cart Update
useEffect(() => { useEffect(() => {
const updateCart = async () => { const serverQty = parseInt(
if (pendingQuantity !== quantity && pendingQuantity > 0) { cartItem?.quantity || cartItem?.product_quantity || 0,
10,
);
// Sadece miktar değiştiyse ve 0'dan büyükse güncelle (0 ise Remove triggerlanır)
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
return;
}
const handler = setTimeout(async () => {
try { try {
setIsLoading(true); setIsLoading(true);
await updateCartItem({ await updateCartItem({
@@ -224,22 +197,16 @@ const ProductPage = ({
}).unwrap(); }).unwrap();
} catch (error) { } catch (error) {
console.error("Failed to update cart item:", error); console.error("Failed to update cart item:", error);
setLocalQuantity(quantity); // Hata durumunda geri al
setPendingQuantity(quantity); setLocalQuantity(serverQty);
setPendingQuantity(serverQty);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} }, 500);
};
const debouncedUpdate = debounce(updateCart, 300); return () => clearTimeout(handler);
}, [pendingQuantity, cartItem, product?.id, updateCartItem]);
if (pendingQuantity !== quantity) {
debouncedUpdate();
}
return () => debouncedUpdate.cancel();
}, [pendingQuantity, quantity, product, updateCartItem]);
if (productLoading || similarProductsLoading) return <Loader />; if (productLoading || similarProductsLoading) return <Loader />;
if (productError || similarProductsError) if (productError || similarProductsError)