diff --git a/package.json b/package.json index c780c26..2c046f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-react-typescript-starter", "private": true, - "version": "1.2.17", + "version": "1.2.18", "type": "module", "scripts": { "dev": "vite", diff --git a/src/pages/ImportArticlesPage.tsx b/src/pages/ImportArticlesPage.tsx index 8ef7117..5821339 100644 --- a/src/pages/ImportArticlesPage.tsx +++ b/src/pages/ImportArticlesPage.tsx @@ -1,8 +1,20 @@ +// Full file: ImportArticlesPage.tsx (combined) import React, { useState, useRef, useCallback, useEffect } from 'react'; import { Header } from '../components/Header'; import { AuthGuard } from '../components/AuthGuard'; import { Article, CategoryTitles, CityTitles } from '../types'; -import { FileJson, Save, ChevronLeft, ChevronRight, Edit2 } from 'lucide-react'; +import { + FileJson, + Save, + ChevronLeft, + ChevronRight, + Edit2, + Trash2, + ArrowLeft, + ArrowRight, + ChevronsLeft, + ChevronsRight +} from 'lucide-react'; const ARTICLES_PER_PAGE = 10; const fallbackImg = '/images/lost-image.webp'; @@ -10,7 +22,7 @@ const fallbackImg = '/images/lost-image.webp'; export function ImportArticlesPage() { const [articles, setArticles] = useState([]); const [currentPage, setCurrentPage] = useState(1); - + const [limitIndexMap, setLimitIndexMap] = useState>({}); const [editingArticle, setEditingArticle] = useState
(null); const [isSaving, setIsSaving] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false); @@ -26,41 +38,39 @@ export function ImportArticlesPage() { window.scrollTo({ top: 0, behavior: 'smooth' }); }, [currentPage]); - // загрузка JSON файла const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; - const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target?.result; if (!text) throw new Error('Файл пустой или не читается'); - const jsonData = JSON.parse(text as string); - if (Array.isArray(jsonData)) { - const normalized = jsonData.map((article: Article) => ({ + const normalized = jsonData.map((article: Article, idx: number) => ({ ...article, - id: String(article.importId), + id: String((article as Article).importId ?? `imp-${idx}`), images: Array.isArray(article.images) ? [...article.images] : [], imageSubs: Array.isArray(article.imageSubs) ? [...article.imageSubs] : [], })); - setArticles(normalized); + const newMap: Record = {}; + normalized.forEach(a => { newMap[a.id] = null; }); + setLimitIndexMap(newMap); setCurrentPage(1); setError(null); } else { throw new Error('Неправильный формат JSON. Ожидается массив статей.'); } - } catch { + } catch (err) { + console.error(err); setError('Ошибка разбора JSON файла. Проверьте формат файла.'); } }; reader.readAsText(file); }; - // универсальное редактирование полей статьи const handleEditField = ( articleId: string, field: K, @@ -78,55 +88,94 @@ export function ImportArticlesPage() { ); }; - // редактирование текста подписи const handleImageSubEdit = (articleId: string, index: number, newValue: string) => { setArticles(prev => prev.map(article => { if (article.id !== articleId) return article; - const subs = [...(article.imageSubs ?? [])]; + if (index >= subs.length) { + for (let i = subs.length; i <= index; i++) subs[i] = ''; + } subs[index] = newValue; - return { ...article, imageSubs: subs }; }) ); }; - // перенос подписи ВЛЕВО - const moveSubLeft = (articleId: string, index: number) => { - if (index === 0) return; + const swapLeft = (articleId: string, index: number) => { + if (index <= 0) return; setArticles(prev => prev.map(article => { if (article.id !== articleId) return article; - const subs = [...(article.imageSubs ?? [])]; + if (index >= subs.length) return article; [subs[index - 1], subs[index]] = [subs[index], subs[index - 1]]; return { ...article, imageSubs: subs }; }) ); }; - // перенос подписи ВПРАВО - const moveSubRight = (articleId: string, index: number) => { + const swapRight = (articleId: string, index: number) => { setArticles(prev => prev.map(article => { if (article.id !== articleId) return article; - - if (index >= (article.imageSubs ?? []).length - 1) return article; - const subs = [...(article.imageSubs ?? [])]; + if (index < 0 || index >= subs.length - 1) return article; [subs[index], subs[index + 1]] = [subs[index + 1], subs[index]]; return { ...article, imageSubs: subs }; }) ); }; - // отправка на backend + const shiftRight = (articleId: string, index: number) => { + setArticles(prev => + prev.map(article => { + if (article.id !== articleId) return article; + const subs = [...(article.imageSubs ?? [])]; + const total = subs.length; + if (index < 0 || index >= total) return article; + const limit = limitIndexMap[articleId]; + const end = (limit !== undefined && limit !== null && limit > index) ? Math.min(limit, total - 1) : total - 1; + if (end <= index) return article; + for (let i = end; i > index; i--) { + subs[i] = subs[i - 1]; + } + subs[index] = ''; + return { ...article, imageSubs: subs }; + }) + ); + }; + + const shiftLeft = (articleId: string, index: number) => { + setArticles(prev => + prev.map(article => { + if (article.id !== articleId) return article; + const subs = [...(article.imageSubs ?? [])]; + const total = subs.length; + if (index < 0 || index >= total) return article; + const limit = limitIndexMap[articleId]; + const end = (limit !== undefined && limit !== null && limit > index) ? Math.min(limit, total - 1) : total - 1; + if (end <= index) return article; + for (let i = index; i < end; i++) { + subs[i] = subs[i + 1]; + } + subs[end] = ''; + return { ...article, imageSubs: subs }; + }) + ); + }; + + const toggleLimiter = (articleId: string, index: number) => { + setLimitIndexMap(prev => { + const prevVal = prev[articleId] ?? null; + return { ...prev, [articleId]: prevVal === index ? null : index }; + }); + }; + const handleSaveToBackend = useCallback(async () => { if (articles.length === 0) return; setIsSaving(true); setError(null); - try { const response = await fetch('/api/articles/import', { method: 'POST', @@ -136,13 +185,12 @@ export function ImportArticlesPage() { }, body: JSON.stringify(articles), }); - if (!response.ok) { throw new Error(`Ошибка сервера: ${response.statusText}`); } - setShowSuccessModal(true); - } catch { + } catch (err) { + console.error(err); setError('Не удалось сохранить статьи. Попробуйте снова.'); } finally { setIsSaving(false); @@ -157,7 +205,6 @@ export function ImportArticlesPage() {
- {/* Header panel */}

