Compare commits
3 Commits
9419ec0af0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f3079801b | ||
|
|
684ab6917d | ||
|
|
6cdde96c61 |
@@ -7,7 +7,7 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params.type) queryParams.append("type", params.type);
|
if (params.type) queryParams.append("type", params.type);
|
||||||
if (params.page) queryParams.append("page", params.page);
|
if (params.page) queryParams.append("page", params.page);
|
||||||
if (params.limit) queryParams.append("limit", params.limit);
|
if (params.perPage) queryParams.append("perPage", params.perPage);
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
return `/brands${queryString ? `?${queryString}` : ""}`;
|
return `/brands${queryString ? `?${queryString}` : ""}`;
|
||||||
},
|
},
|
||||||
@@ -25,10 +25,10 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
return `/brands/${params}/products`;
|
return `/brands/${params}/products`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, page = 1, limit = 24, sorting, min_price, max_price } = params;
|
const { id, page = 1, perPage = 12, sorting, min_price, max_price } = params;
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
urlParams.append("page", page);
|
urlParams.append("page", page);
|
||||||
urlParams.append("limit", limit);
|
urlParams.append("perPage", perPage);
|
||||||
if (sorting) urlParams.append("sorting", sorting);
|
if (sorting) urlParams.append("sorting", sorting);
|
||||||
if (min_price) urlParams.append("min_price", min_price);
|
if (min_price) urlParams.append("min_price", min_price);
|
||||||
if (max_price) urlParams.append("max_price", max_price);
|
if (max_price) urlParams.append("max_price", max_price);
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
getCategoryProducts: builder.query({
|
getCategoryProducts: builder.query({
|
||||||
query: ({ categoryId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
|
query: ({ categoryId, page = 1, perPage = 12, brands, min_price, max_price, sorting }) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", page);
|
params.append("page", page);
|
||||||
params.append("limit", limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append("brands", brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append("min_price", min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append("max_price", max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
@@ -41,7 +41,7 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
|
|
||||||
getAllCategoryProductsPaginated: builder.query({
|
getAllCategoryProductsPaginated: builder.query({
|
||||||
async queryFn(
|
async queryFn(
|
||||||
{ category, page = 1, limit = 24, brands, min_price, max_price, sorting },
|
{ category, page = 1, perPage = 12, brands, min_price, max_price, sorting },
|
||||||
_queryApi,
|
_queryApi,
|
||||||
_extraOptions,
|
_extraOptions,
|
||||||
baseQuery
|
baseQuery
|
||||||
@@ -58,7 +58,7 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
if (categoryIds.length === 1) {
|
if (categoryIds.length === 1) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", page);
|
params.append("page", page);
|
||||||
params.append("limit", limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append("brands", brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append("min_price", min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append("max_price", max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
@@ -86,7 +86,7 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
const requests = categoryIds.map((categoryId) => {
|
const requests = categoryIds.map((categoryId) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", page);
|
params.append("page", page);
|
||||||
params.append("limit", limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append("brands", brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append("min_price", min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append("max_price", max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
|||||||
53
src/app/api/channelsApi.js
Normal file
53
src/app/api/channelsApi.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { baseApi } from "./baseApi";
|
||||||
|
|
||||||
|
export const channelsApi = baseApi.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getChannelProducts: builder.query({
|
||||||
|
query: (params) => {
|
||||||
|
// params: { channelId, page, limit, min_price, max_price, sorting }
|
||||||
|
const {
|
||||||
|
channelId,
|
||||||
|
page = 1,
|
||||||
|
perPage = 24,
|
||||||
|
min_price,
|
||||||
|
max_price,
|
||||||
|
sorting,
|
||||||
|
} = params;
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.append("page", page);
|
||||||
|
urlParams.append("perPage", perPage);
|
||||||
|
if (min_price) urlParams.append("min_price", min_price);
|
||||||
|
if (max_price) urlParams.append("max_price", max_price);
|
||||||
|
if (sorting) urlParams.append("sorting", sorting);
|
||||||
|
return `/channels/${channelId}/products?${urlParams.toString()}`;
|
||||||
|
},
|
||||||
|
transformResponse: (response) => ({
|
||||||
|
data: response.data || response,
|
||||||
|
pagination: response.pagination || {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
getChannels: builder.query({
|
||||||
|
query: (params = {}) => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (params.page) queryParams.append("page", params.page);
|
||||||
|
if (params.perPage) queryParams.append("perPage", params.perPage);
|
||||||
|
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,
|
||||||
|
useGetChannelsQuery,
|
||||||
|
useLazyGetChannelsQuery,
|
||||||
|
} = channelsApi;
|
||||||
@@ -19,17 +19,17 @@ export const collectionsApi = baseApi.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
checkCollectionHasProducts: builder.query({
|
checkCollectionHasProducts: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}/products?limit=1`,
|
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
hasProducts: response.data && response.data.length > 0,
|
hasProducts: response.data && response.data.length > 0,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCollectionProductsPaginated: builder.query({
|
getCollectionProductsPaginated: builder.query({
|
||||||
query: ({ collectionId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
|
query: ({ collectionId, page = 1, perPage = 24, brands, min_price, max_price, sorting }) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", page);
|
params.append("page", page);
|
||||||
params.append("limit", limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append("brands", brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append("min_price", min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append("max_price", max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (params?.brand_id) {
|
if (params?.brand_id) {
|
||||||
queryParams.append("brand_id", String(params.brand_id))
|
queryParams.append("brand_id", String(params.brand_id))
|
||||||
}
|
}
|
||||||
|
if (params?.channel_id) {
|
||||||
|
queryParams.append("channel_id", String(params.channel_id))
|
||||||
|
}
|
||||||
|
|
||||||
return `/filters?${queryParams.toString()}`
|
return `/filters?${queryParams.toString()}`
|
||||||
},
|
},
|
||||||
@@ -22,6 +25,7 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
return {
|
return {
|
||||||
categories: response.data?.categories || [],
|
categories: response.data?.categories || [],
|
||||||
brands: response.data?.brands || [],
|
brands: response.data?.brands || [],
|
||||||
|
channels: response.data?.channels || [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keepUnusedDataFor: 300,
|
keepUnusedDataFor: 300,
|
||||||
@@ -40,7 +44,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (queryArgs.brand_id) {
|
if (queryArgs.brand_id) {
|
||||||
parts.push(`brd:${queryArgs.brand_id}`);
|
parts.push(`brd:${queryArgs.brand_id}`);
|
||||||
}
|
}
|
||||||
|
if (queryArgs.channel_id) {
|
||||||
|
parts.push(`chn:${queryArgs.channel_id}`);
|
||||||
|
}
|
||||||
return parts.length > 0 ? parts.join('|') : 'no-params';
|
return parts.length > 0 ? parts.join('|') : 'no-params';
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -66,7 +72,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (arg.brand_id) {
|
if (arg.brand_id) {
|
||||||
tags.push({ type: "Filters", id: `brd-${arg.brand_id}` });
|
tags.push({ type: "Filters", id: `brd-${arg.brand_id}` });
|
||||||
}
|
}
|
||||||
|
if (arg.channel_id) {
|
||||||
|
tags.push({ type: "Filters", id: `chn-${arg.channel_id}` });
|
||||||
|
}
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -87,24 +87,31 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 1336px;
|
width: 1336px;
|
||||||
max-height: 520px;
|
max-height: 520px;
|
||||||
// max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- LEFT LIST ----
|
// ---- LEFT LIST ----
|
||||||
.categoriesList {
|
.categoriesList {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
border-right: 1px solid #e5e7eb;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
max-height: 520px;
|
max-height: 520px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: #d1d5db;
|
||||||
border-radius: 2px;
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +126,13 @@
|
|||||||
transition: background-color 0.12s, color 0.12s;
|
transition: background-color 0.12s, color 0.12s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
background-color: #f3f4f6;
|
||||||
color: #000;
|
color: #e63946;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: #f3f4f6;
|
||||||
color: #000;
|
color: #e63946;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -138,7 +145,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
color: rgba(255, 255, 255, 0.4);
|
color: #9ca3af;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,11 +158,18 @@
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
border-radius: 2px;
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,11 +174,11 @@ const Checkout = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className={styles.optionTitle}>{payment.name}</span>
|
<span className={styles.optionTitle}>{payment.name}</span>
|
||||||
<span className={styles.optionDesc}>
|
{/* <span className={styles.optionDesc}>
|
||||||
{payment.name === "Nagt"
|
{payment.name === "Nagt"
|
||||||
? t("checkout.payment_in_cash_upon_delivery_of_the_order")
|
? t("checkout.payment_in_cash_upon_delivery_of_the_order")
|
||||||
: t("checkout.payment_by_card")}
|
: t("checkout.payment_by_card")}
|
||||||
</span>
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ const Footer = () => {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<img src={apk} alt="Download APK" className={styles.appLogo} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,23 +7,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandsScroll {
|
.brandsSwiper {
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
/* Hide scrollbar for Webkit */
|
|
||||||
&::-webkit-scrollbar {
|
.brandSlide {
|
||||||
display: none;
|
width: auto;
|
||||||
}
|
|
||||||
/* Hide scrollbar for Firefox, IE, Edge */
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandCard {
|
.brandCard {
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 122px;
|
width: 122px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -56,37 +48,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.allButton {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 122px;
|
|
||||||
height: 67.6px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
gap: 4px;
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
color: #aaaaaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.brandCard, .allButton {
|
.brandCard {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
// height: 79.2px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,67 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useGetBrandsQuery } from '../../app/api/brandsApi';
|
import { useGetBrandsQuery } from '../../app/api/brandsApi';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Autoplay } from 'swiper/modules';
|
||||||
|
import 'swiper/css';
|
||||||
import styles from './HomeBrands.module.scss';
|
import styles from './HomeBrands.module.scss';
|
||||||
import { Logo } from '../Icons';
|
import { Logo } from '../Icons';
|
||||||
import { IoIosArrowForward } from 'react-icons/io';
|
|
||||||
|
|
||||||
const HomeBrands = () => {
|
const HomeBrands = () => {
|
||||||
const { t, i18n } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// We fetch a larger amount so we have enough to shuffle.
|
// Fetch brands.
|
||||||
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 50 });
|
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 100 });
|
||||||
|
|
||||||
const randomBrands = useMemo(() => {
|
|
||||||
if (!brandsData) return [];
|
|
||||||
// Create a shallow copy and shuffle it
|
|
||||||
const shuffled = [...brandsData].sort(() => 0.5 - Math.random());
|
|
||||||
// Pick the first 9 brands
|
|
||||||
return shuffled.slice(0, 8);
|
|
||||||
}, [brandsData]);
|
|
||||||
|
|
||||||
if (isLoading || !brandsData || brandsData.length === 0) return null;
|
if (isLoading || !brandsData || brandsData.length === 0) return null;
|
||||||
|
|
||||||
// "Еще" in ru, "Hemmesi" in tm, "More" in en
|
|
||||||
const getMoreText = () => {
|
|
||||||
const lang = i18n.language;
|
|
||||||
if (lang === 'ru') return 'Еще';
|
|
||||||
if (lang === 'en') return 'More';
|
|
||||||
return 'Hemmesi';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.brandsScroll}>
|
<Swiper
|
||||||
{randomBrands.map((brand) => (
|
modules={[Autoplay]}
|
||||||
<div
|
spaceBetween={12}
|
||||||
key={brand.id}
|
slidesPerView={'auto'}
|
||||||
className={styles.brandCard}
|
slidesPerGroup={2}
|
||||||
onClick={() => navigate(`/brands/${brand.id}`)}
|
loop={true}
|
||||||
>
|
autoplay={{
|
||||||
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
|
delay: 3000,
|
||||||
<img
|
disableOnInteraction: false,
|
||||||
src={
|
}}
|
||||||
brand.media?.[0]?.thumbnail ||
|
className={styles.brandsSwiper}
|
||||||
brand.media?.[0]?.images_800x800 ||
|
>
|
||||||
brand.logo
|
{brandsData.map((brand) => (
|
||||||
}
|
<SwiperSlide key={brand.id} className={styles.brandSlide}>
|
||||||
alt={brand.name}
|
<div
|
||||||
onError={(e) => {
|
className={styles.brandCard}
|
||||||
e.target.style.display = "none";
|
onClick={() => navigate(`/brands/${brand.id}`)}
|
||||||
e.target.nextSibling.style.display = "flex";
|
>
|
||||||
}}
|
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
|
||||||
/>
|
<img
|
||||||
) : (
|
src={
|
||||||
<div className={styles.logoFallback}>
|
brand.media?.[0]?.thumbnail ||
|
||||||
|
brand.media?.[0]?.images_800x800 ||
|
||||||
|
brand.logo
|
||||||
|
}
|
||||||
|
alt={brand.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = "none";
|
||||||
|
e.target.nextSibling.style.display = "flex";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.logoFallback}>
|
||||||
|
<Logo width={40} height={40} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.logoFallback} style={{ display: "none" }}>
|
||||||
<Logo width={40} height={40} />
|
<Logo width={40} height={40} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className={styles.logoFallback} style={{ display: "none" }}>
|
|
||||||
<Logo width={40} height={40} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
<div
|
</Swiper>
|
||||||
className={styles.allButton}
|
|
||||||
onClick={() => navigate('/brands')}
|
|
||||||
>
|
|
||||||
<span>{getMoreText()}</span>
|
|
||||||
<IoIosArrowForward />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HomeBrands;
|
export default HomeBrands;
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,22 @@ export const OrderIcon = () => (
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</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 = () => (
|
export const CategoryIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
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 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 { FaGlobe } from "react-icons/fa6";
|
||||||
import { Input, Badge, Menu, Dropdown } from "antd";
|
import { Input, Badge, Menu, Dropdown } from "antd";
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -151,6 +151,15 @@ const NavbarDown = () => {
|
|||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</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}>
|
<li className={styles.searchWrapper}>
|
||||||
<CiSearch />
|
<CiSearch />
|
||||||
<input
|
<input
|
||||||
@@ -255,7 +264,10 @@ const NavbarDown = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.stick}></div>
|
<div className={styles.stick}></div>
|
||||||
<div className={styles.location}>
|
<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>
|
||||||
<div className={styles.stick}></div>
|
<div className={styles.stick}></div>
|
||||||
<div className={styles.searchIcon} onClick={toggleSearch}>
|
<div className={styles.searchIcon} onClick={toggleSearch}>
|
||||||
|
|||||||
@@ -72,8 +72,16 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 2.4em;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 426px) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 2.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +90,15 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 2.8em;
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
height: 2.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +106,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: auto;
|
||||||
margin: 0;
|
margin-bottom: 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const ProductCard = ({
|
|||||||
onAddToCart,
|
onAddToCart,
|
||||||
onToggleFavorite,
|
onToggleFavorite,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
descriptionMaxLength = 85,
|
descriptionMaxLength = 120,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
login: "Login",
|
login: "Login",
|
||||||
signUp: "Sign Up",
|
signUp: "Sign Up",
|
||||||
brands: "Brands",
|
brands: "Brands",
|
||||||
|
stores: "Stores",
|
||||||
search: "Search by product name...",
|
search: "Search by product name...",
|
||||||
cart: "Cart",
|
cart: "Cart",
|
||||||
home: "Home",
|
home: "Home",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
login: "Войти",
|
login: "Войти",
|
||||||
signUp: "Регистрация",
|
signUp: "Регистрация",
|
||||||
brands: "Бренды",
|
brands: "Бренды",
|
||||||
|
stores: "Магазины",
|
||||||
search: "Поиск по названию товара...",
|
search: "Поиск по названию товара...",
|
||||||
cart: "Корзина",
|
cart: "Корзина",
|
||||||
home: "Главная",
|
home: "Главная",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
login: "Giriş",
|
login: "Giriş",
|
||||||
signUp: "Agza bolmak",
|
signUp: "Agza bolmak",
|
||||||
brands: "Brendler",
|
brands: "Brendler",
|
||||||
|
stores: "Dükanlar",
|
||||||
search: "Haryt ady boýunça gözleg...",
|
search: "Haryt ady boýunça gözleg...",
|
||||||
cart: "Sebet",
|
cart: "Sebet",
|
||||||
home: "Baş sahypa",
|
home: "Baş sahypa",
|
||||||
|
|||||||
@@ -499,3 +499,45 @@ width: 85%;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.channelLogo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelInfo {
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.channelLogo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelInfo h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import {
|
|||||||
useGetFiltersQuery,
|
useGetFiltersQuery,
|
||||||
useLazyGetFiltersQuery,
|
useLazyGetFiltersQuery,
|
||||||
} from "../../../app/api/filtersApi";
|
} from "../../../app/api/filtersApi";
|
||||||
|
import { useGetChannelsQuery } from "../../../app/api/channelsApi";
|
||||||
|
|
||||||
const useCategoryData = ({
|
const useCategoryData = ({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedFilterCategory,
|
selectedFilterCategory,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -23,8 +25,9 @@ const useCategoryData = ({
|
|||||||
if (categoryId) return { category_id: categoryId };
|
if (categoryId) return { category_id: categoryId };
|
||||||
if (collectionId) return { collection_id: collectionId };
|
if (collectionId) return { collection_id: collectionId };
|
||||||
if (brandId) return { brand_id: brandId };
|
if (brandId) return { brand_id: brandId };
|
||||||
|
if (channelId) return { channel_id: channelId };
|
||||||
return null;
|
return null;
|
||||||
}, [categoryId, collectionId, brandId, selectedFilterCategory, searchQuery]);
|
}, [categoryId, collectionId, brandId, channelId, selectedFilterCategory, searchQuery]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: filtersData,
|
data: filtersData,
|
||||||
@@ -44,6 +47,19 @@ const useCategoryData = ({
|
|||||||
skip: !collectionId,
|
skip: !collectionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: channelsListData,
|
||||||
|
isLoading: channelsLoading,
|
||||||
|
error: channelsError,
|
||||||
|
} = useGetChannelsQuery({ perPage: 100 }, {
|
||||||
|
skip: !channelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelData = useMemo(() => {
|
||||||
|
if (!channelId || !channelsListData?.data) return null;
|
||||||
|
return channelsListData.data.find(c => String(c.id) === String(channelId));
|
||||||
|
}, [channelId, channelsListData]);
|
||||||
|
|
||||||
const isSubCategory = useMemo(() => {
|
const isSubCategory = useMemo(() => {
|
||||||
if (!categoriesData?.data || !categoryId) return false;
|
if (!categoriesData?.data || !categoryId) return false;
|
||||||
|
|
||||||
@@ -92,8 +108,8 @@ const useCategoryData = ({
|
|||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
}, [categoryId, categoriesData]);
|
}, [categoryId, categoriesData]);
|
||||||
|
|
||||||
const isLoading = filtersLoading || collectionLoading;
|
const isLoading = filtersLoading || collectionLoading || channelsLoading;
|
||||||
const hasError = filtersError || collectionError;
|
const hasError = filtersError || collectionError || channelsError;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoriesData,
|
categoriesData,
|
||||||
@@ -101,6 +117,7 @@ const useCategoryData = ({
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
filtersData: activeFilters,
|
filtersData: activeFilters,
|
||||||
collectionData,
|
collectionData,
|
||||||
|
channelData,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasError,
|
hasError,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
useLazyGetAllCategoryProductsPaginatedQuery,
|
useLazyGetAllCategoryProductsPaginatedQuery,
|
||||||
useGetCategoryProductsQuery,
|
useGetCategoryProductsQuery,
|
||||||
} from "../../../app/api/categories";
|
} from "../../../app/api/categories";
|
||||||
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
|
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
|
||||||
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
|
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
|
||||||
|
import { useLazyGetChannelProductsQuery } from "../../../app/api/channelsApi"; // EKLE
|
||||||
|
|
||||||
const useCategoryProducts = ({
|
const useCategoryProducts = ({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -27,8 +29,6 @@ const useCategoryProducts = ({
|
|||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
const activeRequestId = useRef(0);
|
const activeRequestId = useRef(0);
|
||||||
// Tüm parametreleri ref'te tut — stale closure'ı tamamen engelle
|
|
||||||
const paramsRef = useRef({});
|
|
||||||
|
|
||||||
const shouldUseBaseQuery =
|
const shouldUseBaseQuery =
|
||||||
categoryId &&
|
categoryId &&
|
||||||
@@ -36,7 +36,8 @@ const useCategoryProducts = ({
|
|||||||
!searchQuery &&
|
!searchQuery &&
|
||||||
!selectedFilterCategory &&
|
!selectedFilterCategory &&
|
||||||
!brandId &&
|
!brandId &&
|
||||||
!collectionId;
|
!collectionId &&
|
||||||
|
!channelId;
|
||||||
|
|
||||||
const { data: baseQueryData, isFetching: baseQueryFetching } =
|
const { data: baseQueryData, isFetching: baseQueryFetching } =
|
||||||
useGetCategoryProductsQuery(
|
useGetCategoryProductsQuery(
|
||||||
@@ -54,8 +55,17 @@ const useCategoryProducts = ({
|
|||||||
const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
|
const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
|
||||||
const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
|
const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
|
||||||
const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
|
const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
|
||||||
|
const [fetchChannelPaginated] = useLazyGetChannelProductsQuery();
|
||||||
|
|
||||||
|
// ✅ Ref'e al — dependency array'den çıkar, stale closure yok
|
||||||
|
const fetchersRef = useRef({});
|
||||||
|
fetchersRef.current = {
|
||||||
|
fetchCategoryPaginated,
|
||||||
|
fetchBrandPaginated,
|
||||||
|
fetchCollectionPaginated,
|
||||||
|
fetchChannelPaginated,
|
||||||
|
};
|
||||||
|
|
||||||
// Base query handler
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldUseBaseQuery || !baseQueryData) return;
|
if (!shouldUseBaseQuery || !baseQueryData) return;
|
||||||
const data = baseQueryData.data || [];
|
const data = baseQueryData.data || [];
|
||||||
@@ -69,11 +79,23 @@ const useCategoryProducts = ({
|
|||||||
setHasMore(hasNextPage);
|
setHasMore(hasNextPage);
|
||||||
}, [baseQueryData, currentPage, shouldUseBaseQuery]);
|
}, [baseQueryData, currentPage, shouldUseBaseQuery]);
|
||||||
|
|
||||||
// Her fetch çağrısını doğrudan effect içinde yap — useCallback kaldırıldı
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldUseBaseQuery || searchQuery) return;
|
if (shouldUseBaseQuery || searchQuery) return;
|
||||||
|
|
||||||
// Parametreleri snapshot al
|
|
||||||
|
console.log("🔥 LAZY EFFECT TRIGGERED", {
|
||||||
|
shouldUseBaseQuery,
|
||||||
|
categoryId,
|
||||||
|
collectionId,
|
||||||
|
brandId,
|
||||||
|
channelId,
|
||||||
|
isSubCategory,
|
||||||
|
selectedFilterCategory,
|
||||||
|
selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
currentPage,
|
currentPage,
|
||||||
selectedFilterCategory,
|
selectedFilterCategory,
|
||||||
@@ -81,6 +103,7 @@ const useCategoryProducts = ({
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
brandId,
|
brandId,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
channelId,
|
||||||
selectedFilterBrand,
|
selectedFilterBrand,
|
||||||
minPrice,
|
minPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
@@ -92,9 +115,16 @@ const useCategoryProducts = ({
|
|||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
try {
|
try {
|
||||||
|
const {
|
||||||
|
fetchCategoryPaginated,
|
||||||
|
fetchBrandPaginated,
|
||||||
|
fetchCollectionPaginated,
|
||||||
|
fetchChannelPaginated,
|
||||||
|
} = fetchersRef.current; // ✅ ref'ten oku
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
page: snapshot.currentPage,
|
page: snapshot.currentPage,
|
||||||
limit: 24,
|
perPage: 12,
|
||||||
brands: snapshot.selectedFilterBrand || undefined,
|
brands: snapshot.selectedFilterBrand || undefined,
|
||||||
min_price: snapshot.minPrice || undefined,
|
min_price: snapshot.minPrice || undefined,
|
||||||
max_price: snapshot.maxPrice || undefined,
|
max_price: snapshot.maxPrice || undefined,
|
||||||
@@ -123,6 +153,11 @@ const useCategoryProducts = ({
|
|||||||
collectionId: snapshot.collectionId,
|
collectionId: snapshot.collectionId,
|
||||||
...params,
|
...params,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
} else if (snapshot.channelId) {
|
||||||
|
result = await fetchChannelPaginated({
|
||||||
|
channelId: snapshot.channelId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId !== activeRequestId.current) return;
|
if (requestId !== activeRequestId.current) return;
|
||||||
@@ -159,7 +194,6 @@ const useCategoryProducts = ({
|
|||||||
|
|
||||||
run();
|
run();
|
||||||
}, [
|
}, [
|
||||||
// useCallback YOK — her dependency değişince effect direkt çalışır
|
|
||||||
shouldUseBaseQuery,
|
shouldUseBaseQuery,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -168,13 +202,12 @@ const useCategoryProducts = ({
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
brandId,
|
brandId,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
channelId,
|
||||||
selectedFilterBrand,
|
selectedFilterBrand,
|
||||||
minPrice,
|
minPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
sorting,
|
sorting,
|
||||||
fetchCategoryPaginated,
|
// ✅ fetcher fonksiyonlar dependency'den tamamen çıktı
|
||||||
fetchBrandPaginated,
|
|
||||||
fetchCollectionPaginated,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
|
const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
|
||||||
@@ -188,4 +221,5 @@ const useCategoryProducts = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCategoryProducts;
|
export default useCategoryProducts;
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,19 @@ import MobilePhoneCard from "./components/Mobilephonecard";
|
|||||||
|
|
||||||
const CategoryPage = () => {
|
const CategoryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { categoryId, collectionId, brandId } = useParams();
|
const { categoryId, collectionId, brandId, channelId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const routeKey = useMemo(
|
const routeKey = useMemo(
|
||||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}-${channelId || "x"}`,
|
||||||
[categoryId, collectionId, brandId],
|
[categoryId, collectionId, brandId, channelId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSavedState = (key, defaultVal) => {
|
const getSavedState = (key, defaultVal) => {
|
||||||
|
if (location.state?.clearFilters) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
|
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
|
||||||
if (saved) return JSON.parse(saved);
|
if (saved) return JSON.parse(saved);
|
||||||
@@ -85,6 +88,7 @@ const CategoryPage = () => {
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
filtersData,
|
filtersData,
|
||||||
collectionData,
|
collectionData,
|
||||||
|
channelData,
|
||||||
isLoading: dataLoading,
|
isLoading: dataLoading,
|
||||||
hasError: dataError,
|
hasError: dataError,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
@@ -92,6 +96,7 @@ const CategoryPage = () => {
|
|||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedFilterCategory: filterState.selectedFilterCategory,
|
selectedFilterCategory: filterState.selectedFilterCategory,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
});
|
});
|
||||||
@@ -105,6 +110,7 @@ const CategoryPage = () => {
|
|||||||
} = useCategoryProducts({
|
} = useCategoryProducts({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
channelId,
|
||||||
brandId,
|
brandId,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
@@ -133,14 +139,16 @@ const CategoryPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevRouteRef.current === routeKey) return;
|
if (prevRouteRef.current === routeKey && !location.state?.clearFilters) return;
|
||||||
|
|
||||||
prevRouteRef.current = routeKey;
|
prevRouteRef.current = routeKey;
|
||||||
|
|
||||||
const savedPageState = getSavedStateByKey(routeKey, "pageState");
|
const shouldClear = location.state?.clearFilters;
|
||||||
const savedFilterState = getSavedStateByKey(routeKey, "filterState");
|
|
||||||
const savedProducts = getSavedStateByKey(routeKey, "products");
|
const savedPageState = shouldClear ? null : getSavedStateByKey(routeKey, "pageState");
|
||||||
const savedHasMore = getSavedStateByKey(routeKey, "hasMore");
|
const savedFilterState = shouldClear ? null : getSavedStateByKey(routeKey, "filterState");
|
||||||
|
const savedProducts = shouldClear ? null : getSavedStateByKey(routeKey, "products");
|
||||||
|
const savedHasMore = shouldClear ? null : getSavedStateByKey(routeKey, "hasMore");
|
||||||
|
|
||||||
if (savedPageState && savedFilterState && savedProducts) {
|
if (savedPageState && savedFilterState && savedProducts) {
|
||||||
setPageState(savedPageState);
|
setPageState(savedPageState);
|
||||||
@@ -152,8 +160,10 @@ const CategoryPage = () => {
|
|||||||
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setAllProducts([]);
|
if (prevRouteRef.current !== routeKey) {
|
||||||
setHasMore(true);
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
|
}
|
||||||
setPageState({
|
setPageState({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
minPrice: "",
|
minPrice: "",
|
||||||
@@ -364,18 +374,35 @@ const CategoryPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.categoryPage}>
|
<div className={styles.categoryPage}>
|
||||||
{(categoryId || filterState.selectedFilterCategory) && (
|
{channelId && channelData ? (
|
||||||
<CategoryBreadcrumbs
|
<div className={styles.channelHeader}>
|
||||||
categoriesData={categoriesData}
|
{channelData.media?.[0]?.thumbnail && (
|
||||||
categoryId={filterState.selectedFilterCategory || categoryId}
|
<img
|
||||||
onCategoryClick={handleCategoryClick}
|
src={channelData.media[0].thumbnail}
|
||||||
/>
|
alt={channelData.name}
|
||||||
)}
|
className={styles.channelLogo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={styles.channelInfo}>
|
||||||
|
<h1>{channelData.name}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{(categoryId || filterState.selectedFilterCategory) && (
|
||||||
|
<CategoryBreadcrumbs
|
||||||
|
categoriesData={categoriesData}
|
||||||
|
categoryId={filterState.selectedFilterCategory || categoryId}
|
||||||
|
onCategoryClick={handleCategoryClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<h2>{pageTitle}</h2>
|
<h2>{pageTitle}</h2>
|
||||||
<p className={styles.sum}>
|
<p className={styles.sum}>
|
||||||
{t("category.total")}: {totalItems} {t("category.items")}
|
{t("category.total")}: {totalItems} {t("category.items")}
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.bars}>
|
<div className={styles.bars}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -329,8 +329,8 @@
|
|||||||
to top,
|
to top,
|
||||||
rgba(0, 0, 0, 0.95) 0%,
|
rgba(0, 0, 0, 0.95) 0%,
|
||||||
rgba(0, 0, 0, 0.7) 0%,
|
rgba(0, 0, 0, 0.7) 0%,
|
||||||
rgba(0, 0, 0, 0.3) 70%,
|
rgba(0, 0, 0, 0.3) 35%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 35%
|
||||||
);
|
);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -386,9 +386,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productDescriptionCollapsed {
|
.productDescriptionCollapsed {
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 10;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
import styles from "./ProductPage.module.scss";
|
import styles from "./ProductPage.module.scss";
|
||||||
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||||
import { FaShoppingCart } from "react-icons/fa";
|
import { FaShoppingCart } from "react-icons/fa";
|
||||||
@@ -69,14 +69,78 @@ const ProductPage = ({
|
|||||||
|
|
||||||
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
||||||
const [showReadMore, setShowReadMore] = useState(false);
|
const [showReadMore, setShowReadMore] = useState(false);
|
||||||
|
const [collapsedMaxHeight, setCollapsedMaxHeight] = useState(null);
|
||||||
const descRef = React.useRef(null);
|
const descRef = React.useRef(null);
|
||||||
const productInfoRef = React.useRef(null);
|
const productInfoRef = React.useRef(null);
|
||||||
|
const imageColRef = React.useRef(null);
|
||||||
|
|
||||||
|
// Ürün değişince desc'i kapat
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDescExpanded(false);
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
// Resim kolonu yüksekliği ile desc kolonu yüksekliğini karşılaştır
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!product?.description) return;
|
if (!product?.description) return;
|
||||||
|
|
||||||
const plainText = product.description.replace(/<[^>]*>/g, "").trim();
|
const imageEl = imageColRef.current;
|
||||||
setShowReadMore(plainText.length > 300);
|
const infoEl = productInfoRef.current;
|
||||||
|
if (!imageEl || !infoEl) return;
|
||||||
|
|
||||||
|
const checkHeights = () => {
|
||||||
|
const descEl = descRef.current;
|
||||||
|
if (!descEl) return;
|
||||||
|
|
||||||
|
const descTrueH = descEl.scrollHeight;
|
||||||
|
const descVisibleH = descEl.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
// ── Mobil: tek kolon layout, sabit eşik kullan ──────────────────
|
||||||
|
if (window.innerWidth <= 639) {
|
||||||
|
const MOBILE_THRESHOLD = 220;
|
||||||
|
if (descTrueH > MOBILE_THRESHOLD) {
|
||||||
|
setShowReadMore(true);
|
||||||
|
setCollapsedMaxHeight(MOBILE_THRESHOLD);
|
||||||
|
} else {
|
||||||
|
setShowReadMore(false);
|
||||||
|
setCollapsedMaxHeight(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Desktop/tablet: resim kolonu yüksekliğiyle karşılaştır ──────
|
||||||
|
const imageH = imageEl.getBoundingClientRect().height;
|
||||||
|
if (imageH === 0) return;
|
||||||
|
|
||||||
|
const infoCurrentH = infoEl.getBoundingClientRect().height;
|
||||||
|
// Info kolonunun gerçek (kısıtsız) yüksekliği:
|
||||||
|
const infoTrueH = infoCurrentH + (descTrueH - descVisibleH);
|
||||||
|
|
||||||
|
if (infoTrueH > imageH) {
|
||||||
|
const overflow = infoTrueH - imageH;
|
||||||
|
const newDescMaxH = Math.max(descTrueH - overflow, 60);
|
||||||
|
setShowReadMore(true);
|
||||||
|
setCollapsedMaxHeight(newDescMaxH);
|
||||||
|
} else {
|
||||||
|
setShowReadMore(false);
|
||||||
|
setCollapsedMaxHeight(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// İlk kontrol (DOM yerleştikten sonra)
|
||||||
|
const raf = requestAnimationFrame(checkHeights);
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(checkHeights);
|
||||||
|
ro.observe(imageEl);
|
||||||
|
ro.observe(infoEl);
|
||||||
|
|
||||||
|
// Mobil↔desktop geçişi için window resize de dinlenir
|
||||||
|
window.addEventListener("resize", checkHeights);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
ro.disconnect();
|
||||||
|
window.removeEventListener("resize", checkHeights);
|
||||||
|
};
|
||||||
}, [product?.description]);
|
}, [product?.description]);
|
||||||
|
|
||||||
const { getCartItem } = useCart();
|
const { getCartItem } = useCart();
|
||||||
@@ -325,7 +389,7 @@ const ProductPage = ({
|
|||||||
{/* ── 3 kolon ana section ── */}
|
{/* ── 3 kolon ana section ── */}
|
||||||
<div className={styles.productSection}>
|
<div className={styles.productSection}>
|
||||||
{/* KOLON 1: Resim */}
|
{/* KOLON 1: Resim */}
|
||||||
<div className={styles.productImage}>
|
<div className={styles.productImage} ref={imageColRef}>
|
||||||
<ImageCarousel
|
<ImageCarousel
|
||||||
images={product.media}
|
images={product.media}
|
||||||
altText={product.name}
|
altText={product.name}
|
||||||
@@ -366,12 +430,14 @@ const ProductPage = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{product.channel?.[0]?.name && (
|
{product.channel?.[0]?.name && (
|
||||||
<div 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.metaLabel}>{t("order.channel")}</span>
|
||||||
<span className={styles.metaValue}>
|
<span className={styles.metaValue}>
|
||||||
{product.channel[0].name}
|
{product.channel[0].name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{product.properties?.length > 0 && (
|
{product.properties?.length > 0 && (
|
||||||
@@ -408,6 +474,11 @@ const ProductPage = ({
|
|||||||
className={`${styles.productDescription} ${
|
className={`${styles.productDescription} ${
|
||||||
!isDescExpanded && showReadMore ? styles.productDescriptionCollapsed : ""
|
!isDescExpanded && showReadMore ? styles.productDescriptionCollapsed : ""
|
||||||
}`}
|
}`}
|
||||||
|
style={
|
||||||
|
!isDescExpanded && showReadMore && collapsedMaxHeight
|
||||||
|
? { maxHeight: `${collapsedMaxHeight}px` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||||
/>
|
/>
|
||||||
{showReadMore && !isDescExpanded && (
|
{showReadMore && !isDescExpanded && (
|
||||||
@@ -440,7 +511,7 @@ const ProductPage = ({
|
|||||||
<div className={styles.priceRight}>
|
<div className={styles.priceRight}>
|
||||||
{isPriceZero(product.price_amount) ? (
|
{isPriceZero(product.price_amount) ? (
|
||||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
<PendingPriceBadge />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
143
src/pages/Stores/Stores.module.scss
Normal file
143
src/pages/Stores/Stores.module.scss
Normal 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
202
src/pages/Stores/index.jsx
Normal 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, perPage: 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 && allStores.length > 0) {
|
||||||
|
const nextPage = page + 1;
|
||||||
|
getChannels({ page: nextPage, perPage: itemsPerPage });
|
||||||
|
setPage(nextPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchTerm(value);
|
||||||
|
setPage(1);
|
||||||
|
setAllStores([]);
|
||||||
|
setHasMore(true);
|
||||||
|
getChannels({ page: 1, perPage: 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;
|
||||||
@@ -21,6 +21,7 @@ const DeliveryTerms = lazy(() => import("./pages/DeliveryTerms/index.jsx"));
|
|||||||
const AboutUs = lazy(() => import("./pages/AboutUs/index.jsx"));
|
const AboutUs = lazy(() => import("./pages/AboutUs/index.jsx"));
|
||||||
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy/index.jsx"));
|
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy/index.jsx"));
|
||||||
const AdminPage = lazy(() => import("./pages/CarconfiguratorAdmin/index.jsx"));
|
const AdminPage = lazy(() => import("./pages/CarconfiguratorAdmin/index.jsx"));
|
||||||
|
const StoresPage = lazy(() => import("./pages/Stores/index.jsx"));
|
||||||
|
|
||||||
export default function Router() {
|
export default function Router() {
|
||||||
const routes = useRoutes([
|
const routes = useRoutes([
|
||||||
@@ -34,12 +35,14 @@ export default function Router() {
|
|||||||
children: [
|
children: [
|
||||||
{ path: "/", element: <Home /> },
|
{ path: "/", element: <Home /> },
|
||||||
{ path: "/brands", element: <BrandsPage /> },
|
{ path: "/brands", element: <BrandsPage /> },
|
||||||
|
{ path: "/stores", element: <StoresPage /> },
|
||||||
{ path: "/brands/:brandId", element: <Category /> },
|
{ path: "/brands/:brandId", element: <Category /> },
|
||||||
{ path: "/cart", element: <CartPage /> },
|
{ path: "/cart", element: <CartPage /> },
|
||||||
{ path: "/wishlist", element: <WishList /> },
|
{ path: "/wishlist", element: <WishList /> },
|
||||||
{ path: "/category/:categoryId", element: <Category /> },
|
{ path: "/category/:categoryId", element: <Category /> },
|
||||||
{ path: "/search", element: <Category /> },
|
{ path: "/search", element: <Category /> },
|
||||||
{ path: "/collections/:collectionId", element: <Category /> },
|
{ path: "/collections/:collectionId", element: <Category /> },
|
||||||
|
{ path: "/channel/:channelId", element: <Category /> },
|
||||||
{ path: "/product/:productId", element: <ProductDetail /> },
|
{ path: "/product/:productId", element: <ProductDetail /> },
|
||||||
{ path: "/profile", element: <ProfileMenu /> },
|
{ path: "/profile", element: <ProfileMenu /> },
|
||||||
{ path: "/orders", element: <Orders /> },
|
{ path: "/orders", element: <Orders /> },
|
||||||
|
|||||||
Reference in New Issue
Block a user