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",
|
"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: () => ({}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,20 +25,20 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
query: (brandId) => `/brands/${brandId}`,
|
query: (brandId) => `/brands/${brandId}`,
|
||||||
transformResponse: (response) => response.data || response,
|
transformResponse: (response) => response.data || response,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getBrandProducts: builder.query({
|
getBrandProducts: builder.query({
|
||||||
query: (params) => {
|
query: (params) => {
|
||||||
if (typeof params === 'string' || typeof params === 'number') {
|
if (typeof params === 'string' || typeof params === 'number') {
|
||||||
return `/brands/${params}/products`;
|
return `/brands/${params}/products`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, page = 1, limit } = params;
|
const { id, page = 1, limit } = params;
|
||||||
let url = `/brands/${id}/products?page=${page}`;
|
let url = `/brands/${id}/products?page=${page}`;
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
url += `&limit=${limit}`;
|
url += `&limit=${limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = [
|
||||||
@@ -88,4 +73,4 @@ const FooterBar = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FooterBar;
|
export default FooterBar;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -305,4 +289,4 @@ const NavbarDown = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavbarDown;
|
export default NavbarDown;
|
||||||
|
|||||||
@@ -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,
|
||||||
setLocalQuantity(qty);
|
10,
|
||||||
setPendingQuantity(qty);
|
);
|
||||||
} else {
|
setLocalQuantity(qty);
|
||||||
setLocalQuantity(0);
|
setPendingQuantity(qty);
|
||||||
setPendingQuantity(0);
|
}, [cartItem]);
|
||||||
}
|
|
||||||
}, [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) {
|
||||||
try {
|
return;
|
||||||
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);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedUpdate = debounce(updateCart, 300);
|
|
||||||
|
|
||||||
const currentCartQty =
|
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0;
|
|
||||||
if (pendingQuantity !== currentCartQty) {
|
|
||||||
debouncedUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => debouncedUpdate.cancel();
|
const handler = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateCartItem({
|
||||||
|
productId: product.id,
|
||||||
|
quantity: pendingQuantity,
|
||||||
|
}).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update cart item:", error);
|
||||||
|
setLocalQuantity(serverQty);
|
||||||
|
setPendingQuantity(serverQty);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(handler);
|
||||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||||
|
|
||||||
const handleQuantityIncrease = (event) => {
|
const handleQuantityIncrease = (event) => {
|
||||||
|
|||||||
@@ -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!",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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:
|
||||||
|
"Мы всегда открыты к сотрудничеству и рады обратной связи. Есть идея, вопрос или предложение? Напишите нам — мы с удовольствием ответим!",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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!",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
@@ -35,8 +35,8 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
|||||||
__html: isExpanded
|
__html: isExpanded
|
||||||
? description
|
? description
|
||||||
: shouldTruncate
|
: shouldTruncate
|
||||||
? description.substring(0, maxLength) + "..."
|
? description.substring(0, maxLength) + "..."
|
||||||
: description,
|
: description,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,25 +44,8 @@ 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({});
|
||||||
const [addToCart] = useAddToCartMutation();
|
const [addToCart] = useAddToCartMutation();
|
||||||
@@ -89,20 +72,22 @@ 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)
|
||||||
if (!items || !items.length) return null;
|
.map(([storeSlug, items]) => {
|
||||||
|
if (!items || !items.length) return null;
|
||||||
// Get store info from first item
|
|
||||||
const storeInfo = items[0]?.product?.channel?.[0];
|
// Get store info from first item
|
||||||
|
const storeInfo = items[0]?.product?.channel?.[0];
|
||||||
return {
|
|
||||||
id: storeInfo?.id || storeSlug,
|
return {
|
||||||
name: storeInfo?.name || storeSlug,
|
id: storeInfo?.id || storeSlug,
|
||||||
slug: storeSlug,
|
name: storeInfo?.name || storeSlug,
|
||||||
shipping_price: storeInfo?.shipping_price,
|
slug: storeSlug,
|
||||||
items: items
|
shipping_price: storeInfo?.shipping_price,
|
||||||
};
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
timers[productId] = setTimeout(async () => {
|
||||||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
try {
|
||||||
|
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();
|
||||||
}
|
} catch (error) {
|
||||||
// ✅ RTK Query otomatik cache'i güncelleyecek, refetch'e gerek yok
|
console.error("Failed to update cart:", error);
|
||||||
|
// Hata durumunda rollback
|
||||||
} 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;
|
|
||||||
setLocalQuantities((prev) => ({
|
setLocalQuantities((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[productId]: originalQty,
|
[productId]: serverQuantity,
|
||||||
}));
|
}));
|
||||||
setPendingQuantities((prev) => ({
|
setPendingQuantities((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[productId]: originalQty,
|
[productId]: serverQuantity,
|
||||||
}));
|
}));
|
||||||
|
} finally {
|
||||||
|
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||||||
}
|
}
|
||||||
} finally {
|
}, 500);
|
||||||
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]();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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,27 +213,25 @@ 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)
|
||||||
: 20;
|
: 20;
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -292,7 +258,7 @@ const CartPage = () => {
|
|||||||
if (itemToDelete) {
|
if (itemToDelete) {
|
||||||
try {
|
try {
|
||||||
await removeFromCart({ productId: itemToDelete }).unwrap();
|
await removeFromCart({ productId: itemToDelete }).unwrap();
|
||||||
|
|
||||||
setLocalQuantities((prev) => {
|
setLocalQuantities((prev) => {
|
||||||
const newState = { ...prev };
|
const newState = { ...prev };
|
||||||
delete newState[itemToDelete];
|
delete newState[itemToDelete];
|
||||||
@@ -318,9 +284,7 @@ const CartPage = () => {
|
|||||||
const handleEmptyCartConfirm = async () => {
|
const handleEmptyCartConfirm = async () => {
|
||||||
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,14 +361,14 @@ 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;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{background:"white", width: "100%"}}>
|
<div style={{ background: "white", width: "100%" }}>
|
||||||
<div className={styles.storeHeader}>
|
<div className={styles.storeHeader}>
|
||||||
<h3>{store.name}</h3>
|
<h3>{store.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -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>
|
||||||
@@ -547,4 +526,4 @@ const CartPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CartPage;
|
export default CartPage;
|
||||||
|
|||||||
@@ -1,3 +1,144 @@
|
|||||||
|
// Price Filter Styles
|
||||||
|
.priceFilterContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersContainer{
|
||||||
|
|
||||||
|
.filterSection {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: auto;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customRadio {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #d1d5db;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
background-color: #d1d5db;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + .customRadio {
|
||||||
|
background-color: #888888;
|
||||||
|
}
|
||||||
|
input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customCheckbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #d1d5db;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkIcon {
|
||||||
|
display: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked + .customCheckbox {
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked + .customCheckbox .checkIcon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.categoryPage {
|
.categoryPage {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -35,42 +176,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bars {
|
.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) {
|
@media screen and (min-width: 1024px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.sum {
|
|
||||||
color: #6b7280;
|
.filterButton {
|
||||||
font-size: 12px;
|
position: fixed;
|
||||||
text-align: left;
|
bottom: 80px;
|
||||||
background-color: transparent;
|
right: 20px;
|
||||||
border: 1px solid #6b7280;
|
z-index: 1000;
|
||||||
padding: 3px 6px;
|
background-color: #d32824;
|
||||||
display: block;
|
border-radius: 12px;
|
||||||
border-radius: 0.5rem;
|
font-size: 16px;
|
||||||
margin: 0;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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 {
|
.subCategories {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -100,15 +234,18 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
aside {
|
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 5rem;
|
top: 5rem;
|
||||||
background-color: #ffff;
|
background-color: #ffff;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow-x: auto;
|
overflow-y: auto;
|
||||||
height: calc(-8.25rem + 100vh);
|
height: calc(-8.25rem + 100vh);
|
||||||
|
|
||||||
@media screen and (max-width: 1280px) {
|
@media screen and (max-width: 1280px) {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
@@ -124,98 +261,7 @@
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterSection {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #000000;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
width: auto;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customRadio {
|
|
||||||
display: inline-block;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid #d1d5db;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 8px;
|
|
||||||
background-color: #d1d5db;
|
|
||||||
transition: background-color 0.2s, border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"]:checked + .customRadio {
|
|
||||||
background-color: #888888;
|
|
||||||
}
|
|
||||||
input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customCheckbox {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
margin-right: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #d1d5db;
|
|
||||||
position: relative;
|
|
||||||
transition: background-color 0.2s, border-color 0.2s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkIcon {
|
|
||||||
display: none;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
fill: #888888;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked + .customCheckbox {
|
|
||||||
background-color: #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked + .customCheckbox .checkIcon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,33 +176,37 @@ const ProductPage = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Debounced Cart Update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateCart = async () => {
|
const serverQty = parseInt(
|
||||||
if (pendingQuantity !== quantity && pendingQuantity > 0) {
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
try {
|
10,
|
||||||
setIsLoading(true);
|
);
|
||||||
await updateCartItem({
|
|
||||||
productId: product.id,
|
|
||||||
quantity: pendingQuantity,
|
|
||||||
}).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update cart item:", error);
|
|
||||||
setLocalQuantity(quantity);
|
|
||||||
setPendingQuantity(quantity);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedUpdate = debounce(updateCart, 300);
|
// Sadece miktar değiştiyse ve 0'dan büyükse güncelle (0 ise Remove triggerlanır)
|
||||||
|
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
||||||
if (pendingQuantity !== quantity) {
|
return;
|
||||||
debouncedUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => debouncedUpdate.cancel();
|
const handler = setTimeout(async () => {
|
||||||
}, [pendingQuantity, quantity, product, updateCartItem]);
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateCartItem({
|
||||||
|
productId: product.id,
|
||||||
|
quantity: pendingQuantity,
|
||||||
|
}).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update cart item:", error);
|
||||||
|
// Hata durumunda geri al
|
||||||
|
setLocalQuantity(serverQty);
|
||||||
|
setPendingQuantity(serverQty);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [pendingQuantity, cartItem, product?.id, updateCartItem]);
|
||||||
|
|
||||||
if (productLoading || similarProductsLoading) return <Loader />;
|
if (productLoading || similarProductsLoading) return <Loader />;
|
||||||
if (productError || similarProductsError)
|
if (productError || similarProductsError)
|
||||||
|
|||||||
Reference in New Issue
Block a user