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 ;
+}
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;