Skip to content

Commit

Permalink
Сверстан виджет Карточка товара на странице товара, фича Карусель фот…
Browse files Browse the repository at this point in the history
…ографий товара. Пока без API
  • Loading branch information
kirill-k88 committed Feb 23, 2024
1 parent f225fc6 commit e803a09
Show file tree
Hide file tree
Showing 20 changed files with 1,121 additions and 18 deletions.
38 changes: 38 additions & 0 deletions src/features/ProductImgCarousel/ProductImgCarousel.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@use '@/shared/styles/utils/variables' as var;
@use '@/shared/styles/utils/mixins' as media;

.wrapper {
height: 670px;
display: flex;
background-color: var.$white;

@include media.respond-to('middle') {
flex-direction: column-reverse;
align-items: center;
}

.carouselwrapper {
width: 500px;
height: 100%;

@include media.respond-to('middle') {
width: 300px;
}

.maincontainer {
width: 100%;
height: 100%;
background-color: var.$white;
display: flex;
flex-direction: row;
align-items: center;
border-top: 2px solid var.$bg-subscribe;
border-bottom: 2px solid var.$bg-subscribe;

@include media.respond-to('middle') {
border-top: none;
border-bottom: none;
}
}
}
}
53 changes: 53 additions & 0 deletions src/features/ProductImgCarousel/ProductImgCarousel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Meta, StoryObj } from '@storybook/react'

import { ProductImgCarousel } from './ProductImgCarousel'

export default { component: ProductImgCarousel }

const Metadata = {
title: 'features/ProductImgCarousel',
component: ProductImgCarousel,
parameters: {
product: 'данные о товаре'
}
} as Meta<typeof ProductImgCarousel>

type Story = StoryObj<typeof Metadata>

export const Default: Story = {
args: {
imgList: [
{
image: 'https://ir.ozone.ru/s3/multimedia-b/wc1000/6661991531.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-9/wc1000/6661991493.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-j/wc1000/6661991575.jpg'
},
{
image:
'https://maxboom.ru/image/cachewebp/catalog/vol862/part86256/86256350/images/big/1-1000x1000.webp'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-y/wc1000/6661991482.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-v/wc1000/6661991551.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-1-x/wc1000/6928616361.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-n/wc1000/6767151887.jpg'
},
{
image: 'https://ir.ozone.ru/s3/multimedia-d/wc1000/6767151877.jpg'
}
],
setShowPopup: () => {
alert('Должен всплывать попап')
}
}
}
26 changes: 26 additions & 0 deletions src/features/ProductImgCarousel/ProductImgCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState, type FC } from 'react'

import { TProductImgCarouselProps } from './model/types/productImgCarouselType'
import styles from './ProductImgCarousel.module.scss'
import { ImgCarousel } from './ui/ImgCarousel/ImgCarousel'
import { PreviewCarousel } from './ui/PreviewCarousel/PreviewCarousel'

/**
* Компонент переключения фотографий товара на странице товара
* @param imgList (TImgList) - список фотографий товаров
* @param setShowPopup (f(boolean)) - функция управления видимостью попапа
*/
export const ProductImgCarousel: FC<TProductImgCarouselProps> = ({ imgList, setShowPopup }) => {
const [curImg, setCurImg] = useState<number>(0)

return (
<section className={styles.wrapper}>
<PreviewCarousel imgList={imgList} curImg={curImg} setCurImg={setCurImg} />
<div className={styles.carouselwrapper}>
<div className={styles.maincontainer}>
{<ImgCarousel isPopup={false} imgList={imgList} setShowPopup={setShowPopup} curImg={curImg} />}
</div>
</div>
</section>
)
}
22 changes: 22 additions & 0 deletions src/features/ProductImgCarousel/model/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TImgSizes } from '../types/productImgCarouselType'

export const IMG_SIZE_PAGE: TImgSizes = {
Xl: {
width: 500,
height: 500
},
Md: {
width: 300,
height: 300
}
}
export const IMG_SIZE_POPUP: TImgSizes = {
Xl: {
width: 720,
height: 720
},
Md: {
width: 470,
height: 470
}
}
18 changes: 18 additions & 0 deletions src/features/ProductImgCarousel/model/functions/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TImgList } from '../types/productImgCarouselType'

export const slicedImgList = (imgList: TImgList, maxCount: number, curIndex: number): TImgList => {
if (imgList.length > maxCount) {
let end = maxCount
let start = 0
if (curIndex > maxCount - 1) {
end = curIndex + 1
start = curIndex - (maxCount - 1)
}
return imgList.slice(start, end)
}
return imgList.slice(0)
}

export const getDisplayedIndex = (maxCount: number, curIndex: number): number => {
return curIndex > maxCount - 1 ? maxCount - 1 : curIndex
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Dispatch, SetStateAction } from 'react'

export type TBoxProps = {
children: React.ReactNode
}

interface IObjectWithImage {
image: string
index?: number
}

export type TImgList = Array<IObjectWithImage>

export type TImgCarouselProps = {
imgList: TImgList
isPopup: boolean
setShowPopup: Dispatch<SetStateAction<boolean>>
curImg: number
}

