From be96ff329f5fe9fc1f6c52185748699edaea10a9 Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Tue, 23 Sep 2025 20:50:17 +0300 Subject: [PATCH 01/11] first commit --- src/app/compare/ComparePage.tsx | 153 ++++++++++++++++++ src/app/compare/CompareSlider.tsx | 119 ++++++++++++++ src/app/compare/page.tsx | 7 + src/components/Products/AddToCartButton.tsx | 1 - .../Products/AddToCompareButton.tsx | 52 ++++++ src/components/UI/CategoriesHeading.tsx | 12 +- src/components/UI/CategoryHeader.tsx | 7 +- .../pages/ProductDetails/ProductDetails.tsx | 1 - .../ProductDetailsOrderOptions.tsx | 36 +++-- src/lib/features/compare/compareSlice.tsx | 59 +++++++ src/lib/store.ts | 6 +- src/lib/utils.ts | 20 ++- src/types/CategoryName.ts | 1 + src/types/NavBarRightComponents.tsx | 15 +- 14 files changed, 462 insertions(+), 27 deletions(-) create mode 100644 src/app/compare/ComparePage.tsx create mode 100644 src/app/compare/CompareSlider.tsx create mode 100644 src/app/compare/page.tsx create mode 100644 src/components/Products/AddToCompareButton.tsx create mode 100644 src/lib/features/compare/compareSlice.tsx diff --git a/src/app/compare/ComparePage.tsx b/src/app/compare/ComparePage.tsx new file mode 100644 index 0000000..6acf83a --- /dev/null +++ b/src/app/compare/ComparePage.tsx @@ -0,0 +1,153 @@ +'use client'; + +import React, { useMemo, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '@/lib/store'; +import CompareSlider from './CompareSlider'; +import { ProductType } from '@/types/CategoryType'; +import { CategoryName } from '@/types/CategoryName'; +import { mapProductTypeToProduct } from '@/lib/utils'; +import { Product } from '@/types/product'; +import SelectArrow from '@/components/UI/SelectArrow'; +import { + clearCompare, + clearCompareByCategory, +} from '@/lib/features/compare/compareSlice'; +import CategoryHeader from '@/components/UI/CategoryHeader'; + +const categoryLabels: Partial> = { + [CategoryName.Phones]: 'Phones', + [CategoryName.Tablets]: 'Tablets', + [CategoryName.Accessories]: 'Accessories', +}; + +const ComparePage = () => { + const compareItems = useSelector( + (state: RootState) => state.persisted.compare.items, + ); + + const [mounted, setMounted] = useState(false); + const [selectedCategory, setSelectedCategory] = useState( + '', + ); + const dispatch = useDispatch(); + + useEffect(() => setMounted(true), []); + + const itemsByCategory = useMemo(() => { + return compareItems.reduce>( + (acc, item) => { + if (!acc[item.category]) acc[item.category] = []; + acc[item.category].push(item); + return acc; + }, + {} as Record, + ); + }, [compareItems]); + + useEffect(() => { + const firstCategory = Object.keys(itemsByCategory)[0] as + | CategoryName + | undefined; + if (firstCategory && !selectedCategory) setSelectedCategory(firstCategory); + }, [itemsByCategory, selectedCategory]); + + if (!mounted) return null; + + if (compareItems.length < 2) + return ( +
+ Для порівняння потрібні щонайменше два товари +
+ ); + + const products: Product[] = + selectedCategory ? + itemsByCategory[selectedCategory].map(mapProductTypeToProduct) + : []; + + return ( +
+ + +
+ + +
+ +
+
+ + {products.length >= 2 ? + + :
+ At least two items required for the compression +
+ } + + {/* Кнопки очищення */} +
+ + + {selectedCategory && ( + + )} +
+
+ ); +}; + +export default ComparePage; diff --git a/src/app/compare/CompareSlider.tsx b/src/app/compare/CompareSlider.tsx new file mode 100644 index 0000000..36fe240 --- /dev/null +++ b/src/app/compare/CompareSlider.tsx @@ -0,0 +1,119 @@ +'use client'; + +import React, { useRef } from 'react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation } from 'swiper/modules'; +import { motion } from 'framer-motion'; + +import 'swiper/css'; +import 'swiper/css/navigation'; +import ProductCart from '@/components/Products/ProductCart'; +import { Product } from '@/types/product'; + +interface CompareSliderProps { + title: string; + products: Product[]; +} + +export default function CompareSlider({ title, products }: CompareSliderProps) { + const prevRef = useRef(null); + const nextRef = useRef(null); + + if (!products || products.length < 2) return null; + console.log(products); + + return ( +
+
+ + {title} + +
+ + + +
+
+ + { + // @ts-expect-error: Swiper types don't know about refs + swiper.params.navigation.prevEl = prevRef.current; + // @ts-expect-error: Swiper types don't know about refs + swiper.params.navigation.nextEl = nextRef.current; + swiper.navigation.init(); + swiper.navigation.update(); + }} + breakpoints={{ + 0: { slidesPerView: 1 }, + 500: { slidesPerView: 2 }, + 640: { slidesPerView: 2 }, + 900: { slidesPerView: 3 }, + 1200: { slidesPerView: 4 }, + }} + className="multiple-slide-carousel" + > + {products.map((product) => ( + + + + ))} + +
+ ); +} diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx new file mode 100644 index 0000000..1e4c77f --- /dev/null +++ b/src/app/compare/page.tsx @@ -0,0 +1,7 @@ +import ComparePage from './ComparePage'; + +const page = () => { + return ; +}; + +export default page; diff --git a/src/components/Products/AddToCartButton.tsx b/src/components/Products/AddToCartButton.tsx index 31b29fb..d59f63a 100644 --- a/src/components/Products/AddToCartButton.tsx +++ b/src/components/Products/AddToCartButton.tsx @@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux'; import { addItem } from '@/lib/features/cart/cartSlice'; import { Product } from '@/types/product'; -import { toast } from 'sonner'; interface AddToCartButtonProps { product: Product; diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx new file mode 100644 index 0000000..f7cc16f --- /dev/null +++ b/src/components/Products/AddToCompareButton.tsx @@ -0,0 +1,52 @@ +'use client'; + +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { addToCompare } from '@/lib/features/compare/compareSlice'; +import { RootState } from '@/lib/store'; +import { ProductType } from '@/types/CategoryType'; + +interface AddToCompareButtonProps { + product: ProductType; + onClick?: () => void; +} + +const AddToCompareButton: React.FC = ({ + product, + onClick, +}) => { + const dispatch = useDispatch(); + const items = useSelector( + (state: RootState) => state.persisted.compare.items, + ); + + const isAdded = items.some( + (i) => + i.id === product.id && + i.color === product.color && + i.capacity === product.capacity, + ); + + const handleAddToCompare = () => { + if (!isAdded) { + dispatch(addToCompare(product)); + onClick?.(); + } + }; + + return ( + + ); +}; + +export default AddToCompareButton; diff --git a/src/components/UI/CategoriesHeading.tsx b/src/components/UI/CategoriesHeading.tsx index cffa2dd..6e4d369 100644 --- a/src/components/UI/CategoriesHeading.tsx +++ b/src/components/UI/CategoriesHeading.tsx @@ -24,12 +24,14 @@ const CategoryHeading: React.FC = ({ `Mobile ${categoryName.toLowerCase()}` : formattedCategory} -

- {total} {!isFavourites ? 'models' : 'items'} -

+ > + {total} {!isFavourites ? 'models' : 'items'} +

