Skip to content

Commit 0f330a4

Browse files
authored
Merge pull request #92 from codeit-maso/feature/jeon
✨feat : 캐러셀 한칸 이동, 스켈레톤 UI, css 수정
2 parents 57994be + 7ffd1ce commit 0f330a4

File tree

17 files changed

+415
-126
lines changed

17 files changed

+415
-126
lines changed

src/api/getRecipients.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { api } from './api';
22

3-
export default async function getRecipients() {
3+
export default async function getRecipients(sort = '') {
44
const teamId = import.meta.env.VITE_TEAM_ID;
5-
const res = await api.get(`/${teamId}/recipients/`);
5+
const res = await api.get(
6+
`/${teamId}/recipients/?limit=8&offset=0&sort=${sort}`,
7+
);
68
return res.data;
79
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useState, useEffect } from 'react';
2+
import styles from './Carousel.module.scss';
3+
import RecipientCard from '../RecipientCard/RecipientCard';
4+
5+
export default function Carousel({ recipients }) {
6+
const [index, setIndex] = useState(0);
7+
const [offsetX, setOffsetX] = useState({}); // x좌표
8+
const [startX, setstartX] = useState(0); // 클릭 시작 좌표 - 터치 스크롤
9+
const [isBouncing, setBouncing] = useState(false); // 캐러셀 끝이면 bouncing 모션
10+
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
11+
const isDesktop = windowWidth > 1023;
12+
13+
useEffect(() => {
14+
function handleResize() {
15+
setWindowWidth(window.innerWidth);
16+
}
17+
window.addEventListener('resize', handleResize);
18+
return () => {
19+
window.removeEventListener('resize', handleResize);
20+
};
21+
}, []);
22+
23+
// 캐러셀 버튼 작동과정: button onclick --> settingIndex(), setIndex --> useEffect( setOffsetX(),[index] ): x좌표 상태 업데이트: 캐러셀 이동
24+
useEffect(() => {
25+
setOffsetX({
26+
transform: `translateX(-${index * 295}px)`,
27+
});
28+
}, [index]);
29+
30+
function settingIndex(direction) {
31+
setIndex((prev) => (direction === 'next' ? prev + 1 : prev - 1)); // next? next index : back index
32+
}
33+
34+
// 터치, 마우스 드래그 감지 --> 캐러셀 한 칸 이동
35+
function handleStart(e) {
36+
if (isDesktop) return;
37+
const touchStart =
38+
e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
39+
setstartX(touchStart);
40+
}
41+
42+
function handleEnd(e) {
43+
const touchEnd =
44+
e.type === 'touchend' ? e.changedTouches[0].clientX : e.clientX;
45+
const distance = Math.abs(touchEnd - startX); //드래그 거리
46+
const isNext = startX > touchEnd; // direction(next,back) 결정
47+
if (isDesktop || distance < 10) return;
48+
49+
if (!isNext) {
50+
if (index === 0) {
51+
setBouncing(true); //캐러셀 끝 -> setBounce(띠용)
52+
return;
53+
} else if (index > 0) {
54+
settingIndex('back');
55+
return;
56+
}
57+
} else if (isNext) {
58+
if (index === 5) {
59+
setBouncing(true);
60+
return;
61+
} else if (index < 5) {
62+
settingIndex('next');
63+
return;
64+
}
65+
}
66+
}
67+
useEffect(() => {
68+
if (isBouncing) {
69+
const timer = setTimeout(() => {
70+
setBouncing(false); // Bouncing 모션 끝나고 바로 리셋
71+
}, 500);
72+
}
73+
}, [isBouncing]);
74+
75+
return (
76+
<div
77+
className={`${styles.carousel} ${isBouncing && styles['end-of-carousel']}`}
78+
onMouseDown={handleStart}
79+
onMouseUp={handleEnd}
80+
onTouchStart={handleStart}
81+
onTouchEnd={handleEnd}
82+
>
83+
<div className={styles['carousel__cardset-wrapper']}>
84+
<div className={styles['carousel__cardset']} style={offsetX}>
85+
{recipients.map((it) => (
86+
<RecipientCard Recipient={it} key={it.id}></RecipientCard>
87+
))}
88+
</div>
89+
</div>
90+
{index > 0 && ( //시작점 이후부터
91+
<button
92+
onClick={() => settingIndex('back')}
93+
className={`${styles['carousel__direction-button']} ${styles.back}`}
94+
></button>
95+
)}
96+
{recipients.length > 4 && // 캐러셀 끝에 도달하기 전까지
97+
index < 4 && (
98+
<button
99+
onClick={() => settingIndex('next')}
100+
className={`${styles['carousel__direction-button']}`}
101+
></button>
102+
)}
103+
</div>
104+
);
105+
}

src/pages/RecipientList/Carousel.module.scss renamed to src/components/Carousel/Carousel.module.scss

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
.carousel__cardset {
1212
display: flex;
1313
gap: 20px;
14-
transition: transform 0.5s ease-in-out;
14+
transition: transform 0.45s ease-in-out;
1515
}
1616

1717
.carousel::before,
@@ -73,37 +73,74 @@
7373
transform: scaleX(-1) translateY(-50%);
7474
left: 40px;
7575
}
76+
77+
&:active {
78+
border: solid 2px $purple-200;
79+
}
7680
}
7781

