init
This commit is contained in:
parent
50a62794ed
commit
550865f643
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,3 +34,5 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
order.sqlite3
|
@ -1,5 +1,3 @@
|
|||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
First, run the development server:
|
First, run the development server:
|
||||||
@ -28,9 +26,3 @@ To learn more about Next.js, take a look at the following resources:
|
|||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
|
@ -1,33 +1,76 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
@layer base {
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-start-rgb: 214, 219, 220;
|
|
||||||
--background-end-rgb: 255, 255, 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
:root {
|
||||||
--foreground-rgb: 255, 255, 255;
|
--background: 0 0% 100%;
|
||||||
--background-start-rgb: 0, 0, 0;
|
--foreground: 240 10% 3.9%;
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
--card: 0 0% 100%;
|
||||||
color: rgb(var(--foreground-rgb));
|
--card-foreground: 240 10% 3.9%;
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
rgb(var(--background-end-rgb))
|
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
--popover: 0 0% 100%;
|
||||||
.text-balance {
|
--popover-foreground: 240 10% 3.9%;
|
||||||
text-wrap: balance;
|
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter, Noto_Sans_KR } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const NotoSansKR = Noto_Sans_KR({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-sans",
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
@ -15,8 +19,9 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="ko">
|
||||||
<body className={inter.className}>{children}</body>
|
<head />
|
||||||
|
<body className={cn("min-h-dvh bg-background font-sans antialiased", NotoSansKR.variable)}>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
31
app/order/action.ts
Normal file
31
app/order/action.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { Order } from "@/hooks/useOrder";
|
||||||
|
import { saveOrder, Payment, cancelOrder, completeOrder } from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export async function saveOrderApi(orders: Order[], payment: Payment){
|
||||||
|
console.log(orders, "주문");
|
||||||
|
|
||||||
|
// db에 주문 저장
|
||||||
|
const id = await saveOrder(orders, payment);
|
||||||
|
// cache revaildation
|
||||||
|
revalidatePath("/", "page");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
completed : false,
|
||||||
|
orders: orders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelOrderApi(uid: string){
|
||||||
|
await cancelOrder(uid);
|
||||||
|
revalidatePath("/", "page");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function completeOrderApi(uid: string, completed: boolean = true){
|
||||||
|
await completeOrder(uid, completed);
|
||||||
|
revalidatePath("/", "page");
|
||||||
|
revalidatePath("/stat", "page");
|
||||||
|
}
|
20
app/order/page.tsx
Normal file
20
app/order/page.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
|
||||||
|
import { readMenu } from "../../lib/readCsv";
|
||||||
|
import OrderComponent from "@/components/order";
|
||||||
|
import NavMenu from "@/components/NavMenu";
|
||||||
|
|
||||||
|
export default async function Order() {
|
||||||
|
//read csv file
|
||||||
|
const menu = await readMenu();
|
||||||
|
const categories = [...(new Set(menu.map(item => item.category)))];
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<NavMenu />
|
||||||
|
<div className="p-4">
|
||||||
|
<OrderComponent menus={menu} categories={categories} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
123
app/page.tsx
123
app/page.tsx
@ -1,113 +1,22 @@
|
|||||||
import Image from "next/image";
|
import OrderCard from "@/components/orderCard";
|
||||||
|
import { loadOrder } from "@/lib/db";
|
||||||
|
import NavMenu from "@/components/NavMenu";
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
|
const orders = await loadOrder();
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="min-h-screen">
|
||||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
<NavMenu />
|
||||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
{orders.length === 0 && (
|
||||||
Get started by editing
|
<div className="flex items-center justify-center h-[calc(100vh-4rem)]">
|
||||||
<code className="font-mono font-bold">app/page.tsx</code>
|
<p className="text-muted-foreground text-2xl">주문이 없습니다.</p>
|
||||||
</p>
|
|
||||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
|
|
||||||
<a
|
|
||||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{" "}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
|
|
||||||
<Image
|
|
||||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Docs{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
||||||
Find in-depth information about Next.js features and API.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Learn{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
||||||
Learn about Next.js in an interactive course with quizzes!
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Templates{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
||||||
Explore starter templates for Next.js.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Deploy{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
|
|
||||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<section className="grid grid-cols-[repeat(auto-fill,_minmax(300px,_auto))] p-4 gap-2">
|
||||||
|
{orders.map((order) => (
|
||||||
|
<OrderCard key={order.id} order={order} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
65
app/stat/page.tsx
Normal file
65
app/stat/page.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import NavMenu from "@/components/NavMenu";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Order } from "@/hooks/useOrder";
|
||||||
|
import { loadOrder } from "@/lib/db";
|
||||||
|
|
||||||
|
function StatItem({title, value}: {title: string, value: string}) {
|
||||||
|
return <div className="flex flex-col">
|
||||||
|
<p className="text-muted-foreground text-sm">{title}</p>
|
||||||
|
<p className="text-lg">{value}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function sum (...args: number[]) {
|
||||||
|
return args.reduce((acc, cur) => acc + cur, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ordersSum(orders: Order[]){
|
||||||
|
return orders.reduce((acc, cur) => acc + cur.price * cur.quantity * 1000, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Stat() {
|
||||||
|
const orders = await loadOrder();
|
||||||
|
const completed_order= orders.filter(order => order.completed === 1);
|
||||||
|
const cash_order = completed_order.filter(order => order.payment === "cash");
|
||||||
|
const account_order = completed_order.filter(order => order.payment === "account");
|
||||||
|
return <div className="">
|
||||||
|
<NavMenu />
|
||||||
|
<div className="p-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>주문통계</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2">
|
||||||
|
<StatItem title="총 주문 수" value={orders.length.toString()} />
|
||||||
|
<StatItem title="완료된 주문 수" value={completed_order.length.toString()} />
|
||||||
|
<StatItem title="현금 결제 주문 수" value={cash_order.length.toString()} />
|
||||||
|
<StatItem title="계좌 이체 주문 수" value={account_order.length.toString()} />
|
||||||
|
</div>
|
||||||
|
<hr className="my-2" />
|
||||||
|
<div className="grid grid-cols-2">
|
||||||
|
<StatItem title="총 매출" value={sum(...completed_order.map(order=> ordersSum(order.orders)))
|
||||||
|
.toLocaleString("ko-KR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "KRW",
|
||||||
|
})
|
||||||
|
} />
|
||||||
|
<StatItem title="현금 매출" value={sum(...cash_order.map(order=> ordersSum(order.orders)))
|
||||||
|
.toLocaleString("ko-KR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "KRW",
|
||||||
|
})
|
||||||
|
} />
|
||||||
|
<StatItem title="계좌 매출" value={sum(...account_order.map(order=> ordersSum(order.orders)))
|
||||||
|
.toLocaleString("ko-KR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "KRW",
|
||||||
|
})
|
||||||
|
} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
37
components/NavMenu.tsx
Normal file
37
components/NavMenu.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
navigationMenuTriggerStyle
|
||||||
|
} from "./ui/navigation-menu";
|
||||||
|
|
||||||
|
|
||||||
|
export default function NavMenu() {
|
||||||
|
return <header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="container flex h-14 max-w-screen-xl">
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<Link href="/" legacyBehavior passHref>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>홈</NavigationMenuLink>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<Link href="/order" legacyBehavior passHref>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>주문</NavigationMenuLink>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<Link href="/stat" legacyBehavior passHref>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>통계</NavigationMenuLink>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
}
|
78
components/menu.tsx
Normal file
78
components/menu.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { MenuItem } from "@/lib/readCsv";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
|
import { Dispatch } from "react";
|
||||||
|
import { OrderAction } from "@/hooks/useOrder";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
export default function Menu({
|
||||||
|
menus,
|
||||||
|
categories,
|
||||||
|
dispatch
|
||||||
|
}: {
|
||||||
|
menus: MenuItem[];
|
||||||
|
categories: string[];
|
||||||
|
dispatch: Dispatch<OrderAction>
|
||||||
|
}) {
|
||||||
|
const menu = menus;
|
||||||
|
return <Tabs defaultValue={categories[0]}>
|
||||||
|
<TabsList>
|
||||||
|
{[...categories].map(category => (
|
||||||
|
<TabsTrigger key={category} value={category}>{category}</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{
|
||||||
|
[...categories].map(category => (
|
||||||
|
<TabsContent key={category} value={category}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{category}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
|
||||||
|
<ul className="w-full flex flex-col">
|
||||||
|
<li className="flex gap-2">
|
||||||
|
<span className="w-80 text-lg">Name</span>
|
||||||
|
<span className="w-12 text-lg">HOT</span>
|
||||||
|
<span className="w-12 text-lg">COLD</span>
|
||||||
|
</li>
|
||||||
|
<hr className=""/>
|
||||||
|
{menu.filter(item => item.category === category).map((item,index) => (
|
||||||
|
<li key={item.name} className={cn("flex h-10 items-center gap-2", index % 2 === 0 ? "bg-accent" : "")}>
|
||||||
|
<span className="w-80">{item.name}</span>
|
||||||
|
<span className="w-12">{item.HOT}</span>
|
||||||
|
<span className="w-12">{item.COLD}</span>
|
||||||
|
<Button variant="outline" className="bg-sky-400 hover:bg-sky-500"
|
||||||
|
disabled={!item.COLD}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: "ADD_ORDER", order: {
|
||||||
|
name: item.name,
|
||||||
|
HOT: false,
|
||||||
|
price: item.COLD ?? 0,
|
||||||
|
quantity: 1,
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
>ICE Order</Button>
|
||||||
|
<Button variant="outline" className="bg-red-400 hover:bg-red-500"
|
||||||
|
disabled={!item.HOT}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: "ADD_ORDER", order: {
|
||||||
|
name: item.name,
|
||||||
|
HOT: true,
|
||||||
|
price: item.HOT ?? 0,
|
||||||
|
quantity: 1,
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
>HOT Order</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Tabs>
|
||||||
|
}
|
95
components/order.tsx
Normal file
95
components/order.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { MenuItem } from "@/lib/readCsv";
|
||||||
|
import { useOrder } from "../hooks/useOrder";
|
||||||
|
import Menu from "./menu";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "./ui/drawer";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { saveOrderApi } from "@/app/order/action";
|
||||||
|
|
||||||
|
export default function Order({
|
||||||
|
menus,
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
|
||||||
|
menus: MenuItem[];
|
||||||
|
categories: string[];
|
||||||
|
}) {
|
||||||
|
const { state, dispatch } = useOrder();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu menus={menus} categories={categories} dispatch={dispatch} />
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button className="w-full mt-2 flex items-center">Order <span className="
|
||||||
|
ml-1 flex items-center justify-center rounded-full h-6 px-1 text-center bg-background text-foreground">{state.orders.reduce(
|
||||||
|
(acc, order) => acc + order.quantity, 0)}</span></Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>다음 주문이 맞나요?</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<hr className="mx-4"/>
|
||||||
|
<ul className="flex flex-col gap-2 justify-center m-4">
|
||||||
|
{state.orders.map(order => (
|
||||||
|
<li key={`${order.HOT ? "HOT" : "COLD"} ${order.name}`} className="flex items-center">
|
||||||
|
<span className="w-12">{order.HOT ? "HOT " : "COLD"}</span>
|
||||||
|
<span className="flex-1">{order.name}</span>
|
||||||
|
<span className="flex-1">단위 가격{order.price * 1000}원</span>
|
||||||
|
<span className="flex-1">{order.quantity}개</span>
|
||||||
|
<span className="flex-1">총 가격 {order.price * order.quantity * 1000}원</span>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button variant="outline"
|
||||||
|
className="rounded-full"
|
||||||
|
onClick={() => dispatch({ type: "ADD_ORDER",
|
||||||
|
order:{
|
||||||
|
...order,
|
||||||
|
quantity: 1
|
||||||
|
}})} >+1</Button>
|
||||||
|
<Button variant="outline"
|
||||||
|
className="rounded-full"
|
||||||
|
onClick={() => dispatch({ type: "ADD_ORDER",
|
||||||
|
order: {
|
||||||
|
...order,
|
||||||
|
quantity: -1
|
||||||
|
}})}>-1</Button>
|
||||||
|
<Button variant="outline" onClick={() => dispatch({ type: "REMOVE_ORDER", order })}>취소</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<hr className="mx-4" />
|
||||||
|
<div className="flex mx-4 flex-col items-end">
|
||||||
|
<span className="text-sm text-muted-foreground">총 가격</span>
|
||||||
|
<span className="flex-1 text-lg">{state.orders.map(order => order.price * order.quantity)
|
||||||
|
.reduce((acc, price) => acc + price, 0) * 1000}원
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<DrawerFooter>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button onClick={async ()=>{
|
||||||
|
console.log("order complete", router);
|
||||||
|
const d = await saveOrderApi(state.orders, "account")
|
||||||
|
console.log(d);
|
||||||
|
console.log("a complete", router);
|
||||||
|
router.push("/");
|
||||||
|
|
||||||
|
}}>계좌이체 주문하기</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button onClick={()=>{
|
||||||
|
saveOrderApi(state.orders, "cash").then(()=>{
|
||||||
|
console.log("order complete", router);
|
||||||
|
router.push("/");
|
||||||
|
})
|
||||||
|
}}>현금 주문하기</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
63
components/orderCard.tsx
Normal file
63
components/orderCard.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { OrderStateColumns } from "@/lib/types";
|
||||||
|
import { cancelOrderApi, completeOrderApi } from "@/app/order/action";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||||
|
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "./ui/drawer";
|
||||||
|
import { Pencil2Icon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
export default function OrderCard({
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
order: OrderStateColumns;
|
||||||
|
}) {
|
||||||
|
return <Card className="flex flex-col h-60">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<div className="absolute right-2">
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button variant="ghost" disabled={order.completed == 1}>
|
||||||
|
<TrashIcon />
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>
|
||||||
|
주문 취소
|
||||||
|
</DrawerTitle>
|
||||||
|
<DrawerDescription>
|
||||||
|
정말로 주문을 취소하시겠습니까?
|
||||||
|
</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button onClick={() => {
|
||||||
|
cancelOrderApi(order.id);
|
||||||
|
}}>취소</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
<CardTitle className={order.completed == 1 ? "text-muted-foreground" : ""} >{order.id.split("/")[1]}번 주문</CardTitle>
|
||||||
|
<CardDescription>{order.completed ? "완료" : "대기중..."}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 overflow-scroll">
|
||||||
|
<hr />
|
||||||
|
{order.orders.map((item) => (
|
||||||
|
<div key={item.name}>
|
||||||
|
<span>{item.HOT ? "HOT" : "COLD"} {item.name} {item.quantity}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button onClick={() => {
|
||||||
|
if (order.completed) {
|
||||||
|
completeOrderApi(order.id, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
completeOrderApi(order.id);
|
||||||
|
}
|
||||||
|
}}>{order.completed == 0 ? "완료" : "완료 취소"}</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
}
|
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
76
components/ui/card.tsx
Normal file
76
components/ui/card.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
118
components/ui/drawer.tsx
Normal file
118
components/ui/drawer.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Drawer = ({
|
||||||
|
shouldScaleBackground = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||||
|
<DrawerPrimitive.Root
|
||||||
|
shouldScaleBackground={shouldScaleBackground}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Drawer.displayName = "Drawer"
|
||||||
|
|
||||||
|
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||||
|
|
||||||
|
const DrawerPortal = DrawerPrimitive.Portal
|
||||||
|
|
||||||
|
const DrawerClose = DrawerPrimitive.Close
|
||||||
|
|
||||||
|
const DrawerOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DrawerContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
))
|
||||||
|
DrawerContent.displayName = "DrawerContent"
|
||||||
|
|
||||||
|
const DrawerHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DrawerHeader.displayName = "DrawerHeader"
|
||||||
|
|
||||||
|
const DrawerFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DrawerFooter.displayName = "DrawerFooter"
|
||||||
|
|
||||||
|
const DrawerTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DrawerDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
128
components/ui/navigation-menu.tsx
Normal file
128
components/ui/navigation-menu.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const NavigationMenu = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<NavigationMenuViewport />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
))
|
||||||
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const NavigationMenuList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||||
|
|
||||||
|
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const NavigationMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||||
|
|
||||||
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
NavigationMenuViewport.displayName =
|
||||||
|
NavigationMenuPrimitive.Viewport.displayName
|
||||||
|
|
||||||
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
))
|
||||||
|
NavigationMenuIndicator.displayName =
|
||||||
|
NavigationMenuPrimitive.Indicator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
}
|
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
69
hooks/useOrder.ts
Normal file
69
hooks/useOrder.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useReducer } from "react";
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
HOT: boolean;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface OrderState {
|
||||||
|
orders: Order[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
orders: [],
|
||||||
|
} as OrderState;
|
||||||
|
|
||||||
|
export type OrderAction =
|
||||||
|
| { type: "ADD_ORDER"; order: Order }
|
||||||
|
| { type: "REMOVE_ORDER"; order: Order }
|
||||||
|
| { type: "UPDATE_ORDER"; order: Order };
|
||||||
|
|
||||||
|
function compareOrder(a: Order, b: Order) {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.HOT && !b.HOT) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!a.HOT && b.HOT) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(state: OrderState, action: OrderAction): OrderState {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_ORDER":
|
||||||
|
if (state.orders.find(order => compareOrder(order, action.order) === 0)) {
|
||||||
|
return {
|
||||||
|
orders: state.orders.map(order => compareOrder(order, action.order) === 0 ? {
|
||||||
|
...order,
|
||||||
|
quantity: Math.max(order.quantity + action.order.quantity, 0)
|
||||||
|
} : order),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
orders: [...state.orders, action.order],
|
||||||
|
};
|
||||||
|
case "REMOVE_ORDER":
|
||||||
|
return {
|
||||||
|
orders: state.orders.filter(order => compareOrder(order, action.order) !== 0),
|
||||||
|
};
|
||||||
|
case "UPDATE_ORDER":
|
||||||
|
return {
|
||||||
|
orders: state.orders.map(order => compareOrder(order, action.order) === 0 ? action.order : order),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrder() {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
return { state, dispatch };
|
||||||
|
}
|
97
lib/db.ts
Normal file
97
lib/db.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { Order } from "@/hooks/useOrder";
|
||||||
|
import { Database, NewOrderState } from './types' // this is the Database interface we defined earlier
|
||||||
|
import SQLite from 'better-sqlite3'
|
||||||
|
import { Kysely, SqliteDialect } from 'kysely'
|
||||||
|
|
||||||
|
const dialect = new SqliteDialect({
|
||||||
|
database: new SQLite('order.sqlite3'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const db = new Kysely<Database>({
|
||||||
|
dialect,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Payment = "account" | "cash";
|
||||||
|
|
||||||
|
export async function requestOrderNumber() : Promise<number> {
|
||||||
|
const date = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
return await db.transaction().execute(async trx=> {
|
||||||
|
const order_number = await trx.selectFrom("Order_Number")
|
||||||
|
.where("id", "==", date)
|
||||||
|
.selectAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!order_number) {
|
||||||
|
await trx.insertInto("Order_Number")
|
||||||
|
.values({
|
||||||
|
id: date,
|
||||||
|
number: 1,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const num = order_number.number + 1;
|
||||||
|
await trx.updateTable("Order_Number")
|
||||||
|
.set("number", num)
|
||||||
|
.where("id", "==", date)
|
||||||
|
.execute();
|
||||||
|
return num;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveOrder(order: Order[], payment: Payment) {
|
||||||
|
const date = new Date().toISOString().split("T")[0];
|
||||||
|
const num = await requestOrderNumber();
|
||||||
|
const id = `${date}/${num}`;
|
||||||
|
await db.insertInto("Order_State")
|
||||||
|
.values({
|
||||||
|
id,
|
||||||
|
orders: JSON.stringify(order),
|
||||||
|
completed: 0,
|
||||||
|
payment,
|
||||||
|
} as NewOrderState)
|
||||||
|
.execute();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadOrder() {
|
||||||
|
const orders = (await db.selectFrom("Order_State")
|
||||||
|
.selectAll()
|
||||||
|
.orderBy(["completed","id desc"])
|
||||||
|
.execute())
|
||||||
|
.map(order => ({
|
||||||
|
...order,
|
||||||
|
orders: JSON.parse(order.orders as unknown as string) as Order[],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearOrder() {
|
||||||
|
await db.deleteFrom("Order_State")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelOrder(order_id: string) {
|
||||||
|
await db.deleteFrom("Order_State")
|
||||||
|
.where("id", "==", order_id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function completeOrder(uid: string, completed: boolean = true) {
|
||||||
|
await db.updateTable("Order_State")
|
||||||
|
.set("completed", completed ? 1 : 0)
|
||||||
|
.where("id", "==", uid)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadOrderById(uid: string) {
|
||||||
|
const order = await db.selectFrom("Order_State")
|
||||||
|
.where("id", "==", uid)
|
||||||
|
.selectAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
return order ? {
|
||||||
|
...order,
|
||||||
|
orders: JSON.parse(order.orders as unknown as string) as Order[],
|
||||||
|
} : null;
|
||||||
|
}
|
25
lib/readCsv.tsx
Normal file
25
lib/readCsv.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { readFile } from "fs/promises";
|
||||||
|
|
||||||
|
export type MenuItem = {
|
||||||
|
name: string;
|
||||||
|
HOT: number | null;
|
||||||
|
COLD: number | null;
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readMenu = async () => {
|
||||||
|
const file = await readFile('public/menu.csv', 'utf-8');
|
||||||
|
const [headersRaw, ...rows] = file.split('\n').map(row => row.trim()).filter(row => row.length > 0);
|
||||||
|
const headers = headersRaw.split(',').map(header => header.trim());
|
||||||
|
|
||||||
|
const data = rows.map(row => {
|
||||||
|
const items = row.split(',');
|
||||||
|
return {
|
||||||
|
name: items[0].trim(),
|
||||||
|
HOT: Number.parseFloat(items[1].trim()) || null,
|
||||||
|
COLD: Number.parseFloat(items[2].trim()) || null,
|
||||||
|
category: items[3].trim(),
|
||||||
|
} satisfies MenuItem;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
34
lib/types.ts
Normal file
34
lib/types.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
ColumnType,
|
||||||
|
Generated,
|
||||||
|
Insertable,
|
||||||
|
JSONColumnType,
|
||||||
|
Selectable,
|
||||||
|
Updateable
|
||||||
|
} from 'kysely';
|
||||||
|
import { Order } from "@/hooks/useOrder";
|
||||||
|
|
||||||
|
export interface OrderState {
|
||||||
|
id: string;
|
||||||
|
orders: JSONColumnType<Order[]>;
|
||||||
|
completed: 0 | 1;
|
||||||
|
payment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewOrderState = Insertable<OrderState>;
|
||||||
|
export type OrderStateColumns = Selectable<OrderState>;
|
||||||
|
export type OrderStateUpdate = Updateable<OrderState>;
|
||||||
|
|
||||||
|
export interface OrderNumber {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewOrderNumber = Insertable<OrderNumber>;
|
||||||
|
export type OrderNumberColumns = Selectable<OrderNumber>;
|
||||||
|
export type OrderNumberUpdate = Updateable<OrderNumber>;
|
||||||
|
|
||||||
|
export interface Database {
|
||||||
|
Order_State: OrderState;
|
||||||
|
Order_Number: OrderNumber;
|
||||||
|
}
|
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
54
migrate.ts
Normal file
54
migrate.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import {
|
||||||
|
Kysely,
|
||||||
|
Migrator,
|
||||||
|
SqliteDialect,
|
||||||
|
FileMigrationProvider,
|
||||||
|
} from 'kysely'
|
||||||
|
import SQLite from "better-sqlite3";
|
||||||
|
import { Database } from './lib/types';
|
||||||
|
|
||||||
|
export async function migrateToLatest() {
|
||||||
|
const __dirname = path.resolve();
|
||||||
|
const db = new Kysely<Database>({
|
||||||
|
dialect: new SqliteDialect({
|
||||||
|
database: new SQLite("order.sqlite3"),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { up } = await import('./migration/2024-04-19');
|
||||||
|
|
||||||
|
await up(db);
|
||||||
|
|
||||||
|
// const migrator = new Migrator({
|
||||||
|
// db,
|
||||||
|
// provider: new FileMigrationProvider({
|
||||||
|
// fs,
|
||||||
|
// path,
|
||||||
|
// // This needs to be an absolute path.
|
||||||
|
// migrationFolder: path.join(__dirname, './migration'),
|
||||||
|
// }),
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const { error, results } = await migrator.migrateToLatest()
|
||||||
|
|
||||||
|
// results?.forEach((it) => {
|
||||||
|
// if (it.status === 'Success') {
|
||||||
|
// console.log(`migration "${it.migrationName}" was executed successfully`)
|
||||||
|
// } else if (it.status === 'Error') {
|
||||||
|
// console.error(`failed to execute migration "${it.migrationName}"`)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (error) {
|
||||||
|
// console.error('failed to migrate')
|
||||||
|
// console.error(error)
|
||||||
|
// process.exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
await db.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateToLatest()
|
22
migration/2024-04-19.ts
Normal file
22
migration/2024-04-19.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Kysely } from 'kysely'
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('Order_State')
|
||||||
|
.addColumn('id', 'varchar', col => col.primaryKey().notNull())
|
||||||
|
.addColumn('orders', 'jsonb', col => col.notNull())
|
||||||
|
.addColumn('completed', 'boolean', col => col.notNull())
|
||||||
|
.addColumn('payment', 'varchar', col => col.notNull())
|
||||||
|
.execute()
|
||||||
|
await db.schema
|
||||||
|
.createTable('Order_Number')
|
||||||
|
// current date. (e.g. 2024-04-19)
|
||||||
|
.addColumn('id', 'varchar', col => col.primaryKey().notNull())
|
||||||
|
.addColumn('number', 'integer', col => col.notNull())
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable('Order_State').execute()
|
||||||
|
await db.schema.dropTable('Order_Number').execute()
|
||||||
|
}
|
@ -1,4 +1,8 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: ["@deno/kv"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
30
package.json
30
package.json
@ -6,21 +6,39 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"shadcn": "shadcn-ui"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@deno/kv": "^0.7.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"better-sqlite3": "^9.5.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"kysely": "^0.27.3",
|
||||||
|
"next": "14.2.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"next": "14.2.1"
|
"tailwind-merge": "^2.2.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tsx": "^4.7.2",
|
||||||
|
"vaul": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"postcss": "^8",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.1"
|
"eslint-config-next": "14.2.1",
|
||||||
|
"postcss": "^8",
|
||||||
|
"shadcn-ui": "^0.8.0",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2062
pnpm-lock.yaml
2062
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
26
public/menu.csv
Normal file
26
public/menu.csv
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name,HOT,COLD,category
|
||||||
|
아메리카노,1.5,2,커피
|
||||||
|
헤이즐넛 아메리카노,1.5,2,커피
|
||||||
|
바닐라 아메리카노,1.5,2,커피
|
||||||
|
꿀단지 커피,2.5,3,커피
|
||||||
|
곰다방커피,,2,커피
|
||||||
|
콜드브루,,2,커피
|
||||||
|
카페모카,2,2.5,커피
|
||||||
|
카페라떼,3,3.5,카페라떼
|
||||||
|
헤이즐넛 카페라떼,3,3.5,카페라떼
|
||||||
|
바닐라 카페라떼,3,3.5,카페라떼
|
||||||
|
초코라떼,3,3.5,카페라떼
|
||||||
|
딸기라떼,3,3.5,카페라떼
|
||||||
|
사과유자차,3,3.5,차
|
||||||
|
꿀유자차,2,2.5,차
|
||||||
|
폴라복숭아,,2.5,차
|
||||||
|
허니자몽 블랙티,2.5,3,차
|
||||||
|
오미자 차,2,2.5,차
|
||||||
|
달곰상곰 딸기레몬,3.5,3.5,탄산
|
||||||
|
청포도 에이드,3,3,탄산
|
||||||
|
자몽에이드,3,3,탄산
|
||||||
|
레몬에이드,3,3,탄산
|
||||||
|
오미자 에이드,3,3,탄산
|
||||||
|
허니브레드,,3,빵
|
||||||
|
소금빵,,3,빵
|
||||||
|
크로크무슈,,3.5,빵
|
|
@ -1,20 +1,84 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss"
|
||||||
|
import defaultTheme from "tailwindcss/defaultTheme"
|
||||||
|
|
||||||
const config: Config = {
|
const config = {
|
||||||
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
'./pages/**/*.{ts,tsx}',
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
'./components/**/*.{ts,tsx}',
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
],
|
],
|
||||||
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
fontFamily: {
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
|
||||||
"gradient-conic":
|
},
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwindcss-animate")],
|
||||||
};
|
} satisfies Config
|
||||||
export default config;
|
|
||||||
|
export default config
|
@ -10,6 +10,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"target": "ESNext",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
Loading…
Reference in New Issue
Block a user