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
+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>