Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions app/(main)/meetup/list/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Suspense } from "react";
import { Metadata } from "next";
import { cn } from "@/utils/cn";
import Container from "@/components/layout/Container";
import QueryErrorBoundary from "@/components/common/QueryErrorBoundary";
import Banner from "@/features/meetup/list/components/Banner";
import { MeetupListScrollProvider } from "@/features/meetup/list/providers/MeetupListScrollProvider";
import ListFilters from "@/features/meetup/list/components/ListFilters";
import CreateOpenButton from "@/features/meetup/create/components/CreateOpenButton";
import ListFiltersSkeleton from "@/features/meetup/list/components/ListFilters/ListFiltersSkeleton";
import MeetupCardList from "@/features/meetup/list/components/MeetupCardList";
import { MeetupListScrollProvider } from "@/features/meetup/list/providers/MeetupListScrollProvider";
import QueryErrorBoundary from "@/components/common/QueryErrorBoundary";
import { cn } from "@/utils/cn";
import CreateOpenButton from "@/features/meetup/create/components/CreateOpenButton";

export const metadata: Metadata = {
title: "모임 찾기",
Expand All @@ -17,6 +18,8 @@ export const metadata: Metadata = {
},
};

const size = 10;

export default function MeetupListPage() {
return (
<MeetupListScrollProvider>
Expand All @@ -26,18 +29,18 @@ export default function MeetupListPage() {
"md:min-h-[calc(100vh-88px)] md:gap-y-4 md:p-6 lg:gap-y-6 lg:pt-7",
)}>
<Banner />
<Suspense fallback={null}>
<ListFilters className={cn("mx-0 bg-gray-50 px-6 py-2", "md:-mx-4 md:px-6")} />
</Suspense>
<Suspense fallback={null}>
<QueryErrorBoundary prefix="모임 목록을 ">
<MeetupCardList className={cn("mb-10 flex-1 px-4", "md:mb-12 md:px-0 lg:mb-26")} />
</QueryErrorBoundary>
<Suspense fallback={<ListFiltersSkeleton className={ListFiltersStyle} />}>
<ListFilters className={ListFiltersStyle} />
</Suspense>
<QueryErrorBoundary prefix="모임 목록을 ">
<MeetupCardList size={size} />
</QueryErrorBoundary>
<Suspense fallback={null}>
<CreateOpenButton className="fixed right-6 bottom-6 z-10" />
</Suspense>
</Container>
</MeetupListScrollProvider>
);
}

const ListFiltersStyle = "mx-0 bg-gray-50 px-6 py-2 md:-mx-4 md:px-6";
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use client";

import { useCategoryStore } from "@/store/category.store";
import { cn } from "@/utils/cn";

interface ListFiltersSkeletonProps {
className?: string;
}

export default function ListFiltersSkeleton({ className }: ListFiltersSkeletonProps) {
const { categories } = useCategoryStore();
const typeTabCount = categories.length + 1;

return (
<div
className={cn(
"flex flex-col justify-center gap-y-2 md:gap-4 lg:flex-row lg:items-start",
className,
)}
aria-hidden>
<div className="relative min-w-0">
<TabRowSkeleton typeTabCount={typeTabCount} />
<KeywordAreaSkeleton />
</div>
<DropdownRowSkeleton />
</div>
);
}

const tabScrollRowClass =
"overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden";
const tabSkeletonShape = "box-border h-9 shrink-0 rounded-[0.875rem] px-4 md:h-10 md:rounded-2xl";
const dropdownTriggerShape = "h-6 shrink-0 rounded-lg md:h-8";

function TabRowSkeleton({ typeTabCount }: { typeTabCount: number }) {
return (
<ul className={cn(tabScrollRowClass, "flex gap-x-2.5")} aria-hidden>
{/* 검색 토글 */}
<li className="whitespace-nowrap">
<div
className={cn(
tabSkeletonShape,
"flex min-w-[52px] animate-pulse items-center justify-center bg-gray-200",
)}
/>
</li>
{/* 타입 탭 */}
{Array.from({ length: typeTabCount }, (_, i) => (
<li key={i} className="whitespace-nowrap">
<div className={cn(tabSkeletonShape, "w-20 animate-pulse bg-gray-200")} />
</li>
))}
</ul>
);
}

