Skip to content

Commit

Permalink
[theme-market] 검색/ 베스트 리스트 뷰 추가 (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
ars-ki-00 authored Jan 31, 2025
1 parent ce771c7 commit 0e8b098
Show file tree
Hide file tree
Showing 33 changed files with 436 additions and 74 deletions.
1 change: 1 addition & 0 deletions apps/theme-market/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"react-intersection-observer": "^9.15.1",
"zustand": "^5.0.1"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { BottomSheet } from "@/app/_components/BottomSheet";
import { ThemeDetail } from "@/app/_pages/Main/BottomSheet/ThemeDetail";
import { ThemeDetail } from "@/app/_pages/BottomSheet/ThemeDetail";
import { useThemeStore } from "@/app/_providers/ThemeProvider";
import { useUserStore } from "@/app/_providers/UserProvider";
import { themeService } from "@/services/ThemeService";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BestPage } from "@/app/_pages/Main/ThemeDownload/Best";

export default async function DownloadBestPageRoute() {
return <BestPage />;
}
10 changes: 10 additions & 0 deletions apps/theme-market/src/app/(routes)/(main)/search/[query]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SearchPage } from "@/app/_pages/Search";

interface Props {
params: Promise<{ query: string }>;
}

export default async function SearchPageRoute({ params }: Props) {
const query = decodeURIComponent((await params).query);
return <SearchPage query={query} />;
}
1 change: 0 additions & 1 deletion apps/theme-market/src/app/_components/Error/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Image from "next/image";
import styles from "./index.module.css";

import SvgCat from "@/assets/icons/svgCat.svg";
import { ReactNode } from "react";

