build ui for adding and displaying added items
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
export default function App() {
|
||||
return <h1 className="text-3xl font-bold text-blue-600 p-4">Bill Generator</h1>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useBill";
|
||||
@@ -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
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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">;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./calculator";
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
|
||||
Reference in New Issue
Block a user