export type TProductImgCarouselProps = {
imgList: TImgList
setShowPopup: Dispatch<SetStateAction<boolean>>
}

export type TImgSize = {
width: number
height: number
}

export type TImgSizes = {
Xl: TImgSize
Md: TImgSize
}

export type TPreviewCarouselProps = {
imgList: TImgList
curImg: number
setCurImg: Dispatch<SetStateAction<number>>
}

export type TDisplayedImgList = {
displayedIndex: number
displayedImages: TImgList
}

export type TchangeImgArgs = 'next' | 'prev' | 'current'
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@use '@/shared/styles/utils/variables' as var;
@use '@/shared/styles/utils/mixins' as media;

.imgcarousel {
height: 100%;
width: 100%;
overflow: hidden;

&__allimg {
height: 100%;
display: flex;
align-items: center;
transition: translate;
transition-property: transform;
transition-duration: 300ms;
transition-timing-function: ease-in-out;
}

&__imageframe {
min-width: '100%';
height: '100%';
background-color: var.$white;
display: flex;
justify-content: center;
align-items: center;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
cursor: pointer;
}
}
128 changes: 128 additions & 0 deletions src/features/ProductImgCarousel/ui/ImgCarousel/ImgCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React, { FC, useEffect, useMemo, useState } from 'react'

import { useResize } from '@/shared/libs/hooks/useResize'

import { IMG_SIZE_PAGE, IMG_SIZE_POPUP } from '../../model/constants/constants'
import { TImgCarouselProps } from '../../model/types/productImgCarouselType'

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

/**
* Компонент фотографии товара на странице товара.
* @param isPopup (boolean) - значение true, если компонент отображается в рамках попапа
* @param imgList (TImgList) - список изображений
* @param setShowPopup (f(boolean)) - функция управления видимостью попапа
* @param curImg (number) - индекс текущего изображения
*/
export const ImgCarousel: FC<TImgCarouselProps> = ({ imgList, isPopup, setShowPopup, curImg }) => {
const [offset, setOffset] = useState(() => ({
imgOffset: '0',
offsetX: 'center',
offsetY: 'center'
}))
const [scale, setScale] = useState('contain')

const resize = useResize()

const getImgSise = () => {
if (!resize.isScreenMd) {
return isPopup ? IMG_SIZE_POPUP.Md : IMG_SIZE_PAGE.Md
} else {
return isPopup ? IMG_SIZE_POPUP.Xl : IMG_SIZE_PAGE.Xl
}
}

const [imgSize, setImgSize] = useState(getImgSise())

useEffect(() => {
setImgSize(getImgSise())
}, [resize.isScreenMd])

useEffect(() => {
setOffset({
imgOffset: (-imgSize.width * curImg).toString(),
offsetX: 'center',
offsetY: 'center'
})
setScale('contain')
}, [curImg])

const resetScale = (): void => {
setScale('contain')
setOffset(prevOffset => ({
imgOffset: prevOffset.imgOffset,
offsetX: 'center',
offsetY: 'center'
}))
}

//Ф-я определяет, что курсор покидает картинку. Нужна т.к. события mouseOut/mouseLeave зачастую не срабатывают
const isMouseOut = (x: number, y: number): boolean => {
return x > imgSize.width - 10 || x < 10 || y < 10 || y > imgSize.height - 10
}

const moseMoveHandle = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (isMouseOut(e.nativeEvent.offsetX, e.nativeEvent.offsetY)) {
resetScale()
} else {
setOffset(prevOffset => ({
imgOffset: prevOffset.imgOffset,
offsetX: `${Math.max(-e.nativeEvent.offsetX, -imgSize.width / 2)}px`,
offsetY: `${-e.nativeEvent.offsetY}px`
}))
if (scale !== '150%') {
setScale('150%')
}
}
}

const mouseLeaveHanle = () => {
resetScale()
}

const mouseClickHandle = () => {
setShowPopup(true)
//фиксация body, для отключения прокрутки при откытом попапе
//убираю скрол
const body = document.body
body.style.position = 'fixed'
}

const photoList = useMemo(() => {
return imgList.map(i => (
<div
className={`${styles.imgcarousel__imageframe}`}
key={i.index}
style={{
backgroundImage: `url(${i.image})`,
backgroundPositionX: `${offset.offsetX}`,
backgroundPositionY: `${offset.offsetY}`,
backgroundSize: `${scale}`,
minWidth: `100%`,
height: `100%`,
cursor: `${isPopup ? 'all-scroll' : 'pointer'}`
}}
onMouseMove={moseMoveHandle}
onMouseOut={mouseLeaveHanle}
onClick={!isPopup ? mouseClickHandle : undefined}></div>
))
}, [imgList, offset, scale])

return (
<div
className={styles.imgcarousel}
style={{
height: `${imgSize.height}px`
}}>
<div className={styles.popular}></div>

<div
className={styles.imgcarousel__allimg}
style={{
transform: `translateX(${offset.imgOffset}px)`
}}>
{photoList}
</div>
</div>
)
}
Loading

0 comments on commit e803a09

Please sign in to comment.