Bootstrap
This commit is contained in:
parent
1aad1d9e2d
commit
1fcf094c73
10 changed files with 1268 additions and 204 deletions
|
@ -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
17
components/Layout.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
53
lib/state.ts
Normal 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 }),
|
||||||
|
]);
|
||||||
|
}
|
|
@ -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
1163
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
·
|
||||||
|
<a href="https://www.gnu.org/licenses/agpl-3.0.txt">[license]</a>
|
||||||
|
</small>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue