added store to navbar

This commit is contained in:
Jelaletdin12
2026-04-30 23:17:44 +05:00
parent 6cdde96c61
commit 684ab6917d
11 changed files with 408 additions and 10 deletions

View File

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

View File

@@ -207,7 +207,22 @@ export const OrderIcon = () => (
></path>
</svg>
);
export const StoreIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="#4b5563"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
width={16}
height={16}
>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
);
export const CategoryIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { CartIcon, WishlistIcon, BrandIcon, OrderIcon } from "../Icons";
import { CartIcon, WishlistIcon, BrandIcon, OrderIcon, StoreIcon } from "../Icons";
import styles from "./Navbar.module.scss";
import { UserOutlined, LogoutOutlined, HomeOutlined } from "@ant-design/icons";
import { UserOutlined, LogoutOutlined, HomeOutlined, ShopOutlined } from "@ant-design/icons";
import { FaGlobe } from "react-icons/fa6";
import { Input, Badge, Menu, Dropdown } from "antd";
const { Search } = Input;
@@ -151,6 +151,15 @@ const NavbarDown = () => {
</button>
</Link>
</li>
<div className={styles.stick}></div>
<li>
<Link to={"/stores"}>
<button className={styles.navButton}>
<ShopOutlined />
{t("navbar.stores")}
</button>
</Link>
</li>
<li className={styles.searchWrapper}>
<CiSearch />
<input
@@ -255,7 +264,10 @@ const NavbarDown = () => {
</div>
<div className={styles.stick}></div>
<div className={styles.location}>
<CiLocationOn /> Aşgabat
<Link to={'/stores'} style={{textDecoration: 'none'}}><button className={styles.navButton}>
<ShopOutlined />
{t("navbar.stores")}
</button></Link>
</div>
<div className={styles.stick}></div>
<div className={styles.searchIcon} onClick={toggleSearch}>

View File

@@ -5,6 +5,7 @@ export default {
login: "Login",
signUp: "Sign Up",
brands: "Brands",
stores: "Stores",
search: "Search by product name...",
cart: "Cart",
home: "Home",

View File

@@ -5,6 +5,7 @@ export default {
login: "Войти",
signUp: "Регистрация",
brands: "Бренды",
stores: "Магазины",
search: "Поиск по названию товара...",
cart: "Корзина",
home: "Главная",

View File

@@ -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",

View File

@@ -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 {
if (prevRouteRef.current !== routeKey) {
setAllProducts([]);
setHasMore(true);
}
setPageState({
currentPage: 1,
minPrice: "",

View File

@@ -367,7 +367,7 @@ const ProductPage = ({
{product.channel?.[0]?.name && (
<Link to={`/channel/${product.channel[0].id}`} state={{ clearFilters: true }} className={styles.metaItem}>
<Link to={`/channel/${product.channel[0].id}`} target="_blank" state={{ clearFilters: true }} className={styles.metaItem}>
<span className={styles.metaLabel}>{t("order.channel")}</span>
<span className={styles.metaValue}>
{product.channel[0].name}

View File

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

202
src/pages/Stores/index.jsx Normal file
View File

@@ -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 <Loader />;
if (error)
return (
<Result
status="500"
title="500"
subTitle={t("common.error_occurred") || "Näbelli ýalňyşlyk ýüze çykdy."}
extra={
<Button type="primary" onClick={() => navigate("/")}>
{t("common.back_to_home") || "Baş sahypa gidiň"}
</Button>
}
/>
);
return (
<div className={styles.storesContainer}>
<div className={styles.searchWrapper}>
<CiSearch />
<input
type="text"
placeholder={t("common.search")}
value={searchTerm}
onChange={handleSearch}
/>
</div>
<InfiniteScroll
dataLength={allStores.length}
next={loadMoreStores}
hasMore={hasMore && !searchTerm}
loader={<div style={{ textAlign: 'center', padding: '20px' }}><Loader /></div>}
>
{visibleStores.map((group, index) => (
<section key={index} className={styles.categorySection}>
<h2>{group.title}</h2>
<div className={styles.storesGrid}>
{group.stores.map((store) => (
<div
key={store.id}
className={styles.storeCard}
onClick={() => handleStoreClick(store.id)}
>
<div className={styles.imageWrapper}>
{store.media?.[0]?.thumbnail ||
store.media?.[0]?.images_800x800 ||
store.logo ? (
<img
src={
store.media?.[0]?.thumbnail ||
store.media?.[0]?.images_800x800 ||
store.logo
}
alt={store.name}
width={120}
height={120}
onError={(e) => {
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
) : (
<div className={styles.logoFallback}>
<Logo width={60} height={60} />
</div>
)}
<div
className={styles.logoFallback}
style={{ display: "none" }}
>
<Logo width={60} height={60} />
</div>
</div>
<div className={styles.divider}></div>
<h3 className={styles.storeName}>{store.name}</h3>
</div>
))}
</div>
</section>
))}
</InfiniteScroll>
</div>
);
};
export default StoresPage;

View File

@@ -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: <Home /> },
{ path: "/brands", element: <BrandsPage /> },
{ path: "/stores", element: <StoresPage /> },
{ path: "/brands/:brandId", element: <Category /> },
{ path: "/cart", element: <CartPage /> },
{ path: "/wishlist", element: <WishList /> },