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 { useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
|
||||
export interface Props {
|
||||
valueAtom: Atom<number>;
|
||||
}
|
||||
|
||||
const ClickableContainer = styled.span`
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function EditBox({ valueAtom }: Props) {
|
||||
const [value, setValue] = useAtom(valueAtom);
|
||||
const [valueInput, setValueInput] = useState("");
|
||||
|
@ -45,6 +53,10 @@ export default function EditBox({ valueAtom }: Props) {
|
|||
</form>
|
||||
);
|
||||
} 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 EditBox from "./EditBox";
|
||||
import Person from "./Person";
|
||||
import Person, { IPerson } from "./Person";
|
||||
|
||||
export interface IReceiptItem {
|
||||
name: string;
|
||||
price: Atom<number>;
|
||||
splitBetween: Atom<Atom<string>[]>;
|
||||
splitBetween: Atom<Atom<IPerson>[]>;
|
||||
}
|
||||
|
||||
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,
|
||||
swcMinify: true,
|
||||
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
|
||||
typescript: {
|
||||
// TODO: Move fast and break things lmao
|
||||
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": {
|
||||
"@reduxjs/toolkit": "^1.8.6",
|
||||
"bootstrap": "^5.2.2",
|
||||
"jotai": "^1.8.6",
|
||||
"next": "12.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-bootstrap": "^2.5.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "^8.0.4"
|
||||
"react-redux": "^8.0.4",
|
||||
"styled-components": "^5.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.4",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"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 Layout from "../components/Layout";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
return (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
|
|
|
@ -1,48 +1,16 @@
|
|||
import { atom, PrimitiveAtom, useAtom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { atom, 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 Totals from "../components/Totals";
|
||||
import parseInput, { ParsedInputDisplay } from "../lib/parseInput";
|
||||
|
||||
const totalAtom = atom(0);
|
||||
const receiptAtom = atom<PrimitiveAtom<IReceiptItem>[]>([]);
|
||||
|
||||
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;
|
||||
});
|
||||
import {
|
||||
addLine,
|
||||
receiptAtom,
|
||||
receiptTotalAtom,
|
||||
totalAtom,
|
||||
} from "../lib/state";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [receipt, setReceipt] = useAtom(receiptAtom);
|
||||
|
@ -56,28 +24,19 @@ const Home: NextPage = () => {
|
|||
|
||||
const add = (e) => {
|
||||
e.preventDefault();
|
||||
let parsed = parseInput(input);
|
||||
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 }),
|
||||
]);
|
||||
addLine(input, setReceipt);
|
||||
setInput("");
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h2>Items</h2>
|
||||
<h1>Items</h1>
|
||||
|
||||
<form onSubmit={add}>
|
||||
<Form onSubmit={add}>
|
||||
<ParsedInputDisplay input={input} />
|
||||
|
||||
<input
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
placeholder="Add item..."
|
||||
|
@ -85,7 +44,7 @@ const Home: NextPage = () => {
|
|||
value={input}
|
||||
style={{ padding: "8px", fontSize: "1.5em" }}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<div>
|
||||
Receipt Total:
|
||||
|
@ -112,6 +71,12 @@ const Home: NextPage = () => {
|
|||
))}
|
||||
</ul>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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