Update blog editor features
This commit is contained in:
@@ -7,6 +7,8 @@ import { useAdmin, useFieldSaver } from './AdminContext'
|
||||
import { adminApi } from './adminClient'
|
||||
import type { EntityConfig, FieldConfig } from './entities'
|
||||
import { ImageField } from '../components/admin/ImageField'
|
||||
import { JalaliDatePicker } from '../components/admin/JalaliDatePicker'
|
||||
import { LuxeSelect, LuxeTagsSelect } from '../components/admin/LuxeSelect'
|
||||
import { ArticleEditor } from '../components/admin/ArticleEditor'
|
||||
|
||||
type EntityDrawerProps = {
|
||||
@@ -111,21 +113,33 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6">
|
||||
<div className="grid gap-6">
|
||||
{config.fields.map((field) => (
|
||||
<FieldControl
|
||||
key={field.name}
|
||||
field={field}
|
||||
value={item[field.name]}
|
||||
onSave={(value) => saveField(field.name, value)}
|
||||
registerFlush={
|
||||
field.type === 'wysiwyg'
|
||||
? (fn) => {
|
||||
flushRef.current = fn
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{config.fields.map((field) => {
|
||||
// فیلد Alt تصویر: برای گالری همان ستون alt و برای بقیه <نام فیلد>_alt
|
||||
const altKey =
|
||||
field.type === 'image'
|
||||
? field.name === 'src' && 'alt' in item
|
||||
? 'alt'
|
||||
: `${field.name}_alt`
|
||||
: ''
|
||||
return (
|
||||
<FieldControl
|
||||
key={field.name}
|
||||
field={field}
|
||||
value={item[field.name]}
|
||||
altValue={altKey ? String(item[altKey] ?? '') : undefined}
|
||||
altFallback={String(item[config.titleKey] ?? '')}
|
||||
onSave={(value) => saveField(field.name, value)}
|
||||
onSaveAlt={altKey ? (alt) => saveField(altKey, alt) : undefined}
|
||||
registerFlush={
|
||||
field.type === 'wysiwyg'
|
||||
? (fn) => {
|
||||
flushRef.current = fn
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,12 +164,19 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
||||
function FieldControl({
|
||||
field,
|
||||
value,
|
||||
altValue,
|
||||
altFallback,
|
||||
onSave,
|
||||
onSaveAlt,
|
||||
registerFlush,
|
||||
}: {
|
||||
field: FieldConfig
|
||||
value: unknown
|
||||
/** مقدار Alt تصویر — فقط برای فیلدهای image */
|
||||
altValue?: string
|
||||
altFallback?: string
|
||||
onSave: (value: unknown) => Promise<boolean>
|
||||
onSaveAlt?: (alt: string) => Promise<boolean>
|
||||
registerFlush?: (fn: () => void) => void
|
||||
}) {
|
||||
const [text, setText] = useState<string>(String(value ?? ''))
|
||||
@@ -167,7 +188,53 @@ function FieldControl({
|
||||
}
|
||||
|
||||
if (field.type === 'image') {
|
||||
return <ImageField label={field.label} value={String(value ?? '')} onSave={(url) => onSave(url)} />
|
||||
return (
|
||||
<ImageField
|
||||
label={field.label}
|
||||
value={String(value ?? '')}
|
||||
alt={altValue}
|
||||
altFallback={altFallback}
|
||||
onSave={(url) => onSave(url)}
|
||||
onSaveAlt={onSaveAlt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'select') {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
|
||||
{field.hint && <p className="mt-1 text-[10px] font-light text-ivory/40">{field.hint}</p>}
|
||||
<div className="mt-2">
|
||||
<LuxeSelect
|
||||
value={String(value ?? '')}
|
||||
options={field.options ?? []}
|
||||
onChange={(next) => {
|
||||
setText(next)
|
||||
void onSave(next)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'date') {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
|
||||
{field.hint && <p className="mt-1 text-[10px] font-light text-ivory/40">{field.hint}</p>}
|
||||
<div className="mt-2">
|
||||
<JalaliDatePicker
|
||||
value={String(value ?? '')}
|
||||
onChange={(isoDate) => {
|
||||
setText(isoDate)
|
||||
void onSave(isoDate)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'wysiwyg') {
|
||||
@@ -175,7 +242,11 @@ function FieldControl({
|
||||
}
|
||||
|
||||
if (field.type === 'list') {
|
||||
return <ListField field={field} value={value} onSave={onSave} />
|
||||
return field.ui === 'tags' ? (
|
||||
<TagsField field={field} value={value} onSave={onSave} />
|
||||
) : (
|
||||
<ListField field={field} value={value} onSave={onSave} />
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'textarea') {
|
||||
@@ -210,6 +281,34 @@ function FieldControl({
|
||||
)
|
||||
}
|
||||
|
||||
/** فیلد هشتگها — react-select قابل ساخت (Creatable) با ذخیره فوری */
|
||||
function TagsField({
|
||||
field,
|
||||
value,
|
||||
onSave,
|
||||
}: {
|
||||
field: FieldConfig
|
||||
value: unknown
|
||||
onSave: (value: unknown) => Promise<boolean>
|
||||
}) {
|
||||
const [tags, setTags] = useState<string[]>(Array.isArray(value) ? (value as string[]) : [])
|
||||
|
||||
const update = (next: string[]) => {
|
||||
setTags(next)
|
||||
void onSave(next)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
|
||||
{field.hint && <p className="mt-1 text-[10px] font-light text-ivory/40">{field.hint}</p>}
|
||||
<div className="mt-2">
|
||||
<LuxeTagsSelect value={tags} onChange={update} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** ویرایش آرایه خطی (پاراگرافها، گالری، نکات) — هر تغییر ذخیره میشود */
|
||||
function ListField({
|
||||
field,
|
||||
|
||||
Reference in New Issue
Block a user