+ ); }; diff --git a/src/components/UI/CategoryHeader.tsx b/src/components/UI/CategoryHeader.tsx index 337a60c..8f9d9c0 100644 --- a/src/components/UI/CategoryHeader.tsx +++ b/src/components/UI/CategoryHeader.tsx @@ -9,11 +9,12 @@ type Props = { isFavourites?: true; categoryName: CategoryName; - total: number; + total?: number; }; const CategoryHeader: React.FC = ({ isCart, isFavourites, + categoryName, total, }) => { @@ -25,12 +26,12 @@ const CategoryHeader: React.FC = ({ {isFavourites ? : } diff --git a/src/components/pages/ProductDetails/ProductDetails.tsx b/src/components/pages/ProductDetails/ProductDetails.tsx index d60cb9f..b8da754 100644 --- a/src/components/pages/ProductDetails/ProductDetails.tsx +++ b/src/components/pages/ProductDetails/ProductDetails.tsx @@ -51,7 +51,6 @@ const ProductDetails = ({ (spec): spec is { label: string; value: string } => spec.value !== undefined, ); - return (
= ({ product, - capacityAvailable, - namespaceId, + variants, colorsAvailable, + capacityAvailable, priceDiscount, priceRegular, - variants, + namespaceId, }) => { - const router = useRouter(); - return (
+ {/* Вибір кольору */}

Available colors

ID: {namespaceId}
- = ({ scroll={false} > @@ -114,6 +117,7 @@ const ProductDetailsOrderOptions: React.FC = ({
+ {/* Ціни */}
${priceDiscount} @@ -121,10 +125,14 @@ const ProductDetailsOrderOptions: React.FC = ({
+ {/* Кнопки */}
-
+
+
+ +
diff --git a/src/lib/features/compare/compareSlice.tsx b/src/lib/features/compare/compareSlice.tsx new file mode 100644 index 0000000..1c6a1c2 --- /dev/null +++ b/src/lib/features/compare/compareSlice.tsx @@ -0,0 +1,59 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ProductType } from '@/types/CategoryType'; + +interface CompareState { + items: ProductType[]; +} + +const initialState: CompareState = { + items: [], +}; + +const compareSlice = createSlice({ + name: 'compare', + initialState, + reducers: { + addToCompare: (state, action: PayloadAction) => { + const product = action.payload; + + const exists = state.items.some( + (i) => + i.id === product.id && + i.color === product.color && + i.capacity === product.capacity, + ); + + if (!exists) state.items.push(product); + }, + + removeFromCompare: (state, action: PayloadAction) => { + const product = action.payload; + + state.items = state.items.filter( + (i) => + !( + i.id === product.id && + i.color === product.color && + i.capacity === product.capacity + ), + ); + }, + + clearCompare: (state) => { + state.items = []; + }, + + clearCompareByCategory: (state, action: PayloadAction) => { + state.items = state.items.filter((p) => p.category !== action.payload); + }, + }, +}); + +export const { + addToCompare, + removeFromCompare, + clearCompare, + clearCompareByCategory, +} = compareSlice.actions; + +export default compareSlice.reducer; diff --git a/src/lib/store.ts b/src/lib/store.ts index af506a8..00f78ff 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -4,6 +4,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { apiSlice } from '@/lib/features/api/apiSlice'; import favouritesReducer from '@/lib/features/favourites/favouritesSlice'; import cartReducer from '@/lib/features/cart/cartSlice'; +import compareReducer from '@/lib/features/compare/compareSlice'; import { persistReducer, persistStore } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; @@ -12,12 +13,15 @@ import { CategoryName } from '@/types/CategoryName'; const persistedReducers = combineReducers({ favourites: favouritesReducer, cart: cartReducer, + + // 👇 додаємо сюди compare, якщо хочеш зберігати його у localStorage + compare: compareReducer, }); const persistConfig = { key: 'root', storage, - whitelist: [CategoryName.Favourites, CategoryName.Cart], + whitelist: [CategoryName.Favourites, CategoryName.Cart, 'compare'], }; const persistedReducer = persistReducer(persistConfig, persistedReducers); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8164923..1d4fe45 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,25 @@ -import { CategoryName } from '@/types/CategoryName'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; +import { Product } from '@/types/product'; +import { ProductType } from '@/types/CategoryType'; + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export function mapProductTypeToProduct(product: ProductType): Product { + return { + id: +`${product.id}-${product.color}-${product.capacity}`, + itemId: String(product.itemId), + category: product.category, + name: product.name || 'Без назви', + image: Array.isArray(product.images) ? product.images[0] : product.image, + price: product.price ?? 0, + fullPrice: product.fullPrice ?? 0, + screen: product.screen ?? '—', + capacity: product.capacity ?? '—', + ram: product.ram ?? '—', + color: product.color ?? '—', + year: product.year ?? 2023, + }; +} diff --git a/src/types/CategoryName.ts b/src/types/CategoryName.ts index 7f9b919..176250b 100644 --- a/src/types/CategoryName.ts +++ b/src/types/CategoryName.ts @@ -5,4 +5,5 @@ export enum CategoryName { Accessories = 'accessories', Favourites = 'favourites', Cart = 'cart', + Compare = 'compare', } diff --git a/src/types/NavBarRightComponents.tsx b/src/types/NavBarRightComponents.tsx index b3590e1..411a291 100644 --- a/src/types/NavBarRightComponents.tsx +++ b/src/types/NavBarRightComponents.tsx @@ -2,9 +2,22 @@ import FavouritesLink from '@/components/UI/NavBar/FavouritesLink'; import ShoppingCartLink from '@/components/UI/NavBar/ShoppingCartLink'; import ThemeSwitcher from '@/components/UI/ThemeSwitcher'; import React from 'react'; -import { CategoryName } from './CategoryName'; +import { CategoryName } from '@/types/CategoryName'; +import Link from 'next/link'; + +export const Compare = () => { + return ( + + Compare + + ); +}; export const NavBarRightComponents = [ + { id: CategoryName.Compare, element: }, { id: 'theme', element: }, { id: CategoryName.Favourites, element: }, { id: CategoryName.Cart, element: }, From b424c968c2ca3eb8534d4349495c969ab18243c9 Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Wed, 24 Sep 2025 01:19:09 +0300 Subject: [PATCH 02/11] second commit --- src/app/compare/ComparePage.tsx | 179 ++++++++++-------- src/app/compare/CompareSlider.tsx | 65 ++++--- src/components/Products/ProductCart.tsx | 113 +++++++---- src/components/animate-ui/icons/x.tsx | 126 ++++++++++++ .../pages/ProductDetails/ProductDetails.tsx | 2 +- .../ProductDetailsOptionGroup.tsx | 7 +- .../ProductDetailsOrderOptions.tsx | 2 +- src/lib/features/compare/compareSlice.tsx | 20 +- src/lib/store.ts | 4 +- src/lib/utils.ts | 18 +- src/types/product.ts | 12 +- 11 files changed, 377 insertions(+), 171 deletions(-) create mode 100644 src/components/animate-ui/icons/x.tsx diff --git a/src/app/compare/ComparePage.tsx b/src/app/compare/ComparePage.tsx index 6acf83a..0d3fb05 100644 --- a/src/app/compare/ComparePage.tsx +++ b/src/app/compare/ComparePage.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/lib/store'; -import CompareSlider from './CompareSlider'; +import CompareSlider from '@/app/compare/CompareSlider'; import { ProductType } from '@/types/CategoryType'; import { CategoryName } from '@/types/CategoryName'; import { mapProductTypeToProduct } from '@/lib/utils'; @@ -14,6 +14,7 @@ import { clearCompareByCategory, } from '@/lib/features/compare/compareSlice'; import CategoryHeader from '@/components/UI/CategoryHeader'; +import ActionButton from '@/components/UI/ActionButton'; const categoryLabels: Partial> = { [CategoryName.Phones]: 'Phones', @@ -25,6 +26,7 @@ const ComparePage = () => { const compareItems = useSelector( (state: RootState) => state.persisted.compare.items, ); + console.log('Compare Items:', compareItems); const [mounted, setMounted] = useState(false); const [selectedCategory, setSelectedCategory] = useState( @@ -34,6 +36,7 @@ const ComparePage = () => { useEffect(() => setMounted(true), []); + // Групуємо товари по категоріях const itemsByCategory = useMemo(() => { return compareItems.reduce>( (acc, item) => { @@ -45,27 +48,46 @@ const ComparePage = () => { ); }, [compareItems]); + // Список доступних категорій з товарами + const availableCategories = Object.keys(itemsByCategory).filter( + (cat) => itemsByCategory[cat as CategoryName].length > 0, + ) as CategoryName[]; + + // Автоматичний вибір категорії при зміні товарів useEffect(() => { - const firstCategory = Object.keys(itemsByCategory)[0] as - | CategoryName - | undefined; - if (firstCategory && !selectedCategory) setSelectedCategory(firstCategory); - }, [itemsByCategory, selectedCategory]); + if (!selectedCategory && availableCategories.length > 0) { + setSelectedCategory(availableCategories[0]); + } else if ( + selectedCategory && + (!itemsByCategory[selectedCategory] || + itemsByCategory[selectedCategory].length === 0) + ) { + // Якщо поточна категорія порожня, беремо наступну доступну + setSelectedCategory(availableCategories[0] || ''); + } + }, [selectedCategory, itemsByCategory, availableCategories]); if (!mounted) return null; - if (compareItems.length < 2) - return ( -
- Для порівняння потрібні щонайменше два товари -
- ); - const products: Product[] = - selectedCategory ? + selectedCategory && itemsByCategory[selectedCategory] ? itemsByCategory[selectedCategory].map(mapProductTypeToProduct) : []; + const hasProducts = products.length > 0; + if (!hasProducts) { + return ( +
+

+ Your compare list is empty +

+

+ Looks like you haven't added anything yet. +

+ +
+ ); + } return (
{ total={products.length} /> -
- - + setSelectedCategory(e.target.value as CategoryName) + } + className="w-full appearance-none bg-light-theme-bg-dark dark:bg-dark-theme-btn-selected px-4 py-2 pr-10 font-[600] text-[14px] leading-[21px] text-light-theme-text dark:text-text-light border border-light-theme-border-active focus:outline-none focus:ring-0 dark:border-dark-theme-btn-selected cursor-pointer" - > - {Object.keys(itemsByCategory).map((category) => ( - - ))} - -
- -
-
- - {products.length >= 2 ? - - :
- At least two items required for the compression -
- } - - {/* Кнопки очищення */} -
- - - {selectedCategory && ( - - )} -
+ {availableCategories.map((category) => ( + + ))} + +
+ +
+
+ + {/* Слайдер порівняння */} + + + {/* Кнопки очищення */} +
+ + + +
+ + )}
); }; diff --git a/src/app/compare/CompareSlider.tsx b/src/app/compare/CompareSlider.tsx index 36fe240..5c4f797 100644 --- a/src/app/compare/CompareSlider.tsx +++ b/src/app/compare/CompareSlider.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Navigation } from 'swiper/modules'; import { motion } from 'framer-motion'; @@ -18,9 +18,8 @@ interface CompareSliderProps { export default function CompareSlider({ title, products }: CompareSliderProps) { const prevRef = useRef(null); const nextRef = useRef(null); - - if (!products || products.length < 2) return null; - console.log(products); + const [isBeginning, setIsBeginning] = useState(true); + const [isEnd, setIsEnd] = useState(false); return (
@@ -37,9 +36,11 @@ export default function CompareSlider({ title, products }: CompareSliderProps) {
); diff --git a/src/components/Products/ProductCart.tsx b/src/components/Products/ProductCart.tsx index 4ed8454..f40e594 100644 --- a/src/components/Products/ProductCart.tsx +++ b/src/components/Products/ProductCart.tsx @@ -1,6 +1,6 @@ import Image from 'next/image'; import React, { useEffect, useState } from 'react'; -import { Product } from '@/types/product'; +import { Category, Product } from '@/types/product'; import Link from 'next/link'; import { motion } from 'framer-motion'; @@ -8,19 +8,24 @@ import FavoriteButton from '@/components/Products/FavoriteButton'; import AddToCartButton from '@/components/Products/AddToCartButton'; import { toast } from 'sonner'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/lib/store'; +import CloseComponent from '@/components/UI/ShoppingCartIcons/CloseComponent'; +import { removeFromCompareByKey } from '@/lib/features/compare/compareSlice'; +import { CategoryName } from '@/types/CategoryName'; interface ProductCartProps { product: Product; index?: number; disableOnce?: boolean; + isCompare?: boolean; } const ProductCart = ({ product, index = 0, disableOnce = false, + isCompare = false, }: ProductCartProps) => { const [isClient, setIsClient] = useState(false); const imgSrc = `/${product.image}`; @@ -31,9 +36,49 @@ const ProductCart = ({ ), ); - useEffect(() => { - setIsClient(true); - }, []); + const dispatch = useDispatch(); + + useEffect(() => setIsClient(true), []); + + const renderSpecs = ( + specs: { label: string; value: string | string[] | number | Category }[], + ) => + specs.map((spec) => ( +
+ + {( + product.category === CategoryName.Accessories && + spec.label === 'Screen' + ) ? + 'Display size' + : spec.label} + + + {Array.isArray(spec.value) ? spec.value.join(', ') : spec.value} + +
+ )); + + const compareSpecs = + isCompare ? + [ + { label: 'Screen', value: product.screen ?? '—' }, + { label: 'Resolution', value: product.resolution ?? '—' }, + { label: 'Processor', value: product.processor ?? '—' }, + { label: 'RAM', value: product.ram ?? '—' }, + { label: 'Capacity', value: product.capacity ?? '—' }, + { label: 'Camera', value: product.camera ?? '—' }, + { label: 'Zoom', value: product.zoom ?? '—' }, + { label: 'Cell', value: product.cell ?? '—' }, + ] + : [ + { label: 'Screen', value: product.screen ?? '—' }, + { label: 'Capacity', value: product.capacity ?? '—' }, + { label: 'RAM', value: product.ram ?? '—' }, + ]; return ( + {isCompare && ( +
+ +
+ )}
-

+

-
- - Screen - - - {product.screen} - -
-
- - Capacity - - - {product.capacity} - -
-
- - RAM - - - {product.ram} - -
+ {renderSpecs(compareSpecs)}
{isClient && isInCart ? diff --git a/src/components/animate-ui/icons/x.tsx b/src/components/animate-ui/icons/x.tsx new file mode 100644 index 0000000..2bfb466 --- /dev/null +++ b/src/components/animate-ui/icons/x.tsx @@ -0,0 +1,126 @@ +'use client'; + +import * as React from 'react'; +import { motion, type Variants } from 'motion/react'; + +import { + getVariants, + useAnimateIconContext, + IconWrapper, + type IconProps, +} from '@/components/animate-ui/icons/icon'; + +type XProps = IconProps; + +const animations = { + default: { + line1: { + initial: { + rotate: 0, + transition: { ease: 'easeInOut', duration: 0.4 }, + }, + animate: { + rotate: 90, + transition: { ease: 'easeInOut', duration: 0.4 }, + }, + }, + line2: { + initial: { + rotate: 0, + transition: { ease: 'easeInOut', duration: 0.4, delay: 0.1 }, + }, + animate: { + rotate: 90, + transition: { ease: 'easeInOut', duration: 0.4, delay: 0.1 }, + }, + }, + } satisfies Record, + plus: { + line1: { + initial: { + rotate: 0, + x1: 6, + y1: 18, + x2: 18, + y2: 6, + transition: { ease: 'easeInOut', duration: 0.3, delay: 0.1 }, + }, + animate: { + rotate: 45, + x1: 7.1, + y1: 16.9, + x2: 16.9, + y2: 7.1, + transition: { ease: 'easeInOut', duration: 0.3, delay: 0.1 }, + }, + }, + line2: { + initial: { + rotate: 0, + x1: 6, + y1: 6, + x2: 18, + y2: 18, + transition: { ease: 'easeInOut', duration: 0.3 }, + }, + animate: { + rotate: 45, + x1: 7.1, + y1: 7.1, + x2: 16.9, + y2: 16.9, + transition: { ease: 'easeInOut', duration: 0.3 }, + }, + }, + } satisfies Record, +} as const; + +function IconComponent({ size, ...props }: XProps) { + const { controls } = useAnimateIconContext(); + const variants = getVariants(animations); + + return ( + + + + + ); +} + +function X(props: XProps) { + return ( + + ); +} + +export { animations, X, X as XIcon, type XProps, type XProps as XIconProps }; diff --git a/src/components/pages/ProductDetails/ProductDetails.tsx b/src/components/pages/ProductDetails/ProductDetails.tsx index b8da754..86fd904 100644 --- a/src/components/pages/ProductDetails/ProductDetails.tsx +++ b/src/components/pages/ProductDetails/ProductDetails.tsx @@ -66,7 +66,7 @@ const ProductDetails = ({
) => { - const product = action.payload; - + removeFromCompareByKey: ( + state, + action: PayloadAction<{ + id: string | number; + color: string; + capacity: string; + }>, + ) => { + const { id, color, capacity } = action.payload; state.items = state.items.filter( (i) => - !( - i.id === product.id && - i.color === product.color && - i.capacity === product.capacity - ), + !(i.itemId === id && i.color === color && i.capacity === capacity), ); }, @@ -51,7 +53,7 @@ const compareSlice = createSlice({ export const { addToCompare, - removeFromCompare, + removeFromCompareByKey, clearCompare, clearCompareByCategory, } = compareSlice.actions; diff --git a/src/lib/store.ts b/src/lib/store.ts index 00f78ff..574ca09 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -13,15 +13,13 @@ import { CategoryName } from '@/types/CategoryName'; const persistedReducers = combineReducers({ favourites: favouritesReducer, cart: cartReducer, - - // 👇 додаємо сюди compare, якщо хочеш зберігати його у localStorage compare: compareReducer, }); const persistConfig = { key: 'root', storage, - whitelist: [CategoryName.Favourites, CategoryName.Cart, 'compare'], + whitelist: [CategoryName.Favourites, CategoryName.Cart, CategoryName.Compare], }; const persistedReducer = persistReducer(persistConfig, persistedReducers); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1d4fe45..b91cf6b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -7,19 +7,29 @@ import { ProductType } from '@/types/CategoryType'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + export function mapProductTypeToProduct(product: ProductType): Product { return { - id: +`${product.id}-${product.color}-${product.capacity}`, - itemId: String(product.itemId), + id: product.id, + itemId: product.itemId, category: product.category, name: product.name || 'Без назви', image: Array.isArray(product.images) ? product.images[0] : product.image, - price: product.price ?? 0, - fullPrice: product.fullPrice ?? 0, + price: product.priceDiscount ?? 0, + fullPrice: product.priceRegular ?? 0, screen: product.screen ?? '—', capacity: product.capacity ?? '—', ram: product.ram ?? '—', color: product.color ?? '—', year: product.year ?? 2023, + + processor: product.processor ?? '—', + resolution: product.resolution ?? '—', + camera: product.camera ?? '—', + zoom: product.zoom ?? '—', + cell: + Array.isArray(product.cell) ? + product.cell.join(', ') + : (product.cell ?? '—'), }; } diff --git a/src/types/product.ts b/src/types/product.ts index 53bf0ec..e3cbc2d 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -1,11 +1,11 @@ -import { CategoryName } from './CategoryName'; +import { CategoryName } from '@/types/CategoryName'; export const VALID_CATEGORIES = Object.values(CategoryName).slice(1, 4); -type Category = (typeof VALID_CATEGORIES)[number]; +export type Category = (typeof VALID_CATEGORIES)[number]; export interface Product { - id: number; + id: number | string; category: Category; itemId: string; name: string; @@ -17,6 +17,12 @@ export interface Product { ram: string; year: number; image: string; + + resolution?: string; + processor?: string; + camera?: string; + zoom?: string; + cell?: string[]; } export function isProduct(item: unknown): item is Product { From 4137916e7b0863d615bf203c1bcea50c8e0ec62e Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Wed, 24 Sep 2025 15:21:16 +0300 Subject: [PATCH 03/11] third commit --- src/app/compare/ComparePage.tsx | 5 +- src/app/compare/CompareSlider.tsx | 2 +- .../Products/AddToCompareButton.tsx | 97 +++++++++++++++---- src/components/Products/ProductCart.tsx | 21 +++- src/components/UI/NavBar/CategoriesMenu.tsx | 2 +- src/components/UI/NavBar/CompareLink.tsx | 34 +++++++ src/components/UI/icons/Scale(Black).svg | 7 ++ src/components/UI/icons/Scale(Grey).svg | 7 ++ src/components/UI/icons/Scale(White).svg | 7 ++ src/components/UI/icons/Scale(Yellow).svg | 7 ++ .../ProductDetailsOrderOptions.tsx | 14 ++- src/lib/features/compare/compareSlice.tsx | 6 +- src/lib/utils.ts | 4 +- src/types/CategoryType.ts | 1 + src/types/NavBarRightComponents.tsx | 15 +-- src/types/product.ts | 2 +- 16 files changed, 181 insertions(+), 50 deletions(-) create mode 100644 src/components/UI/NavBar/CompareLink.tsx create mode 100644 src/components/UI/icons/Scale(Black).svg create mode 100644 src/components/UI/icons/Scale(Grey).svg create mode 100644 src/components/UI/icons/Scale(White).svg create mode 100644 src/components/UI/icons/Scale(Yellow).svg diff --git a/src/app/compare/ComparePage.tsx b/src/app/compare/ComparePage.tsx index 0d3fb05..c3e5e0d 100644 --- a/src/app/compare/ComparePage.tsx +++ b/src/app/compare/ComparePage.tsx @@ -32,11 +32,11 @@ const ComparePage = () => { const [selectedCategory, setSelectedCategory] = useState( '', ); + const dispatch = useDispatch(); useEffect(() => setMounted(true), []); - // Групуємо товари по категоріях const itemsByCategory = useMemo(() => { return compareItems.reduce>( (acc, item) => { @@ -48,12 +48,10 @@ const ComparePage = () => { ); }, [compareItems]); - // Список доступних категорій з товарами const availableCategories = Object.keys(itemsByCategory).filter( (cat) => itemsByCategory[cat as CategoryName].length > 0, ) as CategoryName[]; - // Автоматичний вибір категорії при зміні товарів useEffect(() => { if (!selectedCategory && availableCategories.length > 0) { setSelectedCategory(availableCategories[0]); @@ -62,7 +60,6 @@ const ComparePage = () => { (!itemsByCategory[selectedCategory] || itemsByCategory[selectedCategory].length === 0) ) { - // Якщо поточна категорія порожня, беремо наступну доступну setSelectedCategory(availableCategories[0] || ''); } }, [selectedCategory, itemsByCategory, availableCategories]); diff --git a/src/app/compare/CompareSlider.tsx b/src/app/compare/CompareSlider.tsx index 5c4f797..82330ea 100644 --- a/src/app/compare/CompareSlider.tsx +++ b/src/app/compare/CompareSlider.tsx @@ -120,7 +120,7 @@ export default function CompareSlider({ title, products }: CompareSliderProps) { key={`${product.itemId}-${product.color}-${product.capacity}`} > diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx index f7cc16f..fd19164 100644 --- a/src/components/Products/AddToCompareButton.tsx +++ b/src/components/Products/AddToCompareButton.tsx @@ -2,50 +2,113 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { addToCompare } from '@/lib/features/compare/compareSlice'; +import { + addToCompare, + removeFromCompareByKey, +} from '@/lib/features/compare/compareSlice'; import { RootState } from '@/lib/store'; +import Image from 'next/image'; +import ScaleBlack from '@/components/UI/icons/Scale(Black).svg'; +import ScaleGrey from '@/components/UI/icons/Scale(Grey).svg'; +import ScaleYellow from '@/components/UI/icons/Scale(Yellow).svg'; import { ProductType } from '@/types/CategoryType'; +import { useGetProductByIdQuery } from '@/lib/features/api/apiSlice'; interface AddToCompareButtonProps { product: ProductType; onClick?: () => void; + isCart: boolean; } const AddToCompareButton: React.FC = ({ product, onClick, + isCart, }) => { const dispatch = useDispatch(); const items = useSelector( (state: RootState) => state.persisted.compare.items, ); - const isAdded = items.some( + // Перевіряємо, чи товар вже в compare + const existsInCompare = items.some( (i) => i.id === product.id && i.color === product.color && i.capacity === product.capacity, ); - const handleAddToCompare = () => { - if (!isAdded) { - dispatch(addToCompare(product)); - onClick?.(); + // @ts-expect-error TS2339: The “itemId” property does not exist in the “ProductType” + const { data: fullProduct } = useGetProductByIdQuery(product.itemId, { + skip: existsInCompare || !isCart, + }); + + const handleToggle = () => { + if (existsInCompare) { + dispatch( + removeFromCompareByKey({ + id: product.id, + color: product.color, + capacity: product.capacity, + }), + ); + } else { + const productToAdd = isCart && fullProduct ? fullProduct : product; + dispatch(addToCompare(productToAdd)); } + onClick?.(); }; return ( - + <> + {!isCart ? + + : + } + ); }; diff --git a/src/components/Products/ProductCart.tsx b/src/components/Products/ProductCart.tsx index f40e594..f52a88f 100644 --- a/src/components/Products/ProductCart.tsx +++ b/src/components/Products/ProductCart.tsx @@ -13,6 +13,7 @@ import { RootState } from '@/lib/store'; import CloseComponent from '@/components/UI/ShoppingCartIcons/CloseComponent'; import { removeFromCompareByKey } from '@/lib/features/compare/compareSlice'; import { CategoryName } from '@/types/CategoryName'; +import AddToCompareButton from './AddToCompareButton'; interface ProductCartProps { product: Product; @@ -91,31 +92,41 @@ const ProductCart = ({ }} viewport={{ once: !disableOnce, amount: 0.3 }} className=" - border border-light-theme-border-color + border border-light-theme-border-color group rounded-2xl dark:bg-item-bg dark:border-dark-theme-border-color transition-shadow duration-700 hover:shadow-[0_3px_13px_0_rgba(23,32,49,0.4)] " > + {!isCompare && ( +
+ +
+ )} {isCompare && (
)} +
= ({ direction = 'row', onClose }) => { pathname === `/${category}` || (category === CategoryName.Home && pathname === '/') ) ? - 'after:absolute after:left-0 after:right-0 after:h-[3px] after:bg-light-theme-text-hover after:bottom-0 after:scale-x-100' + 'after:absolute after:left-0 after:right-0 after:h-[3px] after:bg-light-theme-text-hover after:bottom-0 after:scale-x-100' : ''; return ( diff --git a/src/components/UI/NavBar/CompareLink.tsx b/src/components/UI/NavBar/CompareLink.tsx new file mode 100644 index 0000000..c439a1e --- /dev/null +++ b/src/components/UI/NavBar/CompareLink.tsx @@ -0,0 +1,34 @@ +// #region Imports +import Image from 'next/image'; +import React from 'react'; + +import ScaleBlack from '@/components/UI/icons/Scale(Black).svg'; +import ScaleWhite from '@/components/UI/icons/Scale(White).svg'; +import Link from 'next/link'; +import { CategoryName } from '@/types/CategoryName'; +// #endregion + +const CompareLink: React.FC = () => { + return ( + <> + + ScaleWhite + ScaleBlack + + + ); +}; + +export default CompareLink; diff --git a/src/components/UI/icons/Scale(Black).svg b/src/components/UI/icons/Scale(Black).svg new file mode 100644 index 0000000..6fc8952 --- /dev/null +++ b/src/components/UI/icons/Scale(Black).svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/UI/icons/Scale(Grey).svg b/src/components/UI/icons/Scale(Grey).svg new file mode 100644 index 0000000..df6fcd5 --- /dev/null +++ b/src/components/UI/icons/Scale(Grey).svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/UI/icons/Scale(White).svg b/src/components/UI/icons/Scale(White).svg new file mode 100644 index 0000000..cb16e56 --- /dev/null +++ b/src/components/UI/icons/Scale(White).svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/UI/icons/Scale(Yellow).svg b/src/components/UI/icons/Scale(Yellow).svg new file mode 100644 index 0000000..2a27a46 --- /dev/null +++ b/src/components/UI/icons/Scale(Yellow).svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/pages/ProductDetails/ProductDetailsOrderOptions.tsx b/src/components/pages/ProductDetails/ProductDetailsOrderOptions.tsx index 0bcee58..c8d88f5 100644 --- a/src/components/pages/ProductDetails/ProductDetailsOrderOptions.tsx +++ b/src/components/pages/ProductDetails/ProductDetailsOrderOptions.tsx @@ -3,10 +3,10 @@ import Link from 'next/link'; import AddToCartButton from '@/components/Products/AddToCartButton'; import FavoriteButton from '@/components/Products/FavoriteButton'; -import AddToCompareButton from '@/components/Products/AddToCompareButton'; import OptionGroup from '@/components/pages/ProductDetails/ProductDetailsOptionGroup'; import { Product } from '@/types/product'; import { ProductType } from '@/types/CategoryType'; +import AddToCompareButton from '@/components/Products/AddToCompareButton'; interface Props { product: Product; @@ -62,6 +62,7 @@ const ProductDetailsOrderOptions: React.FC = ({ @@ -92,6 +93,7 @@ const ProductDetailsOrderOptions: React.FC = ({ @@ -130,10 +132,14 @@ const ProductDetailsOrderOptions: React.FC = ({
-
- + +
+
-
+
diff --git a/src/lib/features/compare/compareSlice.tsx b/src/lib/features/compare/compareSlice.tsx index 8ddac1e..c53ca53 100644 --- a/src/lib/features/compare/compareSlice.tsx +++ b/src/lib/features/compare/compareSlice.tsx @@ -34,10 +34,12 @@ const compareSlice = createSlice({ capacity: string; }>, ) => { + console.log('removeFromCompareByKey called with', action.payload); + console.log('current items before', state.items); + const { id, color, capacity } = action.payload; state.items = state.items.filter( - (i) => - !(i.itemId === id && i.color === color && i.capacity === capacity), + (i) => !(i.id === id && i.color === color && i.capacity === capacity), ); }, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b91cf6b..23edc64 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -11,9 +11,10 @@ export function cn(...inputs: ClassValue[]) { export function mapProductTypeToProduct(product: ProductType): Product { return { id: product.id, - itemId: product.itemId, + itemId: product.id, category: product.category, name: product.name || 'Без назви', + // @ts-expect-error TS2332: The type “string | undefined” cannot be assigned to the type “string”. image: Array.isArray(product.images) ? product.images[0] : product.image, price: product.priceDiscount ?? 0, fullPrice: product.priceRegular ?? 0, @@ -21,7 +22,6 @@ export function mapProductTypeToProduct(product: ProductType): Product { capacity: product.capacity ?? '—', ram: product.ram ?? '—', color: product.color ?? '—', - year: product.year ?? 2023, processor: product.processor ?? '—', resolution: product.resolution ?? '—', diff --git a/src/types/CategoryType.ts b/src/types/CategoryType.ts index 54beeaf..7e7ed18 100644 --- a/src/types/CategoryType.ts +++ b/src/types/CategoryType.ts @@ -31,6 +31,7 @@ export type ProductType = { camera?: string; zoom?: string; cell: string[]; + image?: string; }; export type ProductData = { diff --git a/src/types/NavBarRightComponents.tsx b/src/types/NavBarRightComponents.tsx index 411a291..c74f0e4 100644 --- a/src/types/NavBarRightComponents.tsx +++ b/src/types/NavBarRightComponents.tsx @@ -3,21 +3,10 @@ import ShoppingCartLink from '@/components/UI/NavBar/ShoppingCartLink'; import ThemeSwitcher from '@/components/UI/ThemeSwitcher'; import React from 'react'; import { CategoryName } from '@/types/CategoryName'; -import Link from 'next/link'; - -export const Compare = () => { - return ( - - Compare - - ); -}; +import CompareLink from '@/components/UI/NavBar/CompareLink'; export const NavBarRightComponents = [ - { id: CategoryName.Compare, element: }, + { id: CategoryName.Compare, element: }, { id: 'theme', element: }, { id: CategoryName.Favourites, element: }, { id: CategoryName.Cart, element: }, diff --git a/src/types/product.ts b/src/types/product.ts index e3cbc2d..aec47b2 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -22,7 +22,7 @@ export interface Product { processor?: string; camera?: string; zoom?: string; - cell?: string[]; + cell?: string[] | string; } export function isProduct(item: unknown): item is Product { From cab7c4f44fde1d2ddba7faa42a5a1555920d555e Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Wed, 24 Sep 2025 17:32:47 +0300 Subject: [PATCH 04/11] fifth commit --- .../Products/AddToCompareButton.tsx | 37 +++++++++---------- src/components/Products/ProductCart.tsx | 18 +++++---- src/components/UI/NavBar/CompareLink.tsx | 28 ++++++++++++-- src/lib/utils.ts | 33 ++++++++++++----- 4 files changed, 76 insertions(+), 40 deletions(-) diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx index fd19164..78861ed 100644 --- a/src/components/Products/AddToCompareButton.tsx +++ b/src/components/Products/AddToCompareButton.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { addToCompare, @@ -26,22 +26,21 @@ const AddToCompareButton: React.FC = ({ isCart, }) => { const dispatch = useDispatch(); - const items = useSelector( - (state: RootState) => state.persisted.compare.items, - ); - // Перевіряємо, чи товар вже в compare - const existsInCompare = items.some( - (i) => - i.id === product.id && - i.color === product.color && - i.capacity === product.capacity, + const existsInCompare = useSelector((state: RootState) => + state.persisted.compare.items.some( + (i) => + i.id === product.id && + i.color === product.color && + i.capacity === product.capacity, + ), ); - // @ts-expect-error TS2339: The “itemId” property does not exist in the “ProductType” + const { data: fullProduct } = useGetProductByIdQuery(product.itemId, { - skip: existsInCompare || !isCart, + skip: !isCart, }); + console.log('fullProduct', fullProduct); const handleToggle = () => { if (existsInCompare) { @@ -65,13 +64,13 @@ const AddToCompareButton: React.FC = ({
)); - const compareSpecs = isCompare ? [ @@ -100,12 +99,17 @@ const ProductCart = ({ " > {!isCompare && ( -
- +
+
+ +
)} {isCompare && ( diff --git a/src/components/UI/NavBar/CompareLink.tsx b/src/components/UI/NavBar/CompareLink.tsx index c439a1e..10d126b 100644 --- a/src/components/UI/NavBar/CompareLink.tsx +++ b/src/components/UI/NavBar/CompareLink.tsx @@ -1,31 +1,51 @@ // #region Imports import Image from 'next/image'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import ScaleBlack from '@/components/UI/icons/Scale(Black).svg'; import ScaleWhite from '@/components/UI/icons/Scale(White).svg'; import Link from 'next/link'; import { CategoryName } from '@/types/CategoryName'; +import { RootState } from '@/lib/store'; +import classNames from 'classnames'; + // #endregion const CompareLink: React.FC = () => { + const [isClient, setIsClient] = useState(false); + const compareCount = useSelector( + (state: RootState) => state.persisted.compare.items.length, + ); + + useEffect(() => { + setIsClient(true); + }, []); return ( <> - + ScaleWhite ScaleBlack + {isClient && compareCount > 0 && ( + + {compareCount > 9 ? '9+' : compareCount} + + )} ); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 23edc64..56c8d84 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,7 @@ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; -import { Product } from '@/types/product'; +import { Category, Product } from '@/types/product'; import { ProductType } from '@/types/CategoryType'; export function cn(...inputs: ClassValue[]) { @@ -10,26 +10,39 @@ export function cn(...inputs: ClassValue[]) { export function mapProductTypeToProduct(product: ProductType): Product { return { - id: product.id, - itemId: product.id, - category: product.category, + // Ідентифікатори для синхронізації в Redux + id: product.id, // може бути string | number + itemId: product.id, // використовується для порівняння + + category: product.category as Category, name: product.name || 'Без назви', - // @ts-expect-error TS2332: The type “string | undefined” cannot be assigned to the type “string”. - image: Array.isArray(product.images) ? product.images[0] : product.image, + + // Гарантуємо, що завжди є картинка + image: + Array.isArray(product.images) && product.images.length > 0 ? + product.images[0] + : (product.image ?? ''), + + // Ціни price: product.priceDiscount ?? 0, fullPrice: product.priceRegular ?? 0, + + // Основні характеристики screen: product.screen ?? '—', capacity: product.capacity ?? '—', ram: product.ram ?? '—', color: product.color ?? '—', + // Додаткові характеристики processor: product.processor ?? '—', resolution: product.resolution ?? '—', camera: product.camera ?? '—', zoom: product.zoom ?? '—', - cell: - Array.isArray(product.cell) ? - product.cell.join(', ') - : (product.cell ?? '—'), + + // Залишаємо як масив для коректної синхронізації + cell: product.cell ?? [], + + // Додаткові обов'язкові поля + year: new Date().getFullYear(), // або 0, якщо хочеш }; } From 767d75aa8827fcbfdab90593a0d8b53bbd82e551 Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Wed, 24 Sep 2025 17:48:17 +0300 Subject: [PATCH 05/11] six commit --- .../Products/AddToCompareButton.tsx | 44 ++++++++++++------- src/components/Products/ProductCart.tsx | 7 ++- src/components/UI/NavBar/CompareLink.tsx | 1 - src/lib/features/compare/compareSlice.tsx | 21 +++++++-- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx index 78861ed..37b4e70 100644 --- a/src/components/Products/AddToCompareButton.tsx +++ b/src/components/Products/AddToCompareButton.tsx @@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { addToCompare, removeFromCompareByKey, + toggleCompare, } from '@/lib/features/compare/compareSlice'; import { RootState } from '@/lib/store'; import Image from 'next/image'; @@ -13,6 +14,7 @@ import ScaleGrey from '@/components/UI/icons/Scale(Grey).svg'; import ScaleYellow from '@/components/UI/icons/Scale(Yellow).svg'; import { ProductType } from '@/types/CategoryType'; import { useGetProductByIdQuery } from '@/lib/features/api/apiSlice'; +import { toast } from 'sonner'; interface AddToCompareButtonProps { product: ProductType; @@ -36,28 +38,38 @@ const AddToCompareButton: React.FC = ({ ), ); // @ts-expect-error TS2339: The “itemId” property does not exist in the “ProductType” - - const { data: fullProduct } = useGetProductByIdQuery(product.itemId, { - skip: !isCart, - }); - console.log('fullProduct', fullProduct); + // const { data: fullProduct } = useGetProductByIdQuery(product.itemId, { + // skip: !isCart, + // }); const handleToggle = () => { + dispatch(toggleCompare(product)); + if (existsInCompare) { - dispatch( - removeFromCompareByKey({ - id: product.id, - color: product.color, - capacity: product.capacity, - }), - ); + toast(`${product.name} removed from compare`); } else { - const productToAdd = isCart && fullProduct ? fullProduct : product; - dispatch(addToCompare(productToAdd)); + toast(`${product.name} added to compare`); } + onClick?.(); }; + // const handleToggle = () => { + // if (existsInCompare) { + // dispatch( + // removeFromCompareByKey({ + // id: product.id, + // color: product.color, + // capacity: product.capacity, + // }), + // ); + // } else { + // const productToAdd = isCart && fullProduct ? fullProduct : product; + // dispatch(addToCompare(productToAdd)); + // } + // onClick?.(); + // }; + return ( <> {!isCart ? @@ -77,14 +89,14 @@ const AddToCompareButton: React.FC = ({ width={30} height={30} alt={existsInCompare ? 'Remove from compare' : 'Add to compare'} - className="dark:hidden" + className="dark:hidden transition-all duration-500 hover:scale-[1.2]" /> {existsInCompare : + disabled={isBeginning} + > + + + + - + disabled={isEnd} + > + + + + + + )}
diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx index 37b4e70..782173b 100644 --- a/src/components/Products/AddToCompareButton.tsx +++ b/src/components/Products/AddToCompareButton.tsx @@ -89,14 +89,14 @@ const AddToCompareButton: React.FC = ({ width={30} height={30} alt={existsInCompare ? 'Remove from compare' : 'Add to compare'} - className="dark:hidden transition-all duration-500 hover:scale-[1.2]" + className="dark:hidden transition-all duration-300 hover:scale-[1.2]" /> {existsInCompare : + + {product.name} + + +

+ + {product.name} + +

+ +
+
+

+ ${product.priceDiscount} +

+

+ ${product.priceRegular} +

+
+
+
+
+ {specList.map((spec) => { + const raw = product[spec.key]; + const value = + Array.isArray(raw) ? raw.join(' | ') + : raw !== undefined && raw !== null ? String(raw) + : '-'; + + return ( +
+ + {spec.label} + + + {value} + +
+ ); + })} +
+ + ); +}; + +export default CompareItemCard; diff --git a/src/app/compare/CompareSlider.tsx b/src/app/compare/CompareList.tsx similarity index 60% rename from src/app/compare/CompareSlider.tsx rename to src/app/compare/CompareList.tsx index 35d6072..353f5a8 100644 --- a/src/app/compare/CompareSlider.tsx +++ b/src/app/compare/CompareList.tsx @@ -7,23 +7,36 @@ import { motion } from 'framer-motion'; import 'swiper/css'; import 'swiper/css/navigation'; -import ProductCart from '@/components/Products/ProductCart'; -import { Product } from '@/types/product'; +import CompareItemCard from './CompareItemCard'; +import { useSelector } from 'react-redux'; +import { ProductType } from '@/types/CategoryType'; +import { RootState } from '@/lib/store'; interface CompareSliderProps { - title: string; - products: Product[]; + title?: string; } -export default function CompareSlider({ title, products }: CompareSliderProps) { +export default function CompareList({ title }: CompareSliderProps) { const prevRef = useRef(null); const nextRef = useRef(null); const [isBeginning, setIsBeginning] = useState(true); const [isEnd, setIsEnd] = useState(false); + const compareItems = useSelector( + (state: RootState) => state.persisted.compare.items, + ) as ProductType[]; + + const filteredItems = + title ? + compareItems.filter((item: ProductType) => item.category === title) + : compareItems; + + if (filteredItems.length === 0) return null; + const normalizeTitle = + title!.slice(0, 1).toUpperCase() + title!.slice(1).toLowerCase(); return (
-
+
- {title} + {normalizeTitle}
- {products.length > 4 && ( + {filteredItems.length > 4 && ( <>
- - { - if (prevRef.current && nextRef.current) { - // @ts-expect-error: Swiper types don't know about refs - swiper.params.navigation.prevEl = prevRef.current; - // @ts-expect-error: Swiper types don't know about refs - swiper.params.navigation.nextEl = nextRef.current; - } - }} - onInit={(swiper) => { - swiper.navigation.init(); - swiper.navigation.update(); - setIsBeginning(swiper.isBeginning); - setIsEnd(swiper.isEnd); - }} - onSlideChange={(swiper) => { - setIsBeginning(swiper.isBeginning); - setIsEnd(swiper.isEnd); - }} - breakpoints={{ - 0: { slidesPerView: 1 }, - 500: { slidesPerView: 2 }, - 640: { slidesPerView: 2 }, - 900: { slidesPerView: 3 }, - 1200: { slidesPerView: 4 }, - }} - > - {products.map((product) => { - return ( +
+ { + if (prevRef.current && nextRef.current) { + // @ts-expect-error: Swiper types don't know about refs + swiper.params.navigation.prevEl = prevRef.current; + // @ts-expect-error: Swiper types don't know about refs + swiper.params.navigation.nextEl = nextRef.current; + } + }} + onInit={(swiper) => { + swiper.navigation.init(); + swiper.navigation.update(); + setIsBeginning(swiper.isBeginning); + setIsEnd(swiper.isEnd); + }} + onSlideChange={(swiper) => { + setIsBeginning(swiper.isBeginning); + setIsEnd(swiper.isEnd); + }} + breakpoints={{ + 0: { slidesPerView: 1 }, + 500: { slidesPerView: 2 }, + 640: { slidesPerView: 2 }, + 990: { slidesPerView: 3 }, + 1100: { slidesPerView: 4 }, + }} + className="multiple-slide-carousel" + > + {filteredItems.map((product, idx) => ( - - ); - })} - + ))} + +
); } diff --git a/src/app/compare/ComparePage.tsx b/src/app/compare/ComparePage.tsx index c3e5e0d..79fea24 100644 --- a/src/app/compare/ComparePage.tsx +++ b/src/app/compare/ComparePage.tsx @@ -3,11 +3,8 @@ import React, { useMemo, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/lib/store'; -import CompareSlider from '@/app/compare/CompareSlider'; import { ProductType } from '@/types/CategoryType'; import { CategoryName } from '@/types/CategoryName'; -import { mapProductTypeToProduct } from '@/lib/utils'; -import { Product } from '@/types/product'; import SelectArrow from '@/components/UI/SelectArrow'; import { clearCompare, @@ -15,6 +12,7 @@ import { } from '@/lib/features/compare/compareSlice'; import CategoryHeader from '@/components/UI/CategoryHeader'; import ActionButton from '@/components/UI/ActionButton'; +import CompareList from './CompareList'; const categoryLabels: Partial> = { [CategoryName.Phones]: 'Phones', @@ -66,12 +64,12 @@ const ComparePage = () => { if (!mounted) return null; - const products: Product[] = + const productTypes = selectedCategory && itemsByCategory[selectedCategory] ? - itemsByCategory[selectedCategory].map(mapProductTypeToProduct) + itemsByCategory[selectedCategory] : []; - const hasProducts = products.length > 0; + const hasProducts = productTypes.length > 0; if (!hasProducts) { return (
@@ -86,80 +84,86 @@ const ComparePage = () => { ); } return ( -
+
{hasProducts && ( <> {/* Вибір категорії */} -
- - + setSelectedCategory(e.target.value as CategoryName) + } + className="w-full appearance-none bg-light-theme-bg-dark dark:bg-dark-theme-btn-selected px-4 py-2 pr-10 font-[600] text-[14px] leading-[21px] text-light-theme-text dark:text-text-light border border-light-theme-border-active focus:outline-none focus:ring-0 dark:border-dark-theme-btn-selected cursor-pointer" - > - {availableCategories.map((category) => ( - - ))} - -
- + > + {availableCategories.map((category) => ( + + ))} + +
+ +
+
+
+ + +
- {/* Слайдер порівняння */} - - - {/* Кнопки очищення */} -
- - - -
+ )}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 40ab380..0978d95 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -61,10 +61,7 @@ export default function RootLayout({ }, }} /> -
+
diff --git a/src/components/Layout/NavBar/HydradetIcons.tsx b/src/components/Layout/NavBar/HydradetIcons.tsx index 6508eeb..fb2c771 100644 --- a/src/components/Layout/NavBar/HydradetIcons.tsx +++ b/src/components/Layout/NavBar/HydradetIcons.tsx @@ -5,6 +5,8 @@ import { HydrationProvider } from '@/lib/HydrationProvider'; import { IconsSkeleton } from './IconsSkeleton'; import FavouritesLink from '@/components/UI/NavBar/FavouritesLink'; import ShoppingCartLink from '@/components/UI/NavBar/ShoppingCartLink'; +import ThemeSwitcher from '@/components/UI/ThemeSwitcher'; +import CompareLink from '@/components/UI/NavBar/CompareLink'; type IconsProps = { onClose?: () => void; @@ -14,6 +16,35 @@ export function HydratedIcons({ onClose }: IconsProps) { return ( }> {/* Цей блок буде показано ПІСЛЯ гідратації */} + +
+
+ +
+ +
+ +
+
= ({ onClose }) => { direction="col" />
-
- -
- - {/*
- - -
*/}
); diff --git a/src/components/Layout/NavBar/NavBar.tsx b/src/components/Layout/NavBar/NavBar.tsx index babe9d8..be6ee57 100644 --- a/src/components/Layout/NavBar/NavBar.tsx +++ b/src/components/Layout/NavBar/NavBar.tsx @@ -30,7 +30,7 @@ const NavBar: React.FC = () => { return ( <> -
diff --git a/src/components/UI/SelectArrow.tsx b/src/components/UI/SelectArrow.tsx index e81fea8..4bf1670 100644 --- a/src/components/UI/SelectArrow.tsx +++ b/src/components/UI/SelectArrow.tsx @@ -2,12 +2,17 @@ import React from 'react'; type Props = { className?: string; + isCompare?: boolean; + isCategory?: boolean; }; -const SelectArrow: React.FC = ({ className }) => { +const SelectArrow: React.FC = ({ className, isCategory, isCompare }) => { return ( Date: Fri, 26 Sep 2025 00:17:47 +0300 Subject: [PATCH 10/11] deleted shadcn, tailwind-scrollbar --- package.json | 1 - pnpm-lock.yaml | 41 ----------------------------------------- 2 files changed, 42 deletions(-) diff --git a/package.json b/package.json index a8ce88e..871f433 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "stripe": "^18.5.0", "swiper": "^12.0.1", "tailwind-merge": "^3.3.1", - "tailwind-scrollbar": "^4.0.2", "three": "^0.180.0", "tsparticles": "^3.9.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f2d0a0..7bac4f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,9 +100,6 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 - tailwind-scrollbar: - specifier: ^4.0.2 - version: 4.0.2(react@19.1.0)(tailwindcss@4.1.13) three: specifier: ^0.180.0 version: 0.180.0 @@ -2115,12 +2112,6 @@ packages: integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==, } - '@types/prismjs@1.26.5': - resolution: - { - integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==, - } - '@types/react-dom@19.1.9': resolution: { @@ -5337,14 +5328,6 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } - prism-react-renderer@2.4.1: - resolution: - { - integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==, - } - peerDependencies: - react: '>=16.0.0' - promise-worker-transferable@1.0.4: resolution: { @@ -6170,15 +6153,6 @@ packages: integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==, } - tailwind-scrollbar@4.0.2: - resolution: - { - integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==, - } - engines: { node: '>=12.13.0' } - peerDependencies: - tailwindcss: 4.x - tailwindcss@4.1.13: resolution: { @@ -7999,8 +7973,6 @@ snapshots: '@types/offscreencanvas@2019.7.3': {} - '@types/prismjs@1.26.5': {} - '@types/react-dom@19.1.9(@types/react@19.1.13)': dependencies: '@types/react': 19.1.13 @@ -10109,12 +10081,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prism-react-renderer@2.4.1(react@19.1.0): - dependencies: - '@types/prismjs': 1.26.5 - clsx: 2.1.1 - react: 19.1.0 - promise-worker-transferable@1.0.4: dependencies: is-promise: 2.2.2 @@ -10706,13 +10672,6 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-scrollbar@4.0.2(react@19.1.0)(tailwindcss@4.1.13): - dependencies: - prism-react-renderer: 2.4.1(react@19.1.0) - tailwindcss: 4.1.13 - transitivePeerDependencies: - - react - tailwindcss@4.1.13: {} tapable@2.2.3: {} From d1cfa194e95eaa3c2ea25d7965b0dc8b33ba4f46 Mon Sep 17 00:00:00 2001 From: Yevhenii Kaliaiev Date: Fri, 26 Sep 2025 00:56:04 +0300 Subject: [PATCH 11/11] arrow --- src/app/compare/CompareList.tsx | 7 +------ src/app/compare/ComparePage.tsx | 1 - src/components/Products/AddToCompareButton.tsx | 1 - src/components/Products/CategorySortSelectors.tsx | 1 + src/components/Products/ProductCart.tsx | 1 - src/components/UI/AddOrNavToCartButton.tsx | 1 - src/components/home/CategoryShowcase/CategoryShowcase.tsx | 1 - 7 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/compare/CompareList.tsx b/src/app/compare/CompareList.tsx index 67078f4..7a0a0e7 100644 --- a/src/app/compare/CompareList.tsx +++ b/src/app/compare/CompareList.tsx @@ -15,14 +15,9 @@ import { RootState } from '@/lib/store'; interface CompareSliderProps { title?: string; showSpecs: boolean; - differingSpecs?: ProductType[]; } -export default function CompareList({ - title, - showSpecs, - differingSpecs, -}: CompareSliderProps) { +export default function CompareList({ title, showSpecs }: CompareSliderProps) { const prevRef = useRef(null); const nextRef = useRef(null); const [isBeginning, setIsBeginning] = useState(true); diff --git a/src/app/compare/ComparePage.tsx b/src/app/compare/ComparePage.tsx index d46f7f0..a1cc2a1 100644 --- a/src/app/compare/ComparePage.tsx +++ b/src/app/compare/ComparePage.tsx @@ -166,7 +166,6 @@ const ComparePage = () => { )} diff --git a/src/components/Products/AddToCompareButton.tsx b/src/components/Products/AddToCompareButton.tsx index cf72ae9..292c0f1 100644 --- a/src/components/Products/AddToCompareButton.tsx +++ b/src/components/Products/AddToCompareButton.tsx @@ -11,7 +11,6 @@ import Image from 'next/image'; import ScaleBlack from '@/components/UI/icons/Scale(Black).svg'; import ScaleGrey from '@/components/UI/icons/Scale(Grey).svg'; import ScaleYellow from '@/components/UI/icons/Scale(Yellow).svg'; -import { useGetProductByIdQuery } from '@/lib/features/api/apiSlice'; import { toast } from 'sonner'; import { ProductType } from '@/types/CategoryType'; diff --git a/src/components/Products/CategorySortSelectors.tsx b/src/components/Products/CategorySortSelectors.tsx index eb7b1ae..e13907f 100644 --- a/src/components/Products/CategorySortSelectors.tsx +++ b/src/components/Products/CategorySortSelectors.tsx @@ -136,6 +136,7 @@ const CategorySortSelectors: React.FC = ({ ))} +
diff --git a/src/components/Products/ProductCart.tsx b/src/components/Products/ProductCart.tsx index 2daf0c7..f9c7a1a 100644 --- a/src/components/Products/ProductCart.tsx +++ b/src/components/Products/ProductCart.tsx @@ -9,7 +9,6 @@ import FavoriteButton from '@/components/Products/FavoriteButton'; import AddOrNavToCartButton from '../UI/AddOrNavToCartButton'; import AddToCompareButton from './AddToCompareButton'; import { useGetProductByIdQuery } from '@/lib/features/api/apiSlice'; -import { mapDetailsToSummary } from '@/lib/mappers'; interface ProductCartProps { product: Product; diff --git a/src/components/UI/AddOrNavToCartButton.tsx b/src/components/UI/AddOrNavToCartButton.tsx index bb32239..c3203a0 100644 --- a/src/components/UI/AddOrNavToCartButton.tsx +++ b/src/components/UI/AddOrNavToCartButton.tsx @@ -18,7 +18,6 @@ export default function AddOrNavToCartButton({ product }: Props) { (item) => item.productId === product.itemId, ), ); - console.log(product.itemId); useEffect(() => { setIsClient(true); diff --git a/src/components/home/CategoryShowcase/CategoryShowcase.tsx b/src/components/home/CategoryShowcase/CategoryShowcase.tsx index ef1848f..dd94427 100644 --- a/src/components/home/CategoryShowcase/CategoryShowcase.tsx +++ b/src/components/home/CategoryShowcase/CategoryShowcase.tsx @@ -21,7 +21,6 @@ const containerVariants = { const CategoryShowcase: React.FC = ({ categories }) => { // TODO remove comments - // console.log(categories); // const { data: categories, isLoading, isError } = useGetCategoriesQuery(); // if (isLoading) {