diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index b422423..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function App() { - return

Bill Generator

-} \ No newline at end of file diff --git a/src/components/App.tsx b/src/components/App.tsx new file mode 100644 index 0000000..8384a30 --- /dev/null +++ b/src/components/App.tsx @@ -0,0 +1,46 @@ +import { useBill } from "../hooks"; +import { BillItemRow } from "./BillItemRow"; +import { ItemForm } from "./ItemForm"; + +export default function App() { + const { + bill, + computedBill, + updateTitle, + updateCurrency, + updateTax, + addItem, + removeItem, + resetBill, + } = useBill(); + return ( +
+
+
+ + {computedBill.items.length > 0 && ( +
+ {computedBill.items.map((item) => ( + + ))} +
+ )} + + {computedBill.items.length > 0 && ( + + )} +
+
+
+ ); +} diff --git a/src/components/BillItemRow.tsx b/src/components/BillItemRow.tsx new file mode 100644 index 0000000..e79495c --- /dev/null +++ b/src/components/BillItemRow.tsx @@ -0,0 +1,35 @@ +import type { ComputedBillItem } from "../types"; +import { formatCurrency } from "../utils"; + +interface BillItemRowProps { + item: ComputedBillItem; + currency: string; + onRemove: (id: string) => void; +} + +export function BillItemRow({ item, currency, onRemove }: BillItemRowProps) { + return ( +
+
+

+ {item.name} +

+

+ {item.quantity} ☓ {formatCurrency(item.unitPrice, currency)} +

+
+ +
+ + {formatCurrency(item.finalPrice, currency)} + + +
+
+ ); +} diff --git a/src/components/ItemForm.tsx b/src/components/ItemForm.tsx new file mode 100644 index 0000000..df34e55 --- /dev/null +++ b/src/components/ItemForm.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import type { BillItemForm } from "../types"; + +interface ItemFormProps { + currency: string; + onAdd: (form: BillItemForm) => void; +} + +const emptyForm: BillItemForm = { + name: "", + quantity: 1, + unitPrice: 0, +}; + +export function ItemForm({ currency, onAdd }: ItemFormProps) { + const [form, setForm] = useState(emptyForm); + const [error, setError] = useState(null); + + function handleSubmit() { + if (!form.name.trim()) { + setError("Item name is required"); + return; + } + if (form.quantity <= 0) { + setError("Quantity must be greater than 0"); + return; + } + if (form.unitPrice <= 0) { + setError("Price must be greater than 0"); + return; + } + onAdd(form); + setForm(emptyForm); + setError(null); + } + + return ( +
+

+ Add Item +

+ +
+ + setForm((prev) => ({ ...prev, name: e.target.value })) + } + className="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + +
+ + setForm((prev) => ({ ...prev, quantity: Number(e.target.value) })) + } + className="w-1/3 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + + setForm((prev) => ({ + ...prev, + unitPrice: Number(e.target.value), + })) + } + className="w-2/3 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> +
+ + {error &&

{error}

} + + +
+
+ ); +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..7252359 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useBill"; diff --git a/src/hooks/useBill.ts b/src/hooks/useBill.ts new file mode 100644 index 0000000..6f7a746 --- /dev/null +++ b/src/hooks/useBill.ts @@ -0,0 +1,78 @@ +import { useCallback, useState } from "react"; +import { Currency, TaxType } from "../types"; +import type { Bill, BillItem, BillItemForm, ComputedBill } from "../types"; +import { computeBill, generateId } from "../utils"; + +export function useBill() { + const [bill, setBill] = useState({ + id: generateId(), + title: "New Bill", + currency: Currency.INR, + createdAt: new Date(), + taxPercent: 18, + taxType: TaxType.GST, + items: [], + }); + + const computedBill: ComputedBill = computeBill(bill); + + const updateTitle = useCallback((title: string) => { + setBill((prev) => ({ ...prev, title })); + }, []); + + const updateCurrency = useCallback((currency: Currency) => { + setBill((prev) => ({ ...prev, currency })); + }, []); + + const updateTax = useCallback((taxType: TaxType, taxPercent: number) => { + setBill((prev) => ({ ...prev, taxType, taxPercent })); + }, []); + + const addItem = useCallback((form: BillItemForm) => { + const newItem: BillItem = { + ...form, + id: generateId(), + }; + setBill((prev) => ({ ...prev, items: [...prev.items, newItem] })); + }, []); + + const updateItem = useCallback((id: string, form: BillItemForm) => { + setBill((prev) => ({ + ...prev, + items: prev.items.map((item) => + item.id === id ? { ...item, ...form } : item, + ), + })); + }, []); + + const removeItem = useCallback((id: string) => { + setBill((prev) => ({ + ...prev, + items: prev.items.filter((item) => item.id !== id), + })); + }, []); + + const resetBill = useCallback(() => { + setBill({ + id: generateId(), + title: "New Bill", + currency: Currency.INR, + createdAt: new Date(), + taxPercent: 18, + taxType: TaxType.GST, + items: [], + }); + }, []); + + return { + bill, + computedBill, + updateTitle, + updateCurrency, + updateTax, + addItem, + updateItem, + removeItem, + resetBill, + }; +} diff --git a/src/main.tsx b/src/main.tsx index bef5202..b64f912 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./components/App"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..ec5f58d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,51 @@ +export interface BillItem { + id: string; + name: string; + quantity: number; + unitPrice: number; +} + +export enum Currency { + INR = "INR", + USD = "USD", +} + +export enum TaxType { + None = "NONE", + GST = "GST", + VAT = "VAT", +} + +export interface Bill { + id: string; + title: string; + currency: Currency; + taxType: TaxType; + taxPercent: number; + items: BillItem[]; + createdAt: Date; +} + +export type ComputedBillItem = BillItem & { + finalPrice: number; +}; + +// export interface ComputedBillItem extends BillItem { +// finalPrice: number; +// } + +// export type ComputedBill = Bill & { +// subtotal: number; +// taxAmount: number; +// total: number; +// items: ComputedBillItem[]; +// }; + +export interface ComputedBill extends Bill { + subtotal: number; + taxAmount: number; + total: number; + items: ComputedBillItem[]; +} + +export type BillItemForm = Omit; diff --git a/src/utils/calculator.ts b/src/utils/calculator.ts new file mode 100644 index 0000000..db7896c --- /dev/null +++ b/src/utils/calculator.ts @@ -0,0 +1,47 @@ +import type { Bill, BillItem, ComputedBill, ComputedBillItem } from "../types"; + +export function findById( + items: T[], + id: string, +): T | undefined { + return items.find((item) => item.id === id); +} + +export function computeItem(item: BillItem): ComputedBillItem { + return { + ...item, + finalPrice: item.unitPrice * item.quantity, + }; +} + +export function computeBill(bill: Bill): ComputedBill { + const computedItems = bill.items.map(computeItem); + + const subtotal = computedItems.reduce( + (sum, item) => sum + item.finalPrice, + 0, + ); + + const taxAmount = (subtotal * bill.taxPercent) / 100; + const total = subtotal + taxAmount; + + return { + ...bill, + items: computedItems, + subtotal, + taxAmount, + total, + }; +} + +export function formatCurrency(amount: number, currency: string): string { + return new Intl.NumberFormat("en-IN", { + style: "currency", + currency, + minimumFractionDigits: 2, + }).format(amount); +} + +export function generateId(): string { + return Math.random().toString(36).slice(2, 9); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..a647953 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./calculator"; diff --git a/tsconfig.app.json b/tsconfig.app.json index 7f42e5f..dda89ed 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,7 +18,7 @@ /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true }, "include": ["src"] diff --git a/tsconfig.node.json b/tsconfig.node.json index d3c52ea..4ae4fc1 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -17,7 +17,7 @@ /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"]