Добавлена новая модель HeroBanner с соответствующими мутациями и запросами для управления баннерами героя. Обновлены типы GraphQL и резолверы для обработки данных баннеров, что улучшает функциональность приложения. В боковое меню добавлен новый элемент для навигации по баннерам героя, что повышает удобство работы с интерфейсом.
This commit is contained in:
@ -710,6 +710,20 @@ model TopSalesProduct {
|
|||||||
@@map("top_sales_products")
|
@@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 {
|
enum UserRole {
|
||||||
ADMIN
|
ADMIN
|
||||||
MODERATOR
|
MODERATOR
|
||||||
|
453
src/app/dashboard/hero-banners/page.tsx
Normal file
453
src/app/dashboard/hero-banners/page.tsx
Normal file
@ -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<HeroBanner | null>(null)
|
||||||
|
const [formData, setFormData] = useState<BannerFormData>(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 (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||||
|
<div className="text-red-600 text-center">
|
||||||
|
<div className="text-lg font-semibold mb-2">Ошибка загрузки данных</div>
|
||||||
|
<div className="text-sm mb-4">{error.message}</div>
|
||||||
|
<Button onClick={() => refetch()}>Повторить</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Баннеры героя</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Управление баннерами на главной странице
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button onClick={() => handleOpenDialog()}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Добавить баннер
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingBanner ? 'Редактировать баннер' : 'Создать баннер'}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Заголовок *</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
placeholder="Введите заголовок баннера"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="sortOrder">Порядок сортировки</Label>
|
||||||
|
<Input
|
||||||
|
id="sortOrder"
|
||||||
|
type="number"
|
||||||
|
value={formData.sortOrder}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, sortOrder: parseInt(e.target.value) || 0 }))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="subtitle">Подзаголовок</Label>
|
||||||
|
<Textarea
|
||||||
|
id="subtitle"
|
||||||
|
value={formData.subtitle}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, subtitle: e.target.value }))}
|
||||||
|
placeholder="Введите подзаголовок баннера (необязательно)"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="linkUrl">Ссылка</Label>
|
||||||
|
<Input
|
||||||
|
id="linkUrl"
|
||||||
|
type="url"
|
||||||
|
value={formData.linkUrl}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, linkUrl: e.target.value }))}
|
||||||
|
placeholder="https://example.com (необязательно)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Изображение *</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.imageUrl && (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt="Превью"
|
||||||
|
className="w-full h-32 object-cover rounded-lg border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FileUpload
|
||||||
|
onUpload={(url) => setFormData(prev => ({ ...prev, imageUrl: url }))}
|
||||||
|
accept="image/*"
|
||||||
|
maxSize={5 * 1024 * 1024}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
{uploading && (
|
||||||
|
<div className="text-sm text-gray-500">Загрузка изображения...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="isActive"
|
||||||
|
checked={formData.isActive}
|
||||||
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isActive: checked }))}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="isActive">Активен</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={handleCloseDialog}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={uploading}>
|
||||||
|
{editingBanner ? 'Обновить' : 'Создать'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Статистика */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Всего баннеров</CardTitle>
|
||||||
|
<Image className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{banners.length}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Активные</CardTitle>
|
||||||
|
<Image className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{banners.filter(b => b.isActive).length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Неактивные</CardTitle>
|
||||||
|
<Image className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-gray-600">
|
||||||
|
{banners.filter(b => !b.isActive).length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Таблица баннеров */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Список баннеров ({banners.length})</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Управление баннерами на главной странице сайта
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{banners.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<Image className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||||
|
<p>Нет созданных баннеров</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => handleOpenDialog()}
|
||||||
|
>
|
||||||
|
Создать первый баннер
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Изображение</TableHead>
|
||||||
|
<TableHead>Заголовок</TableHead>
|
||||||
|
<TableHead>Статус</TableHead>
|
||||||
|
<TableHead>Порядок</TableHead>
|
||||||
|
<TableHead>Ссылка</TableHead>
|
||||||
|
<TableHead>Дата создания</TableHead>
|
||||||
|
<TableHead>Действия</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{[...banners]
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
.map((banner) => (
|
||||||
|
<TableRow key={banner.id}>
|
||||||
|
<TableCell>
|
||||||
|
<img
|
||||||
|
src={banner.imageUrl}
|
||||||
|
alt={banner.title}
|
||||||
|
className="w-16 h-10 object-cover rounded border"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{banner.title}</div>
|
||||||
|
{banner.subtitle && (
|
||||||
|
<div className="text-sm text-gray-500 truncate max-w-xs">
|
||||||
|
{banner.subtitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={banner.isActive ? 'default' : 'secondary'}>
|
||||||
|
{banner.isActive ? 'Активен' : 'Неактивен'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{banner.sortOrder}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{banner.linkUrl ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ExternalLink className="w-4 h-4 mr-1 text-gray-400" />
|
||||||
|
<span className="text-sm text-blue-600 truncate max-w-xs">
|
||||||
|
{banner.linkUrl}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">—</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(banner.createdAt).toLocaleDateString('ru-RU')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleOpenDialog(banner)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(banner)}
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -14,7 +14,8 @@ import {
|
|||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Receipt,
|
Receipt,
|
||||||
Palette,
|
Palette,
|
||||||
Star
|
Star,
|
||||||
|
Image
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useAuth } from '@/components/providers/AuthProvider'
|
import { useAuth } from '@/components/providers/AuthProvider'
|
||||||
@ -44,6 +45,11 @@ const navigationItems = [
|
|||||||
href: '/dashboard/homepage-products',
|
href: '/dashboard/homepage-products',
|
||||||
icon: Star,
|
icon: Star,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Баннеры героя',
|
||||||
|
href: '/dashboard/hero-banners',
|
||||||
|
icon: Image,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Заказы',
|
title: 'Заказы',
|
||||||
href: '/dashboard/orders',
|
href: '/dashboard/orders',
|
||||||
|
@ -1404,3 +1404,42 @@ export const DELETE_TOP_SALES_PRODUCT = gql`
|
|||||||
deleteTopSalesProduct(id: $id)
|
deleteTopSalesProduct(id: $id)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Hero Banners mutations
|
||||||
|
export const CREATE_HERO_BANNER = gql`
|
||||||
|
mutation CreateHeroBanner($input: HeroBannerInput!) {
|
||||||
|
createHeroBanner(input: $input) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
imageUrl
|
||||||
|
linkUrl
|
||||||
|
isActive
|
||||||
|
sortOrder
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const UPDATE_HERO_BANNER = gql`
|
||||||
|
mutation UpdateHeroBanner($id: String!, $input: HeroBannerUpdateInput!) {
|
||||||
|
updateHeroBanner(id: $id, input: $input) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
imageUrl
|
||||||
|
linkUrl
|
||||||
|
isActive
|
||||||
|
sortOrder
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const DELETE_HERO_BANNER = gql`
|
||||||
|
mutation DeleteHeroBanner($id: String!) {
|
||||||
|
deleteHeroBanner(id: $id)
|
||||||
|
}
|
||||||
|
`
|
@ -1467,4 +1467,37 @@ export const GET_PARTSINDEX_CATEGORIES = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Hero Banners queries
|
||||||
|
export const GET_HERO_BANNERS = gql`
|
||||||
|
query GetHeroBanners {
|
||||||
|
heroBanners {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
imageUrl
|
||||||
|
linkUrl
|
||||||
|
isActive
|
||||||
|
sortOrder
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_HERO_BANNER = gql`
|
||||||
|
query GetHeroBanner($id: String!) {
|
||||||
|
heroBanner(id: $id) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
imageUrl
|
||||||
|
linkUrl
|
||||||
|
isActive
|
||||||
|
sortOrder
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
@ -456,6 +456,24 @@ interface TopSalesProductUpdateInput {
|
|||||||
sortOrder?: number
|
sortOrder?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HeroBannerInput {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
imageUrl: string
|
||||||
|
linkUrl?: string
|
||||||
|
isActive?: boolean
|
||||||
|
sortOrder?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeroBannerUpdateInput {
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
imageUrl?: string
|
||||||
|
linkUrl?: string
|
||||||
|
isActive?: boolean
|
||||||
|
sortOrder?: number
|
||||||
|
}
|
||||||
|
|
||||||
// Утилиты
|
// Утилиты
|
||||||
const createSlug = (text: string): string => {
|
const createSlug = (text: string): string => {
|
||||||
return text
|
return text
|
||||||
@ -4004,6 +4022,30 @@ export const resolvers = {
|
|||||||
console.error('Ошибка получения новых поступлений:', error)
|
console.error('Ошибка получения новых поступлений:', error)
|
||||||
throw new Error('Не удалось получить новые поступления')
|
throw new Error('Не удалось получить новые поступления')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hero Banners queries
|
||||||
|
heroBanners: async () => {
|
||||||
|
try {
|
||||||
|
return await prisma.heroBanner.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { sortOrder: 'asc' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения баннеров героя:', error)
|
||||||
|
throw new Error('Не удалось получить баннеры героя')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
heroBanner: async (_: unknown, { id }: { id: string }) => {
|
||||||
|
try {
|
||||||
|
return await prisma.heroBanner.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения баннера героя:', error)
|
||||||
|
throw new Error('Не удалось получить баннер героя')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -9436,6 +9478,98 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
throw new Error('Не удалось удалить товар из топ продаж')
|
throw new Error('Не удалось удалить товар из топ продаж')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hero Banner mutations
|
||||||
|
createHeroBanner: async (_: unknown, { input }: { input: HeroBannerInput }, context: Context) => {
|
||||||
|
try {
|
||||||
|
if (!context.userId) {
|
||||||
|
throw new Error('Пользователь не авторизован')
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroBanner = await prisma.heroBanner.create({
|
||||||
|
data: {
|
||||||
|
title: input.title,
|
||||||
|
subtitle: input.subtitle,
|
||||||
|
imageUrl: input.imageUrl,
|
||||||
|
linkUrl: input.linkUrl,
|
||||||
|
isActive: input.isActive ?? true,
|
||||||
|
sortOrder: input.sortOrder ?? 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return heroBanner
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка создания баннера героя:', error)
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
throw new Error('Не удалось создать баннер героя')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHeroBanner: async (_: unknown, { id, input }: { id: string; input: HeroBannerUpdateInput }, context: Context) => {
|
||||||
|
try {
|
||||||
|
if (!context.userId) {
|
||||||
|
throw new Error('Пользователь не авторизован')
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBanner = await prisma.heroBanner.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingBanner) {
|
||||||
|
throw new Error('Баннер героя не найден')
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroBanner = await prisma.heroBanner.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(input.title !== undefined && { title: input.title }),
|
||||||
|
...(input.subtitle !== undefined && { subtitle: input.subtitle }),
|
||||||
|
...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }),
|
||||||
|
...(input.linkUrl !== undefined && { linkUrl: input.linkUrl }),
|
||||||
|
...(input.isActive !== undefined && { isActive: input.isActive }),
|
||||||
|
...(input.sortOrder !== undefined && { sortOrder: input.sortOrder })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return heroBanner
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка обновления баннера героя:', error)
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
throw new Error('Не удалось обновить баннер героя')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteHeroBanner: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||||
|
try {
|
||||||
|
if (!context.userId) {
|
||||||
|
throw new Error('Пользователь не авторизован')
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBanner = await prisma.heroBanner.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingBanner) {
|
||||||
|
throw new Error('Баннер героя не найден')
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.heroBanner.delete({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления баннера героя:', error)
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
throw new Error('Не удалось удалить баннер героя')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1054,6 +1054,10 @@ export const typeDefs = gql`
|
|||||||
topSalesProducts: [TopSalesProduct!]!
|
topSalesProducts: [TopSalesProduct!]!
|
||||||
topSalesProduct(id: ID!): TopSalesProduct
|
topSalesProduct(id: ID!): TopSalesProduct
|
||||||
|
|
||||||
|
# Баннеры героя
|
||||||
|
heroBanners: [HeroBanner!]!
|
||||||
|
heroBanner(id: String!): HeroBanner
|
||||||
|
|
||||||
# Новые поступления
|
# Новые поступления
|
||||||
newArrivals(limit: Int = 8): [Product!]!
|
newArrivals(limit: Int = 8): [Product!]!
|
||||||
}
|
}
|
||||||
@ -1262,6 +1266,11 @@ export const typeDefs = gql`
|
|||||||
createTopSalesProduct(input: TopSalesProductInput!): TopSalesProduct!
|
createTopSalesProduct(input: TopSalesProductInput!): TopSalesProduct!
|
||||||
updateTopSalesProduct(id: ID!, input: TopSalesProductUpdateInput!): TopSalesProduct!
|
updateTopSalesProduct(id: ID!, input: TopSalesProductUpdateInput!): TopSalesProduct!
|
||||||
deleteTopSalesProduct(id: ID!): Boolean!
|
deleteTopSalesProduct(id: ID!): Boolean!
|
||||||
|
|
||||||
|
# Баннеры героя
|
||||||
|
createHeroBanner(input: HeroBannerInput!): HeroBanner!
|
||||||
|
updateHeroBanner(id: String!, input: HeroBannerUpdateInput!): HeroBanner!
|
||||||
|
deleteHeroBanner(id: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input LoginInput {
|
input LoginInput {
|
||||||
@ -2302,4 +2311,35 @@ export const typeDefs = gql`
|
|||||||
isActive: Boolean
|
isActive: Boolean
|
||||||
sortOrder: Int
|
sortOrder: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Типы для баннеров героя
|
||||||
|
type HeroBanner {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
subtitle: String
|
||||||
|
imageUrl: String!
|
||||||
|
linkUrl: String
|
||||||
|
isActive: Boolean!
|
||||||
|
sortOrder: Int!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
input HeroBannerInput {
|
||||||
|
title: String!
|
||||||
|
subtitle: String
|
||||||
|
imageUrl: String!
|
||||||
|
linkUrl: String
|
||||||
|
isActive: Boolean
|
||||||
|
sortOrder: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input HeroBannerUpdateInput {
|
||||||
|
title: String
|
||||||
|
subtitle: String
|
||||||
|
imageUrl: String
|
||||||
|
linkUrl: String
|
||||||
|
isActive: Boolean
|
||||||
|
sortOrder: Int
|
||||||
|
}
|
||||||
`
|
`
|
Reference in New Issue
Block a user