diff options
| -rw-r--r-- | src/app/page.tsx | 9 | ||||
| -rw-r--r-- | src/components/invoice-generator.tsx | 131 | ||||
| -rw-r--r-- | src/components/ui/button.tsx | 13 | ||||
| -rw-r--r-- | src/components/ui/input.tsx | 13 |
4 files changed, 163 insertions, 3 deletions
diff --git a/src/app/page.tsx b/src/app/page.tsx index 75f3c42..c71255f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,10 @@ +import { InvoiceGenerator } from "@/components/invoice-generator"; + export default function Home() { return ( - <div> - <h1>Free Invoice Generator</h1> - </div> + <main className="space-y-4 p-2"> + <h1 className="text-xl">Free Invoice Generator</h1> + <InvoiceGenerator /> + </main> ); } diff --git a/src/components/invoice-generator.tsx b/src/components/invoice-generator.tsx new file mode 100644 index 0000000..d1552c1 --- /dev/null +++ b/src/components/invoice-generator.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { Invoice, InvoiceSchema } from "@/models/invoice"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useFieldArray, useForm, useWatch } from "react-hook-form"; +import { Input } from "./ui/input"; +import React from "react"; +import { cn } from "@/utils/cn"; +import { Button } from "./ui/button"; + +const defaultItem = { + description: "", + quantity: 1, + rate: 0, +}; + +const defaultValues: Invoice = { + invoice_number: 0, + from: "", + line_items: [defaultItem], + currency: "USD", +}; + +export const InvoiceGenerator = () => { + const { + register, + handleSubmit, + control, + watch, + formState: { errors }, + } = useForm<Invoice>({ + resolver: zodResolver(InvoiceSchema), + defaultValues, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "line_items", + }); + const lineItems = useWatch({ control, name: "line_items" }); + const currency = watch("currency", "USD"); + + return ( + <form className="max-w-5xl rounded-sm border p-4 shadow-lg"> + <div className="flex justify-between gap-4"> + <section className="grid auto-rows-min grid-cols-2 gap-2"> + <Input + {...register("from")} + placeholder="Who is this from?" + className="col-span-2" + /> + {errors["from"] && ( + <p className="col-span-2">{errors["from"].message ?? "Error"}</p> + )} + <div className="grid"> + <label>Bill To</label> + <Input {...register("bill_to")} placeholder="Who is this for?" /> + </div> + <div className="grid"> + <label>Ship To</label> + <Input {...register("ship_to")} placeholder="(optional)" /> + </div> + </section> + <section className="grid auto-rows-min grid-cols-2 gap-2"> + <label>Invoice #</label> + <Input {...register("invoice_number", { valueAsNumber: true })} /> + <label>Date</label> + <Input {...register("date")} type="date" /> + <label>Due Date</label> + <Input {...register("due_date")} type="date" /> + <label>PO number</label> + <Input {...register("po_number", { valueAsNumber: true })} /> + </section> + </div> + <h2>Your Items</h2> + <div + className={cn( + "bg-foreground text-background rounded-sm px-2 py-1", + "mb-2 grid grid-cols-11 gap-2", + )} + > + <p className="col-span-7">Item</p> + <p className="pl-2">Quantity</p> + <p className="pl-2">Rate</p> + <p className="text-center">Amount</p> + <p className="pr-2 text-right">Actions</p> + </div> + <section className="grid grid-cols-11 gap-2"> + {fields.map((f, ind) => ( + <React.Fragment key={f.id}> + <Input + {...register(`line_items.${ind}.description`)} + placeholder="Description of item/service..." + className="col-span-7" + /> + <Input + {...register(`line_items.${ind}.quantity`, { + valueAsNumber: true, + })} + /> + <Input + {...register(`line_items.${ind}.rate`, { valueAsNumber: true })} + /> + <p className="m-auto text-center"> + {(lineItems[ind]?.rate ?? 0) * (lineItems[ind]?.quantity ?? 0)}{" "} + {currency} + </p> + <Button + type="button" + onClick={() => remove(ind)} + className="m-auto size-fit" + > + x + </Button> + </React.Fragment> + ))} + <Button type="button" onClick={() => append(defaultItem)}> + + Item + </Button> + </section> + <section className="text-right text-xl"> + Subtotal:{" "} + {lineItems.reduce( + (acc, cur) => acc + (cur.rate ?? 0) * (cur.quantity ?? 0), + 0, + )}{" "} + {currency} + </section> + </form> + ); +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..32b9aac --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/utils/cn"; + +export const Button = ({ + className, + ...props +}: React.ComponentPropsWithRef<"button">) => { + return ( + <button + {...props} + className={cn("rounded-sm border px-2", "cursor-pointer", className)} + /> + ); +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..11bcd12 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/utils/cn"; + +export const Input = ({ + className, + ...props +}: React.ComponentPropsWithRef<"input">) => { + return ( + <input + {...props} + className={cn("rounded-sm border px-2 py-1", className)} + /> + ); +}; |
