Skip to content

Commit ea977ad

Browse files
kimnamheeeec0912jyclaude
authored
배포 (#96)
* 🧹 chore: add tanstack router plugins * ✨ feat(routes): integrate tanstack router * ✨ feat(routes): add basic routes profile, explore, login * 🧹 chore(eslint): add ignore for shared/ui * ✨ feat(components): add sidebar component * 🧹 chore(asset): add instagram logo * 💎 style(index): initialize style * ✨ feat(route): add /me to route * ✨ feat(provider): add sidebar provider * ✨ feat(route): configure provider * ✨ feat(components): add sidebar navigation item component * ✨ feat(component): add sidebar component * ✨ feat(routes): separate homepage component * ✨ feat(components): add drawer component * ✨ feat(components): add dialog component * ✨ feat(components): add action, type to navItems * ✨ feat(components): add app level modal, drawer * ✨ feat(components): open drawer on search click, open modal on create click * 로그인 페이지 UI 구현 * 비밀번호 찾기 화면 구현 * 계정 생성 UI 및 비밀번호 생성 로직 구현 * 로그인 메인화면 가입하기 버튼 버그 수정 * ✨ feat(components): add create post modal component * package-lock.json 삭제 * ✨ feat(components): add sonner * ✨ feat(provider): integrate toaster * ✨ feat(components): add dropzone component * ✨ feat(components): integrate drag and drop to createmodal * ♻️ refactor(components): separate logic using custom hook * ♻️ refactor(components): separate ui components * ✨ feat(components): add post details pane * ✨ feat(components): add double modal on exit * 🧹 chore: add testing library * 🧪 test(createmodal): add create post modal related tests * ✨ feat(components): add profile header * ✨ feat(api): initialize ky instance * ✨ feat(components): add tabs * ✨ feat(routes): separate app route layer * ✨ feat(routes): update route tree * ✨ feat(components): add fallback image * ✨ feat(profile-posts): add grid ui * ✨ feat(profilepage): add posts grid ui * 🧹 chore(homepage): remove duplicate navigation shell * ♻️ refactor(profilepage): add common style container component * ✨ feat(profile): add profile page component * ✨ feat(navigation): add useNavController hook * 🧹 chore: rerun yarn install * ✨ feat(msw): msw 기본 설정 * ✨ feat(components): add carousel * ♻️ refactor(sidebar): collapse sidebar below xl * ✨ feat(components): add stories field * ✨ feat(stories): add link to story route * ✨ feat: msw 구조 수정 * ✨ feat: msw post db 업데이트 및 기본 레이아웃 구현 * ✨ feat: 사진 기본 ui 구현 * ✨ feat: 하트 색깔 및 불투명도 수정 * ✨ feat: 하트 올라가는 애니메이션 구현 * ✨ feat: 하트 애니메이션 구체화 및 버그 수정 * ✨ feat(msw): add common response type * 🐛 fix(msw): fix common response type field name * ✨ feat(msw): add mock users * ✨ feat(msw): add follow handlers * 💎 style(sidebar): add padding * ✨ feat(components): add follow list modal component needs to be fixed based on real api * ✨ feat(components): add mock data / match data type * 💎 style(search): adjust search drawer style * ✨ feat: ... (설정) 화면 모달 구현 * ✨ feat(search): apply debounce to input value * ✨ feat: msw 구현 (comment) * 🧹 chore: remove duplicate key * ✨ feat: 댓글 ui * ✨ feat: 댓글 설정창(...) ui 구현 * ✨ feat: ... 모달 로직 수정 * ✨ feat: 글 본문 및 프로필 ui 수정 (여백 정리) * ✨ feat(msw): warn unhandled requests for debugging * ✨ feat(gitignore): add env * ✨ feat(providers): add query client provider * ✨ feat(api): add test (health check) handler * ✨ feat(route): add test route for api connection test * ✨ feat: 액션 바 (상호작용) ui 구현 * ✨ feat: env 삭제 * 💎 style(sidebar): update layout stability * ✨ feat(sidebar): updage search drawer toggle logic * ✨ feat(sidebar): remain page width when opening search drawer * ✨ feat(components): add pagination * ✨ feat(components): add shared components lazyimage / card * ✨ feat(msw): add post mock data * ✨ feat(api): add feed schema / types * ✨ feat(api): add handler / api function * ✨ feat(home): add feed ui * ✨ feat(actions): add env variable on cd workflow * 💎 style(sidebar): collapse sidebar on search drawer toggle * 🐛 fix(route): update route from profile_id to post_id * ✨ feat(feed): link to post detail on click * ✨ feat(components): add dropdown * ✨ feat(msw): add album handler * ✨ feat(api): add album api call functions * ✨ feat(api): add album api call function * 🐛 fix(msw): adjust handler order * 💎 style(dropdown): remove circle icon * ✨ feat(album): add album dropdown * 🧪 test(album): add album dropdown tests * 🐛 fix(actions): apply secrets * 🧹 chore: update test:ci command option * ✨ feat(msw): add bookmarks handler * ✨ feat(bookmarks): add bookmarked posts ui * ✨ feat(postdetail): return to previous page when closing page * ✨ feat(postdetail): validate search params with zod * ✨ feat: 하단 푸터 - 위치 화면 구현 * ✨ feat: 하단 푸터 수정 (Instagram Lite 추가) * ✨ feat: 푸터 완성 * ✨ feat: 푸터 ui, 기능, 경로 설정 완성 * ✨ feat: 로그인 msw 설정 * ✨ feat: 로그인 검사 로직 추가 및 카카오톡 로그인 ui 구현 * ✨ feat: 파일 구조 변경 실제 구현과 유사하게 경로 변경 & 유지보수 및 관리 용이 목적 * ✨ feat: 푸터 완성 * ✨ feat: 비밀번호 찾기 창 ui 수정 * 💎 style(createmodal): fix modal ratio * ✨ feat(sidebar): 만들기 버튼을 게시글/스토리 업로드로 분리 - 기존 '만들기' 버튼을 '게시글 업로드'와 '스토리 업로드'로 분리 - 아이콘 구분: SquarePlus(게시글), CirclePlus(스토리) - 모바일 하단바 배경색 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✨ feat(create-story): 스토리 업로드 기능 구현 - 사진 1장만 업로드 가능 (초과 시 toast 알림) - 9:16 비율 미리보기, 검은 배경으로 빈 공간 채움 - 본문/앨범 단계 없이 바로 공유하기 - 이미지 업로드 후 헤더 타이틀 숨김 처리 - 이탈 시 확인 다이얼로그 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 💎 style(ui): 모달 외곽선 제거 및 안내 문구 word-break 추가 - DialogContent 기본 스타일에서 border 제거 - 이미지 업로드 안내 문구에 break-keep 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 💎 style(story): 레이아웃 수정 - xs 너비에서 미리보기, dropzone 영역의 높이가 지나치게 높아지는 문제 해결 * 🧹 chore(constants): add constant for api errors * ✨ feat(msw): add handlers for image upload * ✨ feat(msw): add handlers for stories * ♻️ refactor(component): change component name * ✨ feat(msw): add search handler * ✨ feat(explore): separate page component * ♻️ refactor: update variable names * 🧹 chore(msw): add mock data * ✨ feat(explore): add ui / api handling * ✨ feat(profile): add albums tab * ✨ feat(profile): add albums summary ui * 💎 style(album): update badge background color * ✨ feat(msw): add relations to msw mock data * ✨ feat(msw): add like related handlers * ✨ feat(msw): add bookmark toggle handler * ✨ feat: 비밀번호 찾기 기능 구현 * ✨ feat(like/bookmark): add like, bookmark toggle functionality * ✨ feat: footer 파일 위치 변경 * ✨ feat(home): add footer * ✨ feat(album): integrate get album detail api * ✨ feat: 컨플릭트 수정 * 🐛 fix(route): fix routeTree syntax error * ✨ feat: 회원가입 ui 구현 * ✨ feat: 회원가입 아이디 중복 체크 로직 구현 * ✨ feat: 회원가입 컴포넌트 분리 및 아이디 랜덤 생성 로직 수정 * ✨ feat: reset.tsx 구조 분리 * ✨ feat(msw): add post create handler * ✨ feat(create-post): add functionality and ui for creating post * ✨ feat(create-post): show close button on post create finish * ✨ feat(utils): add crop image util function * ✨ feat(create-story): integrate upload story api / image crop logic * 🐛 fix(test): add react query provider to tests * ✨ feat: 회원가입 정보 입력 화면 구현 * ✨ feat(stories): implement story feed api / integrate with component * 🐛 fix(route): update route tree file * 💎 style: layout stability * ✨ feat: 회원가입 구현 아직 토큰 관리 미흡, 이메일 전송 로직 미구현 * ✨ feat: 회원가입 라우트 간 보안 강화 로직 추가 * ✨ feat: 회원가입 푸터 라우트 연결 * ✨ feat: 푸터 버그 수정 * ✨ feat: 로그인 버그 수정 * ✨ feat: 로그인 버그 수정 * ✨ feat(msw): add search, recent search api handler * ✨ feat(search): add api function for search * ✨ feat(api): add followed / name to search api * ✨ feat(recent): add recent search keyword management * 🧹 chore: remove unused files * ✨ feat: 댓글 api 연결 * 🐛 fix(entity): update schema make name, profileImageUrl nullable * ✨ feat(ui): add fallback for null profileImages * ✨ feat: 댓글 추가 및 좋아요 api 연결 * ✨ feat(follow): integrate follow/unfollow apis * ✨ feat(user): add GET profile api function * ✨ feat(search): add staletime for cache control * 🐛 fix(entity): update schema make bio, name, profileImageUrl nullable * ✨ feat(routes): update profile routes * 🐛 fix(msw): fix field name success -> isSuccess * ♻️ refactor(schema): rename success to isSuccess in API response schemas Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ♻️ refactor(api): update isSuccess field references in API functions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ♻️ refactor(auth): update isSuccess field references in auth components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✨ feat(user): add fallback UI for null profileImageUrl Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ♻️ refactor(search): update search UI components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✨ feat(profile): update profile page and navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✨ feat: 로그인 토큰 구현 * 🐛 fix(msw): fix handler order * 🐛 fix(test): update api field name for test * ✨ feat: 댓글 api 연결 수정 및 로그인 토큰 작업, 회원가입 시 자동 로그인 구현 --------- Co-authored-by: c0912jy <c0912jy@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9875ac3 commit ea977ad

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1604
-729
lines changed

src/app/providers/Providers.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import type { PropsWithChildren } from 'react'
2-
32
import { ThemeProvider } from 'next-themes'
4-
53
import { SidebarProvider } from '@/shared/ui/sidebar'
64
import { Toaster } from '@/shared/ui/sonner'
75
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
6+
import { AuthProvider } from '@/shared/auth/AuthProvider'
87

98
const queryClient = new QueryClient()
109

1110
export function Providers({ children }: PropsWithChildren) {
1211
return (
1312
<QueryClientProvider client={queryClient}>
1413
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
15-
<SidebarProvider>{children}</SidebarProvider>
14+
<AuthProvider>
15+
<SidebarProvider>{children}</SidebarProvider>
16+
</AuthProvider>
1617
<Toaster duration={1500} className="text-center" />
1718
</ThemeProvider>
1819
</QueryClientProvider>

src/components/auth/LoginCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface FloatingInputProps {
1414
}
1515

1616
interface LoginResponse {
17-
success: boolean
17+
isSuccess: boolean
1818
code: string
1919
message: string
2020
data: {
@@ -89,7 +89,7 @@ const LoginCard = () => {
8989
})
9090
.json<LoginResponse>()
9191

92-
if (res.success && res.data) {
92+
if (res.isSuccess && res.data) {
9393
localStorage.setItem('accessToken', res.data.accessToken)
9494
localStorage.setItem('refreshToken', res.data.refreshToken)
9595
navigate({ to: '/' })

src/components/post/CommentItem.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ interface Comment {
1010
content: string
1111
profileImageUrl: string
1212
createdAt: string
13+
likeCount: number
14+
liked: boolean
1315
}
1416

1517
interface CommentItemProps {
@@ -44,31 +46,33 @@ export default function CommentItem({
4446
setIsMenuOpen(false)
4547
}
4648

49+
const displayLikeCount = comment.likeCount || 0
50+
4751
return (
4852
<>
4953
<div
5054
className="group relative flex cursor-pointer items-start justify-between gap-3 px-1 py-1.5 select-none"
5155
onDoubleClick={() => onDoubleClick(comment.id)}
5256
>
53-
<div className="flex flex-1 gap-3">
57+
<div className="flex flex-1 items-start gap-3">
5458
<img
5559
src={comment.profileImageUrl}
56-
className={`${isReply ? 'h-6 w-6' : 'h-8 w-8'} shrink-0 rounded-full object-cover`}
60+
className={`${isReply ? 'h-6 w-6' : 'h-8 w-8'} mt-0.5 shrink-0 rounded-full object-cover`}
5761
alt=""
5862
/>
59-
<div className="text-sm">
63+
<div className="text-sm leading-tight">
6064
<span className="mr-2 font-semibold text-black">
6165
{comment.nickname}
6266
</span>
6367
<span className="break-all text-black">{comment.content}</span>
64-
<div className="mt-1 flex h-4 items-center gap-3 text-xs font-semibold text-gray-500">
65-
<span>{timeDisplay}</span>
66-
<button
67-
className="hover:text-gray-900"
68-
onClick={(e) => e.stopPropagation()}
69-
>
70-
좋아요
71-
</button>
68+
69+
<div className="mt-[7px] flex h-4 items-center gap-3 text-xs font-semibold text-gray-500">
70+
<span className="font-normal">{timeDisplay}</span>
71+
{displayLikeCount > 0 && (
72+
<span className="font-semibold text-gray-500">
73+
좋아요 {displayLikeCount}
74+
</span>
75+
)}
7276
<button
7377
className="hover:text-gray-900"
7478
onClick={(e) => e.stopPropagation()}
@@ -84,14 +88,18 @@ export default function CommentItem({
8488
</div>
8589
</div>
8690
</div>
91+
8792
<button
88-
onClick={(e) => onHeartClick(comment.id, e)}
89-
className="mt-1.5 flex-shrink-0"
93+
onClick={(e) => {
94+
e.stopPropagation()
95+
onHeartClick(comment.id, e)
96+
}}
97+
className="mt-1.5 flex-shrink-0 p-1"
9098
>
9199
<Heart
92-
className={`h-3 w-3 transition-colors ${
100+
className={`h-3 w-3 transition-all ${
93101
isLiked
94-
? 'fill-red-500 text-red-500'
102+
? 'scale-110 fill-[#ED4956] text-[#ED4956]'
95103
: 'text-gray-400 hover:text-gray-600'
96104
}`}
97105
/>

src/components/post/PostDetail.tsx

Lines changed: 127 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,33 @@ import { useParams, useLocation, useNavigate } from '@tanstack/react-router'
33
import { X, ChevronLeft, ChevronRight, Heart } from 'lucide-react'
44
import { motion, AnimatePresence } from 'framer-motion'
55
import PostInfoSection from './PostInfoSection'
6+
import { instance } from '../../shared/api/ky'
7+
8+
export interface PostImage {
9+
id: number
10+
url: string
11+
orderIndex: number
12+
}
613

714
export interface PostData {
8-
id: string
9-
images: string[]
10-
caption: string
11-
username: string
12-
userImage: string
13-
createdAt: string
15+
id: number
16+
userId: number
17+
nickname: string
18+
profileImageUrl: string
19+
content: string
20+
albumId: number
21+
images: PostImage[]
1422
likeCount: number
1523
commentCount: number
24+
createdAt: string
25+
updatedAt: string
26+
liked: boolean
27+
bookmarked: boolean
28+
}
29+
30+
interface SearchParams {
31+
returnToPath?: string
32+
returnToSearch?: Record<string, string>
1633
}
1734

1835
export default function PostDetail() {
@@ -27,17 +44,25 @@ export default function PostDetail() {
2744
const [isAnimating, setIsAnimating] = useState(false)
2845

2946
useEffect(() => {
30-
fetch(`/api/v1/posts/${postId}`)
31-
.then((res) => res.json())
32-
.then((json) => {
33-
if (json.success) setPostData(json.data)
34-
})
47+
const fetchPost = async () => {
48+
try {
49+
const res = await instance
50+
.get(`api/v1/posts/${postId}`)
51+
.json<{ data: PostData }>()
52+
setPostData(res.data)
53+
} catch {
54+
console.error('Failed to fetch post')
55+
}
56+
}
57+
fetchPost()
3558
}, [postId])
3659

3760
const images = postData?.images || []
61+
3862
const handleClose = () => {
39-
const returnToPath = location.search.returnToPath
40-
const returnToSearch = location.search.returnToSearch
63+
const search = location.search as SearchParams
64+
const returnToPath = search.returnToPath
65+
const returnToSearch = search.returnToSearch
4166

4267
if (returnToPath) {
4368
navigate({
@@ -58,13 +83,10 @@ export default function PostDetail() {
5883

5984
const handleDoubleLike = () => {
6085
if (isAnimating) return
61-
6286
setIsAnimating(true)
6387
const rotate = Math.floor(Math.random() * 61) - 30
6488
setRandomRotate(rotate)
6589
setShowHeart(true)
66-
67-
// 기울기 유지(0.4s) + 정방향 회전(0.3s) = 총 0.7초 후 사라짐 시작
6890
setTimeout(() => {
6991
setShowHeart(false)
7092
}, 700)
@@ -91,82 +113,102 @@ export default function PostDetail() {
91113
</button>
92114

93115
<div
94-
className="relative flex h-[90%] w-[95%] max-w-[1200px] overflow-hidden rounded-sm bg-white shadow-2xl"
116+
className="relative flex h-fit max-h-[90%] w-[95%] max-w-[1200px] overflow-hidden rounded-sm bg-white shadow-2xl"
95117
onClick={(e) => e.stopPropagation()}
96118
>
97-
<div className="relative hidden w-[60%] items-center justify-center overflow-hidden bg-black md:flex">
98-
<motion.div
99-
className="flex h-full w-full"
100-
animate={{ x: `-${currentIndex * 100}%` }}
101-
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
102-
onDoubleClick={handleDoubleLike}
119+
<div className="relative hidden w-[60%] flex-col items-center justify-center bg-black md:flex">
120+
<div
121+
className="relative w-full overflow-hidden"
122+
style={{ aspectRatio: '1 / 1' }}
103123
>
104-
{images.map((img, idx) => (
105-
<div
106-
key={idx}
107-
className="flex h-full min-w-full shrink-0 items-center justify-center"
108-
>
109-
<img
110-
src={img}
111-
alt=""
112-
className="h-full w-full object-contain select-none"
113-
/>
114-
</div>
115-
))}
116-
</motion.div>
117-
118-
<AnimatePresence>
119-
{showHeart && (
120-
<motion.div
121-
key="rising-heart"
122-
initial={{ scale: 0, opacity: 1, y: 0, rotate: randomRotate }}
123-
animate={{
124-
scale: [0, 1.2, 1],
125-
y: -20,
126-
rotate: [randomRotate, randomRotate, 0],
127-
}}
128-
exit={{
129-
y: -700,
130-
transition: { duration: 0.2, ease: 'circIn' },
124+
<motion.div
125+
className="flex h-full w-full"
126+
animate={{ x: `-${currentIndex * 100}%` }}
127+
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
128+
onDoubleClick={handleDoubleLike}
129+
>
130+
{images.map((img) => (
131+
<div
132+
key={img.id}
133+
className="flex h-full min-w-full shrink-0 items-center justify-center"
134+
>
135+
<img
136+
src={img.url}
137+
alt=""
138+
className="h-full w-full object-cover select-none"
139+
/>
140+
</div>
141+
))}
142+
</motion.div>
143+
144+
<AnimatePresence>
145+
{showHeart && (
146+
<motion.div
147+
key="rising-heart"
148+
initial={{ scale: 0, opacity: 1, y: 0, rotate: randomRotate }}
149+
animate={{
150+
scale: [0, 1.2, 1],
151+
y: -20,
152+
rotate: [randomRotate, randomRotate, 0],
153+
}}
154+
exit={{
155+
y: -700,
156+
transition: { duration: 0.2, ease: 'circIn' },
157+
}}
158+
transition={{
159+
duration: 0.7,
160+
times: [0, 0.57, 1],
161+
ease: 'easeInOut',
162+
}}
163+
onAnimationComplete={() => setIsAnimating(false)}
164+
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center"
165+
>
166+
<Heart
167+
className="h-32 w-32 drop-shadow-2xl"
168+
style={{ fill: 'url(#heart-gradient)', stroke: 'none' }}
169+
/>
170+
</motion.div>
171+
)}
172+
</AnimatePresence>
173+
174+
{images.length > 1 && currentIndex > 0 && (
175+
<button
176+
type="button"
177+
onClick={(e) => {
178+
e.stopPropagation()
179+
moveSlide(-1)
131180
}}
132-
transition={{
133-
duration: 0.7,
134-
times: [0, 0.57, 1],
135-
ease: 'easeInOut',
181+
className="absolute top-1/2 left-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
182+
>
183+
<ChevronLeft className="h-5 w-5 text-black" strokeWidth={3} />
184+
</button>
185+
)}
186+
{images.length > 1 && currentIndex < images.length - 1 && (
187+
<button
188+
type="button"
189+
onClick={(e) => {
190+
e.stopPropagation()
191+
moveSlide(1)
136192
}}
137-
onAnimationComplete={() => setIsAnimating(false)}
138-
className="pointer-events-none absolute z-20 flex items-center justify-center"
193+
className="absolute top-1/2 right-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
139194
>
140-
<Heart
141-
className="h-32 w-32 drop-shadow-2xl"
142-
style={{ fill: 'url(#heart-gradient)', stroke: 'none' }}
143-
/>
144-
</motion.div>
195+
<ChevronRight className="h-5 w-5 text-black" strokeWidth={3} />
196+
</button>
145197
)}
146-
</AnimatePresence>
147-
148-
{images.length > 1 && currentIndex > 0 && (
149-
<button
150-
onClick={(e) => {
151-
e.stopPropagation()
152-
moveSlide(-1)
153-
}}
154-
className="absolute left-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 shadow-md"
155-
>
156-
<ChevronLeft className="h-5 w-5 text-black" strokeWidth={3} />
157-
</button>
158-
)}
159-
{images.length > 1 && currentIndex < images.length - 1 && (
160-
<button
161-
onClick={(e) => {
162-
e.stopPropagation()
163-
moveSlide(1)
164-
}}
165-
className="absolute right-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 shadow-md"
166-
>
167-
<ChevronRight className="h-5 w-5 text-black" strokeWidth={3} />
168-
</button>
169-
)}
198+
199+
{images.length > 1 && (
200+
<div className="absolute bottom-4 left-1/2 z-30 flex -translate-x-1/2 gap-1.5">
201+
{images.map((_, i) => (
202+
<div
203+
key={i}
204+
className={`h-1.5 w-1.5 rounded-full transition-colors ${
205+
i === currentIndex ? 'bg-white' : 'bg-white/50'
206+
}`}
207+
/>
208+
))}
209+
</div>
210+
)}
211+
</div>
170212
</div>
171213

172214
<div className="flex w-full flex-col border-l border-gray-200 bg-white md:w-[40%]">

0 commit comments

Comments
 (0)