Skip to content

Commit

Permalink
Merge pull request #192 from INtiful/feat/homin/KAN-123-gatherings-an…
Browse files Browse the repository at this point in the history
…imation-skeleton

feat: /gatherings/[id] 이미지에 스켈레톤 적용 및 framer motion 적용
  • Loading branch information
HMRyu authored Oct 16, 2024
2 parents 687f09e + 3e20c31 commit d03c385
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 153 deletions.
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@hookform/resolvers": "^3.9.0",
"@lukemorales/query-key-factory": "^1.3.4",
"@tanstack/react-query": "^5.54.1",
"framer-motion": "^11.11.8",
"next": "14.2.7",
"next-themes": "^0.3.0",
"qs": "^6.13.0",
Expand Down
13 changes: 12 additions & 1 deletion src/app/(main)/gatherings/[id]/_component/GatheringImage.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
'use client';

import Image from 'next/image';

import { IconAlarm } from '@/public/icons';
import getDaysUntilRegistrationEnd from '@/utils/getDaysUntilRegistrationEnd';
import getTagMessage from '@/utils/getTagMessage';
import { useState } from 'react';

interface GatheringImageProps {
image: string;
endTime?: string;
}

const GatheringImage = ({ image, endTime }: GatheringImageProps) => {
const [isLoading, setIsLoading] = useState(true);

const daysLeft = endTime ? getDaysUntilRegistrationEnd(endTime) : null;
const tagMessage = getTagMessage(daysLeft, endTime);

return (
<div className='relative h-[270px] w-full md:w-[50vw] lg:max-w-[486px]'>
{isLoading && (
<div className='absolute inset-0 h-full w-full animate-pulse rounded-[20px] bg-slate-700'></div>
)}

<Image
className='rounded-[20px] object-cover'
className={`rounded-[20px] object-cover transition-opacity duration-500 ${isLoading ? 'opacity-0' : 'opacity-100'}`}
src={image || '/images/mock-image.png'}
alt='review image'
fill
quality={85}
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw'
onLoadingComplete={() => setIsLoading(false)}
/>

{daysLeft !== null && daysLeft <= 7 && (
<div className='absolute right-0 top-0 flex items-center gap-4 rounded-bl-[12px] rounded-tr-[20px] bg-orange-600 py-2 pl-4 pr-6 text-xs font-medium text-var-white'>
<IconAlarm width='24' height='24' />
Expand Down
38 changes: 20 additions & 18 deletions src/app/(main)/gatherings/_component/GatheringCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CardList from '@/app/components/CardList/CardList';
import GradientOverlay from '@/app/components/GradientOverlay/GradientOverlay';
import { GatheringType } from '@/types/data.type';
import { useSavedGatheringList } from '@/context/SavedGatheringContext';
import MotionWrapper from '@/app/components/MotionWrapper/MotionWrapper';
import useScrollGradientEffect from '@/hooks/useScrollGradientEffect';

interface GatheringCardListProps {
Expand Down Expand Up @@ -41,24 +42,25 @@ const GatheringCardList = ({ gatherings }: GatheringCardListProps) => {
</div>
) : (
gatherings.map((gathering, index) => (
<div
key={gathering.id}
ref={
index === 0
? firstGatheringRef
: index === gatherings.length - 1
? lastGatheringRef
: null
}
>
<Link href={`/gatherings/${gathering.id}`}>
<CardList
data={gathering}
isSaved={isSaved(gathering.id)}
handleButtonClick={handleButtonClick}
/>
</Link>
</div>
<MotionWrapper key={gathering.id}>
<div
ref={
index === 0
? firstGatheringRef
: index === gatherings.length - 1
? lastGatheringRef
: null
}
>
<Link href={`/gatherings/${gathering.id}`}>
<CardList
data={gathering}
isSaved={isSaved(gathering.id)}
handleButtonClick={handleButtonClick}
/>
</Link>
</div>
</MotionWrapper>
))
)}
</div>
Expand Down
48 changes: 25 additions & 23 deletions src/app/(main)/reviews/_components/ReviewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Loader from '@/app/components/Loader/Loader';
import GradientOverlay from '@/app/components/GradientOverlay/GradientOverlay';
import { ReviewsType } from '@/types/data.type';
import useScrollGradientEffect from '@/hooks/useScrollGradientEffect';
import MotionWrapper from '@/app/components/MotionWrapper/MotionWrapper';

interface ReviewListProps {
reviewList: ReviewsType[][];
Expand Down Expand Up @@ -43,29 +44,30 @@ const ReviewList = ({

{reviewList.flatMap((list) =>
list.map((item, index) => (
<div
key={item.id}
ref={
index === 0
? firstReviewRef
: index === reviewList.length - 1
? lastReviewRef
: null
}
data-testid='review-item'
>
<Link href={`/gatherings/${item.Gathering.id}`} className='block'>
<Review
image_source={item.Gathering.image}
rating={item.score}
description={item.comment}
place={item.Gathering.name}
location={item.Gathering.location}
user_name={item.User.name}
date={item.createdAt}
/>
</Link>
</div>
<MotionWrapper key={item.id}>
<div
ref={
index === 0
? firstReviewRef
: index === reviewList.length - 1
? lastReviewRef
: null
}
data-testid='review-item'
>
<Link href={`/gatherings/${item.Gathering.id}`} className='block'>
<Review
image_source={item.Gathering.image}
rating={item.score}
description={item.comment}
place={item.Gathering.name}
location={item.Gathering.location}
user_name={item.User.name}
date={item.createdAt}
/>
</Link>
</div>
</MotionWrapper>
)),
)}

Expand Down
6 changes: 5 additions & 1 deletion src/app/(main)/saved/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import useSavedGatherings from '@/hooks/useSavedGatherings';
import { useSavedGatheringList } from '@/context/SavedGatheringContext';
import { SORT_OPTIONS } from '@/constants/common';

import MotionWrapper from '@/app/components/MotionWrapper/MotionWrapper';

const SavedPage = () => {
const { savedGatherings } = useSavedGatheringList();

Expand Down Expand Up @@ -48,7 +50,9 @@ const SavedPage = () => {
{/* data list */}
<div className='mt-24 flex grow flex-col gap-24'>
{gatheringListData.length > 0 ? (
<SavedList dataList={gatheringListData} />
<MotionWrapper>
<SavedList dataList={gatheringListData} />
</MotionWrapper>
) : (
<div className='flex size-full grow items-center justify-center text-14 font-medium text-var-gray-500 dark:text-neutral-200'>
아직 찜한 모임이 없어요.
Expand Down
25 changes: 0 additions & 25 deletions src/app/components/Filter/Filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,31 +69,6 @@ describe('Filter List Component', () => {
expect(filterElement).toHaveTextContent('Filter');
expect(filterElement).toHaveClass('bg-var-gray-50 text-var-gray-800');
});

// 외부 영역 클릭 시 드롭다운 닫힘 테스트
it('closes dropdown when clicked outside', () => {
const filterElement = screen.getByTestId('filter-component');
fireEvent.click(filterElement);

fireEvent.mouseDown(document.body);

defaultProps.options.forEach((option) => {
const optionElement = screen.queryByText(option);
expect(optionElement).not.toBeInTheDocument();
});
});

// 리스트 아이템 클릭 시 드롭다운 닫힘 테스트
it('closes dropdown when clicked option item', () => {
const filterElement = screen.getByTestId('filter-component');
fireEvent.click(filterElement);

const optionElement = screen.getByText('Option 1');
fireEvent.click(optionElement);

const anotherOptionElement = screen.queryByText('Option 2');
expect(anotherOptionElement).not.toBeInTheDocument();
});
});

// Filter 컴포넌트 - type : sort 테스트
Expand Down
8 changes: 6 additions & 2 deletions src/app/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,18 @@ const Filter = ({
/>
)}

{isOpen && (
<div
className={`relative transition-opacity duration-300 ease-in-out ${
isOpen ? 'z-filter opacity-100' : 'pointer-events-none opacity-0'
}`}
>
<DropDown
options={[children, ...options]}
onSelect={handleOptionSelect}
onClose={() => setIsOpen(false)}
classnames={type === 'sort' ? 'min-w-max right-0' : ''}
/>
)}
</div>
</div>
);
};
Expand Down
47 changes: 6 additions & 41 deletions src/app/components/Filter/FilterDate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,51 +52,16 @@ describe('FilterDate Component Test', () => {
expect(iconElement).toBeInTheDocument();
});

// 클릭 시 모달이 열리고 닫히는지 확인
it('toggles dropdown on click', () => {
const filterElement = screen.getByTestId('filterDate');

fireEvent.click(filterElement);
expect(screen.getByTestId('calendar-modal')).toBeInTheDocument();
// 날짜 선택을 누르면 모달이 보이는지 확인
it('toggles dropdown visibility based on isOpen state', () => {
const filterElement = screen.getByText('날짜 선택');

fireEvent.click(filterElement);
expect(screen.queryByTestId('calendar-modal')).not.toBeInTheDocument();
});

// 외부 영역 클릭 시 모달이 닫히는지 확인
it('closes dropdown when clicked outside', () => {
const filterElement = screen.getByTestId('filterDate');
const modalWrapper = screen.getByTestId('calendar-modal-wrapper');

fireEvent.click(filterElement);
expect(screen.getByTestId('calendar-modal')).toBeInTheDocument();

fireEvent.mouseDown(document.body);
expect(screen.queryByTestId('calendar-modal')).not.toBeInTheDocument();
});

// 날짜 선택 시 상태 업데이트 확인
it('selects a date and updates state', () => {
const filterElement = screen.getByTestId('filterDate');
fireEvent.click(filterElement);

const dateInput = screen.getByTestId('date-input');

// 내일 날짜를 input에 세팅
const date = new Date();
date.setDate(date.getDate() + 1);
const dateStr = date.toISOString().slice(0, 10);
fireEvent.change(dateInput, { target: { value: dateStr } });

// '적용' 버튼 클릭
const applyButton = screen.getByText('적용');
fireEvent.click(applyButton);

// state 업데이트 확인
expect(filterElement).toHaveClass('text-var-gray-50 bg-var-gray-900');
// 선택된 날짜 표시 확인
expect(filterElement).toHaveTextContent(formatDate(dateStr));
// 모달이 닫히는지 확인
expect(screen.queryByTestId('calendar-modal')).not.toBeInTheDocument();
expect(modalWrapper).toHaveStyle('visibility: visible');
expect(modalWrapper).not.toHaveClass('pointer-events-none');
});

// 초기화 버튼 클릭 시 상태 업데이트 확인
Expand Down
25 changes: 13 additions & 12 deletions src/app/components/Filter/FilterDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,19 @@ const FilterDate = ({
/>
</div>

{isOpen && (
<div
className={`absolute z-dropdown mt-4 h-auto w-full min-w-max overflow-y-auto rounded-xl bg-var-gray-50 ring-2 ring-var-gray-400 dark:bg-neutral-800 dark:ring-neutral-700`}
>
<CalendarModal
initialSelectedData={selectedDate}
handleClickButtons={handleClickButtons}
CalendarProps={{ changeEndDays: 0 }}
onCloseModal={() => setIsOpen(false)}
/>
</div>
)}
<div
className={`absolute z-dropdown mt-4 h-auto w-full min-w-max overflow-y-auto rounded-xl bg-var-gray-50 ring-2 ring-var-gray-400 transition-opacity duration-300 ease-in-out dark:bg-neutral-800 dark:ring-neutral-700 ${
isOpen ? 'z-filter opacity-100' : 'pointer-events-none opacity-0'
}`}
data-testid='calendar-modal-wrapper'
>
<CalendarModal
initialSelectedData={selectedDate}
handleClickButtons={handleClickButtons}
CalendarProps={{ changeEndDays: 0 }}
onCloseModal={() => setIsOpen(false)}
/>
</div>
</div>
);
};
Expand Down
5 changes: 4 additions & 1 deletion src/app/components/Modal/ModalFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactNode } from 'react';
import ModalPortal from './ModalPortal';
import MotionWrapper from '../MotionWrapper/MotionWrapper';

interface ModalFrameProps {
children: ReactNode;
Expand All @@ -13,7 +14,9 @@ const ModalFrame = ({ children, onClose }: ModalFrameProps) => {
onClick={onClose}
className='fixed left-0 top-0 z-popup flex h-full w-full items-center justify-center bg-var-black/50 md:h-screen'
>
<div onClick={(e) => e.stopPropagation()}>{children}</div>
<MotionWrapper>
<div onClick={(e) => e.stopPropagation()}>{children}</div>
</MotionWrapper>
</div>
</ModalPortal>
);
Expand Down
Loading

0 comments on commit d03c385

Please sign in to comment.