Bootstrap

This commit is contained in:
Michael Zhang 2022-10-24 02:40:14 -05:00
parent 1aad1d9e2d
commit 1fcf094c73
10 changed files with 1268 additions and 204 deletions

View file

@ -1,10 +1,18 @@
import { Atom, useAtom } from "jotai"; import { Atom, useAtom } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { Form } from "react-bootstrap";
import styled from "styled-components";
export interface Props { export interface Props {
valueAtom: Atom<number>; valueAtom: Atom<number>;
} }
const ClickableContainer = styled.span`
&:hover {
background-color: #eee;
}
`;
export default function EditBox({ valueAtom }: Props) { export default function EditBox({ valueAtom }: Props) {
const [value, setValue] = useAtom(valueAtom); const [value, setValue] = useAtom(valueAtom);
const [valueInput, setValueInput] = useState(""); const [valueInput, setValueInput] = useState("");
@ -45,6 +53,10 @@ export default function EditBox({ valueAtom }: Props) {
</form> </form>
); );
} else { } else {
return <span onClick={startEditing}>{formatter.format(value)}</span>; return (
<ClickableContainer onClick={startEditing}>
{formatter.format(value)}
</ClickableContainer>
);
} }
} }

17
components/Layout.tsx Normal file
View file

@ -0,0 +1,17 @@
import { Container, Navbar } from "react-bootstrap";
export default function Layout({ children }) {
return (
<>
<Navbar bg="dark" variant="dark" expand="lg">
<Container>
<Navbar.Brand href="">WiseSplit</Navbar.Brand>
</Container>
</Navbar>
<Container>
<main>{children}</main>
</Container>
</>
);
}

View file

