Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement 289 favorite page #351

Merged
merged 9 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading