added new ui for mobile phones
This commit is contained in:
@@ -140,6 +140,7 @@ export default {
|
|||||||
photo: "Photo",
|
photo: "Photo",
|
||||||
brand: "Brand",
|
brand: "Brand",
|
||||||
code: "Code",
|
code: "Code",
|
||||||
|
channel: "Store",
|
||||||
quantity: "Quantity",
|
quantity: "Quantity",
|
||||||
price: "Price",
|
price: "Price",
|
||||||
total: "Total",
|
total: "Total",
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export default {
|
|||||||
photo: "Фото",
|
photo: "Фото",
|
||||||
brand: "Бренд",
|
brand: "Бренд",
|
||||||
code: "Код",
|
code: "Код",
|
||||||
|
channel: "Магазин",
|
||||||
quantity: "Количество",
|
quantity: "Количество",
|
||||||
price: "Цена",
|
price: "Цена",
|
||||||
total: "Итого",
|
total: "Итого",
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export default {
|
|||||||
brand: "Brend",
|
brand: "Brend",
|
||||||
code: "Kody",
|
code: "Kody",
|
||||||
quantity: "Sany",
|
quantity: "Sany",
|
||||||
|
channel: "Magazin",
|
||||||
price: "Bahasy",
|
price: "Bahasy",
|
||||||
total: "Jemi",
|
total: "Jemi",
|
||||||
orderNumber: "Sargyt belgisi",
|
orderNumber: "Sargyt belgisi",
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
|
||||||
|
.mobilePhoneGrid {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
// Price Filter Styles
|
// Price Filter Styles
|
||||||
.priceFilterContainer {
|
.priceFilterContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
324
src/pages/Category/components/Mobilephonecard.jsx
Normal file
324
src/pages/Category/components/Mobilephonecard.jsx
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||||
|
import { FaShoppingCart } from "react-icons/fa";
|
||||||
|
import { DecreaseIcon, IncreaseIcon } from "../../../components/Icons";
|
||||||
|
import {
|
||||||
|
useAddFavoriteMutation,
|
||||||
|
useRemoveFavoriteMutation,
|
||||||
|
useGetFavoritesQuery,
|
||||||
|
} from "../../../app/api/favoritesApi";
|
||||||
|
import {
|
||||||
|
useAddToCartMutation,
|
||||||
|
useUpdateCartItemMutation,
|
||||||
|
useRemoveFromCartMutation,
|
||||||
|
} from "../../../app/api/cartApi";
|
||||||
|
import { useCart } from "../../../app/api/useCart";
|
||||||
|
import styles from "./MobilePhoneCard.module.scss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses product.description HTML into spec pairs.
|
||||||
|
* Format inside HTML: "Label1: Value1; Label2: Value2; ..."
|
||||||
|
*/
|
||||||
|
const parseSpecs = (htmlString) => {
|
||||||
|
if (!htmlString) return [];
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.innerHTML = htmlString;
|
||||||
|
|
||||||
|
let processedHtml = htmlString
|
||||||
|
.replace(/<br\s*\/?>/gi, "\n")
|
||||||
|
.replace(/<\/div>/gi, "\n")
|
||||||
|
.replace(/<\/strong>/gi, ": ");
|
||||||
|
|
||||||
|
div.innerHTML = processedHtml;
|
||||||
|
const text = (div.textContent || div.innerText || "").trim();
|
||||||
|
|
||||||
|
return text
|
||||||
|
.split(/[;\n]+/)
|
||||||
|
.map((chunk) => chunk.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((chunk) => {
|
||||||
|
const separatorIdx = chunk.search(/[:|]/);
|
||||||
|
if (separatorIdx === -1) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: chunk.slice(0, separatorIdx).trim(),
|
||||||
|
value: chunk.slice(separatorIdx + 1).trim(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null && item.value !== "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobilePhoneCard = ({
|
||||||
|
product,
|
||||||
|
showAddToCart = true,
|
||||||
|
showFavoriteButton = true,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// ── Favorites ──────────────────────────────────────────────────────────────
|
||||||
|
const [addFavorite] = useAddFavoriteMutation();
|
||||||
|
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||||
|
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||||
|
const [localIsFavorite, setLocalIsFavorite] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Array.isArray(favoriteProducts)) {
|
||||||
|
setLocalIsFavorite(
|
||||||
|
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [favoriteProducts, product.id]);
|
||||||
|
|
||||||
|
// ── Cart ───────────────────────────────────────────────────────────────────
|
||||||
|
const { getCartItem } = useCart();
|
||||||
|
const [addToCart] = useAddToCartMutation();
|
||||||
|
const [updateCartItem] = useUpdateCartItemMutation();
|
||||||
|
const [removeFromCart] = useRemoveFromCartMutation();
|
||||||
|
|
||||||
|
const cartItem = getCartItem(product.id);
|
||||||
|
const [localQuantity, setLocalQuantity] = useState(0);
|
||||||
|
const [pendingQuantity, setPendingQuantity] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const qty = parseInt(
|
||||||
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
setLocalQuantity(qty);
|
||||||
|
setPendingQuantity(qty);
|
||||||
|
}, [cartItem]);
|
||||||
|
|
||||||
|
// Debounced sync to server
|
||||||
|
useEffect(() => {
|
||||||
|
const serverQty = parseInt(
|
||||||
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
if (pendingQuantity === serverQty || pendingQuantity <= 0) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateCartItem({
|
||||||
|
productId: product.id,
|
||||||
|
quantity: pendingQuantity,
|
||||||
|
}).unwrap();
|
||||||
|
} catch {
|
||||||
|
setLocalQuantity(serverQty);
|
||||||
|
setPendingQuantity(serverQty);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||||
|
|
||||||
|
// ── Handlers ───────────────────────────────────────────────────────────────
|
||||||
|
const handleCardClick = () => navigate(`/product/${product.id}`);
|
||||||
|
|
||||||
|
const handleAddToCart = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (product.stock <= 0) {
|
||||||
|
setStockErrorModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalQuantity((p) => p + 1);
|
||||||
|
setPendingQuantity((p) => p + 1);
|
||||||
|
try {
|
||||||
|
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||||
|
} catch {
|
||||||
|
setLocalQuantity((p) => p - 1);
|
||||||
|
setPendingQuantity((p) => p - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIncrease = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isLoading) return;
|
||||||
|
if (localQuantity >= product.stock) {
|
||||||
|
setStockErrorModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalQuantity((p) => p + 1);
|
||||||
|
setPendingQuantity((p) => p + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecrease = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isLoading) return;
|
||||||
|
if (pendingQuantity <= 1) {
|
||||||
|
setLocalQuantity(0);
|
||||||
|
setPendingQuantity(0);
|
||||||
|
setIsLoading(true);
|
||||||
|
removeFromCart({ productId: product.id })
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
setLocalQuantity(1);
|
||||||
|
setPendingQuantity(1);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
} else {
|
||||||
|
setLocalQuantity((p) => p - 1);
|
||||||
|
setPendingQuantity((p) => p - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isLoading) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
setLocalIsFavorite((prev) => !prev);
|
||||||
|
try {
|
||||||
|
if (localIsFavorite) await removeFavorite(product.id).unwrap();
|
||||||
|
else await addFavorite(product.id).unwrap();
|
||||||
|
} catch {
|
||||||
|
setLocalIsFavorite((prev) => !prev);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Derived ────────────────────────────────────────────────────────────────
|
||||||
|
const specs = parseSpecs(product.description);
|
||||||
|
const thumbnail =
|
||||||
|
product.media?.[0]?.images_400x400 || product.media?.[0]?.thumbnail;
|
||||||
|
const hasDiscount =
|
||||||
|
product.old_price_amount &&
|
||||||
|
parseFloat(product.old_price_amount) > parseFloat(product.price_amount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.card} onClick={handleCardClick}>
|
||||||
|
{/* Image */}
|
||||||
|
<div className={styles.imageCol}>
|
||||||
|
{product.stock === 0 && (
|
||||||
|
<span className={styles.outOfStockBadge}>
|
||||||
|
{t("common.out_of_stock")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{thumbnail ? (
|
||||||
|
<img src={thumbnail} alt={product.name} className={styles.image} />
|
||||||
|
) : (
|
||||||
|
<div className={styles.imagePlaceholder}>📱</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className={styles.infoCol}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<span className={styles.name}>{product.name}</span>
|
||||||
|
{product.brand?.name && (
|
||||||
|
<span className={styles.brand}>{product.brand.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dense spec paragraph — mirrors reference image */}
|
||||||
|
{specs.length > 0 && (
|
||||||
|
<p className={styles.specLine}>
|
||||||
|
{specs.map((s, i) => (
|
||||||
|
<span key={i}>
|
||||||
|
<span className={styles.specLabel}>{s.label}: </span>
|
||||||
|
<span className={styles.specValue}>{s.value}</span>
|
||||||
|
{i < specs.length - 1 && "; "}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className={styles.footer} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className={styles.priceBlock}>
|
||||||
|
<span className={styles.price}>{product.price_amount} m.</span>
|
||||||
|
{hasDiscount && (
|
||||||
|
<span className={styles.oldPrice}>
|
||||||
|
{product.old_price_amount} m.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{showFavoriteButton && (
|
||||||
|
<button
|
||||||
|
className={`${styles.iconBtn} ${
|
||||||
|
localIsFavorite ? styles.favActive : ""
|
||||||
|
}`}
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddToCart &&
|
||||||
|
(localQuantity > 0 ? (
|
||||||
|
<div className={styles.quantityControls}>
|
||||||
|
<button
|
||||||
|
className={styles.qtyBtn}
|
||||||
|
onClick={handleDecrease}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<DecreaseIcon />
|
||||||
|
</button>
|
||||||
|
<span className={styles.qtyValue}>{localQuantity}</span>
|
||||||
|
<button
|
||||||
|
className={styles.qtyBtn}
|
||||||
|
onClick={handleIncrease}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<IncreaseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={styles.cartBtn}
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
disabled={isLoading || product.stock === 0}
|
||||||
|
>
|
||||||
|
<FaShoppingCart />
|
||||||
|
<span>Sebede goş</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t("common.warning")}
|
||||||
|
open={stockErrorModalVisible}
|
||||||
|
onOk={() => setStockErrorModalVisible(false)}
|
||||||
|
onCancel={() => setStockErrorModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<button
|
||||||
|
key="ok"
|
||||||
|
onClick={() => setStockErrorModalVisible(false)}
|
||||||
|
className={styles.modalButton}
|
||||||
|
>
|
||||||
|
{t("common.ok")}
|
||||||
|
</button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{t("common.not_enough_stock", {
|
||||||
|
available: product.stock,
|
||||||
|
requested: localQuantity + 1,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MOBILE_PHONE_CATEGORY_ID = 531;
|
||||||
|
export default MobilePhoneCard;
|
||||||
270
src/pages/Category/components/Mobilephonecard.module.scss
Normal file
270
src/pages/Category/components/Mobilephonecard.module.scss
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
// MobilePhoneCard.module.scss
|
||||||
|
// Mimics the dense spec-row layout from the reference image (e.g. DNS/Citilink product list)
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f8ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Image column ─────────────────────────────────────────────
|
||||||
|
.imageCol {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 250px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 250px;
|
||||||
|
height: 260px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagePlaceholder {
|
||||||
|
width: 110px;
|
||||||
|
height: 130px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outOfStockBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #999;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info column ──────────────────────────────────────────────
|
||||||
|
.infoCol {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0645ad;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dense spec paragraph — key selling point of this card
|
||||||
|
.specLine {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specLabel {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specValue {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Footer ───────────────────────────────────────────────────
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceBlock {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oldPrice {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Actions ──────────────────────────────────────────────────
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBtn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 34px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #888;
|
||||||
|
border-color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.favActive {
|
||||||
|
color: #888;
|
||||||
|
border-color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartBtn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
background: #d32824;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 36px;
|
||||||
|
&:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Quantity controls ────────────────────────────────────────
|
||||||
|
.quantityControls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid #d32824;
|
||||||
|
background-color: #d32824;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 160px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: background 0.1s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e86064;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyValue {
|
||||||
|
min-width: 28px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Modal ────────────────────────────────────────────────────
|
||||||
|
.modalButton {
|
||||||
|
padding: 6px 20px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import CategoryBreadcrumbs from "./components/CategoryBreadcrumbs";
|
|||||||
import useCategoryData from "./hooks/useCategoryData";
|
import useCategoryData from "./hooks/useCategoryData";
|
||||||
import useCategoryProducts from "./hooks/useCategoryProducts";
|
import useCategoryProducts from "./hooks/useCategoryProducts";
|
||||||
|
|
||||||
|
import MobilePhoneCard from "./components/Mobilephonecard";
|
||||||
|
|
||||||
const CategoryPage = () => {
|
const CategoryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { categoryId, collectionId, brandId } = useParams();
|
const { categoryId, collectionId, brandId } = useParams();
|
||||||
@@ -35,6 +37,13 @@ const CategoryPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => setWindowWidth(window.innerWidth);
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const routeKey = useMemo(
|
const routeKey = useMemo(
|
||||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
||||||
@@ -86,7 +95,10 @@ const CategoryPage = () => {
|
|||||||
maxPrice: pageState.maxPrice,
|
maxPrice: pageState.maxPrice,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
});
|
});
|
||||||
|
const isMobilePhoneView =
|
||||||
|
(Number(categoryId) === 531 ||
|
||||||
|
Number(filterState.selectedFilterCategory) === 531) &&
|
||||||
|
windowWidth >= 768;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialMount.current) {
|
if (isInitialMount.current) {
|
||||||
isInitialMount.current = false;
|
isInitialMount.current = false;
|
||||||
@@ -298,12 +310,20 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
|
setPageState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
minPrice: value,
|
||||||
|
currentPage: 1,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
|
setPageState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
maxPrice: value,
|
||||||
|
currentPage: 1,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
}}
|
}}
|
||||||
@@ -317,10 +337,9 @@ const CategoryPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
||||||
<div className={styles.Container}>
|
<div className={styles.Container}>
|
||||||
<CategoryFilters
|
<CategoryFilters
|
||||||
className={styles.sidebar}
|
className={styles.sidebar}
|
||||||
filtersData={filtersData}
|
filtersData={filtersData}
|
||||||
selectedFilterCategory={filterState.selectedFilterCategory}
|
selectedFilterCategory={filterState.selectedFilterCategory}
|
||||||
selectedFilterBrand={filterState.selectedFilterBrand}
|
selectedFilterBrand={filterState.selectedFilterBrand}
|
||||||
@@ -329,12 +348,20 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
|
setPageState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
minPrice: value,
|
||||||
|
currentPage: 1,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
|
setPageState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
maxPrice: value,
|
||||||
|
currentPage: 1,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
}}
|
}}
|
||||||
@@ -363,16 +390,27 @@ const CategoryPage = () => {
|
|||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={styles.productGrid}
|
className={`${styles.productGrid} ${
|
||||||
|
isMobilePhoneView ? styles.mobilePhoneGrid : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{filteredProducts.map((product) => (
|
{filteredProducts.map((product) =>
|
||||||
<ProductCard
|
isMobilePhoneView ? (
|
||||||
key={product.id}
|
<MobilePhoneCard
|
||||||
product={product}
|
key={product.id}
|
||||||
showFavoriteButton
|
product={product}
|
||||||
showAddToCart
|
showFavoriteButton
|
||||||
/>
|
showAddToCart
|
||||||
))}
|
/>
|
||||||
|
) : (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
showFavoriteButton
|
||||||
|
showAddToCart
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
) : (
|
) : (
|
||||||
<div>{t("search.noResults")}</div>
|
<div>{t("search.noResults")}</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const ProductPage = ({
|
|||||||
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 { getCartItem } = useCart();
|
const { getCartItem } = useCart();
|
||||||
@@ -73,7 +73,7 @@ const ProductPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const qty = parseInt(
|
const qty = parseInt(
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
10,
|
10
|
||||||
);
|
);
|
||||||
setLocalQuantity(qty);
|
setLocalQuantity(qty);
|
||||||
setPendingQuantity(qty);
|
setPendingQuantity(qty);
|
||||||
@@ -83,7 +83,7 @@ const ProductPage = ({
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ const ProductPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serverQty = parseInt(
|
const serverQty = parseInt(
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
10,
|
10
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sadece miktar değiştiyse ve 0'dan büyükse güncelle (0 ise Remove triggerlanır)
|
// Sadece miktar değiştiyse ve 0'dan büyükse güncelle (0 ise Remove triggerlanır)
|
||||||
@@ -278,6 +278,15 @@ const ProductPage = ({
|
|||||||
<span className={styles.metaValue}>{product.brand.name}</span>
|
<span className={styles.metaValue}>{product.brand.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{product.channel?.[0]?.name && (
|
||||||
|
<div className={styles.metaItem}>
|
||||||
|
<span className={styles.metaLabel}>{t("order.channel")}</span>
|
||||||
|
<span className={styles.metaValue}>
|
||||||
|
{product.channel[0].name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.productActions}>
|
<div className={styles.productActions}>
|
||||||
<div className={styles.priceContainer}>
|
<div className={styles.priceContainer}>
|
||||||
|
|||||||
Reference in New Issue
Block a user