Skip to content

Commit

Permalink
Feat: 예약 현황(체험 선택, 캘린더, modal open) 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
MEGUMMY1 committed Jul 14, 2024
1 parent 56e5428 commit 4e9c076
Show file tree
Hide file tree
Showing 10 changed files with 907 additions and 9 deletions.
94 changes: 94 additions & 0 deletions components/Calendar/ActivitySelector.tsx
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>
);
}
122 changes: 122 additions & 0 deletions components/Calendar/Calendar.tsx
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;
19 changes: 19 additions & 0 deletions components/Calendar/Calendar.types.ts
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[];
}
25 changes: 25 additions & 0 deletions components/Calendar/ModalTabs.tsx
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;
Loading

0 comments on commit 4e9c076

Please sign in to comment.