added new ui for mobile phones

This commit is contained in:
@jcarymuhammedow
2026-02-26 12:49:38 +05:00
parent 4e58062899
commit 013c7c09c7
8 changed files with 670 additions and 20 deletions

View File

@@ -140,6 +140,7 @@ export default {
photo: "Photo",
brand: "Brand",
code: "Code",
channel: "Store",
quantity: "Quantity",
price: "Price",
total: "Total",

View File

@@ -137,6 +137,7 @@ export default {
photo: "Фото",
brand: "Бренд",
code: "Код",
channel: "Магазин",
quantity: "Количество",
price: "Цена",
total: "Итого",

View File

@@ -141,6 +141,7 @@ export default {
brand: "Brend",
code: "Kody",
quantity: "Sany",
channel: "Magazin",
price: "Bahasy",
total: "Jemi",
orderNumber: "Sargyt belgisi",

View File

@@ -1,3 +1,9 @@
.mobilePhoneGrid {
display: flex !important;
flex-direction: column;
gap: 0;
}
// Price Filter Styles
.priceFilterContainer {
display: flex;

View 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;

View 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;
}
}

View File

@@ -16,6 +16,8 @@ import CategoryBreadcrumbs from "./components/CategoryBreadcrumbs";
import useCategoryData from "./hooks/useCategoryData";
import useCategoryProducts from "./hooks/useCategoryProducts";
import MobilePhoneCard from "./components/Mobilephonecard";
const CategoryPage = () => {
const { t } = useTranslation();
const { categoryId, collectionId, brandId } = useParams();
@@ -35,6 +37,13 @@ const CategoryPage = () => {
});
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(
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
@@ -86,7 +95,10 @@ const CategoryPage = () => {
maxPrice: pageState.maxPrice,
searchQuery,
});
const isMobilePhoneView =
(Number(categoryId) === 531 ||
Number(filterState.selectedFilterCategory) === 531) &&
windowWidth >= 768;
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
@@ -298,12 +310,20 @@ const CategoryPage = () => {
minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => {
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
setPageState((prev) => ({
...prev,
minPrice: value,
currentPage: 1,
}));
setAllProducts([]);
setHasMore(true);
}}
onMaxPriceChange={(value) => {
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
setPageState((prev) => ({
...prev,
maxPrice: value,
currentPage: 1,
}));
setAllProducts([]);
setHasMore(true);
}}
@@ -317,7 +337,6 @@ const CategoryPage = () => {
/>
</Drawer>
<div className={styles.Container}>
<CategoryFilters
className={styles.sidebar}
@@ -329,12 +348,20 @@ const CategoryPage = () => {
minPrice={pageState.minPrice}
maxPrice={pageState.maxPrice}
onMinPriceChange={(value) => {
setPageState((prev) => ({ ...prev, minPrice: value, currentPage: 1 }));
setPageState((prev) => ({
...prev,
minPrice: value,
currentPage: 1,
}));
setAllProducts([]);
setHasMore(true);
}}
onMaxPriceChange={(value) => {
setPageState((prev) => ({ ...prev, maxPrice: value, currentPage: 1 }));
setPageState((prev) => ({
...prev,
maxPrice: value,
currentPage: 1,
}));
setAllProducts([]);
setHasMore(true);
}}
@@ -363,16 +390,27 @@ const CategoryPage = () => {
<Loader />
</div>
}
className={styles.productGrid}
className={`${styles.productGrid} ${
isMobilePhoneView ? styles.mobilePhoneGrid : ""
}`}
>
{filteredProducts.map((product) => (
{filteredProducts.map((product) =>
isMobilePhoneView ? (
<MobilePhoneCard
key={product.id}
product={product}
showFavoriteButton
showAddToCart
/>
) : (
<ProductCard
key={product.id}
product={product}
showFavoriteButton
showAddToCart
/>
))}
)
)}
</InfiniteScroll>
) : (
<div>{t("search.noResults")}</div>

View File

@@ -56,7 +56,7 @@ const ProductPage = ({
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
const [isLoading, setIsLoading] = useState(false);
const [localIsFavorite, setLocalIsFavorite] = useState(
favoriteProducts.some((fav) => fav.product?.id === product?.id),
favoriteProducts.some((fav) => fav.product?.id === product?.id)
);
const { getCartItem } = useCart();
@@ -73,7 +73,7 @@ const ProductPage = ({
useEffect(() => {
const qty = parseInt(
cartItem?.quantity || cartItem?.product_quantity || 0,
10,
10
);
setLocalQuantity(qty);
setPendingQuantity(qty);
@@ -83,7 +83,7 @@ const ProductPage = ({
useEffect(() => {
if (Array.isArray(favoriteProducts)) {
const isFav = favoriteProducts.some(
(fav) => fav.product?.id === product?.id,
(fav) => fav.product?.id === product?.id
);
setLocalIsFavorite(isFav);
}
@@ -180,7 +180,7 @@ const ProductPage = ({
useEffect(() => {
const serverQty = parseInt(
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)
@@ -278,6 +278,15 @@ const ProductPage = ({
<span className={styles.metaValue}>{product.brand.name}</span>
</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 className={styles.productActions}>
<div className={styles.priceContainer}>