From 696853e9884d2d1ddce35650d4ab997d8c22c9c6 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Mon, 30 Mar 2026 22:08:32 +0500 Subject: [PATCH] added category carousel --- .../CategoryCarousel/CategoryCarousel.jsx | 115 ++++++++++++++ .../Categorycarousel.module.scss | 145 ++++++++++++++++++ .../imageCarousel/ImageCarousel.module.scss | 2 + .../ProductDetail/ProductPage.module.scss | 39 +++-- src/pages/ProductDetail/index.jsx | 23 ++- src/pages/home/index.jsx | 11 +- 6 files changed, 302 insertions(+), 33 deletions(-) create mode 100644 src/components/CategoryCarousel/CategoryCarousel.jsx create mode 100644 src/components/CategoryCarousel/Categorycarousel.module.scss diff --git a/src/components/CategoryCarousel/CategoryCarousel.jsx b/src/components/CategoryCarousel/CategoryCarousel.jsx new file mode 100644 index 0000000..ea06cbd --- /dev/null +++ b/src/components/CategoryCarousel/CategoryCarousel.jsx @@ -0,0 +1,115 @@ +import React, { useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { useGetCategoriesQuery } from "../../app/api/categories.js"; +import styles from "./CategoryCarousel.module.scss"; + +const CategoryCarousel = () => { + const { data, isLoading } = useGetCategoriesQuery("tree"); + const scrollRef = useRef(null); + const navigate = useNavigate(); + + const isDragging = useRef(false); + const startX = useRef(0); + const scrollLeft = useRef(0); + const hasDragged = useRef(false); + + const mainCategories = + data?.data?.filter((cat) => cat.parent_id === null) ?? []; + + const scroll = (dir) => { + if (!scrollRef.current) return; + scrollRef.current.scrollBy({ left: dir * 320, behavior: "smooth" }); + }; + + const onMouseDown = (e) => { + isDragging.current = true; + hasDragged.current = false; + startX.current = e.pageX - scrollRef.current.offsetLeft; + scrollLeft.current = scrollRef.current.scrollLeft; + scrollRef.current.style.cursor = "grabbing"; + }; + + const onMouseMove = (e) => { + if (!isDragging.current) return; + e.preventDefault(); + const x = e.pageX - scrollRef.current.offsetLeft; + const walk = x - startX.current; + if (Math.abs(walk) > 4) hasDragged.current = true; + scrollRef.current.scrollLeft = scrollLeft.current - walk; + }; + + const onMouseUp = () => { + isDragging.current = false; + if (scrollRef.current) scrollRef.current.style.cursor = "grab"; + }; + + const handleCardClick = (e, slug) => { + if (hasDragged.current) { + e.preventDefault(); + return; + } + navigate(`/category/${slug}`); + }; + + if (isLoading || mainCategories.length === 0) return null; + + return ( +
+ + +
+ {mainCategories.map((cat) => { + const thumb = + cat.media?.[0]?.images_400x400 ?? + cat.media?.[0]?.thumbnail ?? + null; + + return ( +
handleCardClick(e, cat.slug)} + > +
+ {thumb ? ( + {cat.name} + ) : ( +
+ )} +
+ {cat.name} +
+ ); + })} +
+ + +
+ ); +}; + +export default CategoryCarousel; \ No newline at end of file diff --git a/src/components/CategoryCarousel/Categorycarousel.module.scss b/src/components/CategoryCarousel/Categorycarousel.module.scss new file mode 100644 index 0000000..8e4b02f --- /dev/null +++ b/src/components/CategoryCarousel/Categorycarousel.module.scss @@ -0,0 +1,145 @@ +.wrapper { + position: relative; + display: flex; + align-items: center; + gap: 4px; + margin: 20px 0 24px 0; +} + +/* ── Scrollable track ── */ +.track { + display: flex; + gap: 12px; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding: 4px 2px 8px 2px; + flex: 1; + cursor: grab; + user-select: none; + + &::-webkit-scrollbar { + display: none; + } + + &:active { + cursor: grabbing; + } +} + +/* ── Card ── */ +.card { + flex: 0 0 auto; + scroll-snap-align: start; + width: 180px; + border-radius: 12px; + background: #f5f5f5; + overflow: hidden; + cursor: pointer; + display: flex; + flex-direction: column; + transition: box-shadow 0.2s ease, transform 0.2s ease; + + &:hover { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + + .imgWrap img { + transform: scale(1.04); + } + } +} + +/* ── Image area ── */ +.imgWrap { + width: 100%; + aspect-ratio: 1 / 1; + overflow: hidden; + background: #ebebeb; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + pointer-events: none; + } +} + +.placeholder { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #e0e0e0 0%, #d0d0d0 100%); +} + +/* ── Name label ── */ +.name { + padding: 10px 12px 12px 12px; + font-size: 13px; + font-weight: 500; + color: #222; + text-align: center; + line-height: 1.4; + background: #fff; +} + +/* ── Arrow buttons ── */ +.arrow { + flex: 0 0 auto; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid #ddd; + background: #fff; + font-size: 22px; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #444; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + transition: background 0.15s, border-color 0.15s, color 0.15s, + box-shadow 0.15s; + padding: 0; + z-index: 1; + + &:hover { + background: #d24141; + border-color: #d24141; + color: #fff; + box-shadow: 0 2px 8px rgba(210, 65, 65, 0.35); + } + + &.left { + margin-right: 4px; + } + + &.right { + margin-left: 4px; + } +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .arrow { + display: none; + } + + .card { + width: 140px; + } +} + +@media (max-width: 480px) { + .card { + width: 120px; + border-radius: 10px; + } + + .name { + font-size: 12px; + padding: 8px 8px 10px 8px; + } +} \ No newline at end of file diff --git a/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss b/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss index b84dc33..7c3297f 100644 --- a/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss +++ b/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss @@ -4,6 +4,7 @@ height: 100%; overflow: hidden; touch-action: pan-y; + border-radius: 8px; } .productImage { width: 99%; @@ -149,6 +150,7 @@ justify-content: center; width: 100%; position: relative; + padding: 12px; } .thumbnail { diff --git a/src/pages/ProductDetail/ProductPage.module.scss b/src/pages/ProductDetail/ProductPage.module.scss index e396221..71b5cbd 100644 --- a/src/pages/ProductDetail/ProductPage.module.scss +++ b/src/pages/ProductDetail/ProductPage.module.scss @@ -28,10 +28,10 @@ display: flex; gap: 24px; align-items: flex-start; - background-color: #fff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + // background-color: #fff; + // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 4px; - padding: 1.25rem; + // padding: 1.25rem; box-sizing: border-box; @media screen and (max-width: 900px) { @@ -40,26 +40,30 @@ @media screen and (max-width: 639px) { flex-direction: column; - padding: 0.75rem; + border-radius: 8px; + // padding: 0.75rem; } } // ─── Sol: resim kolonu ──────────────────────────────────────────── .productImage { background: #fff; - padding: 20px; + // padding: 20px; border-radius: 8px; width: 35%; flex-shrink: 0; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); @media screen and (max-width: 900px) { width: 45%; - padding: 5px; + border-radius: 8px; + // padding: 5px; } @media screen and (max-width: 639px) { width: 100%; padding: 0; + border-radius: 8px; } img { @@ -76,6 +80,7 @@ flex-direction: column; gap: 12px; min-width: 0; + @media screen and (max-width: 900px) { // tablet: image(45%) + info yan yana, purchase wrap ile alta iner @@ -132,7 +137,8 @@ } .priceLabel { - font-size: 15px; + font-size: 18px; + font-weight: 500; color: #666; } @@ -267,14 +273,18 @@ .productMeta { border: 1px solid #e5e7eb; border-radius: 8px; - overflow: hidden; - background: #f5f5f5; + padding: 16px 20px; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + gap: 12px; + display: flex; + flex-direction: column; .metaItem { display: flex; justify-content: space-between; - padding: 8px 16px; - border-bottom: 2px solid #ffffff; + padding-bottom: 8px; + border-bottom: 1px solid #f1f1f1; &:last-child { border-bottom: none; @@ -284,11 +294,12 @@ .metaLabel { color: #000; font-size: 14px; + font-weight: 600; } .metaValue { font-size: 14px; - font-weight: 500; + font-weight: 600; } } @@ -298,7 +309,7 @@ border-radius: 8px; padding: 16px 20px; background: #fff; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); } .descriptionHeader { @@ -356,6 +367,8 @@ padding: 10px 16px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08); gap: 12px; + border-radius: 8px; + margin-top: 8px; } } diff --git a/src/pages/ProductDetail/index.jsx b/src/pages/ProductDetail/index.jsx index 68cfbe2..18649fb 100644 --- a/src/pages/ProductDetail/index.jsx +++ b/src/pages/ProductDetail/index.jsx @@ -60,7 +60,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(); @@ -77,7 +77,7 @@ const ProductPage = ({ useEffect(() => { const qty = parseInt( cartItem?.quantity || cartItem?.product_quantity || 0, - 10 + 10, ); setLocalQuantity(qty); setPendingQuantity(qty); @@ -87,7 +87,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); } @@ -184,7 +184,7 @@ const ProductPage = ({ useEffect(() => { const serverQty = parseInt( cartItem?.quantity || cartItem?.product_quantity || 0, - 10 + 10, ); if (pendingQuantity === serverQty || pendingQuantity <= 0) { @@ -308,7 +308,6 @@ const ProductPage = ({ {/* ── 3 kolon ana section ── */}
- {/* KOLON 1: Resim */}
-

{product.name}

- {/* Meta tablo */}
-
+

{product.name}

+ + {/*
{t("product.productCode")} @@ -337,7 +336,7 @@ const ProductPage = ({ {t("product.barCode")} {product.barcode}
- )} + )} */} {product.brand?.name && (
@@ -389,9 +388,7 @@ const ProductPage = ({
{t("product.price")}:
- - {product.price_amount} m. - + {product.price_amount} m. {product.old_price_amount && ( {product.old_price_amount} m. @@ -475,4 +472,4 @@ const ProductPage = ({ ); }; -export default ProductPage; \ No newline at end of file +export default ProductPage; diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx index cde881a..c7c7e49 100644 --- a/src/pages/home/index.jsx +++ b/src/pages/home/index.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; import CategorySection from "../../components/CategorySection/index"; import Carousel from "../../components/Banner/index"; +import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel"; import styles from "./Home.module.scss"; import { useGetCollectionsQuery } from "../../app/api/collectionsApi"; import PageLoader from "../../components/Loader/pageLoader"; @@ -20,7 +21,6 @@ const Home = () => { const processCollections = async (collectionsData) => { if (!collectionsData || !collectionsData.data) return []; - // Cache the processed collections to prevent duplicate processing const collectionsWithProducts = []; for (const collection of collectionsData.data) { @@ -44,8 +44,6 @@ const Home = () => { }; const checkIfCollectionHasProducts = async (collectionId) => { - // This is a placeholder - your actual implementation would check if products exist - // For now, we just return true as in your original code return true; }; @@ -71,7 +69,6 @@ const Home = () => { setPage(page + 1); } - // Check if we've loaded all collections if (endIndex >= collections.length) { setHasMore(false); } @@ -80,7 +77,6 @@ const Home = () => { } }; - // if (isLoading) return ; if (error) return (
@@ -100,6 +96,7 @@ const Home = () => { return (
+
{ ))} @@ -122,4 +119,4 @@ const Home = () => { ); }; -export default Home; +export default Home; \ No newline at end of file