refactor: extract component
This commit is contained in:
parent
2aa123ef98
commit
9ca305de9e
89
src/client/components/menu/AddMenuItem.tsx
Normal file
89
src/client/components/menu/AddMenuItem.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAddMenuItem } from "@/hooks/useMenu";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { MenuItemColumns } from "src/db";
|
||||
import { MenuInput } from "./MenuInput.tsx";
|
||||
|
||||
export function AddMenuItem() {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isPending } = useAddMenuItem(queryClient);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [data, setData] = useState<MenuItemColumns>({
|
||||
name: "",
|
||||
category: "",
|
||||
HOT: null,
|
||||
COLD: null,
|
||||
price: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<div className="border p-4 hover:bg-accent flex items-center justify-center">
|
||||
<h2 className="text-xl font-bold">메뉴 추가</h2>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>메뉴 추가</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 추가합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="m-4">
|
||||
<div className="mb-2">
|
||||
<div className="text-muted-foreground">메뉴 이름</div>
|
||||
<Input
|
||||
value={data.name}
|
||||
onChange={(e) =>
|
||||
setData({
|
||||
...data,
|
||||
name: e.target.value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<MenuInput
|
||||
data={data}
|
||||
onChange={(c) => {
|
||||
setData({
|
||||
...data,
|
||||
...c,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
mutate(data, {
|
||||
onSuccess: () => {
|
||||
setData({
|
||||
name: "",
|
||||
category: "",
|
||||
HOT: null,
|
||||
COLD: null,
|
||||
price: null,
|
||||
});
|
||||
setDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
추가하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
63
src/client/components/menu/MenuInput.tsx
Normal file
63
src/client/components/menu/MenuInput.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UpdateMenuItemType } from "src/schema";
|
||||
import { onlyNumber } from "../../lib/onlyNumber.tsx";
|
||||
|
||||
export function MenuInput({
|
||||
data,
|
||||
onChange,
|
||||
className = "",
|
||||
}: {
|
||||
data: UpdateMenuItemType;
|
||||
onChange: (data: UpdateMenuItemType) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<div>
|
||||
<div className="text-muted-foreground">카테고리</div>
|
||||
<Input
|
||||
value={data.category}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
category: e.target.value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Hot 가격</div>
|
||||
<Input
|
||||
value={data.HOT ? (data.HOT * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
HOT: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Ice 가격</div>
|
||||
<Input
|
||||
value={data.COLD ? (data.COLD * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
COLD: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">그냥 가격</div>
|
||||
<Input
|
||||
value={data.price ? (data.price * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
price: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
111
src/client/components/menu/MenuItem.tsx
Normal file
111
src/client/components/menu/MenuItem.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { useDeleteMenuItem, useUpdateMenuItem } from "@/hooks/useMenu";
|
||||
import { TrashIcon } from "@radix-ui/react-icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { MenuItemColumns } from "src/db";
|
||||
import { UpdateMenuItemType } from "src/schema";
|
||||
import { currencyFormat } from "../../lib/currencyFormat.tsx";
|
||||
import { MenuInput } from "./MenuInput.tsx";
|
||||
|
||||
export function MenuItem({
|
||||
item,
|
||||
}: {
|
||||
item: MenuItemColumns;
|
||||
}) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [deleteDrawerOpen, setDeleteDrawerOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: mutateUpdate, isPending: isUpdatePending } = useUpdateMenuItem(queryClient);
|
||||
const { mutate: mutateDelete, isPending: isDeletePending } = useDeleteMenuItem(queryClient);
|
||||
const [data, setData] = useState<UpdateMenuItemType>({
|
||||
category: item.category,
|
||||
HOT: item.HOT,
|
||||
COLD: item.COLD,
|
||||
price: item.price,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer open={deleteDrawerOpen} onOpenChange={setDeleteDrawerOpen}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{item.name} 삭제</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 삭제합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isDeletePending}
|
||||
onClick={() => {
|
||||
mutateDelete(item.name, {
|
||||
onSuccess: () => {
|
||||
setDeleteDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
삭제하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<div className="border p-4 hover:bg-accent relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={isDeletePending}
|
||||
className="absolute top-4 right-2"
|
||||
onClick={(e) => {
|
||||
setDeleteDrawerOpen(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<span className="text-muted-foreground font-light text-sm leading-3">{item.category}</span>
|
||||
<h2 className="text-xl font-bold">{item.name}</h2>
|
||||
{item.HOT && <p className="text-sm text-muted-foreground">HOT {currencyFormat(item.HOT * 1000)}</p>}
|
||||
{item.COLD && <p className="text-sm text-muted-foreground">COLD {currencyFormat(item.COLD * 1000)}</p>}
|
||||
{item.price && <p className="text-sm text-muted-foreground">{currencyFormat(item.price * 1000)}</p>}
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{item.name} 수정</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 수정합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<MenuInput className="m-4" data={data} onChange={setData} />
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isUpdatePending}
|
||||
onClick={() => {
|
||||
mutateUpdate({
|
||||
...item,
|
||||
...data,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
수정하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
6
src/client/lib/currencyFormat.tsx
Normal file
6
src/client/lib/currencyFormat.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
export function currencyFormat(price: number) {
|
||||
return price.toLocaleString("ko-KR", {
|
||||
style: "currency",
|
||||
currency: "KRW",
|
||||
});
|
||||
}
|
3
src/client/lib/onlyNumber.tsx
Normal file
3
src/client/lib/onlyNumber.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function onlyNumber(str: string) {
|
||||
return str.replace(/[^0-9]/g, "");
|
||||
}
|
@ -1,260 +1,9 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAddMenuItem, useDeleteMenuItem, useMenu, useUpdateMenuItem } from "@/hooks/useMenu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TrashIcon } from "@radix-ui/react-icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { MenuItemColumns } from "src/db";
|
||||
import { UpdateMenuItemType } from "src/schema";
|
||||
import { useMenu } from "@/hooks/useMenu";
|
||||
import { AddMenuItem } from "../components/menu/AddMenuItem";
|
||||
import { MenuItem } from "../components/menu/MenuItem";
|
||||
import ErrorMessage from "./ErrorPage";
|
||||
import LoadingPage from "./Loading";
|
||||
|
||||
function currencyFormat(price: number) {
|
||||
return price.toLocaleString("ko-KR", {
|
||||
style: "currency",
|
||||
currency: "KRW",
|
||||
});
|
||||
}
|
||||
|
||||
function onlyNumber(str: string) {
|
||||
return str.replace(/[^0-9]/g, "");
|
||||
}
|
||||
|
||||
function MenuInput({
|
||||
data,
|
||||
onChange,
|
||||
className = "",
|
||||
}: {
|
||||
data: UpdateMenuItemType;
|
||||
onChange: (data: UpdateMenuItemType) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<div>
|
||||
<div className="text-muted-foreground">카테고리</div>
|
||||
<Input
|
||||
value={data.category}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
category: e.target.value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Hot 가격</div>
|
||||
<Input
|
||||
value={data.HOT ? (data.HOT * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
HOT: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Ice 가격</div>
|
||||
<Input
|
||||
value={data.COLD ? (data.COLD * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
COLD: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">그냥 가격</div>
|
||||
<Input
|
||||
value={data.price ? (data.price * 1000).toString() : ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...data,
|
||||
price: e.target.value ? parseInt(onlyNumber(e.target.value)) / 1000 : null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({
|
||||
item,
|
||||
}: {
|
||||
item: MenuItemColumns;
|
||||
}) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [deleteDrawerOpen, setDeleteDrawerOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: mutateUpdate, isPending: isUpdatePending } = useUpdateMenuItem(queryClient);
|
||||
const { mutate: mutateDelete, isPending: isDeletePending } = useDeleteMenuItem(queryClient);
|
||||
const [data, setData] = useState<UpdateMenuItemType>({
|
||||
category: item.category,
|
||||
HOT: item.HOT,
|
||||
COLD: item.COLD,
|
||||
price: item.price,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer open={deleteDrawerOpen} onOpenChange={setDeleteDrawerOpen}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{item.name} 삭제</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 삭제합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isDeletePending}
|
||||
onClick={() => {
|
||||
mutateDelete(item.name, {
|
||||
onSuccess: () => {
|
||||
setDeleteDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
삭제하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<div className="border p-4 hover:bg-accent relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={isDeletePending}
|
||||
className="absolute top-4 right-2"
|
||||
onClick={(e) => {
|
||||
setDeleteDrawerOpen(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<span className="text-muted-foreground font-light text-sm leading-3">{item.category}</span>
|
||||
<h2 className="text-xl font-bold">{item.name}</h2>
|
||||
{item.HOT && <p className="text-sm text-muted-foreground">HOT {currencyFormat(item.HOT * 1000)}</p>}
|
||||
{item.COLD && <p className="text-sm text-muted-foreground">COLD {currencyFormat(item.COLD * 1000)}</p>}
|
||||
{item.price && <p className="text-sm text-muted-foreground">{currencyFormat(item.price * 1000)}</p>}
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{item.name} 수정</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 수정합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<MenuInput className="m-4" data={data} onChange={setData} />
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isUpdatePending}
|
||||
onClick={() => {
|
||||
mutateUpdate({
|
||||
...item,
|
||||
...data,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
수정하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AddMenuItem() {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isPending } = useAddMenuItem(queryClient);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [data, setData] = useState<MenuItemColumns>({
|
||||
name: "",
|
||||
category: "",
|
||||
HOT: null,
|
||||
COLD: null,
|
||||
price: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<div className="border p-4 hover:bg-accent flex items-center justify-center">
|
||||
<h2 className="text-xl font-bold">메뉴 추가</h2>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>메뉴 추가</DrawerTitle>
|
||||
<DrawerDescription>메뉴를 추가합니다.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="m-4">
|
||||
<div className="mb-2">
|
||||
<div className="text-muted-foreground">메뉴 이름</div>
|
||||
<Input
|
||||
value={data.name}
|
||||
onChange={(e) =>
|
||||
setData({
|
||||
...data,
|
||||
name: e.target.value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<MenuInput
|
||||
data={data}
|
||||
onChange={(c) => {
|
||||
setData({
|
||||
...data,
|
||||
...c,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
mutate(data, {
|
||||
onSuccess: () => {
|
||||
setData({
|
||||
name: "",
|
||||
category: "",
|
||||
HOT: null,
|
||||
COLD: null,
|
||||
price: null,
|
||||
});
|
||||
setDrawerOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
추가하기
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MenuPage() {
|
||||
const { data: menuItems, status } = useMenu();
|
||||
if (status === "pending") {
|
||||
|
Loading…
Reference in New Issue
Block a user