Compare commits
5 Commits
7060d05ae9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f3079801b | ||
|
|
684ab6917d | ||
|
|
6cdde96c61 | ||
|
|
9419ec0af0 | ||
|
|
cc89455967 |
@@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
getBrands: builder.query({
|
getBrands: builder.query({
|
||||||
query: (params = {}) => {
|
query: (params = {}) => {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params.type) {
|
if (params.type) queryParams.append("type", params.type);
|
||||||
queryParams.append("type", params.type);
|
if (params.page) queryParams.append("page", params.page);
|
||||||
}
|
if (params.perPage) queryParams.append("perPage", params.perPage);
|
||||||
if (params.page) {
|
|
||||||
queryParams.append("page", params.page);
|
|
||||||
}
|
|
||||||
if (params.limit) {
|
|
||||||
queryParams.append("limit", params.limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
return `/brands${queryString ? `?${queryString}` : ""}`;
|
return `/brands${queryString ? `?${queryString}` : ""}`;
|
||||||
},
|
},
|
||||||
@@ -28,30 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
|
|
||||||
getBrandProducts: builder.query({
|
getBrandProducts: builder.query({
|
||||||
query: (params) => {
|
query: (params) => {
|
||||||
if (typeof params === 'string' || typeof params === 'number') {
|
if (typeof params === "string" || typeof params === "number") {
|
||||||
return `/brands/${params}/products`;
|
return `/brands/${params}/products`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, page = 1, limit, sorting, min_price, max_price, brands } = params;
|
const { id, page = 1, perPage = 12, sorting, min_price, max_price } = params;
|
||||||
let url = `/brands/${id}/products?page=${page}`;
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.append("page", page);
|
||||||
|
urlParams.append("perPage", perPage);
|
||||||
|
if (sorting) urlParams.append("sorting", sorting);
|
||||||
|
if (min_price) urlParams.append("min_price", min_price);
|
||||||
|
if (max_price) urlParams.append("max_price", max_price);
|
||||||
|
|
||||||
if (limit) {
|
return `/brands/${id}/products?${urlParams.toString()}`;
|
||||||
url += `&limit=${limit}`;
|
|
||||||
}
|
|
||||||
if (sorting) {
|
|
||||||
url += `&sorting=${encodeURIComponent(sorting)}`;
|
|
||||||
}
|
|
||||||
if (min_price) {
|
|
||||||
url += `&min_price=${encodeURIComponent(min_price)}`;
|
|
||||||
}
|
|
||||||
if (max_price) {
|
|
||||||
url += `&max_price=${encodeURIComponent(max_price)}`;
|
|
||||||
}
|
|
||||||
if (brands) {
|
|
||||||
url += `&brands=${encodeURIComponent(brands)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
data: response.data || response,
|
data: response.data || response,
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
getCategories: builder.query({
|
getCategories: builder.query({
|
||||||
query: (type = "tree") => `/categories?type=${type}`,
|
query: (type = "tree") => `/categories?type=${type}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCategoryProducts: builder.query({
|
getCategoryProducts: builder.query({
|
||||||
query: ({ categoryId, page = 1, limit, 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);
|
||||||
if (limit) 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);
|
||||||
if (sorting) params.append('sorting', sorting);
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
return `categories/${categoryId}/products?${params.toString()}`;
|
return `categories/${categoryId}/products?${params.toString()}`;
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
@@ -23,80 +22,105 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
pagination: response.pagination || {},
|
pagination: response.pagination || {},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAllCategoryProducts: builder.query({
|
getAllCategoryProducts: builder.query({
|
||||||
async queryFn(category, queryApi, extraOptions, baseQuery) {
|
async queryFn(category, _queryApi, _extraOptions, baseQuery) {
|
||||||
const fetchProducts = async (categoryId) => {
|
const fetchProducts = async (categoryId) => {
|
||||||
const result = await baseQuery(`categories/${categoryId}/products`);
|
const result = await baseQuery(`categories/${categoryId}/products`);
|
||||||
return result.data ? result.data.data : [];
|
return result.data ? result.data.data : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
let allProducts = await fetchProducts(category.id);
|
let allProducts = await fetchProducts(category.id);
|
||||||
|
|
||||||
for (const child of category.children) {
|
for (const child of category.children) {
|
||||||
const childProducts = await fetchProducts(child.id);
|
const childProducts = await fetchProducts(child.id);
|
||||||
allProducts = [...allProducts, ...childProducts];
|
allProducts = [...allProducts, ...childProducts];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: allProducts };
|
return { data: allProducts };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAllCategoryProductsPaginated: builder.query({
|
getAllCategoryProductsPaginated: builder.query({
|
||||||
async queryFn(
|
async queryFn(
|
||||||
{ category, page = 1, limit = 6, brands, min_price, max_price, sorting },
|
{ category, page = 1, perPage = 12, brands, min_price, max_price, sorting },
|
||||||
queryApi,
|
_queryApi,
|
||||||
extraOptions,
|
_extraOptions,
|
||||||
baseQuery
|
baseQuery
|
||||||
) {
|
) {
|
||||||
if (!category) return { data: [] };
|
if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hasMoreByCategory = {};
|
|
||||||
|
|
||||||
const fetchProductsForPage = async (categoryIds, currentPage) => {
|
|
||||||
let allPageProducts = [];
|
|
||||||
const perCategoryLimit = Math.ceil(limit / categoryIds.length);
|
|
||||||
|
|
||||||
for (const categoryId of categoryIds) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('page', currentPage);
|
|
||||||
params.append('limit', perCategoryLimit);
|
|
||||||
if (brands) params.append('brands', brands);
|
|
||||||
if (min_price) params.append('min_price', min_price);
|
|
||||||
if (max_price) params.append('max_price', max_price);
|
|
||||||
if (sorting) params.append('sorting', sorting);
|
|
||||||
|
|
||||||
const result = await baseQuery(
|
|
||||||
`categories/${categoryId}/products?${params.toString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.data && result.data.data) {
|
|
||||||
allPageProducts = [...allPageProducts, ...result.data.data];
|
|
||||||
hasMoreByCategory[categoryId] = !!result.data.pagination.next_page_url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allPageProducts;
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryIds = [category.id];
|
const categoryIds = [category.id];
|
||||||
if (category.children && category.children.length > 0) {
|
if (category.children?.length > 0) {
|
||||||
category.children.forEach((child) => categoryIds.push(child.id));
|
category.children.forEach((child) => categoryIds.push(child.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const productsForPage = await fetchProductsForPage(categoryIds, page);
|
// Tek category — direkt fetch, limit tam uygulanır
|
||||||
|
if (categoryIds.length === 1) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page);
|
||||||
|
params.append("perPage", perPage);
|
||||||
|
if (brands) params.append("brands", brands);
|
||||||
|
if (min_price) params.append("min_price", min_price);
|
||||||
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
const hasMorePages = Object.values(hasMoreByCategory).some(
|
const result = await baseQuery(
|
||||||
(hasMore) => hasMore
|
`categories/${categoryIds[0]}/products?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result.error) return { error: result.error };
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: result.data?.data || [],
|
||||||
|
pagination: {
|
||||||
|
currentPage: page,
|
||||||
|
hasMorePages: !!result.data?.pagination?.next_page_url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Birden fazla category — paralel fetch, her biri tam limit ile
|
||||||
|
// Sonra client-side deduplicate + slice
|
||||||
|
const requests = categoryIds.map((categoryId) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page);
|
||||||
|
params.append("perPage", perPage);
|
||||||
|
if (brands) params.append("brands", brands);
|
||||||
|
if (min_price) params.append("min_price", min_price);
|
||||||
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
|
return baseQuery(`categories/${categoryId}/products?${params.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(requests);
|
||||||
|
|
||||||
|
let allProducts = [];
|
||||||
|
let hasMorePages = false;
|
||||||
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.error) continue;
|
||||||
|
const items = result.data?.data || [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (!seenIds.has(item.id)) {
|
||||||
|
seenIds.add(item.id);
|
||||||
|
allProducts.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.data?.pagination?.next_page_url) {
|
||||||
|
hasMorePages = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
data: productsForPage,
|
data: allProducts,
|
||||||
pagination: {
|
pagination: {
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
hasMorePages: hasMorePages,
|
hasMorePages,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -105,11 +129,11 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getProductById: builder.query({
|
getProductById: builder.query({
|
||||||
query: (productId) => `/products/${productId}`,
|
query: (productId) => `/products/${productId}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getRelatedProducts: builder.query({
|
getRelatedProducts: builder.query({
|
||||||
query: (productId) => `/products/${productId}/related`,
|
query: (productId) => `/products/${productId}/related`,
|
||||||
}),
|
}),
|
||||||
|
|||||||
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;
|
||||||
@@ -5,40 +5,35 @@ export const collectionsApi = baseApi.injectEndpoints({
|
|||||||
getCollections: builder.query({
|
getCollections: builder.query({
|
||||||
query: () => `/collections`,
|
query: () => `/collections`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCollectionById: builder.query({
|
getCollectionById: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}`,
|
query: (collectionId) => `/collections/${collectionId}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCollectionProducts: builder.query({
|
getCollectionProducts: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}/products`,
|
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||||
transformResponse: (response) => {
|
transformResponse: (response) => ({
|
||||||
return {
|
data: response.data || [],
|
||||||
data: response.data || [],
|
isEmpty: !response.data || response.data.length === 0,
|
||||||
isEmpty: !response.data || response.data.length === 0,
|
}),
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
checkCollectionHasProducts: builder.query({
|
checkCollectionHasProducts: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}/products?limit=1`,
|
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||||
transformResponse: (response) => {
|
transformResponse: (response) => ({
|
||||||
return {
|
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 = 6, brands, min_price, max_price, sorting = "price_amount-ascending" }) => {
|
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);
|
||||||
if (limit) 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);
|
||||||
params.append('sorting', sorting);
|
if (sorting) params.append("sorting", sorting); // undefined gelirse gönderme
|
||||||
|
|
||||||
return `/collections/${collectionId}/products?${params.toString()}`;
|
return `/collections/${collectionId}/products?${params.toString()}`;
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,78 +1,116 @@
|
|||||||
|
// DropdownMenu.module.scss
|
||||||
|
|
||||||
.dropdownContainer {
|
.dropdownContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- TRIGGER BUTTON ----
|
||||||
.navButton {
|
.navButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
|
||||||
border: none;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
padding-left: 0.875rem;
|
|
||||||
padding-right: 0.875rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.875rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s, color 0.15s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownWrapper {
|
&.navButtonActive {
|
||||||
position: relative;
|
background-color: #e63946;
|
||||||
}
|
color: #ffffff;
|
||||||
|
|
||||||
.dropdownPanel {
|
svg {
|
||||||
position: absolute;
|
color: #ffffff;
|
||||||
top: 100%;
|
}
|
||||||
margin-top: 8px;
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 1366px;
|
|
||||||
padding: 0 1.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.categoriesList {
|
|
||||||
flex: 1;
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-right: 1px solid #ebe7eb;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
// &::-webkit-scrollbar {
|
|
||||||
// width: 6px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- OVERLAY ----
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 998;
|
||||||
|
animation: fadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- WRAPPER + ANIMATION ----
|
||||||
|
.dropdownWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 999;
|
||||||
|
animation: slideDown 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PANEL SHELL ----
|
||||||
|
.dropdownPanel {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 1336px;
|
||||||
|
max-height: 520px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- LEFT LIST ----
|
||||||
|
.categoriesList {
|
||||||
|
width: 270px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
padding: 10px 0;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
}
|
border-radius: 10px;
|
||||||
.title {
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
background: #9ca3af;
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
color: #888888;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,156 +118,169 @@
|
|||||||
.categoryItem {
|
.categoryItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
padding: 6px;
|
padding: 9px 16px;
|
||||||
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
color: #000;
|
||||||
border: 1px solid #3615371a;
|
transition: background-color 0.12s, color 0.12s;
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f9fafb;
|
background-color: #f3f4f6;
|
||||||
|
color: #e63946;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
color: #e63946;
|
||||||
|
|
||||||
.icon {
|
.title {
|
||||||
font-size: 14px;
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
&:hover {
|
|
||||||
color: #888888;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
color: #9ca3af;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RIGHT CONTENT PANEL ----
|
||||||
.contentPanel {
|
.contentPanel {
|
||||||
flex: 3;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 20px 24px;
|
||||||
max-height: 400px;
|
max-height: 520px;
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
// &::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
// width: 6px;
|
width: 6px;
|
||||||
// }
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
&::-webkit-scrollbar-track {
|
background: #f9fafb;
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
// border-radius: 3px;
|
border-radius: 10px;
|
||||||
}
|
|
||||||
.title {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #361517;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
background: #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.column {
|
|
||||||
display: flex;
|
.panelTitle {
|
||||||
flex-direction: column;
|
font-size: 20px;
|
||||||
flex: 2;
|
font-weight: 700;
|
||||||
text-align: left;
|
color: #111827;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COLUMN GRID MODE
|
||||||
|
// SONRA — column layout (iyi, masonry gibi akar)
|
||||||
|
.columnsGrid {
|
||||||
|
columns: 250px auto;
|
||||||
|
column-gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: inline-block; // break-inside'ın çalışması için zorunlu
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #361517;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryList {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryItem {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #361517;
|
font-weight: 800;
|
||||||
padding: 4px 0;
|
color: #111827;
|
||||||
|
margin-bottom: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
color: #e63946;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subCategoriesContainer {
|
.leafItem {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
font-size: 14px;
|
||||||
max-height: 360px;
|
color: #4b5563;
|
||||||
overflow-y: auto;
|
padding: 3px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryContainer:last-child {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryContainer {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: color 0.12s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLAT LIST MODE
|
||||||
|
.flatList {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatListBordered {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatItem {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
transition: background-color 0.12s, color 0.12s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
color: #111827;
|
||||||
|
|
||||||
.categoryLabel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandButton,
|
.navButtonLoading {
|
||||||
.navigateButton {
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
|
||||||
|
.categoryIcon {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingDots {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
span {
|
||||||
background-color: #e5e7eb;
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
animation: dotPulse 1.2s infinite ease-in-out;
|
||||||
|
|
||||||
|
&:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
&:nth-child(3) { animation-delay: 0.4s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nestedChildren {
|
@keyframes dotPulse {
|
||||||
margin-top: 4px;
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||||
}
|
40% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
.noSubcategories {
|
|
||||||
color: #6b7280;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
@@ -1,83 +1,62 @@
|
|||||||
|
// DropdownMenu.jsx
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
import styles from "./DropdownMenu.module.scss";
|
import styles from "./DropdownMenu.module.scss";
|
||||||
import { useGetCategoriesQuery } from "../../app/api/categories";
|
import { useGetCategoriesQuery } from "../../app/api/categories";
|
||||||
import { CategoryIcon } from "../Icons";
|
import { CategoryIcon } from "../Icons";
|
||||||
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
|
|
||||||
|
|
||||||
const NestedCategory = ({
|
const ContentPanel = ({ category, onSelect, onClose }) => {
|
||||||
category,
|
if (!category) return null;
|
||||||
level = 0,
|
|
||||||
handleCategorySelect,
|
|
||||||
closeDropdown,
|
|
||||||
}) => {
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
const hasChildren = category.children && category.children.length > 0;
|
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const children = category.children || [];
|
||||||
e.stopPropagation();
|
const withChildren = children.filter((c) => c.children?.length > 0);
|
||||||
if (hasChildren) {
|
const withoutChildren = children.filter((c) => !c.children?.length);
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
} else {
|
|
||||||
handleCategorySelect(category);
|
|
||||||
closeDropdown();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDirectNavigation = (e) => {
|
const allColumns = [
|
||||||
e.stopPropagation();
|
...withChildren,
|
||||||
handleCategorySelect(category);
|
...withoutChildren.map((c) => ({ ...c, children: [] })),
|
||||||
closeDropdown();
|
];
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.contentPanel}>
|
||||||
className={styles.nestedCategoryContainer}
|
<h2
|
||||||
style={{ paddingLeft: `${level * 16}px` }}
|
className={styles.panelTitle}
|
||||||
>
|
onClick={() => {
|
||||||
<div className={styles.nestedCategoryItem} onClick={handleClick}>
|
onSelect(category);
|
||||||
<div className={styles.categoryLabel}>
|
onClose();
|
||||||
<span className={styles.title}>{category.name}</span>
|
}}
|
||||||
</div>
|
>
|
||||||
|
{category.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{hasChildren && (
|
{allColumns.length > 0 && (
|
||||||
<button
|
<div className={styles.columnsGrid}>
|
||||||
className={styles.expandButton}
|
{allColumns.map((sub) => (
|
||||||
onClick={(e) => {
|
<div key={sub.id} className={styles.columnSection}>
|
||||||
e.stopPropagation();
|
<div
|
||||||
setIsExpanded(!isExpanded);
|
className={styles.sectionTitle}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
onSelect(sub);
|
||||||
{isExpanded ? (
|
onClose();
|
||||||
<ChevronDown size={16} />
|
}}
|
||||||
) : (
|
>
|
||||||
<ChevronRight size={16} />
|
{sub.name}
|
||||||
)}
|
</div>
|
||||||
</button>
|
{sub.children?.map((leaf) => (
|
||||||
)}
|
<span
|
||||||
|
key={leaf.id}
|
||||||
{hasChildren && (
|
className={styles.leafItem}
|
||||||
<button
|
onClick={() => {
|
||||||
className={styles.navigateButton}
|
onSelect(leaf);
|
||||||
onClick={handleDirectNavigation}
|
onClose();
|
||||||
title="Go to category"
|
}}
|
||||||
>
|
>
|
||||||
→
|
{leaf.name}
|
||||||
</button>
|
</span>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasChildren && isExpanded && (
|
|
||||||
<div className={styles.nestedChildren}>
|
|
||||||
{category.children.map((child) => (
|
|
||||||
<NestedCategory
|
|
||||||
key={child.id}
|
|
||||||
category={child}
|
|
||||||
level={level + 1}
|
|
||||||
handleCategorySelect={handleCategorySelect}
|
|
||||||
closeDropdown={closeDropdown}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -89,113 +68,85 @@ const DropdownMenu = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: categoriesData,
|
data: categoriesData,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useGetCategoriesQuery("tree");
|
} = useGetCategoriesQuery("tree");
|
||||||
|
|
||||||
const categories = categoriesData?.data || [];
|
const categories = categoriesData?.data || [];
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [activeMainCategory, setActiveMainCategory] = useState(null);
|
const [activeCategory, setActiveCategory] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (categories.length > 0) {
|
if (categories.length > 0 && !activeCategory) {
|
||||||
const defaultCategory =
|
setActiveCategory(categories[0]);
|
||||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
|
||||||
setActiveMainCategory(defaultCategory);
|
|
||||||
}
|
}
|
||||||
}, [categories]);
|
}, [categories]);
|
||||||
|
|
||||||
const handleToggle = () => {
|
useEffect(() => {
|
||||||
setIsOpen(!isOpen);
|
const handler = (e) => {
|
||||||
};
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handler);
|
||||||
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleSelect = (category) => {
|
||||||
if (categories.length > 0) {
|
|
||||||
const defaultCategory =
|
|
||||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
|
||||||
setActiveMainCategory(defaultCategory);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCategorySelect = (category) => {
|
|
||||||
navigate(`/category/${category.id}`, { state: { category } });
|
navigate(`/category/${category.id}`, { state: { category } });
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
|
||||||
if (error) return <div>Error loading categories</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
||||||
<button onClick={handleToggle} className={styles.navButton}>
|
<button
|
||||||
|
onClick={() => setIsOpen((p) => !p)}
|
||||||
|
className={`${styles.navButton} ${isOpen ? styles.navButtonActive : ""}`}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
>
|
||||||
<CategoryIcon />
|
<CategoryIcon />
|
||||||
{t("navbar.category")}
|
{isLoading
|
||||||
|
? <div className={styles.loadingDots}><span/><span/><span/></div>
|
||||||
|
: t("navbar.category")
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className={styles.dropdownWrapper}>
|
<>
|
||||||
<div className={styles.dropdownPanel} onMouseLeave={handleMouseLeave}>
|
<div className={styles.overlay} onClick={() => setIsOpen(false)} />
|
||||||
<div className={styles.categoriesList}>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<div
|
|
||||||
key={category.id}
|
|
||||||
className={`${styles.categoryItem} ${
|
|
||||||
activeMainCategory?.id === category.id ? styles.active : ""
|
|
||||||
}`}
|
|
||||||
onMouseEnter={() => setActiveMainCategory(category)}
|
|
||||||
onClick={() => handleCategorySelect(category)}
|
|
||||||
>
|
|
||||||
<span className={styles.title}>{category.name}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeMainCategory && (
|
<div className={styles.dropdownWrapper}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.dropdownPanel}>
|
||||||
<h2
|
<div className={styles.categoriesList}>
|
||||||
onClick={() => handleCategorySelect(activeMainCategory)}
|
{categories.map((cat) => (
|
||||||
className={styles.title}
|
<div
|
||||||
>
|
key={cat.id}
|
||||||
{activeMainCategory.name}
|
className={`${styles.categoryItem} ${
|
||||||
</h2>
|
activeCategory?.id === cat.id ? styles.active : ""
|
||||||
|
}`}
|
||||||
<div className={styles.subCategoriesContainer}>
|
onMouseEnter={() => setActiveCategory(cat)}
|
||||||
{activeMainCategory.children &&
|
onClick={() => handleSelect(cat)}
|
||||||
activeMainCategory.children.length > 0 ? (
|
>
|
||||||
activeMainCategory.children.map((subcategory) => (
|
<span className={styles.title}>{cat.name}</span>
|
||||||
<NestedCategory
|
{cat.children?.length > 0 && (
|
||||||
key={subcategory.id}
|
<ChevronRight size={14} className={styles.chevron} />
|
||||||
category={subcategory}
|
)}
|
||||||
handleCategorySelect={handleCategorySelect}
|
</div>
|
||||||
closeDropdown={() => setIsOpen(false)}
|
))}
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className={styles.noSubcategories}>
|
|
||||||
{/* No subcategories available */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<ContentPanel
|
||||||
|
category={activeCategory}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "./Checkout.module.scss";
|
import styles from "./Checkout.module.scss";
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
usePlaceOrderMutation,
|
usePlaceOrderMutation,
|
||||||
@@ -9,202 +8,145 @@ import {
|
|||||||
} from "../../app/api/orderApi";
|
} from "../../app/api/orderApi";
|
||||||
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const useDeviceType = () => {
|
const useDeviceType = () => {
|
||||||
const [deviceType, setDeviceType] = useState("desktop");
|
const [deviceType, setDeviceType] = useState("desktop");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userAgent = navigator.userAgent;
|
setDeviceType(
|
||||||
if (/Mobi|Android/i.test(userAgent)) {
|
/Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "desktop",
|
||||||
setDeviceType("mobile");
|
);
|
||||||
} else {
|
|
||||||
setDeviceType("desktop");
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return deviceType;
|
return deviceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceOrder }) => {
|
const Checkout = ({
|
||||||
|
cartItems,
|
||||||
|
shippingPrice,
|
||||||
|
productIds,
|
||||||
|
onBackToCart,
|
||||||
|
onPlaceOrder,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
customer_name: "",
|
customer_name: "",
|
||||||
customer_phone: "",
|
customer_phone: "+993 ",
|
||||||
customer_address: "",
|
customer_address: "",
|
||||||
deliveryAddress: "null",
|
|
||||||
payment_type_id: "",
|
payment_type_id: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
region: "",
|
region: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedAddress, setSelectedAddress] = useState(null);
|
const [selectedAddress, setSelectedAddress] = useState(null);
|
||||||
const [placeOrder, { isLoading: isPlacingOrder }] = usePlaceOrderMutation();
|
const [placeOrder] = usePlaceOrderMutation();
|
||||||
const { data: orderTimes = {} } = useGetOrderTimesQuery();
|
|
||||||
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
||||||
const { data: locationsData } = useGetLocationsQuery();
|
const { data: locationsData } = useGetLocationsQuery();
|
||||||
const deviceType = useDeviceType();
|
const deviceType = useDeviceType();
|
||||||
|
|
||||||
|
// Sepetteki tüm ürünlerin fiyatı 0 mı?
|
||||||
|
const allItemsZeroPrice = cartItems?.every((item) =>
|
||||||
|
isPriceZero(item.product?.price_amount),
|
||||||
|
);
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
if (name === "customer_phone") {
|
if (name === "customer_phone") {
|
||||||
// Always keep the +993 prefix
|
|
||||||
const prefix = "+993 ";
|
const prefix = "+993 ";
|
||||||
|
if (value.length < prefix.length) return;
|
||||||
// If user is trying to delete the prefix, prevent it
|
|
||||||
if (value.length < prefix.length) {
|
const digits = value
|
||||||
return; // Don't update state, keep the current value
|
.substring(prefix.length)
|
||||||
}
|
.replace(/\D/g, "")
|
||||||
|
.substring(0, 8);
|
||||||
// Extract only the digits after the prefix
|
let formatted = prefix + digits.substring(0, 2);
|
||||||
const inputWithoutPrefix = value.substring(prefix.length).replace(/\D/g, "");
|
if (digits.length > 2) formatted += " " + digits.substring(2);
|
||||||
|
|
||||||
// Limit to 8 digits max (Turkmenistan mobile number format)
|
setFormData((prev) => ({ ...prev, [name]: formatted }));
|
||||||
const limitedDigits = inputWithoutPrefix.substring(0, 8);
|
|
||||||
|
|
||||||
// Format with space after first 2 digits
|
|
||||||
let formattedPhone = prefix;
|
|
||||||
if (limitedDigits.length > 0) {
|
|
||||||
formattedPhone += limitedDigits.substring(0, 2);
|
|
||||||
|
|
||||||
if (limitedDigits.length > 2) {
|
|
||||||
formattedPhone += " " + limitedDigits.substring(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: formattedPhone,
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddressSelect = (value) => {
|
const handleAddressSelect = (value) => {
|
||||||
setSelectedAddress(value);
|
setSelectedAddress(value);
|
||||||
const selectedLocation = locationsData?.data?.find(
|
const selectedLocation = locationsData?.data?.find((l) => l.name === value);
|
||||||
(location) => location.name === value
|
|
||||||
);
|
|
||||||
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
address: value,
|
address: value,
|
||||||
region: selectedLocation ? selectedLocation.region : "",
|
region: selectedLocation?.region || "",
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize phone with prefix
|
|
||||||
useEffect(() => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
customer_phone: "+993 "
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const formatPhoneNumber = (phoneNumber) => {
|
|
||||||
// Remove the +993 prefix and any spaces
|
|
||||||
return phoneNumber.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAddress = () => {
|
const handleClearAddress = () => {
|
||||||
setSelectedAddress(null);
|
setSelectedAddress(null);
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, address: "" }));
|
||||||
...prev,
|
|
||||||
address: "",
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = (event) => {
|
const handleFocus = (e) =>
|
||||||
event.target.scrollIntoView({
|
e.target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
const formatPhoneNumber = (phone) =>
|
||||||
});
|
phone.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
||||||
};
|
|
||||||
|
|
||||||
const getOrderData = () => {
|
const getOrderData = () => {
|
||||||
// Validation checks
|
|
||||||
if (
|
if (
|
||||||
!formData.customer_name ||
|
!formData.customer_name ||
|
||||||
!formData.customer_phone ||
|
!formData.customer_phone ||
|
||||||
!formData.customer_address ||
|
!formData.customer_address ||
|
||||||
!formData.payment_type_id
|
!formData.payment_type_id
|
||||||
) {
|
) {
|
||||||
console.error("Missing required fields");
|
|
||||||
alert("Please fill in all required fields");
|
alert("Please fill in all required fields");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default values for delivery
|
const currentDate = new Date().toISOString().split("T")[0];
|
||||||
const currentDate = new Date().toISOString().split('T')[0];
|
|
||||||
const defaultTimeSlot = {
|
|
||||||
date: currentDate,
|
|
||||||
hour: "12:00-14:00" // Default time slot
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare data in the format expected by the API
|
|
||||||
return {
|
return {
|
||||||
customer_name: formData.customer_name,
|
customer_name: formData.customer_name,
|
||||||
customer_phone: formatPhoneNumber(formData.customer_phone),
|
customer_phone: formatPhoneNumber(formData.customer_phone),
|
||||||
customer_address: formData.customer_address,
|
customer_address: formData.customer_address,
|
||||||
shipping_method: "standard", // Default to standard shipping
|
shipping_method: "standard",
|
||||||
payment_type_id: formData.payment_type_id,
|
payment_type_id: formData.payment_type_id,
|
||||||
delivery_time: defaultTimeSlot.hour,
|
delivery_time: "12:00-14:00",
|
||||||
delivery_at: defaultTimeSlot.date,
|
delivery_at: currentDate,
|
||||||
region: formData.region || "",
|
region: formData.region || "",
|
||||||
notes: formData.notes || "",
|
notes: formData.notes || "",
|
||||||
// Add shipping price and product IDs
|
|
||||||
shipping_price: shippingPrice,
|
shipping_price: shippingPrice,
|
||||||
product_ids: productIds // Array of product IDs [1, 3, 4, etc.]
|
product_ids: productIds,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the place order function
|
|
||||||
const handlePlaceOrder = async () => {
|
const handlePlaceOrder = async () => {
|
||||||
const orderDetails = getOrderData();
|
const orderDetails = getOrderData();
|
||||||
if (!orderDetails) return false;
|
if (!orderDetails) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await placeOrder(orderDetails).unwrap();
|
await placeOrder(orderDetails).unwrap();
|
||||||
|
|
||||||
console.log("Order placed successfully:", response);
|
|
||||||
window.location.href = "/orders";
|
window.location.href = "/orders";
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order:", error);
|
const isHtmlResponse =
|
||||||
|
|
||||||
if (
|
|
||||||
error.data &&
|
error.data &&
|
||||||
typeof error.data === "string" &&
|
typeof error.data === "string" &&
|
||||||
error.data.includes("<!doctype html>")
|
error.data.includes("<!doctype html>");
|
||||||
) {
|
|
||||||
console.error(
|
alert(
|
||||||
"Server returned HTML instead of a proper API response"
|
isHtmlResponse
|
||||||
);
|
? "There was a problem with the server. Please try again later."
|
||||||
alert(
|
: "Failed to place order. Please check your information and try again.",
|
||||||
"There was a problem with the server. Please try again later or contact support."
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
"Failed to place order. Please check your information and try again."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose the function to parent component via callback
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onPlaceOrder) {
|
if (onPlaceOrder) onPlaceOrder(handlePlaceOrder);
|
||||||
onPlaceOrder(handlePlaceOrder);
|
|
||||||
}
|
|
||||||
}, [formData, shippingPrice, productIds]);
|
}, [formData, shippingPrice, productIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.checkoutContainer}>
|
<div className={styles.checkoutContainer}>
|
||||||
<h2>{t("cart.basket")} ({cartItems?.length || 0})</h2>
|
{/* <h2>{t("cart.basket")} ({cartItems?.length || 0})</h2> */}
|
||||||
<div className={styles.formSection}>
|
<div className={styles.formSection}>
|
||||||
<div className={styles.paymentOptions}>
|
<div className={styles.paymentOptions}>
|
||||||
<h3>{t("checkout.paymentMethod")}:</h3>
|
<h3>{t("checkout.paymentMethod")}:</h3>
|
||||||
@@ -221,22 +163,22 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
<label
|
<label
|
||||||
htmlFor={`payment${payment.id}`}
|
htmlFor={`payment${payment.id}`}
|
||||||
className={styles.customRadio}
|
className={styles.customRadio}
|
||||||
></label>
|
/>
|
||||||
<div
|
<div
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
payment_type_id: String(payment.id),
|
payment_type_id: String(payment.id),
|
||||||
}));
|
}))
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
))}
|
))}
|
||||||
@@ -256,7 +198,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.telephone")}*</label>
|
<label>{t("checkout.telephone")}*</label>
|
||||||
<input
|
<input
|
||||||
@@ -270,7 +211,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formRow}>
|
<div className={styles.formRow}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
||||||
@@ -283,7 +223,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.note")}</label>
|
<label>{t("checkout.note")}</label>
|
||||||
<input
|
<input
|
||||||
@@ -301,22 +240,17 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
{t(
|
{t(
|
||||||
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau"
|
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau",
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
{/* <li>
|
|
||||||
{t(
|
|
||||||
"checkout.The_minimum_order_amount_must_be_at_least_50_manat_for_orders_over_150_manat_delivery_is_free"
|
|
||||||
)}
|
|
||||||
</li> */}
|
|
||||||
<li>
|
|
||||||
{t(
|
|
||||||
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request"
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{t(
|
{t(
|
||||||
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return"
|
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request",
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return",
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -326,4 +260,4 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Checkout;
|
export default Checkout;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
55
src/components/HomeBrands/HomeBrands.module.scss
Normal file
55
src/components/HomeBrands/HomeBrands.module.scss
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.container {
|
||||||
|
max-width: 1336px;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 100%;
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandsSwiper {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandSlide {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandCard {
|
||||||
|
width: 122px;
|
||||||
|
height: 50px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoFallback {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.brandCard {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/components/HomeBrands/index.jsx
Normal file
67
src/components/HomeBrands/index.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
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 { Logo } from '../Icons';
|
||||||
|
|
||||||
|
const HomeBrands = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// Fetch brands.
|
||||||
|
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 100 });
|
||||||
|
|
||||||
|
if (isLoading || !brandsData || brandsData.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Swiper
|
||||||
|
modules={[Autoplay]}
|
||||||
|
spaceBetween={12}
|
||||||
|
slidesPerView={'auto'}
|
||||||
|
slidesPerGroup={2}
|
||||||
|
loop={true}
|
||||||
|
autoplay={{
|
||||||
|
delay: 3000,
|
||||||
|
disableOnInteraction: false,
|
||||||
|
}}
|
||||||
|
className={styles.brandsSwiper}
|
||||||
|
>
|
||||||
|
{brandsData.map((brand) => (
|
||||||
|
<SwiperSlide key={brand.id} className={styles.brandSlide}>
|
||||||
|
<div
|
||||||
|
className={styles.brandCard}
|
||||||
|
onClick={() => navigate(`/brands/${brand.id}`)}
|
||||||
|
>
|
||||||
|
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
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} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -221,7 +236,7 @@ export const CategoryIcon = () => (
|
|||||||
height={20}
|
height={20}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="#4b5563"
|
fill="currentColor"
|
||||||
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__satyjy {
|
&__satyjy {
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 785px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 708px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
102
src/components/PendingPriceBadge/PendingPriceBadge.module.scss
Normal file
102
src/components/PendingPriceBadge/PendingPriceBadge.module.scss
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/components/PendingPriceBadge/index.jsx
Normal file
71
src/components/PendingPriceBadge/index.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styles from "./PendingPriceBadge.module.scss";
|
||||||
|
|
||||||
|
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOk={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
okText={t("common.ok") || "Ok"}
|
||||||
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
|
centered
|
||||||
|
title={t("cart.pendingPriceTitle") || "Bahasy anyklamaly"}
|
||||||
|
className="pending-price-modal"
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{t("cart.pendingPriceDesc") ||
|
||||||
|
"Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer."}
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PendingPriceBadge = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPropagation = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onClick={stopPropagation}>
|
||||||
|
<span
|
||||||
|
className={styles.pendingPriceBadgeWrapper}
|
||||||
|
onMouseEnter={() => setTooltipVisible(true)}
|
||||||
|
onMouseLeave={() => setTooltipVisible(false)}
|
||||||
|
onClick={handleClick}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.pendingPriceBadge}>!</span>
|
||||||
|
|
||||||
|
{tooltipVisible && (
|
||||||
|
<span className={styles.pendingPriceTooltip}>
|
||||||
|
<strong>{t("cart.pendingPriceTitle") || "Bahasyny anyklamaly"}</strong>
|
||||||
|
{t("cart.pendingPriceTooltipDesc") ||
|
||||||
|
"Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<PendingPriceModal
|
||||||
|
open={modalVisible}
|
||||||
|
onClose={() => setModalVisible(false)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PendingPriceBadge;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,36 +3,32 @@ import styles from "./ProductCard.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";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { debounce } from "lodash";
|
|
||||||
import {
|
import {
|
||||||
useAddFavoriteMutation,
|
useAddFavoriteMutation,
|
||||||
useRemoveFavoriteMutation,
|
useRemoveFavoriteMutation,
|
||||||
|
useGetFavoritesQuery,
|
||||||
} from "../../app/api/favoritesApi";
|
} from "../../app/api/favoritesApi";
|
||||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
|
||||||
import {
|
import {
|
||||||
useAddToCartMutation,
|
useAddToCartMutation,
|
||||||
useUpdateCartItemMutation,
|
useUpdateCartItemMutation,
|
||||||
useRemoveFromCartMutation,
|
useRemoveFromCartMutation,
|
||||||
useGetCartQuery,
|
|
||||||
} from "../../app/api/cartApi";
|
} from "../../app/api/cartApi";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
||||||
import ImageCarousel from "./imageCarousel/index";
|
import ImageCarousel from "./imageCarousel/index";
|
||||||
|
import { useCart } from "../../app/api/useCart";
|
||||||
|
|
||||||
// Helper function to strip HTML tags and truncate text
|
|
||||||
const truncateDescription = (htmlString, maxLength = 80) => {
|
const truncateDescription = (htmlString, maxLength = 80) => {
|
||||||
const tempDiv = document.createElement("div");
|
const tempDiv = document.createElement("div");
|
||||||
tempDiv.innerHTML = htmlString;
|
tempDiv.innerHTML = htmlString;
|
||||||
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
||||||
const truncatedText =
|
return textContent.length > maxLength
|
||||||
textContent.length > maxLength
|
? textContent.substring(0, maxLength).trim() + "..."
|
||||||
? textContent.substring(0, maxLength).trim() + "..."
|
: textContent;
|
||||||
: textContent;
|
|
||||||
return truncatedText;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
import { useCart } from "../../app/api/useCart";
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const ProductCard = ({
|
const ProductCard = ({
|
||||||
product,
|
product,
|
||||||
@@ -41,26 +37,24 @@ 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();
|
||||||
|
|
||||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const [addFavorite] = useAddFavoriteMutation();
|
const [addFavorite] = useAddFavoriteMutation();
|
||||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||||
favoriteProducts.some((fav) => fav.product?.id === product.id),
|
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||||
);
|
|
||||||
// const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const truncatedDesc = truncateDescription(
|
|
||||||
product.description,
|
|
||||||
descriptionMaxLength,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getCartItem } = useCart();
|
const { getCartItem } = useCart();
|
||||||
|
|
||||||
const [addToCart] = useAddToCartMutation();
|
const [addToCart] = useAddToCartMutation();
|
||||||
const [updateCartItem] = useUpdateCartItemMutation();
|
const [updateCartItem] = useUpdateCartItemMutation();
|
||||||
const [removeFromCart] = useRemoveFromCartMutation();
|
const [removeFromCart] = useRemoveFromCartMutation();
|
||||||
@@ -69,26 +63,54 @@ const ProductCard = ({
|
|||||||
const [localQuantity, setLocalQuantity] = useState(0);
|
const [localQuantity, setLocalQuantity] = useState(0);
|
||||||
const [pendingQuantity, setPendingQuantity] = useState(0);
|
const [pendingQuantity, setPendingQuantity] = useState(0);
|
||||||
|
|
||||||
// ✅ Cart item değiştiğinde local state'i güncelle
|
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
||||||
|
|
||||||
|
const truncatedDesc = truncateDescription(product.description, descriptionMaxLength);
|
||||||
|
|
||||||
|
const calculatedDiscount =
|
||||||
|
!product.discount &&
|
||||||
|
old_price_amount &&
|
||||||
|
price_amount &&
|
||||||
|
old_price_amount > price_amount
|
||||||
|
? Math.round(((old_price_amount - price_amount) / old_price_amount) * 100)
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const qty = parseInt(
|
const qty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
setLocalQuantity(qty);
|
setLocalQuantity(qty);
|
||||||
setPendingQuantity(qty);
|
setPendingQuantity(qty);
|
||||||
}, [cartItem]);
|
}, [cartItem]);
|
||||||
|
|
||||||
// ✅ Favorite state'i güncelle
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Array.isArray(favoriteProducts)) {
|
if (Array.isArray(favoriteProducts)) {
|
||||||
const isFav = favoriteProducts.some(
|
setLocalIsFavorite(
|
||||||
(fav) => fav.product?.id === product.id,
|
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||||
);
|
);
|
||||||
setLocalIsFavorite(isFav);
|
|
||||||
}
|
}
|
||||||
}, [favoriteProducts, product.id]);
|
}, [favoriteProducts, product.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const serverQty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||||
|
|
||||||
|
if (pendingQuantity === serverQty || pendingQuantity <= 0) return;
|
||||||
|
|
||||||
|
const handler = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateCartItem({ productId: product.id, quantity: pendingQuantity }).unwrap();
|
||||||
|
} catch {
|
||||||
|
setLocalQuantity(serverQty);
|
||||||
|
setPendingQuantity(serverQty);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||||
|
|
||||||
|
const handleCardClick = () => navigate(`/product/${product.id}`);
|
||||||
|
|
||||||
const handleAddToCart = async (event) => {
|
const handleAddToCart = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -98,51 +120,17 @@ const ProductCard = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Optimistic update
|
|
||||||
setLocalQuantity((prev) => prev + 1);
|
setLocalQuantity((prev) => prev + 1);
|
||||||
setPendingQuantity((prev) => prev + 1);
|
setPendingQuantity((prev) => prev + 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik cache'i güncelleyecek
|
} catch {
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to add to cart:", error);
|
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalQuantity((prev) => prev - 1);
|
setLocalQuantity((prev) => prev - 1);
|
||||||
setPendingQuantity((prev) => prev - 1);
|
setPendingQuantity((prev) => prev - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Debounced update - sadece mutation, refetch yok
|
|
||||||
useEffect(() => {
|
|
||||||
const serverQty = parseInt(
|
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
await updateCartItem({
|
|
||||||
productId: product.id,
|
|
||||||
quantity: pendingQuantity,
|
|
||||||
}).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update cart item:", error);
|
|
||||||
setLocalQuantity(serverQty);
|
|
||||||
setPendingQuantity(serverQty);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(handler);
|
|
||||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
|
||||||
|
|
||||||
const handleQuantityIncrease = (event) => {
|
const handleQuantityIncrease = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -165,24 +153,17 @@ const ProductCard = ({
|
|||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
if (pendingQuantity <= 1) {
|
if (pendingQuantity <= 1) {
|
||||||
// ✅ Sıfıra düşünce direkt sil
|
|
||||||
setPendingQuantity(0);
|
setPendingQuantity(0);
|
||||||
setLocalQuantity(0);
|
setLocalQuantity(0);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
removeFromCart({ productId: product.id })
|
removeFromCart({ productId: product.id })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => {
|
|
||||||
// ✅ Başarılı - RTK Query cache'i güncelleyecek
|
|
||||||
})
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalQuantity(1);
|
setLocalQuantity(1);
|
||||||
setPendingQuantity(1);
|
setPendingQuantity(1);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => setIsLoading(false));
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setLocalQuantity((prev) => prev - 1);
|
setLocalQuantity((prev) => prev - 1);
|
||||||
setPendingQuantity((prev) => prev - 1);
|
setPendingQuantity((prev) => prev - 1);
|
||||||
@@ -196,53 +177,33 @@ const ProductCard = ({
|
|||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setLocalIsFavorite((prev) => !prev);
|
||||||
// ✅ Optimistic update
|
|
||||||
setLocalIsFavorite(!localIsFavorite);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (localIsFavorite) {
|
if (localIsFavorite) {
|
||||||
const result = await removeFavorite(product.id).unwrap();
|
await removeFavorite(product.id).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
|
||||||
} else {
|
} else {
|
||||||
const result = await addFavorite(product.id).unwrap();
|
await addFavorite(product.id).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("Failed to toggle favorite:", error);
|
setLocalIsFavorite((prev) => !prev); // revert
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalIsFavorite(localIsFavorite);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardClick = () => {
|
|
||||||
navigate(`/product/${product.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
|
|
||||||
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
|
||||||
|
|
||||||
// Hesaplanmış indirim oranı
|
|
||||||
let calculatedDiscount = null;
|
|
||||||
if (!product.discount && old_price_amount && price_amount && old_price_amount > price_amount) {
|
|
||||||
calculatedDiscount = Math.round(((old_price_amount - price_amount) / old_price_amount) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={styles.productCard}
|
className={styles.productCard}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
{(product.discount || calculatedDiscount) && (
|
{(product.discount > 0 || calculatedDiscount > 0) && (
|
||||||
<span className={styles.discountBadge}>
|
<span className={styles.discountBadge}>
|
||||||
-{product.discount ? product.discount : calculatedDiscount}%
|
-{product.discount || calculatedDiscount}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{product.stock === 0 && (
|
{product.stock === 0 && (
|
||||||
@@ -250,22 +211,29 @@ const ProductCard = ({
|
|||||||
{t("common.out_of_stock")}
|
{t("common.out_of_stock")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<ImageCarousel images={media} altText={name} isHovered={isHovered} />
|
||||||
<ImageCarousel images={media} altText={name} isHovered={isHovered}/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.productInfo}>
|
<div className={styles.productInfo}>
|
||||||
<h3 className={styles.productName}>{name}</h3>
|
<h3 className={styles.productName}>{name}</h3>
|
||||||
<p className={styles.productDescription}>{truncatedDesc}</p>
|
<p className={styles.productDescription}>{truncatedDesc}</p>
|
||||||
|
|
||||||
<div className={styles.priceContainer}>
|
<div className={styles.priceContainer}>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
{isPriceZero(price_amount) ? (
|
||||||
{old_price_amount && (
|
<span className={styles.currentPrice}> {t("cart.pendingPriceTitle")}</span>
|
||||||
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||||
|
{old_price_amount && (
|
||||||
|
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
{showFavoriteButton && (
|
{showFavoriteButton && (
|
||||||
<button
|
<button
|
||||||
@@ -276,6 +244,7 @@ const ProductCard = ({
|
|||||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAddToCart && (
|
{showAddToCart && (
|
||||||
<>
|
<>
|
||||||
{localQuantity > 0 ? (
|
{localQuantity > 0 ? (
|
||||||
@@ -337,4 +306,4 @@ const ProductCard = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductCard;
|
export default ProductCard;
|
||||||
@@ -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",
|
||||||
@@ -38,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Your cart is empty",
|
emptyCartTitle: "Your cart is empty",
|
||||||
emptyCartMessage: "Looks like you haven't added any items to your cart yet",
|
emptyCartMessage: "Looks like you haven't added any items to your cart yet",
|
||||||
continueShopping: "Continue Shopping",
|
continueShopping: "Continue Shopping",
|
||||||
|
pendingPriceTitle: "Price pending",
|
||||||
|
pendingPriceDesc: "The price of one or more items in this order has not yet been determined. Our operator will contact you to provide additional information.",
|
||||||
|
pendingPriceTooltipDesc: "The price of this item in the order has not been determined. The operator will call you and provide additional information."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Payment Method",
|
paymentMethod: "Payment Method",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
login: "Войти",
|
login: "Войти",
|
||||||
signUp: "Регистрация",
|
signUp: "Регистрация",
|
||||||
brands: "Бренды",
|
brands: "Бренды",
|
||||||
|
stores: "Магазины",
|
||||||
search: "Поиск по названию товара...",
|
search: "Поиск по названию товара...",
|
||||||
cart: "Корзина",
|
cart: "Корзина",
|
||||||
home: "Главная",
|
home: "Главная",
|
||||||
@@ -38,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Ваша корзина пуста",
|
emptyCartTitle: "Ваша корзина пуста",
|
||||||
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
|
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
|
||||||
continueShopping: "Продолжить покупки",
|
continueShopping: "Продолжить покупки",
|
||||||
|
pendingPriceTitle: "Цена уточняется",
|
||||||
|
pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.",
|
||||||
|
pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Способ оплаты",
|
paymentMethod: "Способ оплаты",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -38,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Sebediňiz boş",
|
emptyCartTitle: "Sebediňiz boş",
|
||||||
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
|
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
|
||||||
continueShopping: "Söwda etmegi dowam etdiriň",
|
continueShopping: "Söwda etmegi dowam etdiriň",
|
||||||
|
pendingPriceTitle: "Bahasyny anyklamaly",
|
||||||
|
pendingPriceDesc: "Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.",
|
||||||
|
pendingPriceTooltipDesc: "Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Töleg görnüşi",
|
paymentMethod: "Töleg görnüşi",
|
||||||
|
|||||||
@@ -16,10 +16,9 @@
|
|||||||
.cartHeader {
|
.cartHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
padding-bottom: 15px;
|
padding-top: 10px;
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -27,11 +26,11 @@
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cartProducts {
|
.cartProducts {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -152,6 +151,7 @@
|
|||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 720px) {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap:10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: static;
|
position: static;
|
||||||
margin-top: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -524,3 +524,106 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import React, { useState, useRef, useEffect, useMemo } from "react";
|
|||||||
import styles from "./CartPage.module.scss";
|
import styles from "./CartPage.module.scss";
|
||||||
import { FaTrashAlt } from "react-icons/fa";
|
import { FaTrashAlt } from "react-icons/fa";
|
||||||
import Checkout from "../../components/Checkout";
|
import Checkout from "../../components/Checkout";
|
||||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import EmptyCartState from "./emptyCart";
|
import EmptyCartState from "./emptyCart";
|
||||||
import {
|
import {
|
||||||
useGetCartQuery,
|
|
||||||
useAddToCartMutation,
|
|
||||||
useRemoveFromCartMutation,
|
useRemoveFromCartMutation,
|
||||||
useUpdateCartItemMutation,
|
useUpdateCartItemMutation,
|
||||||
useCleanCartMutation,
|
useCleanCartMutation,
|
||||||
@@ -16,10 +13,11 @@ import {
|
|||||||
import { useCart } from "../../app/api/useCart";
|
import { useCart } from "../../app/api/useCart";
|
||||||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
const stripHtml = (html) => {
|
const stripHtml = (html) => {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
return doc.body.textContent || "";
|
return doc.body.textContent || "";
|
||||||
@@ -32,11 +30,9 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
|||||||
<div className={styles.truncatedDescription}>
|
<div className={styles.truncatedDescription}>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: isExpanded
|
__html: shouldTruncate
|
||||||
? description
|
? description.substring(0, maxLength) + "..."
|
||||||
: shouldTruncate
|
: description,
|
||||||
? description.substring(0, maxLength) + "..."
|
|
||||||
: description,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,20 +40,16 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CartPage = () => {
|
const CartPage = () => {
|
||||||
const { cartData, cartItems, isLoading, isError, error } = useCart();
|
const { cartData, cartItems, isLoading } = useCart();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { t, i18n } = useTranslation();
|
|
||||||
const [checkoutStores, setCheckoutStores] = useState({});
|
const [checkoutStores, setCheckoutStores] = useState({});
|
||||||
const [addToCart] = useAddToCartMutation();
|
|
||||||
const [removeFromCart] = useRemoveFromCartMutation();
|
const [removeFromCart] = useRemoveFromCartMutation();
|
||||||
const [updateCartItem] = useUpdateCartItemMutation();
|
const [updateCartItem] = useUpdateCartItemMutation();
|
||||||
const [cleanCart] = useCleanCartMutation();
|
const [cleanCart] = useCleanCartMutation();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
const expandedRef = useRef(null);
|
|
||||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||||
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
|
|
||||||
const [localQuantities, setLocalQuantities] = useState({});
|
const [localQuantities, setLocalQuantities] = useState({});
|
||||||
const [pendingQuantities, setPendingQuantities] = useState({});
|
const [pendingQuantities, setPendingQuantities] = useState({});
|
||||||
const [loadingItems, setLoadingItems] = useState({});
|
const [loadingItems, setLoadingItems] = useState({});
|
||||||
@@ -70,43 +62,35 @@ const CartPage = () => {
|
|||||||
width: 400,
|
width: 400,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert grouped data to stores array
|
|
||||||
const stores = useMemo(() => {
|
const stores = useMemo(() => {
|
||||||
return Object.entries(cartData)
|
return Object.entries(cartData)
|
||||||
.map(([storeSlug, items]) => {
|
.map(([storeSlug, items]) => {
|
||||||
if (!items || !items.length) return null;
|
if (!items?.length) return null;
|
||||||
|
|
||||||
// Get store info from first item
|
|
||||||
const storeInfo = items[0]?.product?.channel?.[0];
|
const storeInfo = items[0]?.product?.channel?.[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: storeInfo?.id || storeSlug,
|
id: storeInfo?.id || storeSlug,
|
||||||
name: storeInfo?.name || storeSlug,
|
name: storeInfo?.name || storeSlug,
|
||||||
slug: storeSlug,
|
slug: storeSlug,
|
||||||
shipping_price: storeInfo?.shipping_price,
|
shipping_price: storeInfo?.shipping_price,
|
||||||
items: items,
|
items,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [cartData]);
|
}, [cartData]);
|
||||||
|
|
||||||
// ✅ Initialize local quantities from cart items
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newLocalQuantities = {};
|
const newLocal = {};
|
||||||
const newPendingQuantities = {};
|
const newPending = {};
|
||||||
|
|
||||||
cartItems.forEach((item) => {
|
cartItems.forEach((item) => {
|
||||||
const productId = item.product.id;
|
const id = item.product.id;
|
||||||
const quantity = parseInt(item.product_quantity, 10) || 0;
|
const qty = parseInt(item.product_quantity, 10) || 0;
|
||||||
newLocalQuantities[productId] = quantity;
|
newLocal[id] = qty;
|
||||||
newPendingQuantities[productId] = quantity;
|
newPending[id] = qty;
|
||||||
});
|
});
|
||||||
|
setLocalQuantities(newLocal);
|
||||||
setLocalQuantities(newLocalQuantities);
|
setPendingQuantities(newPending);
|
||||||
setPendingQuantities(newPendingQuantities);
|
|
||||||
}, [cartItems]);
|
}, [cartItems]);
|
||||||
|
|
||||||
// ✅ Debounced Cart Update - Her ürün için ayrı debounce
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timers = {};
|
const timers = {};
|
||||||
|
|
||||||
@@ -114,141 +98,94 @@ const CartPage = () => {
|
|||||||
const serverItem = cartItems.find(
|
const serverItem = cartItems.find(
|
||||||
(item) => String(item.product.id) === String(productId),
|
(item) => String(item.product.id) === String(productId),
|
||||||
);
|
);
|
||||||
const serverQuantity = serverItem
|
const serverQty = serverItem
|
||||||
? parseInt(serverItem.product_quantity, 10)
|
? parseInt(serverItem.product_quantity, 10)
|
||||||
: 0;
|
: 0;
|
||||||
const pendingQuantity = pendingQuantities[productId];
|
const pendingQty = pendingQuantities[productId];
|
||||||
|
|
||||||
// Değişiklik yoksa veya 0 ise (Delete modalı tetikler) bir şey yapma
|
|
||||||
if (
|
if (
|
||||||
pendingQuantity === undefined ||
|
pendingQty === undefined ||
|
||||||
pendingQuantity === serverQuantity ||
|
pendingQty === serverQty ||
|
||||||
pendingQuantity <= 0
|
pendingQty <= 0
|
||||||
) {
|
)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
timers[productId] = setTimeout(async () => {
|
timers[productId] = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
||||||
await updateCartItem({
|
await updateCartItem({ productId, quantity: pendingQty }).unwrap();
|
||||||
productId,
|
} catch {
|
||||||
quantity: pendingQuantity,
|
setLocalQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||||
}).unwrap();
|
setPendingQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update cart:", error);
|
|
||||||
// Hata durumunda rollback
|
|
||||||
setLocalQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: serverQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: serverQuantity,
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => Object.values(timers).forEach(clearTimeout);
|
||||||
Object.values(timers).forEach((timer) => clearTimeout(timer));
|
|
||||||
};
|
|
||||||
}, [pendingQuantities, cartItems, updateCartItem]);
|
}, [pendingQuantities, cartItems, updateCartItem]);
|
||||||
|
|
||||||
const handleQuantityIncrease = (productId) => (event) => {
|
const handleQuantityIncrease = (productId) => (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (loadingItems[productId]) return;
|
if (loadingItems[productId]) return;
|
||||||
|
|
||||||
const item = cartItems.find((item) => item.product.id === productId);
|
const item = cartItems.find((i) => i.product.id === productId);
|
||||||
if (!item) return;
|
if (!item || localQuantities[productId] >= item.product.stock) return;
|
||||||
|
|
||||||
if (localQuantities[productId] >= item.product.stock) {
|
const newQty = (localQuantities[productId] || 0) + 1;
|
||||||
return;
|
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
}
|
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
|
|
||||||
const newQuantity = (localQuantities[productId] || 0) + 1;
|
|
||||||
setLocalQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuantityDecrease = (productId) => (event) => {
|
const handleQuantityDecrease = (productId) => (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (loadingItems[productId]) return;
|
if (loadingItems[productId]) return;
|
||||||
|
|
||||||
const currentQuantity = localQuantities[productId] || 0;
|
const currentQty = localQuantities[productId] || 0;
|
||||||
|
if (currentQty <= 1) {
|
||||||
if (currentQuantity <= 1) {
|
|
||||||
showDeleteConfirm(productId);
|
showDeleteConfirm(productId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newQuantity = currentQuantity - 1;
|
const newQty = currentQty - 1;
|
||||||
setLocalQuantities((prev) => ({
|
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
...prev,
|
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateStoreTotal = (storeItems) => {
|
const getStoreShippingPrice = (store) =>
|
||||||
return storeItems.reduce((sum, item) => {
|
store.shipping_price != null ? parseFloat(store.shipping_price) : 20;
|
||||||
const itemPrice = parseFloat(item.product.price_amount) || 0;
|
|
||||||
const itemQuantity = parseInt(item.product_quantity, 10) || 0;
|
// Store içinde fiyatsız ürün var mı?
|
||||||
return sum + itemPrice * itemQuantity;
|
const storeHasZeroPriceItem = (storeItems) =>
|
||||||
|
storeItems.some((item) => isPriceZero(item.product.price_amount));
|
||||||
|
|
||||||
|
const calculateStoreTotal = (storeItems) =>
|
||||||
|
storeItems.reduce((sum, item) => {
|
||||||
|
return (
|
||||||
|
sum +
|
||||||
|
(parseFloat(item.product.price_amount) || 0) *
|
||||||
|
(parseInt(item.product_quantity, 10) || 0)
|
||||||
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
|
||||||
|
|
||||||
const getStoreShippingPrice = (store) => {
|
const handleCheckout = (storeId) =>
|
||||||
return store.shipping_price !== null && store.shipping_price !== undefined
|
|
||||||
? parseFloat(store.shipping_price)
|
|
||||||
: 20;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckout = (storeId) => {
|
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackToCart = (storeId) => {
|
const handleBackToCart = (storeId) =>
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||||
};
|
|
||||||
|
|
||||||
const handleOrderSubmit = async (storeId, storeItems) => {
|
const handleOrderSubmit = async (storeId) => {
|
||||||
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
||||||
const success = await checkoutRefs.current[storeId]();
|
const success = await checkoutRefs.current[storeId]();
|
||||||
if (success) {
|
if (success) setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
handleCheckout(storeId);
|
handleCheckout(storeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (expandedRef.current && !expandedRef.current.contains(event.target)) {
|
|
||||||
setIsExpanded(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const showDeleteConfirm = (productId) => {
|
const showDeleteConfirm = (productId) => {
|
||||||
setItemToDelete(productId);
|
setItemToDelete(productId);
|
||||||
setDeleteModalVisible(true);
|
setDeleteModalVisible(true);
|
||||||
@@ -258,48 +195,41 @@ const CartPage = () => {
|
|||||||
if (itemToDelete) {
|
if (itemToDelete) {
|
||||||
try {
|
try {
|
||||||
await removeFromCart({ productId: itemToDelete }).unwrap();
|
await removeFromCart({ productId: itemToDelete }).unwrap();
|
||||||
|
|
||||||
setLocalQuantities((prev) => {
|
setLocalQuantities((prev) => {
|
||||||
const newState = { ...prev };
|
const s = { ...prev };
|
||||||
delete newState[itemToDelete];
|
delete s[itemToDelete];
|
||||||
return newState;
|
return s;
|
||||||
});
|
});
|
||||||
setPendingQuantities((prev) => {
|
setPendingQuantities((prev) => {
|
||||||
const newState = { ...prev };
|
const s = { ...prev };
|
||||||
delete newState[itemToDelete];
|
delete s[itemToDelete];
|
||||||
return newState;
|
return s;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Failed to remove item:", error);
|
console.error("Failed to remove item:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDeleteModalVisible(false);
|
setDeleteModalVisible(false);
|
||||||
setItemToDelete(null);
|
setItemToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEmptyCartConfirm = () => {
|
|
||||||
setEmptyCartModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmptyCartConfirm = async () => {
|
const handleEmptyCartConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
await cleanCart().unwrap();
|
await cleanCart().unwrap();
|
||||||
|
|
||||||
setLocalQuantities({});
|
setLocalQuantities({});
|
||||||
setPendingQuantities({});
|
setPendingQuantities({});
|
||||||
setCheckoutStores({});
|
setCheckoutStores({});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Failed to clean cart:", error);
|
console.error("Failed to clean cart:", e);
|
||||||
}
|
}
|
||||||
setEmptyCartModalVisible(false);
|
setEmptyCartModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTotalItemCount = () => {
|
const getTotalItemCount = () =>
|
||||||
return cartItems.reduce(
|
cartItems.reduce(
|
||||||
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.cartContainer}>
|
<div className={styles.cartContainer}>
|
||||||
@@ -339,21 +269,20 @@ const CartPage = () => {
|
|||||||
<h2>
|
<h2>
|
||||||
{t("cart.basket")} ({getTotalItemCount()})
|
{t("cart.basket")} ({getTotalItemCount()})
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<button
|
||||||
<button
|
className={styles.deleteBtn}
|
||||||
className={styles.deleteBtn}
|
style={{ padding: "4px 12px" }}
|
||||||
style={{ padding: "4px 12px" }}
|
onClick={() => setEmptyCartModalVisible(true)}
|
||||||
onClick={showEmptyCartConfirm}
|
>
|
||||||
>
|
<FaTrashAlt /> {t("cart.clearCart")}
|
||||||
<FaTrashAlt /> {t("cart.clearCart")}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stores.map((store) => {
|
{stores.map((store) => {
|
||||||
const shippingPrice = getStoreShippingPrice(store);
|
const shippingPrice = getStoreShippingPrice(store);
|
||||||
const storeTotal = calculateStoreTotal(store.items);
|
const storeTotal = calculateStoreTotal(store.items);
|
||||||
const totalWithShipping = storeTotal + shippingPrice;
|
const totalWithShipping = storeTotal + shippingPrice;
|
||||||
|
const hasZeroPrice = storeHasZeroPriceItem(store.items);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={store.id} className={styles.storeSection}>
|
<div key={store.id} className={styles.storeSection}>
|
||||||
@@ -363,8 +292,8 @@ const CartPage = () => {
|
|||||||
shippingPrice={shippingPrice}
|
shippingPrice={shippingPrice}
|
||||||
productIds={store.items.map((item) => item.product.id)}
|
productIds={store.items.map((item) => item.product.id)}
|
||||||
onBackToCart={() => handleBackToCart(store.id)}
|
onBackToCart={() => handleBackToCart(store.id)}
|
||||||
onPlaceOrder={(placeOrderFn) => {
|
onPlaceOrder={(fn) => {
|
||||||
checkoutRefs.current[store.id] = placeOrderFn;
|
checkoutRefs.current[store.id] = fn;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -391,10 +320,9 @@ const CartPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.priceQuantity}>
|
<div className={styles.priceQuantity}>
|
||||||
<span className={styles.price}>
|
<span className={styles.price}>
|
||||||
{(
|
{isPriceZero(item.product.price_amount)
|
||||||
parseFloat(item.product.price_amount) || 0
|
? t("cart.pendingPriceTitle")
|
||||||
).toFixed(2)}{" "}
|
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
|
||||||
m.
|
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.quantityControls}>
|
<div className={styles.quantityControls}>
|
||||||
<button
|
<button
|
||||||
@@ -441,26 +369,38 @@ const CartPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ✅ Store Summary - fiyatsız ürün varsa "Baha anyklamak" */}
|
||||||
<div className={styles.storeSummary}>
|
<div className={styles.storeSummary}>
|
||||||
<div className={styles.cartContent}>
|
<div className={styles.cartContent}>
|
||||||
<h3>
|
<h3>
|
||||||
{store.name} - {t("cart.basket")}:
|
{store.name} - {t("cart.basket")}:
|
||||||
</h3>
|
</h3>
|
||||||
<div className={styles.summaryRow}>
|
{hasZeroPrice ? (
|
||||||
<span>{t("cart.price")}:</span>
|
<div className={styles.summaryRow}>
|
||||||
<span>{storeTotal.toFixed(2)} m.</span>
|
<span>{t("cart.total")}:</span>
|
||||||
</div>
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||||
<div className={styles.summaryRow}>
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
<span>{t("cart.delivery")}:</span>
|
</span>
|
||||||
<span>{shippingPrice.toFixed(2)} m.</span>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
<div className={styles.summaryRow}>
|
<>
|
||||||
<span>{t("cart.total")}:</span>
|
<div className={styles.summaryRow}>
|
||||||
<span>{totalWithShipping.toFixed(2)} m.</span>
|
<span>{t("cart.price")}:</span>
|
||||||
</div>
|
<span>{storeTotal.toFixed(2)} m.</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>{t("cart.delivery")}:</span>
|
||||||
|
<span>{shippingPrice.toFixed(2)} m.</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>{t("cart.total")}:</span>
|
||||||
|
<span>{totalWithShipping.toFixed(2)} m.</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOrderSubmit(store.id, store.items)}
|
onClick={() => handleOrderSubmit(store.id)}
|
||||||
className={styles.checkoutBtn}
|
className={styles.checkoutBtn}
|
||||||
>
|
>
|
||||||
{checkoutStores[store.id]
|
{checkoutStores[store.id]
|
||||||
@@ -472,7 +412,6 @@ const CartPage = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile sticky summary */}
|
{/* Mobile sticky summary */}
|
||||||
{/* <div className={styles.container}>
|
{/* <div className={styles.container}>
|
||||||
<div className={styles.summaryCard} ref={expandedRef}>
|
<div className={styles.summaryCard} ref={expandedRef}>
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceLabel {
|
.priceLabel {
|
||||||
@@ -144,7 +145,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
width: 85%;
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
}
|
}
|
||||||
@@ -498,3 +499,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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, useMemo, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
useGetCategoryProductsQuery,
|
|
||||||
useLazyGetAllCategoryProductsPaginatedQuery,
|
useLazyGetAllCategoryProductsPaginatedQuery,
|
||||||
|
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,
|
||||||
@@ -19,50 +21,14 @@ const useCategoryProducts = ({
|
|||||||
maxPrice,
|
maxPrice,
|
||||||
sorting,
|
sorting,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
initialProducts = [],
|
||||||
|
initialHasMore = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState(initialProducts);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(initialHasMore);
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
const isFetchingRef = useRef(false);
|
const activeRequestId = useRef(0);
|
||||||
const lastFetchKeyRef = useRef(null);
|
|
||||||
const abortControllerRef = useRef(null);
|
|
||||||
|
|
||||||
const contextId = useMemo(() => {
|
|
||||||
const parts = [
|
|
||||||
selectedFilterCategory && `fcat-${selectedFilterCategory}`,
|
|
||||||
categoryId && `cat-${categoryId}`,
|
|
||||||
brandId && `brand-${brandId}`,
|
|
||||||
collectionId && `col-${collectionId}`,
|
|
||||||
selectedFilterBrand && `fbrand-${selectedFilterBrand}`,
|
|
||||||
minPrice && `min-${minPrice}`,
|
|
||||||
maxPrice && `max-${maxPrice}`,
|
|
||||||
sorting && `sort-${sorting}`,
|
|
||||||
].filter(Boolean);
|
|
||||||
return parts.join("|") || "none";
|
|
||||||
}, [
|
|
||||||
selectedFilterCategory,
|
|
||||||
categoryId,
|
|
||||||
brandId,
|
|
||||||
collectionId,
|
|
||||||
selectedFilterBrand,
|
|
||||||
minPrice,
|
|
||||||
maxPrice,
|
|
||||||
sorting,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fetchParams = useMemo(
|
|
||||||
() => ({
|
|
||||||
page: currentPage,
|
|
||||||
limit: 24,
|
|
||||||
brands: selectedFilterBrand || undefined,
|
|
||||||
min_price: minPrice || undefined,
|
|
||||||
max_price: maxPrice || undefined,
|
|
||||||
sorting: sorting || undefined,
|
|
||||||
}),
|
|
||||||
[currentPage, selectedFilterBrand, minPrice, maxPrice, sorting],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchKey = `${contextId}-p${currentPage}`;
|
|
||||||
|
|
||||||
const shouldUseBaseQuery =
|
const shouldUseBaseQuery =
|
||||||
categoryId &&
|
categoryId &&
|
||||||
@@ -70,240 +36,181 @@ const useCategoryProducts = ({
|
|||||||
!searchQuery &&
|
!searchQuery &&
|
||||||
!selectedFilterCategory &&
|
!selectedFilterCategory &&
|
||||||
!brandId &&
|
!brandId &&
|
||||||
!collectionId;
|
!collectionId &&
|
||||||
|
!channelId;
|
||||||
|
|
||||||
const {
|
const { data: baseQueryData, isFetching: baseQueryFetching } =
|
||||||
data: paginatedCategoryProducts,
|
useGetCategoryProductsQuery(
|
||||||
isLoading: categoryLoading,
|
{
|
||||||
isFetching: categoryFetching,
|
categoryId,
|
||||||
} = useGetCategoryProductsQuery(
|
page: currentPage,
|
||||||
{
|
min_price: minPrice || undefined,
|
||||||
categoryId: categoryId,
|
max_price: maxPrice || undefined,
|
||||||
page: currentPage,
|
brands: selectedFilterBrand || undefined,
|
||||||
min_price: minPrice || undefined,
|
sorting: sorting || undefined,
|
||||||
max_price: maxPrice || undefined,
|
},
|
||||||
brands: selectedFilterBrand || undefined,
|
{ skip: !shouldUseBaseQuery }
|
||||||
sorting: sorting || undefined,
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
skip: !shouldUseBaseQuery,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
|
||||||
|
const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
|
||||||
|
const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
|
||||||
|
const [fetchChannelPaginated] = useLazyGetChannelProductsQuery();
|
||||||
|
|
||||||
|
// ✅ Ref'e al — dependency array'den çıkar, stale closure yok
|
||||||
|
const fetchersRef = useRef({});
|
||||||
|
fetchersRef.current = {
|
||||||
fetchCategoryPaginated,
|
fetchCategoryPaginated,
|
||||||
{
|
|
||||||
data: lazyCategoryProducts,
|
|
||||||
isLoading: lazyCategoryLoading,
|
|
||||||
isFetching: lazyCategoryFetching,
|
|
||||||
reset: resetCategoryPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetAllCategoryProductsPaginatedQuery();
|
|
||||||
|
|
||||||
const [
|
|
||||||
fetchBrandPaginated,
|
fetchBrandPaginated,
|
||||||
{
|
|
||||||
data: paginatedBrandProducts,
|
|
||||||
isLoading: brandPaginatedLoading,
|
|
||||||
isFetching: brandFetching,
|
|
||||||
reset: resetBrandPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetBrandProductsQuery();
|
|
||||||
|
|
||||||
const [
|
|
||||||
fetchCollectionPaginated,
|
fetchCollectionPaginated,
|
||||||
{
|
fetchChannelPaginated,
|
||||||
data: paginatedCollectionProducts,
|
};
|
||||||
isLoading: collectionPaginatedLoading,
|
|
||||||
isFetching: collectionFetching,
|
|
||||||
reset: resetCollectionPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetCollectionProductsPaginatedQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProducts([]);
|
if (!shouldUseBaseQuery || !baseQueryData) return;
|
||||||
setHasMore(true);
|
const data = baseQueryData.data || [];
|
||||||
|
const hasNextPage = !!baseQueryData.pagination?.next_page_url;
|
||||||
|
setProducts((prev) => {
|
||||||
|
if (currentPage === 1) return data;
|
||||||
|
const existingIds = new Set(prev.map((p) => p.id));
|
||||||
|
const newItems = data.filter((p) => !existingIds.has(p.id));
|
||||||
|
return newItems.length > 0 ? [...prev, ...newItems] : prev;
|
||||||
|
});
|
||||||
|
setHasMore(hasNextPage);
|
||||||
|
}, [baseQueryData, currentPage, shouldUseBaseQuery]);
|
||||||
|
|
||||||
resetCategoryPaginated?.();
|
|
||||||
resetBrandPaginated?.();
|
|
||||||
resetCollectionPaginated?.();
|
|
||||||
|
|
||||||
lastFetchKeyRef.current = null;
|
|
||||||
isFetchingRef.current = false;
|
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
abortControllerRef.current = null;
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
contextId,
|
|
||||||
resetCategoryPaginated,
|
|
||||||
resetBrandPaginated,
|
|
||||||
resetCollectionPaginated,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchQuery) return;
|
if (shouldUseBaseQuery || searchQuery) return;
|
||||||
|
|
||||||
if (lastFetchKeyRef.current === fetchKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFetchingRef.current) {
|
console.log("🔥 LAZY EFFECT TRIGGERED", {
|
||||||
return;
|
shouldUseBaseQuery,
|
||||||
}
|
categoryId,
|
||||||
|
collectionId,
|
||||||
|
brandId,
|
||||||
|
channelId,
|
||||||
|
isSubCategory,
|
||||||
|
selectedFilterCategory,
|
||||||
|
selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
const snapshot = {
|
||||||
abortControllerRef.current.abort();
|
currentPage,
|
||||||
}
|
selectedFilterCategory,
|
||||||
|
categoryId,
|
||||||
|
isSubCategory,
|
||||||
|
brandId,
|
||||||
|
collectionId,
|
||||||
|
channelId,
|
||||||
|
selectedFilterBrand,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
sorting,
|
||||||
|
};
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
const requestId = ++activeRequestId.current;
|
||||||
isFetchingRef.current = true;
|
setIsFetching(true);
|
||||||
lastFetchKeyRef.current = fetchKey;
|
|
||||||
|
|
||||||
const executeFetch = async () => {
|
const run = async () => {
|
||||||
try {
|
try {
|
||||||
if (selectedFilterCategory) {
|
const {
|
||||||
await fetchCategoryPaginated({
|
fetchCategoryPaginated,
|
||||||
category: {
|
fetchBrandPaginated,
|
||||||
id: selectedFilterCategory,
|
fetchCollectionPaginated,
|
||||||
children: [],
|
fetchChannelPaginated,
|
||||||
},
|
} = fetchersRef.current; // ✅ ref'ten oku
|
||||||
...fetchParams,
|
|
||||||
});
|
const params = {
|
||||||
|
page: snapshot.currentPage,
|
||||||
|
perPage: 12,
|
||||||
|
brands: snapshot.selectedFilterBrand || undefined,
|
||||||
|
min_price: snapshot.minPrice || undefined,
|
||||||
|
max_price: snapshot.maxPrice || undefined,
|
||||||
|
sorting: snapshot.sorting || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
if (snapshot.selectedFilterCategory) {
|
||||||
|
result = await fetchCategoryPaginated({
|
||||||
|
category: { id: snapshot.selectedFilterCategory, children: [] },
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.categoryId && snapshot.isSubCategory) {
|
||||||
|
result = await fetchCategoryPaginated({
|
||||||
|
category: { id: parseInt(snapshot.categoryId), children: [] },
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.brandId) {
|
||||||
|
result = await fetchBrandPaginated({
|
||||||
|
id: snapshot.brandId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.collectionId) {
|
||||||
|
result = await fetchCollectionPaginated({
|
||||||
|
collectionId: snapshot.collectionId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.channelId) {
|
||||||
|
result = await fetchChannelPaginated({
|
||||||
|
channelId: snapshot.channelId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestId !== activeRequestId.current) return;
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
setHasMore(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryId && isSubCategory) {
|
const data = result.data || [];
|
||||||
await fetchCategoryPaginated({
|
const hasNextPage =
|
||||||
category: {
|
result.pagination?.hasMorePages ||
|
||||||
id: parseInt(categoryId),
|
!!result.pagination?.next_page_url ||
|
||||||
children: [],
|
false;
|
||||||
},
|
|
||||||
...fetchParams,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (brandId) {
|
setProducts((prev) => {
|
||||||
await fetchBrandPaginated({
|
if (snapshot.currentPage === 1) return data;
|
||||||
id: brandId,
|
const existingIds = new Set(prev.map((p) => p.id));
|
||||||
...fetchParams,
|
const newItems = data.filter((p) => !existingIds.has(p.id));
|
||||||
});
|
return newItems.length > 0 ? [...prev, ...newItems] : prev;
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionId) {
|
setHasMore(data.length > 0 ? hasNextPage : false);
|
||||||
await fetchCollectionPaginated({
|
} catch (err) {
|
||||||
collectionId,
|
if (requestId !== activeRequestId.current) return;
|
||||||
...fetchParams,
|
console.error("Fetch error:", err);
|
||||||
});
|
setHasMore(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
console.error("Fetch error:", error);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingRef.current = false;
|
if (requestId === activeRequestId.current) {
|
||||||
|
setIsFetching(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
executeFetch();
|
run();
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [
|
}, [
|
||||||
fetchKey,
|
shouldUseBaseQuery,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
selectedFilterBrand,
|
currentPage,
|
||||||
selectedFilterCategory,
|
selectedFilterCategory,
|
||||||
categoryId,
|
categoryId,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
brandId,
|
brandId,
|
||||||
collectionId,
|
collectionId,
|
||||||
fetchParams,
|
channelId,
|
||||||
fetchCategoryPaginated,
|
selectedFilterBrand,
|
||||||
fetchBrandPaginated,
|
minPrice,
|
||||||
fetchCollectionPaginated,
|
maxPrice,
|
||||||
|
sorting,
|
||||||
|
// ✅ fetcher fonksiyonlar dependency'den tamamen çıktı
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
|
||||||
const updateProducts = (newData, hasNextPage) => {
|
|
||||||
if (!newData || newData.length === 0) {
|
|
||||||
if (currentPage === 1) {
|
|
||||||
setProducts([]);
|
|
||||||
setHasMore(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProducts((prev) => {
|
|
||||||
if (currentPage === 1) {
|
|
||||||
return newData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingIds = new Set(prev.map((p) => p.id));
|
|
||||||
const newProducts = newData.filter((p) => !existingIds.has(p.id));
|
|
||||||
|
|
||||||
return newProducts.length > 0 ? [...prev, ...newProducts] : prev;
|
|
||||||
});
|
|
||||||
|
|
||||||
setHasMore(hasNextPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (paginatedCategoryProducts && shouldUseBaseQuery) {
|
|
||||||
updateProducts(
|
|
||||||
paginatedCategoryProducts.data || [],
|
|
||||||
!!paginatedCategoryProducts.pagination?.next_page_url,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lazyCategoryProducts) {
|
|
||||||
updateProducts(
|
|
||||||
lazyCategoryProducts.data || [],
|
|
||||||
lazyCategoryProducts.pagination?.hasMorePages || false,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Brand products
|
|
||||||
if (paginatedBrandProducts) {
|
|
||||||
updateProducts(
|
|
||||||
paginatedBrandProducts.data || [],
|
|
||||||
!!paginatedBrandProducts.pagination?.next_page_url,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paginatedCollectionProducts) {
|
|
||||||
updateProducts(
|
|
||||||
paginatedCollectionProducts.data || [],
|
|
||||||
!!paginatedCollectionProducts.pagination?.next_page_url,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
paginatedCategoryProducts,
|
|
||||||
lazyCategoryProducts,
|
|
||||||
paginatedBrandProducts,
|
|
||||||
paginatedCollectionProducts,
|
|
||||||
currentPage,
|
|
||||||
shouldUseBaseQuery,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
categoryLoading ||
|
|
||||||
lazyCategoryLoading ||
|
|
||||||
brandPaginatedLoading ||
|
|
||||||
collectionPaginatedLoading ||
|
|
||||||
categoryFetching ||
|
|
||||||
lazyCategoryFetching ||
|
|
||||||
brandFetching ||
|
|
||||||
collectionFetching;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
products,
|
products,
|
||||||
@@ -315,3 +222,4 @@ const useCategoryProducts = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default useCategoryProducts;
|
export default useCategoryProducts;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useMemo, useRef } from "react";
|
import { useEffect, useState, useMemo, useRef } from "react";
|
||||||
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -21,22 +19,50 @@ 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 [pageState, setPageState] = useState({
|
const routeKey = useMemo(
|
||||||
|
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}-${channelId || "x"}`,
|
||||||
|
[categoryId, collectionId, brandId, channelId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSavedState = (key, defaultVal) => {
|
||||||
|
if (location.state?.clearFilters) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
|
||||||
|
if (saved) return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSavedStateByKey = (route, key) => {
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(`category_${key}_${route}`);
|
||||||
|
if (saved) return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [pageState, setPageState] = useState(() => getSavedState("pageState", {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
minPrice: "",
|
minPrice: "",
|
||||||
maxPrice: "",
|
maxPrice: "",
|
||||||
sorting: "",
|
sorting: "",
|
||||||
});
|
}));
|
||||||
|
|
||||||
const [filterState, setFilterState] = useState({
|
const [filterState, setFilterState] = useState(() => getSavedState("filterState", {
|
||||||
selectedFilterCategory: null,
|
selectedFilterCategory: null,
|
||||||
selectedFilterBrand: null,
|
selectedFilterBrand: null,
|
||||||
brandSearchQuery: "",
|
brandSearchQuery: "",
|
||||||
});
|
}));
|
||||||
|
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
@@ -47,11 +73,6 @@ const CategoryPage = () => {
|
|||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const routeKey = useMemo(
|
|
||||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
|
||||||
[categoryId, collectionId, brandId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const prevRouteRef = useRef(routeKey);
|
const prevRouteRef = useRef(routeKey);
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
|
|
||||||
@@ -67,6 +88,7 @@ const CategoryPage = () => {
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
filtersData,
|
filtersData,
|
||||||
collectionData,
|
collectionData,
|
||||||
|
channelData,
|
||||||
isLoading: dataLoading,
|
isLoading: dataLoading,
|
||||||
hasError: dataError,
|
hasError: dataError,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
@@ -74,6 +96,7 @@ const CategoryPage = () => {
|
|||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedFilterCategory: filterState.selectedFilterCategory,
|
selectedFilterCategory: filterState.selectedFilterCategory,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
});
|
});
|
||||||
@@ -87,6 +110,7 @@ const CategoryPage = () => {
|
|||||||
} = useCategoryProducts({
|
} = useCategoryProducts({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
channelId,
|
||||||
brandId,
|
brandId,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
@@ -97,6 +121,8 @@ const CategoryPage = () => {
|
|||||||
maxPrice: pageState.maxPrice,
|
maxPrice: pageState.maxPrice,
|
||||||
sorting: pageState.sorting,
|
sorting: pageState.sorting,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
initialProducts: getSavedState("products", []),
|
||||||
|
initialHasMore: getSavedState("hasMore", true),
|
||||||
});
|
});
|
||||||
const isMobilePhoneView =
|
const isMobilePhoneView =
|
||||||
(Number(categoryId) === 531 ||
|
(Number(categoryId) === 531 ||
|
||||||
@@ -106,21 +132,51 @@ const CategoryPage = () => {
|
|||||||
if (isInitialMount.current) {
|
if (isInitialMount.current) {
|
||||||
isInitialMount.current = false;
|
isInitialMount.current = false;
|
||||||
prevRouteRef.current = routeKey;
|
prevRouteRef.current = routeKey;
|
||||||
|
const savedScroll = getSavedState("scroll", 0);
|
||||||
|
if (savedScroll > 0) {
|
||||||
|
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevRouteRef.current === routeKey) return;
|
if (prevRouteRef.current === routeKey && !location.state?.clearFilters) return;
|
||||||
|
|
||||||
prevRouteRef.current = routeKey;
|
prevRouteRef.current = routeKey;
|
||||||
|
|
||||||
setAllProducts([]);
|
const shouldClear = location.state?.clearFilters;
|
||||||
setHasMore(true);
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
const savedPageState = shouldClear ? null : getSavedStateByKey(routeKey, "pageState");
|
||||||
setFilterState({
|
const savedFilterState = shouldClear ? null : getSavedStateByKey(routeKey, "filterState");
|
||||||
selectedFilterCategory: null,
|
const savedProducts = shouldClear ? null : getSavedStateByKey(routeKey, "products");
|
||||||
selectedFilterBrand: null,
|
const savedHasMore = shouldClear ? null : getSavedStateByKey(routeKey, "hasMore");
|
||||||
brandSearchQuery: "",
|
|
||||||
});
|
if (savedPageState && savedFilterState && savedProducts) {
|
||||||
|
setPageState(savedPageState);
|
||||||
|
setFilterState(savedFilterState);
|
||||||
|
setAllProducts(savedProducts);
|
||||||
|
setHasMore(savedHasMore ?? true);
|
||||||
|
const savedScroll = getSavedStateByKey(routeKey, "scroll");
|
||||||
|
if (savedScroll !== null) {
|
||||||
|
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (prevRouteRef.current !== routeKey) {
|
||||||
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
|
}
|
||||||
|
setPageState({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: "",
|
||||||
|
});
|
||||||
|
setFilterState({
|
||||||
|
selectedFilterCategory: null,
|
||||||
|
selectedFilterBrand: null,
|
||||||
|
brandSearchQuery: "",
|
||||||
|
});
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (location.state?.clearFilters) {
|
if (location.state?.clearFilters) {
|
||||||
navigate(location.pathname, { replace: true, state: {} });
|
navigate(location.pathname, { replace: true, state: {} });
|
||||||
@@ -134,6 +190,46 @@ const CategoryPage = () => {
|
|||||||
setHasMore,
|
setHasMore,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const stateRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
stateRef.current = { routeKey, pageState, filterState, allProducts, hasMore };
|
||||||
|
}, [routeKey, pageState, filterState, allProducts, hasMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (stateRef.current) {
|
||||||
|
try {
|
||||||
|
const { routeKey: key, pageState: ps, filterState: fs, allProducts: ap, hasMore: hm } = stateRef.current;
|
||||||
|
sessionStorage.setItem(`category_pageState_${key}`, JSON.stringify(ps));
|
||||||
|
sessionStorage.setItem(`category_filterState_${key}`, JSON.stringify(fs));
|
||||||
|
sessionStorage.setItem(`category_products_${key}`, JSON.stringify(ap));
|
||||||
|
sessionStorage.setItem(`category_hasMore_${key}`, JSON.stringify(hm));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not save category state to sessionStorage", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pageState, filterState, allProducts, hasMore, routeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let scrollTimeout;
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (scrollTimeout) clearTimeout(scrollTimeout);
|
||||||
|
scrollTimeout = setTimeout(() => {
|
||||||
|
if (stateRef.current) {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(`category_scroll_${stateRef.current.routeKey}`, JSON.stringify(window.scrollY));
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
if (scrollTimeout) clearTimeout(scrollTimeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
let list = searchQuery ? searchResults : allProducts;
|
let list = searchQuery ? searchResults : allProducts;
|
||||||
|
|
||||||
@@ -189,7 +285,12 @@ const CategoryPage = () => {
|
|||||||
selectedFilterBrand: null,
|
selectedFilterBrand: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
setPageState((prev) => ({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: prev.sorting,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
||||||
@@ -197,15 +298,15 @@ const CategoryPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterCategoryDeselect = () => {
|
const handleFilterCategoryDeselect = () => {
|
||||||
setFilterState((prev) => ({
|
setFilterState((prev) => ({ ...prev, selectedFilterCategory: null }));
|
||||||
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedFilterCategory: null,
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
||||||
if (categoryId) fetchFilters({ category_id: categoryId });
|
if (categoryId) fetchFilters({ category_id: categoryId });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,32 +316,29 @@ const CategoryPage = () => {
|
|||||||
selectedFilterBrand: brandId,
|
selectedFilterBrand: brandId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
setPageState((prev) => ({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: prev.sorting,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterBrandDeselect = () => {
|
const handleFilterBrandDeselect = () => {
|
||||||
setFilterState((prev) => ({
|
setFilterState((prev) => ({ ...prev, selectedFilterBrand: null }));
|
||||||
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedFilterBrand: null,
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryClick = (targetId) => {
|
const handleCategoryClick = (targetId) => {
|
||||||
setFilterState({
|
|
||||||
selectedFilterCategory: null,
|
|
||||||
selectedFilterBrand: null,
|
|
||||||
brandSearchQuery: "",
|
|
||||||
});
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
|
|
||||||
navigate(`/category/${targetId}`, {
|
navigate(`/category/${targetId}`, {
|
||||||
replace: false,
|
replace: false,
|
||||||
state: { clearFilters: true, timestamp: Date.now() },
|
state: { clearFilters: true, timestamp: Date.now() },
|
||||||
@@ -276,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
|
||||||
@@ -314,31 +429,22 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
setPageState((prev) => {
|
setAllProducts([]);
|
||||||
// Sadece aktif bir değer girilirse ürünleri sıfırla
|
setHasMore(true);
|
||||||
if (value !== "") {
|
setPageState((prev) => ({
|
||||||
setAllProducts([]);
|
...prev,
|
||||||
setHasMore(true);
|
minPrice: value,
|
||||||
}
|
currentPage: 1,
|
||||||
return {
|
}));
|
||||||
...prev,
|
|
||||||
minPrice: value,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
setPageState((prev) => {
|
setAllProducts([]);
|
||||||
if (value !== "") {
|
setHasMore(true);
|
||||||
setAllProducts([]);
|
setPageState((prev) => ({
|
||||||
setHasMore(true);
|
...prev,
|
||||||
}
|
maxPrice: value,
|
||||||
return {
|
currentPage: 1,
|
||||||
...prev,
|
}));
|
||||||
maxPrice: value,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onCategorySelect={handleFilterCategorySelect}
|
onCategorySelect={handleFilterCategorySelect}
|
||||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||||
@@ -351,16 +457,9 @@ const CategoryPage = () => {
|
|||||||
onSortingChange={(value) => {
|
onSortingChange={(value) => {
|
||||||
setPageState((prev) => {
|
setPageState((prev) => {
|
||||||
const newSorting = prev.sorting === value ? "" : value;
|
const newSorting = prev.sorting === value ? "" : value;
|
||||||
// Sadece aktif bir sort seçilirse ürünleri sıfırla
|
setAllProducts([]); // her zaman sıfırla
|
||||||
if (newSorting !== "") {
|
setHasMore(true);
|
||||||
setAllProducts([]);
|
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||||
setHasMore(true);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
sorting: newSorting,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -377,30 +476,22 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
setPageState((prev) => {
|
setAllProducts([]);
|
||||||
if (value !== "") {
|
setHasMore(true);
|
||||||
setAllProducts([]);
|
setPageState((prev) => ({
|
||||||
setHasMore(true);
|
...prev,
|
||||||
}
|
minPrice: value,
|
||||||
return {
|
currentPage: 1,
|
||||||
...prev,
|
}));
|
||||||
minPrice: value,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
setPageState((prev) => {
|
setAllProducts([]);
|
||||||
if (value !== "") {
|
setHasMore(true);
|
||||||
setAllProducts([]);
|
setPageState((prev) => ({
|
||||||
setHasMore(true);
|
...prev,
|
||||||
}
|
maxPrice: value,
|
||||||
return {
|
currentPage: 1,
|
||||||
...prev,
|
}));
|
||||||
maxPrice: value,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onCategorySelect={handleFilterCategorySelect}
|
onCategorySelect={handleFilterCategorySelect}
|
||||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||||
@@ -413,15 +504,9 @@ const CategoryPage = () => {
|
|||||||
onSortingChange={(value) => {
|
onSortingChange={(value) => {
|
||||||
setPageState((prev) => {
|
setPageState((prev) => {
|
||||||
const newSorting = prev.sorting === value ? "" : value;
|
const newSorting = prev.sorting === value ? "" : value;
|
||||||
if (newSorting) {
|
setAllProducts([]); // her zaman sıfırla
|
||||||
setAllProducts([]);
|
setHasMore(true);
|
||||||
setHasMore(true);
|
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||||
}
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
sorting: newSorting,
|
|
||||||
currentPage: 1,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -441,12 +526,10 @@ const CategoryPage = () => {
|
|||||||
next={loadMoreData}
|
next={loadMoreData}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
scrollThreshold={0.8}
|
scrollThreshold={0.8}
|
||||||
scrollableTarget={null}
|
scrollableTarget={null}
|
||||||
style={{ overflow: "hidden" }}
|
style={{ overflow: "hidden" }}
|
||||||
loader={
|
loader={
|
||||||
<div
|
<div className={`${styles.loaderContainer} `}>
|
||||||
className={`${styles.loaderContainer} `}
|
|
||||||
>
|
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
gap: 12px;
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@@ -157,10 +159,10 @@
|
|||||||
&:first-child {
|
&:first-child {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #000;
|
color: #000;
|
||||||
width: 25%;
|
// width: 25%;
|
||||||
@media screen and (max-width: 640px) {
|
// @media screen and (max-width: 640px) {
|
||||||
width: 50%;
|
// width: 50%;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -295,3 +297,106 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import styles from "./OrderDetail.module.scss";
|
import styles from "./OrderDetail.module.scss";
|
||||||
import { Ban, CircleCheck, X } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetOrderByIdQuery } from "../../app/api/orderApi"; // Update with your correct path
|
import { useGetOrderByIdQuery } from "../../app/api/orderApi";
|
||||||
import track from "../../assets/track.jpg"; // Keep for delivery service icon
|
import track from "../../assets/track.jpg";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const OrderDetail = () => {
|
const OrderDetail = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id } = useParams(); // Get the order ID from URL params
|
const { id } = useParams();
|
||||||
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Format date function
|
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
return new Date(dateString).toLocaleString("tk-TM", {
|
||||||
return date.toLocaleString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format delivery time for display
|
|
||||||
const formatDeliveryTime = (time, date) => {
|
const formatDeliveryTime = (time, date) => {
|
||||||
try {
|
try {
|
||||||
const deliveryDate = new Date(date);
|
const formatted = new Date(date).toLocaleDateString("tk-TM", {
|
||||||
const formattedDate = deliveryDate.toLocaleDateString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
return `${time} (${formattedDate})`;
|
return `${time} (${formatted})`;
|
||||||
} catch (e) {
|
} catch {
|
||||||
return `${time}`;
|
return time;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total order amount
|
|
||||||
const calculateTotal = (orderItems) => {
|
const calculateTotal = (orderItems) => {
|
||||||
if (!orderItems || !orderItems.length) return 0;
|
if (!orderItems?.length) return null;
|
||||||
|
const hasZero = orderItems.some((item) =>
|
||||||
|
isPriceZero(item.unit_price_amount),
|
||||||
|
);
|
||||||
|
if (hasZero) return null;
|
||||||
return orderItems
|
return orderItems
|
||||||
.reduce(
|
.reduce(
|
||||||
(sum, item) => sum + parseFloat(item.unit_price_amount) * item.quantity,
|
(sum, item) => sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
)
|
)
|
||||||
.toFixed(2);
|
.toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle loading state
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
// Handle error state
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
@@ -72,10 +72,8 @@ const OrderDetail = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle case where order data is not available
|
|
||||||
if (!orderData) return <div className={styles.notFound}>Order not found</div>;
|
if (!orderData) return <div className={styles.notFound}>Order not found</div>;
|
||||||
|
|
||||||
// Calculate total
|
|
||||||
const totalAmount = calculateTotal(orderData.orderItems);
|
const totalAmount = calculateTotal(orderData.orderItems);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,39 +82,10 @@ const OrderDetail = () => {
|
|||||||
<h1>
|
<h1>
|
||||||
{t("order.orderNumber")}: {orderData.id}
|
{t("order.orderNumber")}: {orderData.id}
|
||||||
</h1>
|
</h1>
|
||||||
<div className={styles.Buttons}>
|
<div className={styles.Buttons} />
|
||||||
{/* <button className={styles.repeatButton}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M480 256c-17.67 0-32 14.31-32 32c0 52.94-43.06 96-96 96H192L192 344c0-9.469-5.578-18.06-14.23-21.94C169.1 318.3 159 319.8 151.9 326.2l-80 72C66.89 402.7 64 409.2 64 416s2.891 13.28 7.938 17.84l80 72C156.4 509.9 162.2 512 168 512c3.312 0 6.615-.6875 9.756-2.062C186.4 506.1 192 497.5 192 488L192 448h160c88.22 0 160-71.78 160-160C512 270.3 497.7 256 480 256zM160 128h159.1L320 168c0 9.469 5.578 18.06 14.23 21.94C337.4 191.3 340.7 192 343.1 192c5.812 0 11.57-2.125 16.07-6.156l80-72C445.1 109.3 448 102.8 448 95.1s-2.891-13.28-7.938-17.84l-80-72c-7.047-6.312-17.19-7.875-25.83-4.094C325.6 5.938 319.1 14.53 319.1 24L320 64H160C71.78 64 0 135.8 0 224c0 17.69 14.33 32 32 32s32-14.31 32-32C64 171.1 107.1 128 160 128z"></path>
|
|
||||||
</svg>{" "}
|
|
||||||
{t("order.repeatOrder")}
|
|
||||||
</button> */}
|
|
||||||
{/* <button className={styles.cancelButton}>
|
|
||||||
{" "}
|
|
||||||
<Ban />
|
|
||||||
{t("order.dropOrder")}
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>
|
|
||||||
{/* Order Status */}
|
|
||||||
{/* <div className={styles.status}>
|
|
||||||
<p className={styles.statusText}>
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CircleCheck />
|
|
||||||
</span>{" "}
|
|
||||||
{t("order.Your_order_has_been_accepted")}
|
|
||||||
</p>
|
|
||||||
<span className={styles.close}>
|
|
||||||
<X />
|
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Order Details */}
|
<div className={styles.content}>
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div className={styles.rowContainer}>
|
<div className={styles.rowContainer}>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
@@ -132,7 +101,7 @@ const OrderDetail = () => {
|
|||||||
<span>
|
<span>
|
||||||
{formatDeliveryTime(
|
{formatDeliveryTime(
|
||||||
orderData.delivery_time,
|
orderData.delivery_time,
|
||||||
orderData.delivery_at
|
orderData.delivery_at,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,11 +113,26 @@ const OrderDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<span>{t("order.sum")}:</span>
|
<span>{t("order.sum")}:</span>
|
||||||
<span className={styles.total}>{totalAmount} m.</span>
|
<span className={styles.total}>
|
||||||
|
{totalAmount === null ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge t={t} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount} m.`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop table */}
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -165,9 +149,12 @@ const OrderDetail = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{orderData.orderItems.map((item, index) => {
|
{orderData.orderItems.map((item, index) => {
|
||||||
const product = item.product;
|
const product = item.product;
|
||||||
const itemTotal = (
|
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||||
parseFloat(item.unit_price_amount) * item.quantity
|
const itemTotal = zeroPriceItem
|
||||||
).toFixed(2);
|
? null
|
||||||
|
: (
|
||||||
|
parseFloat(item.unit_price_amount) * item.quantity
|
||||||
|
).toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
@@ -181,27 +168,50 @@ const OrderDetail = () => {
|
|||||||
<td>{product.name}</td>
|
<td>{product.name}</td>
|
||||||
<td>{product.brand || "-"}</td>
|
<td>{product.brand || "-"}</td>
|
||||||
<td>{product.id || "-"}</td>
|
<td>{product.id || "-"}</td>
|
||||||
<td>{item.unit_price_amount} m.</td>
|
<td>
|
||||||
|
{zeroPriceItem ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${item.unit_price_amount} m.`
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>{item.quantity}</td>
|
<td>{item.quantity}</td>
|
||||||
<td>{itemTotal} m.</td>
|
<td>
|
||||||
|
{itemTotal === null ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${itemTotal} m.`
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Add delivery service row if shipping method exists */}
|
|
||||||
{orderData.shipping_method && (
|
{orderData.shipping_method && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img
|
<img src={track} alt="Delivery" className={styles.image} />
|
||||||
src={track}
|
|
||||||
alt="Delivery Service"
|
|
||||||
className={styles.image}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>Eltip bermek hyzmaty</td>
|
<td>Eltip bermek hyzmaty</td>
|
||||||
<td>Beýleki</td>
|
<td>Beýleki</td>
|
||||||
<td>DELIVERY</td>
|
<td>DELIVERY</td>
|
||||||
<td>10.00 m.</td>{" "}
|
<td>10.00 m.</td>
|
||||||
{/* You may need to get actual delivery cost from API */}
|
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
<td>10.00 m.</td>
|
<td>10.00 m.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -210,13 +220,15 @@ const OrderDetail = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Mobile View */}
|
|
||||||
|
{/* Mobile cards */}
|
||||||
<div className={styles.productList}>
|
<div className={styles.productList}>
|
||||||
{orderData.orderItems.map((item, index) => {
|
{orderData.orderItems.map((item, index) => {
|
||||||
const product = item.product;
|
const product = item.product;
|
||||||
const itemTotal = (
|
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||||
parseFloat(item.unit_price_amount) * item.quantity
|
const itemTotal = zeroPriceItem
|
||||||
).toFixed(2);
|
? null
|
||||||
|
: (parseFloat(item.unit_price_amount) * item.quantity).toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card} key={index}>
|
<div className={styles.card} key={index}>
|
||||||
@@ -233,18 +245,30 @@ const OrderDetail = () => {
|
|||||||
{t("order.quantity")}: {item.quantity}
|
{t("order.quantity")}: {item.quantity}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.price}>
|
<span className={styles.price}>
|
||||||
{item.unit_price_amount} m.
|
{zeroPriceItem ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${item.unit_price_amount} m.`
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Add delivery service card if shipping method exists */}
|
|
||||||
{orderData.shipping_method && (
|
{/* {orderData.shipping_method && (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<img src={track} alt="Delivery Service" />
|
<img src={track} alt="Delivery" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailsMobile}>
|
<div className={styles.detailsMobile}>
|
||||||
<h3 className={styles.title}>Beýleki</h3>
|
<h3 className={styles.title}>Beýleki</h3>
|
||||||
@@ -257,7 +281,7 @@ const OrderDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// SargytlarymComponent.module.scss
|
// SargytlarymComponent.module.scss
|
||||||
.container {
|
.container {
|
||||||
padding: 15px 24px 0 24px;
|
padding: 15px 24px 24px 24px;
|
||||||
max-width: 1366px;
|
max-width: 1366px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
a{
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
@@ -121,3 +121,106 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #888888;
|
color: #888888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
// Orders.jsx
|
import React, { useState } from "react";
|
||||||
import React from "react";
|
|
||||||
import styles from "./Orders.module.scss";
|
import styles from "./Orders.module.scss";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetOrdersQuery } from "../../app/api/orderApi"; // Update with your correct path
|
import { useGetOrdersQuery } from "../../app/api/orderApi";
|
||||||
import EmptyOrderState from "./emptyOrder"; // Import the EmptyOrderState component
|
import EmptyOrderState from "./emptyOrder";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
|
const orderHasZeroPrice = (orderItems) =>
|
||||||
|
orderItems?.some((item) => isPriceZero(item.unit_price_amount));
|
||||||
|
|
||||||
const Orders = () => {
|
const Orders = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Function to format date - implement this or use a library like date-fns
|
|
||||||
const formatOrderDate = (dateString) => {
|
const formatOrderDate = (dateString) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
return new Date(dateString).toLocaleString("tk-TM", {
|
||||||
return date.toLocaleString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
// Handle error state
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
@@ -45,16 +49,13 @@ const Orders = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle empty orders - render EmptyOrderState component
|
if (!orders || orders.length === 0) return <EmptyOrderState />;
|
||||||
if (!orders || orders.length === 0) {
|
|
||||||
return <EmptyOrderState />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2 className={styles.title}>Sargytlarym</h2>
|
<h2 className={styles.title}>Sargytlarym</h2>
|
||||||
|
|
||||||
{/* Desktop table view */}
|
{/* Desktop table */}
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -69,11 +70,11 @@ const Orders = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
// Calculate total order amount
|
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||||
const totalAmount = order.orderItems.reduce(
|
const totalAmount = order.orderItems.reduce(
|
||||||
(sum, item) =>
|
(sum, item) =>
|
||||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +82,19 @@ const Orders = () => {
|
|||||||
<td>{order.id}</td>
|
<td>{order.id}</td>
|
||||||
<td>{formatOrderDate(order.delivery_at)}</td>
|
<td>{formatOrderDate(order.delivery_at)}</td>
|
||||||
<td style={{ color: "#888888", fontWeight: "700" }}>
|
<td style={{ color: "#888888", fontWeight: "700" }}>
|
||||||
{totalAmount.toFixed(2)} m.
|
{hasZeroPrice ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount.toFixed(2)} m.`
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{order.payment_type}</td>
|
<td>{order.payment_type}</td>
|
||||||
<td>{order.status}</td>
|
<td>{order.status}</td>
|
||||||
@@ -99,50 +112,72 @@ const Orders = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile card view */}
|
{/* Mobile cards */}
|
||||||
<div className={styles.Mobilecontainer}>
|
<div className={styles.Mobilecontainer}>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
|
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||||
const totalAmount = order.orderItems.reduce(
|
const totalAmount = order.orderItems.reduce(
|
||||||
(sum, item) =>
|
(sum, item) =>
|
||||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/orderdetail/${order.id}`} key={order.id}>
|
<div
|
||||||
<div className={styles.orderCard}>
|
key={order.id}
|
||||||
<div className={styles.orderRow}>
|
className={styles.orderCard}
|
||||||
<span className={styles.label}>
|
onClick={(e) => {
|
||||||
{t("order.orderNumber")}:
|
// Modal veya badge içerisine tıklandığında yönlendirmeyi engelle
|
||||||
</span>
|
if (
|
||||||
<span className={styles.value}>{order.id}</span>
|
e.target.closest(`.${styles.pendingPriceBadgeWrapper}`) ||
|
||||||
</div>
|
e.target.closest(".ant-modal-root") ||
|
||||||
<div className={styles.orderRow}>
|
e.target.closest(".ant-modal-wrap")
|
||||||
<span className={styles.label}>{t("order.orderDate")}:</span>
|
) {
|
||||||
<span className={styles.value}>
|
return;
|
||||||
{formatOrderDate(order.delivery_at)}
|
}
|
||||||
</span>
|
navigate(`/orderdetail/${order.id}`);
|
||||||
</div>
|
}}
|
||||||
<div className={styles.orderRow}>
|
style={{ cursor: "pointer" }}
|
||||||
<span className={styles.label}>{t("order.sum")}:</span>
|
>
|
||||||
<span className={styles.total}>
|
<div className={styles.orderRow}>
|
||||||
{totalAmount.toFixed(2)} m.
|
<span className={styles.label}>{t("order.orderNumber")}:</span>
|
||||||
</span>
|
<span className={styles.value}>{order.id}</span>
|
||||||
</div>
|
|
||||||
<div className={styles.orderRow}>
|
|
||||||
<span className={styles.label}>
|
|
||||||
{t("checkout.paymentMethod")}:
|
|
||||||
</span>
|
|
||||||
<span className={styles.value}>{order.payment_type}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.orderRow}>
|
|
||||||
<span className={styles.label}>
|
|
||||||
{t("order.orderStatus")}:
|
|
||||||
</span>
|
|
||||||
<span className={styles.value}>{order.status}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
<div className={styles.orderRow}>
|
||||||
|
<span className={styles.label}>{t("order.orderDate")}:</span>
|
||||||
|
<span className={styles.value}>
|
||||||
|
{formatOrderDate(order.delivery_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.orderRow}>
|
||||||
|
<span className={styles.label}>{t("order.sum")}:</span>
|
||||||
|
<span className={styles.total}>
|
||||||
|
{hasZeroPrice ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount.toFixed(2)} m.`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.orderRow}>
|
||||||
|
<span className={styles.label}>
|
||||||
|
{t("checkout.paymentMethod")}:
|
||||||
|
</span>
|
||||||
|
<span className={styles.value}>{order.payment_type}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.orderRow}>
|
||||||
|
<span className={styles.label}>{t("order.orderStatus")}:</span>
|
||||||
|
<span className={styles.value}>{order.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -27,6 +27,9 @@ import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
|
|||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
import { div } from "framer-motion/client";
|
import { div } from "framer-motion/client";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const ProductPage = ({
|
const ProductPage = ({
|
||||||
productProp,
|
productProp,
|
||||||
@@ -66,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();
|
||||||
@@ -322,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}
|
||||||
@@ -363,12 +430,23 @@ 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.map((prop, index) => (
|
||||||
|
<div key={`${prop.attribute_id}-${index}`} className={styles.metaItem}>
|
||||||
|
<span className={styles.metaLabel}>{prop.name}</span>
|
||||||
|
<span className={styles.metaValue}>{prop.value}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -396,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 && (
|
||||||
@@ -426,11 +509,19 @@ const ProductPage = ({
|
|||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<span className={styles.priceLabel}>{t("product.price")}:</span>
|
<span className={styles.priceLabel}>{t("product.price")}:</span>
|
||||||
<div className={styles.priceRight}>
|
<div className={styles.priceRight}>
|
||||||
<span className={styles.price}>{product.price_amount} m.</span>
|
{isPriceZero(product.price_amount) ? (
|
||||||
{product.old_price_amount && (
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||||
<span className={styles.oldPrice}>
|
<PendingPriceBadge />
|
||||||
{product.old_price_amount} m.
|
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.price}>{product.price_amount} m.</span>
|
||||||
|
{product.old_price_amount && (
|
||||||
|
<span className={styles.oldPrice}>
|
||||||
|
{product.old_price_amount} m.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -444,11 +535,19 @@ const ProductPage = ({
|
|||||||
{/* ── Mobile sticky bar ── */}
|
{/* ── Mobile sticky bar ── */}
|
||||||
<div className={styles.productActionsMobile}>
|
<div className={styles.productActionsMobile}>
|
||||||
<div className={styles.mobilePriceContainer}>
|
<div className={styles.mobilePriceContainer}>
|
||||||
<span className={styles.price}>{product.price_amount} m.</span>
|
{isPriceZero(product.price_amount) ? (
|
||||||
{product.old_price_amount && (
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||||
<span className={styles.oldPrice}>
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
{product.old_price_amount} m.
|
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.price}>{product.price_amount} m.</span>
|
||||||
|
{product.old_price_amount && (
|
||||||
|
<span className={styles.oldPrice}>
|
||||||
|
{product.old_price_amount} m.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.mobileBtnContainer}>
|
<div className={styles.mobileBtnContainer}>
|
||||||
|
|||||||
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;
|
||||||
@@ -3,6 +3,7 @@ import InfiniteScroll from "react-infinite-scroll-component";
|
|||||||
import CategorySection from "../../components/CategorySection/index";
|
import CategorySection from "../../components/CategorySection/index";
|
||||||
import Carousel from "../../components/Banner/index";
|
import Carousel from "../../components/Banner/index";
|
||||||
import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel";
|
import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel";
|
||||||
|
import HomeBrands from "../../components/HomeBrands/index";
|
||||||
import FlashSales from "../../components/FlashSales";
|
import FlashSales from "../../components/FlashSales";
|
||||||
import styles from "./Home.module.scss";
|
import styles from "./Home.module.scss";
|
||||||
import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
|
import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
|
||||||
@@ -98,6 +99,7 @@ const Home = () => {
|
|||||||
<div className={styles.home}>
|
<div className={styles.home}>
|
||||||
<Carousel />
|
<Carousel />
|
||||||
<CategoryCarousel />
|
<CategoryCarousel />
|
||||||
|
<HomeBrands />
|
||||||
<FlashSales />
|
<FlashSales />
|
||||||
<div className={styles.sections}>
|
<div className={styles.sections}>
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
|
|||||||
@@ -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