interface Props {
message?: string;
Expand Down
27 changes: 23 additions & 4 deletions apps/theme-market/src/app/_components/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { InputHTMLAttributes } from "react";
"use client";

import { InputHTMLAttributes, useState } from "react";
import styles from "./index.module.css";
import classNames from "classnames";

interface Props extends InputHTMLAttributes<HTMLInputElement> {}
interface Props extends InputHTMLAttributes<HTMLInputElement> {
onComplete?: (value: string) => void;
}

export const Input = ({
onComplete,
className,
defaultValue,
...props
}: Props) => {
const [value, setValue] = useState<string>((defaultValue || "").toString());

export const Input = (props: Props) => {
return (
<input {...props} className={classNames(props.className, styles.input)} />
<input
{...props}
className={classNames(className, styles.input)}
value={value}
onKeyDown={(e) => {
if (e.key === "Enter") onComplete?.(value);
}}
onChange={(e) => setValue(e.target.value)}
/>
);
};
19 changes: 19 additions & 0 deletions apps/theme-market/src/app/_components/Theme/List/ThemeInfoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { ThemeInfo } from "@/app/_components/Theme/Info";
import { Theme } from "@/entities/Theme";

import styles from "./index.module.css";

interface Props {
themes: Theme[];
}
export const ThemeInfoList = ({ themes }: Props) => {
return (
<div className={styles.wrapper}>
{themes.map((theme) => {
return <ThemeInfo key={theme.id} theme={theme} />;
})}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";

import { ThemeInfo } from "@/app/_components/Theme/Info";
import { useUserStore } from "@/app/_providers/UserProvider";
import { themeService } from "@/services/ThemeService";
import { Theme } from "@/entities/Theme";

import styles from "./index.module.css";

interface Props {
defaultThemes: Theme[];
}

const DEFAULT_PAGE = 2;

export const ThemeListWithInifiniteScorll = ({ defaultThemes }: Props) => {
const [themes, setThemes] = useState<Theme[]>(defaultThemes);
const [page, setPage] = useState<number>(DEFAULT_PAGE);

const { ref, inView } = useInView();

const { accessToken } = useUserStore((state) => state);

const loadThemes = useCallback(async () => {
const { content: themes } = await themeService.getBestThemes(
page,
accessToken
);

setThemes((prev) => [...prev, ...themes]);
setPage((prev) => prev + 1);
}, [accessToken, page]);

useEffect(() => {
if (inView) loadThemes();
}, [inView, loadThemes]);

return (
<>
<div className={styles.wrapper}>
{themes.map((theme) => {
return <ThemeInfo key={theme.id} theme={theme} />;
})}
</div>
<div ref={ref} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 10px;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import { Theme } from "@/entities/Theme";
import styles from "./index.module.css";
import { Preview } from "./Preview";
import classNames from "classnames";

import { Theme } from "@/entities/Theme";
import { Preview } from "./Preview";
import styles from "./index.module.css";
interface Props {
theme: Theme;
isAnonymous: boolean;
Expand Down
45 changes: 45 additions & 0 deletions apps/theme-market/src/app/_pages/Main/Header/MainHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";

import { MENU } from "@/entities/Menu";

import { TabContent } from "@/app/_components/Tab/TabContent";
import { Tab } from "@/app/_components/Tab";
import { Input } from "@/app/_components/Input";

import styles from "./index.module.css";

interface Props {
menu: MENU;
}

export const MainHeader = ({ menu }: Props) => {
const router = useRouter();

return (
<section className={styles.header}>
<div className={styles.inputWrapper}>
<Input
className={styles.input}
placeholder="테마를 검색해보세요"
type="search"
onComplete={(value: String) => router.push(`/search/${value}`)}
/>
</div>
<Tab>
<TabContent selected={menu === "DOWNLOAD"}>
<Link href="/download" replace>
테마 다운로드
</Link>
</TabContent>
<TabContent selected={menu === "MY_THEME"}>
<Link href="/my" replace>
내 테마 올리기
</Link>
</TabContent>
</Tab>
</section>
);
};
21 changes: 21 additions & 0 deletions apps/theme-market/src/app/_pages/Main/Header/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.header {
background-color: var(--color-bg-default);
}

.inputWrapper {
width: 100%;
height: 80px;

padding: 20px 18.5px;
}

.input {
width: 100%;
height: 40px;

padding-left: 40px;

background-image: url("/static/icons/svgSearch.svg");
background-position: 7px 7px;
background-repeat: no-repeat;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.main {
padding: 17px 18px 0;
}

.prev {
padding-left: 3px;

display: flex;
flex-direction: row;

height: 14px;
font-size: 13px;
font-weight: 400;
line-height: 12.6px;

color: var(--palette-gray-2);
}

.themes {
margin-top: 12px;

display: flex;
flex-direction: column;
gap: 10px;
}
34 changes: 34 additions & 0 deletions apps/theme-market/src/app/_pages/Main/ThemeDownload/Best/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Image from "next/image";

import { cookieService } from "@/services/CookieService";
import { MainHeader } from "@/app/_pages/Main/Header/MainHeader";
import { themeService } from "@/services/ThemeService";
import SvgChevronLeft from "@/assets/icons/svgChevronLeft.svg";

import styles from "./index.module.css";
import { DEFAULT_PAGE } from "@/repositories/ThemeRepository";
import { ThemeListWithInifiniteScorll } from "@/app/_components/Theme/List/ThemeListWithInfiniteScroll";
import Link from "next/link";

export const BestPage = async () => {
const accessToken = cookieService.getAccessToken();
const { content: themes } = await themeService.getBestThemes(
DEFAULT_PAGE,
accessToken
);

return (
<>
<MainHeader menu="DOWNLOAD" />
<section className={styles.main}>
<Link className={styles.prev} href="/download">
<Image src={SvgChevronLeft} alt="<" width="16" height="16.6" />
<span>돌아가기</span>
</Link>
<div className={styles.themes}>
<ThemeListWithInifiniteScorll defaultThemes={themes} />
</div>
</section>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Image from "next/image";
import styles from "./index.module.css";
import { ThemeInfo } from "./ThemeInfo";
import Link from "next/link";

import SvgChevronLeft from "@/assets/icons/svgChevronLeft.svg";
import { NotFound } from "@/app/_components/Error/NotFound";
import { ThemeInfoList } from "@/app/_components/Theme/List/ThemeInfoList";

import { Theme } from "@/entities/Theme";

import SvgChevronRight from "@/assets/icons/svgChevronRight.svg";

interface Props {
title: string;
themes: Theme[];
Expand All @@ -20,14 +23,12 @@ export const ThemeList = ({ title, themes }: Props) => {
{themeExists ? (
<>
<div className={styles.themeList}>
{themes.map((theme) => {
return <ThemeInfo key={theme.id} theme={theme} />;
})}
<ThemeInfoList themes={themes} />
</div>
<div className={styles.more}>
<Link className={styles.more} href="/download/best">
<span>전체 보기</span>
<Image src={SvgChevronLeft} alt=">" />
</div>
<Image src={SvgChevronRight} alt=">" />
</Link>
</>
) : (
<div className={styles.notFound}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import { themeService } from "@/services/ThemeService";

import { ThemeList } from "./ThemeList";
import { cookieService } from "@/services/CookieService";
import { DEFAULT_PAGE } from "@/repositories/ThemeRepository";

export const ThemeDownload = async () => {
const accessToken = cookieService.getAccessToken();

const bestThemes = await themeService.getBestThemes(accessToken);
const { content: bestThemes } = await themeService.getBestThemes(
DEFAULT_PAGE,
accessToken
);
const friendsThemes = await themeService.getFriendsThemes(accessToken);

return (
Expand Down
24 changes: 1 addition & 23 deletions apps/theme-market/src/app/_pages/Main/index.module.css
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
.header {
background-color: var(--color-bg-default);
}

.inputWrapper {
width: 100%;
height: 80px;

padding: 20px 18.5px;
}

.input {
width: 100%;
height: 40px;

padding-left: 40px;

background-image: url("/static/icons/svgSearch.svg");
background-position: 7px 7px;
background-repeat: no-repeat;
}

.main {
padding: 17px 18px 0;
padding: 21px 18px 0;
}
Loading

0 comments on commit 0e8b098

Please sign in to comment.