From 7e35b48a037e5bc9132ddea84fa9c50df945d8d4 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Sun, 6 Nov 2022 20:22:47 -0600 Subject: [PATCH] generate new receipt id when you hit the home page --- lib/getMongoDBClient.ts | 5 +- lib/jotaiUtil.ts | 47 +++++++++++++ lib/server/realtime.ts | 2 + lib/state.ts | 3 + pages/api/createReceipt.ts | 20 ++++++ pages/index.tsx | 133 ++++--------------------------------- pages/receipt/[id].tsx | 100 ++++++++++++++++++++++++++++ tsconfig.json | 1 + 8 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 lib/jotaiUtil.ts create mode 100644 lib/server/realtime.ts create mode 100644 pages/api/createReceipt.ts create mode 100644 pages/receipt/[id].tsx diff --git a/lib/getMongoDBClient.ts b/lib/getMongoDBClient.ts index 638610e..4809de8 100644 --- a/lib/getMongoDBClient.ts +++ b/lib/getMongoDBClient.ts @@ -6,9 +6,8 @@ const HOSTNAME = process.env.MONGO_HOSTNAME; const DATABASE_NAME = process.env.MONGO_DATABASE_NAME; const DATABASE_PORT = process.env.MONGO_DATABASE_PORT; -const URI = `mongodb://${USERNAME}:${PASSWORD}@${ - HOSTNAME ?? "localhost" -}:${DATABASE_PORT}`; +const userInfo = USERNAME && PASSWORD ? `${USERNAME}:${PASSWORD}@` : ""; +const URI = `mongodb://${userInfo}${HOSTNAME ?? "localhost"}:${DATABASE_PORT}`; let db: Db | null = null; diff --git a/lib/jotaiUtil.ts b/lib/jotaiUtil.ts new file mode 100644 index 0000000..41840be --- /dev/null +++ b/lib/jotaiUtil.ts @@ -0,0 +1,47 @@ +import { atom, Getter, PrimitiveAtom } from "jotai"; + +export function storedAtom(initial: T) { + return atom(initial); +} + +export function unwrapAtom(get: Getter, obj: T, depth: number = 0): unknown { + const log = (...s: any) => { + // console.log(" ".repeat(depth), ...s); + }; + log("Unwrapping", obj); + let atom, result; + + // Recursively try to unwrap atoms + if ((atom = isAtom(obj))) { + let innerObj = get(atom); + log("Got atom with obj", innerObj); + result = unwrapAtom(get, innerObj, depth + 1); + } else if (Array.isArray(obj)) { + log("Got array"); + result = obj.map((item) => unwrapAtom(get, item, depth + 1)); + } else if (typeof obj == "object" && obj !== null) { + log("Got object"); + result = Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + key, + unwrapAtom(get, value, depth + 1), + ]) + ); + } else { + log("Got else", typeof obj); + result = obj; + } + + log("Result", result); + return result; +} + +function isAtom(obj: T): PrimitiveAtom | null { + if (typeof obj != "object") return null; + if (obj == null) return null; + if (obj.constructor != Object) return null; + + // Heuristically check the fields + if (!("init" in obj && "write" in obj && "read" in obj)) return null; + return obj; +} diff --git a/lib/server/realtime.ts b/lib/server/realtime.ts new file mode 100644 index 0000000..edc8296 --- /dev/null +++ b/lib/server/realtime.ts @@ -0,0 +1,2 @@ +// TODO: Move this to some redis-like persistence layer +// Or figure out how to do sharding based on room ID diff --git a/lib/state.ts b/lib/state.ts index 6d3c59c..0576e66 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -2,11 +2,14 @@ import { atom, PrimitiveAtom } from "jotai"; import { SetAtom } from "jotai/core/atom"; import { IPerson } from "../components/Person"; import { IReceiptItem, Receipt } from "../components/ReceiptItem"; +import { unwrapAtom } from "./jotaiUtil"; import parseInput from "./parseInput"; export const totalAtom = atom(0); export const receiptAtom = atom[]>([]); +export const unwrappedReceiptAtom = atom((get) => unwrapAtom(get, receiptAtom)); + export const receiptTotalAtom = atom((get) => { const totalValue = get(totalAtom); const receipt = get(receiptAtom); diff --git a/pages/api/createReceipt.ts b/pages/api/createReceipt.ts new file mode 100644 index 0000000..b05b0ce --- /dev/null +++ b/pages/api/createReceipt.ts @@ -0,0 +1,20 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getMongoDBClient } from "../../lib/getMongoDBClient"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse[] | Record> +) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Only POST method allowed" }); + } + + const receipt = req.body; + const client = await getMongoDBClient(); + + const receipts = client.collection("receipts"); + const newReceipt = await receipts.insertOne({}); + const id = newReceipt.insertedId.toString(); + + res.json({ id }); +} diff --git a/pages/index.tsx b/pages/index.tsx index 11081a4..b6ea1ad 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,127 +1,22 @@ -import { useAtom, atom } from "jotai"; -import type { NextPage } from "next"; -import { useEffect, useRef } from "react"; -import { SyntheticEvent, useState } from "react"; -import { Form } from "react-bootstrap"; -import NumberEditBox from "../components/NumberEditBox"; -import ReceiptItem from "../components/ReceiptItem"; -import { moneyFormatter } from "../lib/formatter"; -import { ParsedInputDisplay } from "../lib/parseInput"; -import { - addLine, - receiptAtom, - receiptTotalAtom, - totalAtom, -} from "../lib/state"; +import { NextPage } from "next"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; const Home: NextPage = () => { - const [receipt, setReceipt] = useAtom(receiptAtom); - const [input, setInput] = useState(""); - const [total] = useAtom(totalAtom); - const [calculated] = useAtom(receiptTotalAtom); - const isAddCalled = useRef(false); + const router = useRouter(); - const [receiptJson] = useAtom( - atom((get) => { - const receiptJson: any[] = []; - for (const itemAtom of receipt) { - const receiptItemFromAtom = get(itemAtom); - const splitBetweenArray = get(receiptItemFromAtom.splitBetween).map( - (personAtom) => ({ - name: get(get(personAtom).name), - }) - ); - const receiptItemParsed = { - name: get(receiptItemFromAtom.name), - price: get(receiptItemFromAtom.price), - splitBetween: splitBetweenArray, - }; - receiptJson.push(receiptItemParsed); - } - - return receiptJson; - }) - ); - - const formatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", + useEffect(() => { + const newPage = async () => { + let res = await fetch("/api/createReceipt", { method: "POST" }); + let result = await res.json(); + let id = result.id; + if (typeof id != "string") return; + router.push(`/receipt/${id}`); + }; + newPage(); }); - const add = async (e: SyntheticEvent) => { - e.preventDefault(); - isAddCalled.current = true; - - addLine(input, receipt, setReceipt); - setInput(""); - return false; - }; - - const receiptJSONString = JSON.stringify(receiptJson); - useEffect(() => { - const updateDb = async () => { - const response = await fetch("/api/createReceipt", { - method: "POST", - body: JSON.stringify({ receipts: receiptJson }), - }); - console.log(receiptJSONString); - }; - - if (isAddCalled.current) { - updateDb(); - } - }, [receiptJSONString]); - - return ( -
-

Items

- -
- - - setInput(e.currentTarget.value)} - value={input} - style={{ padding: "8px 16px", fontSize: "1.5em" }} - /> - - - {receipt.map((itemAtom, i) => { - return ; - })} - -
- Receipt Total: - - - -
- -
- - {calculated.totalMap.size > 0 && ( - <> -

Weighted Breakdown

- -
-
    - {[...calculated.totalMap.entries()].map(([person, value], i) => ( -
  • - {person}: {formatter.format(value)} -
  • - ))} -
-
- - )} -
- ); + return <>; }; export default Home; diff --git a/pages/receipt/[id].tsx b/pages/receipt/[id].tsx new file mode 100644 index 0000000..f363de2 --- /dev/null +++ b/pages/receipt/[id].tsx @@ -0,0 +1,100 @@ +import { useAtom, atom } from "jotai"; +import type { NextPage } from "next"; +import { useEffect, useRef } from "react"; +import { SyntheticEvent, useState } from "react"; +import { Form } from "react-bootstrap"; +import NumberEditBox from "components/NumberEditBox"; +import ReceiptItem from "components/ReceiptItem"; +import { moneyFormatter } from "lib/formatter"; +import { unwrapAtom } from "lib/jotaiUtil"; +import { ParsedInputDisplay } from "lib/parseInput"; +import { + addLine, + receiptAtom, + receiptTotalAtom, + totalAtom, + unwrappedReceiptAtom, +} from "lib/state"; + +const ReceiptPage: NextPage = () => { + const [receipt, setReceipt] = useAtom(receiptAtom); + const [input, setInput] = useState(""); + const [total] = useAtom(totalAtom); + const [calculated] = useAtom(receiptTotalAtom); + const [unwrappedReceipt] = useAtom(unwrappedReceiptAtom); + const isAddCalled = useRef(false); + + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }); + + const add = async (e: SyntheticEvent) => { + e.preventDefault(); + + addLine(input, receipt, setReceipt); + + const payload = unwrappedReceipt; + console.log("Payload", payload); + fetch("/api/createReceipt", { + method: "POST", + body: JSON.stringify(payload), + }); + + setInput(""); + return false; + }; + + return ( +
+

Items

+ +
+ + + setInput(e.currentTarget.value)} + value={input} + style={{ padding: "8px 16px", fontSize: "1.5em" }} + /> + + + {receipt.map((itemAtom, i) => { + return ; + })} + +
+ Receipt Total: + + + +
+ +
+ + {calculated.totalMap.size > 0 && ( + <> +

Weighted Breakdown

+ +
+
    + {[...calculated.totalMap.entries()].map(([person, value], i) => ( +
  • + {person}: {formatter.format(value)} +
  • + ))} +
+
+ + )} +
+ ); +}; + +export default ReceiptPage; diff --git a/tsconfig.json b/tsconfig.json index e0c0028..05aaf57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", + "baseUrl": ".", "incremental": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],