function KeywordAreaSkeleton() {
return (
<div
className={cn(
"grid transition-[grid-template-rows] duration-300 ease-out",
"grid-rows-[0fr] lg:grid-rows-[1fr]",
)}
aria-hidden>
<div className="min-h-0 overflow-hidden">
<div className="m-0.5 mt-3">
<div className="relative w-94">
<div className="h-11 w-full animate-pulse rounded-full bg-gray-200" />
</div>
</div>
</div>
</div>
);
}

function DropdownRowSkeleton() {
return (
<div className="flex shrink-0 items-center gap-x-1.5 lg:ml-auto" aria-hidden>
{/* 모든 드롭다운 스켈레톤: 동일한 폭으로 통일 */}
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className={cn(dropdownTriggerShape, "w-21 animate-pulse bg-gray-200 md:w-23")}
/>
))}
</div>
);
}
14 changes: 9 additions & 5 deletions features/meetup/list/components/MeetupCardItems/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Empty from "@/components/ui/Empty";
import JoinModal from "../JoinModal";

interface MeetupCardItemsProps {
/** 모임 목록 쿼리 */
query: UseInfiniteQueryResult<InfiniteData<MeetupListResponse>>;
}

Expand Down Expand Up @@ -59,15 +60,18 @@ interface MeetupCardLoadedItemsProps {
data: MeetupItem[] | undefined;
setSelectedData: (data: MeetupItemSelected) => void;
openModalFn: () => void;
className?: string;
}
function MeetupCardLoadedItems({ data, setSelectedData, openModalFn }: MeetupCardLoadedItemsProps) {
if (data?.length === 0) {
return (
<Empty section className="col-span-full">
아직 모임이 없어요
<br />
지금 바로 모임을 만들어보세요!
</Empty>
<li className="col-span-full">
<Empty section>
아직 모임이 없어요
<br />
지금 바로 모임을 만들어보세요!
</Empty>
</li>
);
}
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import GroupCard from "@/components/ui/GroupCard";

export default function MeetupCardListSkeleton({ size }: { size: number }) {
return Array.from({ length: size }).map((_, i) => (
<li key={i} className="w-full">
<GroupCard.Skeleton />
</li>
));
}
27 changes: 7 additions & 20 deletions features/meetup/list/components/MeetupCardList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,26 @@
"use client";

import GroupCard from "@/components/ui/GroupCard";
import { useGetMeetups } from "@/features/meetup/queries";
import MeetupCardItems from "../MeetupCardItems";
import { cn } from "@/utils/cn";

const size = 10;
import MeetupCardItems from "../MeetupCardItems";
import MeetupCardListSkeleton from "./MeetupCardListSkeleton";

interface MeetupCardListProps {
className?: string;
size: number;
}
export default function MeetupCardList({ className }: MeetupCardListProps) {
export default function MeetupCardList({ size }: MeetupCardListProps) {
const query = useGetMeetups(size);
const isRefetching = query.isFetching && !query.isFetchingNextPage && !query.isPending;

return (
<ul
className={cn(
"grid w-full content-start justify-items-stretch gap-4 transition-opacity duration-200 ease-in-out md:gap-6 lg:grid-cols-2",
"mb-10 flex-1 px-4",
"md:mb-12 md:px-0 lg:mb-26",
isRefetching && "pointer-events-none opacity-50",
className,
)}>
{query.isPending ? (
<MeetupCardSkeletonItems size={size} />
) : (
<MeetupCardItems query={query} />
)}
{query.isPending ? <MeetupCardListSkeleton size={size} /> : <MeetupCardItems query={query} />}
</ul>
);
}

function MeetupCardSkeletonItems({ size }: { size: number }) {
return Array.from({ length: size }).map((_, i) => (
<li key={i} className="w-full">
<GroupCard.Skeleton />
</li>
));
}
Loading