386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useState } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { ArrowLeft, Save, X } from 'lucide-react';
|
||
import Link from 'next/link';
|
||
import TextEditor from '@/app/admin/components/TextEditor';
|
||
import ImageUpload from '@/app/admin/components/ImageUpload';
|
||
|
||
interface UiCategory { id: string; name: string }
|
||
|
||
export default function CreateNewsPage() {
|
||
const router = useRouter();
|
||
const [loading, setLoading] = useState(false);
|
||
const [formData, setFormData] = useState({
|
||
title: '',
|
||
slug: '',
|
||
summary: '',
|
||
content: '',
|
||
category: 'company',
|
||
imageUrl: '',
|
||
featured: false,
|
||
published: true,
|
||
publishedAt: new Date().toISOString().slice(0, 16),
|
||
tags: [] as string[]
|
||
});
|
||
const [tagInput, setTagInput] = useState('');
|
||
const [categories, setCategories] = useState<UiCategory[]>([
|
||
{ id: 'company', name: 'Новости компании' },
|
||
{ id: 'promotions', name: 'Акции' },
|
||
{ id: 'other', name: 'Другое' }
|
||
]);
|
||
|
||
useEffect(() => {
|
||
(async () => {
|
||
try {
|
||
const res = await fetch('/api/categories', { cache: 'no-store' });
|
||
const data = await res.json();
|
||
if (res.ok && data?.data?.length) {
|
||
setCategories(data.data.map((c: any) => ({ id: c.slug, name: c.name })));
|
||
// если выбранная категория отсутствует — выставим первую
|
||
setFormData(prev => ({ ...prev, category: data.data[0]?.slug || prev.category }));
|
||
}
|
||
} catch {}
|
||
})();
|
||
}, []);
|
||
|
||
const generateSlug = (title: string) => {
|
||
return title
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9а-я]/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-|-$/g, '');
|
||
};
|
||
|
||
const handleTitleChange = (title: string) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
title,
|
||
slug: generateSlug(title)
|
||
}));
|
||
};
|
||
|
||
const handleAddTag = () => {
|
||
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
tags: [...prev.tags, tagInput.trim()]
|
||
}));
|
||
setTagInput('');
|
||
}
|
||
};
|
||
|
||
const handleRemoveTag = (tagToRemove: string) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
tags: prev.tags.filter(tag => tag !== tagToRemove)
|
||
}));
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
|
||
try {
|
||
const response = await fetch('/api/news', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
...formData,
|
||
publishedAt: new Date(formData.publishedAt).toISOString()
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
router.push('/admin/news');
|
||
} else {
|
||
console.error('Error creating news:', data.error);
|
||
alert('Ошибка при создании новости');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating news:', error);
|
||
alert('Ошибка при создании новости');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSaveAsDraft = async () => {
|
||
const draftData = { ...formData, published: false };
|
||
setFormData(draftData);
|
||
|
||
// Сохраняем как черновик
|
||
try {
|
||
const response = await fetch('/api/news', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
...draftData,
|
||
publishedAt: new Date(draftData.publishedAt).toISOString()
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
router.push('/admin/news');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving draft:', error);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-4">
|
||
<Link
|
||
href="/admin/news"
|
||
className="p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md transition-colors"
|
||
>
|
||
<ArrowLeft className="h-5 w-5" />
|
||
</Link>
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900">Создать новость</h1>
|
||
<p className="text-gray-600 mt-1">Заполните форму для создания новой новости</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{/* Main Content */}
|
||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||
<div className="space-y-6">
|
||
{/* Title */}
|
||
<div>
|
||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
||
Заголовок *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="title"
|
||
value={formData.title}
|
||
onChange={(e) => handleTitleChange(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
placeholder="Введите заголовок новости"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* Slug */}
|
||
<div>
|
||
<label htmlFor="slug" className="block text-sm font-medium text-gray-700 mb-2">
|
||
URL (slug) *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="slug"
|
||
value={formData.slug}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, slug: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
placeholder="url-novosti"
|
||
required
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
URL будет: /news/{formData.slug}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
<div>
|
||
<label htmlFor="summary" className="block text-sm font-medium text-gray-700 mb-2">
|
||
Краткое описание *
|
||
</label>
|
||
<textarea
|
||
id="summary"
|
||
value={formData.summary}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, summary: e.target.value }))}
|
||
rows={3}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
placeholder="Краткое описание новости для превью"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Содержание *
|
||
</label>
|
||
<TextEditor
|
||
value={formData.content}
|
||
onChange={(content) => setFormData(prev => ({ ...prev, content }))}
|
||
placeholder="Введите содержание новости..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Image */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Изображение
|
||
</label>
|
||
<ImageUpload
|
||
value={formData.imageUrl}
|
||
onChange={(imageUrl) => setFormData(prev => ({ ...prev, imageUrl }))}
|
||
onRemove={() => setFormData(prev => ({ ...prev, imageUrl: '' }))}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Settings */}
|
||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Настройки публикации</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{/* Category */}
|
||
<div>
|
||
<label htmlFor="category" className="block text-sm font-medium text-gray-700 mb-2">
|
||
Категория *
|
||
</label>
|
||
<select
|
||
id="category"
|
||
value={formData.category}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
required
|
||
>
|
||
{categories.map((category) => (
|
||
<option key={category.id} value={category.id}>
|
||
{category.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Publish Date */}
|
||
<div>
|
||
<label htmlFor="publishedAt" className="block text-sm font-medium text-gray-700 mb-2">
|
||
Дата публикации *
|
||
</label>
|
||
<input
|
||
type="datetime-local"
|
||
id="publishedAt"
|
||
value={formData.publishedAt}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, publishedAt: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tags */}
|
||
<div className="mt-6">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Теги
|
||
</label>
|
||
<div className="flex flex-wrap gap-2 mb-2">
|
||
{formData.tags.map((tag, index) => (
|
||
<span
|
||
key={index}
|
||
className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||
>
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRemoveTag(tag)}
|
||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="flex space-x-2">
|
||
<input
|
||
type="text"
|
||
value={tagInput}
|
||
onChange={(e) => setTagInput(e.target.value)}
|
||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddTag())}
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
placeholder="Добавить тег"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={handleAddTag}
|
||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
|
||
>
|
||
Добавить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Checkboxes */}
|
||
<div className="mt-6 space-y-3">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.featured}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, featured: e.target.checked }))}
|
||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
/>
|
||
<span className="ml-2 text-sm text-gray-700">Рекомендуемая новость</span>
|
||
</label>
|
||
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.published}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, published: e.target.checked }))}
|
||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
/>
|
||
<span className="ml-2 text-sm text-gray-700">Опубликовать немедленно</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm p-6">
|
||
<Link
|
||
href="/admin/news"
|
||
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
|
||
>
|
||
Отмена
|
||
</Link>
|
||
|
||
<div className="flex space-x-3">
|
||
<button
|
||
type="button"
|
||
onClick={handleSaveAsDraft}
|
||
disabled={loading}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||
>
|
||
Сохранить как черновик
|
||
</button>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center"
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||
Создание...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Save className="h-4 w-4 mr-2" />
|
||
Создать новость
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|