|
|
@@ -7,8 +7,8 @@ import type { Tag, Feature } from '@/types'
|
|
|
/**
|
|
|
* Страница управления тегами и характеристиками.
|
|
|
* Отображает две колонки: список тегов (стили съёмки) и список
|
|
|
- * характеристик. Администратор может добавлять новые записи через
|
|
|
- * форму с полями ID и названия.
|
|
|
+ * характеристик. Администратор может добавлять, редактировать (название)
|
|
|
+ * и удалять записи.
|
|
|
*/
|
|
|
export default function AdminTagsPage() {
|
|
|
const [tags, setTags] = useState<Tag[]>([])
|
|
|
@@ -21,6 +21,11 @@ export default function AdminTagsPage() {
|
|
|
const [newFeatureName, setNewFeatureName] = useState('')
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
+ const [editingTagId, setEditingTagId] = useState<string | null>(null)
|
|
|
+ const [editTagName, setEditTagName] = useState('')
|
|
|
+ const [editingFeatureId, setEditingFeatureId] = useState<string | null>(null)
|
|
|
+ const [editFeatureName, setEditFeatureName] = useState('')
|
|
|
+
|
|
|
const fetchData = async () => {
|
|
|
try {
|
|
|
const [t, f] = await Promise.all([
|
|
|
@@ -62,6 +67,72 @@ export default function AdminTagsPage() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ const startEditTag = (tag: Tag) => {
|
|
|
+ setEditingTagId(tag.id)
|
|
|
+ setEditTagName(tag.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ const cancelEditTag = () => {
|
|
|
+ setEditingTagId(null)
|
|
|
+ setEditTagName('')
|
|
|
+ }
|
|
|
+
|
|
|
+ const saveTag = async (id: string) => {
|
|
|
+ if (!editTagName) return
|
|
|
+ try {
|
|
|
+ await api.put('/admin/tags', { id, name: editTagName })
|
|
|
+ cancelEditTag()
|
|
|
+ setError('')
|
|
|
+ fetchData()
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Ошибка обновления тега')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const deleteTag = async (id: string) => {
|
|
|
+ if (!confirm('Удалить тег «' + id + '»?')) return
|
|
|
+ try {
|
|
|
+ await api.delete('/admin/tags?id=' + encodeURIComponent(id))
|
|
|
+ setError('')
|
|
|
+ fetchData()
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Ошибка удаления тега')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const startEditFeature = (feat: Feature) => {
|
|
|
+ setEditingFeatureId(feat.id)
|
|
|
+ setEditFeatureName(feat.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ const cancelEditFeature = () => {
|
|
|
+ setEditingFeatureId(null)
|
|
|
+ setEditFeatureName('')
|
|
|
+ }
|
|
|
+
|
|
|
+ const saveFeature = async (id: string) => {
|
|
|
+ if (!editFeatureName) return
|
|
|
+ try {
|
|
|
+ await api.put('/admin/features', { id, name: editFeatureName })
|
|
|
+ cancelEditFeature()
|
|
|
+ setError('')
|
|
|
+ fetchData()
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Ошибка обновления характеристики')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const deleteFeature = async (id: string) => {
|
|
|
+ if (!confirm('Удалить характеристику «' + id + '»?')) return
|
|
|
+ try {
|
|
|
+ await api.delete('/admin/features?id=' + encodeURIComponent(id))
|
|
|
+ setError('')
|
|
|
+ fetchData()
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Ошибка удаления характеристики')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (loading) return <p className="text-white/60">Загрузка...</p>
|
|
|
|
|
|
return (
|
|
|
@@ -89,8 +160,43 @@ export default function AdminTagsPage() {
|
|
|
<div className="max-h-[calc(100vh-13rem)] space-y-1 overflow-y-auto pb-8 pr-1">
|
|
|
{tags.map((tag) => (
|
|
|
<div key={tag.id} className="flex items-center justify-between rounded-lg bg-white/5 px-3 py-2">
|
|
|
- <span className="text-sm text-white">{tag.name}</span>
|
|
|
- <span className="text-xs text-white/40">{tag.id}</span>
|
|
|
+ {editingTagId === tag.id ? (
|
|
|
+ <div className="flex flex-1 items-center gap-2">
|
|
|
+ <input value={editTagName} onChange={(e) => setEditTagName(e.target.value)}
|
|
|
+ className="flex-1 rounded border border-white/10 bg-white/10 px-2 py-1 text-sm text-white outline-none"
|
|
|
+ autoFocus
|
|
|
+ onKeyDown={(e) => {
|
|
|
+ if (e.key === 'Enter') saveTag(tag.id)
|
|
|
+ if (e.key === 'Escape') cancelEditTag()
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <button onClick={() => saveTag(tag.id)}
|
|
|
+ className="rounded bg-green-600 px-2 py-1 text-xs text-white hover:bg-green-500">
|
|
|
+ ✓
|
|
|
+ </button>
|
|
|
+ <button onClick={cancelEditTag}
|
|
|
+ className="rounded bg-white/10 px-2 py-1 text-xs text-white/60 hover:text-white">
|
|
|
+ ✕
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <span className="text-sm text-white">{tag.name}</span>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-xs text-white/40">{tag.id}</span>
|
|
|
+ <button onClick={() => startEditTag(tag)}
|
|
|
+ className="text-xs text-white/40 hover:text-white"
|
|
|
+ title="Редактировать">
|
|
|
+ ✏️
|
|
|
+ </button>
|
|
|
+ <button onClick={() => deleteTag(tag.id)}
|
|
|
+ className="text-xs text-white/40 hover:text-red-400"
|
|
|
+ title="Удалить">
|
|
|
+ 🗑️
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -114,8 +220,43 @@ export default function AdminTagsPage() {
|
|
|
<div className="max-h-[calc(100vh-13rem)] space-y-1 overflow-y-auto pb-8 pr-1">
|
|
|
{features.map((feat) => (
|
|
|
<div key={feat.id} className="flex items-center justify-between rounded-lg bg-white/5 px-3 py-2">
|
|
|
- <span className="text-sm text-white">{feat.name}</span>
|
|
|
- <span className="text-xs text-white/40">{feat.id}</span>
|
|
|
+ {editingFeatureId === feat.id ? (
|
|
|
+ <div className="flex flex-1 items-center gap-2">
|
|
|
+ <input value={editFeatureName} onChange={(e) => setEditFeatureName(e.target.value)}
|
|
|
+ className="flex-1 rounded border border-white/10 bg-white/10 px-2 py-1 text-sm text-white outline-none"
|
|
|
+ autoFocus
|
|
|
+ onKeyDown={(e) => {
|
|
|
+ if (e.key === 'Enter') saveFeature(feat.id)
|
|
|
+ if (e.key === 'Escape') cancelEditFeature()
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <button onClick={() => saveFeature(feat.id)}
|
|
|
+ className="rounded bg-green-600 px-2 py-1 text-xs text-white hover:bg-green-500">
|
|
|
+ ✓
|
|
|
+ </button>
|
|
|
+ <button onClick={cancelEditFeature}
|
|
|
+ className="rounded bg-white/10 px-2 py-1 text-xs text-white/60 hover:text-white">
|
|
|
+ ✕
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <span className="text-sm text-white">{feat.name}</span>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-xs text-white/40">{feat.id}</span>
|
|
|
+ <button onClick={() => startEditFeature(feat)}
|
|
|
+ className="text-xs text-white/40 hover:text-white"
|
|
|
+ title="Редактировать">
|
|
|
+ ✏️
|
|
|
+ </button>
|
|
|
+ <button onClick={() => deleteFeature(feat.id)}
|
|
|
+ className="text-xs text-white/40 hover:text-red-400"
|
|
|
+ title="Удалить">
|
|
|
+ 🗑️
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|