Импорт статей

@@ -191,24 +238,19 @@ export function ImportArticlesPage() {
- {/* Errors */} {error && (
{error}
)} - {/* List of articles */} {articles.length > 0 ? (
    - {currentArticles.map(article => (
  • -
    - {/* Title */}
    handleEditField(article.id, 'title', e.target.value)} className="block w-full text-sm font-medium border-0 focus:ring-0 p-0" /> -
    - {/* Excerpt */} - {/* Meta */}
    {CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read
    - {/* Cover image */}
    - {/* Author fields */}
    - handleEditField(article.id, 'photographerName', e.target.value)} - className="w-full border border-gray-300 rounded px-3 py-1 text-sm" - /> +
    + + + handleEditField(article.id, 'photographerName', e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-1 text-sm" + /> +
    - {/* Images and subs */} {(article.images?.length || 0) > 0 && (
    -

    - Изображения с подписями: -

    +

    Изображения с подписями:

    + {(article.images ?? []).map((imageUrl, index) => { + const subsLen = (article.imageSubs ?? []).length; + const isFirst = index === 0; + const isLast = index === subsLen - 1; + const limiterIndex = limitIndexMap[article.id] ?? null; - {(article.images ?? []).map((imageUrl, index) => ( -
    - - {/* Image */} - { - const tgt = e.currentTarget; - tgt.onerror = null; - tgt.src = fallbackImg; - }} - /> - - {/* Row with buttons and input */} -
    - - {/* Move left */} - - - {/* Input field */} - handleImageSubEdit(article.id, index, e.target.value)} - className="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" + return ( +
    + {`image-${index { + const tgt = e.currentTarget; + tgt.onerror = null; + tgt.src = fallbackImg; + }} /> - {/* Move right */} - -
    -
    - ))} +
    + toggleLimiter(article.id, index)} + className="mr-2" + /> + +
    +
    + + + + + handleImageSubEdit(article.id, index, e.target.value)} + className="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" + placeholder={`Подпись ${index + 1}`} + title={(article.imageSubs ?? [])[index] ?? ''} // <<==== tooltip + /> + + + + + + {/* КНОПКА КОРЗИНЫ — НОВАЯ */} + +
    +
    + ); + })}
    )} @@ -352,7 +438,6 @@ export function ImportArticlesPage() { ))}
- {/* Pagination */} {totalPages > 1 && (
@@ -404,9 +489,9 @@ export function ImportArticlesPage() {

Начните с выбора JSON файла.

)} +
- {/* Success modal */} {showSuccessModal && (
@@ -421,7 +506,6 @@ export function ImportArticlesPage() {
)} -