Skip to content

Commit

Permalink
Merge pull request #36 from eunji-0623/조혜진
Browse files Browse the repository at this point in the history
Feat: 예약 현황 구현
  • Loading branch information
MEGUMMY1 authored Jul 16, 2024
2 parents 14ea335 + cbb2abf commit ba1226a
Show file tree
Hide file tree
Showing 17 changed files with 1,085 additions and 37 deletions.
35 changes: 22 additions & 13 deletions components/ActivityDetails/ActivityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
getActivityReviewsResponse,
} from '@/pages/api/activities/apiactivities.types';
import Spinner from '../Spinner/Spinner';
import { userState } from '@/states/userState';
import { useRecoilValue } from 'recoil';

export default function ActivityDetails({ id }: ActivityDetailsProps) {
const router = useRouter();
Expand All @@ -31,6 +33,8 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {

const menuRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false));

const userData = useRecoilValue(userState);

const {
data: activityData,
error: activityError,
Expand Down Expand Up @@ -86,6 +90,7 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {
};

const paginatedReviews = reviewData?.reviews || [];
const isAuthor = activityData?.userId === userData?.id;

return (
<div className="mt-16 t:mt-4 m:mt-4">
Expand Down Expand Up @@ -123,19 +128,23 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {
</div>
</div>
</div>
<MeatballButton onClick={toggleMenu} />
{isOpen && (
<div
ref={menuRef}
className="absolute top-[70px] right-0 mt-2 w-40 h-[114px] bg-white border border-var-gray3 border-solid rounded-lg flex flex-col items-center justify-center text-lg z-10"
>
<button className="block w-full h-[57px] px-4 py-2 text-var-gray8 hover:bg-gray-100 rounded-t-lg border-b border-var-gray3 border-solid">
수정하기
</button>
<button className="block w-full h-[57px] px-4 py-2 text-var-gray8 hover:bg-gray-100 rounded-b-lg">
삭제하기
</button>
</div>
{isAuthor && (
<>
<MeatballButton onClick={toggleMenu} />
{isOpen && (
<div
ref={menuRef}
className="absolute top-[70px] right-0 mt-2 w-40 h-[114px] bg-white border border-var-gray3 border-solid rounded-lg flex flex-col items-center justify-center text-lg z-10"
>
<button className="block w-full h-[57px] px-4 py-2 text-var-gray8 hover:bg-gray-100 rounded-t-lg border-b border-var-gray3 border-solid">
수정하기
</button>
<button className="block w-full h-[57px] px-4 py-2 text-var-gray8 hover:bg-gray-100 rounded-b-lg">
삭제하기
</button>
</div>
)}
</>
)}
</div>
{activityData && (
Expand Down
7 changes: 1 addition & 6 deletions components/ActivityDetails/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,5 @@ export default function Map({ address }: MapProps) {
};
}, [address]);

return (
<div
id="map"
className="w-[800px] h-[500px] rounded-2xl t:w-full t:h-[276px] m:w-full m:h-[450px]"
/>
);
return <div id="map" className="w-[800px] h-[500px] rounded-2xl" />;
}
50 changes: 45 additions & 5 deletions components/ActivityDetails/Reservation/Reservation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import ReservationModal from './ReservationModal';
import ParticipantSelector from './ParticipantSelector';
import CustomCalendar from '@/components/CustomCalendar/CustomCalendar';
import { ReservationProps } from './Reservation.types';
import { useMutation } from '@tanstack/react-query';
import { postActivityRequestParams } from '@/pages/api/activities/apiactivities.types';
import { postActivityRequest } from '@/pages/api/activities/apiactivities';

export default function Reservation({ activity }: ReservationProps) {
const [selectedDate, setSelectedDate] = useState<Date | null>(
Expand Down Expand Up @@ -94,12 +97,49 @@ export default function Reservation({ activity }: ReservationProps) {
closeModal();
};

const { mutate: createReservation } = useMutation({
mutationFn: (data: postActivityRequestParams) =>
postActivityRequest(activity.id, data),
onSuccess: () => {
openPopup({
popupType: 'alert',
content: '예약이 완료되었습니다.',
btnName: ['확인'],
callBackFnc: () => router.push(`/activity-details/${activity.id}`),
});
},
onError: (error) => {
console.error('예약 중 오류 발생:', error);
openPopup({
popupType: 'alert',
content: '예약 중 오류가 발생했습니다. 다시 시도해 주세요.',
btnName: ['확인'],
});
},
});

const handleReservation = () => {
openPopup({
popupType: 'alert',
content: '예약이 완료되었습니다.',
btnName: ['확인'],
callBackFnc: () => router.push(`/`),
if (!selectedDate || !selectedTime) return;

const selectedSchedule = schedules.find(
(schedule) =>
schedule.startTime === selectedTime.split(' ~ ')[0] &&
format(new Date(schedule.date), 'yyyy-MM-dd') ===
format(selectedDate, 'yyyy-MM-dd')
);

if (!selectedSchedule) {
openPopup({
popupType: 'alert',
content: '선택된 시간에 대한 예약 정보가 없습니다.',
btnName: ['확인'],
});
return;
}

createReservation({
scheduleId: selectedSchedule.id,
headCount: participants,
});
};

Expand Down
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>
);
}
113 changes: 113 additions & 0 deletions components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useEffect, 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 { 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 } = useModal();
const [modalDate, setModalDate] = useState<Date | null>(null);

const handleDateClick = (arg: DateClickArg) => {
const newDate = new Date(arg.dateStr);
setModalDate(newDate);
openModal({
title: '예약 정보',
hasButton: false,
content: (
<ReservationModalContent
selectedDate={newDate}
activityId={activityId}
/>
),
});
};

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[];
}
29 changes: 29 additions & 0 deletions components/Calendar/ModalTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 ba1226a

Please sign in to comment.