Add PDF download functionality for stories with images

This commit is contained in:
Riccardo Giorato
2025-12-27 12:08:52 +01:00
parent ad4ee5c737
commit 8730b1053b
5 changed files with 333 additions and 27 deletions
+75
View File
@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
import { getStoryWithPagesBySlug } from "@/lib/db-actions";
import { jsPDF } from "jspdf";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const storySlug = searchParams.get("storySlug");
if (!storySlug) {
return NextResponse.json({ error: "Story slug required" }, { status: 400 });
}
const result = await getStoryWithPagesBySlug(storySlug);
if (!result) {
return NextResponse.json({ error: "Story not found" }, { status: 404 });
}
const { story, pages } = result;
const images = pages
.map((page: any) => page.generatedImageUrl)
.filter((url: string) => url && url !== "/placeholder.svg");
if (images.length === 0) {
return NextResponse.json({ error: "No images to download" }, { status: 400 });
}
// Fetch all images server-side
const imagePromises = images.map(async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${url}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
});
const imageBuffers = await Promise.all(imagePromises);
// Create PDF
const pdf = new jsPDF();
for (let i = 0; i < imageBuffers.length; i++) {
if (i > 0) pdf.addPage();
const imgBuffer = imageBuffers[i];
const imgData = `data:image/jpeg;base64,${imgBuffer.toString('base64')}`;
// For simplicity, assume images fit the page; in production you might want to scale
pdf.addImage(imgData, 'JPEG', 10, 10, 190, 277); // A4 portrait size minus margins
// Add "Created by Make Comics" at the bottom
pdf.setFontSize(8);
pdf.text('Created by Make Comics', 105, 290, { align: 'center' });
}
const pdfBuffer = Buffer.from(pdf.output('arraybuffer'));
// Return PDF as response
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${story.title}.pdf"`,
},
});
} catch (error) {
console.error('Error generating PDF:', error);
return NextResponse.json(
{ error: "Failed to generate PDF" },
{ status: 500 }
);
}
}
@@ -60,6 +60,7 @@ export function StoryEditorClient() {
const [existingCharacterImages, setExistingCharacterImages] = useState<
string[]
>([]);
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const { toast } = useToast();
const [apiKey, setApiKey] = useApiKey();
@@ -225,6 +226,47 @@ export function StoryEditorClient() {
setShowApiModal(true);
};
const downloadPDF = async () => {
if (!story || pages.length === 0) return;
setIsGeneratingPDF(true);
try {
const response = await fetch(`/api/download-pdf?storySlug=${story.slug}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to generate PDF");
}
// Create blob from response and trigger download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${story.title}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
toast({
title: "PDF downloaded",
description: "Your comic has been downloaded as a PDF.",
duration: 3000,
});
} catch (error) {
console.error('Error generating PDF:', error);
toast({
title: "Failed to generate PDF",
description: "An error occurred while generating the PDF.",
variant: "destructive",
duration: 4000,
});
} finally {
setIsGeneratingPDF(false);
}
};
const handleDeletePage = (pageIndex: number) => {
setPageToDelete(pageIndex);
setShowDeleteDialog(true);
@@ -372,6 +414,8 @@ export function StoryEditorClient() {
<EditorToolbar
title={story.title}
onContinueStory={handleAddPage}
onDownloadPDF={downloadPDF}
isGeneratingPDF={isGeneratingPDF}
isOwner={isOwner}
/>
+43 -27
View File
@@ -1,6 +1,6 @@
"use client";
import { ArrowLeft, RefreshCw, Share, Plus, Info } from "lucide-react";
import { ArrowLeft, RefreshCw, Share, Plus, Info, Download } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useToast } from "@/hooks/use-toast";
@@ -8,12 +8,16 @@ import { useToast } from "@/hooks/use-toast";
interface EditorToolbarProps {
title: string;
onContinueStory?: () => void;
onDownloadPDF?: () => void;
isGeneratingPDF?: boolean;
isOwner?: boolean;
}
export function EditorToolbar({
title,
onContinueStory,
onDownloadPDF,
isGeneratingPDF = false,
isOwner = true,
}: EditorToolbarProps) {
const router = useRouter();
@@ -37,32 +41,44 @@ export function EditorToolbar({
</div>
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
<Button
variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex"
onClick={async () => {
const url = window.location.href;
try {
await navigator.clipboard.writeText(url);
toast({
title: "Link copied!",
description: "Story URL has been copied to your clipboard.",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy URL:", err);
toast({
title: "Failed to copy",
description: "Could not copy the URL to clipboard.",
variant: "destructive",
duration: 3000,
});
}
}}
>
<Share className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Share</span>
</Button>
<Button
variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex"
onClick={async () => {
const url = window.location.href;
try {
await navigator.clipboard.writeText(url);
toast({
title: "Link copied!",
description: "Story URL has been copied to your clipboard.",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy URL:", err);
toast({
title: "Failed to copy",
description: "Could not copy the URL to clipboard.",
variant: "destructive",
duration: 3000,
});
}
}}
>
<Share className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Share</span>
</Button>
{isOwner && onDownloadPDF && (
<Button
variant="ghost"
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3"
onClick={onDownloadPDF}
disabled={isGeneratingPDF}
>
<Download className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>{isGeneratingPDF ? 'Generating...' : 'Download PDF'}</span>
</Button>
)}
{isOwner && onContinueStory && (
<Button
+1
View File
@@ -52,6 +52,7 @@
"drizzle-orm": "^0.45.1",
"embla-carousel-react": "8.5.1",
"input-otp": "1.4.1",
"jspdf": "^3.0.4",
"lucide-react": "^0.454.0",
"next": "16.1.1",
"next-s3-upload": "^0.3.4",
+170
View File
@@ -137,6 +137,9 @@ importers:
input-otp:
specifier: 1.4.1
version: 1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
jspdf:
specifier: ^3.0.4
version: 3.0.4
lucide-react:
specifier: ^0.454.0
version: 0.454.0(react@19.2.0)
@@ -2024,9 +2027,15 @@ packages:
'@types/node@22.19.3':
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/pg@8.16.0':
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -2035,6 +2044,9 @@ packages:
'@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@upstash/core-analytics@0.0.10':
resolution: {integrity: sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==}
engines: {node: '>=16.0.0'}
@@ -2069,6 +2081,10 @@ packages:
peerDependencies:
postcss: ^8.1.0
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -2093,6 +2109,10 @@ packages:
caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
canvg@3.0.11:
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
engines: {node: '>=10.0.0'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -2113,6 +2133,12 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
core-js@3.47.0:
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -2195,6 +2221,9 @@ packages:
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
drizzle-kit@0.31.8:
resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
hasBin: true
@@ -2341,6 +2370,9 @@ packages:
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
engines: {node: '>=6.0.0'}
fast-png@6.4.0:
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
fast-sha256@1.3.0:
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
@@ -2348,6 +2380,9 @@ packages:
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
hasBin: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
@@ -2364,6 +2399,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -2380,6 +2419,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
iobuffer@5.4.0:
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -2391,6 +2433,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
jspdf@3.0.4:
resolution: {integrity: sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==}
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
@@ -2525,6 +2570,12 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
@@ -2569,6 +2620,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
react-day-picker@9.8.0:
resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==}
engines: {node: '>=18'}
@@ -2658,9 +2712,16 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
rgbcolor@1.0.1:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -2696,6 +2757,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
standardwebhooks@1.0.0:
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
@@ -2724,6 +2789,10 @@ packages:
babel-plugin-macros:
optional: true
svg-pathdata@6.0.3:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
swr@2.3.4:
resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==}
peerDependencies:
@@ -2744,6 +2813,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -2802,6 +2874,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@@ -5104,12 +5179,17 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/pako@2.0.4': {}
'@types/pg@8.16.0':
dependencies:
'@types/node': 22.19.3
pg-protocol: 1.10.3
pg-types: 2.2.0
'@types/raf@3.4.3':
optional: true
'@types/react-dom@19.2.3(@types/react@19.2.7)':
dependencies:
'@types/react': 19.2.7
@@ -5118,6 +5198,9 @@ snapshots:
dependencies:
csstype: 3.2.3
'@types/trusted-types@2.0.7':
optional: true
'@upstash/core-analytics@0.0.10':
dependencies:
'@upstash/redis': 1.36.0
@@ -5151,6 +5234,9 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
base64-arraybuffer@1.0.2:
optional: true
base64-js@1.5.1: {}
baseline-browser-mapping@2.9.11: {}
@@ -5174,6 +5260,18 @@ snapshots:
caniuse-lite@1.0.30001761: {}
canvg@3.0.11:
dependencies:
'@babel/runtime': 7.28.4
'@types/raf': 3.4.3
core-js: 3.47.0
raf: 3.4.1
regenerator-runtime: 0.13.11
rgbcolor: 1.0.1
stackblur-canvas: 2.7.0
svg-pathdata: 6.0.3
optional: true
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -5196,6 +5294,14 @@ snapshots:
cookie@1.0.2: {}
core-js@3.47.0:
optional: true
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
optional: true
csstype@3.1.3: {}
csstype@3.2.3: {}
@@ -5259,6 +5365,11 @@ snapshots:
'@babel/runtime': 7.28.4
csstype: 3.2.3
dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
optional: true
drizzle-kit@0.31.8:
dependencies:
'@drizzle-team/brocli': 0.10.2
@@ -5362,12 +5473,20 @@ snapshots:
fast-equals@5.4.0: {}
fast-png@6.4.0:
dependencies:
'@types/pako': 2.0.4
iobuffer: 5.4.0
pako: 2.1.0
fast-sha256@1.3.0: {}
fast-xml-parser@5.2.5:
dependencies:
strnum: 2.1.2
fflate@0.8.2: {}
fraction.js@5.3.4: {}
get-nonce@1.0.1: {}
@@ -5380,6 +5499,12 @@ snapshots:
graceful-fs@4.2.11: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
optional: true
ieee754@1.2.1: {}
inherits@2.0.4: {}
@@ -5391,12 +5516,25 @@ snapshots:
internmap@2.0.3: {}
iobuffer@5.4.0: {}
jiti@2.6.1: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
jspdf@3.0.4:
dependencies:
'@babel/runtime': 7.28.4
fast-png: 6.4.0
fflate: 0.8.2
optionalDependencies:
canvg: 3.0.11
core-js: 3.47.0
dompurify: 3.3.1
html2canvas: 1.4.1
lightningcss-android-arm64@1.30.2:
optional: true
@@ -5510,6 +5648,11 @@ snapshots:
object-assign@4.1.1: {}
pako@2.1.0: {}
performance-now@2.1.0:
optional: true
pg-int8@1.0.1: {}
pg-protocol@1.10.3: {}
@@ -5554,6 +5697,11 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
raf@3.4.1:
dependencies:
performance-now: 2.1.0
optional: true
react-day-picker@9.8.0(react@19.2.0):
dependencies:
'@date-fns/tz': 1.2.0
@@ -5648,8 +5796,14 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
regenerator-runtime@0.13.11:
optional: true
resolve-pkg-maps@1.0.0: {}
rgbcolor@1.0.1:
optional: true
safe-buffer@5.2.1: {}
scheduler@0.27.0: {}
@@ -5705,6 +5859,9 @@ snapshots:
source-map@0.6.1: {}
stackblur-canvas@2.7.0:
optional: true
standardwebhooks@1.0.0:
dependencies:
'@stablelib/base64': 1.0.1
@@ -5728,6 +5885,9 @@ snapshots:
client-only: 0.0.1
react: 19.2.0
svg-pathdata@6.0.3:
optional: true
swr@2.3.4(react@19.2.0):
dependencies:
dequal: 2.0.3
@@ -5744,6 +5904,11 @@ snapshots:
tapable@2.3.0: {}
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
optional: true
tiny-invariant@1.3.3: {}
together-ai@0.33.0: {}
@@ -5785,6 +5950,11 @@ snapshots:
util-deprecate@1.0.2: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
optional: true
uuid@8.3.2: {}
vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):