Compare commits

...

2 Commits

Author SHA1 Message Date
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
27 changed files with 1842 additions and 1329 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.limit) queryParams.append("limit", params.limit);
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, limit = 24, sorting, min_price, max_price } = params;
let url = `/brands/${id}/products?page=${page}`; const urlParams = new URLSearchParams();
urlParams.append("page", page);
urlParams.append("limit", limit);
if (sorting) urlParams.append("sorting", sorting);
if (min_price) urlParams.append("min_price", min_price);
if (max_price) urlParams.append("max_price", max_price);
if (limit) { 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, limit = 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("limit", limit);
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, limit = 24, 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("limit", limit);
if (brands) params.append("brands", brands);
if (min_price) params.append("min_price", min_price);
if (max_price) params.append("max_price", max_price);
if (sorting) params.append("sorting", sorting);
const hasMorePages = Object.values(hasMoreByCategory).some( 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("limit", limit);
if (brands) params.append("brands", brands);
if (min_price) params.append("min_price", min_price);
if (max_price) params.append("max_price", max_price);
if (sorting) params.append("sorting", sorting);
return baseQuery(`categories/${categoryId}/products?${params.toString()}`);
});
const results = await Promise.all(requests);
let allProducts = [];
let hasMorePages = false;
const seenIds = new Set();
for (const result of results) {
if (result.error) continue;
const items = result.data?.data || [];
for (const item of items) {
if (!seenIds.has(item.id)) {
seenIds.add(item.id);
allProducts.push(item);
}
}
if (result.data?.pagination?.next_page_url) {
hasMorePages = true;
}
}
return { 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

@@ -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?limit=1`,
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, limit = 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("limit", limit);
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

@@ -1,235 +1,272 @@
// 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;
} }
&.navButtonActive {
background-color: #e63946;
color: #ffffff;
svg {
color: #ffffff;
}
}
} }
// ---- OVERLAY ----
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 998;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// ---- WRAPPER + ANIMATION ----
.dropdownWrapper { .dropdownWrapper {
position: relative;
}
.dropdownPanel {
position: absolute; position: absolute;
top: 100%; top: calc(100% + 8px);
margin-top: 8px; left: 0;
z-index: 50; z-index: 999;
display: flex; animation: slideDown 0.18s ease;
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 { @keyframes slideDown {
flex: 1; from {
max-height: 500px; opacity: 0;
overflow-y: auto; transform: translateY(-6px);
border-right: 1px solid #ebe7eb; }
padding: 20px; to {
opacity: 1;
transform: translateY(0);
}
}
// ---- PANEL SHELL ----
.dropdownPanel {
display: flex; display: flex;
flex-direction: column; background: #fff;
gap: 5px; border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
width: 1336px;
max-height: 520px;
// max-width: calc(100vw - 32px);
}
// &::-webkit-scrollbar { // ---- LEFT LIST ----
// width: 6px; .categoriesList {
// } width: 270px;
flex-shrink: 0;
border-right: 1px solid rgba(255, 255, 255, 0.12);
padding: 10px 0;
max-height: 520px;
overflow-y: auto;
&::-webkit-scrollbar-track { &::-webkit-scrollbar {
background: #e5e7eb; width: 4px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #d1d5db; background: rgba(255, 255, 255, 0.2);
} border-radius: 2px;
.title {
&:hover {
color: #888888;
}
&:active {
color: #888888;
}
} }
} }
.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: rgba(255, 255, 255, 0.08);
color: #000;
} }
&.active { &.active {
background-color: #f3f4f6; background-color: rgba(255, 255, 255, 0.15);
} color: #000;
.icon { .title {
font-size: 14px; font-weight: 600;
}
.title {
font-size: 14px;
&:hover {
color: #888888;
} }
} }
} }
.title {
font-size: 14px;
}
.chevron {
color: rgba(255, 255, 255, 0.4);
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: 4px;
// }
&::-webkit-scrollbar-track {
background: #e5e7eb;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #d1d5db; background: #d1d5db;
// border-radius: 3px; border-radius: 2px;
}
.title {
cursor: pointer;
color: #361517;
font-size: 24px;
font-weight: 600;
&:hover {
color: #888888;
}
} }
} }
.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,15 +163,15 @@ 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}>
@@ -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

@@ -0,0 +1,92 @@
.container {
max-width: 1336px;
margin: 20px auto;
width: 100%;
@media screen and (max-width: 1023px) {
margin: 10px auto;
}
}
.brandsScroll {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 8px;
/* Hide scrollbar for Webkit */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for Firefox, IE, Edge */
-ms-overflow-style: none;
scrollbar-width: none;
}
.brandCard {
flex: 0 0 auto;
width: 122px;
height: 50px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 8px;
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.logoFallback {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.allButton {
flex: 0 0 auto;
width: 122px;
height: 67.6px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
gap: 4px;
color: #111827;
font-weight: 600;
font-size: 14px;
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
color: #aaaaaa;
}
svg {
font-size: 16px;
}
}
@media screen and (max-width: 768px) {
.brandCard, .allButton {
width: 100px;
// height: 79.2px;
}
}

View File

@@ -0,0 +1,77 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useGetBrandsQuery } from '../../app/api/brandsApi';
import styles from './HomeBrands.module.scss';
import { Logo } from '../Icons';
import { IoIosArrowForward } from 'react-icons/io';
const HomeBrands = () => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
// We fetch a larger amount so we have enough to shuffle.
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 50 });
const randomBrands = useMemo(() => {
if (!brandsData) return [];
// Create a shallow copy and shuffle it
const shuffled = [...brandsData].sort(() => 0.5 - Math.random());
// Pick the first 9 brands
return shuffled.slice(0, 8);
}, [brandsData]);
if (isLoading || !brandsData || brandsData.length === 0) return null;
// "Еще" in ru, "Hemmesi" in tm, "More" in en
const getMoreText = () => {
const lang = i18n.language;
if (lang === 'ru') return 'Еще';
if (lang === 'en') return 'More';
return 'Hemmesi';
};
return (
<div className={styles.container}>
<div className={styles.brandsScroll}>
{randomBrands.map((brand) => (
<div
key={brand.id}
className={styles.brandCard}
onClick={() => navigate(`/brands/${brand.id}`)}
>
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
<img
src={
brand.media?.[0]?.thumbnail ||
brand.media?.[0]?.images_800x800 ||
brand.logo
}
alt={brand.name}
onError={(e) => {
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
) : (
<div className={styles.logoFallback}>
<Logo width={40} height={40} />
</div>
)}
<div className={styles.logoFallback} style={{ display: "none" }}>
<Logo width={40} height={40} />
</div>
</div>
))}
<div
className={styles.allButton}
onClick={() => navigate('/brands')}
>
<span>{getMoreText()}</span>
<IoIosArrowForward />
</div>
</div>
</div>
);
};
export default HomeBrands;

View File

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

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

@@ -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,
@@ -45,22 +41,20 @@ const ProductCard = ({
}) => { }) => {
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

@@ -38,6 +38,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

@@ -38,6 +38,9 @@ export default {
emptyCartTitle: "Ваша корзина пуста", emptyCartTitle: "Ваша корзина пуста",
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину", emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
continueShopping: "Продолжить покупки", continueShopping: "Продолжить покупки",
pendingPriceTitle: "Цена уточняется",
pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.",
pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию."
}, },
checkout: { checkout: {
paymentMethod: "Способ оплаты", paymentMethod: "Способ оплаты",

View File

@@ -38,6 +38,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;
} }

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useRef } from "react"; import { useState, useEffect, useRef, useCallback } 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";
@@ -19,50 +19,16 @@ 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); // Tüm parametreleri ref'te tut — stale closure'ı tamamen engelle
const abortControllerRef = useRef(null); const paramsRef = useRef({});
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 &&
@@ -72,238 +38,146 @@ const useCategoryProducts = ({
!brandId && !brandId &&
!collectionId; !collectionId;
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();
fetchCategoryPaginated, const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
{ const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
data: lazyCategoryProducts,
isLoading: lazyCategoryLoading,
isFetching: lazyCategoryFetching,
reset: resetCategoryPaginated,
},
] = useLazyGetAllCategoryProductsPaginatedQuery();
const [
fetchBrandPaginated,
{
data: paginatedBrandProducts,
isLoading: brandPaginatedLoading,
isFetching: brandFetching,
reset: resetBrandPaginated,
},
] = useLazyGetBrandProductsQuery();
const [
fetchCollectionPaginated,
{
data: paginatedCollectionProducts,
isLoading: collectionPaginatedLoading,
isFetching: collectionFetching,
reset: resetCollectionPaginated,
},
] = useLazyGetCollectionProductsPaginatedQuery();
// Base query handler
useEffect(() => { useEffect(() => {
setProducts([]); if (!shouldUseBaseQuery || !baseQueryData) return;
setHasMore(true); const data = baseQueryData.data || [];
const hasNextPage = !!baseQueryData.pagination?.next_page_url;
resetCategoryPaginated?.(); setProducts((prev) => {
resetBrandPaginated?.(); if (currentPage === 1) return data;
resetCollectionPaginated?.(); const existingIds = new Set(prev.map((p) => p.id));
const newItems = data.filter((p) => !existingIds.has(p.id));
lastFetchKeyRef.current = null; return newItems.length > 0 ? [...prev, ...newItems] : prev;
isFetchingRef.current = false; });
setHasMore(hasNextPage);
if (abortControllerRef.current) { }, [baseQueryData, currentPage, shouldUseBaseQuery]);
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}, [
contextId,
resetCategoryPaginated,
resetBrandPaginated,
resetCollectionPaginated,
]);
// Her fetch çağrısını doğrudan effect içinde yap — useCallback kaldırıldı
useEffect(() => { useEffect(() => {
if (searchQuery) return; if (shouldUseBaseQuery || searchQuery) return;
if (lastFetchKeyRef.current === fetchKey) { // Parametreleri snapshot al
return; const snapshot = {
} currentPage,
selectedFilterCategory,
categoryId,
isSubCategory,
brandId,
collectionId,
selectedFilterBrand,
minPrice,
maxPrice,
sorting,
};
if (isFetchingRef.current) { const requestId = ++activeRequestId.current;
return; setIsFetching(true);
}
if (abortControllerRef.current) { const run = async () => {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
isFetchingRef.current = true;
lastFetchKeyRef.current = fetchKey;
const executeFetch = async () => {
try { try {
if (selectedFilterCategory) { const params = {
await fetchCategoryPaginated({ page: snapshot.currentPage,
category: { limit: 24,
id: selectedFilterCategory, brands: snapshot.selectedFilterBrand || undefined,
children: [], min_price: snapshot.minPrice || undefined,
}, max_price: snapshot.maxPrice || undefined,
...fetchParams, sorting: snapshot.sorting || undefined,
}); };
let result = null;
if (snapshot.selectedFilterCategory) {
result = await fetchCategoryPaginated({
category: { id: snapshot.selectedFilterCategory, children: [] },
...params,
}).unwrap();
} else if (snapshot.categoryId && snapshot.isSubCategory) {
result = await fetchCategoryPaginated({
category: { id: parseInt(snapshot.categoryId), children: [] },
...params,
}).unwrap();
} else if (snapshot.brandId) {
result = await fetchBrandPaginated({
id: snapshot.brandId,
...params,
}).unwrap();
} else if (snapshot.collectionId) {
result = await fetchCollectionPaginated({
collectionId: snapshot.collectionId,
...params,
}).unwrap();
}
if (requestId !== activeRequestId.current) return;
if (!result) {
setHasMore(false);
return; 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, // useCallback YOK — her dependency değişince effect direkt çalışır
shouldUseBaseQuery,
searchQuery, searchQuery,
selectedFilterBrand, currentPage,
selectedFilterCategory, selectedFilterCategory,
categoryId, categoryId,
isSubCategory, isSubCategory,
brandId, brandId,
collectionId, collectionId,
fetchParams, selectedFilterBrand,
minPrice,
maxPrice,
sorting,
fetchCategoryPaginated, fetchCategoryPaginated,
fetchBrandPaginated, fetchBrandPaginated,
fetchCollectionPaginated, fetchCollectionPaginated,
]); ]);
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,
@@ -314,4 +188,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";
@@ -25,18 +23,43 @@ const CategoryPage = () => {
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"}`,
[categoryId, collectionId, brandId],
);
const getSavedState = (key, defaultVal) => {
try {
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
if (saved) return JSON.parse(saved);
} catch (e) {
console.error(e);
}
return defaultVal;
};
const getSavedStateByKey = (route, key) => {
try {
const saved = sessionStorage.getItem(`category_${key}_${route}`);
if (saved) return JSON.parse(saved);
} catch (e) {
console.error(e);
}
return null;
};
const [pageState, setPageState] = useState(() => getSavedState("pageState", {
currentPage: 1, 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 +70,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);
@@ -97,6 +115,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,6 +126,10 @@ 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;
} }
@@ -113,14 +137,36 @@ const CategoryPage = () => {
prevRouteRef.current = routeKey; prevRouteRef.current = routeKey;
setAllProducts([]); const savedPageState = getSavedStateByKey(routeKey, "pageState");
setHasMore(true); const savedFilterState = getSavedStateByKey(routeKey, "filterState");
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); const savedProducts = getSavedStateByKey(routeKey, "products");
setFilterState({ const savedHasMore = getSavedStateByKey(routeKey, "hasMore");
selectedFilterCategory: null,
selectedFilterBrand: null, if (savedPageState && savedFilterState && savedProducts) {
brandSearchQuery: "", setPageState(savedPageState);
}); setFilterState(savedFilterState);
setAllProducts(savedProducts);
setHasMore(savedHasMore ?? true);
const savedScroll = getSavedStateByKey(routeKey, "scroll");
if (savedScroll !== null) {
setTimeout(() => window.scrollTo(0, savedScroll), 100);
}
} else {
setAllProducts([]);
setHasMore(true);
setPageState({
currentPage: 1,
minPrice: "",
maxPrice: "",
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 +180,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 +275,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 +288,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 +306,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() },
@@ -314,31 +402,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 +430,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 +449,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 +477,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 +499,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

@@ -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,
@@ -370,6 +373,15 @@ const ProductPage = ({
</span> </span>
</div> </div>
)} )}
{product.properties?.length > 0 && (
product.properties.map((prop, index) => (
<div key={`${prop.attribute_id}-${index}`} className={styles.metaItem}>
<span className={styles.metaLabel}>{prop.name}</span>
<span className={styles.metaValue}>{prop.value}</span>
</div>
))
)}
</div> </div>
{/* Description card */} {/* Description card */}
@@ -426,11 +438,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}> {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> </div>
@@ -444,11 +464,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

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