-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: 예약 현황(체험 선택, 캘린더, modal open) 구현
- Loading branch information
Showing
10 changed files
with
907 additions
and
9 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,94 @@ | ||
import React, { useState } from 'react'; | ||
import Image from 'next/image'; | ||
import Down from '@/public/icon/chevron_down.svg'; | ||
import Up from '@/public/icon/chevron_up.svg'; | ||
import CheckMark from '@/public/icon/Checkmark.svg'; | ||
import useClickOutside from '@/hooks/useClickOutside'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { getMyActivityList } from '@/pages/api/myActivities/apimyActivities'; | ||
import { getMyActivityListResponse } from '@/pages/api/myActivities/apimyActivities.types'; | ||
import { ActivitySelectorProps } from './Calendar.types'; | ||
import Spinner from '../Spinner/Spinner'; | ||
|
||
export default function ActivitySelector({ | ||
onSelectActivity, | ||
selectedActivityId, | ||
}: ActivitySelectorProps) { | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
const dropDownElement = useClickOutside<HTMLDivElement>(() => | ||
setIsOpen(false) | ||
); | ||
|
||
const { data, error, isLoading } = useQuery<getMyActivityListResponse, Error>( | ||
{ | ||
queryKey: ['myActivityList'], | ||
queryFn: () => getMyActivityList({}), | ||
} | ||
); | ||
|
||
if (isLoading) { | ||
return <Spinner />; | ||
} | ||
|
||
if (error) { | ||
return <div>Error: {error.message}</div>; | ||
} | ||
|
||
const handleOnClick = (activityId: number) => { | ||
onSelectActivity(activityId); | ||
setIsOpen(false); | ||
}; | ||
|
||
const selectedActivity = selectedActivityId | ||
? data?.activities.find((activity) => activity.id === selectedActivityId) | ||
?.title | ||
: '체험 선택'; | ||
|
||
return ( | ||
<div className="relative w-full" ref={dropDownElement}> | ||
<div | ||
className={`w-full h-[56px] border-solid border border-var-gray7 rounded flex items-center px-[20px] text-[16px] font-[400] font-sans bg-white cursor-pointer ${selectedActivity ? 'text-black' : 'text-var-gray6'}`} | ||
onClick={() => setIsOpen(!isOpen)} | ||
> | ||
{selectedActivity} | ||
<Image | ||
src={isOpen ? Up : Down} | ||
alt="화살표 아이콘" | ||
width={24} | ||
height={24} | ||
className="absolute right-2 top-4" | ||
/> | ||
</div> | ||
{isOpen && ( | ||
<ul className="z-10 p-2 w-full absolute bg-white border border-solid border-var-gray3 rounded-md mt-1 shadow-lg animate-slideDown flex flex-col"> | ||
{data?.activities.map((activity) => { | ||
const isSelected = selectedActivityId === activity.id; | ||
const backgroundColor = isSelected ? 'bg-nomad-black' : 'bg-white'; | ||
const textColor = isSelected ? 'text-white' : 'text-nomad-black'; | ||
|
||
return ( | ||
<li | ||
key={activity.id} | ||
className={`p-2 h-[40px] hover:bg-var-gray2 ${backgroundColor} ${textColor} rounded-md cursor-pointer flex items-center`} | ||
onClick={() => handleOnClick(activity.id)} | ||
> | ||
{isSelected ? ( | ||
<Image | ||
src={CheckMark} | ||
alt="체크 마크 아이콘" | ||
width={20} | ||
height={20} | ||
className="mr-2" | ||
/> | ||
) : ( | ||
<div className="w-[20px] mr-2" /> | ||
)} | ||
{activity.title} | ||
</li> | ||
); | ||
})} | ||
</ul> | ||
)} | ||
</div> | ||
); | ||
} |
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,122 @@ | ||
import React, { useState } from 'react'; | ||
import FullCalendar from '@fullcalendar/react'; | ||
import dayGridPlugin from '@fullcalendar/daygrid'; | ||
import interactionPlugin from '@fullcalendar/interaction'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { getMyMonthSchedule } from '@/pages/api/myActivities/apimyActivities'; | ||
import Spinner from '../Spinner/Spinner'; | ||
import { CalendarProps } from './Calendar.types'; | ||
import { getMyMonthScheduleResponse } from '@/pages/api/myActivities/apimyActivities.types'; | ||
import { StyleWrapper } from './StyleWrapper'; | ||
import { useModal } from '@/hooks/useModal'; | ||
import ReservationModalContent from './ReservationModalContent'; | ||
import { DateClickArg } from '@fullcalendar/interaction'; | ||
|
||
const Calendar: React.FC<CalendarProps> = ({ activityId }) => { | ||
const year = new Date().getFullYear().toString(); | ||
const month = (new Date().getMonth() + 1).toString().padStart(2, '0'); | ||
|
||
const { data, error, isLoading } = useQuery< | ||
getMyMonthScheduleResponse[], | ||
Error | ||
>({ | ||
queryKey: ['myMonthSchedule', activityId, year, month], | ||
queryFn: () => getMyMonthSchedule({ activityId, year, month }), | ||
}); | ||
|
||
const { openModal, closeModal } = useModal(); | ||
const [isModalOpen, setIsModalOpen] = useState(false); | ||
const [modalDate, setModalDate] = useState<Date | null>(null); | ||
|
||
const handleDateClick = (arg: DateClickArg) => { | ||
setModalDate(new Date(arg.dateStr)); | ||
setIsModalOpen(true); | ||
openModal({ | ||
title: '예약 정보', | ||
hasButton: false, | ||
content: modalDate && ( | ||
<ReservationModalContent | ||
selectedDate={modalDate} | ||
activityId={activityId} | ||
onSelectTime={(scheduleId) => { | ||
console.log('기능 추가 필요 Selected schedule ID', scheduleId); | ||
}} | ||
/> | ||
), | ||
}); | ||
}; | ||
|
||
if (isLoading) { | ||
return <Spinner />; | ||
} | ||
|
||
if (error) { | ||
return <div>Error: {error.message}</div>; | ||
} | ||
|
||
const events = | ||
data?.flatMap((item: getMyMonthScheduleResponse) => { | ||
const { date, reservations } = item; | ||
|
||
const events = []; | ||
|
||
if (reservations.completed > 0) { | ||
events.push({ | ||
title: `완료 ${reservations.completed}`, | ||
start: date, | ||
classNames: ['bg-var-gray3 text-var-gray8'], | ||
}); | ||
} | ||
|
||
if (reservations.pending > 0) { | ||
events.push({ | ||
title: `예약 ${reservations.pending}`, | ||
start: date, | ||
classNames: ['bg-var-blue3 text-white'], | ||
}); | ||
} | ||
|
||
if (reservations.confirmed > 0) { | ||
events.push({ | ||
title: `승인 ${reservations.confirmed}`, | ||
start: date, | ||
classNames: ['bg-var-orange1 text-var-orange2'], | ||
}); | ||
} | ||
|
||
return events; | ||
}) || []; | ||
|
||
return ( | ||
<StyleWrapper> | ||
<FullCalendar | ||
plugins={[dayGridPlugin, interactionPlugin]} | ||
initialView="dayGridMonth" | ||
events={events} | ||
eventContent={renderEventContent} | ||
headerToolbar={{ | ||
start: 'prev', | ||
center: 'title', | ||
end: 'next', | ||
}} | ||
dateClick={handleDateClick} | ||
/> | ||
</StyleWrapper> | ||
); | ||
}; | ||
|
||
const renderEventContent = (eventInfo: { | ||
timeText: string; | ||
event: { title: string; classNames: string[] }; | ||
}) => { | ||
const { title, classNames } = eventInfo.event; | ||
|
||
return ( | ||
<div className={`fc-event-inner ${classNames.join(' ')}`}> | ||
<b>{eventInfo.timeText}</b> | ||
<div className="event-labels">{title}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Calendar; |
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,19 @@ | ||
export interface CalendarProps { | ||
activityId: number; | ||
} | ||
|
||
export interface ActivitySelectorProps { | ||
onSelectActivity: (activityId: number) => void; | ||
selectedActivityId: number | null; | ||
} | ||
|
||
export interface ReservationModalContentProps { | ||
selectedDate: Date; | ||
activityId: number; | ||
onSelectTime: (scheduleId: number) => void; | ||
} | ||
|
||
export interface ModalTabsProps { | ||
labels: string[]; | ||
children: React.ReactNode[]; | ||
} |
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,25 @@ | ||
import React, { useState } from 'react'; | ||
import { ModalTabsProps } from './Calendar.types'; | ||
|
||
function ModalTabs({ labels, children }: ModalTabsProps) { | ||
const [activeTab, setActiveTab] = useState(0); | ||
|
||
return ( | ||
<div> | ||
<div className="flex space-x-4 mb-4 border-b-2 border-gray-300"> | ||
{labels.map((label, index) => ( | ||
<button | ||
key={index} | ||
className={`w-[72px] py-2 font-xl ${index === activeTab ? 'border-b-2 border-var-green2 text-var-green2 font-bold' : 'text-var-gray8'}`} | ||
onClick={() => setActiveTab(index)} | ||
> | ||
{label} | ||
</button> | ||
))} | ||
</div> | ||
<div>{children[activeTab]}</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default ModalTabs; |
Oops, something went wrong.