247 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
}
|