aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/globals.css2
-rw-r--r--src/app/layout.tsx18
-rw-r--r--src/app/page.tsx2
-rw-r--r--src/components/invoice-generator.tsx68
-rw-r--r--src/components/ui/button.tsx6
-rw-r--r--src/components/ui/input.tsx6
-rw-r--r--src/lib/generate.ts54
-rw-r--r--src/models/invoice.ts6
-rw-r--r--src/utils/cn.ts4
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));