Files
CreaBook/client/src/components/cover-designer/CoverDesigner.tsx
T
2026-04-05 03:08:53 +02:00

247 lines
8.0 KiB
TypeScript

import { useState } from 'react';
import { Image, Wand2, Type } from 'lucide-react';
import CoverGallery from './CoverGallery';
import CoverEditor from './CoverEditor';
import { useCoverStore, Cover } from '../../stores/coverStore';
import { coversApi } from '../../services/api';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export default function CoverDesigner() {
const [activeTab, setActiveTab] = useState<'gallery' | 'editor' | 'ai'>('gallery');
const { activeCover, setActiveCover } = useCoverStore();
const queryClient = useQueryClient();
const { data: coversData } = useQuery({
queryKey: ['covers'],
queryFn: async () => {
const response = await coversApi.getAll();
return response.data.covers;
},
});
const generateCoverMutation = useMutation({
mutationFn: async (prompt: string) => {
const response = await coversApi.generate(prompt);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['covers'] });
setActiveTab('gallery');
},
});
const handleGenerateAI = (prompt: string) => {
generateCoverMutation.mutate(prompt);
};
const handleSelectCover = (coverUrl: string) => {
const newCover: Cover = {
id: `cover-${Date.now()}`,
title: 'New Book',
author: 'Author Name',
backgroundImage: coverUrl,
backgroundColor: '#ffffff',
width: 1600,
height: 2400,
layers: [],
createdAt: new Date(),
updatedAt: new Date(),
};
setActiveCover(newCover);
setActiveTab('editor');
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Cover Designer</h2>
<p className="text-gray-500 mt-1">
Design stunning book covers with AI or manual editing
</p>
</div>
{/* Tab Navigation */}
<div className="flex bg-white rounded-lg p-1 border border-gray-200">
<button
onClick={() => setActiveTab('gallery')}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'gallery'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Image className="w-4 h-4" />
Gallery
</button>
<button
onClick={() => setActiveTab('editor')}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'editor'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Type className="w-4 h-4" />
Editor
</button>
<button
onClick={() => setActiveTab('ai')}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'ai'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Wand2 className="w-4 h-4" />
AI Generate
</button>
</div>
</div>
{activeTab === 'gallery' && (
<CoverGallery
covers={coversData || []}
onSelect={handleSelectCover}
onDelete={(id) => {
coversApi.delete(id).then(() => {
queryClient.invalidateQueries({ queryKey: ['covers'] });
});
}}
/>
)}
{activeTab === 'editor' && activeCover && (
<CoverEditor />
)}
{activeTab === 'ai' && (
<AIGenerator onGenerate={handleGenerateAI} isGenerating={generateCoverMutation.isPending} />
)}
</div>
);
}
function AIGenerator({
onGenerate,
isGenerating,
}: {
onGenerate: (prompt: string) => void;
isGenerating: boolean;
}) {
const [prompt, setPrompt] = useState('');
const [selectedStyle, setSelectedStyle] = useState('modern');
const styles = [
{ id: 'modern', name: 'Modern', icon: '✨' },
{ id: 'minimal', name: 'Minimalist', icon: '🎯' },
{ id: 'vintage', name: 'Vintage', icon: '📜' },
{ id: 'bold', name: 'Bold', icon: '🔥' },
{ id: 'elegant', name: 'Elegant', icon: '💎' },
{ id: 'playful', name: 'Playful', icon: '🎨' },
];
const genrePresets = [
{
genre: 'Mystery',
prompt: 'A mysterious book cover with dark shadows, silhouette of a detective, foggy street scene, noir atmosphere',
},
{
genre: 'Romance',
prompt: 'A romantic book cover with soft pastel colors, couple silhouette at sunset, dreamy and emotional',
},
{
genre: 'Fantasy',
prompt: 'An epic fantasy book cover with magical elements, dragon, castle in the background, mystical glowing effects',
},
{
genre: 'Sci-Fi',
prompt: 'A futuristic sci-fi book cover with spaceships, neon lights, cyberpunk city, high-tech atmosphere',
},
{
genre: 'Self-Help',
prompt: 'A clean self-help book cover with inspiring imagery, mountain peak or sunrise, professional and motivating',
},
];
const handleUsePreset = (presetPrompt: string) => {
setPrompt(presetPrompt);
};
const handleGenerate = () => {
if (prompt.trim()) {
const enhancedPrompt = `${prompt}, ${selectedStyle} style, professional book cover design, high quality, detailed`;
onGenerate(enhancedPrompt);
}
};
return (
<div className="card space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
AI Cover Generator
</h3>
{/* Style Selection */}
<div className="mb-6">
<label className="label">Select Style</label>
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2 mt-2">
{styles.map((style) => (
<button
key={style.id}
onClick={() => setSelectedStyle(style.id)}
className={`p-3 rounded-lg border text-center transition-colors ${
selectedStyle === style.id
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="text-xl">{style.icon}</span>
<p className="text-xs mt-1">{style.name}</p>
</button>
))}
</div>
</div>
{/* Prompt Input */}
<div className="mb-6">
<label className="label">Describe Your Cover</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the book cover you want to generate... e.g., 'A mysterious forest with glowing eyes in the darkness'"
className="input min-h-[120px] resize-y"
/>
</div>
{/* Genre Presets */}
<div className="mb-6">
<label className="label">Quick Presets by Genre</label>
<div className="grid gap-2 mt-2">
{genrePresets.map((preset) => (
<button
key={preset.genre}
onClick={() => handleUsePreset(preset.prompt)}
className="text-left px-4 py-2 rounded-lg border border-gray-200 hover:border-primary-300 hover:bg-primary-50 transition-colors"
>
<span className="font-medium">{preset.genre}</span>
<p className="text-sm text-gray-500 truncate">{preset.prompt}</p>
</button>
))}
</div>
</div>
{/* Generate Button */}
<button
onClick={handleGenerate}
disabled={!prompt.trim() || isGenerating}
className="btn-primary w-full py-3 flex items-center justify-center gap-2"
>
<Wand2 className="w-5 h-5" />
{isGenerating ? 'Generating...' : 'Generate Cover'}
</button>
</div>
</div>
);
}