From 684ab6917de9dffd944964e59739ae974717d924 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Thu, 30 Apr 2026 23:17:44 +0500 Subject: [PATCH] added store to navbar --- src/app/api/channelsApi.js | 23 ++- src/components/Icons/index.jsx | 17 ++- src/components/Navbar/NavbarDown.jsx | 18 ++- src/i18n/locales/en.js | 1 + src/i18n/locales/ru.js | 1 + src/i18n/locales/tm.js | 1 + src/pages/Category/index.jsx | 8 +- src/pages/ProductDetail/index.jsx | 2 +- src/pages/Stores/Stores.module.scss | 143 +++++++++++++++++++ src/pages/Stores/index.jsx | 202 +++++++++++++++++++++++++++ src/routes.jsx | 2 + 11 files changed, 408 insertions(+), 10 deletions(-) create mode 100644 src/pages/Stores/Stores.module.scss create mode 100644 src/pages/Stores/index.jsx diff --git a/src/app/api/channelsApi.js b/src/app/api/channelsApi.js index 56bc8aa..b05ff41 100644 --- a/src/app/api/channelsApi.js +++ b/src/app/api/channelsApi.js @@ -26,9 +26,28 @@ export const channelsApi = baseApi.injectEndpoints({ pagination: response.pagination || {}, }), }), + + getChannels: builder.query({ + query: (params = {}) => { + const queryParams = new URLSearchParams(); + if (params.page) queryParams.append("page", params.page); + if (params.limit) queryParams.append("limit", params.limit); + if (params.search) queryParams.append("search", params.search); + const queryString = queryParams.toString(); + return `/channels${queryString ? `?${queryString}` : ""}`; + }, + transformResponse: (response) => ({ + data: response.data || response, + pagination: response.pagination || {}, + }), + }), }), overrideExisting: true, }); -export const { useGetChannelProductsQuery, useLazyGetChannelProductsQuery } = - channelsApi; +export const { + useGetChannelProductsQuery, + useLazyGetChannelProductsQuery, + useGetChannelsQuery, + useLazyGetChannelsQuery, +} = channelsApi; diff --git a/src/components/Icons/index.jsx b/src/components/Icons/index.jsx index c8f84da..10d6654 100644 --- a/src/components/Icons/index.jsx +++ b/src/components/Icons/index.jsx @@ -207,7 +207,22 @@ export const OrderIcon = () => ( > ); - +export const StoreIcon = () => ( + + + + +); export const CategoryIcon = () => ( { +
+
  • + + + +
  • {
    - Aşgabat +
    diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index 5818676..a167e25 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -5,6 +5,7 @@ export default { login: "Login", signUp: "Sign Up", brands: "Brands", + stores: "Stores", search: "Search by product name...", cart: "Cart", home: "Home", diff --git a/src/i18n/locales/ru.js b/src/i18n/locales/ru.js index 4c45e24..eeec789 100644 --- a/src/i18n/locales/ru.js +++ b/src/i18n/locales/ru.js @@ -5,6 +5,7 @@ export default { login: "Войти", signUp: "Регистрация", brands: "Бренды", + stores: "Магазины", search: "Поиск по названию товара...", cart: "Корзина", home: "Главная", diff --git a/src/i18n/locales/tm.js b/src/i18n/locales/tm.js index ef26fae..b04d246 100644 --- a/src/i18n/locales/tm.js +++ b/src/i18n/locales/tm.js @@ -5,6 +5,7 @@ export default { login: "Giriş", signUp: "Agza bolmak", brands: "Brendler", + stores: "Dükanlar", search: "Haryt ady boýunça gözleg...", cart: "Sebet", home: "Baş sahypa", diff --git a/src/pages/Category/index.jsx b/src/pages/Category/index.jsx index ad1d3ce..55e2b69 100644 --- a/src/pages/Category/index.jsx +++ b/src/pages/Category/index.jsx @@ -138,7 +138,7 @@ const CategoryPage = () => { return; } - if (prevRouteRef.current === routeKey) return; + if (prevRouteRef.current === routeKey && !location.state?.clearFilters) return; prevRouteRef.current = routeKey; @@ -159,8 +159,10 @@ const CategoryPage = () => { setTimeout(() => window.scrollTo(0, savedScroll), 100); } } else { - setAllProducts([]); - setHasMore(true); + if (prevRouteRef.current !== routeKey) { + setAllProducts([]); + setHasMore(true); + } setPageState({ currentPage: 1, minPrice: "", diff --git a/src/pages/ProductDetail/index.jsx b/src/pages/ProductDetail/index.jsx index 1ff62cc..1593692 100644 --- a/src/pages/ProductDetail/index.jsx +++ b/src/pages/ProductDetail/index.jsx @@ -367,7 +367,7 @@ const ProductPage = ({ {product.channel?.[0]?.name && ( - + {t("order.channel")} {product.channel[0].name} diff --git a/src/pages/Stores/Stores.module.scss b/src/pages/Stores/Stores.module.scss new file mode 100644 index 0000000..d7567f7 --- /dev/null +++ b/src/pages/Stores/Stores.module.scss @@ -0,0 +1,143 @@ +.storesContainer { + padding: 1rem 4.4rem; + max-width: 1336px; + margin: 0 auto; + @media screen and (max-width: 1023px) { + padding: 1rem 1.25rem; + } +} + +.searchWrapper { + margin-bottom: 24px; + display: flex; + align-items: center; + height: 40px; + svg { + position: absolute; + width: 24px; + height: 24px; + transform: translateX(35%); + color: #9ca3af; + } + + input { + width: 46%; + height: 38px; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 14px; + padding-left: 40px; + outline: none; + @media screen and (max-width: 1023px) { + width: 100%; + } + + &::placeholder { + color: #9ca3af; + } + } +} + +.categorySection { + margin-bottom: 40px; + + h2 { + font-size: 24px; + font-weight: 600; + margin-bottom: 20px; + color: #111827; + cursor: pointer; + &:hover { + color: #aaaaaa; + } + } +} + +.storesGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 180px)); + gap: 16px; + @media screen and (max-width: 1023px) { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 10px; + } + @media screen and (max-width: 900px) { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 5px; + } + @media screen and (max-width: 798px) { + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 5px; + } +} + +.storeCard { + background: white; + border-radius: 8px; + padding: 16px; + text-align: center; + transition: all 0.2s ease; + border: 1px solid #e5e7eb; + cursor: pointer; + @media screen and (max-width: 900px) { + padding: 5px; + } + + &:hover { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + } + + .imageWrapper { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + position: relative; + aspect-ratio: 4/3; + + img { + height: 100%; + object-fit:contain; + } + } + + .placeholder { + width: 80px; + height: 80px; + background: #f3f4f6; + border-radius: 8px; + } + + .storeName { + font-size: 16px; + color: #374151; + margin: 0; + } +} + +.skeleton { + background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + + +.logoFallback { + display: flex; + justify-content: center; + align-items: center; + width: 120px; + height: 100%; + border-radius: 4px; + object-fit: contain; +} diff --git a/src/pages/Stores/index.jsx b/src/pages/Stores/index.jsx new file mode 100644 index 0000000..31f79ab --- /dev/null +++ b/src/pages/Stores/index.jsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { useLazyGetChannelsQuery } from "../../app/api/channelsApi"; +import styles from "./Stores.module.scss"; +import { CiSearch } from "react-icons/ci"; +import { useNavigate } from "react-router-dom"; +import { Logo } from "../../components/Icons"; +import Loader from "../../components/Loader/index"; +import { Result, Button } from "antd"; + +const StoresPage = () => { + const { t } = useTranslation(); + const [searchTerm, setSearchTerm] = useState(""); + const navigate = useNavigate(); + + // Stores data state + const [allStores, setAllStores] = useState([]); + const [visibleStores, setVisibleStores] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const itemsPerPage = 24; + + // Use lazy query to have more control over when to fetch + const [getChannels, { data: channelsData, isLoading, isFetching, error }] = + useLazyGetChannelsQuery(); + + // Initial fetch on component mount + useEffect(() => { + getChannels({ page: 1, limit: itemsPerPage }); + }, [getChannels]); + + // Process stores data when it arrives + useEffect(() => { + if (channelsData) { + const stores = channelsData.data || []; + const pagination = channelsData.pagination || {}; + + console.log("Stores Data Received:", { + count: stores.length, + page, + pagination + }); + + setAllStores((prev) => { + const existingIds = new Set(prev.map((store) => store.id)); + const newStores = stores.filter( + (store) => !existingIds.has(store.id) + ); + return [...prev, ...newStores]; + }); + + // More robust hasMore logic + const hasNext = pagination.next_page_url || + (pagination.current_page && pagination.last_page && pagination.current_page < pagination.last_page) || + (stores.length === itemsPerPage); + + setHasMore(!!hasNext); + } + }, [channelsData, page, itemsPerPage]); + + // Process stores for display whenever all stores or search term changes + useEffect(() => { + if (allStores.length > 0) { + const filteredStores = searchTerm + ? allStores.filter((store) => + store.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + : allStores; + + // Grouping logic (similar to Brands, but defaults to "Stores") + const groupedStores = filteredStores.reduce((acc, store) => { + const type = store.type || "Stores"; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(store); + return acc; + }, {}); + + const displayGroups = Object.entries(groupedStores) + .map(([type, stores]) => ({ + title: type === "Stores" ? t("navbar.stores") : type.charAt(0).toUpperCase() + type.slice(1), + stores, + })) + .filter((group) => group.stores.length > 0); + + setVisibleStores(displayGroups); + if (searchTerm) { + setHasMore(false); + } + } + }, [allStores, searchTerm, t]); + + const loadMoreStores = () => { + if (!searchTerm && !isFetching && hasMore) { + const nextPage = page + 1; + getChannels({ page: nextPage, limit: itemsPerPage }); + setPage(nextPage); + } + }; + + const handleSearch = (e) => { + const value = e.target.value; + setSearchTerm(value); + setPage(1); + setAllStores([]); + setHasMore(true); + getChannels({ page: 1, limit: itemsPerPage, search: value }); + }; + + const handleStoreClick = (storeId) => { + navigate(`/channel/${storeId}`); + }; + + if (isLoading && page === 1) return ; + if (error) + return ( + navigate("/")}> + {t("common.back_to_home") || "Baş sahypa gidiň"} + + } + /> + ); + + return ( +
    +
    + + +
    + +
    } + > + {visibleStores.map((group, index) => ( +
    +

    {group.title}

    + +
    + {group.stores.map((store) => ( +
    handleStoreClick(store.id)} + > +
    + {store.media?.[0]?.thumbnail || + store.media?.[0]?.images_800x800 || + store.logo ? ( + {store.name} { + e.target.style.display = "none"; + e.target.nextSibling.style.display = "flex"; + }} + /> + ) : ( +
    + +
    + )} +
    + +
    +
    +
    +

    {store.name}

    +
    + ))} +
    +
    + ))} + +
    + ); +}; + +export default StoresPage; diff --git a/src/routes.jsx b/src/routes.jsx index 3fa0307..bf09bf6 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -21,6 +21,7 @@ const DeliveryTerms = lazy(() => import("./pages/DeliveryTerms/index.jsx")); const AboutUs = lazy(() => import("./pages/AboutUs/index.jsx")); const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy/index.jsx")); const AdminPage = lazy(() => import("./pages/CarconfiguratorAdmin/index.jsx")); +const StoresPage = lazy(() => import("./pages/Stores/index.jsx")); export default function Router() { const routes = useRoutes([ @@ -34,6 +35,7 @@ export default function Router() { children: [ { path: "/", element: }, { path: "/brands", element: }, + { path: "/stores", element: }, { path: "/brands/:brandId", element: }, { path: "/cart", element: }, { path: "/wishlist", element: },