Compare commits

...

14 commits

Author SHA1 Message Date
Devin Deng
eedf038963 psuhign what I have 2022-11-08 21:20:15 -08:00
fa704809ec socket implementation wip 2022-11-07 00:20:22 -06:00
7e35b48a03 generate new receipt id when you hit the home page 2022-11-06 20:22:47 -06:00
655085cb6d reformat with prettier 2022-11-06 19:33:24 -06:00
b14a49803e add prettier 2022-11-06 19:32:42 -06:00
0de2f649e8 don't add app config for now 2022-11-06 19:31:15 -06:00
Devin Deng
9c3c6c6363 use port env variable for mongodb client 2022-11-06 19:29:32 -06:00
Devin Deng
c34e8e91b5 set db 2022-11-06 19:29:32 -06:00
Devin Deng
4a957bce10 remove console.logs, POST only for updateReceipt 2022-11-06 19:29:32 -06:00
Devin Deng
210f84a0b3 update package-lock, with nodev16 2022-11-06 19:29:32 -06:00
Devin Deng
85091594ce logic for writing to db after adding receipt item 2022-11-06 19:29:32 -06:00
Devin Deng
9b55048720 test 2022-11-06 19:29:32 -06:00
2d5c711979 asdf 2022-11-06 19:02:34 -06:00
6c4130aaf3 Move receipt total below 2022-11-04 12:05:04 -05:00
17 changed files with 3009 additions and 134 deletions

5
.gitignore vendored
View file

@ -18,6 +18,7 @@
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
.vscode
# debug # debug
npm-debug.log* npm-debug.log*
@ -26,7 +27,9 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# local env files # local env files
.env*.local .env*
.local
# vercel # vercel
.vercel .vercel

8
.prettierrc.json5 Normal file
View file

@ -0,0 +1,8 @@
{
tabWidth: 2,
semi: true,
useTabs: false,
singleQuote: false,
trailingComma: "es5",
printWidth: 80,
}

View file

@ -35,7 +35,6 @@ export default function Layout({ children }: Props) {
[license] [license]
</a> </a>
&middot; &middot;
{/* eslint-disable @next/next/no-img-element */} {/* eslint-disable @next/next/no-img-element */}
<a href="https://github.com/iptq/wisesplit/stargazers"> <a href="https://github.com/iptq/wisesplit/stargazers">
<img <img

9
docker-compose.yml Normal file
View file

@ -0,0 +1,9 @@
version: "3"
services:
app:
build: .
db:
image: mongo
ports: [27017:27017]

24
lib/getMongoDBClient.ts Normal file
View file

@ -0,0 +1,24 @@
import { Db, MongoClient } from "mongodb";
const USERNAME = process.env.MONGO_USERNAME;
const PASSWORD = process.env.MONGO_PASSWORD;
const HOSTNAME = process.env.MONGO_HOSTNAME;
const DATABASE_NAME = process.env.MONGO_DATABASE_NAME;
const DATABASE_PORT = process.env.MONGO_DATABASE_PORT;
const userInfo = USERNAME && PASSWORD ? `${USERNAME}:${PASSWORD}@` : "";
const URI = `mongodb://${userInfo}${HOSTNAME ?? "localhost"}:${DATABASE_PORT}`;
let db: Db | null = null;
export const getMongoDBClient = async () => {
if (db) {
return db;
}
const client = new MongoClient(URI);
await client.connect();
db = client.db(DATABASE_NAME);
return db;
};

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 { 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);

2730
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,14 +9,17 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.8.6", "@reduxjs/toolkit": "^1.9.0",
"bootstrap": "^5.2.2", "bootstrap": "^5.2.2",
"jotai": "^1.8.6", "jotai": "^1.8.6",
"mongodb": "^4.11.0",
"next": "12.3.1", "next": "12.3.1",
"react": "18.2.0", "react": "18.2.0",
"react-bootstrap": "^2.5.0", "react-bootstrap": "^2.5.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-redux": "^8.0.4", "react-redux": "^8.0.5",
"socket.io": "^4.5.3",
"socket.io-client": "^4.5.3",
"styled-components": "^5.3.6" "styled-components": "^5.3.6"
}, },
"devDependencies": { "devDependencies": {
@ -26,6 +29,7 @@
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"eslint": "8.26.0", "eslint": "8.26.0",
"eslint-config-next": "12.3.1", "eslint-config-next": "12.3.1",
"prettier": "^2.7.1",
"typescript": "4.8.4" "typescript": "4.8.4"
} }
} }

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,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

35
pages/api/socket.ts Normal file
View file

@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { Server } from "socket.io";
const io = new Server({});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const receiptId = req.body;
console.log(receiptId);
// if (res.socket?.server.io) {
// console.log("Socket is already running");
// res.end();
// return;
// }
// console.log("Socket is initializing");
// const io = new Server(res.socket.server);
// res.socket.server.io = io;
// io.on("connection", (socket) => {
// console.log("Received new connection");
// socket.on("input-change", (msg) => {
// socket.broadcast.emit("update-input", msg);
// });
// });
res.status(200);
res.end();
}

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();
try {
// TODO implement this
res.status(200).json(receipt);
} catch (e) {
res.status(500).json({ message: (e as Error).message });
}
}

View file

@ -1,84 +1,22 @@
import { useAtom } from "jotai"; import { NextPage } from "next";
import type { NextPage } from "next"; import { useRouter } from "next/router";
import { SyntheticEvent, useState } from "react"; import { useEffect } 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 formatter = new Intl.NumberFormat("en-US", { useEffect(() => {
style: "currency", const newPage = async () => {
currency: "USD", 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 = (e: SyntheticEvent) => { return <></>;
e.preventDefault();
addLine(input, receipt, setReceipt);
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>
<div>
Receipt Total:
<span style={total < calculated.subtotal ? { color: "red" } : {}}>
<NumberEditBox
valueAtom={totalAtom}
formatter={moneyFormatter.format}
/>
</span>
</div>
{receipt.map((itemAtom, i) => {
return <ReceiptItem itemAtom={itemAtom} key={`receiptItem-${i}`} />;
})}
{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;

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

@ -0,0 +1,127 @@
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,
unwrappedReceiptAtom,
} from "lib/state";
import { io } from "socket.io-client";
import { useRouter } from "next/router";
let socket;
const ReceiptPage: NextPage = () => {
const router = useRouter();
const [receipt, setReceipt] = useAtom(receiptAtom);
const [input, setInput] = useState("");
const [total] = useAtom(totalAtom);
const [calculated] = useAtom(receiptTotalAtom);
const [unwrappedReceipt] = useAtom(unwrappedReceiptAtom);
const { pathname } = router;
// Connect to the socket server
useEffect(() => {
console.log(pathname);
const socketInitializer = async () => {
await fetch("/api/socket", {
method: "POST",
headers: {
"Content-Type": 'application/json',
},
body: JSON.stringify({ receiptId: id }),
});
// socket = io();
// socket.on("connect", () => {
// console.log("connected");
// });
};
socketInitializer();
}, []);
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, "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"],