From 0c87a287a3adccb10b1d5a0e00dc9f0db9c7c19f Mon Sep 17 00:00:00 2001 From: aramesh-site deploy Date: Mon, 28 Sep 2026 10:33:05 +0000 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=D8=B5=D9=81=D8=AD=D9=87=D9=94?= =?UTF-8?q?=20=D8=A7=D8=AE=D8=AA=D8=B5=D8=A7=D8=B5=DB=8C=20=D9=88=DB=8C?= =?UTF-8?q?=D8=B1=D8=A7=DB=8C=D8=B4/=D8=B3=D8=A7=D8=AE=D8=AA=20=D9=BE?= =?UTF-8?q?=D8=B1=D9=88=DA=98=D9=87=20+=20=DA=AF=D8=A7=D9=84=D8=B1=DB=8C?= =?UTF-8?q?=20=D9=81=D9=82=D8=B7=20=D8=A8=D8=A7=20=D8=A2=D9=BE=D9=84=D9=88?= =?UTF-8?q?=D8=AF=20=D8=A8=D9=87=20MinIO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /admin/projects/new و /admin/projects/:id — همهٔ فیلدها در یک صفحه با ورودی دوزبانهٔ صریح (فارسی/English)، ذخیره، حذف - GalleryField تازه: گالری چندتصویری فقط با آپلود به MinIO (حذف/جابه‌جایی/ افزودن چندفایلی) — فیلد «هر خط یک URL» دیگر استفاده نمی‌شود - مدادِ کارت پروژه و دکمهٔ «افزودن پروژه» به صفحهٔ اختصاصی هدایت می‌شوند؛ دکمهٔ ویرایش روی صفحهٔ جزئیات هم لینک همان صفحه است --- frontend/src/App.tsx | 3 + frontend/src/admin/Editable.tsx | 9 + frontend/src/admin/ProjectWritePage.tsx | 345 ++++++++++++++++++ .../src/components/admin/GalleryField.tsx | 153 ++++++++ frontend/src/pages/ProjectDetail.tsx | 17 +- frontend/src/pages/Projects.tsx | 4 +- 6 files changed, 521 insertions(+), 10 deletions(-) create mode 100644 frontend/src/admin/ProjectWritePage.tsx create mode 100644 frontend/src/components/admin/GalleryField.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4874598..eaad8aa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ import { AdminProvider, useAdmin } from './admin/AdminContext'; import { AdminBar } from './admin/AdminBar'; import { AdminLoginPage } from './admin/AdminLoginPage'; import { ArticleWritePage } from './admin/ArticleWritePage'; +import { ProjectWritePage } from './admin/ProjectWritePage'; import { LangProvider, useLang } from './i18n/lang'; // مسیرهای عمومی یک بار تعریف می‌شوند و دو بار سرو می‌شوند: ریشه فارسی و /en انگلیسی @@ -69,6 +70,8 @@ function AnimatedRoutes() { } /> } /> } /> + } /> + } /> {FA_ROUTES} } /> {EN_ROUTES} diff --git a/frontend/src/admin/Editable.tsx b/frontend/src/admin/Editable.tsx index 84fb0e1..9372f98 100644 --- a/frontend/src/admin/Editable.tsx +++ b/frontend/src/admin/Editable.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import type { ReactNode } from 'react' +import { useNavigate } from 'react-router-dom' import { ArrowDownIcon, ArrowUpIcon, PencilIcon, PlusIcon, Trash2Icon } from 'lucide-react' import { Link } from 'react-router-dom' import Swal from 'sweetalert2' @@ -163,12 +164,16 @@ export function AddItemButton({ resource, label, variant = 'cell', + /** در صورت ارسال، پس از ساخته‌شدن رکورد به صفحهٔ ویرایش اختصاصی هدایت می‌شود (کشو باز نمی‌شود) */ + editHrefBase, }: { resource: string label?: string variant?: 'cell' | 'button' + editHrefBase?: string }) { const { isAuthenticated, isEditMode, runSave } = useAdmin() + const navigate = useNavigate() const { updateSite } = useSiteData() const config = ENTITIES[resource] const [newItem, setNewItem] = useState(null) @@ -183,6 +188,10 @@ export function AddItemButton({ const list = d[config.dataKey as keyof SiteData] as unknown as ItemWithId[] if (Array.isArray(list)) list.push(created) }) + if (editHrefBase) { + navigate(`${editHrefBase}/${created.id}`) + return + } setNewItem(created) }) } diff --git a/frontend/src/admin/ProjectWritePage.tsx b/frontend/src/admin/ProjectWritePage.tsx new file mode 100644 index 0000000..d7e5032 --- /dev/null +++ b/frontend/src/admin/ProjectWritePage.tsx @@ -0,0 +1,345 @@ +import { useEffect, useState } from 'react' +import { Link, Navigate, useNavigate, useParams } from 'react-router-dom' +import { ArrowRightIcon, Trash2Icon } from 'lucide-react' +import Swal from 'sweetalert2' +import { useSiteData } from '../api' +import { useAdmin } from './AdminContext' +import { adminApi } from './adminClient' +import { ImageField } from '../components/admin/ImageField' +import { GalleryField } from '../components/admin/GalleryField' + +type ProjectDraft = { + id: number + slug: string + title: string + title_en: string + location: string + location_en: string + year: string + year_en: string + status: 'done' | 'progress' + project_type: string + project_type_en: string + usage: string + usage_en: string + area: string + area_en: string + floors: string + floors_en: string + units: string + units_en: string + client: string + client_en: string + summary: string + summary_en: string + body: string[] + body_en: string[] + image: string + gallery: string[] +} + +const EMPTY: ProjectDraft = { + id: 0, + slug: `project-${Date.now()}`, + title: '', title_en: '', + location: '', location_en: '', + year: '', year_en: '', + status: 'done', + project_type: '', project_type_en: '', + usage: '', usage_en: '', + area: '', area_en: '', + floors: '', floors_en: '', + units: '', units_en: '', + client: '', client_en: '', + summary: '', summary_en: '', + body: [], body_en: [], + image: '', gallery: [], +} + +const INPUT = + 'w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory placeholder:text-ivory/30 focus:border-copper focus:outline-none' + +/** جفت دوزبانه — فارسی و English همیشه هر دو دیده می‌شوند و با یک ذخیره ثبت می‌شوند */ +function Pair({ + label, + fa, + en, + textarea = false, +}: { + label: string + fa: { value: string; onChange: (v: string) => void } + en: { value: string; onChange: (v: string) => void } + textarea?: boolean +}) { + return ( +
+

{label}

+
+
+ فارسی + {textarea ? ( +