Skip to content

Commit

Permalink
Merge pull request #351 from kirill-k88/enhancement_289_favorite-page
Browse files Browse the repository at this point in the history
Enhancement 289 favorite page
  • Loading branch information
kirill-k88 authored May 7, 2024
2 parents 794d2dd + 5723f7c commit bf1e343
Show file tree
Hide file tree
Showing 14 changed files with 361 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/app/router/AppRouter/ui/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ComparePage from '@/pages/ComparePage/ComparePage'
import ContactsPage from '@/pages/ContactsPage/ContactsPage'
import DeliveryPage from '@/pages/DeliveryPage/DeliveryPage'
import ErrorPage from '@/pages/ErrorPage/ErrorPage'
import FavoritesPage from '@/pages/FavoritesPage/FavoritesPage'
import { FavoritesPage } from '@/pages/FavoritesPage/FavoritesPage'
import { FeedbackPage } from '@/pages/FeedbackPage/FeedbackPage'
import FormReturnPage from '@/pages/FormReturnPage/FormReturnPage'
import HelpPage from '@/pages/HelpPage/HelpPage'
Expand Down
75 changes: 75 additions & 0 deletions src/entities/Favorite/model/functions/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { FAVORITE_PRODUCTS_LIMIT } from '@/shared/constants/constants'
import { SESSION_STORAGE } from '@/shared/constants/sessionStorage'

import type { TProduct } from '../types/types'

/**
* Ф-я проверяет наличие продукта в массив избранных продуктов в session storage
* @param {TProduct} product продукт
* @return {boolean} true/false в зависимости от нахождения в избранном
*/
export const isInFavoriteProducts = (product: TProduct): boolean => {
const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]'
const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[]

if (product && product.slug && includesProduct(product, favoriteProducts)) {
return true
}

return false
}

/**
* Ф-я добавляет продукт в массив избранных продуктов в session storage
* @param {TProduct} product продукт
*/
export const addToFavoriteProducts = (product: TProduct): void => {
const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]'
const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[]

if (product && product.slug && !includesProduct(product, favoriteProducts)) {
if (favoriteProducts.length === FAVORITE_PRODUCTS_LIMIT) {
favoriteProducts.shift()
}
favoriteProducts.push(product)

sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts))
window.dispatchEvent(new Event('storage'))
}
}

/**
* Ф-я удаляет продукт из массива избранных продуктов в session storage, если он в нем есть
* @param {TProduct} product продукт
*/
export const removeFromFavoriteProducts = (product: TProduct): void => {
const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]'
const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[]

if (product && product.slug && includesProduct(product, favoriteProducts)) {
favoriteProducts.splice(indexOfProduct(product, favoriteProducts), 1)

sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts))
window.dispatchEvent(new Event('storage'))
}
}

function includesProduct(product: TProduct, favoriteProducts: TProduct[]): boolean {
return favoriteProducts.some(p => p.slug === product.slug)
}

function indexOfProduct(product: TProduct, favoriteProducts: TProduct[]): number {
return favoriteProducts.findIndex(p => p.slug === product.slug)
}

/**
* Функция возвращает список избранных товаров favoriteProducts из текущей сессии session storage.
*
* @return {TProduct[]} - массив продуктов в избранном
*/
export const getFavoriteProductsFromStorage = (): TProduct[] => {
const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]'
const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[]

return favoriteProducts
}
28 changes: 28 additions & 0 deletions src/entities/Favorite/model/hooks/useFavorite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react'

import { getFavoriteProductsFromStorage } from '../functions/functions'
import type { TProduct } from '../types/types'

/**
* Hook для получения продуктов из избранного
*
* @returns {TProduct[]} состояние favoriteProducts с массивом продуктов в избранном
*/
export const useFavorite = () => {
const [favoriteProducts, setFavoriteProducts] = useState<TProduct[]>([])

useEffect(() => {
setFavoriteProducts(getFavoriteProductsFromStorage())
window.addEventListener('storage', handleStorage)

return () => {
window.removeEventListener('storage', handleStorage)
}
}, [])

const handleStorage = () => {
setFavoriteProducts(getFavoriteProductsFromStorage())
}

return favoriteProducts
}
34 changes: 34 additions & 0 deletions src/entities/Favorite/model/hooks/useWithFavorie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react'

