feat: add landing page templates and registration modal

- Implemented header and hero section in _header.html.twig and _hero_art.html.twig.
- Created a pre-registration modal in _reg_modal.html.twig with form fields and validation.
- Added page scripts for dynamic behavior and interaction in _page_scripts.html.twig.
- Developed landing page structure in landing.html.twig, integrating header, footer, and modal.
- Introduced tests for landing page rendering and registry validation in LandingPageTest.php and LandingRegistryTest.php.
This commit is contained in:
hamed
2026-08-10 14:38:27 +03:30
parent 231ce793bc
commit 68b8b05630
16 changed files with 1676 additions and 596 deletions
+86
View File
@@ -0,0 +1,86 @@
<!-- ===================== Footer ===================== -->
<footer class="site-footer" id="contact">
<div class="wrap">
<div class="footer-grid">
<div class="footer-col footer-brand">
<a class="brand" href="#hero">
<b>کلینیک پرو</b>
<span class="dot"></span>
</a>
<p>کلینیک پرو را ساختیم تا پزشک‌ها به‌جای درگیری با دفتر و کاغذ، حواس‌شان به کار اصلی‌شان باشد: بیمار.</p>
<div class="foot-socials">
<a href="#" aria-label="اینستاگرام">
<svg viewbox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="5" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.8"/><circle cx="17.4" cy="6.6" r="1.2" fill="currentColor"/></svg>
</a>
<a href="#" aria-label="تلگرام">
<svg viewbox="0 0 24 24" fill="none"><path d="M21 4 3 11l5 2 2 6 3-4 5 4 3-15Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
</a>
<a href="#" aria-label="واتساپ">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 20l1.4-4A8 8 0 1 1 9 18.6L4 20Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
</a>
</div>
<!-- نماد اعتماد الکترونیکی — بعد از لود کامل صفحه تزریق می‌شود (footer JS) -->
<div id="enamad-seal" style="margin-top:18px"></div>
</div>
<div class="footer-col">
<h4>دسترسی سریع</h4>
<div class="foot-links">
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود نرم‌افزار</a>
<a href="#contact">تماس با ما</a>
</div>
</div>
<div class="footer-col">
<h4>راهکارها</h4>
<div class="foot-links">
{% for landing in landing_pages.all() %}
<a href="/{{ landing.slug }}">{{ landing.h1 }}</a>
{% endfor %}
</div>
</div>
<div class="footer-col">
<h4>اطلاعات تماس</h4>
<div class="foot-contact">
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-5.2-7-11a7 7 0 0 1 14 0c0 5.8-7 11-7 11Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><circle cx="12" cy="10" r="2.6" stroke="currentColor" stroke-width="1.7"/></svg>
<span>یاسوج، مهریان، خیابان شهید ستاره شیرازی، خیابان امام‌زاده احمد، پلاک ۸ شرکت توسعه فناوری اطلاعات حامی</span>
</div>
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 6.5C4 5 5 4 6.5 4h2c.6 0 1.1.4 1.3 1l1 3c.2.6 0 1.2-.5 1.6L9 11c1 2 2.5 3.5 4.5 4.5l1.4-1.3c.4-.4 1-.6 1.6-.4l3 1c.6.2 1 .7 1 1.3v2c0 1.5-1 2.5-2.5 2.4C10.5 20 4 13.5 4 6.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<a href="tel:02191550875" style="font-variant-numeric:tabular-nums" dir="ltr">02191550875</a>
</div>
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="m4 7 8 5 8-5" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<a href="mailto:info@clinic-pro.ir" class="mono" dir="ltr">info@clinic-pro.ir</a>
</div>
</div>
</div>
<div class="footer-col" style="display:none">
<h4>دانلود نرم‌افزار</h4>
<div class="foot-badges">
<a class="store-badge" href="#" style="background:oklch(1 0 0 / 0.08)">
<svg viewbox="0 0 24 24" fill="currentColor"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.9-3.5.9-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.7 2.3-1.6 2.8-.4 6.9 1.1 9.2.8 1.1 1.6 2.3 2.8 2.3 1.1 0 1.6-.7 2.9-.7 1.4 0 1.7.7 2.9.7 1.2 0 2-1.1 2.7-2.2.9-1.2 1.2-2.5 1.2-2.5s-2.3-.9-2.3-3.6ZM14.2 5.7c.6-.8 1-1.8.9-2.9-.9 0-2 .6-2.6 1.4-.6.7-1.1 1.7-.9 2.7 1 .1 2-.5 2.6-1.2Z"/></svg>
<span class="sb-t">
<small>دانلود برای</small>
<b>مک · macOS</b>
</span>
</a>
<a class="store-badge" href="#" style="background:oklch(1 0 0 / 0.08)">
<svg viewbox="0 0 24 24" fill="currentColor"><path d="M3 5.5 10.5 4.4v7.1H3V5.5Zm0 13L10.5 19.6v-7H3v6Zm8.5 1.3L21 21V12.5h-9.5v7.3Zm0-15.6V11.5H21V3l-9.5 1.2Z"/></svg>
<span class="sb-t">
<small>دانلود برای</small>
<b>ویندوز</b>
</span>
</a>
</div>
</div>
</div>
</div>
<div class="footer-bottom">© ۲۰۲۴ کلینیک پرو · ساخته‌شده برای پزشکان ایران</div>
</footer>
+27
View File
@@ -0,0 +1,27 @@
<!-- ===================== Header ===================== -->
<header class="site-header" id="header">
<div class="wrap nav">
<a class="brand" href="#hero" aria-label="کلینیک پرو">
<b>کلینیک پرو</b>
<span class="dot"></span>
<small>مدیریت مطب</small>
</a>
<nav class="nav-links" id="navLinks">
<a href="#hero" class="active">خانه</a>
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود</a>
<a href="#contact">تماس با ما</a>
<a href="#" id="openRegModalNav" class="nav-reg-mobile">ثبت نام</a>
</nav>
<div class="nav-right">
<button class="nav-burger nav-toggle" id="navToggle" aria-label="منو">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
<button class="btn btn-ghost" id="openRegModal" style="font-size:13px;padding:8px 20px">ثبت نام</button>
<a href="/admin" class="btn btn-blue" style="font-size:13px;padding:8px 20px;text-decoration:none">ورود به پنل</a>
</div>
</div>
</header>
+80
View File
@@ -0,0 +1,80 @@
<div class="hero-art reveal d1">
<div class="art-chip chip-1">
<span class="ci">
<svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 3.5C19 16.7 12 21 12 21Z" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg>
</span>
<div>
<b style="font-variant-numeric:tabular-nums">۹۵٪</b>
<span>رضایت بیماران</span>
</div>
</div>
<div class="art-chip chip-2">
<span class="ci">
<svg viewbox="0 0 24 24" fill="none"><path d="M8 7V5m8 2V5M4 9h16M5 7h14v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7Z" stroke="currentColor" stroke-width="1.9"/></svg>
</span>
<div>
<b style="font-variant-numeric:tabular-nums">۳۸</b>
<span>نوبت امروز</span>
</div>
</div>
<svg viewbox="0 0 600 480" role="img" aria-label="تصویر مدیریت مطب">
<ellipse cx="300" cy="438" rx="250" ry="30" fill="#e2e9fb"/>
<circle cx="300" cy="244" r="184" fill="#eaeffb"/>
<g fill="#dbe4fb"><ellipse cx="138" cy="150" rx="42" ry="20"/><ellipse cx="476" cy="120" rx="48" ry="22"/><ellipse cx="508" cy="250" rx="30" ry="15"/></g>
<rect x="196" y="184" width="208" height="152" rx="13" fill="#fff" stroke="#dde3f7" stroke-width="3"/>
<rect x="210" y="198" width="180" height="124" rx="7" fill="#eef2fd"/>
<rect x="223" y="211" width="68" height="9" rx="4.5" fill="#c4cef2"/>
<circle cx="378" cy="216" r="6" fill="#f8b3a6"/>
<path d="M223 272 h24 l9 -22 12 40 10 -28 8 14 h62" fill="none" stroke="#5666e0" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<g fill="#cdd6f4"><rect x="226" y="292" width="14" height="22" rx="3"/><rect x="246" y="284" width="14" height="30" rx="3"/><rect x="266" y="298" width="14" height="16" rx="3"/></g>
<rect x="330" y="288" width="50" height="26" rx="6" fill="#fde3df"/>
<path d="M166 336 h268 l26 28 H140 z" fill="#cfd9f9"/>
<path d="M166 336 h268 l6 6 H160 z" fill="#aab6ee"/>
<rect x="268" y="350" width="64" height="6" rx="3" fill="#9aa8ea"/>
<g fill="#fff"><ellipse cx="300" cy="190" rx="58" ry="22"/><ellipse cx="258" cy="196" rx="26" ry="15"/><ellipse cx="342" cy="196" rx="26" ry="15"/></g>
<path d="M276 150 q-20 16 -15 42 l19 -11 z" fill="#3a3a7d"/>
<path d="M324 150 q20 16 15 42 l-19 -11 z" fill="#3a3a7d"/>
<rect x="278" y="70" width="44" height="118" rx="22" fill="#fff" stroke="#dfe4f7" stroke-width="2"/>
<path d="M300 50 a22 22 0 0 1 22 26 h-44 a22 22 0 0 1 22 -26z" fill="#f17a6b"/>
<circle cx="300" cy="106" r="13" fill="#cfe0fb" stroke="#5666e0" stroke-width="3"/>
<g fill="#5666e0"><rect x="295" y="130" width="10" height="30" rx="3"/><rect x="285" y="140" width="30" height="10" rx="3"/></g>
<path d="M289 188 q11 24 11 30 q0 -6 11 -30 z" fill="#f8b3a6"/>
<g stroke="#bcc7f2" stroke-width="4" stroke-linecap="round"><path d="M250 92 h-20"/><path d="M258 112 h-13"/><path d="M350 92 h20"/><path d="M342 112 h13"/></g>
<g>
<path d="M86 332 h36 l-2 86 a6 6 0 0 1 -12 0 l-4 -52 -4 52 a6 6 0 0 1 -12 0 z" fill="#3a3a7d"/>
<ellipse cx="90" cy="420" rx="12" ry="6" fill="#2b2b5e"/>
<ellipse cx="118" cy="420" rx="12" ry="6" fill="#2b2b5e"/>
<path d="M80 262 q-14 30 -6 58" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="76" cy="318" r="7" fill="#f3c6a5"/>
<path d="M78 256 q26 -18 54 0 l8 80 h-70 z" fill="#fff" stroke="#dde3f7" stroke-width="2"/>
<path d="M105 250 l-9 56 9 7 9 -7 -9 -56z" fill="#eef2fd"/>
<path d="M99 254 q-12 28 9 42" fill="none" stroke="#5666e0" stroke-width="2.6"/>
<circle cx="110" cy="298" r="5" fill="#5666e0"/>
<path d="M128 260 q42 -6 66 -54" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="196" cy="206" r="7.5" fill="#f3c6a5"/>
<rect x="99" y="232" width="12" height="16" rx="4" fill="#e8b594"/>
<circle cx="105" cy="218" r="22" fill="#f3c6a5"/>
<path d="M83 216 q0 -30 31 -28 q19 1 21 22 q-13 -12 -30 -7 q-15 4 -22 13z" fill="#3a3a7d"/>
</g>
<g>
<path d="M478 332 h36 l-2 86 a6 6 0 0 1 -12 0 l-4 -52 -4 52 a6 6 0 0 1 -12 0 z" fill="#4f51a8"/>
<ellipse cx="482" cy="420" rx="12" ry="6" fill="#34356f"/>
<ellipse cx="510" cy="420" rx="12" ry="6" fill="#34356f"/>
<path d="M512 258 q26 -16 30 -54" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="543" cy="200" r="8" fill="#f3c6a5"/>
<path d="M462 256 q26 -18 54 0 l6 80 h-66 z" fill="#fff" stroke="#dde3f7" stroke-width="2"/>
<path d="M489 250 l-8 56 8 7 8 -7 -8 -56z" fill="#fde3df"/>
<path d="M464 260 q-12 28 -6 54" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round"/>
<circle cx="460" cy="316" r="7" fill="#f3c6a5"/>
<rect x="483" y="232" width="12" height="16" rx="4" fill="#e8b594"/>
<circle cx="489" cy="218" r="22" fill="#f3c6a5"/>
<path d="M467 220 q-4 -32 28 -32 q24 0 22 30 q-2 14 -6 20 q4 -22 -8 -30 q4 12 -2 18 q-10 -20 -34 -6z" fill="#3a3a7d"/>
</g>
</svg>
</div>
+166
View File
@@ -0,0 +1,166 @@
<script>
(function () {
'use strict';
var FA = [
'۰',
'۱',
'۲',
'۳',
'۴',
'۵',
'۶',
'۷',
'۸',
'۹'
];
function groupFa(n) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '،').replace(/\d/g, function (d) {
return FA[+ d];
});
}
var header = document.getElementById('header');
function onScroll() {
header.classList.toggle('scrolled', window.scrollY > 12);
}
window.addEventListener('scroll', onScroll, {passive: true});
onScroll();
var navToggle = document.getElementById('navToggle');
var navLinks = document.getElementById('navLinks');
if (navToggle) {
navToggle.addEventListener('click', function () {
navLinks.classList.toggle('open');
});
navLinks.addEventListener('click', function (e) {
if (e.target.tagName === 'A')
navLinks.classList.remove('open');
});
}
var sections = [
'hero',
'features',
'why',
'download',
'contact'
];
var links = Array.prototype.slice.call(document.querySelectorAll('.nav-links a'));
function syncActive() {
var y = window.scrollY + 120,
cur = 'hero';
sections.forEach(function (id) {
var el = document.getElementById(id);
if (el && el.offsetTop <= y)
cur = id;
});
links.forEach(function (a) {
a.classList.toggle('active', a.getAttribute('href') === '#' + cur);
});
}
window.addEventListener('scroll', syncActive, {passive: true});
syncActive();
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var reveals = Array.prototype.slice.call(document.querySelectorAll('.reveal'));
function inView(el) {
var r = el.getBoundingClientRect();
return r.top<(window.innerHeight||document.documentElement.clientHeight)-30 && r.bottom> 0;
}
function reveal(el) {
el.classList.add('in');
}
if (reduce) {
reveals.forEach(reveal);
} else {
reveals.forEach(function (el) {
if (inView(el))
reveal(el);
});
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (es) {
es.forEach(function (e) {
if (e.isIntersecting) {
reveal(e.target);
io.unobserve(e.target);
}
});
}, {
threshold: 0.12,
rootMargin: '0px 0px -40px 0px'
});
reveals.forEach(function (el) {
if (! el.classList.contains('in'))
io.observe(el);
});
}
var ticking = false;
function sweep() {
if (ticking)
return;
ticking = true;
requestAnimationFrame(function () {
reveals.forEach(function (el) {
if (inView(el))
reveal(el);
});
ticking = false;
});
}
window.addEventListener('scroll', sweep, {passive: true});
window.addEventListener('resize', sweep, {passive: true});
setTimeout(function () {
reveals.forEach(reveal);
}, 2800);
}
// نماد اعتماد الکترونیکی (Enamad) — فقط بعد از لود کامل صفحه تزریق می‌شود تا
// درخواست تصویرِ دامنه‌ی بیرونی با منابع خود سایت رقابت نکند و سرعت لود پایین نیاید.
function mountEnamad() {
var slot = document.getElementById('enamad-seal');
if (! slot)
return;
var a = document.createElement('a');
a.setAttribute('referrerpolicy', 'origin');
a.target = '_blank';
a.href = 'https://trustseal.enamad.ir/?id=755841&Code=8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4';
var img = document.createElement('img');
img.setAttribute('referrerpolicy', 'origin');
img.src = 'https://trustseal.enamad.ir/logo.aspx?id=755841&Code=8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4';
img.alt = 'نماد اعتماد الکترونیکی';
img.style.cursor = 'pointer';
img.setAttribute('code', '8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4');
img.loading = 'lazy';
a.appendChild(img);
slot.appendChild(a);
}
if (document.readyState === 'complete') {
mountEnamad();
} else {
window.addEventListener('load', mountEnamad);
}
})();
</script>
+240
View File
@@ -0,0 +1,240 @@
<!-- ===================== Pre-Registration Modal ===================== -->
<div id="regOverlay" style="display:none;position:fixed;inset:0;background:oklch(0.2 0.05 285 / 0.55);z-index:900;backdrop-filter:blur(4px);overflow-y:auto;padding:24px 16px" onclick="if(event.target===this)closeRegModal()">
<div style="background:#fff;border-radius:20px;max-width:520px;margin:auto;padding:32px 28px;position:relative;box-shadow:0 24px 60px oklch(0.3 0.1 285 / 0.22)">
<button onclick="closeRegModal()" aria-label="بستن" style="position:absolute;top:16px;left:20px;background:none;border:none;cursor:pointer;font-size:22px;color:var(--text-2);line-height:1">×</button>
<h2 style="margin:0 0 6px;font-size:20px;font-weight:800;color:var(--ink)">ثبت نام در کلینیک پرو</h2>
<p style="margin:0 0 24px;font-size:13px;color:var(--text-2)">نوع حساب خود را انتخاب کنید</p>
<!-- Type Cards -->
<div id="typeCards" style="display:flex;flex-direction:column;gap:10px;margin-bottom:22px">
<label class="reg-card" data-type="independent_doctor" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="independent_doctor" style="display:none">
<span style="font-size:26px;flex-shrink:0">🩺</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">دکتر هستم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">مطب شخصی دارم، به تنهایی کار می‌کنم</div>
</div>
</label>
<label class="reg-card" data-type="doctor_with_clinic" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="doctor_with_clinic" style="display:none">
<span style="font-size:26px;flex-shrink:0">🏥</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">دکتر هستم و کلینیک دارم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">با چند دکتر در یک کلینیک همکاری می‌کنم</div>
</div>
</label>
<label class="reg-card" data-type="clinic_manager" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="clinic_manager" style="display:none">
<span style="font-size:26px;flex-shrink:0">🏢</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">مدیر کلینیک هستم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">مدیریت کلینیک را دارم، خودم دکتر نیستم</div>
</div>
</label>
</div>
<!-- Form Fields -->
<div id="regFields" style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">نام کامل
<span style="color:var(--coral)">*</span>
</label>
<input id="regName" type="text" placeholder="مثال: دکتر علی احمدی" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink)"/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">شماره موبایل
<span style="color:var(--coral)">*</span>
</label>
<input id="regMobile" type="tel" placeholder="09xxxxxxxxx" dir="ltr" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink);text-align:right"/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">توضیحات
<span style="font-weight:400;color:var(--text-2)">(اختیاری)</span>
</label>
<textarea id="regInfo" rows="3" placeholder="تخصص، آدرس، سابقه کاری، ..." style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;resize:vertical;color:var(--ink)"></textarea>
</div>
</div>
{% if altcha_enabled %}
<!-- ALTCHA captcha — proof-of-work در پس‌زمینه (بدون تعامل کاربر) -->
{# widget v3: attribute «challenge» (نه challengeurl قدیمی) #}
<altcha-widget id="regAltcha" challenge="/api/v1/altcha/challenge" auto="onload" language="fa" style="display:block;margin-top:16px"></altcha-widget>
{% endif %}
<!-- Error / Success -->
<div id="regMsg" style="display:none;margin-top:14px;padding:12px 16px;border-radius:10px;font-size:13px"></div>
<button id="regSubmitBtn" disabled onclick="submitReg()" style="margin-top:20px;width:100%;background:var(--blue);color:#fff;border:none;border-radius:12px;padding:13px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;opacity:.45;transition:opacity .2s">
ارسال درخواست
</button>
<p style="text-align:center;font-size:12px;color:var(--text-2);margin:12px 0 0">پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود</p>
</div>
</div>
<style>
.reg-card:hover {
border-color: var(--blue) !important;
background: oklch(0.97 0.02 277);
}
.reg-card.selected {
border-color: var(--blue) !important;
background: oklch(0.96 0.03 277);
}
.reg-card.selected div > div:first-child {
color: var(--blue);
}
#regName:focus,
#regMobile:focus,
#regInfo:focus {
border-color: var(--blue);
box-shadow: 0 0 0 3px oklch(0.58 0.185 277 / 0.12);
}
</style>
<script>
(function () {
var overlay = document.getElementById('regOverlay');
var submitBtn = document.getElementById('regSubmitBtn');
var selectedType = null;
// payload حل‌شده‌ی ALTCHA (یک‌بارمصرف؛ در prod الزامی است)
var regAltchaPayload = '';
var regAltchaEl = document.getElementById('regAltcha');
if (regAltchaEl)
regAltchaEl.addEventListener('statechange', function (e) {
if (e.detail && e.detail.state === 'verified')
regAltchaPayload = e.detail.payload;
});
function openRegModal() {
overlay.style.display = 'block';
document.body.style.overflow = 'hidden';
}
window.closeRegModal = function () {
overlay.style.display = 'none';
document.body.style.overflow = '';
};
var btn1 = document.getElementById('openRegModal');
var btn2 = document.getElementById('openRegModal2');
var btn3 = document.getElementById('openRegModalNav');
if (btn1)
btn1.addEventListener('click', openRegModal);
if (btn2)
btn2.addEventListener('click', openRegModal);
if (btn3)
btn3.addEventListener('click', function (e) {
e.preventDefault();
openRegModal();
});
document.querySelectorAll('.reg-card').forEach(function (card) {
card.addEventListener('click', function () {
document.querySelectorAll('.reg-card').forEach(function (c) {
c.classList.remove('selected');
});
card.classList.add('selected');
var radio = card.querySelector('input[type=radio]');
if (radio)
radio.checked = true;
selectedType = card.dataset.type;
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
window.submitReg = function () {
var name = document.getElementById('regName').value.trim();
var mobile = document.getElementById('regMobile').value.trim();
var info = document.getElementById('regInfo').value.trim();
var msg = document.getElementById('regMsg');
if (! selectedType)
return;
if (! name) {
showMsg('نام را وارد کنید', 'error');
return;
}
if (! mobile || mobile.length < 10) {
showMsg('شماره موبایل معتبر نیست', 'error');
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'در حال ارسال...';
fetch('/api/v1/pre-registration', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
type: selectedType,
name: name,
mobile: mobile,
info: info || null,
altcha: regAltchaPayload
}
)
}).then(function (r) {
return r.json();
}).then(function (data) {
if (data.success) {
showMsg('درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود.', 'success');
submitBtn.textContent = 'ارسال شد ✓';
setTimeout(function () {
closeRegModal();
}, 2200);
} else {
var errMsg = (data.errors && data.errors[0]) ? data.errors[0].message : 'خطایی رخ داد';
showMsg(errMsg, 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
}
}).catch(function () {
showMsg('خطا در اتصال به سرور', 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
});
};
// challenge یک‌بارمصرف است؛ بعد از خطا یک challenge تازه بگیر.
function resetRegAltcha() {
regAltchaPayload = '';
if (regAltchaEl && typeof regAltchaEl.reset === 'function')
regAltchaEl.reset();
}
function showMsg(text, type) {
var msg = document.getElementById('regMsg');
msg.textContent = text;
msg.style.display = 'block';
msg.style.background = type === 'success' ? '#dcfce7' : '#fef2f2';
msg.style.color = type === 'success' ? '#16a34a' : '#ef4444';
msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca';
}
})();
</script>
+5 -590
View File
@@ -70,33 +70,7 @@
</head>
<body>
<!-- ===================== Header ===================== -->
<header class="site-header" id="header">
<div class="wrap nav">
<a class="brand" href="#hero" aria-label="کلینیک پرو">
<b>کلینیک پرو</b>
<span class="dot"></span>
<small>مدیریت مطب</small>
</a>
<nav class="nav-links" id="navLinks">
<a href="#hero" class="active">خانه</a>
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود</a>
<a href="#contact">تماس با ما</a>
<a href="#" id="openRegModalNav" class="nav-reg-mobile">ثبت نام</a>
</nav>
<div class="nav-right">
<button class="nav-burger nav-toggle" id="navToggle" aria-label="منو">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
<button class="btn btn-ghost" id="openRegModal" style="font-size:13px;padding:8px 20px">ثبت نام</button>
<a href="/admin" class="btn btn-blue" style="font-size:13px;padding:8px 20px;text-decoration:none">ورود به پنل</a>
</div>
</div>
</header>
{{ include('public/_header.html.twig') }}
<main>
<!-- ===================== Hero ===================== -->
@@ -111,86 +85,7 @@
</div>
</div>
<div class="hero-art reveal d1">
<div class="art-chip chip-1">
<span class="ci">
<svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 3.5C19 16.7 12 21 12 21Z" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg>
</span>
<div>
<b style="font-variant-numeric:tabular-nums">۹۵٪</b>
<span>رضایت بیماران</span>
</div>
</div>
<div class="art-chip chip-2">
<span class="ci">
<svg viewbox="0 0 24 24" fill="none"><path d="M8 7V5m8 2V5M4 9h16M5 7h14v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7Z" stroke="currentColor" stroke-width="1.9"/></svg>
</span>
<div>
<b style="font-variant-numeric:tabular-nums">۳۸</b>
<span>نوبت امروز</span>
</div>
</div>
<svg viewbox="0 0 600 480" role="img" aria-label="تصویر مدیریت مطب">
<ellipse cx="300" cy="438" rx="250" ry="30" fill="#e2e9fb"/>
<circle cx="300" cy="244" r="184" fill="#eaeffb"/>
<g fill="#dbe4fb"><ellipse cx="138" cy="150" rx="42" ry="20"/><ellipse cx="476" cy="120" rx="48" ry="22"/><ellipse cx="508" cy="250" rx="30" ry="15"/></g>
<rect x="196" y="184" width="208" height="152" rx="13" fill="#fff" stroke="#dde3f7" stroke-width="3"/>
<rect x="210" y="198" width="180" height="124" rx="7" fill="#eef2fd"/>
<rect x="223" y="211" width="68" height="9" rx="4.5" fill="#c4cef2"/>
<circle cx="378" cy="216" r="6" fill="#f8b3a6"/>
<path d="M223 272 h24 l9 -22 12 40 10 -28 8 14 h62" fill="none" stroke="#5666e0" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<g fill="#cdd6f4"><rect x="226" y="292" width="14" height="22" rx="3"/><rect x="246" y="284" width="14" height="30" rx="3"/><rect x="266" y="298" width="14" height="16" rx="3"/></g>
<rect x="330" y="288" width="50" height="26" rx="6" fill="#fde3df"/>
<path d="M166 336 h268 l26 28 H140 z" fill="#cfd9f9"/>
<path d="M166 336 h268 l6 6 H160 z" fill="#aab6ee"/>
<rect x="268" y="350" width="64" height="6" rx="3" fill="#9aa8ea"/>
<g fill="#fff"><ellipse cx="300" cy="190" rx="58" ry="22"/><ellipse cx="258" cy="196" rx="26" ry="15"/><ellipse cx="342" cy="196" rx="26" ry="15"/></g>
<path d="M276 150 q-20 16 -15 42 l19 -11 z" fill="#3a3a7d"/>
<path d="M324 150 q20 16 15 42 l-19 -11 z" fill="#3a3a7d"/>
<rect x="278" y="70" width="44" height="118" rx="22" fill="#fff" stroke="#dfe4f7" stroke-width="2"/>
<path d="M300 50 a22 22 0 0 1 22 26 h-44 a22 22 0 0 1 22 -26z" fill="#f17a6b"/>
<circle cx="300" cy="106" r="13" fill="#cfe0fb" stroke="#5666e0" stroke-width="3"/>
<g fill="#5666e0"><rect x="295" y="130" width="10" height="30" rx="3"/><rect x="285" y="140" width="30" height="10" rx="3"/></g>
<path d="M289 188 q11 24 11 30 q0 -6 11 -30 z" fill="#f8b3a6"/>
<g stroke="#bcc7f2" stroke-width="4" stroke-linecap="round"><path d="M250 92 h-20"/><path d="M258 112 h-13"/><path d="M350 92 h20"/><path d="M342 112 h13"/></g>
<g>
<path d="M86 332 h36 l-2 86 a6 6 0 0 1 -12 0 l-4 -52 -4 52 a6 6 0 0 1 -12 0 z" fill="#3a3a7d"/>
<ellipse cx="90" cy="420" rx="12" ry="6" fill="#2b2b5e"/>
<ellipse cx="118" cy="420" rx="12" ry="6" fill="#2b2b5e"/>
<path d="M80 262 q-14 30 -6 58" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="76" cy="318" r="7" fill="#f3c6a5"/>
<path d="M78 256 q26 -18 54 0 l8 80 h-70 z" fill="#fff" stroke="#dde3f7" stroke-width="2"/>
<path d="M105 250 l-9 56 9 7 9 -7 -9 -56z" fill="#eef2fd"/>
<path d="M99 254 q-12 28 9 42" fill="none" stroke="#5666e0" stroke-width="2.6"/>
<circle cx="110" cy="298" r="5" fill="#5666e0"/>
<path d="M128 260 q42 -6 66 -54" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="196" cy="206" r="7.5" fill="#f3c6a5"/>
<rect x="99" y="232" width="12" height="16" rx="4" fill="#e8b594"/>
<circle cx="105" cy="218" r="22" fill="#f3c6a5"/>
<path d="M83 216 q0 -30 31 -28 q19 1 21 22 q-13 -12 -30 -7 q-15 4 -22 13z" fill="#3a3a7d"/>
</g>
<g>
<path d="M478 332 h36 l-2 86 a6 6 0 0 1 -12 0 l-4 -52 -4 52 a6 6 0 0 1 -12 0 z" fill="#4f51a8"/>
<ellipse cx="482" cy="420" rx="12" ry="6" fill="#34356f"/>
<ellipse cx="510" cy="420" rx="12" ry="6" fill="#34356f"/>
<path d="M512 258 q26 -16 30 -54" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
<circle cx="543" cy="200" r="8" fill="#f3c6a5"/>
<path d="M462 256 q26 -18 54 0 l6 80 h-66 z" fill="#fff" stroke="#dde3f7" stroke-width="2"/>
<path d="M489 250 l-8 56 8 7 8 -7 -8 -56z" fill="#fde3df"/>
<path d="M464 260 q-12 28 -6 54" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round"/>
<circle cx="460" cy="316" r="7" fill="#f3c6a5"/>
<rect x="483" y="232" width="12" height="16" rx="4" fill="#e8b594"/>
<circle cx="489" cy="218" r="22" fill="#f3c6a5"/>
<path d="M467 220 q-4 -32 28 -32 q24 0 22 30 q-2 14 -6 20 q4 -22 -8 -30 q4 12 -2 18 q-10 -20 -34 -6z" fill="#3a3a7d"/>
</g>
</svg>
</div>
{{ include('public/_hero_art.html.twig') }}
</div>
</section>
@@ -500,493 +395,13 @@
</div>
</div>
</section>
<!-- ===================== Pre-Registration Modal ===================== -->
<div id="regOverlay" style="display:none;position:fixed;inset:0;background:oklch(0.2 0.05 285 / 0.55);z-index:900;backdrop-filter:blur(4px);overflow-y:auto;padding:24px 16px" onclick="if(event.target===this)closeRegModal()">
<div style="background:#fff;border-radius:20px;max-width:520px;margin:auto;padding:32px 28px;position:relative;box-shadow:0 24px 60px oklch(0.3 0.1 285 / 0.22)">
<button onclick="closeRegModal()" aria-label="بستن" style="position:absolute;top:16px;left:20px;background:none;border:none;cursor:pointer;font-size:22px;color:var(--text-2);line-height:1">×</button>
<h2 style="margin:0 0 6px;font-size:20px;font-weight:800;color:var(--ink)">ثبت نام در کلینیک پرو</h2>
<p style="margin:0 0 24px;font-size:13px;color:var(--text-2)">نوع حساب خود را انتخاب کنید</p>
<!-- Type Cards -->
<div id="typeCards" style="display:flex;flex-direction:column;gap:10px;margin-bottom:22px">
<label class="reg-card" data-type="independent_doctor" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="independent_doctor" style="display:none">
<span style="font-size:26px;flex-shrink:0">🩺</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">دکتر هستم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">مطب شخصی دارم، به تنهایی کار می‌کنم</div>
</div>
</label>
<label class="reg-card" data-type="doctor_with_clinic" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="doctor_with_clinic" style="display:none">
<span style="font-size:26px;flex-shrink:0">🏥</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">دکتر هستم و کلینیک دارم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">با چند دکتر در یک کلینیک همکاری می‌کنم</div>
</div>
</label>
<label class="reg-card" data-type="clinic_manager" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border:2px solid var(--border);border-radius:14px;cursor:pointer;transition:all .2s">
<input type="radio" name="regType" value="clinic_manager" style="display:none">
<span style="font-size:26px;flex-shrink:0">🏢</span>
<div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">مدیر کلینیک هستم</div>
<div style="font-size:12px;color:var(--text-2);margin-top:2px">مدیریت کلینیک را دارم، خودم دکتر نیستم</div>
</div>
</label>
</div>
<!-- Form Fields -->
<div id="regFields" style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">نام کامل
<span style="color:var(--coral)">*</span>
</label>
<input id="regName" type="text" placeholder="مثال: دکتر علی احمدی" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink)"/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">شماره موبایل
<span style="color:var(--coral)">*</span>
</label>
<input id="regMobile" type="tel" placeholder="09xxxxxxxxx" dir="ltr" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink);text-align:right"/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">توضیحات
<span style="font-weight:400;color:var(--text-2)">(اختیاری)</span>
</label>
<textarea id="regInfo" rows="3" placeholder="تخصص، آدرس، سابقه کاری، ..." style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;resize:vertical;color:var(--ink)"></textarea>
</div>
</div>
{% if altcha_enabled %}
<!-- ALTCHA captcha — proof-of-work در پس‌زمینه (بدون تعامل کاربر) -->
{# widget v3: attribute «challenge» (نه challengeurl قدیمی) #}
<altcha-widget id="regAltcha" challenge="/api/v1/altcha/challenge" auto="onload" language="fa" style="display:block;margin-top:16px"></altcha-widget>
{% endif %}
<!-- Error / Success -->
<div id="regMsg" style="display:none;margin-top:14px;padding:12px 16px;border-radius:10px;font-size:13px"></div>
<button id="regSubmitBtn" disabled onclick="submitReg()" style="margin-top:20px;width:100%;background:var(--blue);color:#fff;border:none;border-radius:12px;padding:13px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;opacity:.45;transition:opacity .2s">
ارسال درخواست
</button>
<p style="text-align:center;font-size:12px;color:var(--text-2);margin:12px 0 0">پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود</p>
</div>
</div>
<style>
.reg-card:hover {
border-color: var(--blue) !important;
background: oklch(0.97 0.02 277);
}
.reg-card.selected {
border-color: var(--blue) !important;
background: oklch(0.96 0.03 277);
}
.reg-card.selected div > div:first-child {
color: var(--blue);
}
#regName:focus,
#regMobile:focus,
#regInfo:focus {
border-color: var(--blue);
box-shadow: 0 0 0 3px oklch(0.58 0.185 277 / 0.12);
}
</style>
<script>
(function () {
var overlay = document.getElementById('regOverlay');
var submitBtn = document.getElementById('regSubmitBtn');
var selectedType = null;
// payload حل‌شده‌ی ALTCHA (یک‌بارمصرف؛ در prod الزامی است)
var regAltchaPayload = '';
var regAltchaEl = document.getElementById('regAltcha');
if (regAltchaEl)
regAltchaEl.addEventListener('statechange', function (e) {
if (e.detail && e.detail.state === 'verified')
regAltchaPayload = e.detail.payload;
});
function openRegModal() {
overlay.style.display = 'block';
document.body.style.overflow = 'hidden';
}
window.closeRegModal = function () {
overlay.style.display = 'none';
document.body.style.overflow = '';
};
var btn1 = document.getElementById('openRegModal');
var btn2 = document.getElementById('openRegModal2');
var btn3 = document.getElementById('openRegModalNav');
if (btn1)
btn1.addEventListener('click', openRegModal);
if (btn2)
btn2.addEventListener('click', openRegModal);
if (btn3)
btn3.addEventListener('click', function (e) {
e.preventDefault();
openRegModal();
});
document.querySelectorAll('.reg-card').forEach(function (card) {
card.addEventListener('click', function () {
document.querySelectorAll('.reg-card').forEach(function (c) {
c.classList.remove('selected');
});
card.classList.add('selected');
var radio = card.querySelector('input[type=radio]');
if (radio)
radio.checked = true;
selectedType = card.dataset.type;
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
window.submitReg = function () {
var name = document.getElementById('regName').value.trim();
var mobile = document.getElementById('regMobile').value.trim();
var info = document.getElementById('regInfo').value.trim();
var msg = document.getElementById('regMsg');
if (! selectedType)
return;
if (! name) {
showMsg('نام را وارد کنید', 'error');
return;
}
if (! mobile || mobile.length < 10) {
showMsg('شماره موبایل معتبر نیست', 'error');
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'در حال ارسال...';
fetch('/api/v1/pre-registration', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
type: selectedType,
name: name,
mobile: mobile,
info: info || null,
altcha: regAltchaPayload
}
)
}).then(function (r) {
return r.json();
}).then(function (data) {
if (data.success) {
showMsg('درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود.', 'success');
submitBtn.textContent = 'ارسال شد ✓';
setTimeout(function () {
closeRegModal();
}, 2200);
} else {
var errMsg = (data.errors && data.errors[0]) ? data.errors[0].message : 'خطایی رخ داد';
showMsg(errMsg, 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
}
}).catch(function () {
showMsg('خطا در اتصال به سرور', 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
});
};
// challenge یک‌بارمصرف است؛ بعد از خطا یک challenge تازه بگیر.
function resetRegAltcha() {
regAltchaPayload = '';
if (regAltchaEl && typeof regAltchaEl.reset === 'function')
regAltchaEl.reset();
}
function showMsg(text, type) {
var msg = document.getElementById('regMsg');
msg.textContent = text;
msg.style.display = 'block';
msg.style.background = type === 'success' ? '#dcfce7' : '#fef2f2';
msg.style.color = type === 'success' ? '#16a34a' : '#ef4444';
msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca';
}
})();
</script>
{{ include('public/_reg_modal.html.twig') }}
</main>
<!-- ===================== Footer ===================== -->
<footer class="site-footer" id="contact">
<div class="wrap">
<div class="footer-grid">
<div class="footer-col footer-brand">
<a class="brand" href="#hero">
<b>کلینیک پرو</b>
<span class="dot"></span>
</a>
<p>کلینیک پرو را ساختیم تا پزشک‌ها به‌جای درگیری با دفتر و کاغذ، حواس‌شان به کار اصلی‌شان باشد: بیمار.</p>
<div class="foot-socials">
<a href="#" aria-label="اینستاگرام">
<svg viewbox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="5" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.8"/><circle cx="17.4" cy="6.6" r="1.2" fill="currentColor"/></svg>
</a>
<a href="#" aria-label="تلگرام">
<svg viewbox="0 0 24 24" fill="none"><path d="M21 4 3 11l5 2 2 6 3-4 5 4 3-15Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
</a>
<a href="#" aria-label="واتساپ">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 20l1.4-4A8 8 0 1 1 9 18.6L4 20Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
</a>
</div>
<!-- نماد اعتماد الکترونیکی — بعد از لود کامل صفحه تزریق می‌شود (footer JS) -->
<div id="enamad-seal" style="margin-top:18px"></div>
</div>
{{ include('public/_footer.html.twig') }}
<div class="footer-col">
<h4>دسترسی سریع</h4>
<div class="foot-links">
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود نرم‌افزار</a>
<a href="#contact">تماس با ما</a>
</div>
</div>
<div class="footer-col">
<h4>اطلاعات تماس</h4>
<div class="foot-contact">
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-5.2-7-11a7 7 0 0 1 14 0c0 5.8-7 11-7 11Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><circle cx="12" cy="10" r="2.6" stroke="currentColor" stroke-width="1.7"/></svg>
<span>یاسوج، مهریان، خیابان شهید ستاره شیرازی، خیابان امام‌زاده احمد، پلاک ۸ شرکت توسعه فناوری اطلاعات حامی</span>
</div>
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 6.5C4 5 5 4 6.5 4h2c.6 0 1.1.4 1.3 1l1 3c.2.6 0 1.2-.5 1.6L9 11c1 2 2.5 3.5 4.5 4.5l1.4-1.3c.4-.4 1-.6 1.6-.4l3 1c.6.2 1 .7 1 1.3v2c0 1.5-1 2.5-2.5 2.4C10.5 20 4 13.5 4 6.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<a href="tel:02191550875" style="font-variant-numeric:tabular-nums" dir="ltr">02191550875</a>
</div>
<div class="fl">
<svg viewbox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="m4 7 8 5 8-5" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<a href="mailto:info@clinic-pro.ir" class="mono" dir="ltr">info@clinic-pro.ir</a>
</div>
</div>
</div>
<div class="footer-col" style="display:none">
<h4>دانلود نرم‌افزار</h4>
<div class="foot-badges">
<a class="store-badge" href="#" style="background:oklch(1 0 0 / 0.08)">
<svg viewbox="0 0 24 24" fill="currentColor"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.9-3.5.9-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.7 2.3-1.6 2.8-.4 6.9 1.1 9.2.8 1.1 1.6 2.3 2.8 2.3 1.1 0 1.6-.7 2.9-.7 1.4 0 1.7.7 2.9.7 1.2 0 2-1.1 2.7-2.2.9-1.2 1.2-2.5 1.2-2.5s-2.3-.9-2.3-3.6ZM14.2 5.7c.6-.8 1-1.8.9-2.9-.9 0-2 .6-2.6 1.4-.6.7-1.1 1.7-.9 2.7 1 .1 2-.5 2.6-1.2Z"/></svg>
<span class="sb-t">
<small>دانلود برای</small>
<b>مک · macOS</b>
</span>
</a>
<a class="store-badge" href="#" style="background:oklch(1 0 0 / 0.08)">
<svg viewbox="0 0 24 24" fill="currentColor"><path d="M3 5.5 10.5 4.4v7.1H3V5.5Zm0 13L10.5 19.6v-7H3v6Zm8.5 1.3L21 21V12.5h-9.5v7.3Zm0-15.6V11.5H21V3l-9.5 1.2Z"/></svg>
<span class="sb-t">
<small>دانلود برای</small>
<b>ویندوز</b>
</span>
</a>
</div>
</div>
</div>
</div>
<div class="footer-bottom">© ۲۰۲۴ کلینیک پرو · ساخته‌شده برای پزشکان ایران</div>
</footer>
<script>
(function () {
'use strict';
var FA = [
'۰',
'۱',
'۲',
'۳',
'۴',
'۵',
'۶',
'۷',
'۸',
'۹'
];
function groupFa(n) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '،').replace(/\d/g, function (d) {
return FA[+ d];
});
}
var header = document.getElementById('header');
function onScroll() {
header.classList.toggle('scrolled', window.scrollY > 12);
}
window.addEventListener('scroll', onScroll, {passive: true});
onScroll();
var navToggle = document.getElementById('navToggle');
var navLinks = document.getElementById('navLinks');
if (navToggle) {
navToggle.addEventListener('click', function () {
navLinks.classList.toggle('open');
});
navLinks.addEventListener('click', function (e) {
if (e.target.tagName === 'A')
navLinks.classList.remove('open');
});
}
var sections = [
'hero',
'features',
'why',
'download',
'contact'
];
var links = Array.prototype.slice.call(document.querySelectorAll('.nav-links a'));
function syncActive() {
var y = window.scrollY + 120,
cur = 'hero';
sections.forEach(function (id) {
var el = document.getElementById(id);
if (el && el.offsetTop <= y)
cur = id;
});
links.forEach(function (a) {
a.classList.toggle('active', a.getAttribute('href') === '#' + cur);
});
}
window.addEventListener('scroll', syncActive, {passive: true});
syncActive();
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var reveals = Array.prototype.slice.call(document.querySelectorAll('.reveal'));
function inView(el) {
var r = el.getBoundingClientRect();
return r.top<(window.innerHeight||document.documentElement.clientHeight)-30 && r.bottom> 0;
}
function reveal(el) {
el.classList.add('in');
}
if (reduce) {
reveals.forEach(reveal);
} else {
reveals.forEach(function (el) {
if (inView(el))
reveal(el);
});
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (es) {
es.forEach(function (e) {
if (e.isIntersecting) {
reveal(e.target);
io.unobserve(e.target);
}
});
}, {
threshold: 0.12,
rootMargin: '0px 0px -40px 0px'
});
reveals.forEach(function (el) {
if (! el.classList.contains('in'))
io.observe(el);
});
}
var ticking = false;
function sweep() {
if (ticking)
return;
ticking = true;
requestAnimationFrame(function () {
reveals.forEach(function (el) {
if (inView(el))
reveal(el);
});
ticking = false;
});
}
window.addEventListener('scroll', sweep, {passive: true});
window.addEventListener('resize', sweep, {passive: true});
setTimeout(function () {
reveals.forEach(reveal);
}, 2800);
}
// نماد اعتماد الکترونیکی (Enamad) — فقط بعد از لود کامل صفحه تزریق می‌شود تا
// درخواست تصویرِ دامنه‌ی بیرونی با منابع خود سایت رقابت نکند و سرعت لود پایین نیاید.
function mountEnamad() {
var slot = document.getElementById('enamad-seal');
if (! slot)
return;
var a = document.createElement('a');
a.setAttribute('referrerpolicy', 'origin');
a.target = '_blank';
a.href = 'https://trustseal.enamad.ir/?id=755841&Code=8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4';
var img = document.createElement('img');
img.setAttribute('referrerpolicy', 'origin');
img.src = 'https://trustseal.enamad.ir/logo.aspx?id=755841&Code=8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4';
img.alt = 'نماد اعتماد الکترونیکی';
img.style.cursor = 'pointer';
img.setAttribute('code', '8sNSk0yrUE7EgbY9k5xkqTE0LaeXIaq4');
img.loading = 'lazy';
a.appendChild(img);
slot.appendChild(a);
}
if (document.readyState === 'complete') {
mountEnamad();
} else {
window.addEventListener('load', mountEnamad);
}
})();
</script>
{{ include('public/_page_scripts.html.twig') }}
{{ encore_entry_script_tags('home') }}
</body>
+146
View File
@@ -0,0 +1,146 @@
{# قالب مشترک صفحات فرود. تم دقیقاً همان صفحهٔ اصلی است — همان entry وبپک، همان هدر و
فوتر و مودال. فقط متن و متادیتا از LandingPage می‌آید. #}
{% set base = app.request.schemeAndHttpHost %}
{% set url = base ~ '/' ~ page.slug %}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{{ page.metaTitle }}</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg"/>
<link rel="apple-touch-icon" href="/logo.svg"/>
<meta name="description" content="{{ page.metaDescription }}"/>
<meta name="keywords" content="{{ page.keywords }}"/>
<link rel="canonical" href="{{ url }}"/>
<meta name="robots" content="index, follow, max-image-preview:large"/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="کلینیک پرو"/>
<meta property="og:title" content="{{ page.metaTitle }}"/>
<meta property="og:description" content="{{ page.metaDescription }}"/>
<meta property="og:url" content="{{ url }}"/>
<meta property="og:image" content="{{ base }}/logo.svg"/>
<meta property="og:locale" content="fa_IR"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="{{ page.metaTitle }}"/>
<meta name="twitter:description" content="{{ page.metaDescription }}"/>
<meta name="twitter:image" content="{{ base }}/logo.svg"/>
{# JSON-LD با json_encode ساخته می‌شود نه رشتهٔ دستی: متن فارسی نقل‌قول و کاراکتر
کنترلی دارد و JSONِ دست‌ساز در Twig خیلی راحت نامعتبر می‌شود. #}
<script type="application/ld+json">
{{ {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'SoftwareApplication',
'@id': url ~ '#software',
'name': page.h1,
'applicationCategory': 'BusinessApplication',
'operatingSystem': 'Web',
'url': url,
'description': page.metaDescription,
'inLanguage': 'fa-IR',
'offers': {
'@type': 'Offer',
'price': '0',
'priceCurrency': 'IRR',
'description': 'پلن رایگان برای شروع'
},
'publisher': { '@type': 'Organization', 'name': 'کلینیک پرو', 'url': base ~ '/' }
},
{
'@type': 'BreadcrumbList',
'@id': url ~ '#breadcrumb',
'itemListElement': [
{ '@type': 'ListItem', 'position': 1, 'name': 'کلینیک پرو', 'item': base ~ '/' },
{ '@type': 'ListItem', 'position': 2, 'name': page.h1, 'item': url }
]
}
]|merge(page.faq is not empty ? [{
'@type': 'FAQPage',
'@id': url ~ '#faq',
'mainEntity': page.faq|map(item => {
'@type': 'Question',
'name': item.q,
'acceptedAnswer': { '@type': 'Answer', 'text': item.a }
})
}] : [])
}|json_encode(constant('JSON_UNESCAPED_UNICODE') b-or constant('JSON_UNESCAPED_SLASHES'))|raw }}
</script>
{{ encore_entry_link_tags('home') }}
</head>
<body>
{{ include('public/_header.html.twig') }}
<main>
<section class="hero" id="hero">
<div class="wrap hero-grid">
<div class="hero-copy">
<h1 class="reveal">{{ page.h1 }}</h1>
<p class="reveal d1">{{ page.lead }}</p>
<div class="hero-actions reveal d2">
{# همان idهایی که اسکریپت مودال به آن‌ها bind می‌شود. #}
<button class="btn btn-coral" id="openRegModal2">رایگان شروع کنید</button>
<a href="#contact" class="btn btn-blue">درخواست دمو</a>
</div>
</div>
{{ include('public/_hero_art.html.twig') }}
</div>
</section>
<section class="fcols" id="features">
<div class="wrap fcols-grid">
{% for feature in page.features %}
<div class="fcol reveal{% if loop.index0 % 3 == 1 %} d1{% elseif loop.index0 % 3 == 2 %} d2{% endif %}">
<h3>{{ feature.title }}</h3>
<p>{{ feature.body }}</p>
</div>
{% endfor %}
</div>
</section>
{% if page.faq is not empty %}
<section class="specs" id="faq">
<div class="wrap">
<h2 class="reveal">پرسش‌های متداول</h2>
<div class="fcols-grid">
{% for item in page.faq %}
<div class="fcol reveal">
<h3>{{ item.q }}</h3>
<p>{{ item.a }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{% if related is not empty %}
<section class="band" id="related">
<div class="wrap">
<h2 class="reveal">راهکارهای دیگر کلینیک پرو</h2>
<div class="fcols-grid">
{% for item in related %}
<div class="fcol reveal">
<h3><a href="/{{ item.slug }}">{{ item.h1 }}</a></h3>
<p>{{ item.metaDescription }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{{ include('public/_reg_modal.html.twig') }}
</main>
{{ include('public/_footer.html.twig') }}
{{ include('public/_page_scripts.html.twig') }}
{{ encore_entry_script_tags('home') }}
</body>
</html>