@ -1,11 +1,11 @@
import { Atom, useAtom } from "jotai"; import { Atom, useAtom } from "jotai";
import EditBox from "./EditBox"; import EditBox from "./EditBox";
import Person from "./Person"; import Person, { IPerson } from "./Person";
export interface IReceiptItem { export interface IReceiptItem {
name: string; name: string;
price: Atom<number>; price: Atom<number>;
splitBetween: Atom<Atom<string>[]>; splitBetween: Atom<Atom<IPerson>[]>;
} }
export interface Props { export interface Props {

53
lib/state.ts Normal file
View file

@ -0,0 +1,53 @@
import { atom, PrimitiveAtom } from "jotai";
import { IReceiptItem } from "../components/ReceiptItem";
import parseInput from "./parseInput";
export const totalAtom = atom(0);
export const receiptAtom = atom<PrimitiveAtom<IReceiptItem>[]>([]);
export const receiptTotalAtom = atom((get) => {
const totalValue = get(totalAtom);
const receipt = get(receiptAtom);
const totals = new Map();
let subtotalSum = 0;
for (const itemAtom of receipt) {
const item = get(itemAtom);
const price = get(item.price);
const splitBetween = get(item.splitBetween);
const numSplitters = splitBetween.length;
if (numSplitters == 0) continue;
const eachPrice = price / numSplitters;
subtotalSum += price;
for (const personAtom of splitBetween) {
const person = get(personAtom);
const personName = person.name;
let accum = totals.get(personName) || 0;
accum += eachPrice;
totals.set(personName, accum);
}
}
if (subtotalSum == 0) return totals;
const newTotals = new Map();
const proportion = totalValue / subtotalSum;
for (const [person, value] of totals.entries()) {
newTotals.set(person, value * proportion);
}
return newTotals;
});
export function addLine(line: string, setReceipt) {
let parsed = parseInput(line);
console.log(parsed);
const price = atom(parsed.price || 0);
const splitBetween = atom(
[...parsed.splitBetween].map((a) => atom({ name: a }))
);
setReceipt((prev) => [
...prev,
atom<IReceiptItem>({ name: parsed.itemName, price, splitBetween }),
]);
}

View file

@ -3,6 +3,10 @@ const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
swcMinify: true, swcMinify: true,
compiler: {
styledComponents: true,
},
typescript: { typescript: {
// TODO: Move fast and break things lmao // TODO: Move fast and break things lmao
ignoreBuildErrors: true, ignoreBuildErrors: true,

1163
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,16 +10,20 @@
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.8.6", "@reduxjs/toolkit": "^1.8.6",
"bootstrap": "^5.2.2",
"jotai": "^1.8.6", "jotai": "^1.8.6",
"next": "12.3.1", "next": "12.3.1",
"react": "18.2.0", "react": "18.2.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.4",
"styled-components": "^5.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.11.4", "@types/node": "18.11.4",
"@types/react": "18.0.21", "@types/react": "18.0.21",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",
"@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",
"typescript": "4.8.4" "typescript": "4.8.4"

View file

@ -1,8 +1,13 @@
import "../styles/globals.css"; import "bootstrap/dist/css/bootstrap.min.css";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import Layout from "../components/Layout";
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />; return (
<Layout>
<Component {...pageProps} />
</Layout>
);
} }
export default MyApp; export default MyApp;

View file

@ -1,48 +1,16 @@
import { atom, PrimitiveAtom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { selectAtom } from "jotai/utils";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { useState } from "react"; import { useState } from "react";
import { Form } from "react-bootstrap";
import EditBox from "../components/EditBox"; import EditBox from "../components/EditBox";
import ReceiptItem, { IReceiptItem } from "../components/ReceiptItem"; import ReceiptItem, { IReceiptItem } from "../components/ReceiptItem";
import Totals from "../components/Totals";
import parseInput, { ParsedInputDisplay } from "../lib/parseInput"; import parseInput, { ParsedInputDisplay } from "../lib/parseInput";
import {
const totalAtom = atom(0); addLine,
const receiptAtom = atom<PrimitiveAtom<IReceiptItem>[]>([]); receiptAtom,
receiptTotalAtom,
const receiptTotalAtom = atom((get) => { totalAtom,
const totalValue = get(totalAtom); } from "../lib/state";
const receipt = get(receiptAtom);
const totals = new Map();
let subtotalSum = 0;
for (const itemAtom of receipt) {
const item = get(itemAtom);
const price = get(item.price);
const splitBetween = get(item.splitBetween);
const numSplitters = splitBetween.length;
if (numSplitters == 0) continue;
const eachPrice = price / numSplitters;
subtotalSum += price;
for (const personAtom of splitBetween) {
const person = get(personAtom);
const personName = person.name;
let accum = totals.get(personName) || 0;
accum += eachPrice;
totals.set(personName, accum);
}
}
if (subtotalSum == 0) return totals;
const newTotals = new Map();
const proportion = totalValue / subtotalSum;
for (const [person, value] of totals.entries()) {
newTotals.set(person, value * proportion);
}
return newTotals;
});
const Home: NextPage = () => { const Home: NextPage = () => {
const [receipt, setReceipt] = useAtom(receiptAtom); const [receipt, setReceipt] = useAtom(receiptAtom);
@ -56,28 +24,19 @@ const Home: NextPage = () => {
const add = (e) => { const add = (e) => {
e.preventDefault(); e.preventDefault();
let parsed = parseInput(input); addLine(input, setReceipt);
console.log(parsed);
const price = atom(parsed.price || 0);
const splitBetween = atom(
[...parsed.splitBetween].map((a) => atom({ name: a }))
);
setReceipt((prev) => [
...prev,
atom<IReceiptItem>({ name: parsed.itemName, price, splitBetween }),
]);
setInput(""); setInput("");
return false; return false;
}; };
return ( return (
<main> <main>
<h2>Items</h2> <h1>Items</h1>
<form onSubmit={add}> <Form onSubmit={add}>
<ParsedInputDisplay input={input} /> <ParsedInputDisplay input={input} />
<input <Form.Control
autoFocus={true} autoFocus={true}
type="text" type="text"
placeholder="Add item..." placeholder="Add item..."
@ -85,7 +44,7 @@ const Home: NextPage = () => {
value={input} value={input}
style={{ padding: "8px", fontSize: "1.5em" }} style={{ padding: "8px", fontSize: "1.5em" }}
/> />
</form> </Form>
<div> <div>
Receipt Total: Receipt Total:
@ -112,6 +71,12 @@ const Home: NextPage = () => {
))} ))}
</ul> </ul>
</div> </div>
<small>
<a href="https://github.com/iptq/wisesplit/">[source]</a>
&middot;
<a href="https://www.gnu.org/licenses/agpl-3.0.txt">[license]</a>
</small>
</main> </main>
); );
}; };

View file

@ -1,129 +0,0 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}