diff --git a/src/app/api/brandsApi.js b/src/app/api/brandsApi.js index c067862..1ab6866 100644 --- a/src/app/api/brandsApi.js +++ b/src/app/api/brandsApi.js @@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({ getBrands: builder.query({ query: (params = {}) => { const queryParams = new URLSearchParams(); - if (params.type) { - queryParams.append("type", params.type); - } - if (params.page) { - queryParams.append("page", params.page); - } - if (params.limit) { - queryParams.append("limit", params.limit); - } - + if (params.type) queryParams.append("type", params.type); + if (params.page) queryParams.append("page", params.page); + if (params.limit) queryParams.append("limit", params.limit); const queryString = queryParams.toString(); return `/brands${queryString ? `?${queryString}` : ""}`; }, @@ -28,30 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({ getBrandProducts: builder.query({ query: (params) => { - if (typeof params === 'string' || typeof params === 'number') { + if (typeof params === "string" || typeof params === "number") { return `/brands/${params}/products`; } - const { id, page = 1, limit, sorting, min_price, max_price, brands } = params; - let url = `/brands/${id}/products?page=${page}`; + const { id, page = 1, limit = 24, sorting, min_price, max_price } = params; + const urlParams = new URLSearchParams(); + urlParams.append("page", page); + urlParams.append("limit", limit); + if (sorting) urlParams.append("sorting", sorting); + if (min_price) urlParams.append("min_price", min_price); + if (max_price) urlParams.append("max_price", max_price); - if (limit) { - url += `&limit=${limit}`; - } - if (sorting) { - url += `&sorting=${encodeURIComponent(sorting)}`; - } - if (min_price) { - url += `&min_price=${encodeURIComponent(min_price)}`; - } - if (max_price) { - url += `&max_price=${encodeURIComponent(max_price)}`; - } - if (brands) { - url += `&brands=${encodeURIComponent(brands)}`; - } - - return url; + return `/brands/${id}/products?${urlParams.toString()}`; }, transformResponse: (response) => ({ data: response.data || response, diff --git a/src/app/api/categories.js b/src/app/api/categories.js index c13401c..4cc07ba 100644 --- a/src/app/api/categories.js +++ b/src/app/api/categories.js @@ -5,17 +5,16 @@ export const categoriesApi = baseApi.injectEndpoints({ getCategories: builder.query({ query: (type = "tree") => `/categories?type=${type}`, }), - + getCategoryProducts: builder.query({ - query: ({ categoryId, page = 1, limit, brands, min_price, max_price, sorting }) => { + query: ({ categoryId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => { const params = new URLSearchParams(); - params.append('page', page); - if (limit) params.append('limit', limit); - if (brands) params.append('brands', brands); - if (min_price) params.append('min_price', min_price); - if (max_price) params.append('max_price', max_price); - if (sorting) params.append('sorting', sorting); - + params.append("page", page); + params.append("limit", limit); + if (brands) params.append("brands", brands); + if (min_price) params.append("min_price", min_price); + if (max_price) params.append("max_price", max_price); + if (sorting) params.append("sorting", sorting); return `categories/${categoryId}/products?${params.toString()}`; }, transformResponse: (response) => ({ @@ -23,80 +22,105 @@ export const categoriesApi = baseApi.injectEndpoints({ pagination: response.pagination || {}, }), }), - + getAllCategoryProducts: builder.query({ - async queryFn(category, queryApi, extraOptions, baseQuery) { + async queryFn(category, _queryApi, _extraOptions, baseQuery) { const fetchProducts = async (categoryId) => { const result = await baseQuery(`categories/${categoryId}/products`); return result.data ? result.data.data : []; }; let allProducts = await fetchProducts(category.id); - for (const child of category.children) { const childProducts = await fetchProducts(child.id); allProducts = [...allProducts, ...childProducts]; } - return { data: allProducts }; }, }), getAllCategoryProductsPaginated: builder.query({ async queryFn( - { category, page = 1, limit = 6, brands, min_price, max_price, sorting }, - queryApi, - extraOptions, + { category, page = 1, limit = 24, brands, min_price, max_price, sorting }, + _queryApi, + _extraOptions, baseQuery ) { - if (!category) return { data: [] }; + if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } }; try { - const hasMoreByCategory = {}; - - const fetchProductsForPage = async (categoryIds, currentPage) => { - let allPageProducts = []; - const perCategoryLimit = Math.ceil(limit / categoryIds.length); - - for (const categoryId of categoryIds) { - const params = new URLSearchParams(); - params.append('page', currentPage); - params.append('limit', perCategoryLimit); - if (brands) params.append('brands', brands); - if (min_price) params.append('min_price', min_price); - if (max_price) params.append('max_price', max_price); - if (sorting) params.append('sorting', sorting); - - const result = await baseQuery( - `categories/${categoryId}/products?${params.toString()}` - ); - - if (result.data && result.data.data) { - allPageProducts = [...allPageProducts, ...result.data.data]; - hasMoreByCategory[categoryId] = !!result.data.pagination.next_page_url; - } - } - - return allPageProducts; - }; - const categoryIds = [category.id]; - if (category.children && category.children.length > 0) { + if (category.children?.length > 0) { category.children.forEach((child) => categoryIds.push(child.id)); } - const productsForPage = await fetchProductsForPage(categoryIds, page); + // Tek category — direkt fetch, limit tam uygulanır + if (categoryIds.length === 1) { + const params = new URLSearchParams(); + params.append("page", page); + params.append("limit", limit); + if (brands) params.append("brands", brands); + if (min_price) params.append("min_price", min_price); + if (max_price) params.append("max_price", max_price); + if (sorting) params.append("sorting", sorting); - const hasMorePages = Object.values(hasMoreByCategory).some( - (hasMore) => hasMore - ); + const result = await baseQuery( + `categories/${categoryIds[0]}/products?${params.toString()}` + ); + + if (result.error) return { error: result.error }; + + return { + data: { + data: 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 { data: { - data: productsForPage, + data: allProducts, pagination: { currentPage: page, - hasMorePages: hasMorePages, + hasMorePages, }, }, }; @@ -105,11 +129,11 @@ export const categoriesApi = baseApi.injectEndpoints({ } }, }), - + getProductById: builder.query({ query: (productId) => `/products/${productId}`, }), - + getRelatedProducts: builder.query({ query: (productId) => `/products/${productId}/related`, }), diff --git a/src/app/api/collectionsApi.js b/src/app/api/collectionsApi.js index 7de32b2..9019aab 100644 --- a/src/app/api/collectionsApi.js +++ b/src/app/api/collectionsApi.js @@ -5,40 +5,35 @@ export const collectionsApi = baseApi.injectEndpoints({ getCollections: builder.query({ query: () => `/collections`, }), - + getCollectionById: builder.query({ query: (collectionId) => `/collections/${collectionId}`, }), - + getCollectionProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products`, - transformResponse: (response) => { - return { - data: response.data || [], - isEmpty: !response.data || response.data.length === 0, - }; - }, + transformResponse: (response) => ({ + data: response.data || [], + isEmpty: !response.data || response.data.length === 0, + }), }), - + checkCollectionHasProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products?limit=1`, - transformResponse: (response) => { - return { - hasProducts: response.data && response.data.length > 0, - }; - }, + transformResponse: (response) => ({ + hasProducts: response.data && response.data.length > 0, + }), }), - + getCollectionProductsPaginated: builder.query({ - query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price, sorting = "price_amount-ascending" }) => { + query: ({ collectionId, page = 1, limit = 24, brands, min_price, max_price, sorting }) => { const params = new URLSearchParams(); - params.append('page', page); - if (limit) params.append('limit', limit); - if (brands) params.append('brands', brands); - if (min_price) params.append('min_price', min_price); - if (max_price) params.append('max_price', max_price); - params.append('sorting', sorting); - + params.append("page", page); + params.append("limit", limit); + if (brands) params.append("brands", brands); + if (min_price) params.append("min_price", min_price); + if (max_price) params.append("max_price", max_price); + if (sorting) params.append("sorting", sorting); // undefined gelirse gönderme return `/collections/${collectionId}/products?${params.toString()}`; }, transformResponse: (response) => ({ diff --git a/src/components/CategoryDropdown/DropdownMenu.module.scss b/src/components/CategoryDropdown/DropdownMenu.module.scss index 84380f2..2532815 100644 --- a/src/components/CategoryDropdown/DropdownMenu.module.scss +++ b/src/components/CategoryDropdown/DropdownMenu.module.scss @@ -1,235 +1,272 @@ +// DropdownMenu.module.scss + .dropdownContainer { + position: relative; + @media screen and (max-width: 1023px) { display: none; } } +// ---- TRIGGER BUTTON ---- .navButton { display: flex; - gap: 5px; - border: none; - padding-top: 0.25rem; - padding-bottom: 0.25rem; - padding-left: 0.875rem; - padding-right: 0.875rem; - justify-content: center; align-items: center; + gap: 6px; + border: none; + padding: 0.25rem 0.875rem; border-radius: 0.5rem; height: 2.5rem; - font-size: 0.875rem; + font-size: 16px; + font-weight: 600; color: #4b5563; background-color: transparent; - font-weight: 600; - font-size: 16px; cursor: pointer; + transition: background-color 0.15s, color 0.15s; + position: relative; + z-index: 999; &:hover { background-color: #f3f4f6; } + + &.navButtonActive { + background-color: #e63946; + color: #ffffff; + + svg { + color: #ffffff; + } + } } +// ---- OVERLAY ---- +.overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.45); + z-index: 998; + animation: fadeIn 0.15s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +// ---- WRAPPER + ANIMATION ---- .dropdownWrapper { - position: relative; -} - -.dropdownPanel { position: absolute; - top: 100%; - margin-top: 8px; - z-index: 50; - display: flex; - background: white; - border: 1px solid #e5e7eb; - border-radius: 6px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - box-sizing: border-box; - width: 1366px; - padding: 0 1.375rem; + top: calc(100% + 8px); + left: 0; + z-index: 999; + animation: slideDown 0.18s ease; } -.categoriesList { - flex: 1; - max-height: 500px; - overflow-y: auto; - border-right: 1px solid #ebe7eb; - padding: 20px; +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// ---- PANEL SHELL ---- +.dropdownPanel { display: flex; - flex-direction: column; - gap: 5px; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + overflow: hidden; + width: 1336px; + max-height: 520px; + // max-width: calc(100vw - 32px); +} - // &::-webkit-scrollbar { - // width: 6px; - // } +// ---- LEFT LIST ---- +.categoriesList { + width: 270px; + flex-shrink: 0; + border-right: 1px solid rgba(255, 255, 255, 0.12); + padding: 10px 0; + max-height: 520px; + overflow-y: auto; - &::-webkit-scrollbar-track { - background: #e5e7eb; + &::-webkit-scrollbar { + width: 4px; } - &::-webkit-scrollbar-thumb { - background: #d1d5db; - } - .title { - &:hover { - color: #888888; - } - &:active { - color: #888888; - } + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; } } .categoryItem { display: flex; align-items: center; - gap: 8px; - padding: 6px; + justify-content: space-between; + padding: 9px 16px; + font-size: 16px; cursor: pointer; - transition: background-color 0.2s; - border: 1px solid #3615371a; - border-radius: 6px; + color: #000; + transition: background-color 0.12s, color 0.12s; &:hover { - background-color: #f9fafb; + background-color: rgba(255, 255, 255, 0.08); + color: #000; } &.active { - background-color: #f3f4f6; - } + background-color: rgba(255, 255, 255, 0.15); + color: #000; - .icon { - font-size: 14px; - } - - .title { - font-size: 14px; - &:hover { - color: #888888; + .title { + font-weight: 600; } } } +.title { + font-size: 14px; +} + +.chevron { + color: rgba(255, 255, 255, 0.4); + flex-shrink: 0; +} + +// ---- RIGHT CONTENT PANEL ---- .contentPanel { - flex: 3; - padding: 16px; - max-height: 400px; - overflow-y: hidden; + flex: 1; + padding: 20px 24px; + max-height: 520px; + overflow-y: auto; + background: #ffffff; - // &::-webkit-scrollbar { - // width: 6px; - // } - - &::-webkit-scrollbar-track { - background: #e5e7eb; + &::-webkit-scrollbar { + width: 4px; } - &::-webkit-scrollbar-thumb { background: #d1d5db; - // border-radius: 3px; - } - .title { - cursor: pointer; - color: #361517; - font-size: 24px; - font-weight: 600; - &:hover { - color: #888888; - } + border-radius: 2px; } } -.column { - display: flex; - flex-direction: column; - flex: 2; - text-align: left; + +.panelTitle { + font-size: 20px; + font-weight: 700; + color: #111827; + margin-bottom: 16px; + cursor: pointer; + display: inline-block; + + &:hover { + color: #e63946; + } +} + +// COLUMN GRID MODE +// SONRA — column layout (iyi, masonry gibi akar) +.columnsGrid { + columns: 250px auto; + column-gap: 24px; +} + +.columnSection { + break-inside: avoid; + margin-bottom: 20px; + display: inline-block; // break-inside'ın çalışması için zorunlu + width: 100%; } .sectionTitle { - font-size: 16px; - font-weight: 500; - margin-bottom: 12px; - color: #361517; - cursor: pointer; - &:hover { - color: #888888; - } -} - -.subcategoryList { - margin-bottom: 24px; - display: flex; -} - -.subcategoryItem { font-size: 14px; - color: #361517; - padding: 4px 0; + font-weight: 800; + color: #111827; + margin-bottom: 6px; cursor: pointer; - transition: color 0.2s; &:hover { - color: #888888; + color: #e63946; } } -.subCategoriesContainer { - display: flex; - flex-direction: column; - max-height: 360px; - overflow-y: auto; -} - -.nestedCategoryContainer:last-child { - margin-bottom: 16px; -} - -.nestedCategoryContainer { - margin-bottom: 4px; -} - -.nestedCategoryItem { - display: flex; - align-items: center; - padding: 8px 12px; - border-radius: 4px; +.leafItem { + display: block; + font-size: 14px; + color: #4b5563; + padding: 3px 0; cursor: pointer; - transition: all 0.2s ease; + transition: color 0.12s; + + &:hover { + color: #e63946; + } +} + +// FLAT LIST MODE +.flatList { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + align-content: flex-start; +} + +.flatListBordered { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} + +.flatItem { + font-size: 14px; + color: #4b5563; + cursor: pointer; + padding: 4px 10px; + border-radius: 6px; + border: 1px solid #e5e7eb; + transition: background-color 0.12s, color 0.12s; &:hover { background-color: #f3f4f6; - } - - .categoryLabel { - flex: 1; - display: flex; - align-items: center; - } - - .title { - font-size: 14px; + color: #111827; } } -.expandButton, -.navigateButton { +.navButtonLoading { + opacity: 0.7; + cursor: wait; + + .categoryIcon { + opacity: 0.4; + } +} + +.loadingDots { display: flex; + gap: 3px; align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 4px; - background-color: transparent; - border: none; - cursor: pointer; - &:hover { - background-color: #e5e7eb; + span { + width: 4px; + height: 4px; + border-radius: 50%; + background: currentColor; + animation: dotPulse 1.2s infinite ease-in-out; + + &:nth-child(2) { animation-delay: 0.2s; } + &:nth-child(3) { animation-delay: 0.4s; } } } -.nestedChildren { - margin-top: 4px; -} - -.noSubcategories { - color: #6b7280; - font-style: italic; -} +@keyframes dotPulse { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} \ No newline at end of file diff --git a/src/components/CategoryDropdown/index.jsx b/src/components/CategoryDropdown/index.jsx index d73309e..0e339ab 100644 --- a/src/components/CategoryDropdown/index.jsx +++ b/src/components/CategoryDropdown/index.jsx @@ -1,83 +1,62 @@ +// DropdownMenu.jsx import React, { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import { ChevronRight } from "lucide-react"; import styles from "./DropdownMenu.module.scss"; import { useGetCategoriesQuery } from "../../app/api/categories"; import { CategoryIcon } from "../Icons"; -import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar -const NestedCategory = ({ - category, - level = 0, - handleCategorySelect, - closeDropdown, -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const hasChildren = category.children && category.children.length > 0; +const ContentPanel = ({ category, onSelect, onClose }) => { + if (!category) return null; - const handleClick = (e) => { - e.stopPropagation(); - if (hasChildren) { - setIsExpanded(!isExpanded); - } else { - handleCategorySelect(category); - closeDropdown(); - } - }; + const children = category.children || []; + const withChildren = children.filter((c) => c.children?.length > 0); + const withoutChildren = children.filter((c) => !c.children?.length); - const handleDirectNavigation = (e) => { - e.stopPropagation(); - handleCategorySelect(category); - closeDropdown(); - }; + const allColumns = [ + ...withChildren, + ...withoutChildren.map((c) => ({ ...c, children: [] })), + ]; return ( -