feat: monorepo with NestJS API, admin panel and DB-driven website

Restructure the repo into a pnpm + Turborepo monorepo:

- apps/web: Nuxt 4 site, now server-rendered and reading all content
  from the API; pages cached with SWR and purged on content changes
- apps/api: NestJS 12 + Prisma 7 + PostgreSQL. Auth with short-lived
  access tokens and rotating httpOnly refresh cookies, roles (ADMIN,
  EDITOR), catalog, editable site sections, media upload to S3-compatible
  storage (converted to WebP), contact messages, audit log
- apps/admin: Vue 3 admin panel (Ant Design Vue, Pinia, Tailwind) for
  products, categories, site sections, messages, media and users
- packages/shared: Zod schemas and types shared by API and admin

Also:
- seed script that loads the original 8 categories / 89 products and
  section content
- Docker images for api (with a one-off migrate service), web, admin,
  and an nginx gateway for both domains
- dev and production compose files, daily database backup service
- API e2e tests

Removes the committed static out/ build and the old root Dockerfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
S. Esmaeili
2026-10-21 20:31:05 +03:30
co-authored by Claude Opus 5.5
parent 31076174e2
commit 789d224ea0
395 changed files with 19070 additions and 11592 deletions
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import IconSelect from "@/components/IconSelect.vue";
import ImageRefField from "@/components/ImageRefField.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"about">>({ required: true });
</script>
<template>
<div class="grid gap-6 xl:grid-cols-3">
<div class="space-y-6 xl:col-span-2">
<a-card title="عنوان و متن">
<SectionHeadFields v-model="data.head" :with-desc="false" />
<FormField path="body" label="متن درباره ما" required>
<a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" />
</FormField>
</a-card>
<a-card title="تایم‌لاین" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.timeline" :create="() => ({ year: '', text: '' })" :max="12" add-label="افزودن رویداد" :item-title="(t) => t.year">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-4">
<FormField :path="`timeline.${index}.year`" label="سال" required><a-input v-model:value="item.year" /></FormField>
<FormField :path="`timeline.${index}.text`" label="رویداد" required class="md:col-span-3"><a-input v-model:value="item.text" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
<a-card title="ویژگی‌ها" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.features" :create="() => ({ icon: 'ShieldCheck', label: '' })" :max="6" add-label="افزودن ویژگی" :item-title="(f) => f.label">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField :path="`features.${index}.label`" label="متن" required><a-input v-model:value="item.label" /></FormField>
<FormField :path="`features.${index}.icon`" label="آیکون"><IconSelect v-model="item.icon" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
<div class="space-y-6">
<a-card title="تصویر">
<ImageRefField v-model="data.image" path="image" label="تصویر" />
</a-card>
<a-card title="نشان روی تصویر">
<FormField path="badge.value" label="عدد" required><a-input v-model:value="data.badge.value" /></FormField>
<FormField path="badge.label" label="متن" hint="برای شکستن خط Enter بزنید" required>
<a-textarea v-model:value="data.badge.label" :auto-size="{ minRows: 2 }" />
</FormField>
</a-card>
</div>
</div>
</template>
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"contact">>({ required: true });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" />
<p class="text-sm text-gray-500">نشانی، تلفن، ایمیل و ساعات کاری از «تنظیمات سایت» خوانده می‌شوند.</p>
</a-card>
<div class="grid gap-6 lg:grid-cols-2">
<a-card title="کادر پاسخ‌گویی اضطراری">
<FormField path="emergencyTitle" label="عنوان" required><a-input v-model:value="data.emergencyTitle" /></FormField>
<FormField path="emergencyText" label="متن" required>
<a-textarea v-model:value="data.emergencyText" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
</a-card>
<a-card title="فرم">
<FormField path="formTag" label="برچسب بالای فرم"><a-input v-model:value="data.formTag" /></FormField>
<FormField path="formTitle" label="عنوان فرم" hint="برای شکستن خط Enter بزنید" required>
<a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" />
</FormField>
<FormField path="successTitle" label="عنوان پیام موفقیت" required><a-input v-model:value="data.successTitle" /></FormField>
<FormField path="successText" label="متن پیام موفقیت" required>
<a-textarea v-model:value="data.successText" :auto-size="{ minRows: 2 }" />
</FormField>
</a-card>
</div>
</div>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
const data = defineModel<SectionData<"footer">>({ required: true });
</script>
<template>
<a-card title="فوتر">
<FormField path="about" label="متن معرفی" required>
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<div class="grid gap-x-4 md:grid-cols-3">
<FormField path="copyright" label="متن کپی‌رایت" required><a-input v-model:value="data.copyright" /></FormField>
<FormField path="certLine" label="خط گواهی‌نامه‌ها"><a-input v-model:value="data.certLine" class="ltr-input" /></FormField>
<FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
</div>
<p class="text-sm text-gray-500">لینک دسته‌ها و اطلاعات تماس فوتر به‌صورت خودکار از دسته‌بندی‌ها و تنظیمات سایت ساخته می‌شوند.</p>
</a-card>
</template>
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import IconSelect from "@/components/IconSelect.vue";
import ImageRefField from "@/components/ImageRefField.vue";
import ListEditor from "@/components/ListEditor.vue";
const data = defineModel<SectionData<"hero">>({ required: true });
</script>
<template>
<div class="grid gap-6 xl:grid-cols-3">
<div class="space-y-6 xl:col-span-2">
<a-card title="متن‌ها">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="badge" label="نشان بالای تیتر" required class="md:col-span-2">
<a-input v-model:value="data.badge" />
</FormField>
<FormField path="title" label="تیتر (خط اول)" required>
<a-input v-model:value="data.title" />
</FormField>
<FormField path="titleHighlight" label="تیتر (خط زرد)" required>
<a-input v-model:value="data.titleHighlight" />
</FormField>
<FormField path="intro" label="متن معرفی" required class="md:col-span-2">
<a-textarea v-model:value="data.intro" :auto-size="{ minRows: 3, maxRows: 6 }" />
</FormField>
</div>
</a-card>
<a-card title="دکمه‌ها">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="primaryCta.label" label="دکمه اصلی — متن" required>
<a-input v-model:value="data.primaryCta.label" />
</FormField>
<FormField path="primaryCta.href" label="دکمه اصلی — لینک" hint="مثلاً #contact یا /categories/..." required>
<a-input v-model:value="data.primaryCta.href" class="ltr-input" />
</FormField>
<FormField path="secondaryCta.label" label="دکمه دوم — متن" required>
<a-input v-model:value="data.secondaryCta.label" />
</FormField>
<FormField path="secondaryCta.href" label="دکمه دوم — لینک" required>
<a-input v-model:value="data.secondaryCta.href" class="ltr-input" />
</FormField>
</div>
</a-card>
<a-card title="نشان‌های گواهی" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.chips" :create="() => ({ icon: 'ShieldCheck', label: '' })" :max="6" add-label="افزودن نشان" :item-title="(c) => c.label">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField :path="`chips.${index}.label`" label="متن" required><a-input v-model:value="item.label" /></FormField>
<FormField :path="`chips.${index}.icon`" label="آیکون"><IconSelect v-model="item.icon" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
<a-card title="آمار پایین Hero" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.miniStats" :create="() => ({ value: '', label: '' })" :max="4" add-label="افزودن آمار" :item-title="(s) => s.value">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-3">
<FormField :path="`miniStats.${index}.value`" label="عدد" required><a-input v-model:value="item.value" placeholder="۱۸+" /></FormField>
<FormField :path="`miniStats.${index}.label`" label="توضیح" required class="md:col-span-2"><a-input v-model:value="item.label" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
<div class="space-y-6">
<a-card title="تصویر پس‌زمینه">
<ImageRefField v-model="data.image" path="image" label="تصویر" />
</a-card>
<a-card title="کارت وضعیت">
<FormField path="lastInspection" label="تاریخ آخرین بازرسی" hint="خالی = نمایش داده نمی‌شود">
<a-input v-model:value="data.lastInspection" placeholder="۱۴۰۴/۰۸/۱۲" />
</FormField>
</a-card>
</div>
</div>
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import ImageRefField from "@/components/ImageRefField.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"industries">>({ required: true });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" />
</a-card>
<a-card title="صنایع" :body-style="{ background: '#fafafa' }">
<ListEditor
v-model="data.items"
:create="() => ({ name: '', nameEn: '', count: '', image: { url: '', alt: '' } })"
:max="12"
add-label="افزودن صنعت"
:item-title="(s) => s.name"
>
<template #default="{ item, index }">
<div class="grid gap-x-6 lg:grid-cols-2">
<div>
<FormField :path="`items.${index}.name`" label="نام" required><a-input v-model:value="item.name" /></FormField>
<FormField :path="`items.${index}.nameEn`" label="نام انگلیسی"><a-input v-model:value="item.nameEn" class="ltr-input" /></FormField>
<FormField :path="`items.${index}.count`" label="تعداد پروژه" hint="مثلاً ۱۴۰+ — خالی = نمایش داده نمی‌شود"><a-input v-model:value="item.count" /></FormField>
</div>
<div>
<ImageRefField v-model="item.image" :path="`items.${index}.image`" label="تصویر" />
</div>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import IconSelect from "@/components/IconSelect.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"productsIntro">>({ required: true });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" />
<FormField path="ctaLabel" label="متن دکمه کاتالوگ" required>
<a-input v-model:value="data.ctaLabel" />
</FormField>
<p class="text-sm text-gray-500">کارت‌های دسته‌بندی از بخش «دسته‌بندی‌ها» مدیریت می‌شوند.</p>
</a-card>
<a-card title="کارت‌های اعتماد" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.trust" :create="() => ({ icon: 'BadgeCheck', title: '', desc: '' })" :max="6" add-label="افزودن کارت" :item-title="(t) => t.title">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-3">
<FormField :path="`trust.${index}.title`" label="عنوان" required><a-input v-model:value="item.title" /></FormField>
<FormField :path="`trust.${index}.desc`" label="توضیح"><a-input v-model:value="item.desc" /></FormField>
<FormField :path="`trust.${index}.icon`" label="آیکون"><IconSelect v-model="item.icon" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import IconSelect from "@/components/IconSelect.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
type Service = SectionData<"services">["items"][number];
const data = defineModel<SectionData<"services">>({ required: true });
const newService = (): Service => ({ icon: "ClipboardCheck", title: "", desc: "", items: [] });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" />
</a-card>
<a-card title="خدمات" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.items" :create="newService" :max="10" add-label="افزودن خدمت" :item-title="(s) => s.title">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField :path="`items.${index}.title`" label="عنوان" required><a-input v-model:value="item.title" /></FormField>
<FormField :path="`items.${index}.icon`" label="آیکون"><IconSelect v-model="item.icon" /></FormField>
<FormField :path="`items.${index}.desc`" label="توضیح" required class="md:col-span-2">
<a-textarea v-model:value="item.desc" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<FormField :path="`items.${index}.items`" label="موارد (فهرست)" hint="هر مورد را بنویسید و Enter بزنید" class="md:col-span-2">
<a-select v-model:value="item.items" mode="tags" :open="false" />
</FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>
+132
View File
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { type MediaDto, type SectionData, SOCIAL_TYPES, type SocialType, WEEK_DAYS } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import ImageField from "@/components/ImageField.vue";
import ListEditor from "@/components/ListEditor.vue";
const data = defineModel<SectionData<"settings">>({ required: true });
const DAY_LABELS: Record<(typeof WEEK_DAYS)[number], string> = {
Saturday: "شنبه",
Sunday: "یکشنبه",
Monday: "دوشنبه",
Tuesday: "سه‌شنبه",
Wednesday: "چهارشنبه",
Thursday: "پنجشنبه",
Friday: "جمعه",
};
const SOCIAL_LABELS: Record<SocialType, string> = {
website: "وب‌سایت",
telegram: "تلگرام",
instagram: "اینستاگرام",
linkedin: "لینکدین",
whatsapp: "واتساپ",
bale: "بله",
eitaa: "ایتا",
aparat: "آپارات",
youtube: "یوتیوب",
email: "ایمیل (mailto:)",
phone: "تلفن (tel:)",
};
type Social = SectionData<"settings">["socials"][number];
const URL_PLACEHOLDER = "https://t.me/...";
const newSocial = (): Social => ({ type: "telegram", url: "" });
const socialTitle = (s: Social) => SOCIAL_LABELS[s.type];
const SOCIAL_OPTIONS = SOCIAL_TYPES.map((t) => ({ value: t, label: SOCIAL_LABELS[t] }));
const DAY_OPTIONS = WEEK_DAYS.map((d) => ({ value: d, label: DAY_LABELS[d] }));
function pickOgImage(m: MediaDto) {
data.value.ogImage = { url: m.url, alt: data.value.ogImage.alt || m.alt, width: m.width, height: m.height };
}
</script>
<template>
<div class="grid gap-6 xl:grid-cols-2">
<a-card title="هویت و سئوی پیش‌فرض">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="name" label="نام شرکت" required><a-input v-model:value="data.name" /></FormField>
<FormField path="nameEn" label="نام انگلیسی" required><a-input v-model:value="data.nameEn" class="ltr-input" /></FormField>
<FormField path="title" label="عنوان پیش‌فرض صفحات (title)" required class="md:col-span-2">
<a-input v-model:value="data.title" :maxlength="120" show-count />
</FormField>
<FormField path="description" label="توضیحات متا (description)" required class="md:col-span-2">
<a-textarea v-model:value="data.description" :maxlength="400" show-count :auto-size="{ minRows: 3 }" />
</FormField>
<FormField path="foundingDate" label="سال تأسیس (میلادی)" required>
<a-input v-model:value="data.foundingDate" class="ltr-input" placeholder="2007" />
</FormField>
</div>
<FormField path="ogImage.url" label="تصویر اشتراک‌گذاری (Open Graph)" hint="ترجیحاً ۱۲۰۰×۶۳۰ پیکسل" required>
<ImageField :url="data.ogImage.url" @pick="pickOgImage" />
</FormField>
<FormField path="ogImage.alt" label="متن جایگزین تصویر">
<a-input v-model:value="data.ogImage.alt" />
</FormField>
</a-card>
<a-card title="اطلاعات تماس">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="phoneDisplay" label="تلفن (نمایشی)" required>
<a-input v-model:value="data.phoneDisplay" />
</FormField>
<FormField path="phone" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً ‎+98-21-91008800" required>
<a-input v-model:value="data.phone" class="ltr-input" />
</FormField>
<FormField path="email" label="ایمیل" required class="md:col-span-2">
<a-input v-model:value="data.email" class="ltr-input" />
</FormField>
<FormField path="address.locality" label="شهر" required>
<a-input v-model:value="data.address.locality" />
</FormField>
<FormField path="address.country" label="کد کشور" required>
<a-input v-model:value="data.address.country" class="ltr-input" :maxlength="2" />
</FormField>
<FormField path="address.street" label="نشانی" required class="md:col-span-2">
<a-input v-model:value="data.address.street" />
</FormField>
</div>
</a-card>
<a-card title="ساعات کاری">
<FormField path="workingHours" label="متن نمایشی" required>
<a-input v-model:value="data.workingHours" />
</FormField>
<FormField path="openingHours.days" label="روزهای کاری (برای گوگل)">
<a-checkbox-group
v-model:value="data.openingHours.days"
:options="DAY_OPTIONS"
/>
</FormField>
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="openingHours.opens" label="ساعت شروع" required>
<a-input v-model:value="data.openingHours.opens" class="ltr-input" placeholder="08:00" />
</FormField>
<FormField path="openingHours.closes" label="ساعت پایان" required>
<a-input v-model:value="data.openingHours.closes" class="ltr-input" placeholder="17:00" />
</FormField>
</div>
</a-card>
<a-card title="شبکه‌های اجتماعی و لینک‌ها" :body-style="{ background: '#fafafa' }">
<ListEditor
v-model="data.socials"
:create="newSocial"
:max="12"
add-label="افزودن لینک"
:item-title="socialTitle"
>
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-3">
<FormField :path="`socials.${index}.type`" label="نوع">
<a-select v-model:value="item.type" :options="SOCIAL_OPTIONS" />
</FormField>
<FormField :path="`socials.${index}.url`" label="آدرس" required class="md:col-span-2">
<a-input v-model:value="item.url" class="ltr-input" :placeholder="URL_PLACEHOLDER" />
</FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"standards">>({ required: true });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان و متن">
<SectionHeadFields v-model="data.head" />
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="note" label="کادر توضیح (اختیاری)">
<a-textarea v-model:value="data.note" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<FormField path="ctaLabel" label="متن دکمه" required>
<a-input v-model:value="data.ctaLabel" />
</FormField>
</div>
</a-card>
<a-card title="استانداردها" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.items" :create="() => ({ code: '', label: '' })" :max="12" add-label="افزودن استاندارد" :item-title="(s) => s.code">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField :path="`items.${index}.code`" label="کد استاندارد" required><a-input v-model:value="item.code" class="ltr-input" placeholder="EN 397" /></FormField>
<FormField :path="`items.${index}.label`" label="عنوان" required><a-input v-model:value="item.label" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import ListEditor from "@/components/ListEditor.vue";
const data = defineModel<SectionData<"stats">>({ required: true });
</script>
<template>
<a-card title="شمارنده‌ها" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.items" :create="() => ({ end: 0, suffix: '+', label: '', note: '' })" :max="8" add-label="افزودن شمارنده" :item-title="(s) => s.label">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-4">
<FormField :path="`items.${index}.end`" label="عدد نهایی" required>
<a-input-number v-model:value="item.end" :min="0" class="!w-full" />
</FormField>
<FormField :path="`items.${index}.suffix`" label="پسوند" hint="مثلاً + یا ٪">
<a-input v-model:value="item.suffix" />
</FormField>
<FormField :path="`items.${index}.label`" label="عنوان" required>
<a-input v-model:value="item.label" />
</FormField>
<FormField :path="`items.${index}.note`" label="یادداشت انگلیسی کوچک">
<a-input v-model:value="item.note" class="ltr-input" />
</FormField>
</div>
</template>
</ListEditor>
</a-card>
</template>
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
const data = defineModel<SectionData<"tape">>({ required: true });
</script>
<template>
<a-card title="عبارت‌های نوار متحرک">
<FormField path="items" label="عبارت‌ها" hint="هر عبارت را بنویسید و Enter بزنید؛ برای حذف روی × کلیک کنید" required>
<a-select v-model:value="data.items" mode="tags" :open="false" />
</FormField>
</a-card>
</template>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import FormField from "@/components/FormField.vue";
import ListEditor from "@/components/ListEditor.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"testimonials">>({ required: true });
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" :with-highlight="false" :with-desc="false" />
</a-card>
<a-card title="نظرات" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.items" :create="() => ({ text: '', name: '', role: '' })" :max="12" add-label="افزودن نظر" :item-title="(q) => q.name">
<template #default="{ item, index }">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField :path="`items.${index}.text`" label="متن نظر" required class="md:col-span-2">
<a-textarea v-model:value="item.text" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<FormField :path="`items.${index}.name`" label="نام" required><a-input v-model:value="item.name" /></FormField>
<FormField :path="`items.${index}.role`" label="سمت و سازمان"><a-input v-model:value="item.role" /></FormField>
</div>
</template>
</ListEditor>
</a-card>
</div>
</template>