Версия 1.2.18 Добавлены кнопки массового сдвига подписей при пред-просмотре данных статей для импорта. Очистка подписи, фотографа. Тултип на подпись.
This commit is contained in:
parent
95ae42b4e9
commit
ac328c1f23
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.17",
|
"version": "1.2.18",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
|
// Full file: ImportArticlesPage.tsx (combined)
|
||||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { Header } from '../components/Header';
|
import { Header } from '../components/Header';
|
||||||
import { AuthGuard } from '../components/AuthGuard';
|
import { AuthGuard } from '../components/AuthGuard';
|
||||||
import { Article, CategoryTitles, CityTitles } from '../types';
|
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 ARTICLES_PER_PAGE = 10;
|
||||||
const fallbackImg = '/images/lost-image.webp';
|
const fallbackImg = '/images/lost-image.webp';
|
||||||
@ -10,7 +22,7 @@ const fallbackImg = '/images/lost-image.webp';
|
|||||||
export function ImportArticlesPage() {
|
export function ImportArticlesPage() {
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [limitIndexMap, setLimitIndexMap] = useState<Record<string, number | null>>({});
|
||||||
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
|
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
@ -26,41 +38,39 @@ export function ImportArticlesPage() {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
// загрузка JSON файла
|
|
||||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const text = e.target?.result;
|
const text = e.target?.result;
|
||||||
if (!text) throw new Error('Файл пустой или не читается');
|
if (!text) throw new Error('Файл пустой или не читается');
|
||||||
|
|
||||||
const jsonData = JSON.parse(text as string);
|
const jsonData = JSON.parse(text as string);
|
||||||
|
|
||||||
if (Array.isArray(jsonData)) {
|
if (Array.isArray(jsonData)) {
|
||||||
const normalized = jsonData.map((article: Article) => ({
|
const normalized = jsonData.map((article: Article, idx: number) => ({
|
||||||
...article,
|
...article,
|
||||||
id: String(article.importId),
|
id: String((article as Article).importId ?? `imp-${idx}`),
|
||||||
images: Array.isArray(article.images) ? [...article.images] : [],
|
images: Array.isArray(article.images) ? [...article.images] : [],
|
||||||
imageSubs: Array.isArray(article.imageSubs) ? [...article.imageSubs] : [],
|
imageSubs: Array.isArray(article.imageSubs) ? [...article.imageSubs] : [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setArticles(normalized);
|
setArticles(normalized);
|
||||||
|
const newMap: Record<string, number | null> = {};
|
||||||
|
normalized.forEach(a => { newMap[a.id] = null; });
|
||||||
|
setLimitIndexMap(newMap);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setError(null);
|
setError(null);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Неправильный формат JSON. Ожидается массив статей.');
|
throw new Error('Неправильный формат JSON. Ожидается массив статей.');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
setError('Ошибка разбора JSON файла. Проверьте формат файла.');
|
setError('Ошибка разбора JSON файла. Проверьте формат файла.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
// универсальное редактирование полей статьи
|
|
||||||
const handleEditField = <K extends keyof Article>(
|
const handleEditField = <K extends keyof Article>(
|
||||||
articleId: string,
|
articleId: string,
|
||||||
field: K,
|
field: K,
|
||||||
@ -78,55 +88,94 @@ export function ImportArticlesPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// редактирование текста подписи
|
|
||||||
const handleImageSubEdit = (articleId: string, index: number, newValue: string) => {
|
const handleImageSubEdit = (articleId: string, index: number, newValue: string) => {
|
||||||
setArticles(prev =>
|
setArticles(prev =>
|
||||||
prev.map(article => {
|
prev.map(article => {
|
||||||
if (article.id !== articleId) return article;
|
if (article.id !== articleId) return article;
|
||||||
|
|
||||||
const subs = [...(article.imageSubs ?? [])];
|
const subs = [...(article.imageSubs ?? [])];
|
||||||
|
if (index >= subs.length) {
|
||||||
|
for (let i = subs.length; i <= index; i++) subs[i] = '';
|
||||||
|
}
|
||||||
subs[index] = newValue;
|
subs[index] = newValue;
|
||||||
|
|
||||||
return { ...article, imageSubs: subs };
|
return { ...article, imageSubs: subs };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// перенос подписи ВЛЕВО
|
const swapLeft = (articleId: string, index: number) => {
|
||||||
const moveSubLeft = (articleId: string, index: number) => {
|
if (index <= 0) return;
|
||||||
if (index === 0) return;
|
|
||||||
setArticles(prev =>
|
setArticles(prev =>
|
||||||
prev.map(article => {
|
prev.map(article => {
|
||||||
if (article.id !== articleId) return article;
|
if (article.id !== articleId) return article;
|
||||||
|
|
||||||
const subs = [...(article.imageSubs ?? [])];
|
const subs = [...(article.imageSubs ?? [])];
|
||||||
|
if (index >= subs.length) return article;
|
||||||
[subs[index - 1], subs[index]] = [subs[index], subs[index - 1]];
|
[subs[index - 1], subs[index]] = [subs[index], subs[index - 1]];
|
||||||
return { ...article, imageSubs: subs };
|
return { ...article, imageSubs: subs };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// перенос подписи ВПРАВО
|
const swapRight = (articleId: string, index: number) => {
|
||||||
const moveSubRight = (articleId: string, index: number) => {
|
|
||||||
setArticles(prev =>
|
setArticles(prev =>
|
||||||
prev.map(article => {
|
prev.map(article => {
|
||||||
if (article.id !== articleId) return article;
|
if (article.id !== articleId) return article;
|
||||||
|
|
||||||
if (index >= (article.imageSubs ?? []).length - 1) return article;
|
|
||||||
|
|
||||||
const subs = [...(article.imageSubs ?? [])];
|
const subs = [...(article.imageSubs ?? [])];
|
||||||
|
if (index < 0 || index >= subs.length - 1) return article;
|
||||||
[subs[index], subs[index + 1]] = [subs[index + 1], subs[index]];
|
[subs[index], subs[index + 1]] = [subs[index + 1], subs[index]];
|
||||||
return { ...article, imageSubs: subs };
|
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 () => {
|
const handleSaveToBackend = useCallback(async () => {
|
||||||
if (articles.length === 0) return;
|
if (articles.length === 0) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/articles/import', {
|
const response = await fetch('/api/articles/import', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -136,13 +185,12 @@ export function ImportArticlesPage() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(articles),
|
body: JSON.stringify(articles),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Ошибка сервера: ${response.statusText}`);
|
throw new Error(`Ошибка сервера: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowSuccessModal(true);
|
setShowSuccessModal(true);
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
setError('Не удалось сохранить статьи. Попробуйте снова.');
|
setError('Не удалось сохранить статьи. Попробуйте снова.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@ -157,7 +205,6 @@ export function ImportArticlesPage() {
|
|||||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
<div className="px-4 py-6 sm:px-0">
|
<div className="px-4 py-6 sm:px-0">
|
||||||
|
|
||||||
{/* Header panel */}
|
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Импорт статей</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Импорт статей</h1>
|
||||||
|
|
||||||
@ -191,24 +238,19 @@ export function ImportArticlesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Errors */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-8 bg-red-50 text-red-700 p-4 rounded-md">
|
<div className="mb-8 bg-red-50 text-red-700 p-4 rounded-md">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* List of articles */}
|
|
||||||
{articles.length > 0 ? (
|
{articles.length > 0 ? (
|
||||||
<div className="bg-white shadow sm:rounded-md overflow-hidden">
|
<div className="bg-white shadow sm:rounded-md overflow-hidden">
|
||||||
<ul className="divide-y divide-gray-200">
|
<ul className="divide-y divide-gray-200">
|
||||||
|
|
||||||
{currentArticles.map(article => (
|
{currentArticles.map(article => (
|
||||||
<li key={article.id} className="px-6 py-4">
|
<li key={article.id} className="px-6 py-4">
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 space-y-3">
|
<div className="flex-1 min-w-0 space-y-3">
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -216,7 +258,6 @@ export function ImportArticlesPage() {
|
|||||||
onChange={(e) => handleEditField(article.id, 'title', e.target.value)}
|
onChange={(e) => handleEditField(article.id, 'title', e.target.value)}
|
||||||
className="block w-full text-sm font-medium border-0 focus:ring-0 p-0"
|
className="block w-full text-sm font-medium border-0 focus:ring-0 p-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingArticle(article)}
|
onClick={() => setEditingArticle(article)}
|
||||||
className="ml-4 p-2 text-gray-400 hover:text-blue-600 rounded-full hover:bg-blue-50"
|
className="ml-4 p-2 text-gray-400 hover:text-blue-600 rounded-full hover:bg-blue-50"
|
||||||
@ -225,7 +266,6 @@ export function ImportArticlesPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Excerpt */}
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={article.excerpt}
|
value={article.excerpt}
|
||||||
@ -233,12 +273,10 @@ export function ImportArticlesPage() {
|
|||||||
className="block w-full text-sm text-gray-500 border-0 focus:ring-0 p-0"
|
className="block w-full text-sm text-gray-500 border-0 focus:ring-0 p-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Meta */}
|
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read
|
{CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cover image */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-4">
|
||||||
<div className="border rounded-md p-2 bg-gray-50">
|
<div className="border rounded-md p-2 bg-gray-50">
|
||||||
<img
|
<img
|
||||||
@ -254,7 +292,6 @@ export function ImportArticlesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Author fields */}
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<label className="block text-sm font-medium mb-1">Автор:</label>
|
<label className="block text-sm font-medium mb-1">Автор:</label>
|
||||||
<input
|
<input
|
||||||
@ -277,72 +314,121 @@ export function ImportArticlesPage() {
|
|||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<label className="block text-sm font-medium mb-1">Фотограф:</label>
|
<label className="block text-sm font-medium mb-1">Фотограф:</label>
|
||||||
<input
|
<div className="flex items-center gap-2">
|
||||||
type="text"
|
<button
|
||||||
value={article.photographerName}
|
onClick={() =>
|
||||||
onChange={(e) => handleEditField(article.id, 'photographerName', e.target.value)}
|
handleEditField(article.id, 'photographerName', '')
|
||||||
className="w-full border border-gray-300 rounded px-3 py-1 text-sm"
|
}
|
||||||
/>
|
title="Очистить поле «Фотограф»"
|
||||||
|
className="p-1 bg-white border rounded hover:bg-red-50 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={article.photographerName}
|
||||||
|
onChange={(e) => handleEditField(article.id, 'photographerName', e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded px-3 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images and subs */}
|
|
||||||
{(article.images?.length || 0) > 0 && (
|
{(article.images?.length || 0) > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Изображения с подписями:</h4>
|
||||||
Изображения с подписями:
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{(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) => (
|
return (
|
||||||
<div key={index} className="border rounded-md p-2 bg-gray-50">
|
<div key={`${article.id}-${index}`} className="border rounded-md p-2 bg-gray-50">
|
||||||
|
<img
|
||||||
{/* Image */}
|
src={imageUrl}
|
||||||
<img
|
alt={`image-${index + 1}`}
|
||||||
src={imageUrl}
|
className="w-full h-48 object-cover rounded-md mb-2"
|
||||||
alt=""
|
onError={(e) => {
|
||||||
className="w-full h-48 object-cover rounded-md mb-2"
|
const tgt = e.currentTarget;
|
||||||
onError={(e) => {
|
tgt.onerror = null;
|
||||||
const tgt = e.currentTarget;
|
tgt.src = fallbackImg;
|
||||||
tgt.onerror = null;
|
}}
|
||||||
tgt.src = fallbackImg;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Row with buttons and input */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
|
|
||||||
{/* Move left */}
|
|
||||||
<button
|
|
||||||
onClick={() => moveSubLeft(article.id, index)}
|
|
||||||
disabled={index === 0}
|
|
||||||
className="px-2 py-1 bg-gray-200 text-gray-700 hover:bg-gray-300 rounded disabled:opacity-40"
|
|
||||||
title="Переместить подпись влево"
|
|
||||||
>
|
|
||||||
←
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Input field */}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={(article.imageSubs ?? [])[index] || ''}
|
|
||||||
onChange={(e) => handleImageSubEdit(article.id, index, e.target.value)}
|
|
||||||
className="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Move right */}
|
<div className="flex items-center mb-2">
|
||||||
<button
|
<input
|
||||||
onClick={() => moveSubRight(article.id, index)}
|
id={`lim-${article.id}-${index}`}
|
||||||
disabled={index === (article.imageSubs ?? []).length - 1}
|
type="checkbox"
|
||||||
className="px-2 py-1 bg-gray-200 text-gray-700 hover:bg-gray-300 rounded disabled:opacity-40"
|
checked={limiterIndex === index}
|
||||||
title="Переместить подпись вправо"
|
onChange={() => toggleLimiter(article.id, index)}
|
||||||
>
|
className="mr-2"
|
||||||
→
|
/>
|
||||||
</button>
|
<label htmlFor={`lim-${article.id}-${index}`} className="text-xs text-gray-600">
|
||||||
</div>
|
Ограничитель сдвига (право)
|
||||||
</div>
|
</label>
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => swapLeft(article.id, index)}
|
||||||
|
disabled={isFirst}
|
||||||
|
title="Поменять местами с левым"
|
||||||
|
className="p-1 bg-white border rounded text-sm disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => shiftLeft(article.id, index)}
|
||||||
|
disabled={subsLen === 0 || index < 0 || index >= subsLen}
|
||||||
|
title="Сдвинуть группу влево"
|
||||||
|
className="p-1 bg-white border rounded text-sm disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ChevronsLeft size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(article.imageSubs ?? [])[index] ?? ''}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => shiftRight(article.id, index)}
|
||||||
|
disabled={subsLen === 0 || index < 0 || index >= subsLen}
|
||||||
|
title="Сдвинуть группу вправо"
|
||||||
|
className="p-1 bg-white border rounded text-sm disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ChevronsRight size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => swapRight(article.id, index)}
|
||||||
|
disabled={isLast}
|
||||||
|
title="Поменять местами с правым"
|
||||||
|
className="p-1 bg-white border rounded text-sm disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ArrowRight size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* КНОПКА КОРЗИНЫ — НОВАЯ */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleImageSubEdit(article.id, index, '...')}
|
||||||
|
title="Очистить подпись"
|
||||||
|
className="p-1 bg-white border rounded hover:bg-red-50 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -352,7 +438,6 @@ export function ImportArticlesPage() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||||
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||||
@ -404,9 +489,9 @@ export function ImportArticlesPage() {
|
|||||||
<p className="text-sm text-gray-500">Начните с выбора JSON файла.</p>
|
<p className="text-sm text-gray-500">Начните с выбора JSON файла.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Success modal */}
|
|
||||||
{showSuccessModal && (
|
{showSuccessModal && (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-30 z-50">
|
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-30 z-50">
|
||||||
<div className="bg-white p-6 rounded shadow-lg max-w-sm w-full text-center">
|
<div className="bg-white p-6 rounded shadow-lg max-w-sm w-full text-center">
|
||||||
@ -421,7 +506,6 @@ export function ImportArticlesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user