Skip to content

Commit

Permalink
Merge pull request #199 from gw-lim/feat/artist-drag
Browse files Browse the repository at this point in the history
✨ feat: 아티스트 페이지 바텀 시트 드래그 구현
  • Loading branch information
gw-lim authored Mar 19, 2024
2 parents d202f1d + 4ceb94e commit a332190
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 125 deletions.
73 changes: 73 additions & 0 deletions app/(route)/artist/[artistId]/_components/MobileTab.tsx
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;
70 changes: 70 additions & 0 deletions app/(route)/artist/[artistId]/_components/PcTab.tsx
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;
74 changes: 74 additions & 0 deletions app/(route)/artist/[artistId]/_hooks/useArtistSheet.tsx
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;
104 changes: 26 additions & 78 deletions app/(route)/artist/[artistId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
"use client";

import SortButton from "@/(route)/search/_components/SortButton";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import KakaoMap from "@/components/KakaoMap";
import MetaTag from "@/components/MetaTag";
import TimeFilter from "@/components/TimeFilter";
import DottedLayout from "@/components/layout/DottedLayout";
import { instance } from "@/api/api";
import useCustomMap from "@/hooks/useCustomMap";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import { getSession } from "@/store/session/cookies";
import { getArtist, getGroup } from "@/utils/getArtist";
import { Res_Get_Type } from "@/types/getResType";
import { SORT, STATUS, SortItem } from "@/constants/eventStatus";
import SortIcon from "@/public/icon/sort.svg";
import EventCard from "./_components/EventCard";
import MobileTab from "./_components/MobileTab";
import PcTab from "./_components/PcTab";

const SIZE = 9999;

Expand All @@ -33,35 +29,19 @@ const ArtistIdPage = () => {
const [sort, setSort] = useState<SortItem>(SORT[0]);
const [status, setStatus] = useState(3);

const session = getSession();

const getArtistData = async ({ pageParam = 1 }) => {
const data: Res_Get_Type["eventSearch"] = await instance.get(`/event/artist/${artistId}`, {
sort,
size: SIZE,
page: pageParam,
status: STATUS[status],
userId: session?.user.userId ?? "",
artistId,
});
return data;
};

const {
data: artistData,
fetchNextPage,
isSuccess,
refetch,
} = useInfiniteQuery({
initialPageParam: 1,
} = useQuery<Res_Get_Type["eventSearch"]>({
queryKey: ["events", artistId],
queryFn: getArtistData,
getNextPageParam: (lastPage) => (lastPage.page * SIZE < lastPage.totalCount ? lastPage.page + 1 : null),
});

const containerRef = useInfiniteScroll({
handleScroll: fetchNextPage,
deps: [artistData],
queryFn: () =>
instance.get(`/event/artist/${artistId}`, {
sort,
size: SIZE,
status: STATUS[status],
artistId,
}),
});

useEffect(() => {
Expand All @@ -72,68 +52,36 @@ const ArtistIdPage = () => {

if (!isSuccess) return;

const isEmpty = artistData.pages[0].eventList.length === 0;
const mapData = artistData.pages[0].eventList;
const eventData = artistData.eventList;

return (
<>
<MetaTag title={`${name}`} imgUrl={image} description={`${name}의 행사 정보들을 지도를 통해 쉽게 확인해 보세요.`} />
<DottedLayout size="wide">
<div className="relative h-[calc(100vh-7.2rem)] w-full overflow-hidden pc:mb-128 pc:mt-48 pc:h-[84rem]">
<div
className={`absolute left-0 top-0 z-zero h-full w-full ${mapVar.toggleTab ? "tablet:pl-360 pc:pl-400" : ""} pb-344 tablet:pb-0 pc:h-[84rem] pc:rounded-lg pc:border pc:border-gray-100`}
className={`absolute left-0 top-0 z-zero h-full w-full ${mapVar.toggleTab ? "tablet:pl-360 pc:pl-400" : ""} tablet:pb-0 pc:h-[84rem] pc:rounded-lg pc:border pc:border-gray-100`}
>
<KakaoMap scheduleData={mapData} {...mapVar} />
<KakaoMap scheduleData={eventData} {...mapVar} />
</div>
<button
onClick={mapCallback.handleButtonClick}
className={`tablet:flex-center absolute z-nav hidden h-60 w-24 rounded-r-sm border border-gray-100 bg-white-white tablet:top-44 pc:top-24 ${mapVar.toggleTab ? "border-l-white-white tablet:left-360 pc:left-400" : "left-0"}`}
>
<Image src="/icon/arrow-left.svg" width={20} height={20} alt="화살표" className={`${mapVar.toggleTab || "scale-x-[-1]"}`} />
</button>
{mapVar.toggleTab && (
<div className="absolute bottom-0 flex max-h-352 min-h-84 w-full flex-col gap-16 rounded-t-lg bg-white-black pt-28 shadow-2xl tablet:top-0 tablet:max-h-full tablet:w-360 tablet:rounded-none tablet:border tablet:border-gray-100 tablet:border-t-transparent tablet:pt-20 tablet:shadow-none pc:top-0 pc:h-[84rem] pc:w-400 pc:rounded-l-lg pc:border-t-gray-100 pc:py-20">
<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 : "/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">
{artistData.pages.map((page) =>
page.eventList.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 ref={containerRef} className="h-20 w-full" />
</div>
</div>
)}
<PcTab mapVar={mapVar} mapCallback={mapCallback} eventData={eventData} name={name} image={image} sort={sort} setSort={setSort} status={status} setStatus={setStatus} />
<MobileTab
mapVar={mapVar}
mapCallback={mapCallback}
eventData={eventData}
name={name}
image={image}
sort={sort}
setSort={setSort}
status={status}
setStatus={setStatus}
/>
</div>
</DottedLayout>
</>
Expand Down
4 changes: 2 additions & 2 deletions app/(route)/event/[eventId]/_components/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const Banner = ({ data, eventId }: Props) => {
</SubDescription>
{hasEditApplication && <Alert href={pathname + "/approve"} message="수정요청 정보가 있습니다." />}
</div>
<div className="absolute bottom-0 right-0 hidden text-14 font-400 pc:block">
<div className="bottom-0 right-0 pt-12 text-12 font-400 pc:absolute pc:block pc:text-14">
<Link href={pathname + "/edit"} className="mr-16 text-blue">
수정하기
</Link>
Expand Down Expand Up @@ -176,7 +176,7 @@ const MainDescription = ({ placeName, artists, eventType }: MainDescriptionProps

return (
<div className="flex flex-col gap-8 border-b border-gray-100 pb-16 pc:gap-12 pc:pb-32">
<h1 className="pr-20 text-20 font-600 pc:text-[2.8rem] pc:leading-[2.4rem]">{placeName}</h1>
<h1 className="pr-32 text-20 font-600 pc:text-[2.8rem] pc:leading-[2.8rem]">{placeName}</h1>
<div className="flex items-center gap-8">
<span className="text-16 font-600 pc:max-w-308 pc:text-20">{formattedArtist}</span>
<Chip kind="event" label={eventType} />
Expand Down
Loading

0 comments on commit a332190

Please sign in to comment.