5 Commits
Author SHA1 Message Date
S. EsmaeiliandCursor 9ad88af20f 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>
2026-11-12 18:42:59 +03:30
S. EsmaeiliandCursor cad1a8652e build: refresh admin dist with corrected site URL
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-07 16:57:09 +03:30
S. EsmaeiliandCursor a8184ed59c fix(admin): point view-site links to http://imenpishgaman.ir
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-07 16:56:15 +03:30
S. EsmaeiliandClaude Sonnet 5.5 3dc5f4ae91 fix(compose): give internal services unique network aliases
On a shared Docker network (Dokploy) plain service names like web or api
also resolve to other stacks' containers, so the gateway balanced requests
across foreign containers and returned 502. Services reached by name now
carry an imen-* alias, used by the gateway upstreams and the service URLs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 08:28:04 +03:30
S. EsmaeiliandClaude Sonnet 5.5 f64b18aafb build: refresh production build output
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 16:42:16 +03:30
56 changed files with 424 additions and 232 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# loaded by `vite build` (the pre-built dist/ the admin image serves); dev uses http://localhost:3000 # loaded by `vite build` (the pre-built dist/ the admin image serves); dev uses http://localhost:3000
VITE_SITE_URL=https://imenpishgaman.ir VITE_SITE_URL=http://imenpishgaman.ir
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{b as e}from"./VuesaxIcon-B2OgulS6.js";import{Et as t,It as n,Nt as r,Ot as i,Tt as a,_t as o,dt as s,i as c,kt as l,lt as u,ot as d,r as f,yt as p}from"./index-BuGK6ptM.js";var m=Symbol(`form-errors`),h=e=>Object.fromEntries(e.map(e=>[e.path,e.message]));function g(r,i){let a=n(structuredClone(i)),o=n({});t(m,o);function s(){let t=r.safeParse(a.value);return t.success?(o.value={},t.data):(o.value=h(e(t.error)),d.error(`لطفاً خطاهای فرم را برطرف کنید`),null)}function l(e){let t=f(e).issues;t?.length&&(o.value=h(t)),d.error(c(e))}function u(e){a.value=structuredClone(e),o.value={}}return{model:a,errors:o,validate:s,handleError:l,reset:u}}var _=o({__name:`FormField`,props:{path:{},label:{},hint:{},required:{type:Boolean}},setup(e){let t=e,n=p(m,null),o=u(()=>n?.value[t.path]);return(t,n)=>{let c=l(`a-form-item`);return a(),s(c,{label:e.label,required:e.required,"validate-status":o.value?`error`:void 0,help:o.value||e.hint},{default:r(()=>[i(t.$slots,`default`)]),_:3},8,[`label`,`required`,`validate-status`,`help`])}}});export{g as n,_ as t}; import{b as e}from"./VuesaxIcon-UCFGkXBv.js";import{Et as t,It as n,Nt as r,Ot as i,Tt as a,_t as o,dt as s,i as c,kt as l,lt as u,ot as d,r as f,yt as p}from"./index-1U5l_fLl.js";var m=Symbol(`form-errors`),h=e=>Object.fromEntries(e.map(e=>[e.path,e.message]));function g(r,i){let a=n(structuredClone(i)),o=n({});t(m,o);function s(){let t=r.safeParse(a.value);return t.success?(o.value={},t.data):(o.value=h(e(t.error)),d.error(`لطفاً خطاهای فرم را برطرف کنید`),null)}function l(e){let t=f(e).issues;t?.length&&(o.value=h(t)),d.error(c(e))}function u(e){a.value=structuredClone(e),o.value={}}return{model:a,errors:o,validate:s,handleError:l,reset:u}}var _=o({__name:`FormField`,props:{path:{},label:{},hint:{},required:{type:Boolean}},setup(e){let t=e,n=p(m,null),o=u(()=>n?.value[t.path]);return(t,n)=>{let c=l(`a-form-item`);return a(),s(c,{label:e.label,required:e.required,"validate-status":o.value?`error`:void 0,help:o.value||e.hint},{default:r(()=>[i(t.$slots,`default`)]),_:3},8,[`label`,`required`,`validate-status`,`help`])}}});export{g as n,_ as t};
@@ -1 +1 @@
import{t as e,y as t}from"./VuesaxIcon-B2OgulS6.js";import{Bt as n,Lt as r,Nt as i,Tt as a,_t as o,bt as s,dt as c,gt as l,jt as u,kt as d,pt as f,ut as p}from"./index-BuGK6ptM.js";var m={class:`flex items-center gap-2`},h={class:`flex size-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-[#ff5a00]`},g={key:1,class:`text-xs text-gray-400`},_={class:`flex items-center gap-2.5 py-0.5`,dir:`ltr`},v={class:`text-xs font-mono`},y=o({__name:`IconSelect`,props:s({allowClear:{type:Boolean}},{modelValue:{required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(o){let s=u(o,`modelValue`),y=t.map(e=>({value:e,label:e}));return(t,u)=>{let b=d(`a-select`);return a(),f(`div`,m,[p(`span`,h,[s.value?(a(),c(e,{key:0,name:s.value,size:20},null,8,[`name`])):(a(),f(`span`,g,`---`))]),l(b,{value:s.value,"onUpdate:value":u[0]||=e=>s.value=e,class:`min-w-0 flex-1`,"show-search":``,"allow-clear":o.allowClear,options:r(y),placeholder:`انتخاب آیکون`},{option:i(({value:t})=>[p(`span`,_,[l(e,{name:t,size:18,class:`text-orange-600`},null,8,[`name`]),p(`span`,v,n(t),1)])]),_:1},8,[`value`,`allow-clear`,`options`])])}}});export{y as t}; import{t as e,y as t}from"./VuesaxIcon-UCFGkXBv.js";import{Bt as n,Lt as r,Nt as i,Tt as a,_t as o,bt as s,dt as c,gt as l,jt as u,kt as d,pt as f,ut as p}from"./index-1U5l_fLl.js";var m={class:`flex items-center gap-2`},h={class:`flex size-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-[#ff5a00]`},g={key:1,class:`text-xs text-gray-400`},_={class:`flex items-center gap-2.5 py-0.5`,dir:`ltr`},v={class:`text-xs font-mono`},y=o({__name:`IconSelect`,props:s({allowClear:{type:Boolean}},{modelValue:{required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(o){let s=u(o,`modelValue`),y=t.map(e=>({value:e,label:e}));return(t,u)=>{let b=d(`a-select`);return a(),f(`div`,m,[p(`span`,h,[s.value?(a(),c(e,{key:0,name:s.value,size:20},null,8,[`name`])):(a(),f(`span`,g,`---`))]),l(b,{value:s.value,"onUpdate:value":u[0]||=e=>s.value=e,class:`min-w-0 flex-1`,"show-search":``,"allow-clear":o.allowClear,options:r(y),placeholder:`انتخاب آیکون`},{option:i(({value:t})=>[p(`span`,_,[l(e,{name:t,size:18,class:`text-orange-600`},null,8,[`name`]),p(`span`,v,n(t),1)])]),_:1},8,[`value`,`allow-clear`,`options`])])}}});export{y as t};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as e,n as t,t as n}from"./VuesaxIcon-B2OgulS6.js";import{It as r,Lt as i,Nt as a,Tt as o,_t as s,a as c,gt as l,ht as u,kt as d,mt as f,o as p,pt as m,st as h,t as g,ut as _}from"./index-BuGK6ptM.js";import"./logo-BuUh917n.js";import{n as v,t as y}from"./FormField-CiVT7mqC.js";var b={class:`login-page relative flex min-h-screen overflow-hidden text-[#edede6]`},x={class:`relative z-10 mx-auto grid w-full max-w-[1120px] flex-1 items-center gap-10 px-5 py-12 md:px-8 lg:grid-cols-12 lg:gap-16`},S={class:`login-fade-3 lg:col-span-6`},C={class:`login-panel w-full max-w-md border border-[#2a3d63]/70 bg-[#1a2744]/80 p-7 backdrop-blur-md md:p-8 lg:me-auto`},w=t(s({__name:`LoginPage`,setup(t){let s=g(),w=p(),T=c(),{model:E,validate:D,handleError:O}=v(e,{phone:``,password:``}),k=r(!1);async function A(){let e=D();if(e){k.value=!0;try{await s.login(e);let t=typeof T.query.redirect==`string`?T.query.redirect:`/`;await w.replace(t.startsWith(`/`)?t:`/`)}catch(e){O(e)}finally{k.value=!1}}}return(e,t)=>{let r=d(`a-input`),s=d(`a-input-password`),c=d(`a-button`),p=d(`a-form`);return o(),m(`div`,b,[t[5]||=f(`<div class="pointer-events-none absolute inset-0 bg-[#0b1224]" data-v-2d323d25></div><div class="login-grid pointer-events-none absolute inset-0 opacity-50" data-v-2d323d25></div><div class="pointer-events-none absolute -start-24 top-1/4 size-[420px] rounded-full bg-[#ff5a00]/15 blur-[120px]" data-v-2d323d25></div><div class="pointer-events-none absolute -end-16 bottom-0 size-[360px] rounded-full bg-[#ffb800]/10 blur-[100px]" data-v-2d323d25></div><div class="login-tape pointer-events-none absolute bottom-0 end-0 z-10 h-48 w-1.5 opacity-70" data-v-2d323d25></div><div class="login-noise pointer-events-none absolute inset-0 z-[5]" data-v-2d323d25></div>`,6),_(`div`,x,[_(`div`,S,[_(`div`,C,[t[3]||=_(`div`,{class:`mb-7 text-center`},[_(`div`,{class:`font-mono text-[10px] tracking-[0.28em] text-[#8e9ab0]`},`SIGN IN`),_(`h2`,{class:`mt-2 text-xl font-bold text-[#edede6]`},`ورود به پنل`),_(`p`,{class:`mt-1.5 text-sm text-[#8e9ab0]`},`شماره موبایل و رمز عبور خود را وارد کنید`)],-1),l(p,{class:`login-form`,layout:`vertical`,onSubmit:h(A,[`prevent`])},{default:a(()=>[l(y,{path:`phone`,label:`شماره موبایل`},{default:a(()=>[l(r,{value:i(E).phone,"onUpdate:value":t[0]||=e=>i(E).phone=e,size:`large`,class:`ltr-input`,autocomplete:`tel`,autofocus:``,inputmode:`tel`,placeholder:`09121234567`},{prefix:a(()=>[l(n,{name:`mobile`,size:16,class:`text-[#8e9ab0]`})]),_:1},8,[`value`])]),_:1}),l(y,{path:`password`,label:`رمز عبور`},{default:a(()=>[l(s,{value:i(E).password,"onUpdate:value":t[1]||=e=>i(E).password=e,size:`large`,class:`ltr-input`,autocomplete:`current-password`,placeholder:`••••••••`},{prefix:a(()=>[l(n,{name:`lock`,size:16,class:`text-[#8e9ab0]`})]),_:1},8,[`value`])]),_:1}),l(c,{type:`primary`,"html-type":`submit`,size:`large`,block:``,loading:k.value,class:`!mt-2 !h-12 !rounded-full !font-bold !shadow-[0_8px_32px_rgba(255,90,0,0.35)]`},{default:a(()=>[...t[2]||=[u(` ورود به پنل `,-1)]]),_:1},8,[`loading`])]),_:1})])]),t[4]||=f(`<div class="login-fade-1 lg:col-span-6" data-v-2d323d25><div class="inline-flex items-center gap-2.5 rounded-full border border-[#ff5a00]/30 bg-[#ff5a00]/10 px-4 py-2 backdrop-blur" data-v-2d323d25><span class="size-1.5 animate-pulse rounded-full bg-[#ff5a00]" data-v-2d323d25></span><span class="text-xs font-medium text-[#ff5a00]" data-v-2d323d25>پنل مدیریت ایمن پیشگامان</span></div><div class="mt-8 flex items-center gap-4" data-v-2d323d25><span class="flex size-14 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]" data-v-2d323d25><img src="/logo.png" alt="ایمن پیشگامان" class="size-9 object-contain" data-v-2d323d25></span><div class="leading-tight" data-v-2d323d25><div class="text-2xl font-black tracking-tight md:text-3xl" data-v-2d323d25>ایمن پیشگامان</div><div class="mt-1 font-mono text-[10px] tracking-[0.32em] text-[#8e9ab0]" data-v-2d323d25> IMEN PISHGAMAN CO. </div></div></div><h1 class="mt-8 text-3xl font-black leading-[1.25] md:text-5xl md:leading-[1.2]" data-v-2d323d25> ورود امن <span class="mt-2 inline-block -rotate-1 rounded-lg bg-[#ffb800] px-3 py-1 text-[#0b1224] shadow-[0_10px_40px_rgba(255,184,0,0.22)]" data-v-2d323d25> به سامانه </span></h1><p class="mt-6 max-w-md text-sm leading-8 text-[#edede6]/70 md:text-base md:leading-9" data-v-2d323d25> مدیریت محصولات، محتوا و پیام‌های سایت — فقط برای کاربران مجاز. </p></div>`,1)])])}}}),[[`__scopeId`,`data-v-2d323d25`]]);export{w as default}; import{c as e,n as t,t as n}from"./VuesaxIcon-UCFGkXBv.js";import{It as r,Lt as i,Nt as a,Tt as o,_t as s,a as c,gt as l,ht as u,kt as d,mt as f,o as p,pt as m,st as h,t as g,ut as _}from"./index-1U5l_fLl.js";import"./logo-BuUh917n.js";import{n as v,t as y}from"./FormField-DbkYgDsg.js";var b={class:`login-page relative flex min-h-screen overflow-hidden text-[#edede6]`},x={class:`relative z-10 mx-auto grid w-full max-w-[1120px] flex-1 items-center gap-10 px-5 py-12 md:px-8 lg:grid-cols-12 lg:gap-16`},S={class:`login-fade-3 lg:col-span-6`},C={class:`login-panel w-full max-w-md border border-[#2a3d63]/70 bg-[#1a2744]/80 p-7 backdrop-blur-md md:p-8 lg:me-auto`},w=t(s({__name:`LoginPage`,setup(t){let s=g(),w=p(),T=c(),{model:E,validate:D,handleError:O}=v(e,{phone:``,password:``}),k=r(!1);async function A(){let e=D();if(e){k.value=!0;try{await s.login(e);let t=typeof T.query.redirect==`string`?T.query.redirect:`/`;await w.replace(t.startsWith(`/`)?t:`/`)}catch(e){O(e)}finally{k.value=!1}}}return(e,t)=>{let r=d(`a-input`),s=d(`a-input-password`),c=d(`a-button`),p=d(`a-form`);return o(),m(`div`,b,[t[5]||=f(`<div class="pointer-events-none absolute inset-0 bg-[#0b1224]" data-v-2d323d25></div><div class="login-grid pointer-events-none absolute inset-0 opacity-50" data-v-2d323d25></div><div class="pointer-events-none absolute -start-24 top-1/4 size-[420px] rounded-full bg-[#ff5a00]/15 blur-[120px]" data-v-2d323d25></div><div class="pointer-events-none absolute -end-16 bottom-0 size-[360px] rounded-full bg-[#ffb800]/10 blur-[100px]" data-v-2d323d25></div><div class="login-tape pointer-events-none absolute bottom-0 end-0 z-10 h-48 w-1.5 opacity-70" data-v-2d323d25></div><div class="login-noise pointer-events-none absolute inset-0 z-[5]" data-v-2d323d25></div>`,6),_(`div`,x,[_(`div`,S,[_(`div`,C,[t[3]||=_(`div`,{class:`mb-7 text-center`},[_(`div`,{class:`font-mono text-[10px] tracking-[0.28em] text-[#8e9ab0]`},`SIGN IN`),_(`h2`,{class:`mt-2 text-xl font-bold text-[#edede6]`},`ورود به پنل`),_(`p`,{class:`mt-1.5 text-sm text-[#8e9ab0]`},`شماره موبایل و رمز عبور خود را وارد کنید`)],-1),l(p,{class:`login-form`,layout:`vertical`,onSubmit:h(A,[`prevent`])},{default:a(()=>[l(y,{path:`phone`,label:`شماره موبایل`},{default:a(()=>[l(r,{value:i(E).phone,"onUpdate:value":t[0]||=e=>i(E).phone=e,size:`large`,class:`ltr-input`,autocomplete:`tel`,autofocus:``,inputmode:`tel`,placeholder:`09121234567`},{prefix:a(()=>[l(n,{name:`mobile`,size:16,class:`text-[#8e9ab0]`})]),_:1},8,[`value`])]),_:1}),l(y,{path:`password`,label:`رمز عبور`},{default:a(()=>[l(s,{value:i(E).password,"onUpdate:value":t[1]||=e=>i(E).password=e,size:`large`,class:`ltr-input`,autocomplete:`current-password`,placeholder:`••••••••`},{prefix:a(()=>[l(n,{name:`lock`,size:16,class:`text-[#8e9ab0]`})]),_:1},8,[`value`])]),_:1}),l(c,{type:`primary`,"html-type":`submit`,size:`large`,block:``,loading:k.value,class:`!mt-2 !h-12 !rounded-full !font-bold !shadow-[0_8px_32px_rgba(255,90,0,0.35)]`},{default:a(()=>[...t[2]||=[u(` ورود به پنل `,-1)]]),_:1},8,[`loading`])]),_:1})])]),t[4]||=f(`<div class="login-fade-1 lg:col-span-6" data-v-2d323d25><div class="inline-flex items-center gap-2.5 rounded-full border border-[#ff5a00]/30 bg-[#ff5a00]/10 px-4 py-2 backdrop-blur" data-v-2d323d25><span class="size-1.5 animate-pulse rounded-full bg-[#ff5a00]" data-v-2d323d25></span><span class="text-xs font-medium text-[#ff5a00]" data-v-2d323d25>پنل مدیریت ایمن پیشگامان</span></div><div class="mt-8 flex items-center gap-4" data-v-2d323d25><span class="flex size-14 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]" data-v-2d323d25><img src="/logo.png" alt="ایمن پیشگامان" class="size-9 object-contain" data-v-2d323d25></span><div class="leading-tight" data-v-2d323d25><div class="text-2xl font-black tracking-tight md:text-3xl" data-v-2d323d25>ایمن پیشگامان</div><div class="mt-1 font-mono text-[10px] tracking-[0.32em] text-[#8e9ab0]" data-v-2d323d25> IMEN PISHGAMAN CO. </div></div></div><h1 class="mt-8 text-3xl font-black leading-[1.25] md:text-5xl md:leading-[1.2]" data-v-2d323d25> ورود امن <span class="mt-2 inline-block -rotate-1 rounded-lg bg-[#ffb800] px-3 py-1 text-[#0b1224] shadow-[0_10px_40px_rgba(255,184,0,0.22)]" data-v-2d323d25> به سامانه </span></h1><p class="mt-6 max-w-md text-sm leading-8 text-[#edede6]/70 md:text-base md:leading-9" data-v-2d323d25> مدیریت محصولات، محتوا و پیام‌های سایت — فقط برای کاربران مجاز. </p></div>`,1)])])}}}),[[`__scopeId`,`data-v-2d323d25`]]);export{w as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{n as e,t}from"./VuesaxIcon-B2OgulS6.js";import{Bt as n,Nt as r,Ot as i,Tt as a,_t as o,dt as s,ft as c,gt as l,kt as u,pt as d,ut as f}from"./index-BuGK6ptM.js";var p={class:`page-header`},m={class:`page-header__main`},h={class:`min-w-0`},g={class:`page-header__title`},_={key:0,class:`page-header__subtitle`},v={key:0,class:`page-header__actions`},y=e(o({__name:`PageHeader`,props:{title:{},subtitle:{},back:{}},setup(e){return(o,y)=>{let b=u(`RouterLink`);return a(),d(`div`,p,[f(`div`,m,[e.back?(a(),s(b,{key:0,to:e.back,class:`page-header__back`,title:`بازگشت`},{default:r(()=>[l(t,{name:`arrow-right`,size:18})]),_:1},8,[`to`])):c(``,!0),f(`div`,h,[f(`h1`,g,n(e.title),1),e.subtitle?(a(),d(`p`,_,n(e.subtitle),1)):c(``,!0)])]),o.$slots.default?(a(),d(`div`,v,[i(o.$slots,`default`,{},void 0,!0)])):c(``,!0)])}}}),[[`__scopeId`,`data-v-c0b72af8`]]);export{y as t}; import{n as e,t}from"./VuesaxIcon-UCFGkXBv.js";import{Bt as n,Nt as r,Ot as i,Tt as a,_t as o,dt as s,ft as c,gt as l,kt as u,pt as d,ut as f}from"./index-1U5l_fLl.js";var p={class:`page-header`},m={class:`page-header__main`},h={class:`min-w-0`},g={class:`page-header__title`},_={key:0,class:`page-header__subtitle`},v={key:0,class:`page-header__actions`},y=e(o({__name:`PageHeader`,props:{title:{},subtitle:{},back:{}},setup(e){return(o,y)=>{let b=u(`RouterLink`);return a(),d(`div`,p,[f(`div`,m,[e.back?(a(),s(b,{key:0,to:e.back,class:`page-header__back`,title:`بازگشت`},{default:r(()=>[l(t,{name:`arrow-right`,size:18})]),_:1},8,[`to`])):c(``,!0),f(`div`,h,[f(`h1`,g,n(e.title),1),e.subtitle?(a(),d(`p`,_,n(e.subtitle),1)):c(``,!0)])]),o.$slots.default?(a(),d(`div`,v,[i(o.$slots,`default`,{},void 0,!0)])):c(``,!0)])}}}),[[`__scopeId`,`data-v-c0b72af8`]]);export{y as t};
@@ -1,4 +1,4 @@
import{t as e,v as t}from"./VuesaxIcon-B2OgulS6.js";import{Bt as n,Ct as r,Dt as i,It as a,Lt as o,Mt as s,Nt as c,Rt as l,Tt as u,_t as d,a as f,bt as p,ct as m,dt as h,ft as g,gt as _,ht as v,i as y,jt as b,kt as x,lt as S,n as C,o as ee,ot as w,pt as T,st as te,ut as E}from"./index-BuGK6ptM.js";import{t as D}from"./format-B9MYDQJN.js";import{t as ne}from"./PageHeader-3Ryvg2HD.js";import{n as re,t as O}from"./FormField-CiVT7mqC.js";import{n as ie,r as ae,t as oe}from"./ListEditor-C8F91lSJ.js";import{t as se}from"./slug-Flyc3fOh.js";var ce=d({__name:`LinesInput`,props:p({placeholder:{},rows:{}},{modelValue:{required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(e){let t=b(e,`modelValue`),n=e=>e.split(` import{t as e,v as t}from"./VuesaxIcon-UCFGkXBv.js";import{Bt as n,Ct as r,Dt as i,It as a,Lt as o,Mt as s,Nt as c,Rt as l,Tt as u,_t as d,a as f,bt as p,ct as m,dt as h,ft as g,gt as _,ht as v,i as y,jt as b,kt as x,lt as S,n as C,o as ee,ot as w,pt as T,st as te,ut as E}from"./index-1U5l_fLl.js";import{t as D}from"./format-BwvWvVfG.js";import{t as ne}from"./PageHeader-C9oHYjmE.js";import{n as re,t as O}from"./FormField-DbkYgDsg.js";import{n as ie,r as ae,t as oe}from"./ListEditor-xwdh5hs-.js";import{t as se}from"./slug-Flyc3fOh.js";var ce=d({__name:`LinesInput`,props:p({placeholder:{},rows:{}},{modelValue:{required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(e){let t=b(e,`modelValue`),n=e=>e.split(`
`).map(e=>e.trim()).filter(Boolean),r=a(t.value.join(` `).map(e=>e.trim()).filter(Boolean),r=a(t.value.join(`
`));s(t,e=>{n(r.value).join(` `));s(t,e=>{n(r.value).join(`
`)!==e.join(` `)!==e.join(`
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{o as e,s as t}from"./VuesaxIcon-B2OgulS6.js";import{Bt as n,It as r,Lt as i,Nt as a,Tt as o,_t as s,ct as c,gt as l,ht as u,kt as d,n as f,ot as p,pt as m,st as h,t as g,ut as _}from"./index-BuGK6ptM.js";import{t as v}from"./PageHeader-3Ryvg2HD.js";import{n as y,t as b}from"./FormField-CiVT7mqC.js";var x={class:`grid max-w-4xl gap-4 md:gap-6 md:grid-cols-2`},S={class:`flex items-center gap-3.5 mb-4 pb-4 border-b border-gray-100`},C={class:`flex size-14 items-center justify-center rounded-2xl bg-gradient-to-tr from-[#ff5a00] to-amber-500 text-xl font-black text-white shadow-md shadow-orange-500/20`},w={class:`text-base font-bold text-gray-900`},T={class:`mt-0.5 text-xs font-mono text-gray-500`,dir:`ltr`},E={dir:`ltr`,class:`font-mono`},D=s({__name:`ProfilePage`,setup(s){let D=g(),{model:O,validate:k,handleError:A,reset:j}=y(t,{currentPassword:``,newPassword:``}),M=r(!1);async function N(){let e=k();if(e){M.value=!0;try{await f(`/admin/auth/change-password`,{method:`POST`,body:e}),p.success(`رمز عبور تغییر کرد`),j({currentPassword:``,newPassword:``})}catch(e){A(e)}finally{M.value=!1}}}return(t,r)=>{let s=d(`a-descriptions-item`),f=d(`a-tag`),p=d(`a-descriptions`),g=d(`a-card`),y=d(`a-input-password`),k=d(`a-button`),A=d(`a-form`);return o(),m(c,null,[l(v,{title:`حساب کاربری`,subtitle:`مشخصات فردی، نقش دسترسی و تغییر رمز عبور ورود به سامانه`}),_(`div`,x,[l(g,{title:`مشخصات کاربری`},{default:a(()=>[_(`div`,S,[_(`div`,C,n(i(D).user?.name?.slice(0,1)||`U`),1),_(`div`,null,[_(`div`,w,n(i(D).user?.name),1),_(`div`,T,n(i(D).user?.phone),1)])]),l(p,{column:1,size:`small`},{default:a(()=>[l(s,{label:`نام`},{default:a(()=>[u(n(i(D).user?.name),1)]),_:1}),l(s,{label:`موبایل`},{default:a(()=>[_(`span`,E,n(i(D).user?.phone),1)]),_:1}),l(s,{label:`نقش دسترسی`},{default:a(()=>[l(f,{color:i(D).user?.role===`ADMIN`?`orange`:`blue`},{default:a(()=>[u(n(i(D).user&&i(e)[i(D).user.role]),1)]),_:1},8,[`color`])]),_:1})]),_:1})]),_:1}),l(g,{title:`تغییر رمز عبور`},{default:a(()=>[l(A,{layout:`vertical`,onSubmit:h(N,[`prevent`])},{default:a(()=>[l(b,{path:`currentPassword`,label:`رمز فعلی`,required:``},{default:a(()=>[l(y,{value:i(O).currentPassword,"onUpdate:value":r[0]||=e=>i(O).currentPassword=e,class:`ltr-input h-10`,autocomplete:`current-password`},null,8,[`value`])]),_:1}),l(b,{path:`newPassword`,label:`رمز جدید`,hint:`حداقل ۸ کاراکتر`,required:``},{default:a(()=>[l(y,{value:i(O).newPassword,"onUpdate:value":r[1]||=e=>i(O).newPassword=e,class:`ltr-input h-10`,autocomplete:`new-password`},null,8,[`value`])]),_:1}),l(k,{type:`primary`,"html-type":`submit`,loading:M.value,class:`w-full md:w-auto h-10 !font-bold`},{default:a(()=>[...r[2]||=[u(` تغییر رمز ورود `,-1)]]),_:1},8,[`loading`])]),_:1})]),_:1})])],64)}}});export{D as default}; import{o as e,s as t}from"./VuesaxIcon-UCFGkXBv.js";import{Bt as n,It as r,Lt as i,Nt as a,Tt as o,_t as s,ct as c,gt as l,ht as u,kt as d,n as f,ot as p,pt as m,st as h,t as g,ut as _}from"./index-1U5l_fLl.js";import{t as v}from"./PageHeader-C9oHYjmE.js";import{n as y,t as b}from"./FormField-DbkYgDsg.js";var x={class:`grid max-w-4xl gap-4 md:gap-6 md:grid-cols-2`},S={class:`flex items-center gap-3.5 mb-4 pb-4 border-b border-gray-100`},C={class:`flex size-14 items-center justify-center rounded-2xl bg-gradient-to-tr from-[#ff5a00] to-amber-500 text-xl font-black text-white shadow-md shadow-orange-500/20`},w={class:`text-base font-bold text-gray-900`},T={class:`mt-0.5 text-xs font-mono text-gray-500`,dir:`ltr`},E={dir:`ltr`,class:`font-mono`},D=s({__name:`ProfilePage`,setup(s){let D=g(),{model:O,validate:k,handleError:A,reset:j}=y(t,{currentPassword:``,newPassword:``}),M=r(!1);async function N(){let e=k();if(e){M.value=!0;try{await f(`/admin/auth/change-password`,{method:`POST`,body:e}),p.success(`رمز عبور تغییر کرد`),j({currentPassword:``,newPassword:``})}catch(e){A(e)}finally{M.value=!1}}}return(t,r)=>{let s=d(`a-descriptions-item`),f=d(`a-tag`),p=d(`a-descriptions`),g=d(`a-card`),y=d(`a-input-password`),k=d(`a-button`),A=d(`a-form`);return o(),m(c,null,[l(v,{title:`حساب کاربری`,subtitle:`مشخصات فردی، نقش دسترسی و تغییر رمز عبور ورود به سامانه`}),_(`div`,x,[l(g,{title:`مشخصات کاربری`},{default:a(()=>[_(`div`,S,[_(`div`,C,n(i(D).user?.name?.slice(0,1)||`U`),1),_(`div`,null,[_(`div`,w,n(i(D).user?.name),1),_(`div`,T,n(i(D).user?.phone),1)])]),l(p,{column:1,size:`small`},{default:a(()=>[l(s,{label:`نام`},{default:a(()=>[u(n(i(D).user?.name),1)]),_:1}),l(s,{label:`موبایل`},{default:a(()=>[_(`span`,E,n(i(D).user?.phone),1)]),_:1}),l(s,{label:`نقش دسترسی`},{default:a(()=>[l(f,{color:i(D).user?.role===`ADMIN`?`orange`:`blue`},{default:a(()=>[u(n(i(D).user&&i(e)[i(D).user.role]),1)]),_:1},8,[`color`])]),_:1})]),_:1})]),_:1}),l(g,{title:`تغییر رمز عبور`},{default:a(()=>[l(A,{layout:`vertical`,onSubmit:h(N,[`prevent`])},{default:a(()=>[l(b,{path:`currentPassword`,label:`رمز فعلی`,required:``},{default:a(()=>[l(y,{value:i(O).currentPassword,"onUpdate:value":r[0]||=e=>i(O).currentPassword=e,class:`ltr-input h-10`,autocomplete:`current-password`},null,8,[`value`])]),_:1}),l(b,{path:`newPassword`,label:`رمز جدید`,hint:`حداقل ۸ کاراکتر`,required:``},{default:a(()=>[l(y,{value:i(O).newPassword,"onUpdate:value":r[1]||=e=>i(O).newPassword=e,class:`ltr-input h-10`,autocomplete:`new-password`},null,8,[`value`])]),_:1}),l(k,{type:`primary`,"html-type":`submit`,loading:M.value,class:`w-full md:w-auto h-10 !font-bold`},{default:a(()=>[...r[2]||=[u(` تغییر رمز ورود `,-1)]]),_:1},8,[`loading`])]),_:1})]),_:1})])],64)}}});export{D as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{d as e,f as t}from"./VuesaxIcon-B2OgulS6.js";import{Bt as n,Ct as r,Dt as i,Ft as a,It as o,Lt as s,Nt as c,Rt as l,Tt as u,_t as d,ct as f,dt as p,ft as m,gt as h,ht as g,i as _,kt as v,n as y,o as b,ot as x,pt as S,t as C,ut as w}from"./index-BuGK6ptM.js";import{a as T}from"./format-B9MYDQJN.js";import{t as E}from"./PageHeader-3Ryvg2HD.js";var D={class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-3`},O={class:`flex items-center justify-between gap-2`},k=[`onClick`],A={class:`font-bold text-gray-800 group-hover:text-orange-600 transition-colors`},j={key:0,class:`flex items-center flex-shrink-0`},M=[`onClick`],N=[`onClick`],P={key:0},F=d({__name:`SectionsPage`,setup(d){let F=b(),I=C(),L=e.filter(e=>e!==`settings`||I.isAdmin),R=o(!0),z=a({hero:!0,tape:!0,stats:!0,productsIntro:!0,about:!0,services:!0,standards:!0,industries:!0,testimonials:!0,contact:!0,footer:!0}),B=a({}),V=a({});function H(e){return z[e]!==!1}function U(e){return B[e]??null}async function W(){R.value=!0;try{let e=await y(`/admin/sections`);for(let t of e)z[t.key]=t.enabled!==!1,B[t.key]=t.updatedAt}catch(e){x.error(_(e))}finally{R.value=!1}}async function G(e,n){if(e===`settings`)return;let r=z[e]!==!1;z[e]=n,V[e]=!0;try{let r=await y(`/admin/sections/${e}/visibility`,{method:`PATCH`,body:{enabled:n}});z[e]=r.enabled!==!1,B[e]=r.updatedAt,x.success(`وضعیت «${t[e].title}» به ${n?`نمایش`:`عدم نمایش`} تغییر یافت`)}catch(t){z[e]=r,x.error(_(t))}finally{V[e]=!1}}function K(e){F.push({name:`section-edit`,params:{key:e}})}return r(()=>{W()}),(e,r)=>{let a=v(`a-tag`),o=v(`a-switch`),d=v(`a-card`),_=v(`a-spin`);return u(),S(f,null,[h(E,{title:`بخش‌های سایت`,subtitle:`متن‌ها، تصاویر و فهرست‌های صفحه اصلی؛ تنظیم وضعیت نمایش و ویرایش محتوا`}),h(_,{spinning:R.value},{default:c(()=>[w(`div`,D,[(u(!0),S(f,null,i(s(L),e=>(u(),S(`div`,{key:e,class:`block group`},[h(d,{hoverable:``,class:l([`h-full flex flex-col justify-between transition-all`,{"opacity-75 bg-gray-50/60":e!==`settings`&&!H(e)}])},{default:c(()=>[w(`div`,null,[w(`div`,O,[w(`div`,{class:`flex items-center gap-2 cursor-pointer flex-1`,onClick:t=>K(e)},[w(`span`,A,n(s(t)[e].title),1),e===`settings`?(u(),p(a,{key:0,color:`blue`},{default:c(()=>[...r[0]||=[g(`تنظیمات پایه`,-1)]]),_:1})):H(e)?m(``,!0):(u(),p(a,{key:1,color:`error`},{default:c(()=>[...r[1]||=[g(`مخفی`,-1)]]),_:1}))],8,k),e===`settings`?m(``,!0):(u(),S(`div`,j,[h(o,{checked:H(e),loading:V[e],"checked-children":`نمایش`,"un-checked-children":`مخفی`,onChange:t=>G(e,!!t)},null,8,[`checked`,`loading`,`onChange`])]))]),w(`p`,{class:`mt-2 text-sm leading-6 text-gray-500 cursor-pointer`,onClick:t=>K(e)},n(s(t)[e].desc),9,M)]),w(`div`,{class:`mt-4 flex items-center justify-between border-t border-gray-100 pt-3 text-xs text-gray-400 cursor-pointer`,onClick:t=>K(e)},[r[2]||=w(`span`,{class:`text-primary font-medium group-hover:underline`},`ویرایش بخش ←`,-1),U(e)?(u(),S(`span`,P,`آخرین تغییر: `+n(s(T)(U(e))),1)):m(``,!0)],8,N)]),_:2},1032,[`class`])]))),128))])]),_:1},8,[`spinning`])],64)}}});export{F as default}; import{d as e,f as t}from"./VuesaxIcon-UCFGkXBv.js";import{Bt as n,Ct as r,Dt as i,Ft as a,It as o,Lt as s,Nt as c,Rt as l,Tt as u,_t as d,ct as f,dt as p,ft as m,gt as h,ht as g,i as _,kt as v,n as y,o as b,ot as x,pt as S,t as C,ut as w}from"./index-1U5l_fLl.js";import{a as T}from"./format-BwvWvVfG.js";import{t as E}from"./PageHeader-C9oHYjmE.js";var D={class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-3`},O={class:`flex items-center justify-between gap-2`},k=[`onClick`],A={class:`font-bold text-gray-800 group-hover:text-orange-600 transition-colors`},j={key:0,class:`flex items-center flex-shrink-0`},M=[`onClick`],N=[`onClick`],P={key:0},F=d({__name:`SectionsPage`,setup(d){let F=b(),I=C(),L=e.filter(e=>e!==`settings`||I.isAdmin),R=o(!0),z=a({hero:!0,tape:!0,stats:!0,productsIntro:!0,about:!0,services:!0,standards:!0,industries:!0,testimonials:!0,contact:!0,footer:!0}),B=a({}),V=a({});function H(e){return z[e]!==!1}function U(e){return B[e]??null}async function W(){R.value=!0;try{let e=await y(`/admin/sections`);for(let t of e)z[t.key]=t.enabled!==!1,B[t.key]=t.updatedAt}catch(e){x.error(_(e))}finally{R.value=!1}}async function G(e,n){if(e===`settings`)return;let r=z[e]!==!1;z[e]=n,V[e]=!0;try{let r=await y(`/admin/sections/${e}/visibility`,{method:`PATCH`,body:{enabled:n}});z[e]=r.enabled!==!1,B[e]=r.updatedAt,x.success(`وضعیت «${t[e].title}» به ${n?`نمایش`:`عدم نمایش`} تغییر یافت`)}catch(t){z[e]=r,x.error(_(t))}finally{V[e]=!1}}function K(e){F.push({name:`section-edit`,params:{key:e}})}return r(()=>{W()}),(e,r)=>{let a=v(`a-tag`),o=v(`a-switch`),d=v(`a-card`),_=v(`a-spin`);return u(),S(f,null,[h(E,{title:`بخش‌های سایت`,subtitle:`متن‌ها، تصاویر و فهرست‌های صفحه اصلی؛ تنظیم وضعیت نمایش و ویرایش محتوا`}),h(_,{spinning:R.value},{default:c(()=>[w(`div`,D,[(u(!0),S(f,null,i(s(L),e=>(u(),S(`div`,{key:e,class:`block group`},[h(d,{hoverable:``,class:l([`h-full flex flex-col justify-between transition-all`,{"opacity-75 bg-gray-50/60":e!==`settings`&&!H(e)}])},{default:c(()=>[w(`div`,null,[w(`div`,O,[w(`div`,{class:`flex items-center gap-2 cursor-pointer flex-1`,onClick:t=>K(e)},[w(`span`,A,n(s(t)[e].title),1),e===`settings`?(u(),p(a,{key:0,color:`blue`},{default:c(()=>[...r[0]||=[g(`تنظیمات پایه`,-1)]]),_:1})):H(e)?m(``,!0):(u(),p(a,{key:1,color:`error`},{default:c(()=>[...r[1]||=[g(`مخفی`,-1)]]),_:1}))],8,k),e===`settings`?m(``,!0):(u(),S(`div`,j,[h(o,{checked:H(e),loading:V[e],"checked-children":`نمایش`,"un-checked-children":`مخفی`,onChange:t=>G(e,!!t)},null,8,[`checked`,`loading`,`onChange`])]))]),w(`p`,{class:`mt-2 text-sm leading-6 text-gray-500 cursor-pointer`,onClick:t=>K(e)},n(s(t)[e].desc),9,M)]),w(`div`,{class:`mt-4 flex items-center justify-between border-t border-gray-100 pt-3 text-xs text-gray-400 cursor-pointer`,onClick:t=>K(e)},[r[2]||=w(`span`,{class:`text-primary font-medium group-hover:underline`},`ویرایش بخش ←`,-1),U(e)?(u(),S(`span`,P,`آخرین تغییر: `+n(s(T)(U(e))),1)):m(``,!0)],8,N)]),_:2},1032,[`class`])]))),128))])]),_:1},8,[`spinning`])],64)}}});export{F as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
var e=new Intl.DateTimeFormat(`fa-IR-u-ca-persian`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`}),t=t=>t?e.format(new Date(t)):`—`,n=e=>e<1048576?`${(e/1024).toLocaleString(`fa-IR`,{maximumFractionDigits:0})} کیلوبایت`:`${(e/1024/1024).toLocaleString(`fa-IR`,{maximumFractionDigits:1})} مگابایت`,r=e=>e.toLocaleString(`fa-IR`),i=`https://imenpishgaman.ir`,a={create:`ایجاد`,update:`ویرایش`,delete:`حذف`,upload:`آپلود`,login:`ورود`,publish:`انتشار`,unpublish:`عدم انتشار`,duplicate:`کپی`,reorder:`مرتب‌سازی`,"change-password":`تغییر رمز`},o={product:`محصول`,category:`دسته`,section:`بخش سایت`,media:`رسانه`,user:`کاربر`,message:`پیام`},s=(e,t)=>`${a[e]??e} ${o[t]??t}`;export{t as a,n as i,s as n,r,i as t};
+1
View File
@@ -0,0 +1 @@
var e=new Intl.DateTimeFormat(`fa-IR-u-ca-persian`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`}),t=t=>t?e.format(new Date(t)):`—`,n=e=>e<1048576?`${(e/1024).toLocaleString(`fa-IR`,{maximumFractionDigits:0})} کیلوبایت`:`${(e/1024/1024).toLocaleString(`fa-IR`,{maximumFractionDigits:1})} مگابایت`,r=e=>e.toLocaleString(`fa-IR`),i=`http://imenpishgaman.ir`,a={create:`ایجاد`,update:`ویرایش`,delete:`حذف`,upload:`آپلود`,login:`ورود`,publish:`انتشار`,unpublish:`عدم انتشار`,duplicate:`کپی`,reorder:`مرتب‌سازی`,"change-password":`تغییر رمز`},o={product:`محصول`,category:`دسته`,section:`بخش سایت`,media:`رسانه`,user:`کاربر`,message:`پیام`},s=(e,t)=>`${a[e]??e} ${o[t]??t}`;export{t as a,n as i,s as n,r,i as t};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>پنل مدیریت ایمن پیشگامان</title> <title>پنل مدیریت ایمن پیشگامان</title>
<script type="module" crossorigin src="/assets/index-BuGK6ptM.js"></script> <script type="module" crossorigin src="/assets/index-1U5l_fLl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DjFpBvci.css"> <link rel="stylesheet" crossorigin href="/assets/index-DjFpBvci.css">
</head> </head>
<body> <body>
@@ -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>
+2 -2
View File
@@ -107,9 +107,9 @@ async function submit() {
<div class="mt-8 flex items-center gap-4"> <div class="mt-8 flex items-center gap-4">
<span <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> </span>
<div class="leading-tight"> <div class="leading-tight">
<div class="text-2xl font-black tracking-tight md:text-3xl">ایمن پیشگامان</div> <div class="text-2xl font-black tracking-tight md:text-3xl">ایمن پیشگامان</div>
+58 -22
View File
@@ -1,5 +1,12 @@
<script setup lang="ts"> <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 { message } from "ant-design-vue";
import { type Component, computed, ref } from "vue"; import { type Component, computed, ref } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
@@ -8,7 +15,6 @@ import VuesaxIcon from "@/components/VuesaxIcon.vue";
import type { SectionResponse } from "@/api/types"; import type { SectionResponse } from "@/api/types";
import PageHeader from "@/components/PageHeader.vue"; import PageHeader from "@/components/PageHeader.vue";
import { useZodForm } from "@/composables/useZodForm"; import { useZodForm } from "@/composables/useZodForm";
import { useAuthStore } from "@/stores/auth";
import { jalali, SITE_URL } from "@/utils/format"; import { jalali, SITE_URL } from "@/utils/format";
import AboutForm from "@/sections/AboutForm.vue"; import AboutForm from "@/sections/AboutForm.vue";
import ContactForm from "@/sections/ContactForm.vue"; import ContactForm from "@/sections/ContactForm.vue";
@@ -38,7 +44,6 @@ const FORMS: Record<SectionKey, Component> = {
footer: FooterForm, footer: FooterForm,
}; };
const auth = useAuthStore();
const route = useRoute(); const route = useRoute();
const key = String(route.params.key); const key = String(route.params.key);
const valid = isSectionKey(key); const valid = isSectionKey(key);
@@ -49,6 +54,14 @@ const { model, validate, handleError, reset } = useZodForm(
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"], sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
{} as never, {} 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 loaded = ref(false);
const saving = ref(false); const saving = ref(false);
const updatedAt = ref<string | null>(null); const updatedAt = ref<string | null>(null);
@@ -57,12 +70,20 @@ const label = computed(() => (valid ? SECTION_LABELS[sectionKey] : null));
async function load() { async function load() {
try { try {
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`); 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) { if (sectionKey !== "settings" && data.enabled === undefined) {
data.enabled = true; data.enabled = true;
} }
reset(data as never); reset(data as never);
updatedAt.value = res.updatedAt; 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; loaded.value = true;
} catch (e) { } catch (e) {
void message.error(errorMessage(e)); void message.error(errorMessage(e));
@@ -72,8 +93,25 @@ async function load() {
async function save() { async function save() {
const data = validate(); const data = validate();
if (!data) return; if (!data) return;
if (sectionKey === "contact" && settingsModel.value) {
const settingsParsed = settingsSectionSchema.safeParse(settingsModel.value);
if (!settingsParsed.success) {
void message.error("اطلاعات تماس معتبر نیست — نشانی، تلفن یا ایمیل را بررسی کنید");
return;
}
}
saving.value = true; saving.value = true;
try { 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 }); const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
reset(res.data as never); reset(res.data as never);
updatedAt.value = res.updatedAt; updatedAt.value = res.updatedAt;
@@ -87,9 +125,9 @@ async function save() {
const togglingVisibility = ref(false); const togglingVisibility = ref(false);
async function onVisibilityChange(val: any) { async function onVisibilityChange(val: unknown) {
const nextVal = Boolean(val); const nextVal = Boolean(val);
(model.value as any).enabled = nextVal; (model.value as { enabled?: boolean }).enabled = nextVal;
togglingVisibility.value = true; togglingVisibility.value = true;
try { try {
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>( const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
@@ -99,13 +137,13 @@ async function onVisibilityChange(val: any) {
body: { enabled: nextVal }, body: { enabled: nextVal },
}, },
); );
(model.value as any).enabled = res.enabled !== false; (model.value as { enabled?: boolean }).enabled = res.enabled !== false;
updatedAt.value = res.updatedAt; updatedAt.value = res.updatedAt;
void message.success( void message.success(
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`, `وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
); );
} catch (e) { } catch (e) {
(model.value as any).enabled = !nextVal; (model.value as { enabled?: boolean }).enabled = !nextVal;
void message.error(errorMessage(e)); void message.error(errorMessage(e));
} finally { } finally {
togglingVisibility.value = false; 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"> <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> <span class="text-sm font-medium text-gray-700">نمایش در سایت:</span>
<a-switch <a-switch
:checked="(model as any)?.enabled !== false" :checked="(model as { enabled?: boolean })?.enabled !== false"
:loading="togglingVisibility" :loading="togglingVisibility"
checked-children="نمایش" checked-children="نمایش"
un-checked-children="مخفی" un-checked-children="مخفی"
@@ -138,7 +176,7 @@ if (valid) void load();
type="primary" type="primary"
class="max-lg:hidden" class="max-lg:hidden"
:loading="saving" :loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)" :disabled="!loaded"
@click="save" @click="save"
> >
<template #icon><VuesaxIcon name="save-2" :size="16" /></template> <template #icon><VuesaxIcon name="save-2" :size="16" /></template>
@@ -147,21 +185,19 @@ if (valid) void load();
</PageHeader> </PageHeader>
<a-spin :spinning="!loaded"> <a-spin :spinning="!loaded">
<a-alert <a-alert
v-if="loaded && sectionKey === 'settings' && !auth.isAdmin" v-if="loaded && sectionKey !== 'settings' && (model as { enabled?: boolean }).enabled === false"
type="error"
show-icon
message="فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد."
class="mb-4"
/>
<a-alert
v-else-if="loaded && sectionKey !== 'settings' && (model as any).enabled === false"
type="warning" type="warning"
show-icon show-icon
message="این بخش در حال حاضر در سایت نمایش داده نمی‌شود (مخفی است)." message="این بخش در حال حاضر در سایت نمایش داده نمی‌شود (مخفی است)."
class="mb-4" class="mb-4"
/> />
<a-form v-if="loaded" layout="vertical" class="pb-20 lg:pb-0" @submit.prevent="save"> <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-form>
</a-spin> </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 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"> <div v-if="sectionKey !== 'settings'" class="flex items-center gap-2">
<a-switch <a-switch
:checked="(model as any)?.enabled !== false" :checked="(model as { enabled?: boolean })?.enabled !== false"
:loading="togglingVisibility" :loading="togglingVisibility"
size="small" size="small"
@change="onVisibilityChange" @change="onVisibilityChange"
/> />
<span class="text-xs font-medium text-gray-700"> <span class="text-xs font-medium text-gray-700">
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }} {{ (model as { enabled?: boolean })?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
</span> </span>
</div> </div>
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div> <div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
@@ -191,7 +227,7 @@ if (valid) void load();
size="middle" size="middle"
class="!font-bold !px-5" class="!font-bold !px-5"
:loading="saving" :loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)" :disabled="!loaded"
@click="save" @click="save"
> >
<template #icon><VuesaxIcon name="save-2" :size="16" /></template> <template #icon><VuesaxIcon name="save-2" :size="16" /></template>
+1 -4
View File
@@ -6,13 +6,10 @@ import { useRouter } from "vue-router";
import { api, errorMessage } from "@/api/http"; import { api, errorMessage } from "@/api/http";
import type { AdminSectionSummary } from "@/api/types"; import type { AdminSectionSummary } from "@/api/types";
import PageHeader from "@/components/PageHeader.vue"; import PageHeader from "@/components/PageHeader.vue";
import { useAuthStore } from "@/stores/auth";
import { jalali } from "@/utils/format"; import { jalali } from "@/utils/format";
const router = useRouter(); const router = useRouter();
const auth = useAuthStore(); const keys = SECTION_KEYS;
// site-wide settings are editable by admins only
const keys = SECTION_KEYS.filter((k) => k !== "settings" || auth.isAdmin);
const loading = ref(true); const loading = ref(true);
const sectionEnabled = reactive<Record<string, boolean>>({ const sectionEnabled = reactive<Record<string, boolean>>({
+4 -6
View File
@@ -103,7 +103,7 @@ onMounted(load);
</script> </script>
<template> <template>
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخش‌ها دسترسی دارد؛ ویرایشگر به جز کاربران و تنظیمات سایت"> <PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخش‌ها دسترسی دارد؛ ویرایشگر به جز مدیریت کاربران و حذف داده‌ها">
<a-button type="primary" @click="openNew"> <a-button type="primary" @click="openNew">
<template #icon><VuesaxIcon name="add" :size="16" /></template> <template #icon><VuesaxIcon name="add" :size="16" /></template>
کاربر جدید کاربر جدید
@@ -241,7 +241,6 @@ onMounted(load);
</div> </div>
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside"> <ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
<li>مدیریت کاربران، نقش‌ها و تنظیمات امنیتی</li> <li>مدیریت کاربران، نقش‌ها و تنظیمات امنیتی</li>
<li>تنظیمات پایه سایت، سئو، شبکه‌ها و ساعات کاری</li>
<li>حذف داده‌ها (محصولات، دسته‌ها، پیام‌ها، رسانه‌ها)</li> <li>حذف داده‌ها (محصولات، دسته‌ها، پیام‌ها، رسانه‌ها)</li>
</ul> </ul>
</div> </div>
@@ -253,7 +252,7 @@ onMounted(load);
</div> </div>
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside"> <ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
<li>ایجاد، ویرایش و انتشار محصولات و دسته‌بندی‌ها</li> <li>ایجاد، ویرایش و انتشار محصولات و دسته‌بندی‌ها</li>
<li>ویرایش محتوای بخش‌های صفحه اصلی</li> <li>ویرایش بخش‌های سایت، نشانی، تماس، سئو و شبکه‌ها</li>
<li>آپلود تصاویر در رسانه و مشاهده پیام‌های تماس</li> <li>آپلود تصاویر در رسانه و مشاهده پیام‌های تماس</li>
</ul> </ul>
</div> </div>
@@ -269,7 +268,6 @@ onMounted(load);
</div> </div>
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside"> <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>
<li>مشاهده لاگ‌های امنیتی و رویدادهای کامل سامانه</li> <li>مشاهده لاگ‌های امنیتی و رویدادهای کامل سامانه</li>
</ul> </ul>
@@ -282,7 +280,7 @@ onMounted(load);
</div> </div>
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside"> <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> <li>آپلود و ویرایش اطلاعات رسانه‌ها در کتابخانه رسانه</li>
<li>مشاهده پیام‌های تماس و به‌روزرسانی وضعیت (بدون دسترسی حذف یا مدیریت کاربران)</li> <li>مشاهده پیام‌های تماس و به‌روزرسانی وضعیت (بدون دسترسی حذف یا مدیریت کاربران)</li>
</ul> </ul>
@@ -319,7 +317,7 @@ onMounted(load);
:options="ROLE_OPTIONS" :options="ROLE_OPTIONS"
/> />
<div class="mt-1.5 text-[11px] text-gray-500"> <div class="mt-1.5 text-[11px] text-gray-500">
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف داده‌ها، تنظیمات و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخش‌ها و پیام‌ها (بدون حذف)' }} {{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف داده‌ها و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخش‌های سایت (از جمله نشانی و تماس)، پیام‌ها (بدون حذف)' }}
</div> </div>
</FormField> </FormField>
<FormField v-if="!isNew" path="isActive"> <FormField v-if="!isNew" path="isActive">
+1 -1
View File
@@ -13,7 +13,7 @@ const data = defineModel<SectionData<"about">>({ required: true });
<div class="grid gap-6 xl:grid-cols-3"> <div class="grid gap-6 xl:grid-cols-3">
<div class="space-y-6 xl:col-span-2"> <div class="space-y-6 xl:col-span-2">
<a-card title="عنوان و متن"> <a-card title="عنوان و متن">
<SectionHeadFields v-model="data.head" :with-desc="false" /> <SectionHeadFields v-model="data.head" />
<FormField path="body" label="متن درباره ما" required> <FormField path="body" label="متن درباره ما" required>
<a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" /> <a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" />
</FormField> </FormField>
+21 -1
View File
@@ -1,17 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SectionData } from "@imen/shared"; import type { SectionData } from "@imen/shared";
import ContactInfoFields from "@/components/ContactInfoFields.vue";
import FormField from "@/components/FormField.vue"; import FormField from "@/components/FormField.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue"; import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"contact">>({ required: true }); 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> </script>
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<a-card title="عنوان بخش"> <a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" /> <SectionHeadFields v-model="data.head" />
<p class="text-sm text-gray-500">نشانی، تلفن، ایمیل و ساعات کاری از «تنظیمات سایت» خوانده می‌شوند.</p>
</a-card> </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"> <div class="grid gap-6 lg:grid-cols-2">
<a-card title="کادر پاسخ‌گویی اضطراری"> <a-card title="کادر پاسخ‌گویی اضطراری">
<FormField path="emergencyTitle" label="عنوان" required><a-input v-model:value="data.emergencyTitle" /></FormField> <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> <FormField path="formTitle" label="عنوان فرم" hint="برای شکستن خط Enter بزنید" required>
<a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" /> <a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" />
</FormField> </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="successTitle" label="عنوان پیام موفقیت" required><a-input v-model:value="data.successTitle" /></FormField>
<FormField path="successText" label="متن پیام موفقیت" required> <FormField path="successText" label="متن پیام موفقیت" required>
<a-textarea v-model:value="data.successText" :auto-size="{ minRows: 2 }" /> <a-textarea v-model:value="data.successText" :auto-size="{ minRows: 2 }" />
</FormField> </FormField>
<FormField path="resetLabel" label="متن دکمه بعد از موفقیت" required>
<a-input v-model:value="data.resetLabel" />
</FormField>
</a-card> </a-card>
</div> </div>
</div> </div>
+16 -1
View File
@@ -6,15 +6,30 @@ const data = defineModel<SectionData<"footer">>({ required: true });
</script> </script>
<template> <template>
<div class="space-y-6">
<a-card title="فوتر"> <a-card title="فوتر">
<FormField path="about" label="متن معرفی" required> <FormField path="about" label="متن معرفی" required>
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" /> <a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField> </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"> <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="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="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> <FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
</div> </div>
<p class="text-sm text-gray-500">لینک دسته‌ها و اطلاعات تماس فوتر به‌صورت خودکار از دسته‌بندی‌ها و تنظیمات سایت ساخته می‌شوند.</p> <p class="text-sm text-gray-500">
لینک دسته‌ها و اطلاعات تماس فوتر به‌صورت خودکار از دسته‌بندی‌ها و تنظیمات سایت ساخته می‌شوند.
</p>
</a-card> </a-card>
</div>
</template> </template>
+11 -21
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { type MediaDto, type SectionData, SOCIAL_TYPES, type SocialType, WEEK_DAYS } from "@imen/shared"; 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 FormField from "@/components/FormField.vue";
import ImageField from "@/components/ImageField.vue"; import ImageField from "@/components/ImageField.vue";
import ListEditor from "@/components/ListEditor.vue"; import ListEditor from "@/components/ListEditor.vue";
@@ -66,33 +67,22 @@ function pickOgImage(m: MediaDto) {
</a-card> </a-card>
<a-card title="اطلاعات تماس"> <a-card title="اطلاعات تماس">
<ContactInfoFields v-model="data" />
</a-card>
<a-card title="دکمه هدر">
<div class="grid gap-x-4 md:grid-cols-2"> <div class="grid gap-x-4 md:grid-cols-2">
<FormField path="phoneDisplay" label="تلفن (نمایشی)" required> <FormField path="headerCta.label" label="متن دکمه" required>
<a-input v-model:value="data.phoneDisplay" /> <a-input v-model:value="data.headerCta.label" />
</FormField> </FormField>
<FormField path="phone" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً ‎+98-21-91008800" required> <FormField path="headerCta.href" label="لینک" required>
<a-input v-model:value="data.phone" class="ltr-input" /> <a-input v-model:value="data.headerCta.href" class="ltr-input" placeholder="/#contact" />
</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> </FormField>
</div> </div>
</a-card> </a-card>
<a-card title="ساعات کاری"> <a-card title="ساعات کاری (برای گوگل)">
<FormField path="workingHours" label="متن نمایشی" required> <FormField path="openingHours.days" label="روزهای کاری">
<a-input v-model:value="data.workingHours" />
</FormField>
<FormField path="openingHours.days" label="روزهای کاری (برای گوگل)">
<a-checkbox-group <a-checkbox-group
v-model:value="data.openingHours.days" v-model:value="data.openingHours.days"
:options="DAY_OPTIONS" :options="DAY_OPTIONS"
+1 -1
View File
@@ -10,7 +10,7 @@ const data = defineModel<SectionData<"testimonials">>({ required: true });
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<a-card title="عنوان بخش"> <a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" :with-highlight="false" :with-desc="false" /> <SectionHeadFields v-model="data.head" />
</a-card> </a-card>
<a-card title="نظرات" :body-style="{ background: '#fafafa' }"> <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"> <ListEditor v-model="data.items" :create="() => ({ text: '', name: '', role: '' })" :max="12" add-label="افزودن نظر" :item-title="(q) => q.name">
+1 -4
View File
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
var __param = (this && this.__param) || function (paramIndex, decorator) { var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); } 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 { isSectionKey, SECTION_KEYS, sectionSchemas, toIssues, } from "@imen/shared";
import { AdminAuth, CurrentUser } from "../common/auth.js"; import { AdminAuth, CurrentUser } from "../common/auth.js";
import { AuditService, RevalidateService } from "../common/common.module.js"; import { AuditService, RevalidateService } from "../common/common.module.js";
@@ -44,9 +44,6 @@ let SectionsService = class SectionsService {
async update(key, body, user) { async update(key, body, user) {
if (!isSectionKey(key)) if (!isSectionKey(key))
throw new NotFoundException("بخش پیدا نشد"); throw new NotFoundException("بخش پیدا نشد");
if (key === "settings" && user.role !== "ADMIN") {
throw new ForbiddenException("فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد");
}
const parsed = sectionSchemas[key].safeParse(body); const parsed = sectionSchemas[key].safeParse(body);
if (!parsed.success) { if (!parsed.success) {
throw new BadRequestException({ throw new BadRequestException({
File diff suppressed because one or more lines are too long
+7
View File
@@ -12,6 +12,7 @@ export const SECTIONS = {
locality: "تهران", locality: "تهران",
country: "IR", country: "IR",
}, },
mapUrl: "",
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰", workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
openingHours: { openingHours: {
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"], days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
@@ -19,6 +20,7 @@ export const SECTIONS = {
closes: "17:00", closes: "17:00",
}, },
foundingDate: "2007", foundingDate: "2007",
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
socials: [ socials: [
{ type: "website", url: "/" }, { type: "website", url: "/" },
{ type: "email", url: "mailto:info@imenpishgaman.ir" }, { type: "email", url: "mailto:info@imenpishgaman.ir" },
@@ -246,6 +248,8 @@ export const SECTIONS = {
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم", formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم",
successTitle: "درخواست شما ثبت شد", successTitle: "درخواست شما ثبت شد",
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.", successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.",
submitLabel: "ارسال درخواست مشاوره",
resetLabel: "ثبت درخواست جدید",
}, },
footer: { footer: {
enabled: true, enabled: true,
@@ -253,6 +257,9 @@ export const SECTIONS = {
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.", copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER", certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷", emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
linksTitle: "دسترسی",
productsTitle: "محصولات",
contactTitle: "ارتباط",
}, },
}; };
export const CATEGORY_COVERS = { export const CATEGORY_COVERS = {
File diff suppressed because one or more lines are too long
-5
View File
@@ -2,7 +2,6 @@ import {
BadRequestException, BadRequestException,
Body, Body,
Controller, Controller,
ForbiddenException,
Get, Get,
Injectable, Injectable,
Module, Module,
@@ -55,10 +54,6 @@ export class SectionsService {
async update(key: string, body: unknown, user: AuthUser) { async update(key: string, body: unknown, user: AuthUser) {
if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد"); 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); const parsed = sectionSchemas[key].safeParse(body);
if (!parsed.success) { if (!parsed.success) {
throw new BadRequestException({ throw new BadRequestException({
+7
View File
@@ -19,6 +19,7 @@ export const SECTIONS: SiteSections = {
locality: "تهران", locality: "تهران",
country: "IR", country: "IR",
}, },
mapUrl: "",
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰", workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
openingHours: { openingHours: {
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"], days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
@@ -26,6 +27,7 @@ export const SECTIONS: SiteSections = {
closes: "17:00", closes: "17:00",
}, },
foundingDate: "2007", foundingDate: "2007",
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
socials: [ socials: [
{ type: "website", url: "/" }, { type: "website", url: "/" },
{ type: "email", url: "mailto:info@imenpishgaman.ir" }, { type: "email", url: "mailto:info@imenpishgaman.ir" },
@@ -265,6 +267,8 @@ export const SECTIONS: SiteSections = {
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم", formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم",
successTitle: "درخواست شما ثبت شد", successTitle: "درخواست شما ثبت شد",
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.", successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.",
submitLabel: "ارسال درخواست مشاوره",
resetLabel: "ثبت درخواست جدید",
}, },
footer: { footer: {
@@ -274,6 +278,9 @@ export const SECTIONS: SiteSections = {
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.", copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER", certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷", emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
linksTitle: "دسترسی",
productsTitle: "محصولات",
contactTitle: "ارتباط",
}, },
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"date": "2026-10-24T11:47:36.129Z", "date": "2026-10-24T13:11:46.671Z",
"preset": "node-server", "preset": "node-server",
"framework": { "framework": {
"name": "nuxt", "name": "nuxt",
@@ -1 +1 @@
{"id":"3e7fac48-d5e5-406c-8e09-0f6380ec2f4f","timestamp":1792842383527} {"id":"1fdc7d29-4179-4263-a065-71c6f514739a","timestamp":1792847411809}
@@ -0,0 +1 @@
{"id":"1fdc7d29-4179-4263-a065-71c6f514739a","timestamp":1792847411809,"prerendered":[]}
@@ -1 +0,0 @@
{"id":"3e7fac48-d5e5-406c-8e09-0f6380ec2f4f","timestamp":1792842383527,"prerendered":[]}
+91 -91
View File
@@ -4066,7 +4066,7 @@ function _expandFromEnv(value) {
const _inlineRuntimeConfig = { const _inlineRuntimeConfig = {
"app": { "app": {
"baseURL": "/", "baseURL": "/",
"buildId": "3e7fac48-d5e5-406c-8e09-0f6380ec2f4f", "buildId": "1fdc7d29-4179-4263-a065-71c6f514739a",
"buildAssetsDir": "/_nuxt/", "buildAssetsDir": "/_nuxt/",
"cdnURL": "" "cdnURL": ""
}, },
@@ -4813,6 +4813,13 @@ const assets = {
"size": 337, "size": 337,
"path": "../public/favicon.svg" "path": "../public/favicon.svg"
}, },
"/images/logo.png": {
"type": "image/png",
"etag": "\"45a79-DygL+P0onWMf3/jsNWp1BqEUPE4\"",
"mtime": "2026-10-11T12:10:17.609Z",
"size": 285305,
"path": "../public/images/logo.png"
},
"/fonts/IRANSansWebFaNum.woff2": { "/fonts/IRANSansWebFaNum.woff2": {
"type": "font/woff2", "type": "font/woff2",
"etag": "\"6d28-AufINCNdNZVNkWiYA6/Wp9W2wE4\"", "etag": "\"6d28-AufINCNdNZVNkWiYA6/Wp9W2wE4\"",
@@ -4834,13 +4841,6 @@ const assets = {
"size": 27316, "size": 27316,
"path": "../public/fonts/IRANSansWebFaNum_Bold.woff2" "path": "../public/fonts/IRANSansWebFaNum_Bold.woff2"
}, },
"/fonts/IRANSansWebFaNum_Medium.woff2": {
"type": "font/woff2",
"etag": "\"64b8-6V1kQDm0NUXner/ubiM2u7MPIyY\"",
"mtime": "2026-07-02T15:07:42.535Z",
"size": 25784,
"path": "../public/fonts/IRANSansWebFaNum_Medium.woff2"
},
"/fonts/IRANSansWebFaNum_Light.woff2": { "/fonts/IRANSansWebFaNum_Light.woff2": {
"type": "font/woff2", "type": "font/woff2",
"etag": "\"6ef0-0ZuVZfSKf1HbzDmvdDZdESRzFOs\"", "etag": "\"6ef0-0ZuVZfSKf1HbzDmvdDZdESRzFOs\"",
@@ -4848,40 +4848,12 @@ const assets = {
"size": 28400, "size": 28400,
"path": "../public/fonts/IRANSansWebFaNum_Light.woff2" "path": "../public/fonts/IRANSansWebFaNum_Light.woff2"
}, },
"/_nuxt/BHOIQrRl.js": { "/fonts/IRANSansWebFaNum_Medium.woff2": {
"type": "text/javascript; charset=utf-8", "type": "font/woff2",
"etag": "\"1637-w7apJM7DBwq91AEy0Dz89/JwU4s\"", "etag": "\"64b8-6V1kQDm0NUXner/ubiM2u7MPIyY\"",
"mtime": "2026-10-24T11:47:18.110Z", "mtime": "2026-07-02T15:07:42.535Z",
"size": 5687, "size": 25784,
"path": "../public/_nuxt/BHOIQrRl.js" "path": "../public/fonts/IRANSansWebFaNum_Medium.woff2"
},
"/_nuxt/DlAPF8TW.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"1e85-YNtOmEmkO1nMcpDM/ztRmg/rRLw\"",
"mtime": "2026-10-24T11:47:18.112Z",
"size": 7813,
"path": "../public/_nuxt/DlAPF8TW.js"
},
"/_nuxt/Dd1GE-gU.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"21420-NSMMtehs6gAIMs2sZ84NfSEv/wU\"",
"mtime": "2026-10-24T11:47:18.110Z",
"size": 136224,
"path": "../public/_nuxt/Dd1GE-gU.js"
},
"/_nuxt/D0e0LUpa.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"794-cNNGOqTLOp2+IDNDEbTWshwORp0\"",
"mtime": "2026-10-24T11:47:18.110Z",
"size": 1940,
"path": "../public/_nuxt/D0e0LUpa.js"
},
"/_nuxt/DOASWvjr.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"3827-3BvUfZznriOnNWFcpi6aALVMpOE\"",
"mtime": "2026-10-24T11:47:18.110Z",
"size": 14375,
"path": "../public/_nuxt/DOASWvjr.js"
}, },
"/fonts/IRANSansWebFaNum_UltraLight.woff2": { "/fonts/IRANSansWebFaNum_UltraLight.woff2": {
"type": "font/woff2", "type": "font/woff2",
@@ -4890,73 +4862,101 @@ const assets = {
"size": 28208, "size": 28208,
"path": "../public/fonts/IRANSansWebFaNum_UltraLight.woff2" "path": "../public/fonts/IRANSansWebFaNum_UltraLight.woff2"
}, },
"/_nuxt/DoONAYG5.js": { "/_nuxt/D0e0LUpa.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"3b62c-dc9OPIQbgdy0oXHYg6EoUgVy6yc\"", "etag": "\"794-cNNGOqTLOp2+IDNDEbTWshwORp0\"",
"mtime": "2026-10-24T11:47:18.112Z", "mtime": "2026-10-24T13:11:27.225Z",
"size": 243244, "size": 1940,
"path": "../public/_nuxt/DoONAYG5.js" "path": "../public/_nuxt/D0e0LUpa.js"
}, },
"/_nuxt/VuesaxIcon.4cYnpGo8.css": { "/_nuxt/BHOIQrRl.js": {
"type": "text/css; charset=utf-8",
"etag": "\"dc-Uz1+XA31y3oabrHslWpIJp4Ls74\"",
"mtime": "2026-10-24T11:47:18.112Z",
"size": 220,
"path": "../public/_nuxt/VuesaxIcon.4cYnpGo8.css"
},
"/_nuxt/DlDWjZRl.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"37855-Z6e8Pe/n6IqFkcSEj31zIJ7FhuQ\"", "etag": "\"1637-w7apJM7DBwq91AEy0Dz89/JwU4s\"",
"mtime": "2026-10-24T11:47:18.100Z", "mtime": "2026-10-24T13:11:27.225Z",
"size": 227413, "size": 5687,
"path": "../public/_nuxt/DlDWjZRl.js" "path": "../public/_nuxt/BHOIQrRl.js"
}, },
"/_nuxt/builds/latest.json": { "/_nuxt/Dd1GE-gU.js": {
"type": "application/json",
"etag": "\"47-JKgzcgu4mBBz7nSPUtzomjs30dc\"",
"mtime": "2026-10-24T11:47:23.008Z",
"size": 71,
"path": "../public/_nuxt/builds/latest.json"
},
"/_nuxt/builds/meta/3e7fac48-d5e5-406c-8e09-0f6380ec2f4f.json": {
"type": "application/json",
"etag": "\"58-JIbrCAB+PEB8Ybvxp3RzCo2D6XU\"",
"mtime": "2026-10-24T11:47:23.008Z",
"size": 88,
"path": "../public/_nuxt/builds/meta/3e7fac48-d5e5-406c-8e09-0f6380ec2f4f.json"
},
"/_nuxt/xGM79_uD.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"5023-nhybJtf5A4+tqp5rdRjhnkK+QMY\"", "etag": "\"21420-NSMMtehs6gAIMs2sZ84NfSEv/wU\"",
"mtime": "2026-10-24T11:47:18.112Z", "mtime": "2026-10-24T13:11:27.229Z",
"size": 20515, "size": 136224,
"path": "../public/_nuxt/xGM79_uD.js" "path": "../public/_nuxt/Dd1GE-gU.js"
}, },
"/_nuxt/r7-JdZhN.js": { "/_nuxt/DlAPF8TW.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"78c-4hmOEQGuRxrwCX/1v0DRIhjwpXE\"", "etag": "\"1e85-YNtOmEmkO1nMcpDM/ztRmg/rRLw\"",
"mtime": "2026-10-24T11:47:18.112Z", "mtime": "2026-10-24T13:11:27.230Z",
"size": 1932, "size": 7813,
"path": "../public/_nuxt/r7-JdZhN.js" "path": "../public/_nuxt/DlAPF8TW.js"
},
"/_nuxt/DOASWvjr.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"3827-3BvUfZznriOnNWFcpi6aALVMpOE\"",
"mtime": "2026-10-24T13:11:27.228Z",
"size": 14375,
"path": "../public/_nuxt/DOASWvjr.js"
}, },
"/_nuxt/entry.NJkjIEBJ.css": { "/_nuxt/entry.NJkjIEBJ.css": {
"type": "text/css; charset=utf-8", "type": "text/css; charset=utf-8",
"etag": "\"f954-gFKPvCM4WZHzb2tWqZEkEegmw5w\"", "etag": "\"f954-gFKPvCM4WZHzb2tWqZEkEegmw5w\"",
"mtime": "2026-10-24T11:47:18.112Z", "mtime": "2026-10-24T13:11:27.230Z",
"size": 63828, "size": 63828,
"path": "../public/_nuxt/entry.NJkjIEBJ.css" "path": "../public/_nuxt/entry.NJkjIEBJ.css"
}, },
"/images/logo.png": { "/_nuxt/r7-JdZhN.js": {
"type": "image/png", "type": "text/javascript; charset=utf-8",
"etag": "\"45a79-DygL+P0onWMf3/jsNWp1BqEUPE4\"", "etag": "\"78c-4hmOEQGuRxrwCX/1v0DRIhjwpXE\"",
"mtime": "2026-10-11T12:10:17.609Z", "mtime": "2026-10-24T13:11:27.230Z",
"size": 285305, "size": 1932,
"path": "../public/images/logo.png" "path": "../public/_nuxt/r7-JdZhN.js"
},
"/_nuxt/VuesaxIcon.4cYnpGo8.css": {
"type": "text/css; charset=utf-8",
"etag": "\"dc-Uz1+XA31y3oabrHslWpIJp4Ls74\"",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 220,
"path": "../public/_nuxt/VuesaxIcon.4cYnpGo8.css"
},
"/_nuxt/builds/latest.json": {
"type": "application/json",
"etag": "\"47-qofqu0SWsPJasrRW/9JkfJXfC/4\"",
"mtime": "2026-10-24T13:11:32.444Z",
"size": 71,
"path": "../public/_nuxt/builds/latest.json"
},
"/_nuxt/builds/meta/1fdc7d29-4179-4263-a065-71c6f514739a.json": {
"type": "application/json",
"etag": "\"58-AOn2PtlkYd83L1490p8deI3f16g\"",
"mtime": "2026-10-24T13:11:32.444Z",
"size": 88,
"path": "../public/_nuxt/builds/meta/1fdc7d29-4179-4263-a065-71c6f514739a.json"
},
"/_nuxt/DlDWjZRl.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"37855-Z6e8Pe/n6IqFkcSEj31zIJ7FhuQ\"",
"mtime": "2026-10-24T13:11:27.225Z",
"size": 227413,
"path": "../public/_nuxt/DlDWjZRl.js"
},
"/_nuxt/xGM79_uD.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"5023-nhybJtf5A4+tqp5rdRjhnkK+QMY\"",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 20515,
"path": "../public/_nuxt/xGM79_uD.js"
},
"/_nuxt/DoONAYG5.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"3b62c-dc9OPIQbgdy0oXHYg6EoUgVy6yc\"",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 243244,
"path": "../public/_nuxt/DoONAYG5.js"
}, },
"/_nuxt/D6Ym2dQP.js": { "/_nuxt/D6Ym2dQP.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1241f5-OG41EU7LABNWcdBigZe253LHLTw\"", "etag": "\"1241f5-OG41EU7LABNWcdBigZe253LHLTw\"",
"mtime": "2026-10-24T11:47:18.110Z", "mtime": "2026-10-24T13:11:27.228Z",
"size": 1196533, "size": 1196533,
"path": "../public/_nuxt/D6Ym2dQP.js" "path": "../public/_nuxt/D6Ym2dQP.js"
} }
+12 -3
View File
@@ -79,7 +79,7 @@ const SOCIALS = computed(() =>
<!-- quick links --> <!-- quick links -->
<div class="md:col-span-2"> <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"> <ul class="mt-5 space-y-3">
<li v-for="l in QUICK_LINKS" :key="l.href"> <li v-for="l in QUICK_LINKS" :key="l.href">
<a :href="l.href" class="text-sm text-muted transition-colors hover:text-ink"> <a :href="l.href" class="text-sm text-muted transition-colors hover:text-ink">
@@ -91,7 +91,7 @@ const SOCIALS = computed(() =>
<!-- products --> <!-- products -->
<div class="md:col-span-2"> <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"> <ul class="mt-5 space-y-3">
<li v-for="p in PRODUCT_LINKS" :key="p.href"> <li v-for="p in PRODUCT_LINKS" :key="p.href">
<NuxtLink :to="p.href" class="text-sm text-muted transition-colors hover:text-ink"> <NuxtLink :to="p.href" class="text-sm text-muted transition-colors hover:text-ink">
@@ -103,10 +103,19 @@ const SOCIALS = computed(() =>
<!-- contact --> <!-- contact -->
<div class="md:col-span-3"> <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"> <address class="mt-5 space-y-3 text-sm text-muted not-italic">
<p class="leading-7"> <p class="leading-7">
{{ settings.address.locality }}، {{ settings.address.street }} {{ 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>
<p class="font-mono tracking-wider" dir="ltr"> <p class="font-mono tracking-wider" dir="ltr">
<a :href="`tel:${settings.phone}`" class="transition-colors hover:text-ink">{{ settings.phoneDisplay }}</a> <a :href="`tel:${settings.phone}`" class="transition-colors hover:text-ink">{{ settings.phoneDisplay }}</a>
+4 -4
View File
@@ -72,15 +72,15 @@ onBeforeUnmount(() => {
<span class="size-1.5 animate-pulse rounded-full bg-emerald-400" /> <span class="size-1.5 animate-pulse rounded-full bg-emerald-400" />
<span class="font-mono">{{ time }}</span> <span class="font-mono">{{ time }}</span>
<span class="text-muted/50">|</span> <span class="text-muted/50">|</span>
<span>تهران — شیفت {{ shift }}</span> <span>{{ settings.address.locality }} — شیفت {{ shift }}</span>
</div> </div>
<a <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" 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" /> <VuesaxIcon name="call" class="size-4" />
درخواست مشاوره {{ settings.headerCta.label }}
</a> </a>
<button <button
@@ -139,7 +139,7 @@ onBeforeUnmount(() => {
<VuesaxIcon name="call" class="size-4 text-accent" /> <VuesaxIcon name="call" class="size-4 text-accent" />
<a :href="`tel:${settings.phone}`" class="font-mono" dir="ltr">{{ settings.phoneDisplay }}</a> <a :href="`tel:${settings.phone}`" class="font-mono" dir="ltr">{{ settings.phoneDisplay }}</a>
</div> </div>
<div class="text-xs text-muted">تهران — شیفت {{ shift }}، {{ time }}</div> <div class="text-xs text-muted">{{ settings.address.locality }} — شیفت {{ shift }}، {{ time }}</div>
</div> </div>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
+4 -4
View File
@@ -50,14 +50,14 @@ const step = computed(
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<span <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 <img
src="/images/logo.png" src="/images/logo.png"
alt="ایمن پیشگامان" alt="ایمن پیشگامان"
width="32" width="80"
height="32" height="80"
class="size-8 object-contain" class="size-full scale-110 object-contain object-center"
/> />
</span> </span>
<span class="font-mono text-xs tracking-[0.35em] text-muted"> <span class="font-mono text-xs tracking-[0.35em] text-muted">
+11 -3
View File
@@ -12,7 +12,13 @@ const content = computed(() => site.sections.contact);
const INFO = computed(() => { const INFO = computed(() => {
const s = site.settings; const s = site.settings;
return [ 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: "call", label: "تلفن تماس", value: s.phoneDisplay, ltr: true, href: `tel:${s.phone}` },
{ icon: "sms", label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` }, { icon: "sms", label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` },
{ icon: "clock", label: "ساعات کاری", value: s.workingHours }, { icon: "clock", label: "ساعات کاری", value: s.workingHours },
@@ -59,6 +65,8 @@ const contact = useContactStore();
<component <component
:is="item.href ? 'a' : 'div'" :is="item.href ? 'a' : 'div'"
:href="item.href" :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' : ''}`" :class="`mt-1 block text-sm font-medium ${item.ltr ? 'font-mono tracking-wider' : ''}`"
:dir="item.ltr ? 'ltr' : 'rtl'" :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" 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" @click="contact.reset"
> >
ثبت درخواست جدید {{ content.resetLabel }}
</button> </button>
</div> </div>
@@ -172,7 +180,7 @@ const contact = useContactStore();
</template> </template>
<template v-else> <template v-else>
<VuesaxIcon name="send-2" class="size-5" /> <VuesaxIcon name="send-2" class="size-5" />
ارسال درخواست مشاوره {{ content.submitLabel }}
</template> </template>
</AButton> </AButton>
</div> </div>
+9 -8
View File
@@ -1,23 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
const settings = computed(() => useSiteStore().settings);
</script> </script>
<template> <template>
<NuxtLink to="/" class="flex items-center gap-3" aria-label="ایمن پیشگامان"> <NuxtLink to="/" class="flex items-center gap-3" :aria-label="settings.name">
<span <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 <img
src="/images/logo.png" src="/images/logo.png"
alt="ایمن پیشگامان" :alt="settings.name"
width="28" width="48"
height="28" height="48"
class="size-7 object-contain" class="size-full object-contain object-center"
/> />
</span> </span>
<span class="leading-tight"> <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"> <span class="block font-mono text-[9px] tracking-[0.32em] text-muted">
IMEN PISHGAMAN CO. {{ settings.nameEn }}
</span> </span>
</span> </span>
</NuxtLink> </NuxtLink>
+18 -3
View File
@@ -6,6 +6,9 @@
# gateway (nginx) ─┬─ site domain → web (Nuxt SSR) /api → api /media → minio # gateway (nginx) ─┬─ site domain → web (Nuxt SSR) /api → api /media → minio
# └─ admin domain → admin (static) /api → api /media → minio # └─ admin domain → admin (static) /api → api /media → minio
# services that other containers reach by name get an imen-* alias: on a shared network (Dokploy) plain
# names like web or api would also resolve to other stacks' containers
#
# fixed project name, so this stack never shares containers or volumes with docker-compose.dev.yml # fixed project name, so this stack never shares containers or volumes with docker-compose.dev.yml
name: imen name: imen
@@ -27,6 +30,9 @@ services:
minio: minio:
image: docker.arvancloud.ir/minio/minio:latest image: docker.arvancloud.ir/minio/minio:latest
networks:
default:
aliases: [imen-minio]
restart: unless-stopped restart: unless-stopped
command: server /data command: server /data
environment: environment:
@@ -51,6 +57,9 @@ services:
api: api:
image: imen-api image: imen-api
networks:
default:
aliases: [imen-api]
build: build:
context: . context: .
dockerfile: docker/api.Dockerfile dockerfile: docker/api.Dockerfile
@@ -67,14 +76,14 @@ services:
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
COOKIE_SECURE: ${COOKIE_SECURE:-true} COOKIE_SECURE: ${COOKIE_SECURE:-true}
TRUST_PROXY: ${TRUST_PROXY:-2} TRUST_PROXY: ${TRUST_PROXY:-2}
S3_ENDPOINT: http://minio:9000 S3_ENDPOINT: http://imen-minio:9000
S3_BUCKET: ${S3_BUCKET:-imen-media} S3_BUCKET: ${S3_BUCKET:-imen-media}
S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_FORCE_PATH_STYLE: "true" S3_FORCE_PATH_STYLE: "true"
S3_ENSURE_BUCKET: "true" S3_ENSURE_BUCKET: "true"
MEDIA_PUBLIC_URL: /media MEDIA_PUBLIC_URL: /media
WEB_REVALIDATE_URL: http://web:3000/_internal/revalidate WEB_REVALIDATE_URL: http://imen-web:3000/_internal/revalidate
REVALIDATE_SECRET: ${REVALIDATE_SECRET} REVALIDATE_SECRET: ${REVALIDATE_SECRET}
ADMIN_PHONE: ${ADMIN_PHONE} ADMIN_PHONE: ${ADMIN_PHONE}
ADMIN_PASSWORD: ${ADMIN_PASSWORD} ADMIN_PASSWORD: ${ADMIN_PASSWORD}
@@ -82,6 +91,9 @@ services:
web: web:
image: imen-web image: imen-web
networks:
default:
aliases: [imen-web]
build: build:
context: . context: .
dockerfile: docker/web.Dockerfile dockerfile: docker/web.Dockerfile
@@ -89,12 +101,15 @@ services:
depends_on: depends_on:
- api - api
environment: environment:
NUXT_API_INTERNAL_URL: http://api:4000/api NUXT_API_INTERNAL_URL: http://imen-api:4000/api
NUXT_REVALIDATE_SECRET: ${REVALIDATE_SECRET} NUXT_REVALIDATE_SECRET: ${REVALIDATE_SECRET}
NUXT_PUBLIC_SITE_URL: https://${SITE_DOMAIN} NUXT_PUBLIC_SITE_URL: https://${SITE_DOMAIN}
admin: admin:
image: imen-admin image: imen-admin
networks:
default:
aliases: [imen-admin]
build: build:
context: . context: .
dockerfile: docker/admin.Dockerfile dockerfile: docker/admin.Dockerfile
+4 -4
View File
@@ -2,10 +2,10 @@
# (only ${SITE_DOMAIN}, ${ADMIN_DOMAIN} and ${S3_BUCKET} are replaced; nginx's own $vars stay). # (only ${SITE_DOMAIN}, ${ADMIN_DOMAIN} and ${S3_BUCKET} are replaced; nginx's own $vars stay).
# TLS is expected to be terminated in front of this container (host nginx / Traefik / CDN). # TLS is expected to be terminated in front of this container (host nginx / Traefik / CDN).
upstream web_upstream { server web:3000; } upstream web_upstream { server imen-web:3000; }
upstream api_upstream { server api:4000; } upstream api_upstream { server imen-api:4000; }
upstream admin_upstream { server admin:80; } upstream admin_upstream { server imen-admin:80; }
upstream media_upstream { server minio:9000; } upstream media_upstream { server imen-minio:9000; }
# keep the scheme reported by the TLS proxy in front, if any # keep the scheme reported by the TLS proxy in front, if any
map $http_x_forwarded_proto $forwarded_proto { map $http_x_forwarded_proto $forwarded_proto {
+20
View File
@@ -28,6 +28,7 @@ export declare const settingsSectionSchema: z.ZodObject<{
locality: z.ZodString; locality: z.ZodString;
country: z.ZodDefault<z.ZodString>; country: z.ZodDefault<z.ZodString>;
}, z.core.$strip>; }, z.core.$strip>;
mapUrl: z.ZodDefault<z.ZodString>;
workingHours: z.ZodString; workingHours: z.ZodString;
openingHours: z.ZodObject<{ openingHours: z.ZodObject<{
days: z.ZodArray<z.ZodEnum<{ days: z.ZodArray<z.ZodEnum<{
@@ -43,6 +44,10 @@ export declare const settingsSectionSchema: z.ZodObject<{
closes: z.ZodString; closes: z.ZodString;
}, z.core.$strip>; }, z.core.$strip>;
foundingDate: z.ZodString; foundingDate: z.ZodString;
headerCta: z.ZodDefault<z.ZodObject<{
label: z.ZodString;
href: z.ZodString;
}, z.core.$strip>>;
socials: z.ZodArray<z.ZodObject<{ socials: z.ZodArray<z.ZodObject<{
type: z.ZodEnum<{ type: z.ZodEnum<{
instagram: "instagram"; instagram: "instagram";
@@ -224,6 +229,8 @@ export declare const contactSectionSchema: z.ZodObject<{
formTitle: z.ZodString; formTitle: z.ZodString;
successTitle: z.ZodString; successTitle: z.ZodString;
successText: z.ZodString; successText: z.ZodString;
submitLabel: z.ZodDefault<z.ZodString>;
resetLabel: z.ZodDefault<z.ZodString>;
}, z.core.$strip>; }, z.core.$strip>;
export declare const footerSectionSchema: z.ZodObject<{ export declare const footerSectionSchema: z.ZodObject<{
enabled: z.ZodDefault<z.ZodBoolean>; enabled: z.ZodDefault<z.ZodBoolean>;
@@ -231,6 +238,9 @@ export declare const footerSectionSchema: z.ZodObject<{
copyright: z.ZodString; copyright: z.ZodString;
certLine: z.ZodDefault<z.ZodString>; certLine: z.ZodDefault<z.ZodString>;
emergencyNote: 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>; }, z.core.$strip>;
export declare const sectionSchemas: { export declare const sectionSchemas: {
readonly settings: z.ZodObject<{ readonly settings: z.ZodObject<{
@@ -246,6 +256,7 @@ export declare const sectionSchemas: {
locality: z.ZodString; locality: z.ZodString;
country: z.ZodDefault<z.ZodString>; country: z.ZodDefault<z.ZodString>;
}, z.core.$strip>; }, z.core.$strip>;
mapUrl: z.ZodDefault<z.ZodString>;
workingHours: z.ZodString; workingHours: z.ZodString;
openingHours: z.ZodObject<{ openingHours: z.ZodObject<{
days: z.ZodArray<z.ZodEnum<{ days: z.ZodArray<z.ZodEnum<{
@@ -261,6 +272,10 @@ export declare const sectionSchemas: {
closes: z.ZodString; closes: z.ZodString;
}, z.core.$strip>; }, z.core.$strip>;
foundingDate: z.ZodString; foundingDate: z.ZodString;
headerCta: z.ZodDefault<z.ZodObject<{
label: z.ZodString;
href: z.ZodString;
}, z.core.$strip>>;
socials: z.ZodArray<z.ZodObject<{ socials: z.ZodArray<z.ZodObject<{
type: z.ZodEnum<{ type: z.ZodEnum<{
instagram: "instagram"; instagram: "instagram";
@@ -442,6 +457,8 @@ export declare const sectionSchemas: {
formTitle: z.ZodString; formTitle: z.ZodString;
successTitle: z.ZodString; successTitle: z.ZodString;
successText: z.ZodString; successText: z.ZodString;
submitLabel: z.ZodDefault<z.ZodString>;
resetLabel: z.ZodDefault<z.ZodString>;
}, z.core.$strip>; }, z.core.$strip>;
readonly footer: z.ZodObject<{ readonly footer: z.ZodObject<{
enabled: z.ZodDefault<z.ZodBoolean>; enabled: z.ZodDefault<z.ZodBoolean>;
@@ -449,6 +466,9 @@ export declare const sectionSchemas: {
copyright: z.ZodString; copyright: z.ZodString;
certLine: z.ZodDefault<z.ZodString>; certLine: z.ZodDefault<z.ZodString>;
emergencyNote: 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>; }, z.core.$strip>;
}; };
export type SectionKey = keyof typeof sectionSchemas; export type SectionKey = keyof typeof sectionSchemas;
+1 -1
View File
@@ -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"}
+14 -2
View File
@@ -53,6 +53,8 @@ export const settingsSectionSchema = z.object({
locality: req(80), locality: req(80),
country: z.string().trim().length(2).default("IR"), 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. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */ /** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
workingHours: req(120), workingHours: req(120),
openingHours: z.object({ openingHours: z.object({
@@ -61,6 +63,8 @@ export const settingsSectionSchema = z.object({
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"), closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
}), }),
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"), 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), socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
ogImage: imageRefSchema.extend({ ogImage: imageRefSchema.extend({
width: z.number().int().positive().nullable().default(null), width: z.number().int().positive().nullable().default(null),
@@ -156,6 +160,10 @@ export const contactSectionSchema = z.object({
formTitle: req(160), formTitle: req(160),
successTitle: req(80), successTitle: req(80),
successText: req(300), 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({ export const footerSectionSchema = z.object({
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
@@ -163,6 +171,10 @@ export const footerSectionSchema = z.object({
copyright: req(160), copyright: req(160),
certLine: text(160), certLine: text(160),
emergencyNote: text(80), emergencyNote: text(80),
/** column headings under the brand block */
linksTitle: req(40).default("دسترسی"),
productsTitle: req(40).default("محصولات"),
contactTitle: req(40).default("ارتباط"),
}); });
export const sectionSchemas = { export const sectionSchemas = {
settings: settingsSectionSchema, settings: settingsSectionSchema,
@@ -192,7 +204,7 @@ export const SECTION_LABELS = {
standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" }, standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" },
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" }, industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" }, testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" },
contact: { title: "تماس با ما", desc: "عنوان‌ها و متن‌های فرم تماس" }, contact: { title: "تماس با ما", desc: "عنوان‌ها، متن‌های فرم، و ویرایش نشانی و اطلاعات تماس" },
footer: { title: "فوتر", desc: "متن معرفی، کپی‌رایت و توضیحات پایین صفحه" }, footer: { title: "فوتر", desc: "متن معرفی، عناوین ستون‌ها، کپی‌رایت و توضیحات پایین صفحه" },
}; };
//# sourceMappingURL=sections.js.map //# sourceMappingURL=sections.js.map
File diff suppressed because one or more lines are too long
+14 -2
View File
@@ -61,6 +61,8 @@ export const settingsSectionSchema = z.object({
locality: req(80), locality: req(80),
country: z.string().trim().length(2).default("IR"), 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. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */ /** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
workingHours: req(120), workingHours: req(120),
openingHours: z.object({ openingHours: z.object({
@@ -69,6 +71,8 @@ export const settingsSectionSchema = z.object({
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"), closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
}), }),
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"), 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), socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
ogImage: imageRefSchema.extend({ ogImage: imageRefSchema.extend({
width: z.number().int().positive().nullable().default(null), width: z.number().int().positive().nullable().default(null),
@@ -180,6 +184,10 @@ export const contactSectionSchema = z.object({
formTitle: req(160), formTitle: req(160),
successTitle: req(80), successTitle: req(80),
successText: req(300), 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({ export const footerSectionSchema = z.object({
@@ -188,6 +196,10 @@ export const footerSectionSchema = z.object({
copyright: req(160), copyright: req(160),
certLine: text(160), certLine: text(160),
emergencyNote: text(80), emergencyNote: text(80),
/** column headings under the brand block */
linksTitle: req(40).default("دسترسی"),
productsTitle: req(40).default("محصولات"),
contactTitle: req(40).default("ارتباط"),
}); });
export const sectionSchemas = { export const sectionSchemas = {
@@ -225,6 +237,6 @@ export const SECTION_LABELS: Record<SectionKey, { title: string; desc: string }>
standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" }, standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" },
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" }, industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" }, testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" },
contact: { title: "تماس با ما", desc: "عنوان‌ها و متن‌های فرم تماس" }, contact: { title: "تماس با ما", desc: "عنوان‌ها، متن‌های فرم، و ویرایش نشانی و اطلاعات تماس" },
footer: { title: "فوتر", desc: "متن معرفی، کپی‌رایت و توضیحات پایین صفحه" }, footer: { title: "فوتر", desc: "متن معرفی، عناوین ستون‌ها، کپی‌رایت و توضیحات پایین صفحه" },
}; };