diff --git a/apps/dashboard/src/app/[locale]/(public)/i/[token]/page.tsx b/apps/dashboard/src/app/[locale]/(public)/i/[token]/page.tsx index 5ded860d9..1df4b2bf0 100644 --- a/apps/dashboard/src/app/[locale]/(public)/i/[token]/page.tsx +++ b/apps/dashboard/src/app/[locale]/(public)/i/[token]/page.tsx @@ -1,3 +1,4 @@ +import { HtmlTemplate } from "@midday/invoice/templates/html"; import { verify } from "@midday/invoice/token"; import { getInvoiceQuery } from "@midday/supabase/queries"; import { createClient } from "@midday/supabase/server"; @@ -47,14 +48,17 @@ export default async function Page({ params }: { params: { token: string } }) { try { const { id } = await verify(params.token); - console.log("katt", id); const { data: invoice } = await getInvoiceQuery(supabase, id); if (!invoice) { notFound(); } - return
Invoice: {invoice.invoice_number}
; + return ( +
+ +
+ ); } catch (error) { notFound(); } diff --git a/apps/dashboard/src/styles/globals.css b/apps/dashboard/src/styles/globals.css index 16552eeed..e5495f94d 100644 --- a/apps/dashboard/src/styles/globals.css +++ b/apps/dashboard/src/styles/globals.css @@ -245,3 +245,12 @@ input[type="time"]::-webkit-calendar-picker-indicator { pointer-events: none; height: 0; } + +.dotted-bg { + background-image: radial-gradient( + circle at 1px 1px, + #232323 0.5px, + transparent 0 + ); + background-size: 6px 6px; +} diff --git a/bun.lockb b/bun.lockb index 2c40a6cf1..635a372dc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/invoice/package.json b/packages/invoice/package.json index fc4591e16..2612ec1e6 100644 --- a/packages/invoice/package.json +++ b/packages/invoice/package.json @@ -11,9 +11,13 @@ "exports": { ".": "./src/index.tsx", "./token": "./src/token/index.ts", - "./number": "./src/utils/number.ts" + "./number": "./src/utils/number.ts", + "./templates/html": "./src/templates/html/index.tsx", + "./templates/pdf": "./src/templates/pdf/index.tsx", + "./editor": "./src/editor/index.tsx" }, "dependencies": { + "@midday/ui": "workspace:*", "@react-pdf/renderer": "^4.0.0", "date-fns": "^4.1.0", "jose": "^5.9.6", diff --git a/packages/invoice/src/editor/index.tsx b/packages/invoice/src/editor/index.tsx new file mode 100644 index 000000000..605cf1b39 --- /dev/null +++ b/packages/invoice/src/editor/index.tsx @@ -0,0 +1,3 @@ +export function Editor() { + return null; +} diff --git a/packages/invoice/src/index.tsx b/packages/invoice/src/index.tsx index 131b7bb7d..e98f01334 100644 --- a/packages/invoice/src/index.tsx +++ b/packages/invoice/src/index.tsx @@ -1,2 +1,5 @@ -export * from "./template/invoice"; +export * from "./templates/html"; +export * from "./templates/pdf"; +export * from "./editor"; + export { renderToStream } from "@react-pdf/renderer"; diff --git a/packages/invoice/src/templates/html/components/editor-content.tsx b/packages/invoice/src/templates/html/components/editor-content.tsx new file mode 100644 index 000000000..7ed6d0dc5 --- /dev/null +++ b/packages/invoice/src/templates/html/components/editor-content.tsx @@ -0,0 +1,13 @@ +import { formatEditorToHtml } from "../../../utils/format"; + +export function EditorContent({ content }: { content?: JSON }) { + if (!content) { + return null; + } + + return ( +
+ {formatEditorToHtml(content)} +
+ ); +} diff --git a/packages/invoice/src/templates/html/components/logo.tsx b/packages/invoice/src/templates/html/components/logo.tsx new file mode 100644 index 000000000..f998634c5 --- /dev/null +++ b/packages/invoice/src/templates/html/components/logo.tsx @@ -0,0 +1,10 @@ +import Image from "next/image"; + +type Props = { + logo: string; + customerName: string; +}; + +export function Logo({ logo, customerName }: Props) { + return {customerName}; +} diff --git a/packages/invoice/src/templates/html/components/meta.tsx b/packages/invoice/src/templates/html/components/meta.tsx new file mode 100644 index 000000000..9821318ca --- /dev/null +++ b/packages/invoice/src/templates/html/components/meta.tsx @@ -0,0 +1,55 @@ +import type { Template } from "@midday/invoice/types"; +import { format } from "date-fns"; + +type Props = { + template: Template; + invoiceNumber: string; + issueDate: string; + dueDate: string; +}; + +export function Meta({ template, invoiceNumber, issueDate, dueDate }: Props) { + return ( +
+
+
+ + {template.invoice_no_label}: + + + {invoiceNumber} + +
+
+ +
+
+
+
+ + {template.issue_date_label}: + + + {format(new Date(issueDate), template.date_format)} + +
+
+
+
+
+
+
+
+ + {template.due_date_label}: + + + {format(new Date(dueDate), template.date_format)} + +
+
+
+
+
+ ); +} diff --git a/packages/invoice/src/templates/html/index.tsx b/packages/invoice/src/templates/html/index.tsx new file mode 100644 index 000000000..35fe53154 --- /dev/null +++ b/packages/invoice/src/templates/html/index.tsx @@ -0,0 +1,68 @@ +import { ScrollArea } from "@midday/ui/scroll-area"; +import type { TemplateProps } from "../types"; +import { CustomerDetails } from "./components/customer-details"; +import { EditorContent } from "./components/editor-content"; +import { FromDetails } from "./components/from-details"; +import { Logo } from "./components/logo"; +import { Meta } from "./components/meta"; + +export function HtmlTemplate({ + invoice_number, + issue_date, + due_date, + template, + line_items, + customer_details, + from_details, + payment_details, + note_details, + currency, + customer_name, + vat, + tax, + amount, + size = "a4", +}: TemplateProps) { + const width = size === "letter" ? 816 : 595; + const height = size === "letter" ? 1056 : 842; + + return ( + +
+
+ {template.logo_url && ( + + )} +
+ +
+ +
+ +
+
+

+ {template.from_label} +

+ +
+
+

+ {template.customer_label} +

+ +
+
+
+
+ ); +} diff --git a/packages/invoice/src/components/editor-content.tsx b/packages/invoice/src/templates/pdf/components/editor-content.tsx similarity index 52% rename from packages/invoice/src/components/editor-content.tsx rename to packages/invoice/src/templates/pdf/components/editor-content.tsx index d98f0dc6c..004624fcb 100644 --- a/packages/invoice/src/components/editor-content.tsx +++ b/packages/invoice/src/templates/pdf/components/editor-content.tsx @@ -1,10 +1,10 @@ import { View } from "@react-pdf/renderer"; -import { formatEditorContent } from "../utils/format"; +import { formatEditorToPdf } from "../../../utils/format"; export function EditorContent({ content }: { content?: JSON }) { if (!content) { return null; } - return {formatEditorContent(content)}; + return {formatEditorToPdf(content)}; } diff --git a/packages/invoice/src/components/line-items.tsx b/packages/invoice/src/templates/pdf/components/line-items.tsx similarity index 100% rename from packages/invoice/src/components/line-items.tsx rename to packages/invoice/src/templates/pdf/components/line-items.tsx diff --git a/packages/invoice/src/components/meta.tsx b/packages/invoice/src/templates/pdf/components/meta.tsx similarity index 100% rename from packages/invoice/src/components/meta.tsx rename to packages/invoice/src/templates/pdf/components/meta.tsx diff --git a/packages/invoice/src/components/note.tsx b/packages/invoice/src/templates/pdf/components/note.tsx similarity index 100% rename from packages/invoice/src/components/note.tsx rename to packages/invoice/src/templates/pdf/components/note.tsx diff --git a/packages/invoice/src/components/payment-details.tsx b/packages/invoice/src/templates/pdf/components/payment-details.tsx similarity index 100% rename from packages/invoice/src/components/payment-details.tsx rename to packages/invoice/src/templates/pdf/components/payment-details.tsx diff --git a/packages/invoice/src/components/qr-code.tsx b/packages/invoice/src/templates/pdf/components/qr-code.tsx similarity index 100% rename from packages/invoice/src/components/qr-code.tsx rename to packages/invoice/src/templates/pdf/components/qr-code.tsx diff --git a/packages/invoice/src/components/summary.tsx b/packages/invoice/src/templates/pdf/components/summary.tsx similarity index 100% rename from packages/invoice/src/components/summary.tsx rename to packages/invoice/src/templates/pdf/components/summary.tsx diff --git a/packages/invoice/src/template/invoice.tsx b/packages/invoice/src/templates/pdf/index.tsx similarity index 72% rename from packages/invoice/src/template/invoice.tsx rename to packages/invoice/src/templates/pdf/index.tsx index cfd2c9de7..48df26ad4 100644 --- a/packages/invoice/src/template/invoice.tsx +++ b/packages/invoice/src/templates/pdf/index.tsx @@ -1,12 +1,13 @@ import { Document, Font, Image, Page, Text, View } from "@react-pdf/renderer"; import QRCodeUtil from "qrcode"; -import { EditorContent } from "../components/editor-content"; -import { LineItems } from "../components/line-items"; -import { Meta } from "../components/meta"; -import { Note } from "../components/note"; -import { PaymentDetails } from "../components/payment-details"; -import { QRCode } from "../components/qr-code"; -import { Summary } from "../components/summary"; +import type { TemplateProps } from "../types"; +import { EditorContent } from "./components/editor-content"; +import { LineItems } from "./components/line-items"; +import { Meta } from "./components/meta"; +import { Note } from "./components/note"; +import { PaymentDetails } from "./components/payment-details"; +import { QRCode } from "./components/qr-code"; +import { Summary } from "./components/summary"; const CDN_URL = "https://cdn.midday.ai"; @@ -24,51 +25,7 @@ Font.register({ ], }); -export type Template = { - logo_url?: string; - from_label: string; - customer_label: string; - invoice_no_label: string; - issue_date_label: string; - due_date_label: string; - date_format: string; - payment_label: string; - note_label: string; - description_label: string; - quantity_label: string; - price_label: string; - total_label: string; - tax_label: string; - vat_label: string; -}; - -export type LineItem = { - name: string; - quantity: number; - price: number; - invoice_number?: string; - issue_date?: string; - due_date?: string; -}; - -type Props = { - invoice_number: string; - issue_date: string; - due_date: string; - template: Template; - line_items: LineItem[]; - customer_details?: JSON; - payment_details?: JSON; - from_details?: JSON; - note_details?: JSON; - currency: string; - amount: number; - vat?: number; - tax?: number; - size?: "letter" | "a4"; -}; - -export async function InvoiceTemplate({ +export async function PdfTemplate({ invoice_number, issue_date, due_date, @@ -82,8 +39,8 @@ export async function InvoiceTemplate({ vat, tax, amount, - size = "letter", -}: Props) { + size = "a4", +}: TemplateProps) { const qrCode = await QRCodeUtil.toDataURL( "https://www.youtube.com/watch?v=dQw4w9WgXcQ", { diff --git a/packages/invoice/src/templates/types.ts b/packages/invoice/src/templates/types.ts new file mode 100644 index 000000000..2f8d958e5 --- /dev/null +++ b/packages/invoice/src/templates/types.ts @@ -0,0 +1,44 @@ +export type Template = { + logo_url?: string; + from_label: string; + customer_label: string; + invoice_no_label: string; + issue_date_label: string; + due_date_label: string; + date_format: string; + payment_label: string; + note_label: string; + description_label: string; + quantity_label: string; + price_label: string; + total_label: string; + tax_label: string; + vat_label: string; +}; + +export type LineItem = { + name: string; + quantity: number; + price: number; + invoice_number?: string; + issue_date?: string; + due_date?: string; +}; + +export type TemplateProps = { + invoice_number: string; + issue_date: string; + due_date: string; + template: Template; + line_items: LineItem[]; + customer_details?: JSON; + payment_details?: JSON; + from_details?: JSON; + note_details?: JSON; + currency: string; + amount: number; + customer_name?: string; + vat?: number; + tax?: number; + size?: "letter" | "a4"; +}; diff --git a/packages/invoice/src/utils/format.tsx b/packages/invoice/src/utils/format.tsx index 93f24f789..4d2f47e77 100644 --- a/packages/invoice/src/utils/format.tsx +++ b/packages/invoice/src/utils/format.tsx @@ -31,7 +31,7 @@ interface TextStyle { textDecoration?: string; } -export function formatEditorContent(doc?: EditorDoc): JSX.Element | null { +export function formatEditorToPdf(doc?: EditorDoc): JSX.Element | null { if (!doc || !doc.content) { return null; } @@ -112,6 +112,77 @@ export function formatEditorContent(doc?: EditorDoc): JSX.Element | null { ); } +export function formatEditorToHtml(doc?: EditorDoc): JSX.Element | null { + if (!doc || !doc.content) { + return null; + } + + return ( + <> + {doc.content.map((node, nodeIndex) => { + if (node.type === "paragraph") { + return ( +

+ {node.content?.map((inlineContent, inlineIndex) => { + if (inlineContent.type === "text") { + let style = "text-xs"; + let href: string | undefined; + + if (inlineContent.marks) { + for (const mark of inlineContent.marks) { + if (mark.type === "bold") { + style += " font-medium"; + } else if (mark.type === "italic") { + style += " italic"; + } else if (mark.type === "link") { + href = mark.attrs?.href; + style += " underline"; + } + } + } + + const content = inlineContent.text || ""; + const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content); + + if (href || isEmail) { + const linkHref = + href || (isEmail ? `mailto:${content}` : content); + return ( + + {content} + + ); + } + + return ( + + {content} + + ); + } + if (inlineContent.type === "hardBreak") { + return ( +
+ ); + } + return null; + })} +

+ ); + } + return null; + })} + + ); +} + type FormatAmountParams = { currency: string; amount: number;