build ui for adding and displaying added items

This commit is contained in:
2026-05-06 23:41:25 +05:30
parent 386ebb5a36
commit b41e91d326
12 changed files with 358 additions and 11 deletions
-3
View File
@@ -1,3 +0,0 @@
export default function App() {
return <h1 className="text-3xl font-bold text-blue-600 p-4">Bill Generator</h1>
}
+46
View File
@@ -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 (
<div className="min-h-screen bg-gray-50">
<div className="max-w-md mx-auto">
<div className="px-4 py-4 flex flex-col gap-4">
<ItemForm currency={bill.currency} onAdd={addItem} />
{computedBill.items.length > 0 && (
<div className="bg-white rounded-2xl px-4 shadow-sm">
{computedBill.items.map((item) => (
<BillItemRow
key={item.id}
currency={bill.currency}
item={item}
onRemove={removeItem}
/>
))}
</div>
)}
{computedBill.items.length > 0 && (
<button
className="w-full border border-gray-200 text-gray-400 hover:text-red-400 hover:border-red-200 py-3 rounded-xl text-sm transition-all cursor-pointer"
onClick={resetBill}
>
Clear bill
</button>
)}
</div>
</div>
</div>
);
}
+35
View File
@@ -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 (
<div className="flex items-center justify-between border-b border-gray-100 last:border-0 py-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">
{item.name}
</p>
<p className="text-xs text-gray-400 mt-0.5">
{item.quantity} {formatCurrency(item.unitPrice, currency)}
</p>
</div>
<div className="flex items-center gap-3 ml-3">
<span className="text-sm font-semibold text-gray-800">
{formatCurrency(item.finalPrice, currency)}
</span>
<button
className="text-gray-300 transition-colors hover:text-red-400 text-large leading-none"
onClick={() => onRemove(item.id)}
>
</button>
</div>
</div>
);
}
+91
View File
@@ -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<BillItemForm>(emptyForm);
const [error, setError] = useState<string | null>(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 (
<div className="bg-white rounded-2xl p-4 shadow-sm">
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
Add Item
</h2>
<div className="flex flex-col gap-3">
<input
type="text"
placeholder="Item name"
value={form.name}
onChange={(e) =>
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"
/>
<div className="flex gap-2">
<input
type="number"
placeholder="Qty"
value={form.quantity}
min={1}
onChange={(e) =>
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"
/>
<input
type="number"
placeholder={`Unit price (${currency})`}
value={form.unitPrice}
min={0}
onChange={(e) =>
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"
/>
</div>
{error && <p className="text-red-500 text-xs px-1">{error}</p>}
<button
onClick={handleSubmit}
className="w-full bg-indigo-500 hover:bg-indigo-600 active:scale-95 text-white font-semibold py-3 rounded-xl text-sm transition-all"
>
+ Add Item
</button>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export * from "./useBill";
+78
View File
@@ -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<Bill>({
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,
};
}
+6 -6
View File
@@ -1,10 +1,10 @@
import { StrictMode } from 'react' import { StrictMode } from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import './index.css' import "./index.css";
import App from './App.tsx' import App from "./components/App";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode>, </StrictMode>,
) );
+51
View File
@@ -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<BillItem, "id">;
+47
View File
@@ -0,0 +1,47 @@
import type { Bill, BillItem, ComputedBill, ComputedBillItem } from "../types";
export function findById<T extends { id: string }>(
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);
}
+1
View File
@@ -0,0 +1 @@
export * from "./calculator";
+1 -1
View File
@@ -18,7 +18,7 @@
/* Linting */ /* Linting */
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"] "include": ["src"]
+1 -1
View File
@@ -17,7 +17,7 @@
/* Linting */ /* Linting */
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]