import {
addToFavoriteProducts,
isInFavoriteProducts,
removeFromFavoriteProducts
} from '../functions/functions'
import type { TProduct } from '../types/types'

/**
* Hook для добавления/удаления, проверки наличия продукта в избранном
*
* @param {TProduct} product - продукт
* @returns {object} - состояние isLiked нахождения продукта в избранном и функцию handleLike для добавления/удаления в/из избранное
*/
export const useWithFavorite = (product: TProduct) => {
const [isLiked, setIsLiked] = useState<boolean>(isInFavoriteProducts(product))

useEffect(() => {
setIsLiked(isInFavoriteProducts(product))
}, [product])

const handleLike = () => {
if (!isLiked) {
addToFavoriteProducts(product)
setIsLiked(true)
} else {
removeFromFavoriteProducts(product)
setIsLiked(false)
}
}

return { isLiked, handleLike }
}
30 changes: 30 additions & 0 deletions src/entities/Favorite/model/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export interface IObjectWithImage {
image: string
index?: number
}

export type TImgList = Array<IObjectWithImage>

export type TProduct = {
label_popular: boolean
label_hit: boolean
id: number
category: string
brand: string
price: number
name: string
slug: string
description: string
code: number
wb_urls: string
quantity: number
is_deleted: boolean
wholesale: number
images: TImgList
}

export type TProductSchema = {
product: TProduct
isLoading?: boolean
error?: string | string[]
}
29 changes: 27 additions & 2 deletions src/pages/FavoritesPage/FavoritesPage.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
.heading {
align-self: flex-start;
@use '../../shared/styles/utils/mixins' as media;

.pageDescriptor {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}

.favoritePage__container {
width: 100%;
display: flex;
justify-content: start;
align-items: start;
gap: 10px;

@include media.respond-to('middle') {
flex-direction: column;
gap: 10px;
}
}

.favoritePage__list {
max-width: calc(292px * 3 + 60px);
display: flex;
flex-wrap: wrap;
gap: 30px;
}
90 changes: 84 additions & 6 deletions src/pages/FavoritesPage/FavoritesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,97 @@
import { FC, KeyboardEvent, Suspense, useState } from 'react'

import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent'
import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite'
import SideBarButton from '@/entities/SideBarButton'
import SideBarMenuModal from '@/features/SideBarMenuModal'
import { Routes } from '@/shared/config/routerConfig/routes'
import { useResize } from '@/shared/libs/hooks/useResize'
import { ECardView } from '@/shared/model/types/common'
import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs'
import Heading from '@/shared/ui/Heading/Heading'
import Subheading from '@/shared/ui/Subheading/Subheading'
import Modal from '@/shared/ui/Modal/Modal'
import Spinner from '@/shared/ui/Spinner/Spinner'
import { ProductsList } from '@/widgets/ProductsList/ProductsList'
import SideBarMenu from '@/widgets/SideBarMenu'

import styles from './FavoritesPage.module.scss'

const links = [
{ heading: 'Главная', href: '/' },
{ heading: 'Личный Кабинет', href: Routes.LOGIN },
{ heading: 'Избранные товары', href: '' }
]

/**
* Страница с избранными товарами
*/
const FavoritesPage = () => {
export const FavoritesPage: FC = () => {
const favoriteProducts = useFavorite()
const { isScreenMd } = useResize()
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
const [isModalClosing, setIsModalClosing] = useState<boolean>(false)
const [user, setUser] = useState<string>('Elon Musk') // TODO получать пользователя из редакса

const handleClick = () => {
setIsModalOpen(true)
}

const changeModalState = () => {
setIsModalOpen(!isModalOpen)
}

const handleLogOut = () => {
setUser('')
}

const handleKeyUp = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
handleLogOut()
}
}

return (
<WrapperForMainContent>
<Heading className={styles.heading}>Избранные товары</Heading>
<Subheading>В разработке</Subheading>
<div className={styles.pageDescriptor}>
<Heading>Избранные товары</Heading>
<Breadcrumbs links={links} />
</div>
<div className={styles.favoritePage__container}>
{isScreenMd ? (
<SideBarMenu user={user} handleLogOut={handleLogOut} />
) : (
<SideBarButton onClick={handleClick} />
)}
<section className={styles.favoritePage__list}>
<ProductsList
items={{
category_name: '',
count: favoriteProducts.length,
next: '',
previous: '',
results: favoriteProducts
}}
cardView={ECardView.GRID}
/>
</section>
</div>
{isModalOpen && (
<Modal
isModalOpen={isModalOpen}
onClose={changeModalState}
isModalClosing={isModalClosing}
setIsModalClosing={setIsModalClosing}>
<Suspense fallback={<Spinner />}>
<SideBarMenuModal
handleClose={changeModalState}
onKeyUp={handleKeyUp}
handleLogOut={handleLogOut}
user={user}
/>
</Suspense>
</Modal>
)}
</WrapperForMainContent>
)
}

