-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #199 from gw-lim/feat/artist-drag
✨ feat: 아티스트 페이지 바텀 시트 드래그 구현
- Loading branch information
Showing
19 changed files
with
326 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import SortButton from "@/(route)/search/_components/SortButton"; | ||
import Image from "next/image"; | ||
import { Dispatch, SetStateAction } from "react"; | ||
import TimeFilter from "@/components/TimeFilter"; | ||
import { MapCallbackType, MapVarType } from "@/hooks/useCustomMap"; | ||
import { EventCardType } from "@/types/index"; | ||
import SortIcon from "@/public/icon/sort.svg"; | ||
import useArtistSheet from "../_hooks/useArtistSheet"; | ||
import EventCard from "./EventCard"; | ||
|
||
interface Props { | ||
mapVar: MapVarType; | ||
mapCallback: MapCallbackType; | ||
image: string; | ||
name: string; | ||
status: number; | ||
setStatus: Dispatch<SetStateAction<number>>; | ||
sort: "최신순" | "인기순"; | ||
setSort: Dispatch<SetStateAction<"최신순" | "인기순">>; | ||
eventData: EventCardType[]; | ||
} | ||
|
||
const MobileTab = ({ mapVar, mapCallback, image, name, status, setStatus, sort, setSort, eventData }: Props) => { | ||
const isEmpty = eventData.length === 0; | ||
|
||
const { sheet, content } = useArtistSheet(); | ||
|
||
return ( | ||
<div | ||
ref={sheet} | ||
className="absolute top-[calc(100vh-384px)] flex min-h-84 w-full flex-col gap-16 rounded-t-lg bg-white-black pt-28 shadow-2xl transition duration-150 ease-out tablet:hidden" | ||
> | ||
<div className="absolute left-[calc((100%-64px)/2)] top-12 h-4 w-64 rounded-sm bg-gray-700 tablet:hidden" /> | ||
<div className="flex flex-row items-center justify-start gap-12 px-20 pc:w-full"> | ||
<div className="relative h-36 w-36 pc:h-64 pc:w-64"> | ||
<Image src={image ?? "/image/no-profile.png"} alt="아티스트 이미지" fill sizes="64px" className="rounded-full object-cover" /> | ||
</div> | ||
<p className="text-16 leading-[2.4rem] text-gray-700"> | ||
<span className="font-600">{name}</span> 행사 보기 | ||
</p> | ||
</div> | ||
<TimeFilter setStatus={setStatus} status={status} /> | ||
<div className="flex items-center gap-8 px-20"> | ||
<SortIcon /> | ||
<SortButton onClick={() => setSort("최신순")} selected={sort === "최신순"}> | ||
최신순 | ||
</SortButton> | ||
<SortButton onClick={() => setSort("인기순")} selected={sort === "인기순"}> | ||
인기순 | ||
</SortButton> | ||
</div> | ||
<div ref={content} className="max-h-[80rem] min-h-200 overflow-scroll pb-[70rem] scrollbar-none"> | ||
{isEmpty ? ( | ||
<p className="flex-center w-full pt-20 text-14 font-500">행사가 없습니다.</p> | ||
) : ( | ||
<div className="px-20"> | ||
{eventData.map((event) => ( | ||
<EventCard | ||
key={event.id} | ||
data={event} | ||
isSelected={mapVar.selectedCard?.id === event.id} | ||
onCardClick={() => mapCallback.handleCardClick(event)} | ||
scrollRef={mapVar.selectedCard?.id === event.id ? mapCallback.scrollRefCallback : null} | ||
/> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default MobileTab; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import SortButton from "@/(route)/search/_components/SortButton"; | ||
import Image from "next/image"; | ||
import { Dispatch, SetStateAction } from "react"; | ||
import TimeFilter from "@/components/TimeFilter"; | ||
import { MapCallbackType, MapVarType } from "@/hooks/useCustomMap"; | ||
import { EventCardType } from "@/types/index"; | ||
import SortIcon from "@/public/icon/sort.svg"; | ||
import EventCard from "./EventCard"; | ||
|
||
interface Props { | ||
mapVar: MapVarType; | ||
mapCallback: MapCallbackType; | ||
image: string; | ||
name: string; | ||
status: number; | ||
setStatus: Dispatch<SetStateAction<number>>; | ||
sort: "최신순" | "인기순"; | ||
setSort: Dispatch<SetStateAction<"최신순" | "인기순">>; | ||
eventData: EventCardType[]; | ||
} | ||
|
||
const PcTab = ({ mapVar, mapCallback, image, name, status, setStatus, sort, setSort, eventData }: Props) => { | ||
const isEmpty = eventData.length === 0; | ||
|
||
return ( | ||
<> | ||
{mapVar.toggleTab && ( | ||
<div className="tablet:rounded-none absolute bottom-0 top-0 hidden max-h-full min-h-84 w-360 flex-col gap-16 rounded-t-lg border border-gray-100 border-t-transparent bg-white-black pt-20 shadow-none tablet:flex pc:top-0 pc:h-[84rem] pc:w-400 pc:rounded-l-lg pc:border-t-gray-100 pc:py-20"> | ||
<div className="flex flex-row items-center justify-start gap-12 px-20 pc:w-full"> | ||
<div className="relative h-36 w-36 pc:h-64 pc:w-64"> | ||
<Image src={image ?? "/image/no-profile.png"} alt="아티스트 이미지" fill sizes="64px" className="rounded-full object-cover" /> | ||
</div> | ||
<p className="text-16 leading-[2.4rem] text-gray-700"> | ||
<span className="font-600">{name}</span> 행사 보기 | ||
</p> | ||
</div> | ||
<TimeFilter setStatus={setStatus} status={status} /> | ||
<div className="flex items-center gap-8 px-20"> | ||
<SortIcon /> | ||
<SortButton onClick={() => setSort("최신순")} selected={sort === "최신순"}> | ||
최신순 | ||
</SortButton> | ||
<SortButton onClick={() => setSort("인기순")} selected={sort === "인기순"}> | ||
인기순 | ||
</SortButton> | ||
</div> | ||
<div className="min-h-200 overflow-scroll scrollbar-none pc:h-[65rem]"> | ||
{isEmpty ? ( | ||
<p className="flex-center w-full pt-20 text-14 font-500">행사가 없습니다.</p> | ||
) : ( | ||
<div className="px-20"> | ||
{eventData.map((event) => ( | ||
<EventCard | ||
key={event.id} | ||
data={event} | ||
isSelected={mapVar.selectedCard?.id === event.id} | ||
onCardClick={() => mapCallback.handleCardClick(event)} | ||
scrollRef={mapVar.selectedCard?.id === event.id ? mapCallback.scrollRefCallback : null} | ||
/> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default PcTab; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { useCallback, useRef } from "react"; | ||
|
||
interface BottomSheetMetrics { | ||
touchStart: number; | ||
position: number; | ||
isContentAreaTouched: boolean; | ||
} | ||
|
||
const SNAP = { | ||
top: 280, | ||
bottom: 670, | ||
}; | ||
|
||
const useArtistSheet = () => { | ||
const metrics = useRef<BottomSheetMetrics>({ | ||
touchStart: 0, | ||
position: 0, | ||
isContentAreaTouched: false, | ||
}); | ||
|
||
const sheet = useCallback((node: HTMLElement | null) => { | ||
if (node !== null) { | ||
const handleTouchStart = (e: TouchEvent) => { | ||
metrics.current.touchStart = e.touches[0].clientY; | ||
}; | ||
|
||
const handleTouchMove = (e: TouchEvent) => { | ||
const { touchStart, isContentAreaTouched, position } = metrics.current; | ||
const currentTouch = e.touches[0]; | ||
|
||
if (!isContentAreaTouched) { | ||
e.preventDefault(); | ||
const touchOffset = currentTouch.clientY - touchStart + position; | ||
node.style.setProperty("transform", `translateY(${touchOffset}px)`); | ||
} | ||
}; | ||
|
||
const handleTouchEnd = (e: TouchEvent) => { | ||
const currentY = node.getBoundingClientRect().y; | ||
|
||
if (currentY > SNAP.bottom) { | ||
node.style.setProperty("transform", `translateY(240px)`); | ||
metrics.current.position = 240; | ||
} else if (currentY < SNAP.bottom && currentY > SNAP.top) { | ||
node.style.setProperty("transform", "translateY(0px)"); | ||
metrics.current.position = 0; | ||
} else if (currentY < SNAP.top) { | ||
node.style.setProperty("transform", `translateY(-360px)`); | ||
metrics.current.position = -360; | ||
} | ||
|
||
metrics.current.isContentAreaTouched = false; | ||
}; | ||
|
||
node.addEventListener("touchstart", handleTouchStart); | ||
node.addEventListener("touchmove", handleTouchMove); | ||
node.addEventListener("touchend", handleTouchEnd); | ||
} | ||
}, []); | ||
|
||
const content = useCallback((node: HTMLElement | null) => { | ||
if (node !== null) { | ||
const handleTouchStart = () => { | ||
metrics.current.isContentAreaTouched = true; | ||
}; | ||
|
||
node.addEventListener("touchstart", handleTouchStart); | ||
} | ||
}, []); | ||
|
||
return { sheet, content }; | ||
}; | ||
|
||
export default useArtistSheet; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.