added price filter, changed mobile filter ui
This commit is contained in:
@@ -72,7 +72,7 @@ const customBaseQuery = async (args, api, extraOptions) => {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Token": import.meta.env.VITE_API_TOKEN || "hello-mf-s",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const data = await guestTokenResponse.json();
|
||||
@@ -120,5 +120,6 @@ const customBaseQuery = async (args, api, extraOptions) => {
|
||||
export const baseApi = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: customBaseQuery,
|
||||
tagTypes: ["Favorites", "cartItems", "Orders"],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
// hooks/useCart.js - YENİ DOSYA
|
||||
import { useMemo } from 'react';
|
||||
import { useGetCartQuery } from './cartApi';
|
||||
// hooks/useCart.js
|
||||
import { useMemo } from "react";
|
||||
import { useGetCartQuery } from "./cartApi";
|
||||
|
||||
export const useCart = () => {
|
||||
const { data: cartData, ...rest } = useGetCartQuery(undefined, {
|
||||
const queryResult = useGetCartQuery(undefined, {
|
||||
pollingInterval: 0,
|
||||
refetchOnMountOrArgChange: false,
|
||||
refetchOnMountOrArgChange: false, // Cache'den kullan, gereksiz GET'i engelle
|
||||
refetchOnFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const { data: response = {} } = queryResult;
|
||||
const cartData = response.data || {};
|
||||
|
||||
const cartItems = useMemo(() => {
|
||||
if (!cartData?.data || typeof cartData.data !== 'object') return [];
|
||||
return Object.values(cartData.data).flat();
|
||||
if (!cartData || typeof cartData !== "object") return [];
|
||||
return Object.values(cartData).flat();
|
||||
}, [cartData]);
|
||||
|
||||
const cartCount = useMemo(() => {
|
||||
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);
|
||||
}, [cartItems]);
|
||||
|
||||
const getCartItem = (productId) => {
|
||||
if (!productId) return null;
|
||||
const pid = String(productId);
|
||||
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,
|
||||
cartCount,
|
||||
getCartItem,
|
||||
...rest
|
||||
...queryResult,
|
||||
};
|
||||
};
|
||||
@@ -1,55 +1,12 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
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({
|
||||
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) =>
|
||||
getDefaultMiddleware().concat(
|
||||
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
|
||||
),
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -2,31 +2,16 @@ import React from "react";
|
||||
import { Home, ShoppingBag, ShoppingCart, Heart, User } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
|
||||
const FooterBar = () => {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { data: cartData } = useGetCartQuery();
|
||||
const { cartCount } = useCart();
|
||||
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 navItems = [
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CiLocationOn } from "react-icons/ci";
|
||||
import Sidebar from "../CategorySideBar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
@@ -31,27 +31,11 @@ const NavbarDown = () => {
|
||||
const { data: searchData, refetch } = useSearchProductQuery(searchQuery, {
|
||||
skip: !searchQuery,
|
||||
});
|
||||
const { data: cartData } = useGetCartQuery(undefined, {
|
||||
refetchOnMountOrArgChange: false,
|
||||
});
|
||||
|
||||
const { cartCount: cartItemCount } = useCart();
|
||||
|
||||
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 ordersItemCount = ordersData?.length || 0;
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ const truncateDescription = (htmlString, maxLength = 80) => {
|
||||
return truncatedText;
|
||||
};
|
||||
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
|
||||
const ProductCard = ({
|
||||
product,
|
||||
showAddToCart = true,
|
||||
@@ -49,64 +51,39 @@ const ProductCard = ({
|
||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id),
|
||||
);
|
||||
|
||||
const truncatedDesc = truncateDescription(
|
||||
product.description,
|
||||
descriptionMaxLength
|
||||
descriptionMaxLength,
|
||||
);
|
||||
|
||||
// ✅ Sadece cache'den oku, yeni request gönderme
|
||||
const { data: cartData } = useGetCartQuery(undefined, {
|
||||
selectFromResult: (result) => ({
|
||||
data: result.data,
|
||||
}),
|
||||
refetchOnMountOrArgChange: false, // ✅ Mount'ta yeniden çağırma
|
||||
refetchOnFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
const { getCartItem } = useCart();
|
||||
|
||||
const [addToCart] = useAddToCartMutation();
|
||||
const [updateCartItem] = useUpdateCartItemMutation();
|
||||
const [removeFromCart] = useRemoveFromCartMutation();
|
||||
|
||||
// ✅ Cart data'yı düzgün parse et
|
||||
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 cartItem = getCartItem(product.id);
|
||||
const [localQuantity, setLocalQuantity] = useState(0);
|
||||
const [pendingQuantity, setPendingQuantity] = useState(0);
|
||||
|
||||
// ✅ Cart item değiştiğinde local state'i güncelle
|
||||
useEffect(() => {
|
||||
if (cartItem) {
|
||||
const qty = cartItem.quantity || cartItem.product_quantity || 0;
|
||||
const qty = parseInt(
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||
10,
|
||||
);
|
||||
setLocalQuantity(qty);
|
||||
setPendingQuantity(qty);
|
||||
} else {
|
||||
setLocalQuantity(0);
|
||||
setPendingQuantity(0);
|
||||
}
|
||||
}, [cartItem]); // ✅ Sadece cartItem değişince, cartData değil
|
||||
}, [cartItem]);
|
||||
|
||||
// ✅ Favorite state'i güncelle
|
||||
useEffect(() => {
|
||||
if (Array.isArray(favoriteProducts)) {
|
||||
const isFav = favoriteProducts.some(
|
||||
(fav) => fav.product?.id === product.id
|
||||
(fav) => fav.product?.id === product.id,
|
||||
);
|
||||
setLocalIsFavorite(isFav);
|
||||
}
|
||||
@@ -138,38 +115,32 @@ const ProductCard = ({
|
||||
|
||||
// ✅ Debounced update - sadece mutation, refetch yok
|
||||
useEffect(() => {
|
||||
const updateCart = async () => {
|
||||
const currentCartQty =
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0;
|
||||
const serverQty = parseInt(
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||
10,
|
||||
);
|
||||
|
||||
if (pendingQuantity !== currentCartQty && pendingQuantity > 0) {
|
||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = setTimeout(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateCartItem({
|
||||
productId: product.id,
|
||||
quantity: pendingQuantity,
|
||||
}).unwrap();
|
||||
// ✅ RTK Query invalidatesTags ile otomatik güncellenecek
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart item:", error);
|
||||
// ✅ Hata varsa önceki değere dön
|
||||
setLocalQuantity(currentCartQty);
|
||||
setPendingQuantity(currentCartQty);
|
||||
setLocalQuantity(serverQty);
|
||||
setPendingQuantity(serverQty);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, 500);
|
||||
|
||||
const debouncedUpdate = debounce(updateCart, 300);
|
||||
|
||||
const currentCartQty =
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0;
|
||||
if (pendingQuantity !== currentCartQty) {
|
||||
debouncedUpdate();
|
||||
}
|
||||
|
||||
return () => debouncedUpdate.cancel();
|
||||
return () => clearTimeout(handler);
|
||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||
|
||||
const handleQuantityIncrease = (event) => {
|
||||
|
||||
@@ -156,6 +156,7 @@ export default {
|
||||
category: {
|
||||
total: "Total",
|
||||
items: "items",
|
||||
filter: "Filters",
|
||||
subCategories: "SubCategories",
|
||||
order: "Order",
|
||||
notSelected: "Not Selected",
|
||||
@@ -167,6 +168,9 @@ export default {
|
||||
neverMind: "Default",
|
||||
From_expensive_to_cheap: "From expensive to cheap",
|
||||
From_cheap_to_expensive: "From cheap to expensive",
|
||||
price: "Price",
|
||||
minPrice: "Min Price",
|
||||
maxPrice: "Max Price",
|
||||
},
|
||||
product: {
|
||||
productCode: "Product code",
|
||||
@@ -188,10 +192,13 @@ export default {
|
||||
TermsofUseandPrivacyPolicy: "Terms of Use and Privacy Policy",
|
||||
mobile_applications: "Mobile applications",
|
||||
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_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.",
|
||||
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!"
|
||||
|
||||
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_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.",
|
||||
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!",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
verify: "Верификация",
|
||||
name: "Имя",
|
||||
address: "Address",
|
||||
lastname:"Фамилия"
|
||||
lastname: "Фамилия",
|
||||
},
|
||||
order: {
|
||||
orderDate: "Дата заказа",
|
||||
@@ -153,6 +153,7 @@ export default {
|
||||
category: {
|
||||
total: "Всего",
|
||||
items: "товаров",
|
||||
filter: "Фильтры",
|
||||
subCategories: "Подкатегории",
|
||||
order: "Сортировка",
|
||||
notSelected: "Не выбрано",
|
||||
@@ -164,6 +165,9 @@ export default {
|
||||
neverMind: "По умолчанию",
|
||||
From_expensive_to_cheap: "От дорогих к дешевым",
|
||||
From_cheap_to_expensive: "От дешевых к дорогим",
|
||||
price: "Цена",
|
||||
minPrice: "Минимальная цена",
|
||||
maxPrice: "Максимальная цена",
|
||||
},
|
||||
product: {
|
||||
productCode: "Код товара",
|
||||
@@ -187,9 +191,13 @@ export default {
|
||||
"Условия использования и политика конфиденциальности",
|
||||
mobile_applications: "Мобильные приложения",
|
||||
copyright: "Все права защищены.",
|
||||
about_paragraph1: "Наш маркетплейс — это удобная онлайн-площадка, где вы найдёте всё в одном месте: от автозапчастей и электроники до товаров для дома и свежих продуктов. Мы работаем с 2019 года и за это время собрали сотни надёжных брендов, чтобы вы могли выбирать только лучшее. Ассортимент постоянно растёт — мы внимательно следим за вашими запросами и всегда стараемся предложить больше.",
|
||||
about_paragraph2: "Наша миссия — сделать покупки простыми и удобными. Всё, что вам нужно, теперь можно заказать в пару кликов, не выходя из дома. Вы экономите время, силы и деньги — а мы заботимся о том, чтобы всё приехало быстро и без лишних хлопот.",
|
||||
about_paragraph3: "Оплатить заказ можно как вам удобно: наличными или банковской картой при получении.",
|
||||
about_paragraph4: "Мы всегда открыты к сотрудничеству и рады обратной связи. Есть идея, вопрос или предложение? Напишите нам — мы с удовольствием ответим!"
|
||||
about_paragraph1:
|
||||
"Наш маркетплейс — это удобная онлайн-площадка, где вы найдёте всё в одном месте: от автозапчастей и электроники до товаров для дома и свежих продуктов. Мы работаем с 2019 года и за это время собрали сотни надёжных брендов, чтобы вы могли выбирать только лучшее. Ассортимент постоянно растёт — мы внимательно следим за вашими запросами и всегда стараемся предложить больше.",
|
||||
about_paragraph2:
|
||||
"Наша миссия — сделать покупки простыми и удобными. Всё, что вам нужно, теперь можно заказать в пару кликов, не выходя из дома. Вы экономите время, силы и деньги — а мы заботимся о том, чтобы всё приехало быстро и без лишних хлопот.",
|
||||
about_paragraph3:
|
||||
"Оплатить заказ можно как вам удобно: наличными или банковской картой при получении.",
|
||||
about_paragraph4:
|
||||
"Мы всегда открыты к сотрудничеству и рады обратной связи. Есть идея, вопрос или предложение? Напишите нам — мы с удовольствием ответим!",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
verify: "Tassykla",
|
||||
name: "Ady",
|
||||
address: "Salgy",
|
||||
lastname:"Familýaňyz"
|
||||
lastname: "Familýaňyz",
|
||||
},
|
||||
order: {
|
||||
orderDate: "Sargyt senesi",
|
||||
@@ -156,6 +156,7 @@ export default {
|
||||
category: {
|
||||
total: "Jemi",
|
||||
items: "haryt",
|
||||
filter: "Süzgüç",
|
||||
subCategories: "Içki kategoriýalar",
|
||||
order: "Tertip",
|
||||
notSelected: "Saýlanmadyk",
|
||||
@@ -167,6 +168,9 @@ export default {
|
||||
neverMind: "Sortlanmadyk",
|
||||
From_expensive_to_cheap: "Gymmatdan arzana",
|
||||
From_cheap_to_expensive: "Arzandan gymmada",
|
||||
price: "Bahasy",
|
||||
maxPrice: "Maksimum baha",
|
||||
minPrice: "Minimum baha",
|
||||
},
|
||||
product: {
|
||||
productCode: "Haryt kody",
|
||||
@@ -189,10 +193,13 @@ export default {
|
||||
TermsofUseandPrivacyPolicy: "Ulanyş düzgünleri we gizlinlik syýasaty",
|
||||
mobile_applications: "Mobile goşundylar",
|
||||
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_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.",
|
||||
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!"
|
||||
|
||||
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_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.",
|
||||
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!",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
useUpdateCartItemMutation,
|
||||
useCleanCartMutation,
|
||||
} from "../../app/api/cartApi";
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||||
import { debounce } from "lodash";
|
||||
import Loader from "../../components/Loader/index";
|
||||
|
||||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
@@ -44,24 +44,7 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
};
|
||||
|
||||
const CartPage = () => {
|
||||
const {
|
||||
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 { cartData, cartItems, isLoading, isError, error } = useCart();
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const [checkoutStores, setCheckoutStores] = useState({});
|
||||
@@ -89,7 +72,8 @@ const CartPage = () => {
|
||||
|
||||
// Convert grouped data to stores array
|
||||
const stores = useMemo(() => {
|
||||
return Object.entries(cartData).map(([storeSlug, items]) => {
|
||||
return Object.entries(cartData)
|
||||
.map(([storeSlug, items]) => {
|
||||
if (!items || !items.length) return null;
|
||||
|
||||
// Get store info from first item
|
||||
@@ -100,9 +84,10 @@ const CartPage = () => {
|
||||
name: storeInfo?.name || storeSlug,
|
||||
slug: storeSlug,
|
||||
shipping_price: storeInfo?.shipping_price,
|
||||
items: items
|
||||
items: items,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [cartData]);
|
||||
|
||||
// ✅ Initialize local quantities from cart items
|
||||
@@ -121,73 +106,56 @@ const CartPage = () => {
|
||||
setPendingQuantities(newPendingQuantities);
|
||||
}, [cartItems]);
|
||||
|
||||
// ✅ Debounced update - tek bir useEffect
|
||||
// ✅ Debounced Cart Update - Her ürün için ayrı debounce
|
||||
useEffect(() => {
|
||||
const debouncedUpdates = {};
|
||||
const timers = {};
|
||||
|
||||
const updateItem = async (productId) => {
|
||||
const serverItem = cartItems.find((item) => item.product.id === productId);
|
||||
const serverQuantity = serverItem ? parseInt(serverItem.product_quantity, 10) : 0;
|
||||
Object.keys(pendingQuantities).forEach((productId) => {
|
||||
const serverItem = cartItems.find(
|
||||
(item) => String(item.product.id) === String(productId),
|
||||
);
|
||||
const serverQuantity = serverItem
|
||||
? parseInt(serverItem.product_quantity, 10)
|
||||
: 0;
|
||||
const pendingQuantity = pendingQuantities[productId];
|
||||
|
||||
// ✅ Eğer değişiklik yoksa, güncelleme yapma
|
||||
if (pendingQuantity === undefined || pendingQuantity === serverQuantity) {
|
||||
// Değişiklik yoksa veya 0 ise (Delete modalı tetikler) bir şey yapma
|
||||
if (
|
||||
pendingQuantity === undefined ||
|
||||
pendingQuantity === serverQuantity ||
|
||||
pendingQuantity <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
timers[productId] = setTimeout(async () => {
|
||||
try {
|
||||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
||||
|
||||
if (pendingQuantity <= 0) {
|
||||
await removeFromCart({ productId }).unwrap();
|
||||
} else {
|
||||
await updateCartItem({
|
||||
productId,
|
||||
quantity: pendingQuantity,
|
||||
}).unwrap();
|
||||
}
|
||||
// ✅ RTK Query otomatik cache'i güncelleyecek, refetch'e gerek yok
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart:", error);
|
||||
|
||||
// ✅ Hata durumunda geri al
|
||||
const originalItem = cartItems.find(
|
||||
(item) => item.product.id === productId
|
||||
);
|
||||
if (originalItem) {
|
||||
const originalQty = parseInt(originalItem.product_quantity, 10) || 0;
|
||||
// Hata durumunda rollback
|
||||
setLocalQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: originalQty,
|
||||
[productId]: serverQuantity,
|
||||
}));
|
||||
setPendingQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: originalQty,
|
||||
[productId]: serverQuantity,
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 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]();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Object.values(debouncedUpdates).forEach((debouncedFn) =>
|
||||
debouncedFn.cancel()
|
||||
);
|
||||
Object.values(timers).forEach((timer) => clearTimeout(timer));
|
||||
};
|
||||
}, [pendingQuantities, cartItems, updateCartItem, removeFromCart]);
|
||||
}, [pendingQuantities, cartItems, updateCartItem]);
|
||||
|
||||
const handleQuantityIncrease = (productId) => (event) => {
|
||||
event.preventDefault();
|
||||
@@ -245,8 +213,6 @@ const CartPage = () => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const getStoreShippingPrice = (store) => {
|
||||
return store.shipping_price !== null && store.shipping_price !== undefined
|
||||
? parseFloat(store.shipping_price)
|
||||
@@ -254,18 +220,18 @@ const CartPage = () => {
|
||||
};
|
||||
|
||||
const handleCheckout = (storeId) => {
|
||||
setCheckoutStores(prev => ({ ...prev, [storeId]: true }));
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
||||
};
|
||||
|
||||
const handleBackToCart = (storeId) => {
|
||||
setCheckoutStores(prev => ({ ...prev, [storeId]: false }));
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||
};
|
||||
|
||||
const handleOrderSubmit = async (storeId, storeItems) => {
|
||||
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
||||
const success = await checkoutRefs.current[storeId]();
|
||||
if (success) {
|
||||
setCheckoutStores(prev => ({ ...prev, [storeId]: false }));
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||
}
|
||||
} else {
|
||||
handleCheckout(storeId);
|
||||
@@ -319,8 +285,6 @@ const CartPage = () => {
|
||||
try {
|
||||
await cleanCart().unwrap();
|
||||
|
||||
|
||||
|
||||
setLocalQuantities({});
|
||||
setPendingQuantities({});
|
||||
setCheckoutStores({});
|
||||
@@ -333,7 +297,7 @@ const CartPage = () => {
|
||||
const getTotalItemCount = () => {
|
||||
return cartItems.reduce(
|
||||
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
||||
0
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -397,14 +361,14 @@ const CartPage = () => {
|
||||
<Checkout
|
||||
cartItems={store.items}
|
||||
shippingPrice={shippingPrice}
|
||||
productIds={store.items.map(item => item.product.id)}
|
||||
productIds={store.items.map((item) => item.product.id)}
|
||||
onBackToCart={() => handleBackToCart(store.id)}
|
||||
onPlaceOrder={(placeOrderFn) => {
|
||||
checkoutRefs.current[store.id] = placeOrderFn;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{background:"white", width: "100%"}}>
|
||||
<div style={{ background: "white", width: "100%" }}>
|
||||
<div className={styles.storeHeader}>
|
||||
<h3>{store.name}</h3>
|
||||
</div>
|
||||
@@ -427,23 +391,32 @@ const CartPage = () => {
|
||||
</div>
|
||||
<div className={styles.priceQuantity}>
|
||||
<span className={styles.price}>
|
||||
{(parseFloat(item.product.price_amount) || 0).toFixed(2)} m.
|
||||
{(
|
||||
parseFloat(item.product.price_amount) || 0
|
||||
).toFixed(2)}{" "}
|
||||
m.
|
||||
</span>
|
||||
<div className={styles.quantityControls}>
|
||||
<button
|
||||
onClick={handleQuantityDecrease(item.product.id)}
|
||||
onClick={handleQuantityDecrease(
|
||||
item.product.id,
|
||||
)}
|
||||
className={styles.quantityBtn}
|
||||
disabled={loadingItems[item.product.id]}
|
||||
>
|
||||
<DecreaseIcon />
|
||||
</button>
|
||||
<span>
|
||||
{localQuantities[item.product.id] !== undefined
|
||||
{localQuantities[item.product.id] !==
|
||||
undefined
|
||||
? localQuantities[item.product.id]
|
||||
: parseInt(item.product_quantity, 10) || 0}
|
||||
: parseInt(item.product_quantity, 10) ||
|
||||
0}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleQuantityIncrease(item.product.id)}
|
||||
onClick={handleQuantityIncrease(
|
||||
item.product.id,
|
||||
)}
|
||||
className={styles.quantityBtn}
|
||||
disabled={loadingItems[item.product.id]}
|
||||
>
|
||||
@@ -454,7 +427,9 @@ const CartPage = () => {
|
||||
<div className={styles.deleteBtnContainer}>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
onClick={() => showDeleteConfirm(item.product.id)}
|
||||
onClick={() =>
|
||||
showDeleteConfirm(item.product.id)
|
||||
}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
@@ -468,7 +443,9 @@ const CartPage = () => {
|
||||
|
||||
<div className={styles.storeSummary}>
|
||||
<div className={styles.cartContent}>
|
||||
<h3>{store.name} - {t("cart.basket")}:</h3>
|
||||
<h3>
|
||||
{store.name} - {t("cart.basket")}:
|
||||
</h3>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.price")}:</span>
|
||||
<span>{storeTotal.toFixed(2)} m.</span>
|
||||
@@ -486,7 +463,9 @@ const CartPage = () => {
|
||||
onClick={() => handleOrderSubmit(store.id, store.items)}
|
||||
className={styles.checkoutBtn}
|
||||
>
|
||||
{checkoutStores[store.id] ? t("cart.order") : t("cart.prepareOrders")}
|
||||
{checkoutStores[store.id]
|
||||
? t("cart.order")
|
||||
: t("cart.prepareOrders")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,128 +1,44 @@
|
||||
.categoryPage {
|
||||
// Price Filter Styles
|
||||
.priceFilterContainer {
|
||||
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 {
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
overflow-x: 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;
|
||||
}
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.priceInputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
.priceLabel {
|
||||
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 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.filtersContainer{
|
||||
|
||||
.filterSection {
|
||||
margin-bottom: 20px;
|
||||
@@ -174,7 +90,9 @@
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
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 {
|
||||
@@ -195,7 +113,9 @@
|
||||
border-radius: 4px;
|
||||
background-color: #d1d5db;
|
||||
position: relative;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
border-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -216,6 +136,132 @@
|
||||
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 {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
@@ -9,18 +9,23 @@ const CategoryFilters = ({
|
||||
selectedFilterBrand,
|
||||
brandSearchQuery,
|
||||
searchQuery,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
onMinPriceChange,
|
||||
onMaxPriceChange,
|
||||
onCategorySelect,
|
||||
onCategoryDeselect,
|
||||
onBrandSelect,
|
||||
onBrandDeselect,
|
||||
onBrandSearchChange,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (searchQuery) return null;
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<aside className={`${styles.filtersContainer} ${className}`}>
|
||||
{filtersData?.categories?.length > 0 && (
|
||||
<div className={styles.filterSection}>
|
||||
<h3>{t("category.subCategories")}</h3>
|
||||
@@ -65,7 +70,7 @@ const CategoryFilters = ({
|
||||
.filter((brand) =>
|
||||
brand.name
|
||||
.toLowerCase()
|
||||
.includes(brandSearchQuery.toLowerCase())
|
||||
.includes(brandSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((brand) => (
|
||||
<li key={brand.id}>
|
||||
@@ -91,6 +96,34 @@ const CategoryFilters = ({
|
||||
</ul>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,8 @@ const useCategoryProducts = ({
|
||||
brandId && `brand-${brandId}`,
|
||||
collectionId && `col-${collectionId}`,
|
||||
selectedFilterBrand && `fbrand-${selectedFilterBrand}`,
|
||||
minPrice && `min-${minPrice}`,
|
||||
maxPrice && `max-${maxPrice}`,
|
||||
].filter(Boolean);
|
||||
return parts.join("|") || "none";
|
||||
}, [
|
||||
@@ -41,6 +43,8 @@ const useCategoryProducts = ({
|
||||
brandId,
|
||||
collectionId,
|
||||
selectedFilterBrand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
]);
|
||||
|
||||
const fetchParams = useMemo(
|
||||
@@ -51,7 +55,7 @@ const useCategoryProducts = ({
|
||||
min_price: minPrice || undefined,
|
||||
max_price: maxPrice || undefined,
|
||||
}),
|
||||
[currentPage, selectedFilterBrand, minPrice, maxPrice]
|
||||
[currentPage, selectedFilterBrand, minPrice, maxPrice],
|
||||
);
|
||||
|
||||
const fetchKey = `${contextId}-p${currentPage}`;
|
||||
@@ -78,7 +82,7 @@ const useCategoryProducts = ({
|
||||
},
|
||||
{
|
||||
skip: !shouldUseBaseQuery,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const [
|
||||
@@ -257,7 +261,7 @@ const useCategoryProducts = ({
|
||||
if (paginatedCategoryProducts && shouldUseBaseQuery) {
|
||||
updateProducts(
|
||||
paginatedCategoryProducts.data || [],
|
||||
!!paginatedCategoryProducts.pagination?.next_page_url
|
||||
!!paginatedCategoryProducts.pagination?.next_page_url,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -265,7 +269,7 @@ const useCategoryProducts = ({
|
||||
if (lazyCategoryProducts) {
|
||||
updateProducts(
|
||||
lazyCategoryProducts.data || [],
|
||||
lazyCategoryProducts.pagination?.hasMorePages || false
|
||||
lazyCategoryProducts.pagination?.hasMorePages || false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -274,7 +278,7 @@ const useCategoryProducts = ({
|
||||
if (paginatedBrandProducts) {
|
||||
updateProducts(
|
||||
paginatedBrandProducts.data || [],
|
||||
!!paginatedBrandProducts.pagination?.next_page_url
|
||||
!!paginatedBrandProducts.pagination?.next_page_url,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -282,7 +286,7 @@ const useCategoryProducts = ({
|
||||
if (paginatedCollectionProducts) {
|
||||
updateProducts(
|
||||
paginatedCollectionProducts.data || [],
|
||||
!!paginatedCollectionProducts.pagination?.next_page_url
|
||||
!!paginatedCollectionProducts.pagination?.next_page_url,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
import { useEffect, useState, useMemo, useRef } from "react";
|
||||
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Result, Button } from "antd";
|
||||
import { Result, Button, Drawer } from "antd";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { LuFilter } from "react-icons/lu";
|
||||
|
||||
import styles from "./CategoryPage.module.scss";
|
||||
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 CategoryFilters from "./components/CategoryFilters";
|
||||
@@ -35,6 +34,8 @@ const CategoryPage = () => {
|
||||
brandSearchQuery: "",
|
||||
});
|
||||
|
||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||
|
||||
const routeKey = useMemo(
|
||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
||||
[categoryId, collectionId, brandId]
|
||||
@@ -43,7 +44,10 @@ const CategoryPage = () => {
|
||||
const prevRouteRef = useRef(routeKey);
|
||||
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 {
|
||||
@@ -270,36 +274,70 @@ const CategoryPage = () => {
|
||||
</p>
|
||||
|
||||
<div className={styles.bars}>
|
||||
<button className={styles.sum}>
|
||||
<strong>{t("category.total")}:</strong> <br />
|
||||
{totalItems} {t("category.items")}
|
||||
<button
|
||||
className={styles.filterButton}
|
||||
onClick={() => setIsFilterDrawerOpen(true)}
|
||||
>
|
||||
{t("category.filter")} <LuFilter />
|
||||
</button>
|
||||
<BrandSidebar
|
||||
brands={filtersData?.brands || []}
|
||||
selectedBrand={filterState.selectedFilterBrand}
|
||||
onBrandSelect={handleFilterBrandSelect}
|
||||
onBrandDeselect={handleFilterBrandDeselect}
|
||||
/>
|
||||
{/* <FilterSidebar onPriceSortChange={() => {}} currentPriceSort="none" /> */}
|
||||
</div>
|
||||
|
||||
{selectedCategory?.children && !searchQuery && (
|
||||
<div className={styles.subCategories}>
|
||||
{selectedCategory.children.map((sub) => (
|
||||
<button key={sub.id} onClick={() => handleCategoryClick(sub.id)}>
|
||||
{sub.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.Container}>
|
||||
<Drawer
|
||||
title={t("category.filter")}
|
||||
placement="right"
|
||||
onClose={() => setIsFilterDrawerOpen(false)}
|
||||
open={isFilterDrawerOpen}
|
||||
width="80%"
|
||||
>
|
||||
<CategoryFilters
|
||||
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}
|
||||
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}
|
||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||
onBrandSelect={handleFilterBrandSelect}
|
||||
|
||||
@@ -12,17 +12,16 @@ import {
|
||||
import ReviewSection from "../../components/Review/index";
|
||||
import { Modal } from "antd";
|
||||
|
||||
import { debounce } from "lodash";
|
||||
import {
|
||||
useAddFavoriteMutation,
|
||||
useRemoveFavoriteMutation,
|
||||
} from "../../app/api/favoritesApi";
|
||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
import {
|
||||
useAddToCartMutation,
|
||||
useUpdateCartItemMutation,
|
||||
useRemoveFromCartMutation,
|
||||
useGetCartQuery,
|
||||
} from "../../app/api/cartApi";
|
||||
import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
|
||||
import Loader from "../../components/Loader/index";
|
||||
@@ -49,52 +48,38 @@ const ProductPage = ({
|
||||
error: similarProductsError,
|
||||
isLoading: similarProductsLoading,
|
||||
} = useGetRelatedProductsQuery(productId);
|
||||
const [quantity, setQuantity] = useState(0);
|
||||
const product = productResponse?.data;
|
||||
const similarProducts = similarProductsResponse?.data;
|
||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||
const [addFavorite] = useAddFavoriteMutation();
|
||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||
const { data: favoriteProducts = [], refetch } = useGetFavoritesQuery();
|
||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||
favoriteProducts.some((fav) => fav.product?.id === product?.id),
|
||||
);
|
||||
const { data: cartData } = useGetCartQuery(undefined, {
|
||||
selectFromResult: (result) => ({
|
||||
data: result.data,
|
||||
}),
|
||||
});
|
||||
|
||||
const { getCartItem } = useCart();
|
||||
|
||||
const [addToCart] = useAddToCartMutation();
|
||||
const [updateCartItem] = useUpdateCartItemMutation();
|
||||
const [removeFromCart] = useRemoveFromCartMutation();
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (cartItem) {
|
||||
setLocalQuantity(cartItem.quantity || cartItem.product_quantity || 0);
|
||||
setPendingQuantity(cartItem.quantity || cartItem.product_quantity || 0);
|
||||
} else {
|
||||
setLocalQuantity(0);
|
||||
setPendingQuantity(0);
|
||||
}
|
||||
}, [cartData, cartItem]);
|
||||
const cartItem = getCartItem(product?.id || productId);
|
||||
|
||||
// ✅ 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(() => {
|
||||
if (Array.isArray(favoriteProducts)) {
|
||||
const isFav = favoriteProducts.some(
|
||||
@@ -104,78 +89,55 @@ const ProductPage = ({
|
||||
}
|
||||
}, [favoriteProducts, product?.id]);
|
||||
|
||||
// ✅ Toggle Favorite
|
||||
const handleToggleFavorite = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isLoading) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const originalState = localIsFavorite;
|
||||
setLocalIsFavorite(!originalState); // Optimistic Update
|
||||
|
||||
try {
|
||||
if (localIsFavorite) {
|
||||
const result = await removeFavorite(product.id).unwrap();
|
||||
if (result === "Removed" || result?.status === "success") {
|
||||
setLocalIsFavorite(false);
|
||||
}
|
||||
if (originalState) {
|
||||
await removeFavorite(product.id).unwrap();
|
||||
} else {
|
||||
const result = await addFavorite(product.id).unwrap();
|
||||
if (result === "Added" || result?.status === "success") {
|
||||
setLocalIsFavorite(true);
|
||||
await addFavorite(product.id).unwrap();
|
||||
}
|
||||
}
|
||||
// Refetch after changing favorite status
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle favorite:", error);
|
||||
setLocalIsFavorite(originalState); // Rollback
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Add to Cart (Initial)
|
||||
const handleAddToCart = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Check if stock is available
|
||||
if (product.stock <= 0) {
|
||||
setStockErrorModalVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalQuantity((prev) => prev + 1);
|
||||
setPendingQuantity((prev) => prev + 1);
|
||||
setLocalQuantity(1);
|
||||
setPendingQuantity(1);
|
||||
|
||||
try {
|
||||
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to add to cart:", error);
|
||||
setLocalQuantity((prev) => prev - 1);
|
||||
setPendingQuantity((prev) => prev - 1);
|
||||
setLocalQuantity(0);
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (localQuantity >= product.stock) {
|
||||
@@ -187,11 +149,9 @@ const ProductPage = ({
|
||||
setPendingQuantity((prev) => prev + 1);
|
||||
};
|
||||
|
||||
// Update quantity decrease handler
|
||||
const handleQuantityDecrease = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (pendingQuantity <= 1) {
|
||||
@@ -200,6 +160,9 @@ const ProductPage = ({
|
||||
setIsLoading(true);
|
||||
removeFromCart({ productId: product.id })
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
// Success handled by hook
|
||||
})
|
||||
.catch(() => {
|
||||
setLocalQuantity(1);
|
||||
setPendingQuantity(1);
|
||||
@@ -213,9 +176,19 @@ const ProductPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Debounced Cart Update
|
||||
useEffect(() => {
|
||||
const updateCart = async () => {
|
||||
if (pendingQuantity !== quantity && pendingQuantity > 0) {
|
||||
const serverQty = parseInt(
|
||||
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 {
|
||||
setIsLoading(true);
|
||||
await updateCartItem({
|
||||
@@ -224,22 +197,16 @@ const ProductPage = ({
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart item:", error);
|
||||
setLocalQuantity(quantity);
|
||||
setPendingQuantity(quantity);
|
||||
// Hata durumunda geri al
|
||||
setLocalQuantity(serverQty);
|
||||
setPendingQuantity(serverQty);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, 500);
|
||||
|
||||
const debouncedUpdate = debounce(updateCart, 300);
|
||||
|
||||
if (pendingQuantity !== quantity) {
|
||||
debouncedUpdate();
|
||||
}
|
||||
|
||||
return () => debouncedUpdate.cancel();
|
||||
}, [pendingQuantity, quantity, product, updateCartItem]);
|
||||
return () => clearTimeout(handler);
|
||||
}, [pendingQuantity, cartItem, product?.id, updateCartItem]);
|
||||
|
||||
if (productLoading || similarProductsLoading) return <Loader />;
|
||||
if (productError || similarProductsError)
|
||||
|
||||
Reference in New Issue
Block a user