export default FavoritesPage
3 changes: 3 additions & 0 deletions src/shared/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export const REDUCER_CATEGORIES_PRODUCTS = 'shopCategoriesProducts'
//Product page
export const VIEWED_PRODUCTS_LIMIT = 10

//Favorite page
export const FAVORITE_PRODUCTS_LIMIT = 10

//Feedback form
export const NAME_LENGTH_MIN_LIMIT = 2
export const NAME_LENGTH_MAX_LIMIT = 30
Expand Down
3 changes: 2 additions & 1 deletion src/shared/constants/sessionStorage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum SESSION_STORAGE {
VIEWED = 'viewedProducts'
VIEWED = 'viewedProducts',
FAVORITE = 'favoriteProducts'
}
7 changes: 4 additions & 3 deletions src/widgets/Footer/Footer.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

.footer {
background-color: var.$footer-bg;
width: calc(100vw - 20px);
width: 100%;
display: flex;
justify-content: center;
color: var.$white;

&__container {
display: flex;
max-width: 100%;
width: 100%;
max-width: 1400px;
flex-direction: column;
}

Expand Down Expand Up @@ -178,9 +179,9 @@
}

&__bottom-wrapper {
width: 100%;
display: flex;
gap: 5px;
width: 1370px;
height: 62px;
align-items: center;
justify-content: space-between;
Expand Down
7 changes: 2 additions & 5 deletions src/widgets/Product/Product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type FC, useState } from 'react'

import IconCart from '@/assets/icons/IconCart.svg'
import { useProductInCart } from '@/entities/CartEntity/model/hooks/cartHooks'
import { useWithFavorite } from '@/entities/Favorite/model/hooks/useWithFavorie'
import { CardPreviewHeader } from '@/features/CardPreviewHeader/CardPreviewHeader'
import { ProductAvailability } from '@/features/ProductAvailability/ProductAvailability'
import { ProductImgCarousel } from '@/features/ProductImgCarousel/ProductImgCarousel'
Expand All @@ -17,15 +18,11 @@ import { PopupImg } from './ui/PopupImg/PopupImg'
* @param {TProductProps} product - информация о выбранном товаре
*/
export const Product: FC<TProductProps> = ({ product }) => {
const [isLiked, setIsLiked] = useState<boolean>(false)
const { isLiked, handleLike } = useWithFavorite(product)
const [isInCompared, setIsInCompared] = useState<boolean>(false)
const { isInCart, handleAddToCart } = useProductInCart(product.slug, product.id)
const [showPopup, setShowPopup] = useState<boolean>(false)

const handleLike = () => {
setIsLiked(!isLiked)
}

const handleAddToCompared = () => {
setIsInCompared(!isInCompared)
}
Expand Down
Loading

0 comments on commit bf1e343

Please sign in to comment.