From 95ae42b4e941391f65da48dceffa9fd5329983f4 Mon Sep 17 00:00:00 2001 From: anibilag Date: Wed, 10 Dec 2025 19:42:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=201.2.17?= =?UTF-8?q?=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BA=D0=BD=D0=BE=D0=BF=D1=80=D0=B8=20=D0=BE=D0=B1=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=D0=B4=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B5=D0=B4-=D0=BF=D1=80=D0=BE=D1=81=D0=BC?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D0=B5=D0=B9=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/pages/ImportArticlesPage.tsx | 453 ++++++++++++++++++------------- 2 files changed, 261 insertions(+), 194 deletions(-) diff --git a/package.json b/package.json index fbc93ea..c780c26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-react-typescript-starter", "private": true, - "version": "1.2.16", + "version": "1.2.17", "type": "module", "scripts": { "dev": "vite", diff --git a/src/pages/ImportArticlesPage.tsx b/src/pages/ImportArticlesPage.tsx index 340e51c..8ef7117 100644 --- a/src/pages/ImportArticlesPage.tsx +++ b/src/pages/ImportArticlesPage.tsx @@ -11,10 +11,6 @@ export function ImportArticlesPage() { const [articles, setArticles] = useState([]); const [currentPage, setCurrentPage] = useState(1); - useEffect(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, [currentPage]); - const [editingArticle, setEditingArticle] = useState
(null); const [isSaving, setIsSaving] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false); @@ -26,6 +22,11 @@ export function ImportArticlesPage() { const endIndex = startIndex + ARTICLES_PER_PAGE; const currentArticles = articles.slice(startIndex, endIndex); + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, [currentPage]); + + // загрузка JSON файла const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; @@ -37,10 +38,11 @@ export function ImportArticlesPage() { if (!text) throw new Error('Файл пустой или не читается'); const jsonData = JSON.parse(text as string); + if (Array.isArray(jsonData)) { const normalized = jsonData.map((article: Article) => ({ ...article, - id: article.importId.toString(), // 👈 теперь будет использоваться корректный id + id: String(article.importId), images: Array.isArray(article.images) ? [...article.images] : [], imageSubs: Array.isArray(article.imageSubs) ? [...article.imageSubs] : [], })); @@ -58,23 +60,68 @@ export function ImportArticlesPage() { reader.readAsText(file); }; + // универсальное редактирование полей статьи const handleEditField = ( articleId: string, field: K, value: Article[K] ) => { + setArticles(prev => + prev.map(article => + article.id === articleId + ? { + ...article, + [field]: Array.isArray(value) ? [...value] : value, + } + : article + ) + ); + }; + + // редактирование текста подписи + const handleImageSubEdit = (articleId: string, index: number, newValue: string) => { setArticles(prev => prev.map(article => { if (article.id !== articleId) return article; - return { - ...article, - [field]: Array.isArray(value) ? [...value] : value, // гарантированно новая ссылка - }; + const subs = [...(article.imageSubs ?? [])]; + subs[index] = newValue; + + return { ...article, imageSubs: subs }; }) ); }; + // перенос подписи ВЛЕВО + const moveSubLeft = (articleId: string, index: number) => { + if (index === 0) return; + setArticles(prev => + prev.map(article => { + if (article.id !== articleId) return article; + + const subs = [...(article.imageSubs ?? [])]; + [subs[index - 1], subs[index]] = [subs[index], subs[index - 1]]; + return { ...article, imageSubs: subs }; + }) + ); + }; + + // перенос подписи ВПРАВО + const moveSubRight = (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 ?? [])]; + [subs[index], subs[index + 1]] = [subs[index + 1], subs[index]]; + return { ...article, imageSubs: subs }; + }) + ); + }; + + // отправка на backend const handleSaveToBackend = useCallback(async () => { if (articles.length === 0) return; setIsSaving(true); @@ -94,7 +141,6 @@ export function ImportArticlesPage() { throw new Error(`Ошибка сервера: ${response.statusText}`); } - setError(null); setShowSuccessModal(true); } catch { setError('Не удалось сохранить статьи. Попробуйте снова.'); @@ -103,38 +149,27 @@ export function ImportArticlesPage() { } }, [articles]); - const handleImageSubEdit = (articleId: string, index: number, newValue: string) => { - setArticles(prev => - prev.map(article => { - if (article.id !== articleId) return article; - - const updatedSubs = article.imageSubs ? [...article.imageSubs] : []; - updatedSubs[index] = newValue; - - return { - ...article, - imageSubs: updatedSubs, // новая ссылка! - }; - }) - ); - }; - return (
+
+ + {/* Header panel */}

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