7882
// 반응형
79-
@media (min-width: 0px) and (max-width: 768px) {
83+
@media (max-width: 1200px) {
84+
//캐러셀 안밀리도록 추가함
8085
.carousel__cardset-wrapper {
81-
overflow: scroll;
8286
width: 100vw;
83-
-ms-overflow-style: none;
84-
}
85-
.carousel__cardset-wrapper::-webkit-scrollbar {
86-
display: none;
87-
}
88-
.carousel__direction-button {
89-
display: none;
9087
}
9188
}
92-
@media (min-width: 361px) and (max-width: 768px) {
89+
// 태블릿
90+
@media (max-width: 1023px) {
91+
.carousel__cardset-wrapper {
92+
width: 100vw;
93+
}
9394
.carousel::before,
9495
.carousel::after {
9596
display: none;
9697
}
98+
.carousel__direction-button {
99+
display: none;
100+
}
97101
}
98-
@media (min-width: 0px) and (max-width: 360px) {
102+
103+
//모바일
104+
@media (max-width: 767px) {
99105
h2 {
100106
margin-bottom: 12px;
101107
}
102108
section {
103109
margin-left: 40px 20px 0px 20px;
104110
}
105-
.carousel::after,
106-
.carousel::before {
107-
display: none;
111+
}
112+
113+
// 캐러셀 끝 - 바운스 애니메이션
114+
@-webkit-keyframes bounce-horizontal {
115+
0% {
116+
transform: translateX(0);
117+
}
118+
30% {
119+
transform: translateX(-20px);
108120
}
121+
60% {
122+
transform: translateX(20px);
123+
}
124+
100% {
125+
transform: translateX(0);
126+
}
127+
}
128+
@keyframes bounce-horizontal {
129+
0% {
130+
transform: translateX(0);
131+
}
132+
30% {
133+
transform: translateX(-20px);
134+
}
135+
60% {
136+
transform: translateX(20px);
137+
}
138+
100% {
139+
transform: translateX(0);
140+
}
141+
}
142+
143+
.end-of-carousel {
144+
-webkit-animation: bounce-horizontal 0.5s ease;
145+
animation: bounce-horizontal 0.5s ease;
109146
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import styles from './CarouselSkeleton.module.scss';
2+
3+
export default function CarouselSkeleton() {
4+
return (
5+
<>
6+
<div className={styles['section--sk']}>
7+
<div className={styles['card--sk']}></div>
8+
<div className={styles['card--sk']}></div>
9+
<div className={styles['card--sk']}></div>
10+
<div className={styles['card--sk']}></div>
11+
</div>
12+
</>
13+
);
14+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@-webkit-keyframes shimmer {
2+
from {
3+
transform: translateX(-300px);
4+
}
5+
to {
6+
transform: translateX(600px);
7+
}
8+
}
9+
10+
@keyframes shimmer {
11+
from {
12+
transform: translateX(-300px);
13+
}
14+
to {
15+
transform: translateX(600px);
16+
}
17+
}
18+
19+
.section--sk {
20+
// position: absolute;
21+
// top: 52px;
22+
// z-index: 1;
23+
display: flex;
24+
gap: 20px;
25+
}
26+
.card--sk {
27+
overflow: hidden;
28+
position: relative;
29+
min-width: 275px;
30+
height: 260px;
31+
border-radius: 16px;
32+
background-color: #e0e0e0;
33+
}
34+
.card--sk::after {
35+
content: '';
36+
position: absolute;
37+
left: 0;
38+
width: 300px;
39+
height: 100%;
40+
background-size: 300px;
41+
background: linear-gradient(0.25turn, #e0e0e0 0%, #f5f5f5 50%, #e0e0e0 100%);
42+
-webkit-animation: shimmer 1.2s infinite;
43+
animation: shimmer 1.2s infinite;
44+
}
45+
46+
//모바일,태블릿 공통 적용
47+
@media (max-width: 1023px) {
48+
.section--sk {
49+
max-width: 100%;
50+
overflow: hidden;
51+
padding: 0 20px;
52+
}
53+
}
54+
//모바일
55+
@media (max-width: 767px) {
56+
.card--sk {
57+
min-width: 208px;
58+
height: 232px;
59+
}
60+
}
File renamed without changes.
File renamed without changes.

src/pages/RecipientList/RecipientCard.jsx renamed to src/components/RecipientCard/RecipientCard.jsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styles from './RecipientCard.module.scss';
2-
import RecentMessages from './RecentMessages';
3-
import TopReactions from './TopReactions';
2+
import RecentMessages from '../RecentMessages/RecentMessages';
3+
import TopReactions from '../TopReactions/TopReactions';
44
import { useNavigate } from 'react-router-dom';
55

66
//캐러셀 내부 요소 - 카드 컴포넌트
@@ -29,15 +29,17 @@ export default function RecipientCard({ Recipient }) {
2929
onClick={() => navigate(`/post/${id}`)}
3030
>
3131
{backgroundColor === 'blue' && <div className={styles.triangle} />}
32-
<h3 className={backgroundImageURL && styles.white}>{`To. ${name}`}</h3>
32+
<h3
33+
className={`${styles['card__h3']} ${backgroundImageURL ? styles.white : ''}`}
34+
>{`To. ${name}`}</h3>
3335
<RecentMessages
3436
messages={recentMessages}
3537
count={messageCount}
3638
></RecentMessages>
3739
<div
38-
className={`${styles['writer-count']} ${backgroundImageURL && styles.white}`}
40+
className={`${styles['card__writer-count']} ${backgroundImageURL && styles.white}`}
3941
>
40-
<span className={styles.count}>{messageCount}</span>
42+
<span className={styles['card__count']}>{messageCount}</span>
4143
<span>명이 작성했어요!</span>
4244
</div>
4345
<div className={styles['card__centerline']}></div>

0 commit comments

Comments
 (0)