diff --git a/apps/website/public/images/update/invoice-pdf/invoice.jpg b/apps/website/public/images/update/invoice-pdf/invoice.jpg new file mode 100644 index 0000000000..a16147c4a3 Binary files /dev/null and b/apps/website/public/images/update/invoice-pdf/invoice.jpg differ diff --git a/apps/website/public/images/update/invoice-pdf/pdf-invoice.jpg b/apps/website/public/images/update/invoice-pdf/pdf-invoice.jpg new file mode 100644 index 0000000000..5549f0961a Binary files /dev/null and b/apps/website/public/images/update/invoice-pdf/pdf-invoice.jpg differ diff --git a/apps/website/public/images/update/invoice-pdf/pdf.jpg b/apps/website/public/images/update/invoice-pdf/pdf.jpg new file mode 100644 index 0000000000..842bc21bf9 Binary files /dev/null and b/apps/website/public/images/update/invoice-pdf/pdf.jpg differ diff --git a/apps/website/public/images/update/invoice-pdf/web-invoice.jpg b/apps/website/public/images/update/invoice-pdf/web-invoice.jpg new file mode 100644 index 0000000000..cb2b0964f8 Binary files /dev/null and b/apps/website/public/images/update/invoice-pdf/web-invoice.jpg differ diff --git a/apps/website/src/app/updates/posts/apps.mdx b/apps/website/src/app/updates/posts/apps.mdx index cb566a4d06..87a910c2b8 100644 --- a/apps/website/src/app/updates/posts/apps.mdx +++ b/apps/website/src/app/updates/posts/apps.mdx @@ -1,6 +1,6 @@ --- title: "Introducing Apps" -publishedAt: "2024-10-29" +publishedAt: "2024-09-29" summary: "We are excited to announce the launch of Apps in the Midday. You can now easily connect your favorite tools to streamline your workflow." image: "/images/update/apps/apps.jpg" tag: "Updates" diff --git a/apps/website/src/app/updates/posts/invoice-pdf.mdx b/apps/website/src/app/updates/posts/invoice-pdf.mdx new file mode 100644 index 0000000000..6a57e954a4 --- /dev/null +++ b/apps/website/src/app/updates/posts/invoice-pdf.mdx @@ -0,0 +1,285 @@ +--- +title: "Invoice - generating PDFs" +publishedAt: "2024-10-23" +summary: "We are excited to announce the launch of Apps in the Midday. You can now easily connect your favorite tools to streamline your workflow." +image: "/images/update/invoice-pdf/pdf.jpg" +tag: "Updates" +--- + +With our upcoming Invoicing feature, we have explored different ways to generate PDF invoices, everything from running Pupper to using a headless browser on Cloudflare, to paid services and generating PDFs using React. + +
+We noticed from the comunity that this is a common question on how to generate PDF invoices, so we decided to share our solution with you. +
+ + +## Invoice in Midday +![PDF Invoices](/images/update/invoice-pdf/invoice.jpg) +
+We are building a new experience for invoices in Midday. You will be able to create and send invoices to your customers, and generate PDFs for each invoice. + +
+ +Our interface is highly customizable with a visual editor where you can easily change the layout, add your logo, and customize the text to your liking. + +
+ +We use an editor based on Tiptap to support rich text, AI genearation for grammar and improvin text with just one click. + +
+ +While the editor saves the content using JSON, we also need a way to make this work with our PDF generation. + +
+ +When you have sent an invoice to a customer, they will recive a email with a unique link to the invoice. When they click on the link, it will render the invoice in a web page where you and the +customer can communicate in realtime using our chat interface. + +
+ +You will also know if the customer has viewed the invoice and if they have any questions about the invoice. + +
+ +## PDF Generation +![PDF Invoices](/images/update/invoice-pdf/pdf-invoice.jpg) +
+ +There are many ways to generate PDFs, and we have looked into many different solutions. We have also looked into paid services, but we wanted to make sure to give you full control over the invoices and not rely on another service. + +
+ +We went with `react-pdf` to generate the PDFs. This is a great library that allows us to generate PDFs using React. We can easily customize the layout and add our own styles to the documents and it feels just like `react-email` concept where we use react to generate our templates. + +
+ +The invoice is then generated and saved to your [Vault](https://midday.ai/vault), so we can match it to incoming transactions and mark it as paid. + +
+ +We first create an API endpoint that will generate the PDF and return the PDF as Content Type `application/pdf`. + +```tsx +import { InvoiceTemplate, renderToStream } from "@midday/invoice"; +import { getInvoiceQuery } from "@midday/supabase/queries"; +import { createClient } from "@midday/supabase/server"; +import type { NextRequest } from "next/server"; + +export const preferredRegion = ["fra1", "sfo1", "iad1"]; +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const supabase = createClient(); + const requestUrl = new URL(req.url); + const id = requestUrl.searchParams.get("id"); + const size = requestUrl.searchParams.get("size") as "letter" | "a4"; + const preview = requestUrl.searchParams.get("preview") === "true"; + + if (!id) { + return new Response("No invoice id provided", { status: 400 }); + } + + const { data } = await getInvoiceQuery(supabase, id); + + if (!data) { + return new Response("Invoice not found", { status: 404 }); + } + + const stream = await renderToStream(await InvoiceTemplate({ ...data, size })); + + const blob = await new Response(stream).blob(); + + const headers: Record = { + "Content-Type": "application/pdf", + "Cache-Control": "no-store, max-age=0", + }; + + if (!preview) { + headers["Content-Disposition"] = + `attachment; filename="${data.invoice_number}.pdf"`; + } + + return new Response(blob, { headers }); +} +``` + +
+ +With this approach we can also add `?preview=true` to the URL to generate the PDF in the browser without downloading it. This is useful for previewing the invoice before generating the PDF. + +## React PDF Invoice Template + +And here is the template for the invoice, we register a custom font, generate a QR code and making sections and formatting the invoice. + +
+ +You can find the full code for the invoice template [here](https://go.midday.ai/inv). + + + +```tsx +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"; + +const CDN_URL = "https://cdn.midday.ai"; + +Font.register({ + family: "GeistMono", + fonts: [ + { + src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Regular.ttf`, + fontWeight: 400, + }, + { + src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Medium.ttf`, + fontWeight: 500, + }, + ], +}); + +export async function InvoiceTemplate({ + invoice_number, + issue_date, + due_date, + template, + line_items, + customer_details, + from_details, + payment_details, + note_details, + currency, + vat, + tax, + amount, + size = "letter", + link, +}: Props) { + const qrCode = await QRCodeUtil.toDataURL( + link, + { + width: 40 * 3, + height: 40 * 3, + margin: 0, + }, + ); + + return ( + + + + {template?.logo_url && ( + + )} + + + + + + + + + {template.from_label} + + + + + + + + + {template.customer_label} + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} +``` + +
+## What's next? +![PDF Invoices](/images/update/invoice-pdf/web-invoice.jpg) +
+ +We will be launching the Invoicing feature soon to our early access users, let us know if you want to be included in the early access program to get all the new features as soon as they are ready. + +
+ +We would love to hear what you think about the Invoicing feature, and we would love to hear from you if you have any ideas or feedback for the feature. + +
+ +[Sign up for an account](https://app.midday.ai) and start using Midday today. diff --git a/apps/website/src/app/updates/posts/slack-assistant.mdx b/apps/website/src/app/updates/posts/slack-assistant.mdx index af2a30aa52..8ae214b224 100644 --- a/apps/website/src/app/updates/posts/slack-assistant.mdx +++ b/apps/website/src/app/updates/posts/slack-assistant.mdx @@ -1,6 +1,6 @@ --- title: "Building the Midday Slack Assistant" -publishedAt: "2024-10-29" +publishedAt: "2024-09-29" summary: "This is a technical deep dive into how we built the Midday Slack Assistant using Vercel AI SDK, Trigger.dev and Supabase." image: "/images/update/apps/slack.png" tag: "Updates" diff --git a/apps/website/src/components/mdx.tsx b/apps/website/src/components/mdx.tsx index cf3dc65f9f..ee282a6d03 100644 --- a/apps/website/src/components/mdx.tsx +++ b/apps/website/src/components/mdx.tsx @@ -13,13 +13,13 @@ interface TableProps { function Table({ data }: TableProps) { const headers = data.headers.map((header, index) => ( - {header} + {header} )); const rows = data.rows.map((row, rowIndex) => ( - + {row.map((cell, cellIndex) => ( - {cell} + {cell} ))} )); @@ -49,10 +49,10 @@ function CustomLink({ href, ...props }: CustomLinkProps) { } if (href.startsWith("#")) { - return ; + return ; } - return ; + return ; } interface RoundedImageProps extends React.ComponentProps { @@ -65,7 +65,6 @@ function RoundedImage(props: RoundedImageProps) { interface CodeProps { children: string; - [key: string]: any; } function Code({ children, ...props }: CodeProps) { @@ -77,11 +76,11 @@ function slugify(str: string): string { return str .toString() .toLowerCase() - .trim() // Remove whitespace from both ends of a string - .replace(/\s+/g, "-") // Replace spaces with - - .replace(/&/g, "-and-") // Replace & with 'and' - .replace(/[^\w\-]+/g, "") // Remove all non-word characters except for - - .replace(/\-\-+/g, "-"); // Replace multiple - with single - + .trim() + .replace(/\s+/g, "-") + .replace(/&/g, "-and-") + .replace(/[^\w\-]+/g, "") + .replace(/\-\-+/g, "-"); } function createHeading(level: number) { @@ -130,8 +129,7 @@ const components = { }; interface CustomMDXProps { - components?: Record>; - [key: string]: any; + components?: Record>; } export function CustomMDX(props: CustomMDXProps) {