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_NAME = process.env.MONGO_DATABASE_NAME;
|
||||||
const DATABASE_PORT = process.env.MONGO_DATABASE_PORT;
|
const DATABASE_PORT = process.env.MONGO_DATABASE_PORT;
|
||||||
|
|
||||||
const URI = `mongodb://${USERNAME}:${PASSWORD}@${
|
const userInfo = USERNAME && PASSWORD ? `${USERNAME}:${PASSWORD}@` : "";
|
||||||
HOSTNAME ?? "localhost"
|
const URI = `mongodb://${userInfo}${HOSTNAME ?? "localhost"}:${DATABASE_PORT}`;
|
||||||
}:${DATABASE_PORT}`;
|
|
||||||
|
|
||||||
let db: Db | null = null;
|
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 { SetAtom } from "jotai/core/atom";
|
||||||
import { IPerson } from "../components/Person";
|
import { IPerson } from "../components/Person";
|
||||||
import { IReceiptItem, Receipt } from "../components/ReceiptItem";
|
import { IReceiptItem, Receipt } from "../components/ReceiptItem";
|
||||||
|
import { unwrapAtom } from "./jotaiUtil";
|
||||||
import parseInput from "./parseInput";
|
import parseInput from "./parseInput";
|
||||||
|
|
||||||
export const totalAtom = atom(0);
|
export const totalAtom = atom(0);
|
||||||
export const receiptAtom = atom<PrimitiveAtom<IReceiptItem>[]>([]);
|
export const receiptAtom = atom<PrimitiveAtom<IReceiptItem>[]>([]);
|
||||||
|
|
||||||
|
export const unwrappedReceiptAtom = atom((get) => unwrapAtom(get, receiptAtom));
|
||||||
|
|
||||||
export const receiptTotalAtom = atom((get) => {
|
export const receiptTotalAtom = atom((get) => {
|
||||||
const totalValue = get(totalAtom);
|
const totalValue = get(totalAtom);
|
||||||
const receipt = get(receiptAtom);
|
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 { NextPage } from "next";
|
||||||
import type { NextPage } from "next";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect } 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";
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
const [receipt, setReceipt] = useAtom(receiptAtom);
|
const router = useRouter();
|
||||||
const [input, setInput] = useState("");
|
|
||||||
const [total] = useAtom(totalAtom);
|
|
||||||
const [calculated] = useAtom(receiptTotalAtom);
|
|
||||||
const isAddCalled = useRef(false);
|
|
||||||
|
|
||||||
const [receiptJson] = useAtom(
|
useEffect(() => {
|
||||||
atom((get) => {
|
const newPage = async () => {
|
||||||
const receiptJson: any[] = [];
|
let res = await fetch("/api/createReceipt", { method: "POST" });
|
||||||
for (const itemAtom of receipt) {
|
let result = await res.json();
|
||||||
const receiptItemFromAtom = get(itemAtom);
|
let id = result.id;
|
||||||
const splitBetweenArray = get(receiptItemFromAtom.splitBetween).map(
|
if (typeof id != "string") return;
|
||||||
(personAtom) => ({
|
router.push(`/receipt/${id}`);
|
||||||
name: get(get(personAtom).name),
|
};
|
||||||
})
|
newPage();
|
||||||
);
|
|
||||||
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",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const add = async (e: SyntheticEvent) => {
|
return <></>;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
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,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"baseUrl": ".",
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
|
Loading…
Add table
Reference in a new issue