Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions app/api/shopping-list/[id]/pdf/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { PDFDocument, rgb } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import { promises as fs } from "fs";

type Ctx = {
params: Promise<{ id: string }>;
};

type ShoppingItem = { name: string; amount: number };

function formatDate(d: Date) {
return d.toISOString().slice(0, 10);
}

export async function GET(_req: NextRequest, context: Ctx) {
const session = await getSession();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { id } = await context.params;
const listId = Number(id);

if (!Number.isFinite(listId)) {
return NextResponse.json({ error: "Invalid id" }, { status: 400 });
}

const list = await prisma.shoppingList.findUnique({
where: { id: listId },
});

if (!list || list.userId !== session.user.id) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}

const items = (list.items as ShoppingItem[]) || [];

const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);

const fontBytes = await fs.readFile(
process.cwd() + "/public/fonts/Roboto-Regular.ttf",
);
const page = pdfDoc.addPage();
const { width } = page.getSize();

const font = await pdfDoc.embedFont(fontBytes);
const boldFont = font;

let y = page.getSize().height - 72;

const title = "Lista zakupów";
const titleSize = 20;
const titleWidth = boldFont.widthOfTextAtSize(title, titleSize);
page.drawText(title, {
x: (width - titleWidth) / 2,
y,
size: titleSize,
font: boldFont,
});

y -= 30;

const dateText = `Zakres: ${formatDate(list.startDate)} - ${formatDate(
list.endDate,
)}`;
const dateSize = 12;
const dateWidth = font.widthOfTextAtSize(dateText, dateSize);
page.drawText(dateText, {
x: (width - dateWidth) / 2,
y,
size: dateSize,
font,
});

y -= 40;

if (items.length === 0) {
page.drawText("Brak produktów na liście.", {
x: 72,
y,
size: 12,
font,
});
} else {
// header
const nameX = 72;
const amountX = width - 72 - 80;

page.drawText("Produkt", {
x: nameX,
y,
size: 12,
font: boldFont,
});

page.drawText("Ilość [g]", {
x: amountX + 40,
y,
size: 12,
font: boldFont,
});

y -= 18;

page.drawLine({
start: { x: nameX, y: y + 4 },
end: { x: width - 72, y: y + 4 },
thickness: 0.5,
});

y -= 14;

const lineHeight = 16;
let rowIndex = 0;
for (const item of items) {
const isEvenRow = rowIndex % 2 === 0;
const textColor = isEvenRow ? rgb(0, 0, 0) : rgb(0.4, 0.4, 0.4);

if (y < 72) {
const newPage = pdfDoc.addPage();
y = newPage.getSize().height - 72;

page.drawText("Produkt", {
x: nameX,
y,
size: 12,
font: boldFont,
color: rgb(0, 0, 0),
});
page.drawText("Ilość [g]", {
x: amountX,
y,
size: 12,
font: boldFont,
color: rgb(0, 0, 0),
});
y -= 24;
}

page.drawText(item.name, {
x: nameX,
y,
size: 12,
font,
color: textColor,
});

const amountText = String(item.amount);
const amountWidth = font.widthOfTextAtSize(amountText, 12);
page.drawText(amountText, {
x: amountX + 80 - amountWidth,
y,
size: 12,
font,
color: textColor,
});

y -= lineHeight;
rowIndex += 1;
}
}

const pdfBytes = await pdfDoc.save();

return new NextResponse(Buffer.from(pdfBytes), {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="shopping-list-${listId}.pdf"`,
},
});
}
21 changes: 15 additions & 6 deletions app/shopping-list/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,21 @@ export default async function ShoppingListDetailPage({ params }: Props) {
</p>
</div>

<Link
href="/shopping-list"
className="rounded-xl border border-purple-300 px-3 py-1 text-sm text-purple-200 hover:bg-purple-100/10 transition"
>
← Wróć
</Link>
<div className="flex flex-col items-end gap-2">
<Link
href="/shopping-list"
className="rounded-xl border border-purple-300 px-3 py-1 text-sm text-purple-200 hover:bg-purple-100/10 transition"
>
← Wróć
</Link>

<a
href={`/api/shopping-list/${listId}/pdf`}
className="rounded-xl border border-zinc-700 px-3 py-1 text-sm text-zinc-100 hover:bg-zinc-800/60 transition"
>
Pobierz PDF
</a>
</div>
</div>

<div className="rounded-2xl border border-zinc-800 bg-zinc-900/60 p-6">
Expand Down
47 changes: 47 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"lucide-react": "^0.556.0",
"next": "^16.0.10",
"next-auth": "^4.24.13",
"pdf-lib": "^1.17.1",
"@pdf-lib/fontkit": "^1.1.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"strftime": "^0.10.3",
Expand Down
Binary file added public/fonts/Roboto-Regular.ttf
Binary file not shown.