[Help] Add help/learning page + interactive guided app tour
CI/CD / CI · dotnet build (push) Successful in 46s
CI/CD / Deploy · hamkadr (push) Successful in 1m10s

New /Help page: quick-start, separate guides for کادر درمان (search/near-me/preferences/اعلام تمایل/saved) and مراکز درمانی (register/post/verify-with-docs/applicants), notifications+install, report/complaint, and an FAQ accordion. Self-hosted tour.js (no CDN, RTL): spotlights elements via data-tour hooks in the nav, auto-runs once for new visitors on the home page (localStorage flag), re-runnable from the Help page button or ?tour=1; skips steps whose target is hidden so it works on mobile/other pages. Help linked from nav + footer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 17:39:03 +03:30
parent 70bab6b916
commit 698565c460
4 changed files with 278 additions and 8 deletions
+18
View File
@@ -235,6 +235,24 @@ label { font-size: 13px; }
.footer-links a { color: var(--muted); }
.footer-links a:hover { color: var(--primary); }
/* ---------- Guided tour ---------- */
.tour-overlay { position: fixed; inset: 0; z-index: 1000; }
.tour-hole {
position: fixed; border-radius: 12px; pointer-events: none;
box-shadow: 0 0 0 9999px rgba(13,30,40,.62);
outline: 2px solid var(--accent); transition: all .2s ease;
}
.tour-bubble {
position: fixed; z-index: 1001; background: var(--surface); color: var(--text);
border-radius: 14px; box-shadow: 0 14px 40px rgba(0,0,0,.28);
padding: 16px; max-width: 320px;
}
.tour-title { font-weight: 800; font-size: 16px; margin-bottom: 6px; }
.tour-text { font-size: 14px; color: var(--muted); line-height: 1.9; }
.tour-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 14px; }
.tour-count { font-size: 12px; color: var(--muted); }
.tour-btns { display: flex; gap: 6px; }
/* Legal/policy pages (privacy, rules, terms) — comfortable long-form reading. */
.legal { line-height: 2; }
.legal h2 { font-size: 17px; margin: 22px 0 8px; color: var(--primary-dark); }
+142
View File
@@ -0,0 +1,142 @@
/* همکادر — lightweight guided tour (no external libs, RTL/Persian).
Spotlights elements marked with [data-tour]. Auto-runs once for new visitors on the
home page; re-runnable any time via window.hamkadrTour.start() or ?tour=1.
Steps whose target is missing/hidden are skipped, so it works across pages & mobile. */
(function () {
'use strict';
var DONE_KEY = 'hamkadr_tour_done_v1';
// Ordered steps. `sel` is a [data-tour] value; first matching visible element is used.
var STEPS = [
{ sel: 'home', title: 'به همکادر خوش آمدید 👋', text: 'سامانه‌ی یافتن شیفت و استخدام برای کادر درمان. این تور کوتاه بخش‌های اصلی را نشان می‌دهد.' },
{ sel: 'menu', title: 'منو', text: 'از این دکمه به همه‌ی بخش‌ها دسترسی داری.' },
{ sel: 'shifts', title: 'شیفت‌ها', text: 'فرصت‌های شیفت کاری را اینجا ببین و با فیلتر شهر/محله/نقش پیدا کن.' },
{ sel: 'jobs', title: 'استخدام', text: 'موقعیت‌های استخدامی تمام‌وقت، پاره‌وقت و طرح را اینجا مرور کن.' },
{ sel: 'prefs', title: 'علاقه‌مندی‌ها', text: 'نقش، شهر و نوع شیفتِ موردنظرت را تعیین کن تا پیشنهادهای متناسب بگیری.' },
{ sel: 'bell', title: 'اعلان‌ها', text: 'وقتی فرصت تازه‌ای مطابق علاقه‌ات منتشر شود، همین‌جا باخبر می‌شوی.' },
{ sel: 'login', title: 'ورود', text: 'با شماره موبایل وارد شو تا فرصت‌ها را ذخیره کنی و «اعلام تمایل» بدهی.' },
{ sel: 'panel', title: 'پنل تو', text: 'فرصت‌های ذخیره‌شده و درخواست‌هایت را در پنل کاربری دنبال کن.' },
{ sel: 'help', title: 'راهنما', text: 'هر زمان خواستی، راهنمای کامل و همین تور را از این بخش باز کن. موفق باشی! 🌟' }
];
var overlay, hole, bubble, idx, steps;
function visible(el) {
if (!el) return false;
var r = el.getBoundingClientRect();
return el.offsetParent !== null && r.width > 0 && r.height > 0;
}
function build() {
overlay = document.createElement('div'); overlay.className = 'tour-overlay';
hole = document.createElement('div'); hole.className = 'tour-hole';
bubble = document.createElement('div'); bubble.className = 'tour-bubble';
bubble.innerHTML =
'<div class="tour-title"></div>' +
'<div class="tour-text"></div>' +
'<div class="tour-foot">' +
'<span class="tour-count"></span>' +
'<span class="tour-btns">' +
'<button type="button" class="btn btn-outline btn-sm tour-skip">رد کردن</button>' +
'<button type="button" class="btn btn-outline btn-sm tour-prev">قبلی</button>' +
'<button type="button" class="btn btn-accent btn-sm tour-next">بعدی</button>' +
'</span>' +
'</div>';
overlay.appendChild(hole);
document.body.appendChild(overlay);
document.body.appendChild(bubble);
overlay.addEventListener('click', finish);
bubble.querySelector('.tour-skip').addEventListener('click', finish);
bubble.querySelector('.tour-prev').addEventListener('click', function () { go(idx - 1); });
bubble.querySelector('.tour-next').addEventListener('click', function () { go(idx + 1); });
window.addEventListener('resize', reposition);
window.addEventListener('scroll', reposition, true);
document.addEventListener('keydown', onKey);
}
function onKey(e) {
if (!overlay) return;
if (e.key === 'Escape') finish();
else if (e.key === 'ArrowLeft') go(idx + 1); // RTL: left = next
else if (e.key === 'ArrowRight') go(idx - 1);
}
function target() { return document.querySelector('[data-tour="' + steps[idx].sel + '"]'); }
function reposition() {
if (!overlay || idx == null) return;
var el = target();
if (!el) return;
var r = el.getBoundingClientRect(), pad = 6;
hole.style.top = (r.top - pad) + 'px';
hole.style.left = (r.left - pad) + 'px';
hole.style.width = (r.width + pad * 2) + 'px';
hole.style.height = (r.height + pad * 2) + 'px';
// place the bubble below the target if room, else above
var below = r.bottom + 12, bw = Math.min(320, window.innerWidth - 24);
bubble.style.width = bw + 'px';
var left = Math.min(Math.max(12, r.left + r.width / 2 - bw / 2), window.innerWidth - bw - 12);
bubble.style.left = left + 'px';
var bh = bubble.offsetHeight || 150;
if (below + bh > window.innerHeight - 8 && r.top - 12 - bh > 8) bubble.style.top = (r.top - 12 - bh) + 'px';
else bubble.style.top = below + 'px';
}
function render() {
var s = steps[idx];
bubble.querySelector('.tour-title').textContent = s.title;
bubble.querySelector('.tour-text').textContent = s.text;
bubble.querySelector('.tour-count').textContent = toFa((idx + 1) + ' / ' + steps.length);
bubble.querySelector('.tour-prev').style.visibility = idx === 0 ? 'hidden' : 'visible';
bubble.querySelector('.tour-next').textContent = idx === steps.length - 1 ? 'پایان' : 'بعدی';
}
function go(n) {
if (n < 0) return;
if (n >= steps.length) return finish();
idx = n;
var el = target();
if (el && el.scrollIntoView) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
render();
setTimeout(reposition, 60);
}
function finish() {
try { localStorage.setItem(DONE_KEY, '1'); } catch (e) {}
document.removeEventListener('keydown', onKey);
window.removeEventListener('resize', reposition);
window.removeEventListener('scroll', reposition, true);
if (overlay) overlay.remove();
if (bubble) bubble.remove();
overlay = bubble = null; idx = null;
}
function start() {
// recompute visible steps each run (depends on auth state / page / viewport)
steps = STEPS.filter(function (s) { return visible(document.querySelector('[data-tour="' + s.sel + '"]')); });
if (steps.length === 0) return;
if (overlay) finish();
build();
go(0);
}
function toFa(s) {
var d = ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'];
return String(s).replace(/[0-9]/g, function (c) { return d[+c]; });
}
window.hamkadrTour = { start: start, reset: function () { try { localStorage.removeItem(DONE_KEY); } catch (e) {} } };
function maybeAutoStart() {
var params = new URLSearchParams(location.search);
if (params.get('tour') === '1') { start(); return; }
var onHome = location.pathname === '/' || location.pathname === '';
var done = false; try { done = localStorage.getItem(DONE_KEY) === '1'; } catch (e) {}
if (onHome && !done) setTimeout(start, 900);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', maybeAutoStart);
else maybeAutoStart();
})();