diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ae2b53f..bdbce0d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -710,6 +710,20 @@ model TopSalesProduct { @@map("top_sales_products") } +model HeroBanner { + id String @id @default(cuid()) + title String + subtitle String? + imageUrl String + linkUrl String? + isActive Boolean @default(true) + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("hero_banners") +} + enum UserRole { ADMIN MODERATOR diff --git a/src/app/dashboard/hero-banners/page.tsx b/src/app/dashboard/hero-banners/page.tsx new file mode 100644 index 0000000..638ccb5 --- /dev/null +++ b/src/app/dashboard/hero-banners/page.tsx @@ -0,0 +1,453 @@ +"use client" + +import { useState } from 'react' +import { useQuery, useMutation } from '@apollo/client' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Switch } from '@/components/ui/switch' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter +} from '@/components/ui/dialog' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { FileUpload } from '@/components/ui/file-upload' +import { Plus, Edit, Trash2, Image, ExternalLink } from 'lucide-react' +import { GET_HERO_BANNERS } from '@/lib/graphql/queries' +import { CREATE_HERO_BANNER, UPDATE_HERO_BANNER, DELETE_HERO_BANNER } from '@/lib/graphql/mutations' +import { toast } from 'sonner' + +interface HeroBanner { + id: string + title: string + subtitle?: string + imageUrl: string + linkUrl?: string + isActive: boolean + sortOrder: number + createdAt: string + updatedAt: string +} + +interface BannerFormData { + title: string + subtitle: string + imageUrl: string + linkUrl: string + isActive: boolean + sortOrder: number +} + +const defaultFormData: BannerFormData = { + title: '', + subtitle: '', + imageUrl: '', + linkUrl: '', + isActive: true, + sortOrder: 0 +} + +export default function HeroBannersPage() { + const [showDialog, setShowDialog] = useState(false) + const [editingBanner, setEditingBanner] = useState(null) + const [formData, setFormData] = useState(defaultFormData) + const [uploading, setUploading] = useState(false) + + const { data, loading, error, refetch } = useQuery(GET_HERO_BANNERS, { + fetchPolicy: 'cache-and-network' + }) + + const [createBanner] = useMutation(CREATE_HERO_BANNER, { + onCompleted: () => { + toast.success('Баннер успешно создан') + setShowDialog(false) + setFormData(defaultFormData) + refetch() + }, + onError: (error) => { + toast.error(error.message || 'Ошибка создания баннера') + } + }) + + const [updateBanner] = useMutation(UPDATE_HERO_BANNER, { + onCompleted: () => { + toast.success('Баннер успешно обновлен') + setShowDialog(false) + setEditingBanner(null) + setFormData(defaultFormData) + refetch() + }, + onError: (error) => { + toast.error(error.message || 'Ошибка обновления баннера') + } + }) + + const [deleteBanner] = useMutation(DELETE_HERO_BANNER, { + onCompleted: () => { + toast.success('Баннер успешно удален') + refetch() + }, + onError: (error) => { + toast.error(error.message || 'Ошибка удаления баннера') + } + }) + + const banners: HeroBanner[] = data?.heroBanners || [] + + const handleOpenDialog = (banner?: HeroBanner) => { + if (banner) { + setEditingBanner(banner) + setFormData({ + title: banner.title, + subtitle: banner.subtitle || '', + imageUrl: banner.imageUrl, + linkUrl: banner.linkUrl || '', + isActive: banner.isActive, + sortOrder: banner.sortOrder + }) + } else { + setEditingBanner(null) + setFormData(defaultFormData) + } + setShowDialog(true) + } + + const handleCloseDialog = () => { + setShowDialog(false) + setEditingBanner(null) + setFormData(defaultFormData) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + if (!formData.title.trim()) { + toast.error('Заголовок обязателен') + return + } + + if (!formData.imageUrl.trim()) { + toast.error('Изображение обязательно') + return + } + + if (editingBanner) { + updateBanner({ + variables: { + id: editingBanner.id, + input: formData + } + }) + } else { + createBanner({ + variables: { + input: formData + } + }) + } + } + + const handleDelete = (banner: HeroBanner) => { + if (confirm(`Вы уверены, что хотите удалить баннер "${banner.title}"?`)) { + deleteBanner({ + variables: { id: banner.id } + }) + } + } + + + + if (loading) { + return ( +
+
+
+ ) + } + + if (error) { + return ( +
+
+
Ошибка загрузки данных
+
{error.message}
+ +
+
+ ) + } + + return ( +
+
+
+

Баннеры героя

+

+ Управление баннерами на главной странице +

+
+ + + + + + + + + + {editingBanner ? 'Редактировать баннер' : 'Создать баннер'} + + + +
+
+
+ + setFormData(prev => ({ ...prev, title: e.target.value }))} + placeholder="Введите заголовок баннера" + required + /> +
+ +
+ + setFormData(prev => ({ ...prev, sortOrder: parseInt(e.target.value) || 0 }))} + placeholder="0" + /> +
+
+ +
+ +