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
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(`
`));s(t,e=>{n(r.value).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" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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">
</head>
<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">
<span
class="flex size-14 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
class="flex size-20 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
>
<img src="/logo.png" alt="ایمن پیشگامان" class="size-9 object-contain" />
<img src="/logo.png" alt="ایمن پیشگامان" class="size-full scale-110 object-contain object-center" />
</span>
<div class="leading-tight">
<div class="text-2xl font-black tracking-tight md:text-3xl">ایمن پیشگامان</div>
+58 -22
View File
@@ -1,5 +1,12 @@
<script setup lang="ts">
import { isSectionKey, SECTION_LABELS, type SectionKey, sectionSchemas } from "@imen/shared";
import {
isSectionKey,
SECTION_LABELS,
type SectionData,
type SectionKey,
sectionSchemas,
settingsSectionSchema,
} from "@imen/shared";
import { message } from "ant-design-vue";
import { type Component, computed, ref } from "vue";
import { useRoute } from "vue-router";
@@ -8,7 +15,6 @@ import VuesaxIcon from "@/components/VuesaxIcon.vue";
import type { SectionResponse } from "@/api/types";
import PageHeader from "@/components/PageHeader.vue";
import { useZodForm } from "@/composables/useZodForm";
import { useAuthStore } from "@/stores/auth";
import { jalali, SITE_URL } from "@/utils/format";
import AboutForm from "@/sections/AboutForm.vue";
import ContactForm from "@/sections/ContactForm.vue";
@@ -38,7 +44,6 @@ const FORMS: Record<SectionKey, Component> = {
footer: FooterForm,
};
const auth = useAuthStore();
const route = useRoute();
const key = String(route.params.key);
const valid = isSectionKey(key);
@@ -49,6 +54,14 @@ const { model, validate, handleError, reset } = useZodForm(
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
{} as never,
);
/** Contact page also edits site-wide contact info from settings */
const settingsModel = ref<SectionData<"settings"> | null>(null);
const contactModel = computed({
get: () => model.value as unknown as SectionData<"contact">,
set: (v: SectionData<"contact">) => {
model.value = v as never;
},
});
const loaded = ref(false);
const saving = ref(false);
const updatedAt = ref<string | null>(null);
@@ -57,12 +70,20 @@ const label = computed(() => (valid ? SECTION_LABELS[sectionKey] : null));
async function load() {
try {
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`);
const data = res.data as any;
const data = res.data as Record<string, unknown>;
if (sectionKey !== "settings" && data.enabled === undefined) {
data.enabled = true;
}
reset(data as never);
updatedAt.value = res.updatedAt;
if (sectionKey === "contact") {
const settingsRes = await api<SectionResponse>("/admin/sections/settings");
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
} else {
settingsModel.value = null;
}
loaded.value = true;
} catch (e) {
void message.error(errorMessage(e));
@@ -72,8 +93,25 @@ async function load() {
async function save() {
const data = validate();
if (!data) return;
if (sectionKey === "contact" && settingsModel.value) {
const settingsParsed = settingsSectionSchema.safeParse(settingsModel.value);
if (!settingsParsed.success) {
void message.error("اطلاعات تماس معتبر نیست — نشانی، تلفن یا ایمیل را بررسی کنید");
return;
}
}
saving.value = true;
try {
if (sectionKey === "contact" && settingsModel.value) {
const settingsRes = await api<SectionResponse>("/admin/sections/settings", {
method: "PUT",
body: settingsSectionSchema.parse(settingsModel.value),
});
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
}
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
reset(res.data as never);
updatedAt.value = res.updatedAt;
@@ -87,9 +125,9 @@ async function save() {
const togglingVisibility = ref(false);
async function onVisibilityChange(val: any) {
async function onVisibilityChange(val: unknown) {
const nextVal = Boolean(val);
(model.value as any).enabled = nextVal;
(model.value as { enabled?: boolean }).enabled = nextVal;
togglingVisibility.value = true;
try {
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
@@ -99,13 +137,13 @@ async function onVisibilityChange(val: any) {
body: { enabled: nextVal },
},
);
(model.value as any).enabled = res.enabled !== false;
(model.value as { enabled?: boolean }).enabled = res.enabled !== false;
updatedAt.value = res.updatedAt;
void message.success(
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
);
} catch (e) {
(model.value as any).enabled = !nextVal;
(model.value as { enabled?: boolean }).enabled = !nextVal;
void message.error(errorMessage(e));
} finally {
togglingVisibility.value = false;
@@ -126,7 +164,7 @@ if (valid) void load();
<div v-if="sectionKey !== 'settings'" class="max-lg:hidden flex items-center gap-2 me-2">
<span class="text-sm font-medium text-gray-700">نمایش در سایت:</span>
<a-switch
:checked="(model as any)?.enabled !== false"
:checked="(model as { enabled?: boolean })?.enabled !== false"
:loading="togglingVisibility"
checked-children="نمایش"
un-checked-children="مخفی"
@@ -138,7 +176,7 @@ if (valid) void load();
type="primary"
class="max-lg:hidden"
:loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
:disabled="!loaded"
@click="save"
>
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
@@ -147,21 +185,19 @@ if (valid) void load();
</PageHeader>
<a-spin :spinning="!loaded">
<a-alert
v-if="loaded && sectionKey === 'settings' && !auth.isAdmin"
type="error"
show-icon
message="فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد."
class="mb-4"
/>
<a-alert
v-else-if="loaded && sectionKey !== 'settings' && (model as any).enabled === false"
v-if="loaded && sectionKey !== 'settings' && (model as { enabled?: boolean }).enabled === false"
type="warning"
show-icon
message="این بخش در حال حاضر در سایت نمایش داده نمی‌شود (مخفی است)."
class="mb-4"
/>
<a-form v-if="loaded" layout="vertical" class="pb-20 lg:pb-0" @submit.prevent="save">
<component :is="FORMS[sectionKey]" v-model="model" />
<ContactForm
v-if="sectionKey === 'contact'"
v-model="contactModel"
v-model:settings="settingsModel"
/>
<component v-else :is="FORMS[sectionKey]" v-model="model" />
</a-form>
</a-spin>
@@ -169,13 +205,13 @@ if (valid) void load();
<div class="lg:hidden fixed bottom-0 inset-x-0 z-30 border-t border-gray-200 bg-white/95 px-4 py-2.5 backdrop-blur-md shadow-lg flex items-center justify-between gap-3">
<div v-if="sectionKey !== 'settings'" class="flex items-center gap-2">
<a-switch
:checked="(model as any)?.enabled !== false"
:checked="(model as { enabled?: boolean })?.enabled !== false"
:loading="togglingVisibility"
size="small"
@change="onVisibilityChange"
/>
<span class="text-xs font-medium text-gray-700">
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
{{ (model as { enabled?: boolean })?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
</span>
</div>
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
@@ -191,7 +227,7 @@ if (valid) void load();
size="middle"
class="!font-bold !px-5"
:loading="saving"
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
:disabled="!loaded"
@click="save"
>
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
+1 -4
View File
@@ -6,13 +6,10 @@ import { useRouter } from "vue-router";
import { api, errorMessage } from "@/api/http";
import type { AdminSectionSummary } from "@/api/types";
import PageHeader from "@/components/PageHeader.vue";
import { useAuthStore } from "@/stores/auth";
import { jalali } from "@/utils/format";
const router = useRouter();
const auth = useAuthStore();
// site-wide settings are editable by admins only
const keys = SECTION_KEYS.filter((k) => k !== "settings" || auth.isAdmin);
const keys = SECTION_KEYS;
const loading = ref(true);
const sectionEnabled = reactive<Record<string, boolean>>({
+4 -6
View File
@@ -103,7 +103,7 @@ onMounted(load);
</script>
<template>
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخش‌ها دسترسی دارد؛ ویرایشگر به جز کاربران و تنظیمات سایت">
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخش‌ها دسترسی دارد؛ ویرایشگر به جز مدیریت کاربران و حذف داده‌ها">
<a-button type="primary" @click="openNew">
<template #icon><VuesaxIcon name="add" :size="16" /></template>
کاربر جدید
@@ -241,7 +241,6 @@ onMounted(load);
</div>
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
<li>مدیریت کاربران، نقش‌ها و تنظیمات امنیتی</li>
<li>تنظیمات پایه سایت، سئو، شبکه‌ها و ساعات کاری</li>
<li>حذف داده‌ها (محصولات، دسته‌ها، پیام‌ها، رسانه‌ها)</li>
</ul>
</div>
@@ -253,7 +252,7 @@ onMounted(load);
</div>
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
<li>ایجاد، ویرایش و انتشار محصولات و دسته‌بندی‌ها</li>
<li>ویرایش محتوای بخش‌های صفحه اصلی</li>
<li>ویرایش بخش‌های سایت، نشانی، تماس، سئو و شبکه‌ها</li>
<li>آپلود تصاویر در رسانه و مشاهده پیام‌های تماس</li>
</ul>
</div>
@@ -269,7 +268,6 @@ onMounted(load);
</div>
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
<li>مدیریت کاربران، افزودن کاربر و تغییر نقش‌ها و وضعیت حساب</li>
<li>دسترسی کامل به تنظیمات پایه سایت، ساعات کاری، شبکه‌ها و سئو</li>
<li>مجوز حذف محصولات، دسته‌بندی‌ها، رسانه‌ها و پیام‌های تماس</li>
<li>مشاهده لاگ‌های امنیتی و رویدادهای کامل سامانه</li>
</ul>
@@ -282,7 +280,7 @@ onMounted(load);
</div>
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
<li>ایجاد، ویرایش، کپی و انتشار محصولات و گروه‌ها</li>
<li>ویرایش محتوای بخش‌های صفحه اصلی و فعال/مخفی‌سازی نمایش آنها</li>
<li>ویرایش بخش‌های سایت، نشانی، تماس، سئو، شبکه‌ها و نمایش/مخفی‌سازی</li>
<li>آپلود و ویرایش اطلاعات رسانه‌ها در کتابخانه رسانه</li>
<li>مشاهده پیام‌های تماس و به‌روزرسانی وضعیت (بدون دسترسی حذف یا مدیریت کاربران)</li>
</ul>
@@ -319,7 +317,7 @@ onMounted(load);
:options="ROLE_OPTIONS"
/>
<div class="mt-1.5 text-[11px] text-gray-500">
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف داده‌ها، تنظیمات و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخش‌ها و پیام‌ها (بدون حذف)' }}
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف داده‌ها و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخش‌های سایت (از جمله نشانی و تماس)، پیام‌ها (بدون حذف)' }}
</div>
</FormField>
<FormField v-if="!isNew" path="isActive">
+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="space-y-6 xl:col-span-2">
<a-card title="عنوان و متن">
<SectionHeadFields v-model="data.head" :with-desc="false" />
<SectionHeadFields v-model="data.head" />
<FormField path="body" label="متن درباره ما" required>
<a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" />
</FormField>
+21 -1
View File
@@ -1,17 +1,31 @@
<script setup lang="ts">
import type { SectionData } from "@imen/shared";
import ContactInfoFields from "@/components/ContactInfoFields.vue";
import FormField from "@/components/FormField.vue";
import SectionHeadFields from "@/components/SectionHeadFields.vue";
const data = defineModel<SectionData<"contact">>({ required: true });
/** Site-wide contact info (settings); saved together with this section. */
const settings = defineModel<SectionData<"settings"> | null>("settings", { default: null });
function onSettingsUpdate(value: SectionData<"settings">) {
settings.value = value;
}
</script>
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" />
<p class="text-sm text-gray-500">نشانی، تلفن، ایمیل و ساعات کاری از «تنظیمات سایت» خوانده می‌شوند.</p>
</a-card>
<a-card v-if="settings" title="نشانی و اطلاعات تماس">
<p class="mb-4 text-sm text-gray-500">
این مقادیر در هدر، فوتر، بخش تماس و سئوی سایت مشترک‌اند و با ذخیرهٔ همین صفحه به‌روز می‌شوند.
</p>
<ContactInfoFields :model-value="settings" @update:model-value="onSettingsUpdate" />
</a-card>
<div class="grid gap-6 lg:grid-cols-2">
<a-card title="کادر پاسخ‌گویی اضطراری">
<FormField path="emergencyTitle" label="عنوان" required><a-input v-model:value="data.emergencyTitle" /></FormField>
@@ -24,10 +38,16 @@ const data = defineModel<SectionData<"contact">>({ required: true });
<FormField path="formTitle" label="عنوان فرم" hint="برای شکستن خط Enter بزنید" required>
<a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" />
</FormField>
<FormField path="submitLabel" label="متن دکمه ارسال" required>
<a-input v-model:value="data.submitLabel" />
</FormField>
<FormField path="successTitle" label="عنوان پیام موفقیت" required><a-input v-model:value="data.successTitle" /></FormField>
<FormField path="successText" label="متن پیام موفقیت" required>
<a-textarea v-model:value="data.successText" :auto-size="{ minRows: 2 }" />
</FormField>
<FormField path="resetLabel" label="متن دکمه بعد از موفقیت" required>
<a-input v-model:value="data.resetLabel" />
</FormField>
</a-card>
</div>
</div>
+26 -11
View File
@@ -6,15 +6,30 @@ const data = defineModel<SectionData<"footer">>({ required: true });
</script>
<template>
<a-card title="فوتر">
<FormField path="about" label="متن معرفی" required>
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<div class="grid gap-x-4 md:grid-cols-3">
<FormField path="copyright" label="متن کپی‌رایت" required><a-input v-model:value="data.copyright" /></FormField>
<FormField path="certLine" label="خط گواهی‌نامه‌ها"><a-input v-model:value="data.certLine" class="ltr-input" /></FormField>
<FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
</div>
<p class="text-sm text-gray-500">لینک دسته‌ها و اطلاعات تماس فوتر به‌صورت خودکار از دسته‌بندی‌ها و تنظیمات سایت ساخته می‌شوند.</p>
</a-card>
<div class="space-y-6">
<a-card title="فوتر">
<FormField path="about" label="متن معرفی" required>
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormField>
<div class="grid gap-x-4 md:grid-cols-3">
<FormField path="linksTitle" label="عنوان ستون لینک‌ها" required>
<a-input v-model:value="data.linksTitle" />
</FormField>
<FormField path="productsTitle" label="عنوان ستون محصولات" required>
<a-input v-model:value="data.productsTitle" />
</FormField>
<FormField path="contactTitle" label="عنوان ستون ارتباط" required>
<a-input v-model:value="data.contactTitle" />
</FormField>
</div>
<div class="grid gap-x-4 md:grid-cols-3">
<FormField path="copyright" label="متن کپی‌رایت" required><a-input v-model:value="data.copyright" /></FormField>
<FormField path="certLine" label="خط گواهی‌نامه‌ها"><a-input v-model:value="data.certLine" class="ltr-input" /></FormField>
<FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
</div>
<p class="text-sm text-gray-500">
لینک دسته‌ها و اطلاعات تماس فوتر به‌صورت خودکار از دسته‌بندی‌ها و تنظیمات سایت ساخته می‌شوند.
</p>
</a-card>
</div>
</template>
+11 -21
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { type MediaDto, type SectionData, SOCIAL_TYPES, type SocialType, WEEK_DAYS } from "@imen/shared";
import ContactInfoFields from "@/components/ContactInfoFields.vue";
import FormField from "@/components/FormField.vue";
import ImageField from "@/components/ImageField.vue";
import ListEditor from "@/components/ListEditor.vue";
@@ -66,33 +67,22 @@ function pickOgImage(m: MediaDto) {
</a-card>
<a-card title="اطلاعات تماس">
<ContactInfoFields v-model="data" />
</a-card>
<a-card title="دکمه هدر">
<div class="grid gap-x-4 md:grid-cols-2">
<FormField path="phoneDisplay" label="تلفن (نمایشی)" required>
<a-input v-model:value="data.phoneDisplay" />
<FormField path="headerCta.label" label="متن دکمه" required>
<a-input v-model:value="data.headerCta.label" />
</FormField>
<FormField path="phone" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً ‎+98-21-91008800" required>
<a-input v-model:value="data.phone" class="ltr-input" />
</FormField>
<FormField path="email" label="ایمیل" required class="md:col-span-2">
<a-input v-model:value="data.email" class="ltr-input" />
</FormField>
<FormField path="address.locality" label="شهر" required>
<a-input v-model:value="data.address.locality" />
</FormField>
<FormField path="address.country" label="کد کشور" required>
<a-input v-model:value="data.address.country" class="ltr-input" :maxlength="2" />
</FormField>
<FormField path="address.street" label="نشانی" required class="md:col-span-2">
<a-input v-model:value="data.address.street" />
<FormField path="headerCta.href" label="لینک" required>
<a-input v-model:value="data.headerCta.href" class="ltr-input" placeholder="/#contact" />
</FormField>
</div>
</a-card>
<a-card title="ساعات کاری">
<FormField path="workingHours" label="متن نمایشی" required>
<a-input v-model:value="data.workingHours" />
</FormField>
<FormField path="openingHours.days" label="روزهای کاری (برای گوگل)">
<a-card title="ساعات کاری (برای گوگل)">
<FormField path="openingHours.days" label="روزهای کاری">
<a-checkbox-group
v-model:value="data.openingHours.days"
:options="DAY_OPTIONS"
+1 -1
View File
@@ -10,7 +10,7 @@ const data = defineModel<SectionData<"testimonials">>({ required: true });
<template>
<div class="space-y-6">
<a-card title="عنوان بخش">
<SectionHeadFields v-model="data.head" :with-highlight="false" :with-desc="false" />
<SectionHeadFields v-model="data.head" />
</a-card>
<a-card title="نظرات" :body-style="{ background: '#fafafa' }">
<ListEditor v-model="data.items" :create="() => ({ text: '', name: '', role: '' })" :max="12" add-label="افزودن نظر" :item-title="(q) => q.name">
+1 -4
View File
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { BadRequestException, Body, Controller, ForbiddenException, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common";
import { BadRequestException, Body, Controller, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common";
import { isSectionKey, SECTION_KEYS, sectionSchemas, toIssues, } from "@imen/shared";
import { AdminAuth, CurrentUser } from "../common/auth.js";
import { AuditService, RevalidateService } from "../common/common.module.js";
@@ -44,9 +44,6 @@ let SectionsService = class SectionsService {
async update(key, body, user) {
if (!isSectionKey(key))
throw new NotFoundException("بخش پیدا نشد");
if (key === "settings" && user.role !== "ADMIN") {
throw new ForbiddenException("فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد");
}
const parsed = sectionSchemas[key].safeParse(body);
if (!parsed.success) {
throw new BadRequestException({
File diff suppressed because one or more lines are too long
+7
View File
@@ -12,6 +12,7 @@ export const SECTIONS = {
locality: "تهران",
country: "IR",
},
mapUrl: "",
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
openingHours: {
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
@@ -19,6 +20,7 @@ export const SECTIONS = {
closes: "17:00",
},
foundingDate: "2007",
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
socials: [
{ type: "website", url: "/" },
{ type: "email", url: "mailto:info@imenpishgaman.ir" },
@@ -246,6 +248,8 @@ export const SECTIONS = {
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم",
successTitle: "درخواست شما ثبت شد",
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.",
submitLabel: "ارسال درخواست مشاوره",
resetLabel: "ثبت درخواست جدید",
},
footer: {
enabled: true,
@@ -253,6 +257,9 @@ export const SECTIONS = {
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
linksTitle: "دسترسی",
productsTitle: "محصولات",
contactTitle: "ارتباط",
},
};
export const CATEGORY_COVERS = {
File diff suppressed because one or more lines are too long
-5
View File
@@ -2,7 +2,6 @@ import {
BadRequestException,
Body,
Controller,
ForbiddenException,
Get,
Injectable,
Module,
@@ -55,10 +54,6 @@ export class SectionsService {
async update(key: string, body: unknown, user: AuthUser) {
if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد");
// site-wide settings (contact info, SEO defaults) are limited to admins
if (key === "settings" && user.role !== "ADMIN") {
throw new ForbiddenException("فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد");
}
const parsed = sectionSchemas[key].safeParse(body);
if (!parsed.success) {
throw new BadRequestException({
+7
View File
@@ -19,6 +19,7 @@ export const SECTIONS: SiteSections = {
locality: "تهران",
country: "IR",
},
mapUrl: "",
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
openingHours: {
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
@@ -26,6 +27,7 @@ export const SECTIONS: SiteSections = {
closes: "17:00",
},
foundingDate: "2007",
headerCta: { label: "درخواست مشاوره", href: "/#contact" },
socials: [
{ type: "website", url: "/" },
{ type: "email", url: "mailto:info@imenpishgaman.ir" },
@@ -265,6 +267,8 @@ export const SECTIONS: SiteSections = {
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم",
successTitle: "درخواست شما ثبت شد",
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.",
submitLabel: "ارسال درخواست مشاوره",
resetLabel: "ثبت درخواست جدید",
},
footer: {
@@ -274,6 +278,9 @@ export const SECTIONS: SiteSections = {
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
linksTitle: "دسترسی",
productsTitle: "محصولات",
contactTitle: "ارتباط",
},
};
+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",
"framework": {
"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 = {
"app": {
"baseURL": "/",
"buildId": "3e7fac48-d5e5-406c-8e09-0f6380ec2f4f",
"buildId": "1fdc7d29-4179-4263-a065-71c6f514739a",
"buildAssetsDir": "/_nuxt/",
"cdnURL": ""
},
@@ -4813,6 +4813,13 @@ const assets = {
"size": 337,
"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": {
"type": "font/woff2",
"etag": "\"6d28-AufINCNdNZVNkWiYA6/Wp9W2wE4\"",
@@ -4834,13 +4841,6 @@ const assets = {
"size": 27316,
"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": {
"type": "font/woff2",
"etag": "\"6ef0-0ZuVZfSKf1HbzDmvdDZdESRzFOs\"",
@@ -4848,40 +4848,12 @@ const assets = {
"size": 28400,
"path": "../public/fonts/IRANSansWebFaNum_Light.woff2"
},
"/_nuxt/BHOIQrRl.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"1637-w7apJM7DBwq91AEy0Dz89/JwU4s\"",
"mtime": "2026-10-24T11:47:18.110Z",
"size": 5687,
"path": "../public/_nuxt/BHOIQrRl.js"
},
"/_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_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_UltraLight.woff2": {
"type": "font/woff2",
@@ -4890,73 +4862,101 @@ const assets = {
"size": 28208,
"path": "../public/fonts/IRANSansWebFaNum_UltraLight.woff2"
},
"/_nuxt/DoONAYG5.js": {
"/_nuxt/D0e0LUpa.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"3b62c-dc9OPIQbgdy0oXHYg6EoUgVy6yc\"",
"mtime": "2026-10-24T11:47:18.112Z",
"size": 243244,
"path": "../public/_nuxt/DoONAYG5.js"
"etag": "\"794-cNNGOqTLOp2+IDNDEbTWshwORp0\"",
"mtime": "2026-10-24T13:11:27.225Z",
"size": 1940,
"path": "../public/_nuxt/D0e0LUpa.js"
},
"/_nuxt/VuesaxIcon.4cYnpGo8.css": {
"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": {
"/_nuxt/BHOIQrRl.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"37855-Z6e8Pe/n6IqFkcSEj31zIJ7FhuQ\"",
"mtime": "2026-10-24T11:47:18.100Z",
"size": 227413,
"path": "../public/_nuxt/DlDWjZRl.js"
"etag": "\"1637-w7apJM7DBwq91AEy0Dz89/JwU4s\"",
"mtime": "2026-10-24T13:11:27.225Z",
"size": 5687,
"path": "../public/_nuxt/BHOIQrRl.js"
},
"/_nuxt/builds/latest.json": {
"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": {
"/_nuxt/Dd1GE-gU.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"5023-nhybJtf5A4+tqp5rdRjhnkK+QMY\"",
"mtime": "2026-10-24T11:47:18.112Z",
"size": 20515,
"path": "../public/_nuxt/xGM79_uD.js"
"etag": "\"21420-NSMMtehs6gAIMs2sZ84NfSEv/wU\"",
"mtime": "2026-10-24T13:11:27.229Z",
"size": 136224,
"path": "../public/_nuxt/Dd1GE-gU.js"
},
"/_nuxt/r7-JdZhN.js": {
"/_nuxt/DlAPF8TW.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"78c-4hmOEQGuRxrwCX/1v0DRIhjwpXE\"",
"mtime": "2026-10-24T11:47:18.112Z",
"size": 1932,
"path": "../public/_nuxt/r7-JdZhN.js"
"etag": "\"1e85-YNtOmEmkO1nMcpDM/ztRmg/rRLw\"",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 7813,
"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": {
"type": "text/css; charset=utf-8",
"etag": "\"f954-gFKPvCM4WZHzb2tWqZEkEegmw5w\"",
"mtime": "2026-10-24T11:47:18.112Z",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 63828,
"path": "../public/_nuxt/entry.NJkjIEBJ.css"
},
"/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"
"/_nuxt/r7-JdZhN.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"78c-4hmOEQGuRxrwCX/1v0DRIhjwpXE\"",
"mtime": "2026-10-24T13:11:27.230Z",
"size": 1932,
"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": {
"type": "text/javascript; charset=utf-8",
"etag": "\"1241f5-OG41EU7LABNWcdBigZe253LHLTw\"",
"mtime": "2026-10-24T11:47:18.110Z",
"mtime": "2026-10-24T13:11:27.228Z",
"size": 1196533,
"path": "../public/_nuxt/D6Ym2dQP.js"
}
+12 -3
View File
@@ -79,7 +79,7 @@ const SOCIALS = computed(() =>
<!-- quick links -->
<div class="md:col-span-2">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// دسترسی</div>
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.linksTitle }}</div>
<ul class="mt-5 space-y-3">
<li v-for="l in QUICK_LINKS" :key="l.href">
<a :href="l.href" class="text-sm text-muted transition-colors hover:text-ink">
@@ -91,7 +91,7 @@ const SOCIALS = computed(() =>
<!-- products -->
<div class="md:col-span-2">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// محصولات</div>
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.productsTitle }}</div>
<ul class="mt-5 space-y-3">
<li v-for="p in PRODUCT_LINKS" :key="p.href">
<NuxtLink :to="p.href" class="text-sm text-muted transition-colors hover:text-ink">
@@ -103,10 +103,19 @@ const SOCIALS = computed(() =>
<!-- contact -->
<div class="md:col-span-3">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// ارتباط</div>
<div class="font-mono text-xs tracking-[0.25em] text-accent">// {{ footer.contactTitle }}</div>
<address class="mt-5 space-y-3 text-sm text-muted not-italic">
<p class="leading-7">
{{ settings.address.locality }}، {{ settings.address.street }}
<a
v-if="settings.mapUrl"
:href="settings.mapUrl"
target="_blank"
rel="noopener"
class="ms-2 text-accent transition-colors hover:text-ink"
>
نقشه
</a>
</p>
<p class="font-mono tracking-wider" dir="ltr">
<a :href="`tel:${settings.phone}`" class="transition-colors hover:text-ink">{{ settings.phoneDisplay }}</a>
+4 -4
View File
@@ -72,15 +72,15 @@ onBeforeUnmount(() => {
<span class="size-1.5 animate-pulse rounded-full bg-emerald-400" />
<span class="font-mono">{{ time }}</span>
<span class="text-muted/50">|</span>
<span>تهران — شیفت {{ shift }}</span>
<span>{{ settings.address.locality }} — شیفت {{ shift }}</span>
</div>
<a
href="/#contact"
:href="settings.headerCta.href"
class="hidden items-center gap-2 rounded-full bg-accent px-5 py-2.5 text-sm font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_0_30px_rgba(255,90,0,0.4)] md:flex"
>
<VuesaxIcon name="call" class="size-4" />
درخواست مشاوره
{{ settings.headerCta.label }}
</a>
<button
@@ -139,7 +139,7 @@ onBeforeUnmount(() => {
<VuesaxIcon name="call" class="size-4 text-accent" />
<a :href="`tel:${settings.phone}`" class="font-mono" dir="ltr">{{ settings.phoneDisplay }}</a>
</div>
<div class="text-xs text-muted">تهران — شیفت {{ shift }}، {{ time }}</div>
<div class="text-xs text-muted">{{ settings.address.locality }} — شیفت {{ shift }}، {{ time }}</div>
</div>
</motion.div>
</AnimatePresence>
+4 -4
View File
@@ -50,14 +50,14 @@ const step = computed(
<div class="flex items-center gap-4">
<span
class="flex size-12 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(255,90,0,0.3)] ring-1 ring-white/10"
class="flex size-20 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-[0_0_28px_rgba(255,90,0,0.3)] ring-1 ring-white/10"
>
<img
src="/images/logo.png"
alt="ایمن پیشگامان"
width="32"
height="32"
class="size-8 object-contain"
width="80"
height="80"
class="size-full scale-110 object-contain object-center"
/>
</span>
<span class="font-mono text-xs tracking-[0.35em] text-muted">
+11 -3
View File
@@ -12,7 +12,13 @@ const content = computed(() => site.sections.contact);
const INFO = computed(() => {
const s = site.settings;
return [
{ icon: "location", label: "نشانی", value: `${s.address.locality}، ${s.address.street}` },
{
icon: "location",
label: "نشانی",
value: `${s.address.locality}، ${s.address.street}`,
href: s.mapUrl || undefined,
external: Boolean(s.mapUrl),
},
{ icon: "call", label: "تلفن تماس", value: s.phoneDisplay, ltr: true, href: `tel:${s.phone}` },
{ icon: "sms", label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` },
{ icon: "clock", label: "ساعات کاری", value: s.workingHours },
@@ -59,6 +65,8 @@ const contact = useContactStore();
<component
:is="item.href ? 'a' : 'div'"
:href="item.href"
:target="item.external ? '_blank' : undefined"
:rel="item.external ? 'noopener' : undefined"
:class="`mt-1 block text-sm font-medium ${item.ltr ? 'font-mono tracking-wider' : ''}`"
:dir="item.ltr ? 'ltr' : 'rtl'"
>
@@ -106,7 +114,7 @@ const contact = useContactStore();
class="mt-7 rounded-full border border-line px-7 py-3 text-sm transition-colors hover:border-accent hover:text-accent"
@click="contact.reset"
>
ثبت درخواست جدید
{{ content.resetLabel }}
</button>
</div>
@@ -172,7 +180,7 @@ const contact = useContactStore();
</template>
<template v-else>
<VuesaxIcon name="send-2" class="size-5" />
ارسال درخواست مشاوره
{{ content.submitLabel }}
</template>
</AButton>
</div>
+9 -8
View File
@@ -1,23 +1,24 @@
<script setup lang="ts">
const settings = computed(() => useSiteStore().settings);
</script>
<template>
<NuxtLink to="/" class="flex items-center gap-3" aria-label="ایمن پیشگامان">
<NuxtLink to="/" class="flex items-center gap-3" :aria-label="settings.name">
<span
class="flex size-10 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
class="flex size-11 items-center justify-center overflow-hidden rounded-xl bg-white p-0.5 shadow-[0_0_28px_rgba(10,30,92,0.45)] sm:size-12"
>
<img
src="/images/logo.png"
alt="ایمن پیشگامان"
width="28"
height="28"
class="size-7 object-contain"
:alt="settings.name"
width="48"
height="48"
class="size-full object-contain object-center"
/>
</span>
<span class="leading-tight">
<span class="block text-lg font-black">ایمن پیشگامان</span>
<span class="block text-lg font-black">{{ settings.name }}</span>
<span class="block font-mono text-[9px] tracking-[0.32em] text-muted">
IMEN PISHGAMAN CO.
{{ settings.nameEn }}
</span>
</span>
</NuxtLink>
+18 -3
View File
@@ -6,6 +6,9 @@
# gateway (nginx) ─┬─ site domain → web (Nuxt SSR) /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
name: imen
@@ -27,6 +30,9 @@ services:
minio:
image: docker.arvancloud.ir/minio/minio:latest
networks:
default:
aliases: [imen-minio]
restart: unless-stopped
command: server /data
environment:
@@ -51,6 +57,9 @@ services:
api:
image: imen-api
networks:
default:
aliases: [imen-api]
build:
context: .
dockerfile: docker/api.Dockerfile
@@ -67,14 +76,14 @@ services:
JWT_SECRET: ${JWT_SECRET}
COOKIE_SECURE: ${COOKIE_SECURE:-true}
TRUST_PROXY: ${TRUST_PROXY:-2}
S3_ENDPOINT: http://minio:9000
S3_ENDPOINT: http://imen-minio:9000
S3_BUCKET: ${S3_BUCKET:-imen-media}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_FORCE_PATH_STYLE: "true"
S3_ENSURE_BUCKET: "true"
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}
ADMIN_PHONE: ${ADMIN_PHONE}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
@@ -82,6 +91,9 @@ services:
web:
image: imen-web
networks:
default:
aliases: [imen-web]
build:
context: .
dockerfile: docker/web.Dockerfile
@@ -89,12 +101,15 @@ services:
depends_on:
- api
environment:
NUXT_API_INTERNAL_URL: http://api:4000/api
NUXT_API_INTERNAL_URL: http://imen-api:4000/api
NUXT_REVALIDATE_SECRET: ${REVALIDATE_SECRET}
NUXT_PUBLIC_SITE_URL: https://${SITE_DOMAIN}
admin:
image: imen-admin
networks:
default:
aliases: [imen-admin]
build:
context: .
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).
# TLS is expected to be terminated in front of this container (host nginx / Traefik / CDN).
upstream web_upstream { server web:3000; }
upstream api_upstream { server api:4000; }
upstream admin_upstream { server admin:80; }
upstream media_upstream { server minio:9000; }
upstream web_upstream { server imen-web:3000; }
upstream api_upstream { server imen-api:4000; }
upstream admin_upstream { server imen-admin:80; }
upstream media_upstream { server imen-minio:9000; }
# keep the scheme reported by the TLS proxy in front, if any
map $http_x_forwarded_proto $forwarded_proto {
+20
View File
@@ -28,6 +28,7 @@ export declare const settingsSectionSchema: z.ZodObject<{
locality: z.ZodString;
country: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
mapUrl: z.ZodDefault<z.ZodString>;
workingHours: z.ZodString;
openingHours: z.ZodObject<{
days: z.ZodArray<z.ZodEnum<{
@@ -43,6 +44,10 @@ export declare const settingsSectionSchema: z.ZodObject<{
closes: z.ZodString;
}, z.core.$strip>;
foundingDate: z.ZodString;
headerCta: z.ZodDefault<z.ZodObject<{
label: z.ZodString;
href: z.ZodString;
}, z.core.$strip>>;
socials: z.ZodArray<z.ZodObject<{
type: z.ZodEnum<{
instagram: "instagram";
@@ -224,6 +229,8 @@ export declare const contactSectionSchema: z.ZodObject<{
formTitle: z.ZodString;
successTitle: z.ZodString;
successText: z.ZodString;
submitLabel: z.ZodDefault<z.ZodString>;
resetLabel: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
export declare const footerSectionSchema: z.ZodObject<{
enabled: z.ZodDefault<z.ZodBoolean>;
@@ -231,6 +238,9 @@ export declare const footerSectionSchema: z.ZodObject<{
copyright: z.ZodString;
certLine: z.ZodDefault<z.ZodString>;
emergencyNote: z.ZodDefault<z.ZodString>;
linksTitle: z.ZodDefault<z.ZodString>;
productsTitle: z.ZodDefault<z.ZodString>;
contactTitle: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
export declare const sectionSchemas: {
readonly settings: z.ZodObject<{
@@ -246,6 +256,7 @@ export declare const sectionSchemas: {
locality: z.ZodString;
country: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
mapUrl: z.ZodDefault<z.ZodString>;
workingHours: z.ZodString;
openingHours: z.ZodObject<{
days: z.ZodArray<z.ZodEnum<{
@@ -261,6 +272,10 @@ export declare const sectionSchemas: {
closes: z.ZodString;
}, z.core.$strip>;
foundingDate: z.ZodString;
headerCta: z.ZodDefault<z.ZodObject<{
label: z.ZodString;
href: z.ZodString;
}, z.core.$strip>>;
socials: z.ZodArray<z.ZodObject<{
type: z.ZodEnum<{
instagram: "instagram";
@@ -442,6 +457,8 @@ export declare const sectionSchemas: {
formTitle: z.ZodString;
successTitle: z.ZodString;
successText: z.ZodString;
submitLabel: z.ZodDefault<z.ZodString>;
resetLabel: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
readonly footer: z.ZodObject<{
enabled: z.ZodDefault<z.ZodBoolean>;
@@ -449,6 +466,9 @@ export declare const sectionSchemas: {
copyright: z.ZodString;
certLine: z.ZodDefault<z.ZodString>;
emergencyNote: z.ZodDefault<z.ZodString>;
linksTitle: z.ZodDefault<z.ZodString>;
productsTitle: z.ZodDefault<z.ZodString>;
contactTitle: z.ZodDefault<z.ZodString>;
}, z.core.$strip>;
};
export type SectionKey = keyof typeof sectionSchemas;
+1 -1
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),
country: z.string().trim().length(2).default("IR"),
}),
/** Google Maps / Neshan / etc. share link shown next to the address */
mapUrl: text(500),
/** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
workingHours: req(120),
openingHours: z.object({
@@ -61,6 +63,8 @@ export const settingsSectionSchema = z.object({
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
}),
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"),
/** sticky header CTA (e.g. «درخواست مشاوره») */
headerCta: linkSchema.default({ label: "درخواست مشاوره", href: "/#contact" }),
socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
ogImage: imageRefSchema.extend({
width: z.number().int().positive().nullable().default(null),
@@ -156,6 +160,10 @@ export const contactSectionSchema = z.object({
formTitle: req(160),
successTitle: req(80),
successText: req(300),
/** submit button on the contact form */
submitLabel: req(80).default("ارسال درخواست مشاوره"),
/** button shown after a successful submit */
resetLabel: req(80).default("ثبت درخواست جدید"),
});
export const footerSectionSchema = z.object({
enabled: z.boolean().default(true),
@@ -163,6 +171,10 @@ export const footerSectionSchema = z.object({
copyright: req(160),
certLine: text(160),
emergencyNote: text(80),
/** column headings under the brand block */
linksTitle: req(40).default("دسترسی"),
productsTitle: req(40).default("محصولات"),
contactTitle: req(40).default("ارتباط"),
});
export const sectionSchemas = {
settings: settingsSectionSchema,
@@ -192,7 +204,7 @@ export const SECTION_LABELS = {
standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" },
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" },
contact: { title: "تماس با ما", desc: "عنوان‌ها و متن‌های فرم تماس" },
footer: { title: "فوتر", desc: "متن معرفی، کپی‌رایت و توضیحات پایین صفحه" },
contact: { title: "تماس با ما", desc: "عنوان‌ها، متن‌های فرم، و ویرایش نشانی و اطلاعات تماس" },
footer: { title: "فوتر", desc: "متن معرفی، عناوین ستون‌ها، کپی‌رایت و توضیحات پایین صفحه" },
};
//# sourceMappingURL=sections.js.map
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),
country: z.string().trim().length(2).default("IR"),
}),
/** Google Maps / Neshan / etc. share link shown next to the address */
mapUrl: text(500),
/** human-readable working hours, e.g. «شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰» */
workingHours: req(120),
openingHours: z.object({
@@ -69,6 +71,8 @@ export const settingsSectionSchema = z.object({
closes: z.string().regex(/^\d{2}:\d{2}$/, "قالب ساعت باید HH:MM باشد"),
}),
foundingDate: z.string().regex(/^\d{4}$/, "سال میلادی چهاررقمی"),
/** sticky header CTA (e.g. «درخواست مشاوره») */
headerCta: linkSchema.default({ label: "درخواست مشاوره", href: "/#contact" }),
socials: z.array(z.object({ type: z.enum(SOCIAL_TYPES), url: req(300) })).max(12),
ogImage: imageRefSchema.extend({
width: z.number().int().positive().nullable().default(null),
@@ -180,6 +184,10 @@ export const contactSectionSchema = z.object({
formTitle: req(160),
successTitle: req(80),
successText: req(300),
/** submit button on the contact form */
submitLabel: req(80).default("ارسال درخواست مشاوره"),
/** button shown after a successful submit */
resetLabel: req(80).default("ثبت درخواست جدید"),
});
export const footerSectionSchema = z.object({
@@ -188,6 +196,10 @@ export const footerSectionSchema = z.object({
copyright: req(160),
certLine: text(160),
emergencyNote: text(80),
/** column headings under the brand block */
linksTitle: req(40).default("دسترسی"),
productsTitle: req(40).default("محصولات"),
contactTitle: req(40).default("ارتباط"),
});
export const sectionSchemas = {
@@ -225,6 +237,6 @@ export const SECTION_LABELS: Record<SectionKey, { title: string; desc: string }>
standards: { title: "استانداردها", desc: "استانداردها و گواهی‌نامه‌ها" },
industries: { title: "صنایع", desc: "صنایع تحت پوشش و تصویر هرکدام" },
testimonials: { title: "نظرات مشتریان", desc: "نقل‌قول‌های مشتریان" },
contact: { title: "تماس با ما", desc: "عنوان‌ها و متن‌های فرم تماس" },
footer: { title: "فوتر", desc: "متن معرفی، کپی‌رایت و توضیحات پایین صفحه" },
contact: { title: "تماس با ما", desc: "عنوان‌ها، متن‌های فرم، و ویرایش نشانی و اطلاعات تماس" },
footer: { title: "فوتر", desc: "متن معرفی، عناوین ستون‌ها، کپی‌رایت و توضیحات پایین صفحه" },
};