added baha anyklmak, attributes
This commit is contained in:
@@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({
|
||||
getBrands: builder.query({
|
||||
query: (params = {}) => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.type) {
|
||||
queryParams.append("type", params.type);
|
||||
}
|
||||
if (params.page) {
|
||||
queryParams.append("page", params.page);
|
||||
}
|
||||
if (params.limit) {
|
||||
queryParams.append("limit", params.limit);
|
||||
}
|
||||
|
||||
if (params.type) queryParams.append("type", params.type);
|
||||
if (params.page) queryParams.append("page", params.page);
|
||||
if (params.limit) queryParams.append("limit", params.limit);
|
||||
const queryString = queryParams.toString();
|
||||
return `/brands${queryString ? `?${queryString}` : ""}`;
|
||||
},
|
||||
@@ -28,30 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({
|
||||
|
||||
getBrandProducts: builder.query({
|
||||
query: (params) => {
|
||||
if (typeof params === 'string' || typeof params === 'number') {
|
||||
if (typeof params === "string" || typeof params === "number") {
|
||||
return `/brands/${params}/products`;
|
||||
}
|
||||
|
||||
const { id, page = 1, limit, sorting, min_price, max_price, brands } = params;
|
||||
let url = `/brands/${id}/products?page=${page}`;
|
||||
const { id, page = 1, limit = 24, sorting, min_price, max_price } = params;
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.append("page", page);
|
||||
urlParams.append("limit", limit);
|
||||
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) {
|
||||
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;
|
||||
return `/brands/${id}/products?${urlParams.toString()}`;
|
||||
},
|
||||
transformResponse: (response) => ({
|
||||
data: response.data || response,
|
||||
|
||||
@@ -7,15 +7,14 @@ export const categoriesApi = baseApi.injectEndpoints({
|
||||
}),
|
||||
|
||||
getCategoryProducts: builder.query({
|
||||
query: ({ categoryId, page = 1, limit, brands, min_price, max_price, sorting }) => {
|
||||
query: ({ categoryId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page);
|
||||
if (limit) params.append('limit', limit);
|
||||
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);
|
||||
|
||||
params.append("page", page);
|
||||
params.append("limit", limit);
|
||||
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 `categories/${categoryId}/products?${params.toString()}`;
|
||||
},
|
||||
transformResponse: (response) => ({
|
||||
@@ -25,78 +24,103 @@ export const categoriesApi = baseApi.injectEndpoints({
|
||||
}),
|
||||
|
||||
getAllCategoryProducts: builder.query({
|
||||
async queryFn(category, queryApi, extraOptions, baseQuery) {
|
||||
async queryFn(category, _queryApi, _extraOptions, baseQuery) {
|
||||
const fetchProducts = async (categoryId) => {
|
||||
const result = await baseQuery(`categories/${categoryId}/products`);
|
||||
return result.data ? result.data.data : [];
|
||||
};
|
||||
|
||||
let allProducts = await fetchProducts(category.id);
|
||||
|
||||
for (const child of category.children) {
|
||||
const childProducts = await fetchProducts(child.id);
|
||||
allProducts = [...allProducts, ...childProducts];
|
||||
}
|
||||
|
||||
return { data: allProducts };
|
||||
},
|
||||
}),
|
||||
|
||||
getAllCategoryProductsPaginated: builder.query({
|
||||
async queryFn(
|
||||
{ category, page = 1, limit = 6, brands, min_price, max_price, sorting },
|
||||
queryApi,
|
||||
extraOptions,
|
||||
{ category, page = 1, limit = 24, brands, min_price, max_price, sorting },
|
||||
_queryApi,
|
||||
_extraOptions,
|
||||
baseQuery
|
||||
) {
|
||||
if (!category) return { data: [] };
|
||||
if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } };
|
||||
|
||||
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];
|
||||
if (category.children && category.children.length > 0) {
|
||||
if (category.children?.length > 0) {
|
||||
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("limit", limit);
|
||||
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(
|
||||
(hasMore) => hasMore
|
||||
const result = await baseQuery(
|
||||
`categories/${categoryIds[0]}/products?${params.toString()}`
|
||||
);
|
||||
|
||||
if (result.error) return { error: result.error };
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: productsForPage,
|
||||
data: result.data?.data || [],
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
hasMorePages: hasMorePages,
|
||||
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("limit", limit);
|
||||
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 {
|
||||
data: {
|
||||
data: allProducts,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
hasMorePages,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,33 +12,28 @@ export const collectionsApi = baseApi.injectEndpoints({
|
||||
|
||||
getCollectionProducts: builder.query({
|
||||
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||
transformResponse: (response) => {
|
||||
return {
|
||||
transformResponse: (response) => ({
|
||||
data: response.data || [],
|
||||
isEmpty: !response.data || response.data.length === 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
checkCollectionHasProducts: builder.query({
|
||||
query: (collectionId) => `/collections/${collectionId}/products?limit=1`,
|
||||
transformResponse: (response) => {
|
||||
return {
|
||||
transformResponse: (response) => ({
|
||||
hasProducts: response.data && response.data.length > 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
getCollectionProductsPaginated: builder.query({
|
||||
query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price, sorting = "price_amount-ascending" }) => {
|
||||
query: ({ collectionId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page);
|
||||
if (limit) params.append('limit', limit);
|
||||
if (brands) params.append('brands', brands);
|
||||
if (min_price) params.append('min_price', min_price);
|
||||
if (max_price) params.append('max_price', max_price);
|
||||
params.append('sorting', sorting);
|
||||
|
||||
params.append("page", page);
|
||||
params.append("limit", limit);
|
||||
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); // undefined gelirse gönderme
|
||||
return `/collections/${collectionId}/products?${params.toString()}`;
|
||||
},
|
||||
transformResponse: (response) => ({
|
||||
|
||||
@@ -1,235 +1,272 @@
|
||||
// DropdownMenu.module.scss
|
||||
|
||||
.dropdownContainer {
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TRIGGER BUTTON ----
|
||||
.navButton {
|
||||
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;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
padding: 0.25rem 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
background-color: transparent;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
&.navButtonActive {
|
||||
background-color: #e63946;
|
||||
color: #ffffff;
|
||||
|
||||
svg {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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: relative;
|
||||
}
|
||||
|
||||
.dropdownPanel {
|
||||
position: absolute;
|
||||
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;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
animation: slideDown 0.18s ease;
|
||||
}
|
||||
|
||||
.categoriesList {
|
||||
flex: 1;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #ebe7eb;
|
||||
padding: 20px;
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PANEL SHELL ----
|
||||
.dropdownPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
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);
|
||||
}
|
||||
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 6px;
|
||||
// }
|
||||
// ---- LEFT LIST ----
|
||||
.categoriesList {
|
||||
width: 270px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||
padding: 10px 0;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.title {
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
&:active {
|
||||
color: #888888;
|
||||
}
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
justify-content: space-between;
|
||||
padding: 9px 16px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border: 1px solid #3615371a;
|
||||
border-radius: 6px;
|
||||
color: #000;
|
||||
transition: background-color 0.12s, color 0.12s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #000;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: #888888;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// ---- RIGHT CONTENT PANEL ----
|
||||
.contentPanel {
|
||||
flex: 3;
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: hidden;
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 6px;
|
||||
// }
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
// border-radius: 3px;
|
||||
}
|
||||
.title {
|
||||
cursor: pointer;
|
||||
color: #361517;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 2;
|
||||
text-align: left;
|
||||
|
||||
.panelTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
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 {
|
||||
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;
|
||||
color: #361517;
|
||||
padding: 4px 0;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #888888;
|
||||
color: #e63946;
|
||||
}
|
||||
}
|
||||
|
||||
.subCategoriesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nestedCategoryContainer:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nestedCategoryContainer {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nestedCategoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
.leafItem {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
padding: 3px 0;
|
||||
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 {
|
||||
background-color: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryLabel {
|
||||
flex: 1;
|
||||
.navButtonLoading {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
|
||||
.categoryIcon {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingDots {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
span {
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
.expandButton,
|
||||
.navigateButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.nestedChildren {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.noSubcategories {
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
@keyframes dotPulse {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
@@ -1,83 +1,62 @@
|
||||
// DropdownMenu.jsx
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import styles from "./DropdownMenu.module.scss";
|
||||
import { useGetCategoriesQuery } from "../../app/api/categories";
|
||||
import { CategoryIcon } from "../Icons";
|
||||
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
|
||||
|
||||
const NestedCategory = ({
|
||||
category,
|
||||
level = 0,
|
||||
handleCategorySelect,
|
||||
closeDropdown,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasChildren = category.children && category.children.length > 0;
|
||||
const ContentPanel = ({ category, onSelect, onClose }) => {
|
||||
if (!category) return null;
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
setIsExpanded(!isExpanded);
|
||||
} else {
|
||||
handleCategorySelect(category);
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
const children = category.children || [];
|
||||
const withChildren = children.filter((c) => c.children?.length > 0);
|
||||
const withoutChildren = children.filter((c) => !c.children?.length);
|
||||
|
||||
const handleDirectNavigation = (e) => {
|
||||
e.stopPropagation();
|
||||
handleCategorySelect(category);
|
||||
closeDropdown();
|
||||
};
|
||||
const allColumns = [
|
||||
...withChildren,
|
||||
...withoutChildren.map((c) => ({ ...c, children: [] })),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.nestedCategoryContainer}
|
||||
style={{ paddingLeft: `${level * 16}px` }}
|
||||
>
|
||||
<div className={styles.nestedCategoryItem} onClick={handleClick}>
|
||||
<div className={styles.categoryLabel}>
|
||||
<span className={styles.title}>{category.name}</span>
|
||||
</div>
|
||||
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.expandButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
<div className={styles.contentPanel}>
|
||||
<h2
|
||||
className={styles.panelTitle}
|
||||
onClick={() => {
|
||||
onSelect(category);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{category.name}
|
||||
</h2>
|
||||
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.navigateButton}
|
||||
onClick={handleDirectNavigation}
|
||||
title="Go to category"
|
||||
{allColumns.length > 0 && (
|
||||
<div className={styles.columnsGrid}>
|
||||
{allColumns.map((sub) => (
|
||||
<div key={sub.id} className={styles.columnSection}>
|
||||
<div
|
||||
className={styles.sectionTitle}
|
||||
onClick={() => {
|
||||
onSelect(sub);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
)}
|
||||
{sub.name}
|
||||
</div>
|
||||
{sub.children?.map((leaf) => (
|
||||
<span
|
||||
key={leaf.id}
|
||||
className={styles.leafItem}
|
||||
onClick={() => {
|
||||
onSelect(leaf);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{leaf.name}
|
||||
</span>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
@@ -89,113 +68,85 @@ const DropdownMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const {
|
||||
data: categoriesData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGetCategoriesQuery("tree");
|
||||
|
||||
const categories = categoriesData?.data || [];
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeMainCategory, setActiveMainCategory] = useState(null);
|
||||
const [activeCategory, setActiveCategory] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (categories.length > 0) {
|
||||
const defaultCategory =
|
||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
||||
setActiveMainCategory(defaultCategory);
|
||||
if (categories.length > 0 && !activeCategory) {
|
||||
setActiveCategory(categories[0]);
|
||||
}
|
||||
}, [categories]);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (categories.length > 0) {
|
||||
const defaultCategory =
|
||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
||||
setActiveMainCategory(defaultCategory);
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, []);
|
||||
|
||||
const handleCategorySelect = (category) => {
|
||||
const handleSelect = (category) => {
|
||||
navigate(`/category/${category.id}`, { state: { category } });
|
||||
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 (
|
||||
<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 />
|
||||
{t("navbar.category")}
|
||||
{isLoading
|
||||
? <div className={styles.loadingDots}><span/><span/><span/></div>
|
||||
: t("navbar.category")
|
||||
}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className={styles.overlay} onClick={() => setIsOpen(false)} />
|
||||
|
||||
<div className={styles.dropdownWrapper}>
|
||||
<div className={styles.dropdownPanel} onMouseLeave={handleMouseLeave}>
|
||||
<div className={styles.dropdownPanel}>
|
||||
<div className={styles.categoriesList}>
|
||||
{categories.map((category) => (
|
||||
{categories.map((cat) => (
|
||||
<div
|
||||
key={category.id}
|
||||
key={cat.id}
|
||||
className={`${styles.categoryItem} ${
|
||||
activeMainCategory?.id === category.id ? styles.active : ""
|
||||
activeCategory?.id === cat.id ? styles.active : ""
|
||||
}`}
|
||||
onMouseEnter={() => setActiveMainCategory(category)}
|
||||
onClick={() => handleCategorySelect(category)}
|
||||
onMouseEnter={() => setActiveCategory(cat)}
|
||||
onClick={() => handleSelect(cat)}
|
||||
>
|
||||
<span className={styles.title}>{category.name}</span>
|
||||
<span className={styles.title}>{cat.name}</span>
|
||||
{cat.children?.length > 0 && (
|
||||
<ChevronRight size={14} className={styles.chevron} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeMainCategory && (
|
||||
<div className={styles.contentPanel}>
|
||||
<h2
|
||||
onClick={() => handleCategorySelect(activeMainCategory)}
|
||||
className={styles.title}
|
||||
>
|
||||
{activeMainCategory.name}
|
||||
</h2>
|
||||
|
||||
<div className={styles.subCategoriesContainer}>
|
||||
{activeMainCategory.children &&
|
||||
activeMainCategory.children.length > 0 ? (
|
||||
activeMainCategory.children.map((subcategory) => (
|
||||
<NestedCategory
|
||||
key={subcategory.id}
|
||||
category={subcategory}
|
||||
handleCategorySelect={handleCategorySelect}
|
||||
closeDropdown={() => setIsOpen(false)}
|
||||
<ContentPanel
|
||||
category={activeCategory}
|
||||
onSelect={handleSelect}
|
||||
onClose={() => setIsOpen(false)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.noSubcategories}>
|
||||
{/* No subcategories available */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
92
src/components/HomeBrands/HomeBrands.module.scss
Normal file
92
src/components/HomeBrands/HomeBrands.module.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
.container {
|
||||
max-width: 1336px;
|
||||
margin: 20px auto;
|
||||
width: 100%;
|
||||
@media screen and (max-width: 1023px) {
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.brandsScroll {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
|
||||
/* Hide scrollbar for Webkit */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for Firefox, IE, Edge */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.brandCard {
|
||||
flex: 0 0 auto;
|
||||
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%;
|
||||
}
|
||||
|
||||
.allButton {
|
||||
flex: 0 0 auto;
|
||||
width: 122px;
|
||||
height: 67.6px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 4px;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.brandCard, .allButton {
|
||||
width: 100px;
|
||||
// height: 79.2px;
|
||||
}
|
||||
}
|
||||
77
src/components/HomeBrands/index.jsx
Normal file
77
src/components/HomeBrands/index.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBrandsQuery } from '../../app/api/brandsApi';
|
||||
import styles from './HomeBrands.module.scss';
|
||||
import { Logo } from '../Icons';
|
||||
import { IoIosArrowForward } from 'react-icons/io';
|
||||
|
||||
const HomeBrands = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
// We fetch a larger amount so we have enough to shuffle.
|
||||
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 50 });
|
||||
|
||||
const randomBrands = useMemo(() => {
|
||||
if (!brandsData) return [];
|
||||
// Create a shallow copy and shuffle it
|
||||
const shuffled = [...brandsData].sort(() => 0.5 - Math.random());
|
||||
// Pick the first 9 brands
|
||||
return shuffled.slice(0, 8);
|
||||
}, [brandsData]);
|
||||
|
||||
if (isLoading || !brandsData || brandsData.length === 0) return null;
|
||||
|
||||
// "Еще" in ru, "Hemmesi" in tm, "More" in en
|
||||
const getMoreText = () => {
|
||||
const lang = i18n.language;
|
||||
if (lang === 'ru') return 'Еще';
|
||||
if (lang === 'en') return 'More';
|
||||
return 'Hemmesi';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.brandsScroll}>
|
||||
{randomBrands.map((brand) => (
|
||||
<div
|
||||
key={brand.id}
|
||||
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>
|
||||
))}
|
||||
<div
|
||||
className={styles.allButton}
|
||||
onClick={() => navigate('/brands')}
|
||||
>
|
||||
<span>{getMoreText()}</span>
|
||||
<IoIosArrowForward />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeBrands;
|
||||
@@ -221,7 +221,7 @@ export const CategoryIcon = () => (
|
||||
height={20}
|
||||
>
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
&__satyjy {
|
||||
@media screen and (max-width: 500px) {
|
||||
@media screen and (max-width: 785px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -288,7 +288,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
@media screen and (max-width: 500px) {
|
||||
@media screen and (max-width: 708px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -201,9 +201,9 @@ const ProductCard = ({
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className={styles.imageContainer}>
|
||||
{(product.discount || calculatedDiscount) && (
|
||||
{(product.discount > 0 || calculatedDiscount > 0) && (
|
||||
<span className={styles.discountBadge}>
|
||||
-{product.discount ?? calculatedDiscount}%
|
||||
-{product.discount || calculatedDiscount}%
|
||||
</span>
|
||||
)}
|
||||
{product.stock === 0 && (
|
||||
@@ -221,7 +221,7 @@ const ProductCard = ({
|
||||
<div className={styles.priceContainer}>
|
||||
<div>
|
||||
{isPriceZero(price_amount) ? (
|
||||
<span className={styles.currentPrice}>Bahasyny anyklamaly</span>
|
||||
<span className={styles.currentPrice}> {t("cart.pendingPriceTitle")}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
emptyCartTitle: "Your cart is empty",
|
||||
emptyCartMessage: "Looks like you haven't added any items to your cart yet",
|
||||
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: {
|
||||
paymentMethod: "Payment Method",
|
||||
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
emptyCartTitle: "Ваша корзина пуста",
|
||||
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
|
||||
continueShopping: "Продолжить покупки",
|
||||
pendingPriceTitle: "Цена уточняется",
|
||||
pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.",
|
||||
pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию."
|
||||
},
|
||||
checkout: {
|
||||
paymentMethod: "Способ оплаты",
|
||||
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
emptyCartTitle: "Sebediňiz boş",
|
||||
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
|
||||
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: {
|
||||
paymentMethod: "Töleg görnüşi",
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cartProducts {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -152,6 +151,7 @@
|
||||
@media screen and (max-width: 720px) {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
gap:10px;
|
||||
}
|
||||
|
||||
.price {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
@@ -320,7 +321,7 @@ const CartPage = () => {
|
||||
<div className={styles.priceQuantity}>
|
||||
<span className={styles.price}>
|
||||
{isPriceZero(item.product.price_amount)
|
||||
? "Bahasyny anyklamaly"
|
||||
? t("cart.pendingPriceTitle")
|
||||
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
|
||||
</span>
|
||||
<div className={styles.quantityControls}>
|
||||
@@ -375,20 +376,12 @@ const CartPage = () => {
|
||||
{store.name} - {t("cart.basket")}:
|
||||
</h3>
|
||||
{hasZeroPrice ? (
|
||||
<>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.price")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.delivery")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.total")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.summaryRow}>
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
}
|
||||
|
||||
.priceLabel {
|
||||
@@ -144,7 +145,7 @@
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
width: 85%;
|
||||
&::placeholder {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
useGetCategoryProductsQuery,
|
||||
useLazyGetAllCategoryProductsPaginatedQuery,
|
||||
useGetCategoryProductsQuery,
|
||||
} from "../../../app/api/categories";
|
||||
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
|
||||
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
|
||||
@@ -19,50 +19,16 @@ const useCategoryProducts = ({
|
||||
maxPrice,
|
||||
sorting,
|
||||
searchQuery,
|
||||
initialProducts = [],
|
||||
initialHasMore = true,
|
||||
}) => {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [products, setProducts] = useState(initialProducts);
|
||||
const [hasMore, setHasMore] = useState(initialHasMore);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
||||
const isFetchingRef = useRef(false);
|
||||
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 activeRequestId = useRef(0);
|
||||
// Tüm parametreleri ref'te tut — stale closure'ı tamamen engelle
|
||||
const paramsRef = useRef({});
|
||||
|
||||
const shouldUseBaseQuery =
|
||||
categoryId &&
|
||||
@@ -72,238 +38,146 @@ const useCategoryProducts = ({
|
||||
!brandId &&
|
||||
!collectionId;
|
||||
|
||||
const {
|
||||
data: paginatedCategoryProducts,
|
||||
isLoading: categoryLoading,
|
||||
isFetching: categoryFetching,
|
||||
} = useGetCategoryProductsQuery(
|
||||
const { data: baseQueryData, isFetching: baseQueryFetching } =
|
||||
useGetCategoryProductsQuery(
|
||||
{
|
||||
categoryId: categoryId,
|
||||
categoryId,
|
||||
page: currentPage,
|
||||
min_price: minPrice || undefined,
|
||||
max_price: maxPrice || undefined,
|
||||
brands: selectedFilterBrand || undefined,
|
||||
sorting: sorting || undefined,
|
||||
},
|
||||
{
|
||||
skip: !shouldUseBaseQuery,
|
||||
},
|
||||
{ skip: !shouldUseBaseQuery }
|
||||
);
|
||||
|
||||
const [
|
||||
fetchCategoryPaginated,
|
||||
{
|
||||
data: lazyCategoryProducts,
|
||||
isLoading: lazyCategoryLoading,
|
||||
isFetching: lazyCategoryFetching,
|
||||
reset: resetCategoryPaginated,
|
||||
},
|
||||
] = useLazyGetAllCategoryProductsPaginatedQuery();
|
||||
|
||||
const [
|
||||
fetchBrandPaginated,
|
||||
{
|
||||
data: paginatedBrandProducts,
|
||||
isLoading: brandPaginatedLoading,
|
||||
isFetching: brandFetching,
|
||||
reset: resetBrandPaginated,
|
||||
},
|
||||
] = useLazyGetBrandProductsQuery();
|
||||
|
||||
const [
|
||||
fetchCollectionPaginated,
|
||||
{
|
||||
data: paginatedCollectionProducts,
|
||||
isLoading: collectionPaginatedLoading,
|
||||
isFetching: collectionFetching,
|
||||
reset: resetCollectionPaginated,
|
||||
},
|
||||
] = useLazyGetCollectionProductsPaginatedQuery();
|
||||
const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
|
||||
const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
|
||||
const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
|
||||
|
||||
// Base query handler
|
||||
useEffect(() => {
|
||||
setProducts([]);
|
||||
setHasMore(true);
|
||||
|
||||
resetCategoryPaginated?.();
|
||||
resetBrandPaginated?.();
|
||||
resetCollectionPaginated?.();
|
||||
|
||||
lastFetchKeyRef.current = null;
|
||||
isFetchingRef.current = false;
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, [
|
||||
contextId,
|
||||
resetCategoryPaginated,
|
||||
resetBrandPaginated,
|
||||
resetCollectionPaginated,
|
||||
]);
|
||||
if (!shouldUseBaseQuery || !baseQueryData) return;
|
||||
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]);
|
||||
|
||||
// Her fetch çağrısını doğrudan effect içinde yap — useCallback kaldırıldı
|
||||
useEffect(() => {
|
||||
if (searchQuery) return;
|
||||
if (shouldUseBaseQuery || searchQuery) return;
|
||||
|
||||
if (lastFetchKeyRef.current === fetchKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFetchingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
isFetchingRef.current = true;
|
||||
lastFetchKeyRef.current = fetchKey;
|
||||
|
||||
const executeFetch = async () => {
|
||||
try {
|
||||
if (selectedFilterCategory) {
|
||||
await fetchCategoryPaginated({
|
||||
category: {
|
||||
id: selectedFilterCategory,
|
||||
children: [],
|
||||
},
|
||||
...fetchParams,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryId && isSubCategory) {
|
||||
await fetchCategoryPaginated({
|
||||
category: {
|
||||
id: parseInt(categoryId),
|
||||
children: [],
|
||||
},
|
||||
...fetchParams,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (brandId) {
|
||||
await fetchBrandPaginated({
|
||||
id: brandId,
|
||||
...fetchParams,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (collectionId) {
|
||||
await fetchCollectionPaginated({
|
||||
collectionId,
|
||||
...fetchParams,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== "AbortError") {
|
||||
console.error("Fetch error:", error);
|
||||
}
|
||||
} finally {
|
||||
isFetchingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
executeFetch();
|
||||
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
fetchKey,
|
||||
searchQuery,
|
||||
selectedFilterBrand,
|
||||
// Parametreleri snapshot al
|
||||
const snapshot = {
|
||||
currentPage,
|
||||
selectedFilterCategory,
|
||||
categoryId,
|
||||
isSubCategory,
|
||||
brandId,
|
||||
collectionId,
|
||||
fetchParams,
|
||||
selectedFilterBrand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
sorting,
|
||||
};
|
||||
|
||||
const requestId = ++activeRequestId.current;
|
||||
setIsFetching(true);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const params = {
|
||||
page: snapshot.currentPage,
|
||||
limit: 24,
|
||||
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();
|
||||
}
|
||||
|
||||
if (requestId !== activeRequestId.current) return;
|
||||
|
||||
if (!result) {
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.data || [];
|
||||
const hasNextPage =
|
||||
result.pagination?.hasMorePages ||
|
||||
!!result.pagination?.next_page_url ||
|
||||
false;
|
||||
|
||||
setProducts((prev) => {
|
||||
if (snapshot.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(data.length > 0 ? hasNextPage : false);
|
||||
} catch (err) {
|
||||
if (requestId !== activeRequestId.current) return;
|
||||
console.error("Fetch error:", err);
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
if (requestId === activeRequestId.current) {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, [
|
||||
// useCallback YOK — her dependency değişince effect direkt çalışır
|
||||
shouldUseBaseQuery,
|
||||
searchQuery,
|
||||
currentPage,
|
||||
selectedFilterCategory,
|
||||
categoryId,
|
||||
isSubCategory,
|
||||
brandId,
|
||||
collectionId,
|
||||
selectedFilterBrand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
sorting,
|
||||
fetchCategoryPaginated,
|
||||
fetchBrandPaginated,
|
||||
fetchCollectionPaginated,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
|
||||
|
||||
return {
|
||||
products,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo, useRef } from "react";
|
||||
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,18 +23,43 @@ const CategoryPage = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [pageState, setPageState] = useState({
|
||||
const routeKey = useMemo(
|
||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
||||
[categoryId, collectionId, brandId],
|
||||
);
|
||||
|
||||
const getSavedState = (key, 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,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
sorting: "",
|
||||
});
|
||||
}));
|
||||
|
||||
const [filterState, setFilterState] = useState({
|
||||
const [filterState, setFilterState] = useState(() => getSavedState("filterState", {
|
||||
selectedFilterCategory: null,
|
||||
selectedFilterBrand: null,
|
||||
brandSearchQuery: "",
|
||||
});
|
||||
}));
|
||||
|
||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
@@ -47,11 +70,6 @@ const CategoryPage = () => {
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
const routeKey = useMemo(
|
||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
||||
[categoryId, collectionId, brandId],
|
||||
);
|
||||
|
||||
const prevRouteRef = useRef(routeKey);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
@@ -97,6 +115,8 @@ const CategoryPage = () => {
|
||||
maxPrice: pageState.maxPrice,
|
||||
sorting: pageState.sorting,
|
||||
searchQuery,
|
||||
initialProducts: getSavedState("products", []),
|
||||
initialHasMore: getSavedState("hasMore", true),
|
||||
});
|
||||
const isMobilePhoneView =
|
||||
(Number(categoryId) === 531 ||
|
||||
@@ -106,6 +126,10 @@ const CategoryPage = () => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
prevRouteRef.current = routeKey;
|
||||
const savedScroll = getSavedState("scroll", 0);
|
||||
if (savedScroll > 0) {
|
||||
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,14 +137,36 @@ const CategoryPage = () => {
|
||||
|
||||
prevRouteRef.current = routeKey;
|
||||
|
||||
const savedPageState = getSavedStateByKey(routeKey, "pageState");
|
||||
const savedFilterState = getSavedStateByKey(routeKey, "filterState");
|
||||
const savedProducts = getSavedStateByKey(routeKey, "products");
|
||||
const savedHasMore = getSavedStateByKey(routeKey, "hasMore");
|
||||
|
||||
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 {
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setPageState({
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
sorting: "",
|
||||
});
|
||||
setFilterState({
|
||||
selectedFilterCategory: null,
|
||||
selectedFilterBrand: null,
|
||||
brandSearchQuery: "",
|
||||
});
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
if (location.state?.clearFilters) {
|
||||
navigate(location.pathname, { replace: true, state: {} });
|
||||
@@ -134,6 +180,46 @@ const CategoryPage = () => {
|
||||
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(() => {
|
||||
let list = searchQuery ? searchResults : allProducts;
|
||||
|
||||
@@ -189,7 +275,12 @@ const CategoryPage = () => {
|
||||
selectedFilterBrand: null,
|
||||
}));
|
||||
|
||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
||||
setPageState((prev) => ({
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
sorting: prev.sorting,
|
||||
}));
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
|
||||
@@ -197,15 +288,15 @@ const CategoryPage = () => {
|
||||
};
|
||||
|
||||
const handleFilterCategoryDeselect = () => {
|
||||
setFilterState((prev) => ({
|
||||
setFilterState((prev) => ({ ...prev, selectedFilterCategory: null }));
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
selectedFilterCategory: null,
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
}));
|
||||
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
|
||||
if (categoryId) fetchFilters({ category_id: categoryId });
|
||||
};
|
||||
|
||||
@@ -215,32 +306,29 @@ const CategoryPage = () => {
|
||||
selectedFilterBrand: brandId,
|
||||
}));
|
||||
|
||||
setPageState((prev) => ({ currentPage: 1, minPrice: "", maxPrice: "", sorting: prev.sorting }));
|
||||
setPageState((prev) => ({
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
sorting: prev.sorting,
|
||||
}));
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
};
|
||||
|
||||
const handleFilterBrandDeselect = () => {
|
||||
setFilterState((prev) => ({
|
||||
setFilterState((prev) => ({ ...prev, selectedFilterBrand: null }));
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
selectedFilterBrand: null,
|
||||
currentPage: 1,
|
||||
minPrice: "",
|
||||
maxPrice: "",
|
||||
}));
|
||||
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
};
|
||||
|
||||
const handleCategoryClick = (targetId) => {
|
||||
setFilterState({
|
||||
selectedFilterCategory: null,
|
||||
selectedFilterBrand: null,
|
||||
brandSearchQuery: "",
|
||||
});
|
||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
|
||||
navigate(`/category/${targetId}`, {
|
||||
replace: false,
|
||||
state: { clearFilters: true, timestamp: Date.now() },
|
||||
@@ -314,31 +402,22 @@ const CategoryPage = () => {
|
||||
minPrice={pageState.minPrice}
|
||||
maxPrice={pageState.maxPrice}
|
||||
onMinPriceChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
// Sadece aktif bir değer girilirse ürünleri sıfırla
|
||||
if (value !== "") {
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
minPrice: value,
|
||||
currentPage: 1,
|
||||
};
|
||||
});
|
||||
}));
|
||||
}}
|
||||
onMaxPriceChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
if (value !== "") {
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
maxPrice: value,
|
||||
currentPage: 1,
|
||||
};
|
||||
});
|
||||
}));
|
||||
}}
|
||||
onCategorySelect={handleFilterCategorySelect}
|
||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||
@@ -351,16 +430,9 @@ const CategoryPage = () => {
|
||||
onSortingChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
const newSorting = prev.sorting === value ? "" : value;
|
||||
// Sadece aktif bir sort seçilirse ürünleri sıfırla
|
||||
if (newSorting !== "") {
|
||||
setAllProducts([]);
|
||||
setAllProducts([]); // her zaman sıfırla
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
sorting: newSorting,
|
||||
currentPage: 1,
|
||||
};
|
||||
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -377,30 +449,22 @@ const CategoryPage = () => {
|
||||
minPrice={pageState.minPrice}
|
||||
maxPrice={pageState.maxPrice}
|
||||
onMinPriceChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
if (value !== "") {
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
minPrice: value,
|
||||
currentPage: 1,
|
||||
};
|
||||
});
|
||||
}));
|
||||
}}
|
||||
onMaxPriceChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
if (value !== "") {
|
||||
setAllProducts([]);
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
maxPrice: value,
|
||||
currentPage: 1,
|
||||
};
|
||||
});
|
||||
}));
|
||||
}}
|
||||
onCategorySelect={handleFilterCategorySelect}
|
||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||
@@ -413,15 +477,9 @@ const CategoryPage = () => {
|
||||
onSortingChange={(value) => {
|
||||
setPageState((prev) => {
|
||||
const newSorting = prev.sorting === value ? "" : value;
|
||||
if (newSorting) {
|
||||
setAllProducts([]);
|
||||
setAllProducts([]); // her zaman sıfırla
|
||||
setHasMore(true);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
sorting: newSorting,
|
||||
currentPage: 1,
|
||||
};
|
||||
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -444,9 +502,7 @@ const CategoryPage = () => {
|
||||
scrollableTarget={null}
|
||||
style={{ overflow: "hidden" }}
|
||||
loader={
|
||||
<div
|
||||
className={`${styles.loaderContainer} `}
|
||||
>
|
||||
<div className={`${styles.loaderContainer} `}>
|
||||
<Loader />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,79 +1,14 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import styles from "./OrderDetail.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetOrderByIdQuery } from "../../app/api/orderApi";
|
||||
import track from "../../assets/track.jpg";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import { Result, Button, Modal } from "antd";
|
||||
import { Result, Button } from "antd";
|
||||
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
okText={t ? t("common.ok") : "OK"}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
centered
|
||||
title="Bahasy anyklamaly"
|
||||
className="pending-price-modal"
|
||||
width={400}
|
||||
>
|
||||
<p>
|
||||
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 = ({ t }) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMobile) setModalVisible(true);
|
||||
};
|
||||
|
||||
const stopPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<span onClick={stopPropagation}>
|
||||
<span
|
||||
className={styles.pendingPriceBadgeWrapper}
|
||||
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
onClick={handleClick}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span className={styles.pendingPriceBadge}>!</span>
|
||||
|
||||
{tooltipVisible && (
|
||||
<span className={styles.pendingPriceTooltip}>
|
||||
<strong>Bahasyny anyklamaly</strong>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const OrderDetail = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
@@ -242,7 +177,7 @@ const OrderDetail = () => {
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
<PendingPriceBadge />
|
||||
</span>
|
||||
) : (
|
||||
`${item.unit_price_amount} m.`
|
||||
@@ -258,7 +193,7 @@ const OrderDetail = () => {
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
<PendingPriceBadge />
|
||||
</span>
|
||||
) : (
|
||||
`${itemTotal} m.`
|
||||
@@ -318,7 +253,7 @@ const OrderDetail = () => {
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
<PendingPriceBadge />
|
||||
</span>
|
||||
) : (
|
||||
`${item.unit_price_amount} m.`
|
||||
|
||||
@@ -5,79 +5,15 @@ import { useTranslation } from "react-i18next";
|
||||
import { useGetOrdersQuery } from "../../app/api/orderApi";
|
||||
import EmptyOrderState from "./emptyOrder";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import { Result, Button, Modal } 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 orderHasZeroPrice = (orderItems) =>
|
||||
orderItems?.some((item) => isPriceZero(item.unit_price_amount));
|
||||
|
||||
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
okText={t("common.ok")}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
centered
|
||||
title="Bahasy anyklamaly"
|
||||
className="pending-price-modal"
|
||||
width={400}
|
||||
>
|
||||
<p>
|
||||
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 = ({ t }) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMobile) setModalVisible(true);
|
||||
};
|
||||
|
||||
const stopPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<span onClick={stopPropagation}>
|
||||
<span
|
||||
className={styles.pendingPriceBadgeWrapper}
|
||||
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
onClick={handleClick}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span className={styles.pendingPriceBadge}>!</span>
|
||||
|
||||
{tooltipVisible && (
|
||||
<span className={styles.pendingPriceTooltip}>
|
||||
<strong>Bahasyny anyklamaly</strong>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const Orders = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
||||
@@ -154,7 +90,7 @@ const Orders = () => {
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Bahasyny anyklamaly <PendingPriceBadge t={t} />
|
||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||
</span>
|
||||
) : (
|
||||
`${totalAmount.toFixed(2)} m.`
|
||||
@@ -224,7 +160,7 @@ const Orders = () => {
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Bahasyny anyklamaly <PendingPriceBadge t={t} />
|
||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||
</span>
|
||||
) : (
|
||||
`${totalAmount.toFixed(2)} m.`
|
||||
|
||||
@@ -27,6 +27,9 @@ import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import { Result, Button } from "antd";
|
||||
import { div } from "framer-motion/client";
|
||||
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const ProductPage = ({
|
||||
productProp,
|
||||
@@ -370,6 +373,15 @@ const ProductPage = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
{/* Description card */}
|
||||
@@ -426,12 +438,20 @@ const ProductPage = ({
|
||||
<div className={styles.priceRow}>
|
||||
<span className={styles.priceLabel}>{t("product.price")}:</span>
|
||||
<div className={styles.priceRight}>
|
||||
{isPriceZero(product.price_amount) ? (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||
</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>
|
||||
|
||||
@@ -444,12 +464,20 @@ const ProductPage = ({
|
||||
{/* ── Mobile sticky bar ── */}
|
||||
<div className={styles.productActionsMobile}>
|
||||
<div className={styles.mobilePriceContainer}>
|
||||
{isPriceZero(product.price_amount) ? (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||
</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 className={styles.mobileBtnContainer}>
|
||||
<CartButtons />
|
||||
|
||||
@@ -3,6 +3,7 @@ import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import CategorySection from "../../components/CategorySection/index";
|
||||
import Carousel from "../../components/Banner/index";
|
||||
import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel";
|
||||
import HomeBrands from "../../components/HomeBrands/index";
|
||||
import FlashSales from "../../components/FlashSales";
|
||||
import styles from "./Home.module.scss";
|
||||
import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
|
||||
@@ -98,6 +99,7 @@ const Home = () => {
|
||||
<div className={styles.home}>
|
||||
<Carousel />
|
||||
<CategoryCarousel />
|
||||
<HomeBrands />
|
||||
<FlashSales />
|
||||
<div className={styles.sections}>
|
||||
<InfiniteScroll
|
||||
|
||||
Reference in New Issue
Block a user