diff --git a/public/images/bg-film.webp b/public/images/bg-film.webp new file mode 100644 index 0000000..7b89622 Binary files /dev/null and b/public/images/bg-film.webp differ diff --git a/public/images/bg-music.webp b/public/images/bg-music.webp new file mode 100644 index 0000000..beca63c Binary files /dev/null and b/public/images/bg-music.webp differ diff --git a/public/images/bg-sport.webp b/public/images/bg-sport.webp new file mode 100644 index 0000000..0a7f11e Binary files /dev/null and b/public/images/bg-sport.webp differ diff --git a/public/images/bg-theatre.webp b/public/images/bg-theatre.webp new file mode 100644 index 0000000..6df3ad4 Binary files /dev/null and b/public/images/bg-theatre.webp differ diff --git a/public/images/gpt_film.webp b/public/images/gpt_film.webp new file mode 100644 index 0000000..2b2dbc2 Binary files /dev/null and b/public/images/gpt_film.webp differ diff --git a/public/images/gpt_theatre.webp b/public/images/gpt_theatre.webp new file mode 100644 index 0000000..cd51fce Binary files /dev/null and b/public/images/gpt_theatre.webp differ diff --git a/src/components/ArticleDeleteModal.tsx b/src/components/ArticleDeleteModal.tsx new file mode 100644 index 0000000..bb917f8 --- /dev/null +++ b/src/components/ArticleDeleteModal.tsx @@ -0,0 +1,29 @@ +interface DeleteModalProps { + onConfirm: () => void; + onCancel: () => void; +} + +export function ArticleDeleteModal({ onConfirm, onCancel }: DeleteModalProps) { + return ( +
+
+

Удалить статью

+

Вы уверены? Это действие нельзя отменить.

+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/ArticleForm.tsx b/src/components/ArticleForm.tsx new file mode 100644 index 0000000..6e678d7 --- /dev/null +++ b/src/components/ArticleForm.tsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect } from 'react'; +import { TipTapEditor } from './TipTapEditor.tsx'; +import { CoverImageUpload } from './ImageUpload/CoverImageUpload.tsx'; +import { ImageUploader } from './ImageUpload/ImageUploader.tsx'; +import { GalleryManager } from './GalleryManager'; +import { useGallery } from '../hooks/useGallery'; +import { CategoryTitles, CityTitles, Author, GalleryImage, ArticleData } from '../types'; +import { useAuthStore } from '../stores/authStore'; + +interface FormState { + title: string; + excerpt: string; + categoryId: number; + cityId: number; + coverImage: string; + readTime: number; + content: string; + authorId: string; + galleryImages: GalleryImage[]; +} + +interface ArticleFormProps { + editingId: string | null; + articleId: string; + initialFormState: FormState | null; + onSubmit: (articleData: ArticleData, closeForm: boolean) => Promise; + onCancel: () => void; + authors: Author[]; + availableCategoryIds: number[]; + availableCityIds: number[]; +} + +export function ArticleForm({ + editingId, + articleId, + initialFormState, + onSubmit, + onCancel, + authors, + availableCategoryIds, + availableCityIds, +}: ArticleFormProps) { + const { user } = useAuthStore(); + const isAdmin = user?.permissions.isAdmin || false; + + const [title, setTitle] = useState(''); + const [excerpt, setExcerpt] = useState(''); + const [categoryId, setCategoryId] = useState(availableCategoryIds[0] || 1); + const [cityId, setCityId] = useState(availableCityIds[0] || 1); + const [coverImage, setCoverImage] = useState('/images/cover-placeholder.webp'); + const [readTime, setReadTime] = useState(5); + const [content, setContent] = useState(''); + const [authorId, setAuthorId] = useState(authors[0]?.id || ''); + const [displayedImages, setDisplayedImages] = useState([]); + const [formNewImageUrl, setFormNewImageUrl] = useState(''); + const [error, setError] = useState(null); + const [hasChanges, setHasChanges] = useState(false); + const [isInitialLoad, setIsInitialLoad] = useState(true); + + const { images: galleryImages, loading: galleryLoading, error: galleryError, addImage: addGalleryImage, updateImage: updateGalleryImage, deleteImage: deleteGalleryImage, reorderImages: reorderGalleryImages } = useGallery(editingId || ''); + + useEffect(() => { + if (editingId) { + setDisplayedImages(galleryImages); + } else { + setDisplayedImages([]); + } + }, [editingId, galleryImages]); + + useEffect(() => { + if (initialFormState) { + setTitle(initialFormState.title); + setExcerpt(initialFormState.excerpt); + setCategoryId(initialFormState.categoryId); + setCityId(initialFormState.cityId); + setCoverImage(initialFormState.coverImage); + setReadTime(initialFormState.readTime); + setContent(initialFormState.content); + setAuthorId(initialFormState.authorId); + setDisplayedImages(initialFormState.galleryImages || []); + } + }, [initialFormState]); + + useEffect(() => { + if (!initialFormState) return; + + const currentState: FormState = { title, excerpt, categoryId, cityId, coverImage, readTime, content, authorId, galleryImages: displayedImages }; + const areRequiredFieldsFilled = title.trim() !== '' && excerpt.trim() !== ''; + const hasFormChanges = Object.keys(initialFormState).some(key => { + if (key === 'galleryImages') { + const isDifferent = JSON.stringify(currentState[key]) !== JSON.stringify(initialFormState[key]); + if (isInitialLoad && isDifferent) return false; + return isDifferent; + } + if (key === 'content') { + const isDifferent = JSON.stringify(currentState[key]) !== JSON.stringify(initialFormState[key]); + return isDifferent; + } + const currentValue = typeof currentState[key as keyof FormState] === 'number' ? String(currentState[key as keyof FormState]) : currentState[key as keyof FormState]; + const initialValue = typeof initialFormState[key as keyof FormState] === 'number' ? String(initialFormState[key as keyof FormState]) : initialFormState[key as keyof FormState]; + const isDifferent = currentValue !== initialValue; + return isDifferent; + }); + + setHasChanges(hasFormChanges && areRequiredFieldsFilled); + + if (isInitialLoad) { + setIsInitialLoad(false); + } + }, [title, excerpt, categoryId, cityId, coverImage, readTime, content, authorId, displayedImages, initialFormState, isInitialLoad]); + + const handleSubmit = async (e: React.FormEvent, closeForm: boolean = true) => { + e.preventDefault(); + + if (!title.trim() || !excerpt.trim()) { + setError('Пожалуйста, заполните обязательные поля: Заголовок и Краткое описание.'); + return; + } + + if (!hasChanges) return; + + const selectedAuthor = editingId && isAdmin ? authors.find(a => a.id === authorId) || authors[0] : user; + const articleData = { + title, + excerpt, + categoryId, + cityId, + coverImage, + readTime, + gallery: displayedImages, + content: content || '', + importId: 0, + isActive: false, + author: selectedAuthor, + }; + + await onSubmit(articleData, closeForm); + }; + + const handleApply = (e: React.FormEvent) => { + handleSubmit(e, false); + }; + + if (editingId && galleryLoading) { + return ( +
+
+
+ ); + } + + return ( +
+

{editingId ? 'Редактировать статью' : 'Создать новую статью'}

+ {(error || galleryError) && ( +
{error || galleryError}
+ )} +
handleSubmit(e, true)} className="space-y-6"> +
+ + setTitle(e.target.value)} + className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200" + required + /> +
+
+ +