This commit is contained in:
Your Name committed 2026-10-03 12:56:11 +07:00
1 parent 0c3faaba29
commit b8a255821b
6 files changed
+1820 -432

No files matched your search

+4
View File
@@ -52,6 +52,7 @@ import GlobalAnalyticsPage from './pages/GlobalAnalyticsPage'
import MediaLibraryPage from './pages/MediaLibraryPage'
import ContactMessagesPage from './pages/ContactMessagesPage'
import ProductAttributesPage from './pages/ProductAttributesPage'
import ProductAttributeEditPage from './pages/ProductAttributeEditPage'
import TiktokPriceSimulatorPage from './pages/TiktokPriceSimulatorPage'
import TiktokShopCasePage from './pages/TiktokShopCasePage'
import ShopeeShopCasePage from './pages/ShopeeShopCasePage'
@@ -95,6 +96,9 @@ function App() {
<Route path="/products" element={<ProductsPage />} />
<Route path="/product-attributes" element={<ProductAttributesPage />} />
<Route path="/product-attributes/new" element={<ProductAttributeEditPage />} />
<Route path="/product-attributes/:id" element={<ProductAttributeEditPage />} />
<Route path="/product-attributes/:id/edit" element={<ProductAttributeEditPage />} />
<Route path="/products/new" element={<ProductEditPage />} />
<Route path="/products/:id/edit" element={<ProductEditPage />} />
<Route path="/products/:id/cms" element={<ProductCmsPage />} />
File diff suppressed because it is too large. Load diff
+227 -393
View File
@@ -1,74 +1,32 @@
import { useEffect, useMemo, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { API_BASE, type AttributeDefinition, type Category } from '@homaroma/types'
import { Edit3, FileJson, Plus, Save, Settings2, Trash2, X } from 'lucide-react'
import ContentLanguageTabs from '../components/ContentLanguageTabs'
import {
Edit3,
FileJson,
Plus,
Settings2,
Trash2,
Search,
Filter,
Layers,
Tag,
ListFilter,
} from 'lucide-react'
import ProductAttributeBulkJsonModal from '../components/ProductAttributeBulkJsonModal'
const LANGUAGES = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'] as const
const ATTRIBUTE_TYPES = ['TEXT', 'NUMBER', 'BOOLEAN', 'SELECT', 'JSON'] as const
type EditableDefinition = {
id?: number
code: string
type: typeof ATTRIBUTE_TYPES[number]
unit: string
isRequired: boolean
isFilterable: boolean
sortOrder: number
categoryId: number | null
translations: Record<string, { label: string; description: string }>
options: Record<string, string>[]
}
function emptyDefinition(): EditableDefinition {
return {
code: '',
type: 'TEXT',
unit: '',
isRequired: false,
isFilterable: false,
sortOrder: 0,
categoryId: null,
translations: Object.fromEntries(LANGUAGES.map((lang) => [lang, { label: '', description: '' }])),
options: [],
}
}
function parseDefinition(definition: AttributeDefinition): EditableDefinition {
const translations = Object.fromEntries(
LANGUAGES.map((lang) => [lang, {
label: typeof definition.label === 'string' ? (lang === 'th' ? definition.label : '') : (definition.label?.[lang] || ''),
description: typeof definition.description === 'string' ? (lang === 'th' ? definition.description : '') : (definition.description?.[lang] || ''),
}])
)
return {
id: definition.id,
code: definition.code,
type: definition.type,
unit: definition.unit || '',
isRequired: definition.isRequired,
isFilterable: definition.isFilterable,
sortOrder: definition.sortOrder,
categoryId: definition.categoryId ?? null,
translations,
options: Array.isArray(definition.options)
? definition.options.map((option) => typeof option === 'string' ? { th: option } : option)
: [],
}
}
export default function ProductAttributesPage() {
const { t, i18n } = useTranslation()
const navigate = useNavigate()
const [definitions, setDefinitions] = useState<AttributeDefinition[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [editing, setEditing] = useState<EditableDefinition | null>(null)
const [activeLang, setActiveLang] = useState('th')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isJsonModalOpen, setIsJsonModalOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategoryFilter, setSelectedCategoryFilter] = useState<string>('all')
const fetchData = async () => {
setLoading(true)
@@ -92,67 +50,8 @@ export default function ProductAttributesPage() {
fetchData()
}, [])
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
if (!editing) return
setSaving(true)
setError(null)
try {
const baseLabel = editing.translations.th?.label || editing.translations.en?.label
if (!baseLabel) throw new Error('Thai or English label is required')
const payload = {
code: editing.code,
type: editing.type,
unit: editing.unit,
isRequired: editing.isRequired,
isFilterable: editing.isFilterable,
sortOrder: Number(editing.sortOrder) || 0,
categoryId: editing.categoryId ? Number(editing.categoryId) : null,
label: Object.fromEntries(
Object.entries(editing.translations)
.filter(([, val]) => val.label.trim())
.map(([lang, val]) => [lang, val.label.trim()])
),
description: Object.fromEntries(
Object.entries(editing.translations)
.filter(([, val]) => val.description.trim())
.map(([lang, val]) => [lang, val.description.trim()])
),
options: editing.type === 'SELECT'
? editing.options.map((opt) => Object.fromEntries(
Object.entries(opt).filter(([, val]) => typeof val === 'string' && val.trim())
)).filter((opt) => Object.keys(opt).length > 0)
: undefined,
}
const url = editing.id
? `${API_BASE}/product-attributes/definitions/${editing.id}`
: `${API_BASE}/product-attributes/definitions`
const method = editing.id ? 'PUT' : 'POST'
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
const errData = await res.json().catch(() => null)
throw new Error(errData?.message || 'Failed to save attribute definition')
}
await fetchData()
setEditing(null)
} catch (err: any) {
setError(err.message)
} finally {
setSaving(false)
}
}
const handleDelete = async (id: number) => {
if (!confirm(t('common.confirmDelete', 'Are you sure you want to delete this attribute definition?'))) return
const handleDelete = async (id: number, label: string) => {
if (!confirm(t('common.confirmDelete', `Are you sure you want to delete attribute "${label}"?`))) return
try {
const res = await fetch(`${API_BASE}/product-attributes/definitions/${id}`, { method: 'DELETE' })
if (!res.ok) {
@@ -160,7 +59,6 @@ export default function ProductAttributesPage() {
throw new Error(errData?.message || 'Failed to delete attribute definition')
}
await fetchData()
if (editing?.id === id) setEditing(null)
} catch (err: any) {
setError(err.message)
}
@@ -170,47 +68,58 @@ export default function ProductAttributesPage() {
const map = new Map<number, string>()
const currentLang = i18n.language
categories.forEach((cat) => {
const name = typeof cat.name === 'string'
? cat.name
: (cat.name?.[currentLang] || cat.name?.th || cat.name?.en || `Category #${cat.id}`)
const name =
typeof cat.name === 'string'
? cat.name
: cat.name?.[currentLang] || cat.name?.th || cat.name?.en || `Category #${cat.id}`
map.set(cat.id, name)
})
return map
}, [categories, i18n.language])
const handleCopyFromLang = (fromLang: string) => {
if (!editing) return
const src = editing.translations[fromLang]
if (!src) return
setEditing({
...editing,
translations: {
...editing.translations,
[activeLang]: {
label: src.label || '',
description: src.description || '',
},
},
})
}
// Filter & Search attributes
const filteredDefinitions = useMemo(() => {
return definitions.filter((def) => {
// Category filter
if (selectedCategoryFilter === 'global' && def.categoryId !== null) return false
if (selectedCategoryFilter !== 'all' && selectedCategoryFilter !== 'global') {
if (def.categoryId !== Number(selectedCategoryFilter)) return false
}
const labelTranslationsMap = useMemo(() => {
if (!editing) return {}
return Object.fromEntries(
Object.entries(editing.translations).map(([lang, val]) => [lang, val.label])
)
}, [editing])
// Search query
if (!searchQuery.trim()) return true
const q = searchQuery.toLowerCase()
const codeMatch = def.code.toLowerCase().includes(q)
const labelMatch =
typeof def.label === 'string'
? def.label.toLowerCase().includes(q)
: Object.values(def.label || {}).some((v) => typeof v === 'string' && v.toLowerCase().includes(q))
const descMatch =
typeof def.description === 'string'
? def.description.toLowerCase().includes(q)
: Object.values(def.description || {}).some((v) => typeof v === 'string' && v.toLowerCase().includes(q))
return codeMatch || labelMatch || descMatch
})
}, [definitions, searchQuery, selectedCategoryFilter])
return (
<div className="space-y-8 animate-fade-in">
<div className="space-y-6 max-w-7xl mx-auto pb-12 animate-fade-in">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-cyan-glow">
<div className="flex items-center gap-2.5 text-cyan-glow">
<Settings2 className="w-6 h-6" />
<h1 className="text-3xl font-bold tracking-tight">{t('nav.productAttributes', 'Product Attributes')}</h1>
<h1 className="text-3xl font-bold tracking-tight text-white">
{t('nav.productAttributes', 'Product Attributes')}
</h1>
</div>
<p className="text-slate-500 text-sm">Define multilingual specs, units, and select options for products.</p>
<p className="text-slate-400 text-sm">
Manage multilingual specifications, units, facet filters, and select options for products.
</p>
</div>
<div className="flex items-center gap-3">
<button
type="button"
@@ -220,16 +129,14 @@ export default function ProductAttributesPage() {
<FileJson className="w-4 h-4 text-cyan-glow" />
<span>JSON Import / Export</span>
</button>
<button
onClick={() => {
setEditing(emptyDefinition())
setActiveLang('th')
}}
className="btn-primary flex items-center gap-2"
<Link
to="/product-attributes/new"
className="btn-primary flex items-center gap-2 px-4 py-2.5"
>
<Plus className="w-4 h-4" />
{t('common.addNew', 'Add Definition')}
</button>
<span>{t('common.addNew', 'Add Definition')}</span>
</Link>
</div>
</div>
@@ -247,252 +154,179 @@ export default function ProductAttributesPage() {
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
<section className="admin-card space-y-4">
<h2 className="font-bold text-lg text-white">Defined Attributes ({definitions.length})</h2>
<div className="space-y-3">
{loading && Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-24 rounded-2xl bg-white/5 animate-pulse" />
{/* Toolbar & Filters */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-white/[0.02] border border-white/5 p-4 rounded-2xl">
<div className="relative w-full sm:w-80">
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search attributes by name or code..."
className="w-full pl-10 pr-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white placeholder-slate-500 focus:border-cyan-glow/40 outline-none"
/>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto">
<Filter className="w-4 h-4 text-slate-400 hidden sm:block" />
<select
value={selectedCategoryFilter}
onChange={(e) => setSelectedCategoryFilter(e.target.value)}
className="w-full sm:w-60 px-3 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none cursor-pointer"
>
<option value="all">All Scopes ({definitions.length})</option>
<option value="global">🌐 Global Attributes Only</option>
{categories.map((c) => (
<option key={c.id} value={c.id}>
📁 {typeof c.name === 'string' ? c.name : (c.name as any)?.th || (c.name as any)?.en || `Category #${c.id}`}
</option>
))}
</select>
</div>
</div>
{!loading && definitions.map((definition) => {
const currentLang = i18n.language
const label = typeof definition.label === 'string'
? definition.label
: definition.label?.[currentLang] || definition.label?.th || definition.label?.en || definition.code
const description = typeof definition.description === 'string'
? definition.description
: definition.description?.[currentLang] || definition.description?.th || definition.description?.en || ''
return (
<div key={definition.id} className="p-4 rounded-2xl border border-white/5 bg-white/[0.02] flex items-start justify-between gap-4 hover:border-cyan-glow/30 transition-all">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-lg font-bold text-white">{label}</h3>
<span className="text-[10px] uppercase tracking-widest text-cyan-glow bg-cyan-glow/10 border border-cyan-glow/20 px-2 py-0.5 rounded-full font-bold">
{definition.type}
</span>
</div>
<p className="text-[10px] uppercase tracking-widest text-slate-500">{definition.code}</p>
<p className="text-sm text-slate-400">{description || 'No description yet'}</p>
<p className="text-xs text-slate-500">
{definition.categoryId ? `Category: ${categoryNameMap.get(definition.categoryId) || definition.categoryId}` : 'Global attribute'}
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
setEditing(parseDefinition(definition))
setActiveLang('th')
}}
title={t('common.edit', 'Edit')}
className="p-2 rounded-xl text-slate-500 hover:text-cyan-glow hover:bg-cyan-glow/10 transition-all cursor-pointer"
{/* Attributes List / Table */}
<div className="admin-card overflow-hidden p-0 border border-white/5">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/5 bg-white/[0.02] text-[10px] uppercase font-bold tracking-widest text-slate-400">
<th className="py-3.5 px-5">Attribute & Code</th>
<th className="py-3.5 px-4">Data Type</th>
<th className="py-3.5 px-4">Category Scope</th>
<th className="py-3.5 px-4">Options / Unit</th>
<th className="py-3.5 px-4">Filterable</th>
<th className="py-3.5 px-4">Required</th>
<th className="py-3.5 px-5 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 text-sm">
{loading ? (
Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="animate-pulse">
<td className="py-4 px-5">
<div className="h-4 w-32 bg-white/10 rounded mb-1.5" />
<div className="h-3 w-20 bg-white/5 rounded" />
</td>
<td className="py-4 px-4"><div className="h-4 w-16 bg-white/5 rounded" /></td>
<td className="py-4 px-4"><div className="h-4 w-24 bg-white/5 rounded" /></td>
<td className="py-4 px-4"><div className="h-4 w-16 bg-white/5 rounded" /></td>
<td className="py-4 px-4"><div className="h-4 w-12 bg-white/5 rounded" /></td>
<td className="py-4 px-4"><div className="h-4 w-12 bg-white/5 rounded" /></td>
<td className="py-4 px-5 text-right"><div className="h-8 w-16 bg-white/5 rounded ml-auto" /></td>
</tr>
))
) : filteredDefinitions.length === 0 ? (
<tr>
<td colSpan={7} className="py-16 text-center text-slate-500">
<Layers className="w-8 h-8 text-slate-600 mx-auto mb-2" />
<p className="font-semibold text-slate-400">No attribute definitions found</p>
<p className="text-xs mt-1">Try clearing filters or click "Add Definition" to create one.</p>
</td>
</tr>
) : (
filteredDefinitions.map((def) => {
const currentLang = i18n.language
const label =
typeof def.label === 'string'
? def.label
: def.label?.[currentLang] || def.label?.th || def.label?.en || def.code
const optionsCount = Array.isArray(def.options) ? def.options.length : 0
return (
<tr
key={def.id}
onClick={() => navigate(`/product-attributes/${def.id}`)}
className="hover:bg-white/[0.03] transition-colors cursor-pointer group"
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(definition.id)}
title={t('common.delete', 'Delete')}
className="p-2 rounded-xl text-slate-500 hover:text-rose-500 hover:bg-rose-500/10 transition-all cursor-pointer"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)
})}
</div>
</section>
<td className="py-4 px-5">
<div className="font-bold text-white group-hover:text-cyan-glow transition-colors flex items-center gap-2">
<span>{label}</span>
<span className="text-[11px] font-mono text-slate-500">#{def.id}</span>
</div>
<div className="text-[11px] font-mono text-slate-400 uppercase tracking-wider">
{def.code}
</div>
</td>
<section className="admin-card space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h2 className="font-bold text-lg text-white">{editing?.id ? 'Edit Definition' : 'Create Definition'}</h2>
<p className="text-slate-500 text-sm">Type-aware definitions with 4-language localized labels and SELECT options.</p>
</div>
{editing && (
<button
onClick={() => setEditing(null)}
className="p-2 rounded-xl text-slate-500 hover:text-white hover:bg-white/10 transition-all cursor-pointer"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<td className="py-4 px-4">
<span className="text-[10px] uppercase font-bold tracking-widest px-2.5 py-1 rounded-full bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow">
{def.type}
</span>
</td>
{!editing ? (
<div className="text-center py-16 border-2 border-dashed border-white/5 rounded-2xl text-slate-500 text-sm">
Select an attribute definition to edit or click Create Definition.
</div>
) : (
<form onSubmit={handleSave} className="space-y-5">
<ContentLanguageTabs
activeLang={activeLang}
onSelectLang={setActiveLang}
translationsMap={labelTranslationsMap}
onCopyFrom={handleCopyFromLang}
/>
<td className="py-4 px-4">
{def.categoryId ? (
<span className="text-xs text-slate-300 font-medium flex items-center gap-1.5">
📁 {categoryNameMap.get(def.categoryId) || `Category #${def.categoryId}`}
</span>
) : (
<span className="text-xs text-slate-400 flex items-center gap-1.5">
🌐 Global
</span>
)}
</td>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Code</label>
<input
type="text"
required
value={editing.code}
onChange={(e) => setEditing({ ...editing, code: e.target.value })}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder="e.g. burn_time, weight"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Type</label>
<select
value={editing.type}
onChange={(e) => setEditing({ ...editing, type: e.target.value as EditableDefinition['type'], options: e.target.value === 'SELECT' ? editing.options : [] })}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
>
{ATTRIBUTE_TYPES.map((type) => <option key={type} value={type}>{type}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Unit</label>
<input
type="text"
value={editing.unit}
onChange={(e) => setEditing({ ...editing, unit: e.target.value })}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder="e.g. g, ml, hrs"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Sort Order</label>
<input
type="number"
value={editing.sortOrder}
onChange={(e) => setEditing({ ...editing, sortOrder: Number(e.target.value) })}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
/>
</div>
</div>
<td className="py-4 px-4">
{def.type === 'SELECT' ? (
<span className="text-xs text-slate-300 font-medium">
{optionsCount} {optionsCount === 1 ? 'option' : 'options'}
</span>
) : def.unit ? (
<span className="text-xs font-mono text-slate-400 bg-white/5 px-2 py-0.5 rounded border border-white/10">
{def.unit}
</span>
) : (
<span className="text-xs text-slate-600">-</span>
)}
</td>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">
Label ({activeLang.toUpperCase()})
</label>
<input
type="text"
required={activeLang === 'th'}
value={editing.translations[activeLang]?.label || ''}
onChange={(e) => setEditing({
...editing,
translations: {
...editing.translations,
[activeLang]: {
...editing.translations[activeLang],
label: e.target.value,
},
},
})}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={`Label in ${activeLang.toUpperCase()}`}
/>
</div>
<td className="py-4 px-4">
{def.isFilterable ? (
<span className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
<ListFilter className="w-3 h-3" /> Yes
</span>
) : (
<span className="text-xs text-slate-600">No</span>
)}
</td>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">
Description ({activeLang.toUpperCase()})
</label>
<textarea
value={editing.translations[activeLang]?.description || ''}
onChange={(e) => setEditing({
...editing,
translations: {
...editing.translations,
[activeLang]: {
...editing.translations[activeLang],
description: e.target.value,
},
},
})}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none min-h-[70px]"
placeholder={`Description in ${activeLang.toUpperCase()}`}
/>
</div>
<td className="py-4 px-4">
{def.isRequired ? (
<span className="inline-flex items-center gap-1 text-xs font-semibold text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full border border-amber-500/20">
<Tag className="w-3 h-3" /> Required
</span>
) : (
<span className="text-xs text-slate-600">Optional</span>
)}
</td>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Category Restriction</label>
<select
value={editing.categoryId ?? 0}
onChange={(e) => setEditing({ ...editing, categoryId: Number(e.target.value) || null })}
className="w-full px-4 py-3 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
>
<option value={0}>Global (All categories)</option>
{categories.map((c) => (
<option key={c.id} value={c.id}>
{typeof c.name === 'string' ? c.name : (c.name as any)?.th || (c.name as any)?.en || ''}
</option>
))}
</select>
</div>
{editing.type === 'SELECT' && (
<div className="space-y-3 pt-3 border-t border-white/5">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">
Options ({activeLang.toUpperCase()})
</label>
<button
type="button"
onClick={() => setEditing({
...editing,
options: [...editing.options, { [activeLang]: '' }],
})}
className="text-xs font-bold text-cyan-glow hover:text-cyan-glow/80 flex items-center gap-1 cursor-pointer"
>
<Plus className="w-3.5 h-3.5" /> Add Option
</button>
</div>
{editing.options.map((opt, idx) => (
<div key={idx} className="flex items-center gap-2">
<input
type="text"
value={opt[activeLang] || ''}
onChange={(e) => {
const nextOptions = [...editing.options]
nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value }
setEditing({ ...editing, options: nextOptions })
}}
className="flex-1 px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={`Option ${idx + 1} (${activeLang.toUpperCase()})`}
/>
<button
type="button"
onClick={() => {
const nextOptions = editing.options.filter((_, i) => i !== idx)
setEditing({ ...editing, options: nextOptions })
}}
className="p-2 text-slate-500 hover:text-rose-500 rounded-xl transition-all cursor-pointer"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
<td className="py-4 px-5 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1.5">
<Link
to={`/product-attributes/${def.id}`}
className="p-2 rounded-xl text-slate-400 hover:text-cyan-glow hover:bg-cyan-glow/10 border border-transparent hover:border-cyan-glow/20 transition-all cursor-pointer"
title="Edit Attribute Definition"
>
<Edit3 className="w-4 h-4" />
</Link>
<button
type="button"
onClick={() => handleDelete(def.id, label)}
className="p-2 rounded-xl text-slate-400 hover:text-rose-400 hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition-all cursor-pointer"
title="Delete Attribute Definition"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
)
})
)}
<div className="pt-4 flex gap-3">
<button
type="submit"
disabled={saving}
className="flex-1 btn-primary py-3 flex items-center justify-center gap-2 cursor-pointer"
>
<Save className="w-4 h-4" />
{saving ? t('common.saving', 'Saving...') : t('common.saveChanges', 'Save Changes')}
</button>
</div>
</form>
)}
</section>
</tbody>
</table>
</div>
</div>
</div>
)