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 && (