Skip to content

Commit

Permalink
Merge pull request #22 from team-nabi/NABI-87
Browse files Browse the repository at this point in the history
🎉 이미지 슬라이더 구현
  • Loading branch information
juyeon-park authored Nov 7, 2023
2 parents 66d2054 + 023b4a9 commit c156c75
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 0 deletions.
3 changes: 3 additions & 0 deletions public/images/icon-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/images/icon-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions src/components/domain/Slider/Slider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/react'
import ImageSlider from './Slider'

const meta = {
title: 'DOMAIN/Slider',
component: ImageSlider,
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof ImageSlider>

const mockData = [
'https://cdn.pixabay.com/photo/2023/09/30/09/12/dachshund-8285220_1280.jpg',
'https://cdn.pixabay.com/photo/2014/04/13/20/49/cat-323262_1280.jpg',
'https://cdn.pixabay.com/photo/2018/10/01/09/21/pets-3715733_1280.jpg',
]

export default meta
type Story = StoryObj<typeof meta>

export const Slider: Story = {
args: {
slides: mockData,
},
render: () => {
return (
<div className="max-w-[640px] relative">
<ImageSlider slides={mockData} imageAspectRatio="1/1" />
</div>
)
},
}

export const AutoSlider: Story = {
args: { slides: mockData },
render: () => {
return (
<div className="max-w-[640px] relative">
<ImageSlider
slides={mockData}
autoSlide={true}
imageAspectRatio="1/1"
/>
</div>
)
},
}
156 changes: 156 additions & 0 deletions src/components/domain/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'use client'

import { useState, useEffect, useRef, useCallback } from 'react'
import Image from 'next/image'
import Assets from '@/config/assets'

type SliderProps = {
autoSlide?: boolean
autoSlideInterval?: number
slides: string[]
imageAspectRatio?: string
}

const Slider = ({
autoSlide = false,
autoSlideInterval = 3000,
slides,
imageAspectRatio = 'auto',
}: SliderProps) => {
const [currentIndex, setCurrentIndex] = useState(1)
const [coordinate, setCoordinate] = useState({ start: 0, end: 0 })
const [style, setStyle] = useState({
transform: `translateX(-${currentIndex}00%)`,
transition: `all 0.4s ease-out`,
})

const ref = useRef<HTMLDivElement>(null)
const imageList = [slides.at(-1), ...slides, slides[0]]

const goPrevSlide = () => {
setCurrentIndex(currentIndex - 1)
setStyle({
transform: `translateX(-${currentIndex - 1}00%)`,
transition: `all 0.4s ease-out`,
})
}
const goNextSlide = useCallback(() => {
setCurrentIndex(currentIndex + 1)
setStyle({
transform: `translateX(-${currentIndex + 1}00%)`,
transition: `all 0.4s ease-out`,
})
}, [currentIndex])

const handleTouchStart = (e: React.TouchEvent) => {
setCoordinate({
...coordinate,
start: e.touches[0].pageX,
})
}

const handleTouchMove = (e: React.TouchEvent) => {
if (ref.current) {
const current = ref.current.clientWidth * currentIndex
const result = -current + (e.targetTouches[0].pageX - coordinate.start)
setStyle({
transform: `translate3d(${result}px, 0px, 0px)`,
transition: '0ms',
})
}
}

const handleTouchEnd = (e: React.TouchEvent) => {
const end = e.changedTouches[0].pageX
const distance = Math.abs(coordinate.start - end)

if (coordinate.start > end && distance > 2) {
goNextSlide()
} else if (coordinate.start < end && distance < 2) {
goPrevSlide()
}
setCoordinate({
...coordinate,
end,
})
}

useEffect(() => {
if (!autoSlide) return
const slideInterval = setInterval(goNextSlide, autoSlideInterval)
return () => clearInterval(slideInterval)
}, [autoSlide, autoSlideInterval, currentIndex, goNextSlide])

useEffect(() => {
if (currentIndex === 0) {
setCurrentIndex(imageList.length - 2)
setTimeout(function () {
setStyle({
transform: `translateX(-${imageList.length - 2}00%)`,
transition: '0ms',
})
}, 400)
}

if (currentIndex >= imageList.length - 1) {
setCurrentIndex(1)
setTimeout(() => {
setStyle({
transform: `translateX(-100%)`,
transition: '0ms',
})
}, 400)
}
}, [currentIndex, imageList.length])

return (
<div
className="overflow-hidden relative"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div ref={ref} className="flex " style={style}>
{imageList.map((url, i) => (
<Image
src={url as string}
alt="itemImage"
key={i}
priority
width={0}
height={0}
className={`w-full cursor-pointer aspect-${imageAspectRatio}`}
/>
))}
</div>
<div className="absolute p-2 w-full flex justify-between top-[50%]">
<button onClick={goPrevSlide} className="p-1 rounded-full bg-white/20">
<Image src={Assets.leftIcon} alt="left" />
</button>

<button onClick={goNextSlide} className="p-1 rounded-full bg-white/20">
<Image src={Assets.rightIcon} alt="right" priority />
</button>
</div>
<div className="absolute bottom-4 right-0 left-0">
<div className="flex items-center justify-center gap-2" ref={ref}>
{slides.map((_, i) => (
<button
key={i}
className={`
transition-all w-3 h-3 rounded-full
${
currentIndex === i + 1
? 'w-4 h-4 bg-primary-color'
: 'bg-white bg-opacity-50'
}
`}
></button>
))}
</div>
</div>
</div>
)
}

export default Slider
3 changes: 3 additions & 0 deletions src/components/domain/Slider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { default as ImageSlider } from './Slider'

export default ImageSlider
4 changes: 4 additions & 0 deletions src/config/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import FilterIcon from '/public/images/filter.svg'
import GoogleIcon from '/public/images/google.png'
import ActiveHeartIcon from '/public/images/icon-heart-active.svg'
import InActiveHeartIcon from '/public/images/icon-heart-inactive.svg'
import LeftIcon from '/public/images/icon-left.svg'
import MarkerIcon from '/public/images/icon-marker.svg'
import MoneyIcon from '/public/images/icon-money.svg'
import RightIcon from '/public/images/icon-right.svg'
import UsersIcon from '/public/images/icon-users.svg'
import KakaoIcon from '/public/images/kakao.png'
import Logo from '/public/images/logo.svg'
Expand All @@ -20,6 +22,8 @@ const Assets = {
arrowLeftIcon: ArrowLeftIcon,
filterIcon: FilterIcon,
alarmIcon: AlarmIcon,
leftIcon: LeftIcon,
rightIcon: RightIcon,
googleIcon: GoogleIcon,
kakaoIcon: KakaoIcon,
logo: Logo,
Expand Down

0 comments on commit c156c75

Please sign in to comment.