diff --git a/app/api/download-pdf/route.ts b/app/api/download-pdf/route.ts
new file mode 100644
index 0000000..acd215b
--- /dev/null
+++ b/app/api/download-pdf/route.ts
@@ -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 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/story/[storySlug]/story-editor-client.tsx b/app/story/[storySlug]/story-editor-client.tsx
index a744fd1..75915f8 100644
--- a/app/story/[storySlug]/story-editor-client.tsx
+++ b/app/story/[storySlug]/story-editor-client.tsx
@@ -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() {
diff --git a/components/editor/editor-toolbar.tsx b/components/editor/editor-toolbar.tsx
index 0331cca..d72f6e7 100644
--- a/components/editor/editor-toolbar.tsx
+++ b/components/editor/editor-toolbar.tsx
@@ -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({
-
+
+
+ {isOwner && onDownloadPDF && (
+
+ )}
{isOwner && onContinueStory && (