diff --git a/components/EditBox.tsx b/components/EditBox.tsx index 7523cc3..7de7073 100644 --- a/components/EditBox.tsx +++ b/components/EditBox.tsx @@ -2,8 +2,15 @@ import { Atom, useAtom } from "jotai"; import { useState } from "react"; import styled from "styled-components"; -export interface Props { - valueAtom: Atom; +export interface CanBeConvertedToString { + toString(): string; +} + +export interface Props { + valueAtom: Atom; + formatter?: (arg: T) => string; + inputType?: string; + validator: (arg: string) => T | null; } const ClickableContainer = styled.span` @@ -12,7 +19,6 @@ const ClickableContainer = styled.span` margin: 4px; border: 1px solid #eee; border-radius: 5px; - width: 120px; &:hover { background-color: #eee; @@ -25,19 +31,18 @@ const EditingBox = styled.input` margin: 4px; border: 1px solid #eee; border-radius: 5px; - width: 120px; `; -export default function EditBox({ valueAtom }: Props) { +export default function EditBox({ + valueAtom, + formatter, + inputType, + validator, +}: Props) { const [value, setValue] = useAtom(valueAtom); const [valueInput, setValueInput] = useState(""); const [editing, setEditing] = useState(false); - const formatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }); - const startEditing = (_: any) => { setValueInput(value.toString()); setEditing(true); @@ -45,13 +50,10 @@ export default function EditBox({ valueAtom }: Props) { const finalize = (e: Event) => { e.preventDefault(); - try { - const n = parseFloat(valueInput); - if (isNaN(n) || !isFinite(n)) return; - setValue(n); + const validateResult = validator(valueInput); + if (validateResult !== null) { + setValue(validateResult); setEditing(false); - } catch (e) { - // TODO: Handle } }; @@ -60,7 +62,7 @@ export default function EditBox({ valueAtom }: Props) {
- {formatter.format(value)} + {formatter ? formatter(value) : value.toString()} ); } diff --git a/components/NumberEditBox.tsx b/components/NumberEditBox.tsx new file mode 100644 index 0000000..0788e36 --- /dev/null +++ b/components/NumberEditBox.tsx @@ -0,0 +1,28 @@ +import { Atom } from "jotai"; +import EditBox from "./EditBox"; + +export interface Props { + valueAtom: Atom; + formatter?: (arg: number) => string; +} + +export default function NumberEditBox({ valueAtom, formatter }: Props) { + const validator = (arg: string) => { + try { + const n = parseFloat(arg); + if (isNaN(n) || !isFinite(n)) return; + return n; + } catch (e) { + return null; + } + }; + + return ( + n.toString())} + validator={validator} + /> + ); +} diff --git a/components/Person.tsx b/components/Person.tsx index c424828..675bcea 100644 --- a/components/Person.tsx +++ b/components/Person.tsx @@ -1,13 +1,14 @@ import { Atom, useAtom, WritableAtom } from "jotai"; import { Badge, ListGroup } from "react-bootstrap"; +import EditBox from "./EditBox"; export interface IPerson { - name: string; + name: Atom; } export interface Props { personAtom: Atom; - splitBetweenAtom: WritableAtom[]>; + splitBetweenAtom: Atom[]>; } export default function Person({ personAtom, splitBetweenAtom }: Props) { @@ -19,8 +20,9 @@ export default function Person({ personAtom, splitBetweenAtom }: Props) { }; return ( - - {person.name} + <> + s} /> + × - + ); } diff --git a/components/ReceiptItem.tsx b/components/ReceiptItem.tsx index 9cb3650..52b5a53 100644 --- a/components/ReceiptItem.tsx +++ b/components/ReceiptItem.tsx @@ -1,18 +1,22 @@ import { Atom, useAtom } from "jotai"; import { Badge, Card } from "react-bootstrap"; +import { moneyFormatter } from "../lib/formatter"; import { receiptAtom } from "../lib/state"; import EditBox from "./EditBox"; +import NumberEditBox from "./NumberEditBox"; import { IPerson } from "./Person"; import SplitBetween from "./SplitBetween"; export interface IReceiptItem { - name: string; + name: Atom; price: Atom; splitBetween: Atom[]>; } function Price({ priceAtom }) { - return ; + return ( + + ); } export interface Props { @@ -31,7 +35,9 @@ export default function ReceiptItem({ itemAtom }: Props) { -

{item.name}

+

+ s} /> +

{ e.preventDefault(); - setSplitBetween([...splitBetween, atom({ name: input })]); + const person = { name: atom(input) }; + setSplitBetween([...splitBetween, atom(person)]); setEditing(false); }; return (
Split between ({splitBetween.length}): - - {splitBetween.map((a, i) => ( - - ))} - - {editing ? ( - - setEditing(false)} - onInput={(e) => setInput(e.target.value)} - /> - - ) : ( - "[+]" - )} - - + {splitBetween.map((a, i) => ( + + ))} +
); } diff --git a/lib/formatter.ts b/lib/formatter.ts new file mode 100644 index 0000000..4ee5906 --- /dev/null +++ b/lib/formatter.ts @@ -0,0 +1,4 @@ +export const moneyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", +}); diff --git a/lib/state.ts b/lib/state.ts index afcaba5..9f5c9a4 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -22,32 +22,33 @@ export const receiptTotalAtom = atom((get) => { subtotalSum += price; for (const personAtom of splitBetween) { const person = get(personAtom); - const personName = person.name; + const personName = get(person.name); let accum = totals.get(personName) || 0; accum += eachPrice; totals.set(personName, accum); } } - if (subtotalSum == 0) return totals; + if (subtotalSum == 0) return { subtotal: subtotalSum, totalMap: totals }; const newTotals = new Map(); const proportion = totalValue / subtotalSum; for (const [person, value] of totals.entries()) { newTotals.set(person, value * proportion); } - return newTotals; + return { subtotal: subtotalSum, totalMap: newTotals }; }); export function addLine(line: string, setReceipt) { let parsed = parseInput(line); console.log(parsed); + const name = atom(parsed.itemName); const price = atom(parsed.price || 0); const splitBetween = atom( [...parsed.splitBetween].map((a) => atom({ name: a })) ); setReceipt((prev) => [ ...prev, - atom({ name: parsed.itemName, price, splitBetween }), + atom({ name, price, splitBetween }), ]); } diff --git a/pages/index.tsx b/pages/index.tsx index 9992d6c..afa55a8 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,10 +1,11 @@ -import { atom, useAtom } from "jotai"; +import { useAtom } from "jotai"; import type { NextPage } from "next"; import { useState } from "react"; import { Form } from "react-bootstrap"; -import EditBox from "../components/EditBox"; -import ReceiptItem, { IReceiptItem } from "../components/ReceiptItem"; -import parseInput, { ParsedInputDisplay } from "../lib/parseInput"; +import NumberEditBox from "../components/NumberEditBox"; +import ReceiptItem from "../components/ReceiptItem"; +import { moneyFormatter } from "../lib/formatter"; +import { ParsedInputDisplay } from "../lib/parseInput"; import { addLine, receiptAtom, @@ -14,8 +15,9 @@ import { const Home: NextPage = () => { const [receipt, setReceipt] = useAtom(receiptAtom); - const [total] = useAtom(receiptTotalAtom); const [input, setInput] = useState(""); + const [total] = useAtom(totalAtom); + const [calculated] = useAtom(receiptTotalAtom); const formatter = new Intl.NumberFormat("en-US", { style: "currency", @@ -48,20 +50,25 @@ const Home: NextPage = () => {
Receipt Total: - + + +
{receipt.map((itemAtom, i) => { return ; })} - {total.size > 0 && ( + {calculated.totalMap.size > 0 && ( <>

Weighted Breakdown

    - {[...total.entries()].map(([person, value], i) => ( + {[...calculated.totalMap.entries()].map(([person, value], i) => (
  • {person}: {formatter.format(value)}