From 9895200db67a885c1dee3d5e0201c9e6dc1efc79 Mon Sep 17 00:00:00 2001 From: Riccardo Giorato Date: Fri, 26 Dec 2025 17:30:08 +0100 Subject: [PATCH] wip --- app/editor/[storySlug]/page.tsx | 1 + components/editor/editor-toolbar.tsx | 36 ++- components/editor/generate-page-modal.tsx | 310 +++++++++++----------- 3 files changed, 179 insertions(+), 168 deletions(-) diff --git a/app/editor/[storySlug]/page.tsx b/app/editor/[storySlug]/page.tsx index 461d913..cc14c41 100644 --- a/app/editor/[storySlug]/page.tsx +++ b/app/editor/[storySlug]/page.tsx @@ -217,6 +217,7 @@ export default function StoryEditorPage() { setShowInfoSheet(true)} + onContinueStory={handleAddPage} />
diff --git a/components/editor/editor-toolbar.tsx b/components/editor/editor-toolbar.tsx index b9ad214..8682f30 100644 --- a/components/editor/editor-toolbar.tsx +++ b/components/editor/editor-toolbar.tsx @@ -1,16 +1,21 @@ -"use client" +"use client"; -import { ArrowLeft, RefreshCw, Download, Plus, Info } from "lucide-react" -import { Button } from "@/components/ui/button" -import { useRouter } from "next/navigation" +import { ArrowLeft, RefreshCw, Download, Plus, Info } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; interface EditorToolbarProps { - title: string - onInfoClick: () => void + title: string; + onInfoClick: () => void; + onContinueStory?: () => void; } -export function EditorToolbar({ title, onInfoClick }: EditorToolbarProps) { - const router = useRouter() +export function EditorToolbar({ + title, + onInfoClick, + onContinueStory, +}: EditorToolbarProps) { + const router = useRouter(); return (
@@ -24,7 +29,9 @@ export function EditorToolbar({ title, onInfoClick }: EditorToolbarProps) { -

{title}

+

+ {title} +

@@ -52,7 +59,16 @@ export function EditorToolbar({ title, onInfoClick }: EditorToolbarProps) { Download PDF + +
- ) + ); } diff --git a/components/editor/generate-page-modal.tsx b/components/editor/generate-page-modal.tsx index 453c03a..cff4299 100644 --- a/components/editor/generate-page-modal.tsx +++ b/components/editor/generate-page-modal.tsx @@ -1,33 +1,38 @@ -"use client" +"use client"; -import { useState, useRef, useEffect, useMemo } from "react" -import { Upload, X, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { useToast } from "@/hooks/use-toast" -import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils" -import { COMIC_STYLES } from "@/lib/constants" +import { useState, useRef, useEffect, useMemo } from "react"; +import { Upload, X, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"; +import { COMIC_STYLES } from "@/lib/constants"; interface PageReference { - pageNumber: number - characterImages: string[] - prompt: string - imageUrl?: string - style: string + pageNumber: number; + characterImages: string[]; + prompt: string; + imageUrl?: string; + style: string; } interface GeneratePageModalProps { - isOpen: boolean - onClose: () => void + isOpen: boolean; + onClose: () => void; onGenerate: (data: { - prompt: string - style: string - characterFiles?: File[] - characterUrls?: string[] - isContinuation?: boolean - }) => void - pageNumber: number - allPages: PageReference[] + prompt: string; + style: string; + characterFiles?: File[]; + characterUrls?: string[]; + isContinuation?: boolean; + }) => void; + pageNumber: number; + allPages: PageReference[]; } export function GeneratePageModal({ @@ -37,40 +42,46 @@ export function GeneratePageModal({ pageNumber, allPages, }: GeneratePageModalProps) { - const [prompt, setPrompt] = useState("") - const [uploadedFiles, setUploadedFiles] = useState([]) - const [selectedExistingCharacters, setSelectedExistingCharacters] = useState([]) - const [referencePageNumber, setReferencePageNumber] = useState(allPages.length > 0 ? allPages.length : 1) - const [pageImage, setPageImage] = useState(null) - const [previews, setPreviews] = useState([]) - const [showPreview, setShowPreview] = useState(null) - const [isGenerating, setIsGenerating] = useState(false) - const fileInputRef = useRef(null) - const { toast } = useToast() + const [prompt, setPrompt] = useState(""); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [selectedExistingCharacters, setSelectedExistingCharacters] = useState< + string[] + >([]); + const [referencePageNumber, setReferencePageNumber] = useState( + allPages.length > 0 ? allPages.length : 1 + ); + const [pageImage, setPageImage] = useState(null); + const [previews, setPreviews] = useState([]); + const [showPreview, setShowPreview] = useState(null); + const [isGenerating, setIsGenerating] = useState(false); + const fileInputRef = useRef(null); + const { toast } = useToast(); - const referencePage = allPages.find((p) => p.pageNumber === referencePageNumber) || null - const referenceStyleId = referencePage?.style || "noir" - const selectedStyleName = COMIC_STYLES.find((s) => s.id === referenceStyleId)?.name || "Noir" - const selectedStyle = useMemo(() => selectedStyleName, [referenceStyleId]) + const referencePage = + allPages.find((p) => p.pageNumber === referencePageNumber) || null; + const referenceStyleId = referencePage?.style || "noir"; + const selectedStyleName = + COMIC_STYLES.find((s) => s.id === referenceStyleId)?.name || "Noir"; + const selectedStyle = useMemo(() => selectedStyleName, [referenceStyleId]); useEffect(() => { if (isOpen && referencePage) { - setSelectedExistingCharacters(referencePage.characterImages) - setPageImage(referencePage.imageUrl || null) + setSelectedExistingCharacters(referencePage.characterImages); + setPageImage(referencePage.imageUrl || null); } else if (isOpen && !referencePage) { - setPageImage(null) + setPageImage(null); } - }, [isOpen, referencePage]) + }, [isOpen, referencePage]); const handleFiles = async (newFiles: FileList | null) => { - if (!newFiles) return + if (!newFiles) return; - const filesArray = Array.from(newFiles) + const filesArray = Array.from(newFiles); - const validationResults = filesArray.map(file => ({ + const validationResults = filesArray.map((file) => ({ file, - validation: validateFileForUpload(file, true) - })) + validation: validateFileForUpload(file, true), + })); validationResults.forEach(({ validation }) => { if (!validation.valid && validation.error) { @@ -79,72 +90,74 @@ export function GeneratePageModal({ description: validation.error, variant: "destructive", duration: 4000, - }) + }); } - }) + }); const validFiles = validationResults .filter(({ validation }) => validation.valid) - .map(({ file }) => file) + .map(({ file }) => file); - if (validFiles.length === 0) return + if (validFiles.length === 0) return; - const totalFiles = [...uploadedFiles, ...validFiles].slice(0, 2) - setUploadedFiles(totalFiles) + const totalFiles = [...uploadedFiles, ...validFiles].slice(0, 2); + setUploadedFiles(totalFiles); const newPreviews = await Promise.all( totalFiles.map((file) => generateFilePreview(file)) - ) - setPreviews(newPreviews) - } + ); + setPreviews(newPreviews); + }; const removeFile = (index: number) => { - const newFiles = uploadedFiles.filter((_, i) => i !== index) - const newPreviews = previews.filter((_, i) => i !== index) - setUploadedFiles(newFiles) - setPreviews(newPreviews) - setShowPreview(null) + const newFiles = uploadedFiles.filter((_, i) => i !== index); + const newPreviews = previews.filter((_, i) => i !== index); + setUploadedFiles(newFiles); + setPreviews(newPreviews); + setShowPreview(null); if (fileInputRef.current) { - fileInputRef.current.value = "" + fileInputRef.current.value = ""; } - } + }; const toggleExistingCharacter = (characterUrl: string) => { - setSelectedExistingCharacters(prev => + setSelectedExistingCharacters((prev) => prev.includes(characterUrl) - ? prev.filter(url => url !== characterUrl) + ? prev.filter((url) => url !== characterUrl) : [...prev, characterUrl] - ) - } + ); + }; const togglePageImage = () => { - setPageImage(prev => prev === null && referencePage?.imageUrl ? referencePage.imageUrl : null) - } + setPageImage((prev) => + prev === null && referencePage?.imageUrl ? referencePage.imageUrl : null + ); + }; const handleGenerate = async () => { - if (!prompt.trim()) return - setIsGenerating(true) + if (!prompt.trim()) return; + setIsGenerating(true); const fileDataUrls = await Promise.all( uploadedFiles.map((file, index) => { return new Promise((resolve) => { - const reader = new FileReader() + const reader = new FileReader(); reader.onload = (e) => { - const base64 = e.target?.result as string - const [header, base64Data] = base64.split(",") - const base64String = base64Data.replace(/_/g, "/") - resolve(`data:image/jpeg;base64,${base64String}`) - } - reader.readAsDataURL(file) - }) + const base64 = e.target?.result as string; + const [header, base64Data] = base64.split(","); + // Send just the base64 data without the data URL prefix + resolve(base64Data); + }; + reader.readAsDataURL(file); + }); }) - ) + ); const allCharacterUrls: string[] = [ ...selectedExistingCharacters, ...fileDataUrls, ...(pageImage ? [pageImage] : []), - ] + ]; onGenerate({ prompt, @@ -152,28 +165,30 @@ export function GeneratePageModal({ characterFiles: uploadedFiles.length > 0 ? uploadedFiles : undefined, characterUrls: allCharacterUrls, isContinuation: false, - }) - } + }); + }; useEffect(() => { if (!isOpen) { - setIsGenerating(false) - setPrompt("") - setUploadedFiles([]) - setSelectedExistingCharacters([]) - setPageImage(null) + setIsGenerating(false); + setPrompt(""); + setUploadedFiles([]); + setSelectedExistingCharacters([]); + setPageImage(null); if (allPages.length > 0) { - setReferencePageNumber(allPages.length) + setReferencePageNumber(allPages.length); } } - }, [isOpen]) + }, [isOpen]); return ( <> - Generate Page {pageNumber} + + Generate Page {pageNumber} +
@@ -185,7 +200,9 @@ export function GeneratePageModal({