+
+ + {articles.length > 0 && (
+ {/* Errors */} {error && (
{error}
)} + {/* List of articles */} {articles.length > 0 ? ( - <> -
-
    - {currentArticles.map((article) => ( -
  • +
    +
      + + {currentArticles.map(article => ( +
    • + +
      + + {/* Title */}
      -
      -
      - handleEditField(article.id, 'title', e.target.value)} - className="block w-full text-sm font-medium text-gray-900 border-0 focus:ring-0 p-0" - /> - -
      - handleEditField(article.id, 'excerpt', e.target.value)} - className="block w-full text-sm text-gray-500 border-0 focus:ring-0 p-0" + handleEditField(article.id, 'title', e.target.value)} + className="block w-full text-sm font-medium border-0 focus:ring-0 p-0" + /> + + +
      + + {/* Excerpt */} + handleEditField(article.id, 'excerpt', e.target.value)} + className="block w-full text-sm text-gray-500 border-0 focus:ring-0 p-0" + /> + + {/* Meta */} +
      + {CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read +
      + + {/* Cover image */} +
      +
      + { + const tgt = e.currentTarget; + tgt.onerror = null; + tgt.src = fallbackImg; + }} /> -
      - {CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read -
      - -
      -
      -
      - {""} { - const target = e.currentTarget; - target.onerror = null; - target.src = fallbackImg; - }} - /> -
      -
      -
      - -
      - - - handleEditField(article.id, 'authorName', e.target.value) - } - className="w-full border border-gray-300 rounded px-3 py-1 text-sm" - placeholder="Введите имя автора" - /> -
      - -
      - - - handleEditField(article.id, 'coAuthorName', e.target.value) - } - className="w-full border border-gray-300 rounded px-3 py-1 text-sm" - placeholder="Введите имя соавтора" - /> -
      - -
      - - - handleEditField(article.id, 'photographerName', e.target.value) - } - className="w-full border border-gray-300 rounded px-3 py-1 text-sm" - placeholder="Введите имя фотографа" - /> -
      - - {(article.images?.length || 0) > 0 && ( -
      -

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

      -
      - {article.images?.map((imageUrl, index) => ( -
      - {`image-${index { - const target = e.currentTarget; - target.onerror = null; - target.src = fallbackImg; - }} - /> - handleImageSubEdit(article.id, index, e.target.value)} - placeholder={`Подпись ${index + 1}`} - className="w-full border border-gray-300 rounded px-2 py-1 text-sm" - /> -
      - ))} -
      -
      - )}
      -
    • - ))} -
    - {/* Pagination */} - {totalPages > 1 && ( -
    -
    -
    -

    - Showing {startIndex + 1} to{' '} - {Math.min(endIndex, articles.length)} of{' '} - {articles.length} articles -

    + {/* Author fields */} +
    + + handleEditField(article.id, 'authorName', e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-1 text-sm" + />
    -
    - + +
    + + handleEditField(article.id, 'coAuthorName', 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) => ( +
    + + {/* 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" + /> + + {/* Move right */} + +
    +
    + ))} + +
    +
    + )} +
    +
  • + ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+

+ Showing {startIndex + 1} to{' '} + {Math.min(endIndex, articles.length)} of{' '} + {articles.length} articles +

+ +
- )} -
- +
+ )} +
) : (
-

Еще нет статей для импорта

-

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

+

Еще нет статей для импорта

+

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

)}
- {/* Модальное окно подтверждения загрузки */} + + {/* Success modal */} {showSuccessModal && (
-

✅ Успешно!

+

Успешно!

Статьи успешно сохранены на сервере.

)} +