feat(admin): full-screen forms + WYSIWYG rich-text editor

- AdminResource + TemplatesAdmin modals are now large full-height panels
  (max-w-5xl, sticky header/footer, scrolling body, 2-column field grid;
  textarea/richtext span full width)
- RichTextField: dependency-free contentEditable WYSIWYG (bold/italic/underline,
  H2/H3/¶, lists, link, clear) emitting HTML, dir="auto" for fa/en
- blog content + category description now use the rich editor

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 00:51:52 +03:30
parent 7fe5f8a563
commit 5b6f3e851b
4 changed files with 103 additions and 18 deletions
+17 -11
View File
@@ -4,11 +4,12 @@ import { useCallback, useEffect, useState, type ReactNode } from "react";
import { FileUploadField } from "@/components/admin/FileUploadField";
import { AdminThumb } from "@/components/admin/AdminThumb";
import { RichTextField } from "@/components/admin/RichTextField";
export interface FieldDef {
key: string;
label: string;
type?: "text" | "textarea" | "number" | "checkbox" | "select" | "image" | "file";
type?: "text" | "textarea" | "richtext" | "number" | "checkbox" | "select" | "image" | "file";
options?: { value: string; label: string }[];
required?: boolean;
placeholder?: string;
@@ -193,21 +194,26 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
</div>
{(creating || editing) && config.fields && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={closeForm}>
<div className={`${card} w-full max-w-lg p-5`} onClick={(e) => e.stopPropagation()}>
<h2 className="text-sm font-semibold text-white">
{editing ? "ویرایش" : "افزودن"} {config.title}
</h2>
<div className="mt-4 grid max-h-[60vh] gap-3 overflow-y-auto pr-1">
<div className="fixed inset-0 z-50 flex items-stretch justify-center bg-black/70 p-2 sm:p-6" onClick={closeForm}>
<div className={`${card} flex max-h-full w-full max-w-5xl flex-col`} onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-[#1e2235] px-5 py-3">
<h2 className="text-sm font-semibold text-white">
{editing ? "ویرایش" : "افزودن"} {config.title}
</h2>
<button className="rounded-lg px-2 py-1 text-gray-400 hover:bg-[#161a2e] hover:text-white" onClick={closeForm}></button>
</div>
<div className="grid flex-1 grid-cols-1 gap-4 overflow-y-auto p-5 sm:grid-cols-2">
{config.fields.map((f) => (
<div key={f.key}>
<div key={f.key} className={f.type === "textarea" || f.type === "richtext" ? "sm:col-span-2" : ""}>
{f.type !== "checkbox" && (
<label className="mb-1 block text-xs font-medium text-gray-400">
{f.label}{f.required && <span className="text-red-400"> *</span>}
</label>
)}
{f.type === "textarea" ? (
<textarea className={`${inputCls} min-h-[80px]`} placeholder={f.placeholder}
{f.type === "richtext" ? (
<RichTextField value={String(form[f.key] ?? "")} onChange={(html) => setForm({ ...form, [f.key]: html })} />
) : f.type === "textarea" ? (
<textarea className={`${inputCls} min-h-[160px]`} placeholder={f.placeholder}
value={String(form[f.key] ?? "")} onChange={(e) => setForm({ ...form, [f.key]: e.target.value })} />
) : f.type === "select" ? (
<select className={inputCls} value={String(form[f.key] ?? "")}
@@ -234,7 +240,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
</div>
))}
</div>
<div className="mt-5 flex items-center justify-end gap-2">
<div className="flex items-center justify-end gap-2 border-t border-[#1e2235] px-5 py-3">
<button className={btnGhost} onClick={closeForm}>انصراف</button>
<button className={btn} onClick={submit} disabled={saving}>{saving ? "در حال ذخیره…" : "ذخیره"}</button>
</div>