generate new receipt id when you hit the home page

This commit is contained in:
Michael Zhang 2022-11-06 20:22:47 -06:00
parent 655085cb6d
commit 7e35b48a03
8 changed files with 189 additions and 122 deletions

View file

@ -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;

47
lib/jotaiUtil.ts Normal file
View file

@ -0,0 +1,47 @@
import { atom, Getter, PrimitiveAtom } from "jotai";
export function storedAtom<T>(initial: T) {
return atom(initial);
}
export function unwrapAtom<T>(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<T>(obj: T): PrimitiveAtom<unknown> | 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;
}

2
lib/server/realtime.ts Normal file
View file

@ -0,0 +1,2 @@
// TODO: Move this to some redis-like persistence layer
// Or figure out how to do sharding based on room ID

View file

@ -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<PrimitiveAtom<IReceiptItem>[]>([]);
export const unwrappedReceiptAtom = atom((get) => unwrapAtom(get, receiptAtom));
export const receiptTotalAtom = atom((get) => {
const totalValue = get(totalAtom);
const receipt = get(receiptAtom);

View file

@ -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<string, string>[] | Record<string, string>>
) {
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 });
}

View file

@ -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 (
<main>
<h1>Items</h1>
<Form onSubmit={add}>
<ParsedInputDisplay input={input} />
<Form.Control
autoFocus={true}
type="text"
placeholder="Add item..."
onInput={(e) => setInput(e.currentTarget.value)}
value={input}
style={{ padding: "8px 16px", fontSize: "1.5em" }}
/>
</Form>
{receipt.map((itemAtom, i) => {
return <ReceiptItem itemAtom={itemAtom} key={`receiptItem-${i}`} />;
})}
<div>
Receipt Total:
<span style={total < calculated.subtotal ? { color: "red" } : {}}>
<NumberEditBox
valueAtom={totalAtom}
formatter={moneyFormatter.format}
/>
</span>
</div>
<hr />
{calculated.totalMap.size > 0 && (
<>
<h3>Weighted Breakdown</h3>
<div>
<ul>
{[...calculated.totalMap.entries()].map(([person, value], i) => (
<li key={`breakdown-${i}`}>
<b>{person}</b>: {formatter.format(value)}
</li>
))}
</ul>
</div>
</>
)}
</main>
);
return <></>;
};
export default Home;

100
pages/receipt/[id].tsx Normal file
View file

@ -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 (
<main>
<h1>Items</h1>
<Form onSubmit={add}>
<ParsedInputDisplay input={input} />
<Form.Control
autoFocus={true}
type="text"
placeholder="Add item..."
onInput={(e) => setInput(e.currentTarget.value)}
value={input}
style={{ padding: "8px 16px", fontSize: "1.5em" }}
/>
</Form>
{receipt.map((itemAtom, i) => {
return <ReceiptItem itemAtom={itemAtom} key={`receiptItem-${i}`} />;
})}
<div>
Receipt Total:
<span style={total < calculated.subtotal ? { color: "red" } : {}}>
<NumberEditBox
valueAtom={totalAtom}
formatter={moneyFormatter.format}
/>
</span>
</div>
<hr />
{calculated.totalMap.size > 0 && (
<>
<h3>Weighted Breakdown</h3>
<div>
<ul>
{[...calculated.totalMap.entries()].map(([person, value], i) => (
<li key={`breakdown-${i}`}>
<b>{person}</b>: {formatter.format(value)}
</li>
))}
</ul>
</div>
</>
)}
</main>
);
};
export default ReceiptPage;

View file

@ -14,6 +14,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],