diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index e2530ee..778c260 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -140,6 +140,7 @@ export default { photo: "Photo", brand: "Brand", code: "Code", + channel: "Store", quantity: "Quantity", price: "Price", total: "Total", diff --git a/src/i18n/locales/ru.js b/src/i18n/locales/ru.js index 247078b..994f6d5 100644 --- a/src/i18n/locales/ru.js +++ b/src/i18n/locales/ru.js @@ -137,6 +137,7 @@ export default { photo: "Фото", brand: "Бренд", code: "Код", + channel: "Магазин", quantity: "Количество", price: "Цена", total: "Итого", diff --git a/src/i18n/locales/tm.js b/src/i18n/locales/tm.js index 490ae5a..eee4cd4 100644 --- a/src/i18n/locales/tm.js +++ b/src/i18n/locales/tm.js @@ -141,6 +141,7 @@ export default { brand: "Brend", code: "Kody", quantity: "Sany", + channel: "Magazin", price: "Bahasy", total: "Jemi", orderNumber: "Sargyt belgisi", diff --git a/src/pages/Category/CategoryPage.module.scss b/src/pages/Category/CategoryPage.module.scss index 110fd9a..28de483 100644 --- a/src/pages/Category/CategoryPage.module.scss +++ b/src/pages/Category/CategoryPage.module.scss @@ -1,3 +1,9 @@ + +.mobilePhoneGrid { + display: flex !important; + flex-direction: column; + gap: 0; +} // Price Filter Styles .priceFilterContainer { display: flex; diff --git a/src/pages/Category/components/Mobilephonecard.jsx b/src/pages/Category/components/Mobilephonecard.jsx new file mode 100644 index 0000000..458992d --- /dev/null +++ b/src/pages/Category/components/Mobilephonecard.jsx @@ -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(//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 ( + <> +
+ {/* Image */} +
+ {product.stock === 0 && ( + + {t("common.out_of_stock")} + + )} + {thumbnail ? ( + {product.name} + ) : ( +
📱
+ )} +
+ + {/* Info */} +
+
+ {product.name} + {product.brand?.name && ( + {product.brand.name} + )} +
+ + {/* Dense spec paragraph — mirrors reference image */} + {specs.length > 0 && ( +

+ {specs.map((s, i) => ( + + {s.label}: + {s.value} + {i < specs.length - 1 && "; "} + + ))} +

+ )} + + {/* Footer */} +
e.stopPropagation()}> +
+ {product.price_amount} m. + {hasDiscount && ( + + {product.old_price_amount} m. + + )} +
+ +
+ {showFavoriteButton && ( + + )} + + {showAddToCart && + (localQuantity > 0 ? ( +
+ + {localQuantity} + +
+ ) : ( + + ))} +
+
+
+
+ + setStockErrorModalVisible(false)} + onCancel={() => setStockErrorModalVisible(false)} + footer={[ + , + ]} + > +

+ {t("common.not_enough_stock", { + available: product.stock, + requested: localQuantity + 1, + })} +

+
+ + ); +}; + +export const MOBILE_PHONE_CATEGORY_ID = 531; +export default MobilePhoneCard; diff --git a/src/pages/Category/components/Mobilephonecard.module.scss b/src/pages/Category/components/Mobilephonecard.module.scss new file mode 100644 index 0000000..50acd63 --- /dev/null +++ b/src/pages/Category/components/Mobilephonecard.module.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/Category/index.jsx b/src/pages/Category/index.jsx index 8f78d2a..1233399 100644 --- a/src/pages/Category/index.jsx +++ b/src/pages/Category/index.jsx @@ -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,10 +337,9 @@ 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 = () => {
} - className={styles.productGrid} + className={`${styles.productGrid} ${ + isMobilePhoneView ? styles.mobilePhoneGrid : "" + }`} > - {filteredProducts.map((product) => ( - - ))} + {filteredProducts.map((product) => + isMobilePhoneView ? ( + + ) : ( + + ) + )} ) : (
{t("search.noResults")}
diff --git a/src/pages/ProductDetail/index.jsx b/src/pages/ProductDetail/index.jsx index 91a36af..7f8647f 100644 --- a/src/pages/ProductDetail/index.jsx +++ b/src/pages/ProductDetail/index.jsx @@ -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 = ({ {product.brand.name} )} + + {product.channel?.[0]?.name && ( +
+ {t("order.channel")} + + {product.channel[0].name} + +
+ )}