diff --git a/.gitignore b/.gitignore index 502ba03..b11967b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ dist-ssr *.sln *.sw? -/backend/.env \ No newline at end of file +/backend/.env +.vercel +.env*.local diff --git a/src/components/PeopleManager/AddPersonModal.tsx b/src/components/PeopleManager/AddPersonModal.tsx new file mode 100644 index 0000000..31df103 --- /dev/null +++ b/src/components/PeopleManager/AddPersonModal.tsx @@ -0,0 +1,67 @@ +import Modal from "react-bootstrap/Modal"; +import { useState } from "react"; +import { DEFAULT_COLORS } from "./usePeople"; + +interface Props { + show: boolean; + onClose: () => void; + onSubmit: (name: string, color: string) => void; +} + +export default function AddPersonModal({ show, onClose, onSubmit }: Props) { + const [name, setName] = useState(""); + const [color, setColor] = useState(DEFAULT_COLORS[0]); + + const handleAdd = () => { + if (name.trim()) { + onSubmit(name.trim(), color); + setName(""); + setColor(DEFAULT_COLORS[0]); + onClose(); + } + }; + + return ( + + + Add person + + + + + setName(e.target.value)} + placeholder="Enter name" + style={{ width: "100%", marginBottom: 16 }} + /> + + +
+ {DEFAULT_COLORS.map((c) => ( +
+
+ + + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/PeopleManager/PeopleChips.tsx b/src/components/PeopleManager/PeopleChips.tsx new file mode 100644 index 0000000..d3b28b7 --- /dev/null +++ b/src/components/PeopleManager/PeopleChips.tsx @@ -0,0 +1,34 @@ +import styles from "./styles.module.css"; +import type { Person } from "./usePeople"; + +interface Props { + people: Person[]; + onAddClick: () => void; + onRemovePerson: (id: string) => void; +} + +export default function PeopleChips({ people, onAddClick, onRemovePerson }: Props) { + return ( +
+ {people.map((p) => ( + + ))} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/PeopleManager/index.ts b/src/components/PeopleManager/index.ts new file mode 100644 index 0000000..123cb9c --- /dev/null +++ b/src/components/PeopleManager/index.ts @@ -0,0 +1,3 @@ +export { usePeople } from "./usePeople"; +export { default as PeopleChips } from "./PeopleChips"; +export { default as AddPersonModal } from "./AddPersonModal"; \ No newline at end of file diff --git a/src/components/PeopleManager/styles.module.css b/src/components/PeopleManager/styles.module.css new file mode 100644 index 0000000..0d2c908 --- /dev/null +++ b/src/components/PeopleManager/styles.module.css @@ -0,0 +1,71 @@ +/* Container for chips */ +.chipsRow { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 1.5rem; +} + +/* Person chip */ +.personChip { + position: relative; + padding: 6px 14px; + border-radius: 20px; + border: none; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + color: #fff; + text-transform: capitalize; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.personChip:hover { + opacity: 0.9; +} + +.personChip:hover span { + visibility: hidden; +} + +.personChip::after { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0; +} + +.personChip:hover::after { + content: "✕"; + opacity: 1; +} + + +/* Add person button */ +.addChip { + padding: 8px 14px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + background-color: transparent; + color: var(--primary-pink); + border: 2px solid var(--primary-pink); + transition: background-color 0.2s ease, color 0.2s ease; +} + +.addChip:hover { + background-color: var(--primary-pink); + color: #fff; +} + +.personChip:focus, +.addChip:focus { + outline: 3px solid rgba(198, 0, 90, 0.3); + outline-offset: 2px; +} \ No newline at end of file diff --git a/src/components/PeopleManager/usePeople.ts b/src/components/PeopleManager/usePeople.ts new file mode 100644 index 0000000..420b9b8 --- /dev/null +++ b/src/components/PeopleManager/usePeople.ts @@ -0,0 +1,56 @@ +import { useState } from "react"; + +export type Person = { + id: string; + name: string; + color: string; + total: number; +}; + +export const DEFAULT_COLORS = [ + "#C6005A", + "#1F3B57", + "#2EC4B6", + "#FF9F1C", + "#8AC926", + "#FF595E", + "#6A4C93", + "#6F1D1B", + "#D4A373", + "#6B7280" +]; + +export function usePeople(initial?: Person[]) { + const [people, setPeople] = useState(initial || []); + + const addPerson = (name: string, color: string) => { + const id = + typeof crypto !== "undefined" && "randomUUID" in crypto + ? crypto.randomUUID() + : Date.now().toString(); + + setPeople(prev => [...prev, { id, name, color, total: 0 }]); + }; + + const removePerson = (id: string) => { + setPeople(prev => prev.filter(p => p.id !== id)); + }; + + const updateTotal = (id: string, amount: number) => { + setPeople(prev => + prev.map(p => + p.id === id ? { ...p, total: amount } : p + ) + ); + }; + + const grandTotal = people.reduce((sum, p) => sum + p.total, 0); + + return { + people, + addPerson, + removePerson, + updateTotal, + grandTotal + }; +} \ No newline at end of file diff --git a/src/pages/Review.tsx b/src/pages/Review.tsx index a3695d9..74d8de9 100644 --- a/src/pages/Review.tsx +++ b/src/pages/Review.tsx @@ -1,9 +1,10 @@ -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { useState, useEffect } from "react"; import { parseItems, type Item } from "../utils/parseReceipt"; const Review = () => { const ocrText = useLocation().state?.ocrText || ""; + const navigate = useNavigate(); const [items, setItems] = useState([]); useEffect(() => { @@ -24,6 +25,12 @@ const Review = () => { }); }; + const handleContinue = () => { + // Only continue if there’s at least one item + if (!items.length) return; + navigate("/Split", { state: { items } }); + }; + return (

Review and Edit Items

@@ -52,6 +59,13 @@ const Review = () => { />
))} + ); }; diff --git a/src/pages/Split.module.css b/src/pages/Split.module.css new file mode 100644 index 0000000..cddbcc4 --- /dev/null +++ b/src/pages/Split.module.css @@ -0,0 +1,58 @@ +.desktopPage { + display: flex; + flex-direction: row; + gap: 2rem; + align-items: center; + height: 100%; + width: 100%; + max-width: 1200px; + margin: 0 auto; +} + +.mobilePage { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.itemsContainer { + width: 60%; +} + +.itemsContainer button { + font-weight: 600; + font-size: 1rem; + padding: 0.5rem 1rem; + transition: background-color 0.2s ease; +} + +.resultsContainer { + width: 40%; + background-color: var(--primary-pink); + min-width: 300px; + border-radius: 1rem; + color: white; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1.5rem; + padding: 5%; + + @media (min-width: 768px) { + min-height: 30px; + } +} + +.resultsHeader { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.5rem; + border-bottom: 2px solid rgba(255, 255, 255, 0.2); + padding-bottom: 0.5rem; +} + +.total { + margin-top: 0.5rem; + border-top: 2px solid rgba(255, 255, 255, 0.2); + padding-top: 0.5rem; +} \ No newline at end of file diff --git a/src/pages/Split.tsx b/src/pages/Split.tsx index ef880a2..4c66b1d 100644 --- a/src/pages/Split.tsx +++ b/src/pages/Split.tsx @@ -1,7 +1,58 @@ +import { useState } from "react"; +import { useLocation } from "react-router-dom"; +import type { Item } from "../utils/parseReceipt"; +import useIsMobile from "../hooks/useIsMobile"; +import { usePeople, PeopleChips, AddPersonModal } from "../components/PeopleManager"; + +import styles from './Split.module.css' + const Split = () => { - const text = "hi" + const isMobile = useIsMobile(); + const { state } = useLocation() as { state?: { items?: Item[] } }; + const { people, addPerson, removePerson, grandTotal } = usePeople([]); + const [showModal, setShowModal] = useState(false); + const items = state?.items || []; - return text; + return ( + <> +
+
+ setShowModal(true)} + onRemovePerson={removePerson} + /> + + {items.map((item, i) => ( + + + + + + + ))} +
{item.quantity}{item.name}${item.price.toFixed(2)}
+ setShowModal(false)} + onSubmit={(name, color) => addPerson(name, color)} + /> +
+
+
Results
+ + {people.map((p) => ( + + + + + ))} +
{p.name}:${p.total.toFixed(2)}
+
Total: ${grandTotal.toFixed(2)}
+
+
+ + ); } export default Split; \ No newline at end of file