Files
imen_pishghaman/apps/admin/src/pages/SectionEditPage.vue
T

204 lines
7.2 KiB
Vue

<script setup lang="ts">
import { isSectionKey, SECTION_LABELS, type SectionKey, sectionSchemas } from "@imen/shared";
import { message } from "ant-design-vue";
import { type Component, computed, ref } from "vue";
import { useRoute } from "vue-router";
import { api, errorMessage } from "@/api/http";
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";
import FooterForm from "@/sections/FooterForm.vue";
import HeroForm from "@/sections/HeroForm.vue";
import IndustriesForm from "@/sections/IndustriesForm.vue";
import ProductsIntroForm from "@/sections/ProductsIntroForm.vue";
import ServicesForm from "@/sections/ServicesForm.vue";
import SettingsForm from "@/sections/SettingsForm.vue";
import StandardsForm from "@/sections/StandardsForm.vue";
import StatsForm from "@/sections/StatsForm.vue";
import TapeForm from "@/sections/TapeForm.vue";
import TestimonialsForm from "@/sections/TestimonialsForm.vue";
const FORMS: Record<SectionKey, Component> = {
settings: SettingsForm,
hero: HeroForm,
tape: TapeForm,
stats: StatsForm,
productsIntro: ProductsIntroForm,
about: AboutForm,
services: ServicesForm,
standards: StandardsForm,
industries: IndustriesForm,
testimonials: TestimonialsForm,
contact: ContactForm,
footer: FooterForm,
};
const auth = useAuthStore();
const route = useRoute();
const key = String(route.params.key);
const valid = isSectionKey(key);
const sectionKey = (valid ? key : "hero") as SectionKey;
// the page is re-created per key (RouterView keys by path), so one form instance per section
const { model, validate, handleError, reset } = useZodForm(
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
{} as never,
);
const loaded = ref(false);
const saving = ref(false);
const updatedAt = ref<string | null>(null);
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;
if (sectionKey !== "settings" && data.enabled === undefined) {
data.enabled = true;
}
reset(data as never);
updatedAt.value = res.updatedAt;
loaded.value = true;
} catch (e) {
void message.error(errorMessage(e));
}
}
async function save() {
const data = validate();
if (!data) return;
saving.value = true;
try {
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
reset(res.data as never);
updatedAt.value = res.updatedAt;
void message.success("ذخیره شد؛ سایت تا چند لحظه دیگر به‌روز می‌شود");
} catch (e) {
handleError(e);
} finally {
saving.value = false;
}
}
const togglingVisibility = ref(false);
async function onVisibilityChange(val: any) {
const nextVal = Boolean(val);
(model.value as any).enabled = nextVal;
togglingVisibility.value = true;
try {
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
`/admin/sections/${sectionKey}/visibility`,
{
method: "PATCH",
body: { enabled: nextVal },
},
);
(model.value as any).enabled = res.enabled !== false;
updatedAt.value = res.updatedAt;
void message.success(
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
);
} catch (e) {
(model.value as any).enabled = !nextVal;
void message.error(errorMessage(e));
} finally {
togglingVisibility.value = false;
}
}
if (valid) void load();
</script>
<template>
<a-result v-if="!valid" status="404" title="بخش پیدا نشد" />
<template v-else>
<PageHeader
:title="label!.title"
:subtitle="updatedAt ? `آخرین ذخیره: ${jalali(updatedAt)}` : label!.desc"
back="/sections"
>
<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"
:loading="togglingVisibility"
checked-children="نمایش"
un-checked-children="مخفی"
@change="onVisibilityChange"
/>
</div>
<a :href="SITE_URL" target="_blank" rel="noopener" class="max-lg:hidden"><a-button>مشاهده سایت</a-button></a>
<a-button
type="primary"
class="max-lg:hidden"
:loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
@click="save"
>
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
ذخیره
</a-button>
</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"
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" />
</a-form>
</a-spin>
<!-- Mobile Sticky Bottom Action Bar -->
<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"
:loading="togglingVisibility"
size="small"
@change="onVisibilityChange"
/>
<span class="text-xs font-medium text-gray-700">
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
</span>
</div>
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
<div class="flex items-center gap-2">
<a :href="SITE_URL" target="_blank" rel="noopener">
<a-button size="middle" class="!px-3" title="مشاهده سایت">
<template #icon><VuesaxIcon name="eye" :size="16" /></template>
</a-button>
</a>
<a-button
type="primary"
size="middle"
class="!font-bold !px-5"
:loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
@click="save"
>
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
ذخیره تغییرات
</a-button>
</div>
</div>
</template>
</template>