Skip to content

Commit

Permalink
✨ feat: search 페이지 UI 구현 #38
Browse files Browse the repository at this point in the history
  • Loading branch information
froggy1014 committed Aug 20, 2024
1 parent f412746 commit a8a649f
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RegistorButton } from "@/components/core/Button";
import { ErrorIcon } from "@/components/icons";

const SearchFestivalFallback = () => {
return (
<div className="flex flex-col items-center gap-[16px]">
<ErrorIcon width={56} height={56} className="text-gray-scale-300" />
<div className="flex w-full flex-col items-center gap-[6px]">
<h1 className="text-body1-medium text-gray-scale-900">
검색 결과가 없어요
</h1>
<span className="text-caption1-regular text-gray-scale-600">
원하는 페스티벌이 없다면 피에스타에 등록해주세요.
</span>
<RegistorButton
label="페스티벌 등록하기"
href="/festival/new"
className={"w-auto"}
/>
</div>
</div>
);
};

export default SearchFestivalFallback;
57 changes: 57 additions & 0 deletions src/app/(home)/search/_components/SearchFestivalList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import isEmpty from "lodash/isEmpty";
import isString from "lodash/isString";
import { useQueryState } from "nuqs";
import { useEffect } from "react";

import FestivalSearchTile from "@/components/core/List/FestivalSearchTile/FestivalSearchTile";
import { ProgressCircle } from "@/components/core/Progress";
import { useGetSearchFestival } from "@/hooks/useGetSearchFestival";
import { useSearchHistory } from "@/hooks/useSearchHistory";

import SearchFestivalFallback from "./SearchFestivalFallback";

const SearchFestival = () => {
const [query, setQuery] = useQueryState("query", { shallow: true });
const { data, isLoading } = useGetSearchFestival(query ?? "", 300);
const { set } = useSearchHistory();

useEffect(() => {
if (isString(query) && !isEmpty(query)) {
set(query);
}
}, [data]);

if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<ProgressCircle className="size-[100px]" />
</div>
);
}

if (query?.length === 0) {
return null;
}

if (data?.length === 0) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<SearchFestivalFallback />
</div>
);
}

return (
<div className="flex w-full flex-col gap-[4px] rounded-[8px] p-[12px]">
{data?.map((festival) => {
return (
<FestivalSearchTile key={festival.festivalId} festival={festival} />
);
})}
</div>
);
};

export default SearchFestival;
49 changes: 49 additions & 0 deletions src/app/(home)/search/_components/SearchHistory/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { useQueryState } from "nuqs";

import { SearchHistoryChip } from "@/components/core/Chip";
import { useSearchHistory } from "@/hooks/useSearchHistory";
import { useSwipingContainer } from "@/hooks/useSwipingContainer";

const SearchHistory = () => {
const [query, setQuery] = useQueryState("query", { shallow: true });
const { containerRef, mouseDownHandler } = useSwipingContainer();
const { get, set, del, reset } = useSearchHistory();

if (query) {
return null;
}

return (
<div className="flex h-auto w-full flex-col gap-[16px] py-[20px]">
<div className="flex w-full justify-between">
<span className="text-subtitle-semi text-gray-scale-900">
최근 검색한 페스티벌
</span>
<button
type="button"
className="text-caption1-medium text-gray-scale-600"
onClick={reset}
>
전체 삭제
</button>
</div>
<div
ref={containerRef}
onMouseDown={mouseDownHandler}
className="flex min-h-[32px] w-full gap-[8px] overflow-hidden"
>
{get.map((history: string) => (
<SearchHistoryChip
key={history}
text={history}
onClick={() => del(history)}
/>
))}
</div>
</div>
);
};

export default SearchHistory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const TrendingFestivalSkeleton = () => {
return (
<div role="status" className="animate-pulse">
<div className="flex h-full w-full flex-col gap-[16px]">
<div className="h-[19px] w-[150px] rounded-md bg-gray-scale-200" />

<ul className="flex w-full flex-col bg-gray-scale-50 p-[12px]">
{Array.from({ length: 5 }, (_, idx) => (
<li
key={idx}
className="flex h-auto w-full items-center gap-[12px] rounded-[8px] py-[16px]"
>
<div className="h-[17px] w-full rounded-md bg-gray-300"></div>
</li>
))}
</ul>
</div>
<span className="sr-only">Loading...</span>
</div>
);
};

export default TrendingFestivalSkeleton;
60 changes: 60 additions & 0 deletions src/app/(home)/search/_components/TrendingFestival/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useSuspenseQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useQueryState } from "nuqs";

import { getTrendingFestival } from "@/apis/festivals/trendingFestival/trendingFestival";
import { TrendingFestivalKeys } from "@/apis/festivals/trendingFestival/trendingFestivalKeys";
import { cn } from "@/utils";

const TrendingFestival = () => {
const { data } = useSuspenseQuery({
queryKey: TrendingFestivalKeys.all,
queryFn: getTrendingFestival,
});

const [query, setQuery] = useQueryState("query", { shallow: true });

if (query) {
return null;
}

return (
<div className="flex h-full w-full flex-col gap-[16px]">
<div className="flex w-full justify-start gap-[4px]">
<span className="text-subtitle-semi text-gray-scale-900">
실시간 급상승 페스티벌
</span>
<span className="text-subtitle-semi text-primary-01">TOP 5</span>
</div>

<div className="flex w-full flex-col rounded-[8px] bg-gray-scale-50 p-[12px]">
{data.map((festival, idx, arr) => {
return (
<Link
href={`/festival/${festival.festivalId}`}
key={festival.festivalId}
className={cn(
"w-full",
idx + 1 !== arr.length &&
" border-b-[1px] border-gray-scale-200",
)}
>
<div className="flex h-auto w-full items-center gap-[12px] rounded-[8px] py-[16px]">
<div className="text-subtitle-semi text-primary-01">
{idx + 1}
</div>
<div className="line-clamp-1 max-w-[90%] text-ellipsis whitespace-nowrap text-body2-medium text-gray-scale-600">
{festival.name}
</div>
</div>
</Link>
);
})}
</div>
</div>
);
};

export default TrendingFestival;
16 changes: 16 additions & 0 deletions src/app/(home)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Suspense } from "react";

import { SearchHeader } from "@/layout/Mobile/MobileHeader";

import SearchView from "./view";

export default async function SearchPage() {
return (
<Suspense>
<div className="mb-[60px] mt-[44px]">
<SearchHeader />
<SearchView />
</div>
</Suspense>
);
}
22 changes: 22 additions & 0 deletions src/app/(home)/search/view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Suspense } from "react";

import SearchFestival from "./_components/SearchFestivalList";
import SearchHistory from "./_components/SearchHistory";
import TrendingFestival from "./_components/TrendingFestival";
import TrendingFestivalSkeleton from "./_components/TrendingFestival/TrendingFestivalSkeleton";

const SearchView = () => {
return (
<main className="mt-[60px] flex h-full w-full flex-col gap-4 px-[16px]">
<SearchHistory />

<Suspense fallback={<TrendingFestivalSkeleton />}>
<TrendingFestival />
</Suspense>

<SearchFestival />
</main>
);
};

export default SearchView;

0 comments on commit a8a649f

Please sign in to comment.