diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/globals.css | 2 | ||||
| -rw-r--r-- | src/app/layout.tsx | 18 | ||||
| -rw-r--r-- | src/app/page.tsx | 2 | ||||
| -rw-r--r-- | src/components/invoice-generator.tsx | 68 | ||||
| -rw-r--r-- | src/components/ui/button.tsx | 6 | ||||
| -rw-r--r-- | src/components/ui/input.tsx | 6 | ||||
| -rw-r--r-- | src/lib/generate.ts | 54 | ||||
| -rw-r--r-- | src/models/invoice.ts | 6 | ||||
| -rw-r--r-- | src/utils/cn.ts | 4 |
9 files changed, 83 insertions, 83 deletions
diff --git a/src/app/globals.css b/src/app/globals.css index 2a89a48..eb0ee21 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; :root { --background: #ffffff; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 42cf8c3..be69e70 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,20 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import './globals.css'; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Free Invoices", - description: "Generate invoices for free", + title: 'Free Invoices', + description: 'Generate invoices for free', }; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index c71255f..3892b19 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import { InvoiceGenerator } from "@/components/invoice-generator"; +import { InvoiceGenerator } from '@/components/invoice-generator'; export default function Home() { return ( diff --git a/src/components/invoice-generator.tsx b/src/components/invoice-generator.tsx index 47cc458..da97184 100644 --- a/src/components/invoice-generator.tsx +++ b/src/components/invoice-generator.tsx @@ -1,29 +1,29 @@ -"use client"; +'use client'; -import { Invoice, InvoiceSchema } from "@/models/invoice"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { Invoice, InvoiceSchema } from '@/models/invoice'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useFieldArray, useForm, useWatch, SubmitHandler, -} from "react-hook-form"; -import { Input } from "./ui/input"; -import { cn } from "@/utils/cn"; -import { Button } from "./ui/button"; -import { generateInvoice } from "@/lib/generate"; +} from 'react-hook-form'; +import { Input } from './ui/input'; +import { cn } from '@/utils/cn'; +import { Button } from './ui/button'; +import { generateInvoice } from '@/lib/generate'; const defaultItem = { - description: "", + description: '', quantity: 1, rate: 0, }; const defaultValues: Invoice = { invoice_number: 0, - from: "", + from: '', line_items: [defaultItem], - currency: "USD", + currency: 'USD', }; export const InvoiceGenerator = () => { @@ -40,17 +40,17 @@ export const InvoiceGenerator = () => { const { fields, append, remove } = useFieldArray({ control, - name: "line_items", + name: 'line_items', }); - const lineItems = useWatch({ control, name: "line_items" }); - const currency = watch("currency", "USD"); + const lineItems = useWatch({ control, name: 'line_items' }); + const currency = watch('currency', 'USD'); const onSubmit: SubmitHandler<Invoice> = async (data) => { console.log(data); const blob = await generateInvoice(data); - const a = document.createElement("a"); + const a = document.createElement('a'); a.href = URL.createObjectURL(blob); - a.download = "invoice.pdf"; + a.download = 'invoice.pdf'; document.body.appendChild(a); a.click(); @@ -66,17 +66,17 @@ export const InvoiceGenerator = () => { <div className="flex justify-between gap-4"> <section className="grid auto-rows-min gap-2 md:grid-cols-2"> <Input - {...register("from")} + {...register('from')} placeholder="Who is this from?" className="md:col-span-2" /> - {errors["from"] && ( - <p className="md:col-span-2">{errors["from"].message ?? "Error"}</p> + {errors['from'] && ( + <p className="md:col-span-2">{errors['from'].message ?? 'Error'}</p> )} <div className="grid"> <label>Bill To</label> <Input - {...register("bill_to")} + {...register('bill_to')} className="w-full" placeholder="Who is this for?" /> @@ -84,7 +84,7 @@ export const InvoiceGenerator = () => { <div className="grid"> <label>Ship To</label> <Input - {...register("ship_to")} + {...register('ship_to')} className="w-full" placeholder="(optional)" /> @@ -92,20 +92,20 @@ export const InvoiceGenerator = () => { </section> <section className="grid auto-rows-min grid-cols-2 gap-2"> <label>Invoice #</label> - <Input {...register("invoice_number", { valueAsNumber: true })} /> + <Input {...register('invoice_number', { valueAsNumber: true })} /> <label>Date</label> - <Input {...register("date")} type="date" /> + <Input {...register('date')} type="date" /> <label>Due Date</label> - <Input {...register("due_date")} type="date" /> + <Input {...register('due_date')} type="date" /> <label>PO number</label> - <Input {...register("po_number")} /> + <Input {...register('po_number')} /> </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", + '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> @@ -133,14 +133,14 @@ export const InvoiceGenerator = () => { <p> {( (lineItems[ind]?.rate ?? 0) * (lineItems[ind]?.quantity ?? 0) - ).toLocaleString(undefined, { style: "currency", currency })} + ).toLocaleString(undefined, { style: 'currency', currency })} </p> <Button type="button" onClick={() => remove(ind)} className={cn( - "absolute -right-1 m-auto hidden size-fit", - "border-none group-hover:block hover:text-gray-500", + 'absolute -right-1 m-auto hidden size-fit', + 'border-none group-hover:block hover:text-gray-500', )} > X @@ -157,19 +157,19 @@ export const InvoiceGenerator = () => { </Button> </section> <section className="pr-6 text-right text-xl"> - Subtotal:{" "} + Subtotal:{' '} <b> {lineItems .reduce( (acc, cur) => acc + (cur.rate ?? 0) * (cur.quantity ?? 0), 0, ) - .toLocaleString(undefined, { style: "currency", currency })} + .toLocaleString(undefined, { style: 'currency', currency })} </b> </section> - <section className={cn("mt-4 flex flex-row-reverse")}> + <section className={cn('mt-4 flex flex-row-reverse')}> <Button - className={cn("border-green-700 text-green-700", "hover:bg-gray-100")} + className={cn('border-green-700 text-green-700', 'hover:bg-gray-100')} > Download </Button> diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 32b9aac..29768f6 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,13 +1,13 @@ -import { cn } from "@/utils/cn"; +import { cn } from '@/utils/cn'; export const Button = ({ className, ...props -}: React.ComponentPropsWithRef<"button">) => { +}: React.ComponentPropsWithRef<'button'>) => { return ( <button {...props} - className={cn("rounded-sm border px-2", "cursor-pointer", className)} + className={cn('rounded-sm border px-2', 'cursor-pointer', className)} /> ); }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 11bcd12..9966b0b 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,13 +1,13 @@ -import { cn } from "@/utils/cn"; +import { cn } from '@/utils/cn'; export const Input = ({ className, ...props -}: React.ComponentPropsWithRef<"input">) => { +}: React.ComponentPropsWithRef<'input'>) => { return ( <input {...props} - className={cn("rounded-sm border px-2 py-1", className)} + className={cn('rounded-sm border px-2 py-1', className)} /> ); }; diff --git a/src/lib/generate.ts b/src/lib/generate.ts index eebd0cf..8f90425 100644 --- a/src/lib/generate.ts +++ b/src/lib/generate.ts @@ -1,8 +1,8 @@ -"use server"; +'use server'; -import { Invoice } from "@/models/invoice"; -import PDFDocument from "pdfkit"; -import blobStream from "blob-stream"; +import { Invoice } from '@/models/invoice'; +import PDFDocument from 'pdfkit'; +import blobStream from 'blob-stream'; const MARGIN = 30; const T_CELL_PADDING = 30; @@ -19,7 +19,7 @@ const createTableHeader = ( }, ) => { doc.rect(doc.x, doc.y, doc.page.width - MARGIN * 2, T_ROW_HEIGHT).fill(); - doc.fontSize(10).fillColor("white"); + doc.fontSize(10).fillColor('white'); doc.text(params.itemText, MARGIN + T_PADDING, doc.y + 6); doc.moveUp(); doc.text( @@ -48,12 +48,12 @@ const createTableHeader = ( doc.page.width - MARGIN - doc.widthOfString(params.amountText) - T_PADDING, doc.y, ); - doc.fontSize(10).fillColor("black"); - doc.text("", MARGIN + T_PADDING, doc.y + T_ROW_HEIGHT / 2); + doc.fontSize(10).fillColor('black'); + doc.text('', MARGIN + T_PADDING, doc.y + T_ROW_HEIGHT / 2); }; export async function generateInvoice(invoice: Invoice): Promise<Blob> { - const doc = new PDFDocument({ size: "A4", margin: MARGIN }); + const doc = new PDFDocument({ size: 'A4', margin: MARGIN }); const stream = doc.pipe(blobStream()); // Invoice from @@ -64,7 +64,7 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { doc.moveUp(); doc.fontSize(12); - const invNumber = "# " + invoice.invoice_number; + const invNumber = '# ' + invoice.invoice_number; doc.text(invNumber, doc.page.width - MARGIN - doc.widthOfString(invNumber)); if (invoice.date) { @@ -80,33 +80,33 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { // Invoice title doc.fontSize(24); - doc.text("INVOICE", MARGIN, doc.y + 3); + doc.text('INVOICE', MARGIN, doc.y + 3); doc.moveDown(); // Bill to and details if (invoice.bill_to) { doc.fontSize(8); - doc.text("BILL TO"); + doc.text('BILL TO'); doc.text(invoice.bill_to); doc.moveUp(2); - doc.text("", MARGIN + doc.widthOfString("BILL TO") * 3, doc.y); + doc.text('', MARGIN + doc.widthOfString('BILL TO') * 3, doc.y); } if (invoice.po_number) { doc.fontSize(8); - doc.text("DETAILS"); - doc.text("Po number: " + invoice.po_number); - doc.text("", MARGIN, doc.y + 10); + doc.text('DETAILS'); + doc.text('Po number: ' + invoice.po_number); + doc.text('', MARGIN, doc.y + 10); } else { doc.moveDown(2); - doc.text("", MARGIN, doc.y + 10); + doc.text('', MARGIN, doc.y + 10); } const params = { - itemText: "ITEM", - quantityText: "QUANTITY", - rateText: "RATE", - amountText: "AMOUNT", + itemText: 'ITEM', + quantityText: 'QUANTITY', + rateText: 'RATE', + amountText: 'AMOUNT', }; createTableHeader(doc, params); @@ -125,12 +125,12 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { invoice.line_items.forEach((item) => { const amount = (item.quantity * item.rate).toLocaleString(undefined, { - style: "currency", + style: 'currency', currency: invoice.currency, }); const quantity = item.quantity.toString(); const rate = item.rate.toLocaleString(undefined, { - style: "currency", + style: 'currency', currency: invoice.currency, }); doc.text(item.description); @@ -140,7 +140,7 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { doc.text(rate, rateX - doc.widthOfString(rate) / 2, doc.y); doc.moveUp(); doc.text(quantity, quantityX - doc.widthOfString(quantity) / 2, doc.y); - doc.text("", MARGIN + T_PADDING, doc.y + T_ROW_HEIGHT / 2); + doc.text('', MARGIN + T_PADDING, doc.y + T_ROW_HEIGHT / 2); }); const total = invoice.line_items.reduce( @@ -148,9 +148,9 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { 0, ); const totalText = - "Total: " + + 'Total: ' + total.toLocaleString(undefined, { - style: "currency", + style: 'currency', currency: invoice.currency, }); @@ -163,10 +163,10 @@ export async function generateInvoice(invoice: Invoice): Promise<Blob> { doc.end(); return new Promise((resolve, reject) => { - stream.on("finish", () => { + stream.on('finish', () => { const blob = stream.toBlob(); resolve(blob); }); - stream.on("error", reject); + stream.on('error', reject); }); } diff --git a/src/models/invoice.ts b/src/models/invoice.ts index bfe99cf..39d5d67 100644 --- a/src/models/invoice.ts +++ b/src/models/invoice.ts @@ -1,6 +1,6 @@ -import z from "zod"; +import z from 'zod'; -export const CurrencyEnum = z.enum(["USD", "EUR", "RSD"]); +export const CurrencyEnum = z.enum(['USD', 'EUR', 'RSD']); export const LineItemSchema = z.object({ description: z.string(), @@ -9,7 +9,7 @@ export const LineItemSchema = z.object({ }); const optionalDate = z.preprocess((v) => { - if (v === "" || v === null) return undefined; + if (v === '' || v === null) return undefined; if (v instanceof Date && isNaN(v.getTime())) return undefined; return v; }, z.coerce.date().optional()); diff --git a/src/utils/cn.ts b/src/utils/cn.ts index 365058c..9ad0df4 100644 --- a/src/utils/cn.ts +++ b/src/utils/cn.ts @@ -1,5 +1,5 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); |
