Skip to content

Commit

Permalink
feat: 내폴더 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
developerQuo committed Dec 3, 2024
2 parents b15c431 + 1836c2d commit 899b169
Show file tree
Hide file tree
Showing 62 changed files with 4,097 additions and 365 deletions.
5 changes: 3 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": [
"next/core-web-vitals",
"plugin:storybook/recommended"
"plugin:storybook/recommended",
"prettier"
]
}
}
4 changes: 2 additions & 2 deletions app/api/banners/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fetchToServer } from "@/utils/api";
import { serverApi } from "@/utils/api";

export async function GET() {
return fetchToServer({
return serverApi({
path: "api/banners",
});
}
4 changes: 2 additions & 2 deletions app/api/links/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { fetchToServer } from "@/utils/api";
import { serverApi } from "@/utils/api";
import { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const queryString = `sort=${searchParams.get("sort")}&order=${searchParams.get("order")}`;

return fetchToServer({
return serverApi({
path: "api/links",
queryString,
});
Expand Down
5 changes: 5 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ body {
text-wrap: balance;
}
}
@layer components {
.sidebar-menu-scroll {
@apply scrollbar-thin scrollbar-thumb-gray-300 hover:scrollbar-thumb-gray-400 overflow-auto [&::-webkit-scrollbar-thumb]:rounded-full;
}
}
10 changes: 8 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { usePathname } from "next/navigation";
import { publicOnlyPaths } from "@/utils/path";
import Topbar from "@/components/layout/topbar";
import MutateFolderDialog from "./my-folder/mutate/dialog";
import DeleteFolderDialog from "./my-folder/delete-dialog";

const queryClient = new QueryClient();

const pretendard = localFont({
src: "../public/fonts/PretendardVariable.woff2",
Expand All @@ -20,7 +24,6 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const queryClient = new QueryClient();
const pathname = usePathname();
const isPublicOnlyPath = publicOnlyPaths.includes(pathname);

Expand All @@ -43,6 +46,9 @@ export default function RootLayout({
<Component>
<Topbar />
{children}
<div id="modal-root" />
<MutateFolderDialog />
<DeleteFolderDialog />
</Component>
</Sidebar>
</QueryClientProvider>
Expand All @@ -58,7 +64,7 @@ function Component({
children: React.ReactNode;
}>) {
return (
<main className="flex min-h-screen w-full flex-col items-center justify-center">
<main className="relative flex h-screen w-full flex-col items-center">
{children}
</main>
);
Expand Down
25 changes: 25 additions & 0 deletions app/my-folder/api/[linkBookId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { serverApi } from "@/utils/api";
import { NextRequest } from "next/server";

export async function PUT(
request: NextRequest,
context: { params: { linkBookId: string } },
) {
const body = await request.json();
const linkBookId = context.params.linkBookId;

return serverApi({
path: `api/link-books/${linkBookId}`,
method: "PUT",
body,
});
}

export async function DELETE(
request: NextRequest,
context: { params: { linkBookId: string } },
) {
const linkBookId = context.params.linkBookId;

return serverApi({ path: `api/link-books/${linkBookId}`, method: "DELETE" });
}
16 changes: 13 additions & 3 deletions app/my-folder/api/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { fetchToServer } from "@/utils/api";
import { serverApi } from "@/utils/api";
import { NextRequest } from "next/server";

export async function GET() {
return fetchToServer({ path: "api/link-books" });
export async function GET(request: NextRequest) {
const sort = request.nextUrl.searchParams.get("sort");
const queryString = sort && `sort=${sort}`;

return serverApi({ path: "api/link-books", queryString });
}

export async function POST(request: NextRequest) {
const body = await request.json();

return serverApi({ path: "api/link-books", method: "POST", body });
}
85 changes: 85 additions & 0 deletions app/my-folder/delete-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";
import { useSelectLinkBookStore } from "@/store/useLinkBookStore";
import clsx from "clsx";
import { useCallback } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import Dialog from "@/components/dialog";
import { useOpenDialogStore } from "@/store/useDialogStore";

export default function DeleteFolderDialog() {
const { deleteFolder: isOpen, openDeleteFolder: open } = useOpenDialogStore();
const { linkBook, selectLinkBook } = useSelectLinkBookStore();

const onClose = useCallback(() => {
if (linkBook) {
selectLinkBook(undefined);
}
open(false);
}, [open, linkBook, selectLinkBook]);

const queryClient = useQueryClient();
const remove = useMutation<{ deletedLinks: number }, Error>({
mutationFn: async () =>
(
await fetch(`my-folder/api/${linkBook?.linkBookId}`, {
method: "DELETE",
})
).json(),
onSuccess: () => {
queryClient.setQueriesData<TQueryLinkBooks>(
{ queryKey: ["linkBooks"] },
(prevLinkBooks) => {
const index = prevLinkBooks!.linkBooks.findIndex(
(prev) => prev.linkBookId === linkBook?.linkBookId,
);
return {
linkBooks: [
...prevLinkBooks!.linkBooks.slice(0, index),
...prevLinkBooks!.linkBooks.slice(index + 1),
],
totalLinkCount: prevLinkBooks!.totalLinkCount - 1,
};
},
);
onClose();
},
});
async function handleSubmit() {
remove.mutate();
}

if (!isOpen) return null;

if (!linkBook?.linkBookId) {
alert("해당 폴더를 찾을 수 없습니다.");
return null;
}

return (
<Dialog open={isOpen} onCloseCallback={onClose} className="w-[421.78px]">
<div className="flex flex-col items-center gap-5">
<div className="text-center text-[#2F2F2F]">
<p>폴더 내의 모든 링크가 삭제됩니다.</p>
<p>폴더를 삭제하시겠습니까?</p>
</div>
<div className="mt-3 flex justify-center gap-1">
<button
className="h-[56px] w-[164.89px] rounded-lg bg-[#BBBBBB] font-bold text-white"
onClick={onClose}
>
취소
</button>
<button
className={clsx(
"h-[56px] w-[164.89px] rounded-lg font-bold text-white",
"bg-primary",
)}
onClick={handleSubmit}
>
삭제
</button>
</div>
</div>
</Dialog>
);
}
73 changes: 73 additions & 0 deletions app/my-folder/dropdown/more.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useClearDropdown } from "@/hooks/clear-dropdown";
import { useOpenDialogStore } from "@/store/useDialogStore";
import Image from "next/image";
import { useState } from "react";
import { LinkBook } from "../type";
import { useSelectLinkBookStore } from "@/store/useLinkBookStore";

type DropdownItemProps = {
title: string;
handleClick: () => void;
};

const DropdownItem = ({ title, handleClick }: DropdownItemProps) => {
return (
<button
onClick={handleClick}
className="w-full px-5 py-1 font-semibold text-[#1D1D1D]"
>
{title}
</button>
);
};

type InputProps = {
linkBook: LinkBook;
};

const DropdownMore = ({ linkBook }: InputProps) => {
const [isOpen, setIsOpen] = useState(false);

const onClose = () => setIsOpen(false);

const ref = useClearDropdown(onClose);

const { openMutateFolder, openDeleteFolder } = useOpenDialogStore();
const { selectLinkBook } = useSelectLinkBookStore();

const handleModify = () => {
selectLinkBook(linkBook);
openMutateFolder(true);
onClose();
};
const handleDelete = () => {
selectLinkBook(linkBook);
openDeleteFolder(true);
onClose();
};

return (
<div className="relative" data-testid="link-book-more" ref={ref}>
<button
onClick={() => setIsOpen(true)}
className="flex h-12 w-12 items-center justify-center rounded-full bg-black"
>
<Image
src="/icons/icon-more-vertical.png"
alt="more"
width={26.4}
height={26.4}
/>
</button>

{isOpen && (
<div className="absolute z-10 mt-1 flex w-[110px] flex-col rounded-lg border border-background-secondary bg-white py-4 shadow-lg">
<DropdownItem title="폴더 수정" handleClick={handleModify} />
<DropdownItem title="폴더 삭제" handleClick={handleDelete} />
</div>
)}
</div>
);
};

export default DropdownMore;
61 changes: 61 additions & 0 deletions app/my-folder/dropdown/sort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";
import { useClearDropdown } from "@/hooks/clear-dropdown";
import clsx from "clsx";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";

export const sortOptions = [
{ label: "생성순", value: "created_at" },
{ label: "제목순", value: "title" },
{ label: "업데이트순", value: "last_saved_at" },
];

type InputProps = {
selected: string;
setSelected: (option: string) => void;
};

const DropdownSort = ({ selected, setSelected }: InputProps) => {
const [isOpen, setIsOpen] = useState(false);

const ref = useClearDropdown(() => setIsOpen(false));

const selectedOption = sortOptions.find(({ value }) => selected === value);
return (
<div className="relative" data-testid="link-book-list-sort" ref={ref}>
<button
data-testid="open-button"
onClick={() => setIsOpen(!isOpen)}
className="flex h-[24px] items-center p-1 font-semibold text-text-secondary"
>
<div>{selectedOption?.label}</div>
<Image src="/icons/icon-down2.png" alt="down" width={24} height={24} />
</button>

{isOpen && (
<div className="absolute right-0 z-10 mt-1 flex min-w-32 flex-col rounded-lg border border-background-secondary bg-white py-4 shadow-lg">
{sortOptions.map((item) => (
<button
data-testid={`dropdown-${item.label}`}
key={item.value}
onClick={() => {
setSelected(item.value);
setIsOpen(false);
}}
className={clsx(
"w-full px-5 py-1 text-start",
selected === item.value
? "font-bold text-[#1D1D1D]"
: "text-text-secondary",
)}
>
{item.label}
</button>
))}
</div>
)}
</div>
);
};

export default DropdownSort;
Loading

0 comments on commit 899b169

Please sign in to comment.