feat(admin): editable contact fields in sections and larger logos
Allow address, phone, map, and related site chrome to be edited from contact/settings for all editors, and enlarge brand marks on login, header, and preloader. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionData } from "@imen/shared";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
|
||||
/** Shared address / phone / email / hours fields from the settings section. */
|
||||
const data = defineModel<SectionData<"settings">>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** Zod path prefix when nested under another form context */
|
||||
pathPrefix?: string;
|
||||
}>(),
|
||||
{ pathPrefix: "" },
|
||||
);
|
||||
|
||||
function path(field: string) {
|
||||
return props.pathPrefix ? `${props.pathPrefix}.${field}` : field;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-x-4 md:grid-cols-2">
|
||||
<FormField :path="path('phoneDisplay')" label="تلفن (نمایشی)" required>
|
||||
<a-input v-model:value="data.phoneDisplay" />
|
||||
</FormField>
|
||||
<FormField :path="path('phone')" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً +98-21-91008800" required>
|
||||
<a-input v-model:value="data.phone" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField :path="path('email')" label="ایمیل" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.email" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.locality')" label="شهر" required>
|
||||
<a-input v-model:value="data.address.locality" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.country')" label="کد کشور" required>
|
||||
<a-input v-model:value="data.address.country" class="ltr-input" :maxlength="2" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.street')" label="نشانی" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.address.street" />
|
||||
</FormField>
|
||||
<FormField
|
||||
:path="path('mapUrl')"
|
||||
label="لینک نقشه"
|
||||
hint="اختیاری — لینک گوگلمپ، نشان و …"
|
||||
class="md:col-span-2"
|
||||
>
|
||||
<a-input v-model:value="data.mapUrl" class="ltr-input" placeholder="https://maps.google.com/..." />
|
||||
</FormField>
|
||||
<FormField :path="path('workingHours')" label="ساعات کاری (متن نمایشی)" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.workingHours" />
|
||||
</FormField>
|
||||
</div>
|
||||
</template>
|
||||
@@ -107,9 +107,9 @@ async function submit() {
|
||||
|
||||
<div class="mt-8 flex items-center gap-4">
|
||||
<span
|
||||
class="flex size-14 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
class="flex size-20 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
>
|
||||
<img src="/logo.png" alt="ایمن پیشگامان" class="size-9 object-contain" />
|
||||
<img src="/logo.png" alt="ایمن پیشگامان" class="size-full scale-110 object-contain object-center" />
|
||||
</span>
|
||||
<div class="leading-tight">
|
||||
<div class="text-2xl font-black tracking-tight md:text-3xl">ایمن پیشگامان</div>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { isSectionKey, SECTION_LABELS, type SectionKey, sectionSchemas } from "@imen/shared";
|
||||
import {
|
||||
isSectionKey,
|
||||
SECTION_LABELS,
|
||||
type SectionData,
|
||||
type SectionKey,
|
||||
sectionSchemas,
|
||||
settingsSectionSchema,
|
||||
} from "@imen/shared";
|
||||
import { message } from "ant-design-vue";
|
||||
import { type Component, computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
@@ -8,7 +15,6 @@ import VuesaxIcon from "@/components/VuesaxIcon.vue";
|
||||
import type { SectionResponse } from "@/api/types";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { useZodForm } from "@/composables/useZodForm";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { jalali, SITE_URL } from "@/utils/format";
|
||||
import AboutForm from "@/sections/AboutForm.vue";
|
||||
import ContactForm from "@/sections/ContactForm.vue";
|
||||
@@ -38,7 +44,6 @@ const FORMS: Record<SectionKey, Component> = {
|
||||
footer: FooterForm,
|
||||
};
|
||||
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
const key = String(route.params.key);
|
||||
const valid = isSectionKey(key);
|
||||
@@ -49,6 +54,14 @@ const { model, validate, handleError, reset } = useZodForm(
|
||||
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
|
||||
{} as never,
|
||||
);
|
||||
/** Contact page also edits site-wide contact info from settings */
|
||||
const settingsModel = ref<SectionData<"settings"> | null>(null);
|
||||
const contactModel = computed({
|
||||
get: () => model.value as unknown as SectionData<"contact">,
|
||||
set: (v: SectionData<"contact">) => {
|
||||
model.value = v as never;
|
||||
},
|
||||
});
|
||||
const loaded = ref(false);
|
||||
const saving = ref(false);
|
||||
const updatedAt = ref<string | null>(null);
|
||||
@@ -57,12 +70,20 @@ const label = computed(() => (valid ? SECTION_LABELS[sectionKey] : null));
|
||||
async function load() {
|
||||
try {
|
||||
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`);
|
||||
const data = res.data as any;
|
||||
const data = res.data as Record<string, unknown>;
|
||||
if (sectionKey !== "settings" && data.enabled === undefined) {
|
||||
data.enabled = true;
|
||||
}
|
||||
reset(data as never);
|
||||
updatedAt.value = res.updatedAt;
|
||||
|
||||
if (sectionKey === "contact") {
|
||||
const settingsRes = await api<SectionResponse>("/admin/sections/settings");
|
||||
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
|
||||
} else {
|
||||
settingsModel.value = null;
|
||||
}
|
||||
|
||||
loaded.value = true;
|
||||
} catch (e) {
|
||||
void message.error(errorMessage(e));
|
||||
@@ -72,8 +93,25 @@ async function load() {
|
||||
async function save() {
|
||||
const data = validate();
|
||||
if (!data) return;
|
||||
|
||||
if (sectionKey === "contact" && settingsModel.value) {
|
||||
const settingsParsed = settingsSectionSchema.safeParse(settingsModel.value);
|
||||
if (!settingsParsed.success) {
|
||||
void message.error("اطلاعات تماس معتبر نیست — نشانی، تلفن یا ایمیل را بررسی کنید");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (sectionKey === "contact" && settingsModel.value) {
|
||||
const settingsRes = await api<SectionResponse>("/admin/sections/settings", {
|
||||
method: "PUT",
|
||||
body: settingsSectionSchema.parse(settingsModel.value),
|
||||
});
|
||||
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
|
||||
}
|
||||
|
||||
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
|
||||
reset(res.data as never);
|
||||
updatedAt.value = res.updatedAt;
|
||||
@@ -87,9 +125,9 @@ async function save() {
|
||||
|
||||
const togglingVisibility = ref(false);
|
||||
|
||||
async function onVisibilityChange(val: any) {
|
||||
async function onVisibilityChange(val: unknown) {
|
||||
const nextVal = Boolean(val);
|
||||
(model.value as any).enabled = nextVal;
|
||||
(model.value as { enabled?: boolean }).enabled = nextVal;
|
||||
togglingVisibility.value = true;
|
||||
try {
|
||||
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
|
||||
@@ -99,13 +137,13 @@ async function onVisibilityChange(val: any) {
|
||||
body: { enabled: nextVal },
|
||||
},
|
||||
);
|
||||
(model.value as any).enabled = res.enabled !== false;
|
||||
(model.value as { enabled?: boolean }).enabled = res.enabled !== false;
|
||||
updatedAt.value = res.updatedAt;
|
||||
void message.success(
|
||||
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
|
||||
);
|
||||
} catch (e) {
|
||||
(model.value as any).enabled = !nextVal;
|
||||
(model.value as { enabled?: boolean }).enabled = !nextVal;
|
||||
void message.error(errorMessage(e));
|
||||
} finally {
|
||||
togglingVisibility.value = false;
|
||||
@@ -126,7 +164,7 @@ if (valid) void load();
|
||||
<div v-if="sectionKey !== 'settings'" class="max-lg:hidden flex items-center gap-2 me-2">
|
||||
<span class="text-sm font-medium text-gray-700">نمایش در سایت:</span>
|
||||
<a-switch
|
||||
:checked="(model as any)?.enabled !== false"
|
||||
:checked="(model as { enabled?: boolean })?.enabled !== false"
|
||||
:loading="togglingVisibility"
|
||||
checked-children="نمایش"
|
||||
un-checked-children="مخفی"
|
||||
@@ -138,7 +176,7 @@ if (valid) void load();
|
||||
type="primary"
|
||||
class="max-lg:hidden"
|
||||
:loading="saving"
|
||||
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
||||
:disabled="!loaded"
|
||||
@click="save"
|
||||
>
|
||||
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
||||
@@ -147,21 +185,19 @@ if (valid) void load();
|
||||
</PageHeader>
|
||||
<a-spin :spinning="!loaded">
|
||||
<a-alert
|
||||
v-if="loaded && sectionKey === 'settings' && !auth.isAdmin"
|
||||
type="error"
|
||||
show-icon
|
||||
message="فقط مدیر کل میتواند تنظیمات سایت را تغییر دهد."
|
||||
class="mb-4"
|
||||
/>
|
||||
<a-alert
|
||||
v-else-if="loaded && sectionKey !== 'settings' && (model as any).enabled === false"
|
||||
v-if="loaded && sectionKey !== 'settings' && (model as { enabled?: boolean }).enabled === false"
|
||||
type="warning"
|
||||
show-icon
|
||||
message="این بخش در حال حاضر در سایت نمایش داده نمیشود (مخفی است)."
|
||||
class="mb-4"
|
||||
/>
|
||||
<a-form v-if="loaded" layout="vertical" class="pb-20 lg:pb-0" @submit.prevent="save">
|
||||
<component :is="FORMS[sectionKey]" v-model="model" />
|
||||
<ContactForm
|
||||
v-if="sectionKey === 'contact'"
|
||||
v-model="contactModel"
|
||||
v-model:settings="settingsModel"
|
||||
/>
|
||||
<component v-else :is="FORMS[sectionKey]" v-model="model" />
|
||||
</a-form>
|
||||
</a-spin>
|
||||
|
||||
@@ -169,13 +205,13 @@ if (valid) void load();
|
||||
<div class="lg:hidden fixed bottom-0 inset-x-0 z-30 border-t border-gray-200 bg-white/95 px-4 py-2.5 backdrop-blur-md shadow-lg flex items-center justify-between gap-3">
|
||||
<div v-if="sectionKey !== 'settings'" class="flex items-center gap-2">
|
||||
<a-switch
|
||||
:checked="(model as any)?.enabled !== false"
|
||||
:checked="(model as { enabled?: boolean })?.enabled !== false"
|
||||
:loading="togglingVisibility"
|
||||
size="small"
|
||||
@change="onVisibilityChange"
|
||||
/>
|
||||
<span class="text-xs font-medium text-gray-700">
|
||||
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
|
||||
{{ (model as { enabled?: boolean })?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
|
||||
@@ -191,7 +227,7 @@ if (valid) void load();
|
||||
size="middle"
|
||||
class="!font-bold !px-5"
|
||||
:loading="saving"
|
||||
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
||||
:disabled="!loaded"
|
||||
@click="save"
|
||||
>
|
||||
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
||||
|
||||
@@ -6,13 +6,10 @@ import { useRouter } from "vue-router";
|
||||
import { api, errorMessage } from "@/api/http";
|
||||
import type { AdminSectionSummary } from "@/api/types";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { jalali } from "@/utils/format";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
// site-wide settings are editable by admins only
|
||||
const keys = SECTION_KEYS.filter((k) => k !== "settings" || auth.isAdmin);
|
||||
const keys = SECTION_KEYS;
|
||||
|
||||
const loading = ref(true);
|
||||
const sectionEnabled = reactive<Record<string, boolean>>({
|
||||
|
||||
@@ -103,7 +103,7 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخشها دسترسی دارد؛ ویرایشگر به جز کاربران و تنظیمات سایت">
|
||||
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخشها دسترسی دارد؛ ویرایشگر به جز مدیریت کاربران و حذف دادهها">
|
||||
<a-button type="primary" @click="openNew">
|
||||
<template #icon><VuesaxIcon name="add" :size="16" /></template>
|
||||
کاربر جدید
|
||||
@@ -241,7 +241,6 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
|
||||
<li>مدیریت کاربران، نقشها و تنظیمات امنیتی</li>
|
||||
<li>تنظیمات پایه سایت، سئو، شبکهها و ساعات کاری</li>
|
||||
<li>حذف دادهها (محصولات، دستهها، پیامها، رسانهها)</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -253,7 +252,7 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
|
||||
<li>ایجاد، ویرایش و انتشار محصولات و دستهبندیها</li>
|
||||
<li>ویرایش محتوای بخشهای صفحه اصلی</li>
|
||||
<li>ویرایش بخشهای سایت، نشانی، تماس، سئو و شبکهها</li>
|
||||
<li>آپلود تصاویر در رسانه و مشاهده پیامهای تماس</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -269,7 +268,6 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
|
||||
<li>مدیریت کاربران، افزودن کاربر و تغییر نقشها و وضعیت حساب</li>
|
||||
<li>دسترسی کامل به تنظیمات پایه سایت، ساعات کاری، شبکهها و سئو</li>
|
||||
<li>مجوز حذف محصولات، دستهبندیها، رسانهها و پیامهای تماس</li>
|
||||
<li>مشاهده لاگهای امنیتی و رویدادهای کامل سامانه</li>
|
||||
</ul>
|
||||
@@ -282,7 +280,7 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
|
||||
<li>ایجاد، ویرایش، کپی و انتشار محصولات و گروهها</li>
|
||||
<li>ویرایش محتوای بخشهای صفحه اصلی و فعال/مخفیسازی نمایش آنها</li>
|
||||
<li>ویرایش بخشهای سایت، نشانی، تماس، سئو، شبکهها و نمایش/مخفیسازی</li>
|
||||
<li>آپلود و ویرایش اطلاعات رسانهها در کتابخانه رسانه</li>
|
||||
<li>مشاهده پیامهای تماس و بهروزرسانی وضعیت (بدون دسترسی حذف یا مدیریت کاربران)</li>
|
||||
</ul>
|
||||
@@ -319,7 +317,7 @@ onMounted(load);
|
||||
:options="ROLE_OPTIONS"
|
||||
/>
|
||||
<div class="mt-1.5 text-[11px] text-gray-500">
|
||||
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف دادهها، تنظیمات و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخشها و پیامها (بدون حذف)' }}
|
||||
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف دادهها و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخشهای سایت (از جمله نشانی و تماس)، پیامها (بدون حذف)' }}
|
||||
</div>
|
||||
</FormField>
|
||||
<FormField v-if="!isNew" path="isActive">
|
||||
|
||||
@@ -13,7 +13,7 @@ const data = defineModel<SectionData<"about">>({ required: true });
|
||||
<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" />
|
||||
<SectionHeadFields v-model="data.head" />
|
||||
<FormField path="body" label="متن درباره ما" required>
|
||||
<a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" />
|
||||
</FormField>
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionData } from "@imen/shared";
|
||||
import ContactInfoFields from "@/components/ContactInfoFields.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import SectionHeadFields from "@/components/SectionHeadFields.vue";
|
||||
|
||||
const data = defineModel<SectionData<"contact">>({ required: true });
|
||||
/** Site-wide contact info (settings); saved together with this section. */
|
||||
const settings = defineModel<SectionData<"settings"> | null>("settings", { default: null });
|
||||
|
||||
function onSettingsUpdate(value: SectionData<"settings">) {
|
||||
settings.value = value;
|
||||
}
|
||||
</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>
|
||||
|
||||
<a-card v-if="settings" title="نشانی و اطلاعات تماس">
|
||||
<p class="mb-4 text-sm text-gray-500">
|
||||
این مقادیر در هدر، فوتر، بخش تماس و سئوی سایت مشترکاند و با ذخیرهٔ همین صفحه بهروز میشوند.
|
||||
</p>
|
||||
<ContactInfoFields :model-value="settings" @update:model-value="onSettingsUpdate" />
|
||||
</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>
|
||||
@@ -24,10 +38,16 @@ const data = defineModel<SectionData<"contact">>({ required: true });
|
||||
<FormField path="formTitle" label="عنوان فرم" hint="برای شکستن خط Enter بزنید" required>
|
||||
<a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" />
|
||||
</FormField>
|
||||
<FormField path="submitLabel" label="متن دکمه ارسال" required>
|
||||
<a-input v-model:value="data.submitLabel" />
|
||||
</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>
|
||||
<FormField path="resetLabel" label="متن دکمه بعد از موفقیت" required>
|
||||
<a-input v-model:value="data.resetLabel" />
|
||||
</FormField>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,15 +6,30 @@ 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>
|
||||
<div class="space-y-6">
|
||||
<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="linksTitle" label="عنوان ستون لینکها" required>
|
||||
<a-input v-model:value="data.linksTitle" />
|
||||
</FormField>
|
||||
<FormField path="productsTitle" label="عنوان ستون محصولات" required>
|
||||
<a-input v-model:value="data.productsTitle" />
|
||||
</FormField>
|
||||
<FormField path="contactTitle" label="عنوان ستون ارتباط" required>
|
||||
<a-input v-model:value="data.contactTitle" />
|
||||
</FormField>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { type MediaDto, type SectionData, SOCIAL_TYPES, type SocialType, WEEK_DAYS } from "@imen/shared";
|
||||
import ContactInfoFields from "@/components/ContactInfoFields.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import ImageField from "@/components/ImageField.vue";
|
||||
import ListEditor from "@/components/ListEditor.vue";
|
||||
@@ -66,33 +67,22 @@ function pickOgImage(m: MediaDto) {
|
||||
</a-card>
|
||||
|
||||
<a-card title="اطلاعات تماس">
|
||||
<ContactInfoFields v-model="data" />
|
||||
</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 path="headerCta.label" label="متن دکمه" required>
|
||||
<a-input v-model:value="data.headerCta.label" />
|
||||
</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 path="headerCta.href" label="لینک" required>
|
||||
<a-input v-model:value="data.headerCta.href" class="ltr-input" placeholder="/#contact" />
|
||||
</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-card title="ساعات کاری (برای گوگل)">
|
||||
<FormField path="openingHours.days" label="روزهای کاری">
|
||||
<a-checkbox-group
|
||||
v-model:value="data.openingHours.days"
|
||||
:options="DAY_OPTIONS"
|
||||
|
||||
@@ -10,7 +10,7 @@ const data = defineModel<SectionData<"testimonials">>({ required: true });
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<a-card title="عنوان بخش">
|
||||
<SectionHeadFields v-model="data.head" :with-highlight="false" :with-desc="false" />
|
||||
<SectionHeadFields v-model="data.head" />
|
||||
</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">
|
||||
|
||||
+1
-4
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||
return function (target, key) { decorator(target, key, paramIndex); }
|
||||
};
|
||||
import { BadRequestException, Body, Controller, ForbiddenException, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common";
|
||||
import { BadRequestException, Body, Controller, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common";
|
||||
import { isSectionKey, SECTION_KEYS, sectionSchemas, toIssues, } from "@imen/shared";
|
||||
import { AdminAuth, CurrentUser } from "../common/auth.js";
|
||||
import { AuditService, RevalidateService } from "../common/common.module.js";
|
||||
@@ -44,9 +44,6 @@ let SectionsService = class SectionsService {
|
||||
async update(key, body, user) {
|
||||
if (!isSectionKey(key))
|
||||
throw new NotFoundException("بخش پیدا نشد");
|
||||
if (key === "settings" && user.role !== "ADMIN") {
|
||||
throw new ForbiddenException("فقط مدیر کل میتواند تنظیمات سایت را تغییر دهد");
|
||||
}
|
||||
const parsed = sectionSchemas[key].safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestException({
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -12,6 +12,7 @@ export const SECTIONS = {
|
||||
locality: "تهران",
|
||||
country: "IR",
|
||||
},
|
||||
mapUrl: "",
|
||||
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
|
||||
openingHours: {
|
||||
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
|
||||
@@ -19,6 +20,7 @@ export const SECTIONS = {
|
||||
closes: "17:00",
|
||||
},
|
||||
foundingDate: "2007",
|
||||
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
|
||||
socials: [
|
||||
{ type: "website", url: "/" },
|
||||
{ type: "email", url: "mailto:info@imenpishgaman.ir" },
|
||||
@@ -246,6 +248,8 @@ export const SECTIONS = {
|
||||
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس میگیریم",
|
||||
successTitle: "درخواست شما ثبت شد",
|
||||
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس میگیرند.",
|
||||
submitLabel: "ارسال درخواست مشاوره",
|
||||
resetLabel: "ثبت درخواست جدید",
|
||||
},
|
||||
footer: {
|
||||
enabled: true,
|
||||
@@ -253,6 +257,9 @@ export const SECTIONS = {
|
||||
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
|
||||
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
|
||||
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
|
||||
linksTitle: "دسترسی",
|
||||
productsTitle: "محصولات",
|
||||
contactTitle: "ارتباط",
|
||||
},
|
||||
};
|
||||
export const CATEGORY_COVERS = {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@ import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Injectable,
|
||||
Module,
|
||||
@@ -55,10 +54,6 @@ export class SectionsService {
|
||||
|
||||
async update(key: string, body: unknown, user: AuthUser) {
|
||||
if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد");
|
||||
// site-wide settings (contact info, SEO defaults) are limited to admins
|
||||
if (key === "settings" && user.role !== "ADMIN") {
|
||||
throw new ForbiddenException("فقط مدیر کل میتواند تنظیمات سایت را تغییر دهد");
|
||||
}
|
||||
const parsed = sectionSchemas[key].safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestException({
|
||||
|
||||
@@ -19,6 +19,7 @@ export const SECTIONS: SiteSections = {
|
||||
locality: "تهران",
|
||||
country: "IR",
|
||||
},
|
||||
mapUrl: "",
|
||||
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
|
||||
openingHours: {
|
||||
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
|
||||
@@ -26,6 +27,7 @@ export const SECTIONS: SiteSections = {
|
||||
closes: "17:00",
|
||||
},
|
||||
foundingDate: "2007",
|
||||
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
|
||||
socials: [
|
||||
{ type: "website", url: "/" },
|
||||
{ type: "email", url: "mailto:info@imenpishgaman.ir" },
|
||||
@@ -265,6 +267,8 @@ export const SECTIONS: SiteSections = {
|
||||
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس میگیریم",
|
||||
successTitle: "درخواست شما ثبت شد",
|
||||
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس میگیرند.",
|
||||
submitLabel: "ارسال درخواست مشاوره",
|
||||
resetLabel: "ثبت درخواست جدید",
|
||||
},
|
||||
|
||||
footer: {
|
||||
@@ -274,6 +278,9 @@ export const SECTIONS: SiteSections = {
|
||||
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
|
||||
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
|
||||
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
|
||||
linksTitle: "دسترسی",
|
||||
productsTitle: "محصولات",
|
||||
contactTitle: "ارتباط",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const SOCIALS = computed(() =>
|
||||
|
||||
<!-- quick links -->
|
||||
<div class="md:col-span-2">
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// دسترسی</div>
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.linksTitle }}</div>
|
||||
<ul class="mt-5 space-y-3">
|
||||
<li v-for="l in QUICK_LINKS" :key="l.href">
|
||||
<a :href="l.href" class="text-sm text-muted transition-colors hover:text-ink">
|
||||
@@ -91,7 +91,7 @@ const SOCIALS = computed(() =>
|
||||
|
||||
<!-- products -->
|
||||
<div class="md:col-span-2">
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// محصولات</div>
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.productsTitle }}</div>
|
||||
<ul class="mt-5 space-y-3">
|
||||
<li v-for="p in PRODUCT_LINKS" :key="p.href">
|
||||
<NuxtLink :to="p.href" class="text-sm text-muted transition-colors hover:text-ink">
|
||||
@@ -103,10 +103,19 @@ const SOCIALS = computed(() =>
|
||||
|
||||
<!-- contact -->
|
||||
<div class="md:col-span-3">
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// ارتباط</div>
|
||||
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.contactTitle }}</div>
|
||||
<address class="mt-5 space-y-3 text-sm text-muted not-italic">
|
||||
<p class="leading-7">
|
||||
{{ settings.address.locality }}، {{ settings.address.street }}
|
||||
<a
|
||||
v-if="settings.mapUrl"
|
||||
:href="settings.mapUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="ms-2 text-accent transition-colors hover:text-ink"
|
||||
>
|
||||
نقشه
|
||||
</a>
|
||||
</p>
|
||||
<p class="font-mono tracking-wider" dir="ltr">
|
||||
<a :href="`tel:${settings.phone}`" class="transition-colors hover:text-ink">{{ settings.phoneDisplay }}</a>
|
||||
|
||||
@@ -72,15 +72,15 @@ onBeforeUnmount(() => {
|
||||
<span class="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
||||
<span class="font-mono">{{ time }}</span>
|
||||
<span class="text-muted/50">|</span>
|
||||
<span>تهران — شیفت {{ shift }}</span>
|
||||
<span>{{ settings.address.locality }} — شیفت {{ shift }}</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/#contact"
|
||||
:href="settings.headerCta.href"
|
||||
class="hidden items-center gap-2 rounded-full bg-accent px-5 py-2.5 text-sm font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_0_30px_rgba(255,90,0,0.4)] md:flex"
|
||||
>
|
||||
<VuesaxIcon name="call" class="size-4" />
|
||||
درخواست مشاوره
|
||||
{{ settings.headerCta.label }}
|
||||
</a>
|
||||
|
||||
<button
|
||||
@@ -139,7 +139,7 @@ onBeforeUnmount(() => {
|
||||
<VuesaxIcon name="call" class="size-4 text-accent" />
|
||||
<a :href="`tel:${settings.phone}`" class="font-mono" dir="ltr">{{ settings.phoneDisplay }}</a>
|
||||
</div>
|
||||
<div class="text-xs text-muted">تهران — شیفت {{ shift }}، {{ time }}</div>
|
||||
<div class="text-xs text-muted">{{ settings.address.locality }} — شیفت {{ shift }}، {{ time }}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -50,14 +50,14 @@ const step = computed(
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="flex size-12 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(255,90,0,0.3)] ring-1 ring-white/10"
|
||||
class="flex size-20 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-[0_0_28px_rgba(255,90,0,0.3)] ring-1 ring-white/10"
|
||||
>
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
alt="ایمن پیشگامان"
|
||||
width="32"
|
||||
height="32"
|
||||
class="size-8 object-contain"
|
||||
width="80"
|
||||
height="80"
|
||||
class="size-full scale-110 object-contain object-center"
|
||||
/>
|
||||
</span>
|
||||
<span class="font-mono text-xs tracking-[0.35em] text-muted">
|
||||
|
||||
@@ -12,7 +12,13 @@ const content = computed(() => site.sections.contact);
|
||||
const INFO = computed(() => {
|
||||
const s = site.settings;
|
||||
return [
|
||||
{ icon: "location", label: "نشانی", value: `${s.address.locality}، ${s.address.street}` },
|
||||
{
|
||||
icon: "location",
|
||||
label: "نشانی",
|
||||
value: `${s.address.locality}، ${s.address.street}`,
|
||||
href: s.mapUrl || undefined,
|
||||
external: Boolean(s.mapUrl),
|
||||
},
|
||||
{ icon: "call", label: "تلفن تماس", value: s.phoneDisplay, ltr: true, href: `tel:${s.phone}` },
|
||||
{ icon: "sms", label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` },
|
||||
{ icon: "clock", label: "ساعات کاری", value: s.workingHours },
|
||||
@@ -59,6 +65,8 @@ const contact = useContactStore();
|
||||
<component
|
||||
:is="item.href ? 'a' : 'div'"
|
||||
:href="item.href"
|
||||
:target="item.external ? '_blank' : undefined"
|
||||
:rel="item.external ? 'noopener' : undefined"
|
||||
:class="`mt-1 block text-sm font-medium ${item.ltr ? 'font-mono tracking-wider' : ''}`"
|
||||
:dir="item.ltr ? 'ltr' : 'rtl'"
|
||||
>
|
||||
@@ -106,7 +114,7 @@ const contact = useContactStore();
|
||||
class="mt-7 rounded-full border border-line px-7 py-3 text-sm transition-colors hover:border-accent hover:text-accent"
|
||||
@click="contact.reset"
|
||||
>
|
||||
ثبت درخواست جدید
|
||||
{{ content.resetLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +180,7 @@ const contact = useContactStore();
|
||||
</template>
|
||||
<template v-else>
|
||||
<VuesaxIcon name="send-2" class="size-5" />
|
||||
ارسال درخواست مشاوره
|
||||
{{ content.submitLabel }}
|
||||
</template>
|
||||
</AButton>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
const settings = computed(() => useSiteStore().settings);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/" class="flex items-center gap-3" aria-label="ایمن پیشگامان">
|
||||
<NuxtLink to="/" class="flex items-center gap-3" :aria-label="settings.name">
|
||||
<span
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
class="flex size-11 items-center justify-center overflow-hidden rounded-xl bg-white p-0.5 shadow-[0_0_28px_rgba(10,30,92,0.45)] sm:size-12"
|
||||
>
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
alt="ایمن پیشگامان"
|
||||
width="28"
|
||||
height="28"
|
||||
class="size-7 object-contain"
|
||||
:alt="settings.name"
|
||||
width="48"
|
||||
height="48"
|
||||
class="size-full object-contain object-center"
|
||||
/>
|
||||
</span>
|
||||
<span class="leading-tight">
|
||||
<span class="block text-lg font-black">ایمن پیشگامان</span>
|
||||
<span class="block text-lg font-black">{{ settings.name }}</span>
|
||||
<span class="block font-mono text-[9px] tracking-[0.32em] text-muted">
|
||||
IMEN PISHGAMAN CO.
|
||||
{{ settings.nameEn }}
|
||||
</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
|
||||
Vendored
+20
@@ -28,6 +28,7 @@ export declare const settingsSectionSchema: z.ZodObject<{
|
||||
locality: z.ZodString;
|
||||
country: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
mapUrl: z.ZodDefault<z.ZodString>;
|
||||
workingHours: z.ZodString;
|
||||
openingHours: z.ZodObject<{
|
||||
days: z.ZodArray<z.ZodEnum<{
|
||||
@@ -43,6 +44,10 @@ export declare const settingsSectionSchema: z.ZodObject<{
|
||||
closes: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
foundingDate: z.ZodString;
|
||||
headerCta: z.ZodDefault<z.ZodObject<{
|
||||
label: z.ZodString;
|
||||
href: z.ZodString;
|
||||
}, z.core.$strip>>;
|
||||
socials: z.ZodArray<z.ZodObject<{
|
||||
type: z.ZodEnum<{
|
||||
instagram: "instagram";
|
||||
@@ -224,6 +229,8 @@ export declare const contactSectionSchema: z.ZodObject<{
|
||||
formTitle: z.ZodString;
|
||||
successTitle: z.ZodString;
|
||||
successText: z.ZodString;
|
||||
submitLabel: z.ZodDefault<z.ZodString>;
|
||||
resetLabel: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
export declare const footerSectionSchema: z.ZodObject<{
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
@@ -231,6 +238,9 @@ export declare const footerSectionSchema: z.ZodObject<{
|
||||
copyright: z.ZodString;
|
||||
certLine: z.ZodDefault<z.ZodString>;
|
||||
emergencyNote: z.ZodDefault<z.ZodString>;
|
||||
linksTitle: z.ZodDefault<z.ZodString>;
|
||||
productsTitle: z.ZodDefault<z.ZodString>;
|
||||
contactTitle: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
export declare const sectionSchemas: {
|
||||
readonly settings: z.ZodObject<{
|
||||
@@ -246,6 +256,7 @@ export declare const sectionSchemas: {
|
||||
locality: z.ZodString;
|
||||
country: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
mapUrl: z.ZodDefault<z.ZodString>;
|
||||
workingHours: z.ZodString;
|
||||
openingHours: z.ZodObject<{
|
||||
days: z.ZodArray<z.ZodEnum<{
|
||||
@@ -261,6 +272,10 @@ export declare const sectionSchemas: {
|
||||
closes: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
foundingDate: z.ZodString;
|
||||
headerCta: z.ZodDefault<z.ZodObject<{
|
||||
label: z.ZodString;
|
||||
href: z.ZodString;
|
||||
}, z.core.$strip>>;
|
||||
socials: z.ZodArray<z.ZodObject<{
|
||||
type: z.ZodEnum<{
|
||||
instagram: "instagram";
|
||||
@@ -442,6 +457,8 @@ export declare const sectionSchemas: {
|
||||
formTitle: z.ZodString;
|
||||
successTitle: z.ZodString;
|
||||
successText: z.ZodString;
|
||||
submitLabel: z.ZodDefault<z.ZodString>;
|
||||
resetLabel: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
readonly footer: z.ZodObject<{
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
@@ -449,6 +466,9 @@ export declare const sectionSchemas: {
|
||||
copyright: z.ZodString;
|
||||
certLine: z.ZodDefault<z.ZodString>;
|
||||
emergencyNote: z.ZodDefault<z.ZodString>;
|
||||
linksTitle: z.ZodDefault<z.ZodString>;
|
||||
productsTitle: z.ZodDefault<z.ZodString>;
|
||||
contactTitle: z.ZodDefault<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
};
|
||||
export type SectionKey = keyof typeof sectionSchemas;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"sections.d.ts","sourceRoot":"","sources":["../src/sections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,eAAO,MAAM,SAAS,yFAQZ,CAAC;AAEX,eAAO,MAAM,YAAY,+HAYf,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAKvD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEhE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BhC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa5B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;iBAG5B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;iBAY7B,CAAC;AAEH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;iBAKrC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ7B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;iBAahC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;iBAMjC,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;iBAalC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAIpC,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;iBAS/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;iBAM9B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAajB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,cAAc,CAAC;AACrD,eAAO,MAAM,YAAY,EAAkC,UAAU,EAAE,CAAC;AACxE,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,MAAM,MAAM,YAAY,GAAG;KAAG,CAAC,IAAI,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC;CAAE,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,UAAmC,CAAC;AAEtF,yCAAyC;AACzC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAa9E,CAAC"}
|
||||
{"version":3,"file":"sections.d.ts","sourceRoot":"","sources":["../src/sections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,eAAO,MAAM,SAAS,yFAQZ,CAAC;AAEX,eAAO,MAAM,YAAY,+HAYf,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAKvD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEhE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiChC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa5B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;iBAG5B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;iBAY7B,CAAC;AAEH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;iBAKrC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ7B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;iBAahC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;iBAMjC,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;iBAalC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAIpC,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;iBAa/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;iBAU9B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAajB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,cAAc,CAAC;AACrD,eAAO,MAAM,YAAY,EAAkC,UAAU,EAAE,CAAC;AACxE,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,MAAM,MAAM,YAAY,GAAG;KAAG,CAAC,IAAI,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC;CAAE,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,UAAmC,CAAC;AAEtF,yCAAyC;AACzC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAa9E,CAAC"}
|
||||
Vendored
+14
-2
@@ -53,6 +53,8 @@ export const settingsSectionSchema = z.object({
|
||||
locality: req(80),
|
||||
country: z.string().trim().length(2).default("IR"),
|
||||
}),
|
||||
/** Google Maps / Neshan / etc. share link shown next to the address */
|
||||
mapUrl: text(500),
|
||||
/** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
|
||||
workingHours: req(120),
|
||||
openingHours: z.object({
|
||||
@@ -61,6 +63,8 @@ export const settingsSectionSchema = z.object({
|
||||
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
|
||||
}),
|
||||
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"),
|
||||
/** sticky header CTA (e.g. «درخواست مشاوره») */
|
||||
headerCta: linkSchema.default({ label: "درخواست مشاوره", href: "/#contact" }),
|
||||
socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
|
||||
ogImage: imageRefSchema.extend({
|
||||
width: z.number().int().positive().nullable().default(null),
|
||||
@@ -156,6 +160,10 @@ export const contactSectionSchema = z.object({
|
||||
formTitle: req(160),
|
||||
successTitle: req(80),
|
||||
successText: req(300),
|
||||
/** submit button on the contact form */
|
||||
submitLabel: req(80).default("ارسال درخواست مشاوره"),
|
||||
/** button shown after a successful submit */
|
||||
resetLabel: req(80).default("ثبت درخواست جدید"),
|
||||
});
|
||||
export const footerSectionSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
@@ -163,6 +171,10 @@ export const footerSectionSchema = z.object({
|
||||
copyright: req(160),
|
||||
certLine: text(160),
|
||||
emergencyNote: text(80),
|
||||
/** column headings under the brand block */
|
||||
linksTitle: req(40).default("دسترسی"),
|
||||
productsTitle: req(40).default("محصولات"),
|
||||
contactTitle: req(40).default("ارتباط"),
|
||||
});
|
||||
export const sectionSchemas = {
|
||||
settings: settingsSectionSchema,
|
||||
@@ -192,7 +204,7 @@ export const SECTION_LABELS = {
|
||||
standards: { title: "استانداردها", desc: "استانداردها و گواهینامهها" },
|
||||
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
|
||||
testimonials: { title: "نظرات مشتریان", desc: "نقلقولهای مشتریان" },
|
||||
contact: { title: "تماس با ما", desc: "عنوانها و متنهای فرم تماس" },
|
||||
footer: { title: "فوتر", desc: "متن معرفی، کپیرایت و توضیحات پایین صفحه" },
|
||||
contact: { title: "تماس با ما", desc: "عنوانها، متنهای فرم، و ویرایش نشانی و اطلاعات تماس" },
|
||||
footer: { title: "فوتر", desc: "متن معرفی، عناوین ستونها، کپیرایت و توضیحات پایین صفحه" },
|
||||
};
|
||||
//# sourceMappingURL=sections.js.map
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -61,6 +61,8 @@ export const settingsSectionSchema = z.object({
|
||||
locality: req(80),
|
||||
country: z.string().trim().length(2).default("IR"),
|
||||
}),
|
||||
/** Google Maps / Neshan / etc. share link shown next to the address */
|
||||
mapUrl: text(500),
|
||||
/** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
|
||||
workingHours: req(120),
|
||||
openingHours: z.object({
|
||||
@@ -69,6 +71,8 @@ export const settingsSectionSchema = z.object({
|
||||
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
|
||||
}),
|
||||
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"),
|
||||
/** sticky header CTA (e.g. «درخواست مشاوره») */
|
||||
headerCta: linkSchema.default({ label: "درخواست مشاوره", href: "/#contact" }),
|
||||
socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
|
||||
ogImage: imageRefSchema.extend({
|
||||
width: z.number().int().positive().nullable().default(null),
|
||||
@@ -180,6 +184,10 @@ export const contactSectionSchema = z.object({
|
||||
formTitle: req(160),
|
||||
successTitle: req(80),
|
||||
successText: req(300),
|
||||
/** submit button on the contact form */
|
||||
submitLabel: req(80).default("ارسال درخواست مشاوره"),
|
||||
/** button shown after a successful submit */
|
||||
resetLabel: req(80).default("ثبت درخواست جدید"),
|
||||
});
|
||||
|
||||
export const footerSectionSchema = z.object({
|
||||
@@ -188,6 +196,10 @@ export const footerSectionSchema = z.object({
|
||||
copyright: req(160),
|
||||
certLine: text(160),
|
||||
emergencyNote: text(80),
|
||||
/** column headings under the brand block */
|
||||
linksTitle: req(40).default("دسترسی"),
|
||||
productsTitle: req(40).default("محصولات"),
|
||||
contactTitle: req(40).default("ارتباط"),
|
||||
});
|
||||
|
||||
export const sectionSchemas = {
|
||||
@@ -225,6 +237,6 @@ export const SECTION_LABELS: Record<SectionKey, { title: string; desc: string }>
|
||||
standards: { title: "استانداردها", desc: "استانداردها و گواهینامهها" },
|
||||
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
|
||||
testimonials: { title: "نظرات مشتریان", desc: "نقلقولهای مشتریان" },
|
||||
contact: { title: "تماس با ما", desc: "عنوانها و متنهای فرم تماس" },
|
||||
footer: { title: "فوتر", desc: "متن معرفی، کپیرایت و توضیحات پایین صفحه" },
|
||||
contact: { title: "تماس با ما", desc: "عنوانها، متنهای فرم، و ویرایش نشانی و اطلاعات تماس" },
|
||||
footer: { title: "فوتر", desc: "متن معرفی، عناوین ستونها، کپیرایت و توضیحات پایین صفحه" },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user