generate new receipt id when you hit the home page
This commit is contained in:
parent
655085cb6d
commit
7e35b48a03
8 changed files with 189 additions and 122 deletions
|
@ -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
47
lib/jotaiUtil.ts
Normal 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
2
lib/server/realtime.ts
Normal 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
|
|
@ -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);
|
||||
|
|
20
pages/api/createReceipt.ts
Normal file
20
pages/api/createReceipt.ts
Normal 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 });
|
||||
}
|
133
pages/index.tsx
133
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 (
|
||||
<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
100
pages/receipt/[id].tsx
Normal 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;
|
|
@ -14,6 +14,7 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
|
|
Loading…
Reference in a new issue