Compare commits

..

5 Commits

Author SHA1 Message Date
@jcarymuhammedow
8f3079801b fixed some bugs 2026-05-02 14:28:50 +05:00
Jelaletdin12
684ab6917d added store to navbar 2026-04-30 23:17:44 +05:00
@jcarymuhammedow
6cdde96c61 added channels products 2026-04-30 16:15:29 +05:00
Jelaletdin12
9419ec0af0 added baha anyklmak, attributes 2026-04-30 10:44:08 +05:00
Jelaletdin12
cc89455967 added baha tassyklamak 2026-04-26 22:07:09 +05:00
37 changed files with 2486 additions and 1366 deletions

View File

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

View File

@@ -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`,
}), }),

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

View File

@@ -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) => ({

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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: "Способ оплаты",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,143 @@
.storesContainer {
padding: 1rem 4.4rem;
max-width: 1336px;
margin: 0 auto;
@media screen and (max-width: 1023px) {
padding: 1rem 1.25rem;
}
}
.searchWrapper {
margin-bottom: 24px;
display: flex;
align-items: center;
height: 40px;
svg {
position: absolute;
width: 24px;
height: 24px;
transform: translateX(35%);
color: #9ca3af;
}
input {
width: 46%;
height: 38px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
padding-left: 40px;
outline: none;
@media screen and (max-width: 1023px) {
width: 100%;
}
&::placeholder {
color: #9ca3af;
}
}
}
.categorySection {
margin-bottom: 40px;
h2 {
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
color: #111827;
cursor: pointer;
&:hover {
color: #aaaaaa;
}
}
}
.storesGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 180px));
gap: 16px;
@media screen and (max-width: 1023px) {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
}
@media screen and (max-width: 900px) {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 5px;
}
@media screen and (max-width: 798px) {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 5px;
}
}
.storeCard {
background: white;
border-radius: 8px;
padding: 16px;
text-align: center;
transition: all 0.2s ease;
border: 1px solid #e5e7eb;
cursor: pointer;
@media screen and (max-width: 900px) {
padding: 5px;
}
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.imageWrapper {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
position: relative;
aspect-ratio: 4/3;
img {
height: 100%;
object-fit:contain;
}
}
.placeholder {
width: 80px;
height: 80px;
background: #f3f4f6;
border-radius: 8px;
}
.storeName {
font-size: 16px;
color: #374151;
margin: 0;
}
}
.skeleton {
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.logoFallback {
display: flex;
justify-content: center;
align-items: center;
width: 120px;
height: 100%;
border-radius: 4px;
object-fit: contain;
}

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

@@ -0,0 +1,202 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import InfiniteScroll from "react-infinite-scroll-component";
import { useLazyGetChannelsQuery } from "../../app/api/channelsApi";
import styles from "./Stores.module.scss";
import { CiSearch } from "react-icons/ci";
import { useNavigate } from "react-router-dom";
import { Logo } from "../../components/Icons";
import Loader from "../../components/Loader/index";
import { Result, Button } from "antd";
const StoresPage = () => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState("");
const navigate = useNavigate();
// Stores data state
const [allStores, setAllStores] = useState([]);
const [visibleStores, setVisibleStores] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const itemsPerPage = 24;
// Use lazy query to have more control over when to fetch
const [getChannels, { data: channelsData, isLoading, isFetching, error }] =
useLazyGetChannelsQuery();
// Initial fetch on component mount
useEffect(() => {
getChannels({ page: 1, 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;

View File

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

View File

@@ -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 /> },