Commit cf882849 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(proposal): replace static mockup with a working app prototype

The prototype in the proposal was eight inline <div>s toggled with
display:none — screenshots in a phone bezel. It now embeds a real
single-page app from app/, so the screens shown in the proposal are the
screens that ship, and the board can actually use it.

app/ — hash routed, so any screen is linkable (#/dues):
  splash, login, otp, home, dues, pay, paying, success, receipts, qr,
  invites, activities, activity/:id, schedule, notifications, profile

State is live, not scripted. Selecting dues recomputes the total before
paying; paying clears those dues, creates a receipt and posts a
notification; issuing an invite decrements the balance; subscribing to an
activity adds the subscription, consumes a place and generates the first
invoice into المستحقات. The QR regenerates every 60s against a countdown
ring. Dark/light theme persists. Verified end to end by driving the app in
headless Chrome, not just by rendering it.

Chose an SPA over the multi-page pattern used by the older Proposal/
prototype: no white flash between screens, real forward/back transitions,
and shared state across screens, which is the whole point of showing a
collection flow.

Proposal integration:
- Phone hosts <iframe src="app/">; the side list drives it over
  postMessage and the prototype reports its route back, so the list and
  the annotation stay in sync when someone navigates inside the phone
- Theme toggle, reset, and open-fullscreen controls
- Annotation panel rewritten per screen

Removed 409 lines of static screen markup and the 93 lines of CSS that
served it (.sbar/.app-hd/.mcard/.tabbar/.sport-item/.otp-row/.qr-*),
verified dead by checking real class= usage, not substring matches.

Adds .dockerignore: the image is built with `COPY . /usr/share/nginx/html`,
so any stray working file in this folder would be served publicly.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent ff106889
# The image is built with `COPY . /usr/share/nginx/html`, so anything left in
# this folder is served publicly. Keep working files and exports out of it.
_*.html
*.md
.DS_Store
/* ==========================================================================
نادي الصيد المصري — النموذج التفاعلي لتطبيق الأعضاء
Interactive prototype. Arabic-first, RTL, dark + light.
Logical properties throughout so the layout mirrors correctly.
========================================================================== */
:root{
--gold:#C9A227; --gold-2:#E8CC6A; --gold-3:#F3E2A6;
--mint:#16B981; --rose:#E0556B; --sky:#3E8CE0; --amber:#E8A33D;
--r-s:10px; --r-m:16px; --r-l:22px; --r-xl:28px;
--f-head:'Cairo',system-ui,sans-serif;
--f-body:'IBM Plex Sans Arabic',system-ui,sans-serif;
--tab-h:68px; --hd-h:56px;
--ease:cubic-bezier(.22,1,.36,1);
}
:root[data-theme="dark"]{
--bg:#070F1E; --bg-2:#0C1A31; --surface:#101F3A; --surface-2:#16294A;
--line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.16);
--txt:#F2F6FC; --txt-2:#A9BCD8; --txt-3:#7288A8;
--shadow:0 18px 44px rgba(0,0,0,.55);
--sheet:#0E1C33;
}
:root[data-theme="light"]{
--bg:#F4F6FA; --bg-2:#FFFFFF; --surface:#FFFFFF; --surface-2:#F0F3F9;
--line:#E4E9F2; --line-2:#D2DAE8;
--txt:#0C1A2E; --txt-2:#4C5D77; --txt-3:#8494AB;
--shadow:0 14px 34px rgba(12,26,46,.12);
--sheet:#FFFFFF;
}
*,*::before,*::after{ margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ height:100%; }
body{
font-family:var(--f-body); background:var(--bg); color:var(--txt);
line-height:1.7; font-size:15px; overflow:hidden;
-webkit-font-smoothing:antialiased;
transition:background .35s var(--ease), color .35s var(--ease);
}
h1,h2,h3,h4{ font-family:var(--f-head); font-weight:800; line-height:1.3; }
button{ font-family:inherit; border:none; background:none; color:inherit; cursor:pointer; }
input,select{ font-family:inherit; color:inherit; }
img{ display:block; max-width:100%; }
.num{ font-variant-numeric:tabular-nums; direction:ltr; unicode-bidi:isolate; display:inline-block; }
.ico{ width:1em; height:1em; fill:none; stroke:currentColor; stroke-width:1.8;
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
.sprite{ position:absolute; width:0; height:0; overflow:hidden; }
/* ================= APP SHELL ================= */
#app{ position:relative; height:100%; overflow:hidden; background:var(--bg); }
.screen{
position:absolute; inset:0; display:flex; flex-direction:column;
background:var(--bg); will-change:transform,opacity;
}
.screen-body{ flex:1; overflow-y:auto; overscroll-behavior:contain; -webkit-overflow-scrolling:touch; }
.screen-body::-webkit-scrollbar{ width:0; }
.pad{ padding:16px; }
.pad-b{ padding-bottom:26px; }
.has-tab .screen-body{ padding-bottom:calc(var(--tab-h) + 16px); }
/* transitions: forward = new slides in from inline-start */
@keyframes inFwd { from{ transform:translateX(var(--slide-in)); opacity:.4 } to{ transform:none; opacity:1 } }
@keyframes outFwd{ from{ transform:none; opacity:1 } to{ transform:translateX(var(--slide-out)); opacity:0 } }
@keyframes inBack { from{ transform:translateX(var(--slide-out)); opacity:.4 } to{ transform:none; opacity:1 } }
@keyframes outBack{ from{ transform:none; opacity:1 } to{ transform:translateX(var(--slide-in)); opacity:0 } }
@keyframes fadeIn{ from{opacity:0; transform:scale(.98)} to{opacity:1; transform:none} }
:root{ --slide-in:-100%; --slide-out:100%; } /* RTL: forward comes from the left */
.anim-in-fwd { animation:inFwd .34s var(--ease) both; }
.anim-out-fwd{ animation:outFwd .34s var(--ease) both; }
.anim-in-back { animation:inBack .34s var(--ease) both; }
.anim-out-back{ animation:outBack .34s var(--ease) both; }
.anim-fade{ animation:fadeIn .3s var(--ease) both; }
@media (prefers-reduced-motion:reduce){
.anim-in-fwd,.anim-out-fwd,.anim-in-back,.anim-out-back,.anim-fade{ animation-duration:.01s; }
}
/* ================= STATUS BAR + HEADER ================= */
.sbar{
height:38px; display:flex; align-items:flex-end; padding-bottom:5px; justify-content:space-between;
padding:0 18px; font-size:.7rem; font-weight:700; color:var(--txt);
flex-shrink:0; opacity:.9;
}
.sbar .dots{ display:flex; gap:3px; align-items:center; }
.sbar .dots i{ width:3px; height:9px; background:currentColor; border-radius:1px; display:block; }
.sbar .dots i:nth-child(1){ height:5px } .sbar .dots i:nth-child(2){ height:7px }
.sbar .bat{ width:18px; height:9px; border:1.2px solid currentColor; border-radius:2.5px; position:relative; }
.sbar .bat::after{ content:''; position:absolute; inset:1.5px; inset-inline-end:5px; background:currentColor; border-radius:1px; }
.hd{
height:var(--hd-h); flex-shrink:0; display:flex; align-items:center; gap:12px;
padding:0 14px; background:var(--bg); position:relative; z-index:5;
}
.hd h1{ font-size:1.02rem; flex:1; }
.hd .sub{ font-size:.7rem; color:var(--txt-3); font-weight:500; font-family:var(--f-body); }
.hd-btn{
width:38px; height:38px; border-radius:12px; display:grid; place-items:center;
background:var(--surface); border:1px solid var(--line); color:var(--txt);
font-size:1.05rem; transition:.2s; flex-shrink:0;
}
.hd-btn:active{ transform:scale(.92); }
.hd-btn .ico{ width:19px; height:19px; }
.hd-btn.plain{ background:none; border:none; }
.hd-badge{ position:absolute; top:6px; inset-inline-end:6px; width:7px; height:7px; border-radius:50%; background:var(--rose); }
/* ================= TAB BAR ================= */
.tabbar{
height:var(--tab-h); flex-shrink:0; display:flex; align-items:stretch;
background:var(--bg-2); border-top:1px solid var(--line);
position:absolute; inset-inline:0; bottom:0; z-index:20;
padding-bottom:2px;
}
.tabbar button{
flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
color:var(--txt-3); font-size:.63rem; font-weight:700; transition:color .2s;
}
.tabbar button .ico{ width:21px; height:21px; }
.tabbar button.on{ color:var(--gold); }
.tabbar button:active{ transform:scale(.94); }
.tabbar .dot{ width:4px; height:4px; border-radius:50%; background:var(--gold); margin-top:1px; opacity:0; }
.tabbar button.on .dot{ opacity:1; }
/* ================= CARDS / LISTS ================= */
.card{
background:var(--surface); border:1px solid var(--line); border-radius:var(--r-m);
padding:15px; margin-bottom:12px;
}
.card-t{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:10px; }
.card-t h3{ font-size:.9rem; }
.card-t .more{ font-size:.72rem; color:var(--gold); font-weight:700; }
.row{
display:flex; align-items:center; gap:12px; padding:12px 0;
border-bottom:1px solid var(--line);
}
.row:last-child{ border-bottom:none; }
.row .l{ flex:1; min-width:0; }
.row .l b{ display:block; font-size:.85rem; font-weight:700; }
.row .l span{ font-size:.72rem; color:var(--txt-3); }
.row .r{ text-align:end; flex-shrink:0; }
.ic-box{
width:40px; height:40px; border-radius:12px; display:grid; place-items:center;
background:var(--surface-2); color:var(--gold); flex-shrink:0;
}
.ic-box .ico{ width:20px; height:20px; }
.ic-box.mint{ background:rgba(22,185,129,.14); color:var(--mint); }
.ic-box.rose{ background:rgba(224,85,107,.14); color:var(--rose); }
.ic-box.sky{ background:rgba(62,140,224,.14); color:var(--sky); }
.chip{
display:inline-flex; align-items:center; gap:5px; font-size:.66rem; font-weight:700;
padding:3px 10px; border-radius:99px; background:var(--surface-2); color:var(--txt-2);
}
.chip.ok{ background:rgba(22,185,129,.15); color:var(--mint); }
.chip.due{ background:rgba(224,85,107,.15); color:var(--rose); }
.chip.soon{ background:rgba(232,163,61,.16); color:var(--amber); }
.chip.info{ background:rgba(62,140,224,.15); color:var(--sky); }
/* ================= BUTTONS ================= */
.btn{
width:100%; height:50px; border-radius:15px; font-family:var(--f-head);
font-weight:800; font-size:.92rem; display:flex; align-items:center;
justify-content:center; gap:8px; transition:transform .12s, box-shadow .2s;
}
.btn:active{ transform:scale(.975); }
.btn-gold{ background:linear-gradient(135deg,var(--gold-2),var(--gold)); color:#2A2003;
box-shadow:0 10px 26px rgba(201,162,39,.32); }
.btn-ghost{ background:var(--surface); border:1px solid var(--line-2); color:var(--txt); }
.btn-mint{ background:var(--mint); color:#04241A; }
.btn:disabled{ opacity:.4; box-shadow:none; }
.btn-sm{ height:40px; font-size:.8rem; border-radius:12px; width:auto; padding:0 18px; }
.segs{ display:flex; gap:4px; background:var(--surface-2); border-radius:13px; padding:4px; margin-bottom:14px; }
.segs button{ flex:1; height:34px; border-radius:10px; font-size:.76rem; font-weight:700; color:var(--txt-3); }
.segs button.on{ background:var(--surface); color:var(--txt); box-shadow:var(--shadow); }
:root[data-theme="light"] .segs button.on{ box-shadow:0 2px 8px rgba(12,26,46,.10); }
.chips{ display:flex; gap:7px; overflow-x:auto; padding-bottom:4px; margin-bottom:12px; scrollbar-width:none; }
.chips::-webkit-scrollbar{ display:none; }
.chips button{
flex-shrink:0; padding:7px 15px; border-radius:99px; font-size:.75rem; font-weight:700;
background:var(--surface); border:1px solid var(--line); color:var(--txt-2);
}
.chips button.on{ background:var(--gold); border-color:var(--gold); color:#2A2003; }
/* ================= MEMBERSHIP CARD ================= */
.mcard{
position:relative; overflow:hidden; border-radius:var(--r-l); padding:18px;
background:linear-gradient(140deg,#123566,#0A1C38 60%,#16407A);
color:#fff; box-shadow:var(--shadow); margin-bottom:14px;
}
.mcard::after{
content:''; position:absolute; inset-inline-end:-42px; top:-52px; width:180px; height:180px;
border-radius:50%; background:radial-gradient(circle,rgba(232,204,106,.30),transparent 68%);
}
.mcard::before{
content:''; position:absolute; inset-inline-start:-14%; bottom:-58%;
width:190px; aspect-ratio:1240/1754; opacity:.13;
background:url(../../assets/club-logo.png) center/contain no-repeat;
filter:grayscale(1) sepia(1) saturate(2.4) brightness(1.2);
}
.mcard .in{ position:relative; z-index:2; }
.mcard .who{ font-family:var(--f-head); font-weight:800; font-size:1rem; }
.mcard .meta{ font-size:.72rem; color:#B6C9E4; margin-top:2px; }
.mcard .idno{
font-family:var(--f-head); font-size:1.5rem; font-weight:900; color:var(--gold-2);
letter-spacing:.14em; margin-top:14px;
}
/* ================= BIG BALANCE ================= */
.bal{ text-align:center; padding:8px 0 4px; }
.bal .lbl{ font-size:.76rem; color:var(--txt-3); }
.bal .amt{ font-family:var(--f-head); font-weight:900; font-size:2.5rem; line-height:1.15; color:var(--rose); }
.bal .amt.zero{ color:var(--mint); }
.bal .cur{ font-size:.85rem; color:var(--txt-3); font-weight:600; }
/* ================= SELECTABLE DUE ROW ================= */
.due{
display:flex; gap:12px; align-items:flex-start; background:var(--surface);
border:1px solid var(--line); border-radius:var(--r-m); padding:13px;
margin-bottom:10px; transition:border-color .2s, background .2s;
border-inline-start:3px solid var(--rose);
}
.due.sel{ border-color:var(--gold); background:linear-gradient(var(--surface),var(--surface)) padding-box; }
.due.soon{ border-inline-start-color:var(--amber); }
.due .box{
width:22px; height:22px; border-radius:7px; border:1.8px solid var(--line-2);
display:grid; place-items:center; flex-shrink:0; margin-top:2px; transition:.2s;
}
.due.sel .box{ background:var(--gold); border-color:var(--gold); color:#2A2003; }
.due .box .ico{ width:13px; height:13px; stroke-width:3; opacity:0; }
.due.sel .box .ico{ opacity:1; }
.due .mid{ flex:1; min-width:0; }
.due .mid b{ font-size:.86rem; display:block; }
.due .mid span{ font-size:.71rem; color:var(--txt-3); display:block; margin-top:1px; }
.due .val{ font-family:var(--f-head); font-weight:800; font-size:.95rem; white-space:nowrap; }
.paybar{
position:absolute; inset-inline:0; bottom:0; z-index:25;
background:var(--bg-2); border-top:1px solid var(--line);
padding:12px 16px calc(12px + env(safe-area-inset-bottom));
display:flex; align-items:center; gap:14px;
animation:slideUp .3s var(--ease) both;
}
@keyframes slideUp{ from{ transform:translateY(100%) } to{ transform:none } }
.paybar .t{ flex:1; }
.paybar .t span{ font-size:.68rem; color:var(--txt-3); display:block; }
.paybar .t b{ font-family:var(--f-head); font-size:1.15rem; }
.paybar .btn{ width:auto; padding:0 26px; }
/* ================= QR ================= */
.qr-wrap{ display:grid; place-items:center; padding:10px 0 4px; }
.qr-card{
background:#fff; border-radius:var(--r-l); padding:16px; box-shadow:var(--shadow);
position:relative;
}
.qr-grid{ display:grid; grid-template-columns:repeat(21,1fr); width:196px; height:196px; }
.qr-grid i{ background:#0A1428; border-radius:.5px; }
.qr-grid i.off{ background:transparent; }
.ring{ transform:rotate(-90deg); }
.ring circle{ fill:none; stroke-width:3.4; stroke-linecap:round; }
.ring .bg{ stroke:var(--line); }
.ring .fg{ stroke:var(--gold); transition:stroke-dashoffset 1s linear; }
/* ================= INVITES ================= */
.inv-ring{ position:relative; width:132px; height:132px; margin:6px auto 12px; }
.inv-ring .n{
position:absolute; inset:0; display:grid; place-items:center; text-align:center;
font-family:var(--f-head); font-weight:900; font-size:2rem; line-height:1;
}
.inv-ring .n small{ display:block; font-size:.62rem; color:var(--txt-3); font-weight:600; margin-top:3px; }
.stepper{ display:flex; align-items:center; gap:16px; justify-content:center; margin:14px 0; }
.stepper button{
width:42px; height:42px; border-radius:14px; background:var(--surface-2);
border:1px solid var(--line); font-size:1.3rem; font-weight:700; display:grid; place-items:center;
}
.stepper button:active{ transform:scale(.9); }
.stepper .v{ font-family:var(--f-head); font-size:1.9rem; font-weight:900; min-width:52px; text-align:center; }
/* ================= SHEET / MODAL ================= */
.scrim{
position:absolute; inset:0; background:rgba(3,9,20,.62); z-index:60;
animation:fadeIn .22s var(--ease) both; backdrop-filter:blur(2px);
}
.sheet{
position:absolute; inset-inline:0; bottom:0; z-index:61; background:var(--sheet);
border-radius:var(--r-xl) var(--r-xl) 0 0; padding:8px 16px calc(20px + env(safe-area-inset-bottom));
max-height:82%; overflow-y:auto; animation:sheetUp .32s var(--ease) both;
box-shadow:0 -12px 40px rgba(0,0,0,.4);
}
@keyframes sheetUp{ from{ transform:translateY(100%) } to{ transform:none } }
.sheet .grab{ width:38px; height:4px; border-radius:99px; background:var(--line-2); margin:6px auto 14px; }
.sheet h3{ font-size:1rem; margin-bottom:4px; }
.sheet .sub{ font-size:.76rem; color:var(--txt-3); margin-bottom:14px; }
/* ================= TOAST ================= */
.toast{
position:absolute; inset-inline:16px; bottom:calc(var(--tab-h) + 16px); z-index:90;
background:var(--surface-2); border:1px solid var(--line-2); border-radius:14px;
padding:12px 14px; display:flex; align-items:center; gap:10px; font-size:.8rem;
box-shadow:var(--shadow); animation:toastIn .3s var(--ease) both;
}
@keyframes toastIn{ from{ transform:translateY(18px); opacity:0 } to{ transform:none; opacity:1 } }
.toast.out{ animation:toastOut .3s var(--ease) both; }
@keyframes toastOut{ to{ transform:translateY(18px); opacity:0 } }
.toast .ico{ width:18px; height:18px; color:var(--mint); }
/* ================= SPINNER / SUCCESS ================= */
.spin{ width:46px; height:46px; border-radius:50%; border:3px solid var(--line);
border-top-color:var(--gold); animation:sp .8s linear infinite; margin:0 auto; }
@keyframes sp{ to{ transform:rotate(360deg) } }
.tick-big{
width:84px; height:84px; border-radius:50%; background:rgba(22,185,129,.14);
display:grid; place-items:center; margin:0 auto 18px; color:var(--mint);
animation:pop .45s var(--ease) both;
}
.tick-big .ico{ width:42px; height:42px; stroke-width:2.4; }
@keyframes pop{ 0%{ transform:scale(.4); opacity:0 } 60%{ transform:scale(1.08) } 100%{ transform:scale(1); opacity:1 } }
/* ================= LOGIN / OTP ================= */
.brandwrap{ text-align:center; padding:24px 0 12px; }
.brandwrap img{ height:88px; margin:0 auto 14px; filter:drop-shadow(0 10px 26px rgba(0,0,0,.5)); }
.brandwrap h2{ font-size:1.15rem; }
.brandwrap p{ font-size:.78rem; color:var(--txt-3); margin-top:5px; }
.phone-in{
display:flex; align-items:center; gap:10px; direction:ltr;
background:var(--surface); border:1px solid var(--line); border-radius:15px;
padding:14px 16px; margin:20px 0 8px;
}
.phone-in .flag{ width:22px; height:15px; border-radius:2px; overflow:hidden; flex-shrink:0; }
.phone-in .cc{ font-size:.86rem; color:var(--txt-2); font-weight:600; }
.phone-in .sep{ width:1px; height:18px; background:var(--line-2); }
.phone-in .val{ font-size:1rem; font-weight:700; letter-spacing:.05em; flex:1; }
.phone-in .cursor{ display:inline-block; width:2px; height:17px; background:var(--gold);
vertical-align:-3px; animation:blink 1.05s step-end infinite; }
@keyframes blink{ 50%{ opacity:0 } }
.otp{ display:flex; gap:11px; justify-content:center; direction:ltr; margin:22px 0 12px; }
.otp i{
width:52px; height:60px; border-radius:15px; background:var(--surface);
border:1.6px solid var(--line); display:grid; place-items:center;
font-family:var(--f-head); font-size:1.5rem; font-weight:800; font-style:normal;
}
.otp i.fill{ border-color:var(--gold); color:var(--gold-2); }
.keypad{ display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:auto; padding-top:14px; }
.keypad button{
height:52px; border-radius:15px; background:var(--surface); border:1px solid var(--line);
font-family:var(--f-head); font-size:1.25rem; font-weight:800;
}
.keypad button:active{ background:var(--surface-2); transform:scale(.95); }
.keypad button.fn{ background:transparent; border-color:transparent; font-size:1rem; color:var(--txt-3); }
/* ================= MISC ================= */
.empty{ text-align:center; padding:40px 20px; color:var(--txt-3); }
.empty .ico{ width:40px; height:40px; margin:0 auto 12px; opacity:.5; }
.empty p{ font-size:.82rem; }
.bar{ height:7px; border-radius:99px; background:var(--surface-2); overflow:hidden; }
.bar i{ display:block; height:100%; background:linear-gradient(90deg,var(--gold),var(--gold-2)); border-radius:99px;
transition:width .5s var(--ease); }
.sec-t{ font-size:.78rem; font-weight:800; color:var(--txt-3); margin:16px 0 8px; }
.tt-day{ font-size:.74rem; font-weight:800; color:var(--gold); margin:14px 0 6px; }
.grid2{ display:grid; grid-template-columns:1fr 1fr; gap:9px; }
.qa{
background:var(--surface); border:1px solid var(--line); border-radius:15px;
padding:13px 8px; text-align:center; transition:.18s;
}
.qa:active{ transform:scale(.95); }
.qa .ico{ width:21px; height:21px; color:var(--gold); margin:0 auto 6px; }
.qa span{ font-size:.68rem; font-weight:700; color:var(--txt-2); }
/* Long titles must not widen the shell — the phone viewport is fixed. */
.tr{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.screen-body{ overflow-x:hidden; }
.hd .sub{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.row .l b, .row .l span{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.due .mid b{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>نادي الصيد المصري — نموذج تطبيق الأعضاء</title>
<meta name="theme-color" content="#070F1E">
<link rel="icon" href="../assets/club-logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700;800;900&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/app.css">
<style>
.ico.flip{ width:16px;height:16px;color:var(--txt-3);transform:scaleX(-1) }
.ico.tiny{ width:12px;height:12px;stroke-width:3 }
.ico.big{ width:30px;height:30px }
.flagsvg{ width:22px;height:15px;stroke:none }
</style>
</head>
<body>
<svg class="sprite" aria-hidden="true"><defs>
<symbol id="i-home" viewBox="0 0 24 24"><path d="M3.8 10.4 12 3.8l8.2 6.6"/><path d="M6 9.6V20.2h12V9.6"/><path d="M9.9 20.2v-5h4.2v5"/></symbol>
<symbol id="i-activity" viewBox="0 0 24 24"><path d="M4 9v6M7.2 6.8v10.4M16.8 6.8v10.4M20 9v6M7.2 12h9.6"/></symbol>
<symbol id="i-gate" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.3"/><path d="M13.5 14.2h3v3h-3zM19.4 14.2h1.1M13.5 20.4h3M19.6 18.6v1.8"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="3.6"/><path d="M4.6 20.2c0-3.9 3.4-6 7.4-6s7.4 2.1 7.4 6"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M6.2 9.4a5.8 5.8 0 0 1 11.6 0c0 3.9 1.5 5.4 1.5 5.4H4.7s1.5-1.5 1.5-5.4Z"/><path d="M10 18.1a2.2 2.2 0 0 0 4 0"/></symbol>
<symbol id="i-mail" viewBox="0 0 24 24"><rect x="2.8" y="5.4" width="18.4" height="13.2" rx="2.2"/><path d="m3.8 7.1 7.3 5.3a1.6 1.6 0 0 0 1.8 0l7.3-5.3"/></symbol>
<symbol id="i-receipt" viewBox="0 0 24 24"><path d="M6 3.6h12v16.8l-2.4-1.6-2.4 1.6-2.4-1.6-2.4 1.6Z"/><path d="M9.4 8.6h5.2M9.4 12.6h5.2"/></symbol>
<symbol id="i-card" viewBox="0 0 24 24"><rect x="2.6" y="5" width="18.8" height="14" rx="2.4"/><path d="M2.6 9.8h18.8"/><path d="M6.2 14.6h3.6"/></symbol>
<symbol id="i-wallet" viewBox="0 0 24 24"><rect x="6.6" y="2.6" width="10.8" height="18.8" rx="2.4"/><path d="M10.6 5.7h2.8M11 18.4h2"/></symbol>
<symbol id="i-bank" viewBox="0 0 24 24"><path d="M3.6 9.6 12 4.2l8.4 5.4"/><path d="M5.8 10.8v6.8M10 10.8v6.8M14 10.8v6.8M18.2 10.8v6.8"/><path d="M3.6 20.2h16.8"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12.5 4.6 4.5L19 7.5"/></symbol>
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24"><path d="M20 12H8.5"/><path d="m12.5 7-5 5 5 5"/><path d="M4 7v10"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 4.4v11.8"/><path d="m7.6 11.8 4.4 4.4 4.4-4.4"/><path d="M4.6 15v3.4a2 2 0 0 0 2 2h10.8a2 2 0 0 0 2-2V15"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M12 6.9v5.4l3.4 2"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20.1 12a8.1 8.1 0 1 1-2.6-6"/><path d="M20.3 4.3v4.4h-4.4"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14.3A8.6 8.6 0 0 1 9.7 3.5a8.6 8.6 0 1 0 10.8 10.8Z"/></symbol>
<symbol id="i-swim" viewBox="0 0 24 24"><circle cx="15.6" cy="6.4" r="2"/><path d="M4.6 13.2 9.2 10l4 3.1"/><path d="M2.6 17.9c1.7 0 1.7 1.5 3.5 1.5s1.7-1.5 3.5-1.5 1.7 1.5 3.5 1.5 1.7-1.5 3.5-1.5 1.7 1.5 3.4 1.5"/></symbol>
<symbol id="i-football" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="m12 7.6 3.5 2.5-1.3 4.1H9.8l-1.3-4.1Z"/><path d="M12 3.6v4M19.6 9.7l-3.8.4M17 18.9l-2.8-4.7M7 18.9l2.8-4.7M4.4 9.7l3.8.4"/></symbol>
<symbol id="i-karate" viewBox="0 0 24 24"><path d="M3.2 9.4h17.6v4H3.2z"/><path d="m10 13.6-1.4 6.2 3.4-2.2 3.4 2.2-1.4-6.2"/></symbol>
<symbol id="i-tennis" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M6.1 5.6c2.5 2 3.8 4.6 3.8 7.9 0 1.6-.3 3.2-1 4.7"/><path d="M17.9 5.6c-2.5 2-3.8 4.6-3.8 7.9 0 1.6.3 3.2 1 4.7"/></symbol>
<symbol id="i-gym" viewBox="0 0 24 24"><circle cx="12" cy="4.8" r="2.1"/><path d="M12 7.3v6.3"/><path d="m6.6 9.6 5.4 1.6 5.4-1.6"/><path d="m9.1 20.4 2.9-6.8 2.9 6.8"/></symbol>
<symbol id="i-flag-eg" viewBox="0 0 24 16">
<rect x="0" y="0" width="24" height="5.33" fill="#CE1126"/>
<rect x="0" y="5.33" width="24" height="5.34" fill="#F5F5F5"/>
<rect x="0" y="10.67" width="24" height="5.33" fill="#0B0B0B"/>
</symbol>
</defs></svg>
<div id="app"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
</body>
</html>
/* ==========================================================================
Interactive prototype — router, state and screens.
Hash routed so any screen is linkable (#/dues) and the proposal page can
drive it over postMessage. State is in-memory: a demo session mutates it
(paying clears dues, subscribing adds an invoice) and a reload restores
the scripted starting point, which is what a demo wants.
========================================================================== */
(function () {
'use strict';
// ---------- helpers ----------
const $ = (s, r = document) => r.querySelector(s);
const fmt = n => Number(n).toLocaleString('en-US');
const ico = (n, cls = '') => `<svg class="ico ${cls}"><use href="#${n}"/></svg>`;
const app = $('#app');
const clone = o => JSON.parse(JSON.stringify(o));
let S = clone(window.DB); // live demo state
let stack = []; // route history for back-direction
let current = null;
// ---------- theme ----------
function theme(next) {
const t = next || localStorage.getItem('sayd-theme') || 'dark';
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('sayd-theme', t);
}
function toggleTheme() {
theme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
render();
}
// ---------- derived ----------
const totalDue = () => S.dues.reduce((a, d) => a + d.amount, 0);
const invLeft = () => S.invites.free + S.invites.bought - S.invites.used;
const invTotal = () => S.invites.free + S.invites.bought;
const actById = id => S.activities.find(a => a.id === id);
const unreadCount= () => S.notifications.filter(n => n.unread).length;
// ---------- chrome ----------
function statusbar(light) {
return `<div class="sbar">
<span class="num">9:41</span>
<span style="display:flex;align-items:center;gap:6px;">
<span class="dots"><i></i><i></i><i></i></span><span class="bat"></span>
</span>
</div>`;
}
function header(o = {}) {
const { back, title, sub, right = '' } = o;
if (back) {
return `<div class="hd">
<button class="hd-btn" data-back>${ico('i-chev')}</button>
<div style="flex:1;min-width:0;">
<h1 style="font-size:.98rem" class="tr">${title || ''}</h1>
${sub ? `<div class="sub tr">${sub}</div>` : ''}
</div>${right}
</div>`;
}
return `<div class="hd">
<button class="hd-btn plain" data-go="#/notifications" style="position:relative">
${ico('i-bell')}${unreadCount() ? '<span class="hd-badge"></span>' : ''}
</button>
<div style="flex:1;min-width:0;text-align:end">
<h1 style="font-size:.92rem" class="tr">${title || 'نادي الصيد المصري'}</h1>
${sub ? `<div class="sub tr">${sub}</div>` : ''}
</div>
<img src="../assets/club-logo.png" alt="" style="height:34px;flex-shrink:0">
</div>`;
}
function tabbar(active) {
const t = [
['home', 'i-home', 'الرئيسية'],
['activities', 'i-activity', 'الأنشطة'],
['qr', 'i-gate', 'الدخول'],
['profile', 'i-user', 'حسابي']
];
return `<nav class="tabbar">${t.map(([r, i, l]) => `
<button data-go="#/${r}" class="${active === r ? 'on' : ''}">
${ico(i)}<span>${l}</span><span class="dot"></span>
</button>`).join('')}</nav>`;
}
// ---------- overlays ----------
function toast(msg, icon = 'i-check') {
const old = $('.toast'); if (old) old.remove();
const t = document.createElement('div');
t.className = 'toast';
t.innerHTML = `${ico(icon)}<span>${msg}</span>`;
app.appendChild(t);
setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 300); }, 2400);
}
function closeSheet() {
const s = $('.sheet'), c = $('.scrim');
if (s) { s.style.animation = 'sheetUp .25s var(--ease) reverse both'; setTimeout(() => s.remove(), 240); }
if (c) c.remove();
}
function sheet(html, onMount) {
closeSheet();
const scrim = document.createElement('div');
scrim.className = 'scrim'; scrim.onclick = closeSheet;
const el = document.createElement('div');
el.className = 'sheet';
el.innerHTML = `<div class="grab"></div>${html}`;
app.appendChild(scrim); app.appendChild(el);
bindAll(el);
if (onMount) onMount(el);
}
// ---------- QR bitmap ----------
function qrCells(seed) {
const n = 21; let s = seed || 7; const out = [];
for (let i = 0; i < n * n; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
const c = i % n, r = (i / n) | 0;
const finder = (c < 7 && r < 7) || (c > n - 8 && r < 7) || (c < 7 && r > n - 8);
let on;
if (finder) {
const cc = c > n - 8 ? c - (n - 7) : c, rr = r > n - 8 ? r - (n - 7) : r;
const ring = Math.max(Math.abs(cc - 3), Math.abs(rr - 3));
on = ring !== 2;
} else on = (s % 100) > 47;
out.push(on);
}
return out.map(o => `<i class="${o ? '' : 'off'}"></i>`).join('');
}
// ==========================================================
// SCREENS
// ==========================================================
const SCREENS = {};
SCREENS.splash = () => ({
body: `<div style="flex:1;display:grid;place-items:center;text-align:center;
background:linear-gradient(165deg,#0C2247,#070F1E);">
<div class="anim-fade">
<img src="../assets/club-logo.png" style="height:120px;margin:0 auto 20px;
filter:drop-shadow(0 14px 34px rgba(0,0,0,.6))">
<h2 style="color:#fff;font-size:1.2rem">نادي الصيد المصري</h2>
<p style="color:#8FA6C4;font-size:.8rem;margin-top:4px">تطبيق الأعضاء</p>
<div class="spin" style="margin-top:26px;width:26px;height:26px;border-width:2.4px"></div>
</div></div>`,
mount() { setTimeout(() => { if (current === 'splash') go('#/login', true); }, 1300); }
});
SCREENS.login = () => {
let val = '';
return {
body: `${statusbar()}<div class="screen-body pad" style="display:flex;flex-direction:column">
<div class="brandwrap">
<img src="../assets/club-logo.png" alt="">
<h2>أهلًا بك في نادي الصيد</h2>
<p>أدخل رقم الموبايل المسجَّل في بيانات عضويتك<br>وسيصلك كود تحقق برسالة قصيرة</p>
</div>
<div class="phone-in">
<span class="flag">${ico('i-flag-eg', 'flagsvg')}</span>
<span class="cc">+20</span><span class="sep"></span>
<span class="val" id="pv"><span class="cursor"></span></span>
</div>
<p style="font-size:.7rem;color:var(--txt-3);text-align:center">
جرّب الرقم المسجَّل: <b class="num">100 123 4567</b></p>
<div class="keypad" id="kp">
${[1,2,3,4,5,6,7,8,9].map(n => `<button data-k="${n}">${n}</button>`).join('')}
<button class="fn" data-k="fill">تعبئة</button>
<button data-k="0">0</button>
<button class="fn" data-k="del">${ico('i-back')}</button>
</div>
<div style="padding-top:14px">
<button class="btn btn-gold" id="send" disabled>إرسال كود التحقق</button>
<p style="font-size:.66rem;color:var(--txt-3);text-align:center;margin-top:12px;line-height:1.7">
بالدخول أنت توافق على لائحة النادي وشروط استخدام التطبيق</p>
</div></div>`,
mount(root) {
const pv = $('#pv', root), send = $('#send', root);
const paint = () => {
const d = val.replace(/(\d{3})(\d{3})(\d{0,4})/, '$1 $2 $3').trim();
pv.innerHTML = `${d}<span class="cursor"></span>`;
send.disabled = val.length !== 10;
};
$('#kp', root).addEventListener('click', e => {
const b = e.target.closest('[data-k]'); if (!b) return;
const k = b.dataset.k;
if (k === 'del') val = val.slice(0, -1);
else if (k === 'fill') val = '1001234567';
else if (val.length < 10) val += k;
paint();
});
send.onclick = () => go('#/otp');
paint();
}
};
};
SCREENS.otp = () => {
let code = '';
return {
body: `${statusbar()}${header({ back: true, title: 'كود التحقق' })}
<div class="screen-body pad" style="display:flex;flex-direction:column">
<div style="text-align:center;padding-top:8px">
<div class="ic-box" style="width:56px;height:56px;margin:0 auto 14px;border-radius:18px">
${ico('i-mail')}</div>
<h2 style="font-size:1.05rem">أدخل الكود المرسل</h2>
<p style="font-size:.78rem;color:var(--txt-3);margin-top:5px">
أرسلنا كودًا من ٤ أرقام إلى <b class="num">+20 100 123 4567</b></p>
</div>
<div class="otp" id="otp">${[0,1,2,3].map(() => '<i></i>').join('')}</div>
<p style="font-size:.72rem;color:var(--txt-3);text-align:center">
لم يصلك الكود؟ إعادة الإرسال خلال <b class="num" id="cd">00:23</b></p>
<div class="keypad" id="kp">
${[1,2,3,4,5,6,7,8,9].map(n => `<button data-k="${n}">${n}</button>`).join('')}
<button class="fn" data-k="fill">تعبئة</button>
<button data-k="0">0</button>
<button class="fn" data-k="del">${ico('i-back')}</button>
</div>
<div style="padding-top:14px">
<button class="btn btn-gold" id="ok" disabled>تأكيد ودخول</button></div></div>`,
mount(root) {
const cells = [...root.querySelectorAll('#otp i')], ok = $('#ok', root);
const paint = () => {
cells.forEach((c, i) => { c.textContent = code[i] || ''; c.classList.toggle('fill', !!code[i]); });
ok.disabled = code.length !== 4;
};
$('#kp', root).addEventListener('click', e => {
const b = e.target.closest('[data-k]'); if (!b) return;
const k = b.dataset.k;
if (k === 'del') code = code.slice(0, -1);
else if (k === 'fill') code = '4921';
else if (code.length < 4) code += k;
paint();
if (code.length === 4) setTimeout(() => go('#/home', true), 260);
});
ok.onclick = () => go('#/home', true);
let t = 23; const cd = $('#cd', root);
const iv = setInterval(() => {
if (!document.body.contains(cd)) return clearInterval(iv);
t--; cd.textContent = '00:' + String(Math.max(t, 0)).padStart(2, '0');
if (t <= 0) { clearInterval(iv); cd.parentElement.innerHTML = '<b style="color:var(--gold)">إعادة إرسال الكود</b>'; }
}, 1000);
paint();
}
};
};
SCREENS.home = () => {
const due = totalDue(), late = S.dues.some(d => d.state === 'late');
return {
tab: 'home',
body: `${statusbar()}${header({ title: 'صباح الخير، ' + S.member.short, sub: S.member.type })}
<div class="screen-body pad">
<div class="mcard"><div class="in">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
<div><div class="who">${S.member.name}</div>
<div class="meta">${S.member.type} — سارية حتى <span class="num">${S.member.validUntil}</span></div></div>
<span class="chip ok">سارية</span></div>
<div class="idno num">${S.member.id}</div>
</div></div>
${due > 0 ? `
<div class="card" style="border-color:rgba(224,85,107,.34)">
<div class="card-t"><h3>إجمالي المستحق عليك الآن</h3>
${late ? '<span class="chip due">متأخر</span>' : ''}</div>
<div style="font-family:var(--f-head);font-size:1.85rem;font-weight:900;color:var(--rose);line-height:1.15">
<span class="num">${fmt(due)}</span> <span style="font-size:.8rem;color:var(--txt-3)">ج.م</span></div>
<p style="font-size:.72rem;color:var(--txt-3);margin:2px 0 12px">
${S.dues.length} بنود مستحقة</p>
<button class="btn btn-gold" data-go="#/dues">عرض المستحقات والسداد</button>
</div>` : `
<div class="card" style="border-color:rgba(22,185,129,.34);text-align:center">
<div class="tick-big" style="width:56px;height:56px;margin-bottom:10px">${ico('i-check')}</div>
<h3 style="font-size:.95rem">لا مستحقات عليك</h3>
<p style="font-size:.75rem;color:var(--txt-3);margin-top:3px">حسابك مسدَّد بالكامل</p>
</div>`}
<div class="grid2" style="margin-bottom:12px">
<button class="qa" data-go="#/qr">${ico('i-gate')}<span>كود الدخول</span></button>
<button class="qa" data-go="#/invites">${ico('i-mail')}<span>الدعوات</span></button>
<button class="qa" data-go="#/activities">${ico('i-activity')}<span>الأنشطة</span></button>
<button class="qa" data-go="#/receipts">${ico('i-receipt')}<span>الإيصالات</span></button>
</div>
<div class="card">
<div class="card-t"><h3>اشتراكات أبنائك</h3>
<span class="more" data-go="#/activities">الكل</span></div>
${S.subs.length ? S.subs.map(s => { const a = actById(s.actId); return `
<div class="row" data-go="#/schedule">
<div class="ic-box sky">${ico(a ? a.icon : 'i-activity')}</div>
<div class="l"><b>${s.kid}</b><span>${a ? a.name : ''}${s.group}</span></div>
<div class="r"><span class="chip ${s.state === 'نشط' ? 'ok' : 'soon'}">${s.state}</span></div>
</div>`; }).join('') : '<p style="font-size:.78rem;color:var(--txt-3)">لا اشتراكات حالية</p>'}
</div>
<div class="card">
<div class="card-t"><h3>رصيد الدعوات هذا الشهر</h3>
<span class="more" data-go="#/invites">إدارة</span></div>
<div style="display:flex;align-items:center;gap:12px">
<div class="bar" style="flex:1"><i style="width:${(invLeft() / invTotal()) * 100}%"></i></div>
<b style="font-size:.82rem"><span class="num">${invLeft()}</span> من <span class="num">${invTotal()}</span></b>
</div>
</div>
</div>${tabbar('home')}`
};
};
SCREENS.dues = () => {
let sel = new Set(S.dues.map(d => d.id));
return {
tab: 'home',
body: `${statusbar()}${header({ back: true, title: 'المستحقات', sub: `${S.dues.length} بنود` })}
<div class="screen-body pad" id="db"></div>
<div id="pb"></div>`,
mount(root) {
const box = $('#db', root), bar = $('#pb', root);
const paint = () => {
if (!S.dues.length) {
box.innerHTML = `<div class="empty">${ico('i-check')}<p>لا مستحقات على حسابك</p></div>`;
bar.innerHTML = ''; return;
}
box.innerHTML = S.dues.map(d => `
<div class="due ${sel.has(d.id) ? 'sel' : ''} ${d.state === 'soon' ? 'soon' : ''}" data-id="${d.id}">
<div class="box">${ico('i-check')}</div>
<div class="mid"><b>${d.title}</b><span>${d.sub}</span>
<span>تاريخ الاستحقاق: <span class="num">${d.due}</span></span></div>
<div style="text-align:end">
<div class="val" style="color:${d.state === 'late' ? 'var(--rose)' : 'var(--txt)'}">
<span class="num">${fmt(d.amount)}</span></div>
<span class="chip ${d.state === 'late' ? 'due' : 'soon'}" style="margin-top:5px">
${d.state === 'late' ? 'متأخر' : 'يستحق قريبًا'}</span></div>
</div>`).join('');
const tot = S.dues.filter(d => sel.has(d.id)).reduce((a, d) => a + d.amount, 0);
bar.innerHTML = tot ? `<div class="paybar">
<div class="t"><span>إجمالي المحدد (${sel.size})</span>
<b><span class="num">${fmt(tot)}</span> ج.م</b></div>
<button class="btn btn-gold" data-pay>سدّد الآن</button></div>` : '';
box.querySelectorAll('.due').forEach(el => el.onclick = () => {
const id = el.dataset.id;
sel.has(id) ? sel.delete(id) : sel.add(id);
paint();
});
const pay = $('[data-pay]', bar);
if (pay) pay.onclick = () => { PAY = [...sel]; go('#/pay'); };
};
paint();
}
};
};
let PAY = []; // ids being paid
SCREENS.pay = () => {
if (!PAY.length) PAY = S.dues.map(d => d.id); // deep-linked: default to everything due
const items = S.dues.filter(d => PAY.includes(d.id));
const tot = items.reduce((a, d) => a + d.amount, 0);
let method = 'card';
const methods = [
['card', 'i-card', 'بطاقة بنكية', 'Visa · Mastercard · Meeza'],
['wallet', 'i-wallet', 'محفظة إلكترونية', 'فودافون كاش · إنستاباي'],
['desk', 'i-bank', 'الدفع في خزنة النادي', 'يصدر كود سداد يقدَّم للخزنة']
];
return {
body: `${statusbar()}${header({ back: true, title: 'إتمام السداد' })}
<div class="screen-body pad">
<div class="mcard" style="background:linear-gradient(150deg,#0C2247,#123566)"><div class="in">
<div style="font-size:.74rem;opacity:.75">المبلغ المطلوب سداده</div>
<div style="font-family:var(--f-head);font-size:2.1rem;font-weight:900;color:var(--gold-2);line-height:1.2">
<span class="num">${fmt(tot)}</span> <span style="font-size:.82rem;color:#C0D2EA">ج.م</span></div>
<div style="font-size:.7rem;opacity:.7;margin-top:2px">
${items.length} بنود — عضوية <span class="num">${S.member.id}</span></div>
</div></div>
<div class="sec-t">البنود</div>
<div class="card">${items.map(d => `
<div class="row"><div class="l"><b>${d.title}</b><span>${d.sub}</span></div>
<div class="r"><b class="num">${fmt(d.amount)}</b></div></div>`).join('')}</div>
<div class="sec-t">اختر وسيلة الدفع</div>
<div id="ms">${methods.map(([k, i, t, s]) => `
<div class="card" data-m="${k}" style="display:flex;align-items:center;gap:12px;cursor:pointer">
<div class="ic-box">${ico(i)}</div>
<div style="flex:1"><b style="font-size:.86rem;display:block">${t}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${s}</span></div>
<div class="mk" style="width:21px;height:21px;border-radius:50%;border:1.8px solid var(--line-2);
display:grid;place-items:center;color:#2A2003"></div>
</div>`).join('')}</div>
<button class="btn btn-gold" id="doPay" style="margin-top:6px">
ادفع <span class="num">${fmt(tot)}</span> ج.م</button>
<p style="font-size:.68rem;color:var(--txt-3);text-align:center;margin-top:10px">
نموذج تجريبي — لا تُنفَّذ أي عملية دفع حقيقية</p>
</div>`,
mount(root) {
const paint = () => root.querySelectorAll('[data-m]').forEach(c => {
const on = c.dataset.m === method;
c.style.borderColor = on ? 'var(--gold)' : 'var(--line)';
c.querySelector('.mk').style.background = on ? 'var(--gold)' : 'transparent';
c.querySelector('.mk').style.borderColor = on ? 'var(--gold)' : 'var(--line-2)';
c.querySelector('.mk').innerHTML = on ? ico('i-check', 'tiny') : '';
});
root.querySelectorAll('[data-m]').forEach(c => c.onclick = () => { method = c.dataset.m; paint(); });
paint();
$('#doPay', root).onclick = () => { PAYM = method; go('#/paying'); };
}
};
};
let PAYM = 'card';
SCREENS.paying = () => ({
body: `<div style="flex:1;display:grid;place-items:center;text-align:center;padding:24px">
<div><div class="spin"></div>
<h3 style="margin-top:18px;font-size:1rem">جارٍ تنفيذ العملية…</h3>
<p style="font-size:.78rem;color:var(--txt-3);margin-top:5px">لا تغلق التطبيق</p></div></div>`,
mount() {
setTimeout(() => {
const items = S.dues.filter(d => PAY.includes(d.id));
const tot = items.reduce((a, d) => a + d.amount, 0);
const label = { card: 'بطاقة بنكية', wallet: 'محفظة إلكترونية', desk: 'خزنة النادي' }[PAYM];
S.receipts.unshift({
id: 'r' + Date.now(), title: items.map(i => i.title).join(' + '),
amount: tot, date: '2026/03/12', method: label
});
S.dues = S.dues.filter(d => !PAY.includes(d.id));
S.notifications.unshift({ id: 'n' + Date.now(), title: 'تم استلام دفعتك',
body: `وصل سداد ${fmt(tot)} ج.م بنجاح.`, when: 'الآن', kind: 'ok', unread: true });
LAST = { tot, label, n: items.length };
go('#/success', true);
}, 1500);
}
});
let LAST = null;
SCREENS.success = () => ({
body: `<div class="screen-body pad" style="display:flex;flex-direction:column;justify-content:center;text-align:center">
<div class="tick-big">${ico('i-check')}</div>
<h2 style="font-size:1.2rem">تم السداد بنجاح</h2>
<p style="font-size:.82rem;color:var(--txt-3);margin-top:6px">
سُدِّد <b class="num">${LAST ? fmt(LAST.tot) : 0}</b> ج.م عبر ${LAST ? LAST.label : ''}</p>
<div class="card" style="margin-top:22px;text-align:start">
<div class="row"><div class="l"><b>رقم الإيصال</b></div>
<div class="r"><b class="num">2026${Math.floor(Math.random() * 9000 + 1000)}</b></div></div>
<div class="row"><div class="l"><b>عدد البنود</b></div>
<div class="r"><b class="num">${LAST ? LAST.n : 0}</b></div></div>
<div class="row"><div class="l"><b>الحالة</b></div>
<div class="r"><span class="chip ok">مسدَّد</span></div></div>
</div>
<p style="font-size:.74rem;color:var(--txt-3);margin:14px 0 18px">
تحدَّث رصيدك فورًا، ووصلك الإيصال داخل التطبيق.</p>
<button class="btn btn-gold" data-go="#/receipts">عرض الإيصال</button>
<div style="height:10px"></div>
<button class="btn btn-ghost" data-go="#/home">العودة إلى الرئيسية</button>
</div>`
});
SCREENS.receipts = () => ({
tab: 'home',
body: `${statusbar()}${header({ back: true, title: 'الإيصالات', sub: `${S.receipts.length} إيصال` })}
<div class="screen-body pad">
${S.receipts.length ? S.receipts.map(r => `
<div class="card" style="margin-bottom:10px">
<div class="row" style="padding-top:0">
<div class="ic-box mint">${ico('i-receipt')}</div>
<div class="l"><b>${r.title}</b><span>${r.method} — <span class="num">${r.date}</span></span></div>
<div class="r"><b class="num" style="font-family:var(--f-head)">${fmt(r.amount)}</b>
<div style="font-size:.66rem;color:var(--txt-3)">ج.م</div></div>
</div>
<button class="btn btn-ghost btn-sm" style="width:100%;margin-top:4px"
data-toast="نموذج تجريبي — التحميل غير مفعَّل">${ico('i-download')} تحميل PDF</button>
</div>`).join('')
: `<div class="empty">${ico('i-receipt')}<p>لا إيصالات بعد</p></div>`}
</div>`
});
SCREENS.qr = () => ({
tab: 'qr',
body: `${statusbar()}${header({ title: 'كود الدخول', sub: 'وجّه الكود إلى قارئ البوابة' })}
<div class="screen-body pad">
<div class="qr-wrap"><div class="qr-card">
<div class="qr-grid" id="qg">${qrCells(7)}</div></div></div>
<div style="text-align:center;margin-top:14px">
<div class="num" style="font-family:var(--f-head);letter-spacing:.2em;color:var(--gold);font-size:1rem">
${S.member.id}</div>
<div style="font-size:.76rem;color:var(--txt-2);margin-top:2px">${S.member.name}${S.member.type}</div>
<div class="chip ok" style="margin-top:12px">${ico('i-check')} العضوية سارية — الدخول مسموح</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-top:16px">
<svg class="ring" width="34" height="34" viewBox="0 0 34 34">
<circle class="bg" cx="17" cy="17" r="14"/>
<circle class="fg" cx="17" cy="17" r="14" id="rg"
stroke-dasharray="87.9" stroke-dashoffset="0"/></svg>
<span style="font-size:.74rem;color:var(--txt-3)">
يتجدد الكود خلال <b class="num" id="qt">60</b> ثانية</span>
</div>
<div class="card" style="margin-top:18px">
<div class="card-t"><h3>آخر عمليات الدخول</h3></div>
${S.entries.map(e => `<div class="row"><div class="l"><b>${e.gate}</b></div>
<div class="r"><span style="font-size:.73rem;color:var(--txt-3)" class="num">${e.when}</span></div></div>`).join('')}
</div>
</div>${tabbar('qr')}`,
mount(root) {
let t = 60; const g = $('#qg', root), lbl = $('#qt', root), rg = $('#rg', root);
const iv = setInterval(() => {
if (!document.body.contains(g)) return clearInterval(iv);
t--;
if (t <= 0) { t = 60; g.innerHTML = qrCells(Math.floor(Math.random() * 9999)); toast('تم تجديد الكود'); }
lbl.textContent = t;
rg.setAttribute('stroke-dashoffset', String(87.9 * (1 - t / 60)));
}, 1000);
}
});
SCREENS.invites = () => ({
tab: 'qr',
body: `${statusbar()}${header({ back: true, title: 'الدعوات', sub: 'رصيد مارس 2026' })}
<div class="screen-body pad">
<div class="card" style="text-align:center">
<div class="inv-ring">
<svg class="ring" width="132" height="132" viewBox="0 0 132 132">
<circle class="bg" cx="66" cy="66" r="58" stroke-width="8"/>
<circle class="fg" cx="66" cy="66" r="58" stroke-width="8"
stroke-dasharray="364" stroke-dashoffset="${364 * (1 - invLeft() / invTotal())}"/></svg>
<div class="n"><div><span class="num">${invLeft()}</span>
<small>من <span class="num">${invTotal()}</span> دعوة</small></div></div>
</div>
<p style="font-size:.72rem;color:var(--txt-3)">يتجدد الرصيد أول كل شهر ميلادي حسب نوع عضويتك</p>
<div style="height:14px"></div>
<button class="btn btn-gold" id="issue" ${invLeft() ? '' : 'disabled'}>إصدار دعوة جديدة</button>
<div style="height:9px"></div>
<button class="btn btn-ghost" id="buy">شراء دعوات إضافية</button>
</div>
<div class="card">
<div class="card-t"><h3>دعوات هذا الشهر</h3></div>
${S.issued.length ? S.issued.map(i => `
<div class="row"><div class="ic-box">${ico('i-mail')}</div>
<div class="l"><b>${i.name}</b><span class="num">${i.date}</span></div>
<div class="r"><span class="chip ${i.state === 'دخل' ? 'ok' : 'info'}">${i.state}</span></div></div>`).join('')
: '<p style="font-size:.78rem;color:var(--txt-3)">لم تُصدر دعوات بعد</p>'}
</div>
</div>${tabbar('qr')}`,
mount(root) {
$('#issue', root).onclick = () => sheet(`
<h3>إصدار دعوة</h3><p class="sub">تُرسل الدعوة برسالة نصية وتسري ليوم واحد</p>
<div class="card"><div class="row"><div class="l"><b>الرصيد المتاح</b></div>
<div class="r"><b class="num">${invLeft()}</b></div></div>
<div class="row"><div class="l"><b>الصلاحية</b></div><div class="r"><b>يوم واحد</b></div></div></div>
<button class="btn btn-gold" id="cfm">تأكيد الإصدار</button>`,
el => $('#cfm', el).onclick = () => {
S.invites.used++;
S.issued.unshift({ id: 'i' + Date.now(), name: 'دعوة مفتوحة', date: '12/03', state: 'سارية اليوم' });
closeSheet(); render(); toast('صدرت الدعوة وأُرسلت برسالة نصية');
});
$('#buy', root).onclick = () => {
let q = 1;
sheet(`<h3>شراء دعوات إضافية</h3>
<p class="sub">دعوة بالغ <b class="num">${S.invites.priceAdult}</b> ج.م — الحد الأقصى
<b class="num">${S.invites.maxBuy}</b> شهريًا</p>
<div class="stepper"><button id="mn">−</button>
<div class="v num" id="qv">1</div><button id="pl">+</button></div>
<div class="card"><div class="row"><div class="l"><b>الإجمالي</b></div>
<div class="r"><b class="num" id="tv">${S.invites.priceAdult}</b> ج.م</div></div></div>
<button class="btn btn-gold" id="cfm">ادفع واشترِ</button>`,
el => {
const qv = $('#qv', el), tv = $('#tv', el);
const up = () => { qv.textContent = q; tv.textContent = fmt(q * S.invites.priceAdult); };
$('#mn', el).onclick = () => { q = Math.max(1, q - 1); up(); };
$('#pl', el).onclick = () => { q = Math.min(S.invites.maxBuy, q + 1); up(); };
$('#cfm', el).onclick = () => {
S.invites.bought += q; closeSheet(); render();
toast(`أُضيفت ${q} دعوة إلى رصيدك`);
};
});
};
}
});
SCREENS.activities = () => {
let cat = 'الكل';
return {
tab: 'activities',
body: `${statusbar()}${header({ title: 'الأنشطة الرياضية', sub: 'الأسعار معلنة — الاشتراك من التطبيق' })}
<div class="screen-body pad">
<div class="chips" id="cats"></div><div id="list"></div>
</div>${tabbar('activities')}`,
mount(root) {
const cats = ['الكل', ...new Set(S.activities.map(a => a.cat))];
const cbox = $('#cats', root), list = $('#list', root);
const paint = () => {
cbox.innerHTML = cats.map(c => `<button class="${c === cat ? 'on' : ''}" data-c="${c}">${c}</button>`).join('');
cbox.querySelectorAll('[data-c]').forEach(b => b.onclick = () => { cat = b.dataset.c; paint(); });
const items = S.activities.filter(a => cat === 'الكل' || a.cat === cat);
list.innerHTML = items.map(a => {
const full = a.taken >= a.cap;
return `<div class="card" data-go="#/activity/${a.id}"
style="display:flex;align-items:center;gap:12px;cursor:pointer;${full ? 'opacity:.62' : ''}">
<div class="ic-box ${a.cat === 'سباحة' ? 'sky' : a.cat === 'كرة' ? 'mint' : ''}">${ico(a.icon)}</div>
<div style="flex:1;min-width:0">
<b style="font-size:.87rem;display:block">${a.name}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${a.age} · <span class="num">${a.sess}</span> حصة/شهر</span>
${full ? '<span class="chip due" style="margin-top:5px">اكتمل العدد — قائمة انتظار</span>' : ''}
</div>
<div style="text-align:end">
<b style="font-family:var(--f-head);font-size:1rem" class="num">${fmt(a.price)}</b>
<div style="font-size:.64rem;color:var(--txt-3)">ج.م / شهر</div></div>
</div>`;
}).join('');
bindAll(list);
};
paint();
}
};
};
SCREENS.activity = (id) => {
const a = actById(id); if (!a) return SCREENS.activities();
const full = a.taken >= a.cap;
const already = S.subs.some(s => s.actId === a.id);
return {
body: `${statusbar()}${header({ back: true, title: a.name, sub: a.cat })}
<div class="screen-body pad">
<div class="card" style="text-align:center;padding:22px 15px">
<div class="ic-box ${a.cat === 'سباحة' ? 'sky' : a.cat === 'كرة' ? 'mint' : ''}"
style="width:64px;height:64px;border-radius:20px;margin:0 auto 12px">
${ico(a.icon, 'big')}</div>
<h2 style="font-size:1.1rem">${a.name}</h2>
<p style="font-size:.76rem;color:var(--txt-3);margin-top:4px">${a.coach}</p>
<div style="font-family:var(--f-head);font-size:1.9rem;font-weight:900;color:var(--gold);margin-top:12px">
<span class="num">${fmt(a.price)}</span>
<span style="font-size:.78rem;color:var(--txt-3)">ج.م / شهر</span></div>
</div>
<div class="card">
<div class="row"><div class="l"><b>الفئة السنية</b></div><div class="r"><b>${a.age}</b></div></div>
<div class="row"><div class="l"><b>عدد الحصص</b></div>
<div class="r"><b><span class="num">${a.sess}</span> حصة شهريًا</b></div></div>
<div class="row"><div class="l"><b>رسوم الانضمام</b></div>
<div class="r"><b><span class="num">${fmt(a.join)}</span> ج.م</b></div></div>
<div class="row"><div class="l"><b>الأماكن المتاحة</b></div>
<div class="r"><b class="${full ? '' : ''}"><span class="num">${a.cap - a.taken}</span> من <span class="num">${a.cap}</span></b></div></div>
</div>
<div class="card">
<div class="card-t"><h3>السعة</h3><span style="font-size:.72rem;color:var(--txt-3)">
<span class="num">${a.taken}/${a.cap}</span></span></div>
<div class="bar"><i style="width:${(a.taken / a.cap) * 100}%;
background:${full ? 'var(--rose)' : 'linear-gradient(90deg,var(--gold),var(--gold-2))'}"></i></div>
</div>
${already ? `<div class="card" style="text-align:center;border-color:rgba(22,185,129,.35)">
<span class="chip ok">أنت مشترك في هذا النشاط</span></div>
<button class="btn btn-ghost" data-go="#/schedule">عرض جدول التدريب</button>`
: full ? `<button class="btn btn-ghost" data-toast="أُضفت إلى قائمة الانتظار">
الانضمام لقائمة الانتظار</button>`
: `<button class="btn btn-gold" id="sub">اشترك الآن</button>`}
<div style="height:14px"></div>
</div>`,
mount(root) {
const b = $('#sub', root); if (!b) return;
b.onclick = () => {
let kid = S.member.kids[0].name;
sheet(`<h3>الاشتراك في ${a.name}</h3><p class="sub">اختر الابن المشترك</p>
<div id="kids">${S.member.kids.map(k => `
<div class="card" data-k="${k.name}" style="display:flex;align-items:center;gap:11px;cursor:pointer">
<div class="ic-box">${ico('i-user')}</div>
<b style="flex:1;font-size:.86rem">${k.name}</b>
<div class="mk" style="width:20px;height:20px;border-radius:50%;
border:1.8px solid var(--line-2);display:grid;place-items:center;color:#2A2003"></div>
</div>`).join('')}</div>
<div class="card">
<div class="row"><div class="l"><b>اشتراك الشهر الأول</b></div>
<div class="r"><b class="num">${fmt(a.price)}</b></div></div>
<div class="row"><div class="l"><b>رسوم الانضمام</b></div>
<div class="r"><b class="num">${fmt(a.join)}</b></div></div>
<div class="row"><div class="l"><b>الإجمالي المستحق</b></div>
<div class="r"><b class="num" style="color:var(--gold)">${fmt(a.price + a.join)}</b> ج.م</div></div>
</div>
<button class="btn btn-gold" id="cfm">تأكيد الاشتراك</button>`,
el => {
const paint = () => el.querySelectorAll('[data-k]').forEach(c => {
const on = c.dataset.k === kid;
c.style.borderColor = on ? 'var(--gold)' : 'var(--line)';
const m = c.querySelector('.mk');
m.style.background = on ? 'var(--gold)' : 'transparent';
m.style.borderColor = on ? 'var(--gold)' : 'var(--line-2)';
m.innerHTML = on ? ico('i-check') : '';
});
el.querySelectorAll('[data-k]').forEach(c => c.onclick = () => { kid = c.dataset.k; paint(); });
paint();
$('#cfm', el).onclick = () => {
S.subs.push({ id: 's' + Date.now(), actId: a.id, kid, group: 'مجموعة جديدة', state: 'نشط' });
a.taken++;
S.dues.push({ id: 'd' + Date.now(), title: `اشتراك ${a.name}`,
sub: `${kid} — الشهر الأول + رسوم الانضمام`, due: '2026/03/20',
amount: a.price + a.join, state: 'soon', kind: 'activity' });
closeSheet(); go('#/home', true);
setTimeout(() => toast('تم الاشتراك — صدرت فاتورة الشهر الأول'), 380);
};
});
};
}
};
};
SCREENS.schedule = () => {
const first = S.subs[0];
const sc = first ? S.schedule[first.actId] : null;
return {
tab: 'activities',
body: `${statusbar()}${header({ back: true, title: 'جدول التدريب',
sub: first ? `${first.kid}${actById(first.actId) ? actById(first.actId).name : ''}` : '' })}
<div class="screen-body pad">
${!sc ? `<div class="empty">${ico('i-clock')}<p>لا اشتراكات لعرض جدولها</p></div>` : `
<div class="card" style="display:flex;align-items:center;gap:12px">
<div class="ic-box sky">${ico('i-clock')}</div>
<div style="flex:1"><b style="font-size:.86rem;display:block">${sc.group}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${sc.coach} · ${sc.place}</span></div>
<span class="chip ok">نشط</span></div>
${sc.days.map(d => `<div class="tt-day">${d.d}</div>
<div class="card" style="margin-bottom:8px">
<div class="row" style="padding:4px 0"><div class="ic-box">${ico('i-clock')}</div>
<div class="l"><b><span class="num">${d.f}</span> — <span class="num">${d.t}</span></b>
<span>${sc.place}</span></div></div>
${d.note ? `<div style="margin-top:8px;background:rgba(232,163,61,.12);border-radius:10px;
padding:9px 11px;font-size:.72rem;color:var(--amber)">ملاحظة المدرب: ${d.note}</div>` : ''}
</div>`).join('')}
${sc.changes.length ? `<div class="sec-t">تغييرات على الجدول</div><div class="card">
${sc.changes.map(c => `<div class="row"><div class="ic-box rose">${ico('i-bell')}</div>
<div class="l"><span style="font-size:.76rem;color:var(--txt-2)">${c}</span></div></div>`).join('')}
</div>` : ''}`}
</div>${tabbar('activities')}`
};
};
SCREENS.notifications = () => ({
body: `${statusbar()}${header({ back: true, title: 'الإشعارات',
sub: unreadCount() ? `${unreadCount()} غير مقروء` : 'لا جديد' })}
<div class="screen-body pad">
${S.notifications.map(n => {
const m = { due:['rose','i-card'], sched:['sky','i-clock'], act:['','i-activity'], ok:['mint','i-check'] }[n.kind] || ['', 'i-bell'];
return `<div class="card" style="${n.unread ? 'border-color:var(--line-2)' : 'opacity:.68'}">
<div class="row" style="padding:0;border:none;align-items:flex-start">
<div class="ic-box ${m[0]}">${ico(m[1])}</div>
<div class="l"><b>${n.title}</b>
<span style="display:block;color:var(--txt-2);margin-top:2px;line-height:1.65">${n.body}</span>
<span style="display:block;margin-top:4px">${n.when}</span></div>
${n.unread ? '<span style="width:8px;height:8px;border-radius:50%;background:var(--gold);margin-top:6px"></span>' : ''}
</div></div>`;
}).join('')}
<button class="btn btn-ghost" id="mr">تعليم الكل كمقروء</button>
<div style="height:14px"></div>
</div>`,
mount(root) {
$('#mr', root).onclick = () => { S.notifications.forEach(n => n.unread = false); render(); toast('عُلِّمت كل الإشعارات كمقروءة'); };
}
});
SCREENS.profile = () => ({
tab: 'profile',
body: `${statusbar()}${header({ title: 'حسابي', sub: S.member.type })}
<div class="screen-body pad">
<div class="mcard"><div class="in">
<div class="who">${S.member.name}</div>
<div class="meta">${S.member.type} — سارية حتى <span class="num">${S.member.validUntil}</span></div>
<div class="idno num">${S.member.id}</div>
</div></div>
<div class="card">
<div class="row" data-go="#/receipts" style="cursor:pointer">
<div class="ic-box">${ico('i-receipt')}</div>
<div class="l"><b>الإيصالات</b><span><span class="num">${S.receipts.length}</span> إيصال</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/invites" style="cursor:pointer">
<div class="ic-box">${ico('i-mail')}</div>
<div class="l"><b>الدعوات</b><span><span class="num">${invLeft()}</span> متاحة</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/notifications" style="cursor:pointer">
<div class="ic-box">${ico('i-bell')}</div>
<div class="l"><b>الإشعارات</b><span><span class="num">${unreadCount()}</span> غير مقروء</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/schedule" style="cursor:pointer">
<div class="ic-box">${ico('i-clock')}</div>
<div class="l"><b>جداول التدريب</b><span>اشتراكات الأبناء</span></div>
${ico('i-chev', 'flip')}</div>
</div>
<div class="card">
<div class="row"><div class="ic-box">${ico('i-moon')}</div>
<div class="l"><b>المظهر</b><span>فاتح / داكن</span></div>
<button class="btn btn-ghost btn-sm" id="th">تبديل</button></div>
<div class="row"><div class="ic-box rose">${ico('i-refresh')}</div>
<div class="l"><b>إعادة ضبط النموذج</b><span>يعيد البيانات التجريبية</span></div>
<button class="btn btn-ghost btn-sm" id="rs">إعادة</button></div>
</div>
<p style="font-size:.68rem;color:var(--txt-3);text-align:center;margin-top:6px">
نموذج تفاعلي — شركة الأركيد · 2026</p>
<div style="height:10px"></div>
</div>${tabbar('profile')}`,
mount(root) {
$('#th', root).onclick = toggleTheme;
$('#rs', root).onclick = () => { S = clone(window.DB); go('#/home', true); setTimeout(() => toast('أُعيد ضبط النموذج'), 300); };
}
});
// ==========================================================
// ROUTER
// ==========================================================
function parse() {
const h = (location.hash || '#/splash').replace(/^#\//, '');
const [name, ...rest] = h.split('/');
return { name: SCREENS[name] ? name : 'home', arg: rest.join('/') };
}
function bindAll(root) {
root.querySelectorAll('[data-go]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', e => { e.stopPropagation(); go(el.dataset.go); });
});
root.querySelectorAll('[data-back]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', back);
});
root.querySelectorAll('[data-toast]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', () => toast(el.dataset.toast));
});
}
let busy = false;
function paint(dir) {
const { name, arg } = parse();
const def = SCREENS[name](arg);
current = name;
const old = $('.screen', app);
const el = document.createElement('div');
el.className = 'screen' + (def.tab ? ' has-tab' : '');
el.innerHTML = def.body;
app.appendChild(el);
bindAll(el);
if (def.mount) def.mount(el);
if (old && dir) {
busy = true;
el.classList.add(dir === 'back' ? 'anim-in-back' : 'anim-in-fwd');
old.classList.add(dir === 'back' ? 'anim-out-back' : 'anim-out-fwd');
setTimeout(() => { old.remove(); busy = false; }, 350);
} else if (old) old.remove();
try { parent.postMessage({ type: 'sayd:route', route: '#/' + name }, '*'); } catch (e) {}
}
const render = () => paint(null);
function go(hash, replace) {
if (busy) return;
const target = hash.replace(/^#/, '');
if ('#' + target === location.hash) return;
if (replace) stack = [];
else stack.push(location.hash || '#/splash');
location.hash = target;
paint('fwd');
}
function back() {
if (busy) return;
const prev = stack.pop();
if (prev) { history.replaceState(null, '', prev); paint('back'); }
else go('#/home', true);
}
window.addEventListener('hashchange', () => { if (!busy) paint(null); });
// parent (proposal page) can drive the prototype
window.addEventListener('message', e => {
const d = e.data || {};
if (d.type === 'sayd:goto') go(d.route, true);
if (d.type === 'sayd:theme') { theme(d.value); render(); }
if (d.type === 'sayd:reset') { S = clone(window.DB); go('#/home', true); }
});
// ---------- boot ----------
theme();
if (!location.hash) location.hash = '/splash';
paint(null);
window.__sayd = { go, toast, get state() { return S; } };
})();
/* Mock dataset for the prototype. Everything the screens read and mutate lives
here, so a demo session behaves like a real account: paying clears dues and
creates a receipt, subscribing adds an invoice, issuing an invite decrements
the balance. Reset from the profile screen. */
window.DB = {
member: {
id: 10245,
name: 'محمد أحمد سيد إبراهيم',
short: 'محمد أحمد',
type: 'عضو عامل',
validUntil: '2026/12/31',
phone: '100 123 4567',
kids: [
{ id: 'k1', name: 'يوسف محمد' },
{ id: 'k2', name: 'لُجين محمد' }
]
},
dues: [
{ id:'d1', title:'قسط تجديد العضوية', sub:'القسط ٢ من ٣ — فبراير 2026', due:'2026/02/28',
amount:1500, state:'late', kind:'renewal' },
{ id:'d2', title:'اشتراك سباحة مبتدئين', sub:'يوسف محمد — مارس 2026', due:'2026/03/05',
amount:900, state:'soon', kind:'activity' },
{ id:'d3', title:'غرامة تأخير سداد', sub:'عن تأخر قسط يناير', due:'2026/02/28',
amount:350, state:'late', kind:'fine' }
],
receipts: [
{ id:'r0', title:'اشتراك جمباز فني — لُجين', amount:1000, date:'2026/02/03', method:'بطاقة بنكية' },
{ id:'r1', title:'قسط تجديد العضوية — القسط ١', amount:1500, date:'2026/01/08', method:'محفظة إلكترونية' }
],
invites: { free: 4, used: 1, bought: 0, priceAdult: 150, priceChild: 75, maxBuy: 10 },
issued: [
{ id:'i1', name:'أحمد سامي', date:'15/03', state:'دخل' }
],
activities: [
{ id:'SW-01', name:'سباحة مبتدئين', cat:'سباحة', icon:'i-swim', age:'٦ - ٩ سنوات', sess:12, price:900, join:300, cap:24, taken:18, coach:'كابتن هشام صلاح' },
{ id:'SW-02', name:'سباحة متقدم', cat:'سباحة', icon:'i-swim', age:'١٠ - ١٤ سنة', sess:16, price:1200, join:300, cap:20, taken:14, coach:'كابتن هشام صلاح' },
{ id:'FB-01', name:'كرة القدم — الموهبة', cat:'كرة', icon:'i-football', age:'٨ - ١١ سنة', sess:12, price:1100, join:400, cap:28, taken:26, coach:'كابتن طارق منير' },
{ id:'KR-01', name:'كاراتيه مبتدئين', cat:'فردي', icon:'i-karate', age:'٦ - ١٢ سنة', sess:8, price:700, join:250, cap:30, taken:22, coach:'كابتن أمير فؤاد' },
{ id:'TN-01', name:'تنس أرضي — أطفال', cat:'فردي', icon:'i-tennis', age:'٧ - ١٢ سنة', sess:8, price:1400, join:350, cap:12, taken:12, coach:'كابتن نهى سامي' },
{ id:'GY-01', name:'جمباز فني — سيدات', cat:'فردي', icon:'i-gym', age:'١٢ - ١٨ سنة', sess:12, price:1000, join:300, cap:18, taken:11, coach:'كابتن دينا عادل' }
],
subs: [
{ id:'s1', actId:'SW-01', kid:'يوسف محمد', group:'مجموعة أ — صباحي', state:'نشط' },
{ id:'s2', actId:'GY-01', kid:'لُجين محمد', group:'مجموعة ب — مسائي', state:'تُجدَّد خلال ٥ أيام' }
],
schedule: {
'SW-01': { group:'مجموعة أ — صباحي', coach:'كابتن هشام صلاح', place:'حمام السباحة الأولمبي',
days:[ {d:'السبت',f:'09:00',t:'10:00'}, {d:'الاثنين',f:'09:00',t:'10:00'}, {d:'الأربعاء',f:'09:00',t:'10:00', note:'حصة قياس مستوى'} ],
changes:['أُلغيت حصة الاثنين 10/03 لصيانة حمام السباحة.','حصة تعويضية الجمعة 14/03 الساعة 10:00.'] },
'GY-01': { group:'مجموعة ب — مسائي', coach:'كابتن دينا عادل', place:'صالة الجمباز',
days:[ {d:'الأحد',f:'17:00',t:'18:30'}, {d:'الثلاثاء',f:'17:00',t:'18:30'} ], changes:[] }
},
entries: [
{ gate:'البوابة الرئيسية', when:'اليوم 08:14' },
{ gate:'بوابة حمام السباحة', when:'أمس 17:02' },
{ gate:'البوابة الرئيسية — ومعه ضيف', when:'أمس 16:58' }
],
notifications: [
{ id:'n1', title:'قسط مستحق منذ يومين', body:'قسط تجديد العضوية ٢ من ٣ بقيمة 1,500 ج.م.', when:'اليوم 09:10', kind:'due', unread:true },
{ id:'n2', title:'حصة تعويضية', body:'أُضيفت حصة سباحة الجمعة 14/03 الساعة 10:00.', when:'أمس 18:40', kind:'sched', unread:true },
{ id:'n3', title:'تجديد اشتراك لُجين', body:'اشتراك الجمباز يُجدَّد خلال ٥ أيام.', when:'أمس 09:00', kind:'act', unread:true },
{ id:'n4', title:'تم استلام دفعتك', body:'وصل سداد 1,000 ج.م — إيصال رقم 20260203.', when:'03/02', kind:'ok', unread:false }
]
};
......@@ -255,98 +255,9 @@ h2.title{ font-size:clamp(1.7rem,3.4vw,2.5rem); color:var(--navy); margin-bottom
background:#F4F6FA; border-radius:34px; overflow:hidden; height:686px;
position:relative; display:flex; flex-direction:column;
}
.sbar{
height:46px; flex-shrink:0; display:flex; align-items:flex-end; justify-content:space-between;
padding:0 26px 6px; font-size:.72rem; font-weight:700; color:#0E1A2B; position:relative; z-index:6;
}
.sbar.on-dark{ color:#fff; }
.scr-body{ flex:1; overflow-y:auto; overflow-x:hidden; }
.scr-body::-webkit-scrollbar{ width:0; }
/* app chrome */
.app-hd{ padding:6px 20px 18px; }
.app-hd.navy{ background:linear-gradient(150deg,#0C2247,#123566 60%,#16407A); color:#fff; padding-bottom:26px; border-radius:0 0 26px 26px; }
.app-hd h3{ font-size:1.08rem; }
.app-hd .sm{ font-size:.74rem; opacity:.7; }
.app-back{ font-size:1.1rem; }
.tabbar{
flex-shrink:0; height:66px; background:#fff; border-top:1px solid var(--line);
display:flex; align-items:center; justify-content:space-around; padding-bottom:8px;
}
.tab{ display:flex; flex-direction:column; align-items:center; gap:3px; font-size:.62rem; color:var(--txt-3); font-weight:600; }
.tab i{ font-size:1.15rem; font-style:normal; }
.tab.on{ color:var(--navy); }
.tab.on i{ filter:drop-shadow(0 2px 6px rgba(18,53,102,.3)); }
/* app widgets */
.mcard{
margin:0 16px 14px; background:#fff; border-radius:16px; padding:16px;
border:1px solid var(--line); box-shadow:0 1px 3px rgba(10,25,50,.04);
}
.mcard-t{ font-size:.78rem; font-weight:700; color:var(--txt-3); margin-bottom:10px; display:flex; justify-content:space-between; align-items:center; }
.mrow{ display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid var(--paper-3); font-size:.8rem; }
.mrow:last-child{ border-bottom:none; }
.mrow .l{ color:var(--txt-2); }
.mrow .v{ font-weight:700; color:var(--txt); }
.chip{ display:inline-block; font-size:.66rem; font-weight:700; padding:3px 10px; border-radius:99px; }
.chip.due{ background:var(--rose-dim); color:var(--rose); }
.chip.ok{ background:var(--mint-dim); color:#0E8C60; }
.chip.soon{ background:var(--gold-dim); color:#9A7B12; }
.chip.info{ background:rgba(62,140,224,.12); color:var(--sky); }
.mbtn{
display:block; width:100%; text-align:center; padding:13px; border-radius:13px;
font-family:var(--f-head); font-weight:700; font-size:.86rem;
background:linear-gradient(135deg,var(--gold),#B8901C); color:#fff; box-shadow:0 6px 18px rgba(201,162,39,.3);
}
.mbtn.navy{ background:var(--navy); box-shadow:0 6px 18px rgba(18,53,102,.28); }
.mbtn.ghost{ background:#fff; color:var(--navy); border:1.5px solid var(--line-2); box-shadow:none; }
/* membership digital card */
.dcard{
margin:-38px 16px 18px; border-radius:20px; padding:18px;
background:linear-gradient(135deg,#123566,#0C2247 60%,#1B4A8C);
color:#fff; position:relative; overflow:hidden; box-shadow:0 14px 34px rgba(12,34,71,.35);
}
.dcard::after{
content:''; position:absolute; inset-inline-end:-40px; top:-40px; width:150px; height:150px;
border-radius:50%; background:radial-gradient(circle,rgba(201,162,39,.45),transparent 70%);
}
.dcard .who{ font-family:var(--f-head); font-weight:800; font-size:1.02rem; }
.dcard .meta{ font-size:.7rem; opacity:.72; }
.dcard .idno{ font-family:var(--f-head); letter-spacing:.16em; font-size:1rem; color:var(--gold-2); margin-top:12px; }
.qr-box{
width:190px; height:190px; margin:0 auto; background:#fff; border-radius:18px; padding:12px;
box-shadow:0 10px 30px rgba(10,25,50,.14); display:grid; place-items:center;
}
.qr-grid{ width:100%; height:100%; display:grid; grid-template-columns:repeat(11,1fr); grid-template-rows:repeat(11,1fr); gap:2px; }
.qr-grid i{ background:#0E1A2B; border-radius:1px; }
.seg{ display:flex; background:var(--paper-3); border-radius:11px; padding:3px; margin:0 16px 14px; }
.seg button{ flex:1; font-size:.76rem; font-weight:700; padding:8px; border-radius:9px; color:var(--txt-3); }
.seg button.on{ background:#fff; color:var(--navy); box-shadow:var(--sh-s); }
.sport-item{ display:flex; gap:12px; align-items:center; margin:0 16px 10px; background:#fff; border:1px solid var(--line); border-radius:15px; padding:12px; }
.sport-ico{ width:44px; height:44px; border-radius:12px; display:grid; place-items:center; font-size:1.25rem; background:var(--paper-3); flex-shrink:0; }
.sport-item .nm{ font-weight:700; font-size:.85rem; }
.sport-item .ds{ font-size:.7rem; color:var(--txt-3); }
.sport-item .pr{ margin-inline-start:auto; text-align:center; }
.sport-item .pr b{ font-size:.9rem; color:var(--navy); font-family:var(--f-head); }
.sport-item .pr span{ display:block; font-size:.62rem; color:var(--txt-3); }
.tt{ margin:0 16px 10px; background:#fff; border:1px solid var(--line); border-radius:14px; overflow:hidden; }
.tt-d{ background:var(--paper-2); padding:7px 14px; font-size:.74rem; font-weight:700; color:var(--navy); border-bottom:1px solid var(--line); }
.tt-r{ display:flex; align-items:center; gap:10px; padding:10px 14px; font-size:.76rem; border-bottom:1px solid var(--paper-3); }
.tt-r:last-child{ border-bottom:none; }
.tt-r .tm{ font-weight:700; color:var(--navy); font-size:.72rem; direction:ltr; }
.otp-row{ display:flex; gap:10px; justify-content:center; margin:22px 0; direction:ltr; }
.otp-row i{
width:52px; height:60px; border-radius:13px; background:#fff; border:1.5px solid var(--line-2);
display:grid; place-items:center; font-style:normal; font-family:var(--f-head); font-weight:800; font-size:1.4rem; color:var(--navy);
}
.otp-row i.f{ border-color:var(--gold); box-shadow:0 0 0 3px var(--gold-dim); }
/* The in-page phone UI that used to live here (.sbar/.app-hd/.mcard/.tabbar/
.sport-item/.otp-row/.qr-*) was removed with the static prototype — the
phone now embeds the real app from app/, which carries its own styles. */
/* ================= BROWSER / PORTAL ================= */
.browser{
......@@ -617,9 +528,6 @@ footer::before{
}
footer > *{ position:relative; z-index:1; }
/* Hairline gold rule closing each major light section. */
.rule-gold{ height:1px; border:0; margin:0;
background:linear-gradient(90deg,transparent,rgba(201,162,39,.42),transparent); }
@media (max-width:760px){
.wm::before{ width:74vw; inset-inline-end:-26%; opacity:.05; }
......@@ -652,3 +560,14 @@ footer > *{ position:relative; z-index:1; }
/* .card h4 is declared after .dark h4 at equal specificity, so cards placed
inside a .dark section inherited navy-on-navy headings. Raise specificity. */
.dark .card h4{ color:#fff; }
/* ================= LIVE PROTOTYPE =================
The phone now hosts the real prototype in an iframe rather than a stack of
static divs, so the screens in the proposal are the screens that ship. */
.phone iframe.screen{ width:100%; border:0; display:block; background:#070F1E; }
.proto-tools{ display:flex; gap:8px; flex-wrap:wrap; margin-top:14px; }
.proto-tools .pbtn{ cursor:pointer; }
.proto-nav{ max-height:none; }
@media (max-width:1020px){
.proto-tools{ justify-content:center; }
}
......@@ -250,406 +250,39 @@
<div class="proto reveal" style="margin-top:38px;">
<div class="proto-nav">
<h4>شاشات التطبيق</h4>
<button class="pn-item on" data-scr="login"><span class="n">١</span> الدخول والتحقق</button>
<button class="pn-item" data-scr="home"><span class="n">٢</span> الرئيسية وبطاقة العضوية</button>
<button class="pn-item" data-scr="dues"><span class="n">٣</span> المستحقات والأقساط</button>
<button class="pn-item" data-scr="pay"><span class="n">٤</span> السداد الإلكتروني</button>
<button class="pn-item" data-scr="qr"><span class="n">٥</span> كود دخول البوابة</button>
<button class="pn-item" data-scr="invites"><span class="n">٦</span> الدعوات وشراء المزيد</button>
<button class="pn-item" data-scr="sports"><span class="n">٧</span> الأنشطة الرياضية</button>
<button class="pn-item" data-scr="schedule"><span class="n">٨</span> جدول التدريب</button>
<button class="pn-item on" data-route="home"><span class="n">١</span> الرئيسية وبطاقة العضوية</button>
<button class="pn-item" data-route="dues"><span class="n">٢</span> المستحقات والأقساط</button>
<button class="pn-item" data-route="pay"><span class="n">٣</span> السداد الإلكتروني</button>
<button class="pn-item" data-route="qr"><span class="n">٤</span> كود دخول البوابة</button>
<button class="pn-item" data-route="invites"><span class="n">٥</span> الدعوات وشراء المزيد</button>
<button class="pn-item" data-route="activities"><span class="n">٦</span> الأنشطة الرياضية</button>
<button class="pn-item" data-route="activity/SW-01"><span class="n">٧</span> تفاصيل نشاط والاشتراك</button>
<button class="pn-item" data-route="schedule"><span class="n">٨</span> جدول التدريب</button>
<button class="pn-item" data-route="receipts"><span class="n">٩</span> الإيصالات</button>
<button class="pn-item" data-route="notifications"><span class="n">١٠</span> الإشعارات</button>
<button class="pn-item" data-route="profile"><span class="n">١١</span> حسابي والإعدادات</button>
<button class="pn-item" data-route="login"><span class="n">١٢</span> الدخول والتحقق</button>
<div class="pn-desc">
تطبيق أصلي (Native) لنظامي iOS و Android، بواجهة عربية بالكامل من اليمين لليسار،
ويعمل على الأجهزة من إصدار iOS 14 و Android 8 فأحدث.
هذا نموذج يعمل فعليًا لا صور ثابتة: اضغطوا داخل الشاشة نفسها للتنقل والسداد
والاشتراك في نشاط وإصدار دعوة، وستتغير الأرصدة والمستحقات أمامكم.
تطبيق أصلي (Native) لنظامي iOS و Android بواجهة عربية من اليمين لليسار،
يعمل على الأجهزة من إصدار iOS 14 و Android 8 فأحدث.
</div>
</div>
<div class="phone-stage">
<div class="phone">
<div class="screen">
<!-- 1. LOGIN -->
<div class="scr" data-scr="login" style="display:flex;flex-direction:column;height:100%;background:linear-gradient(165deg,#0C2247,#07142B);">
<div class="sbar on-dark"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body" style="display:flex;flex-direction:column;align-items:center;padding:26px 22px;color:#fff;text-align:center;">
<img src="assets/club-logo.png" style="height:88px;margin:18px 0 20px;filter:drop-shadow(0 8px 22px rgba(0,0,0,.5));" alt="">
<h3 style="font-family:var(--f-head);font-size:1.15rem;">أهلًا بك في نادي الصيد</h3>
<p style="font-size:.78rem;color:#9FB6D4;margin-top:6px;line-height:1.8;">
أدخل رقم الموبايل المسجَّل في بيانات عضويتك<br>وسيصلك كود تحقق برسالة قصيرة
</p>
<div style="width:100%;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:14px;padding:13px 16px;margin:24px 0 6px;display:flex;align-items:center;gap:10px;direction:ltr;">
<span style="font-size:.86rem;color:#C7D8EE;display:inline-flex;align-items:center;gap:6px;"><svg class="ico"><use href="#i-flag-eg"/></svg> +20</span>
<span style="width:1px;height:20px;background:rgba(255,255,255,.18);"></span>
<span style="font-size:.95rem;font-weight:700;letter-spacing:.06em;">100 123 4567</span>
</div>
<div class="otp-row">
<i class="f">٤</i><i>٩</i><i>٢</i><i style="color:var(--txt-3);opacity:.4;"></i>
</div>
<p style="font-size:.72rem;color:#8AA3C4;">لم يصلك الكود؟ إعادة الإرسال خلال <span class="num">00:23</span></p>
<div style="margin-top:auto;width:100%;padding-top:26px;">
<div class="mbtn" style="margin:0;">تأكيد ودخول</div>
<p style="font-size:.68rem;color:#6F87A8;margin-top:14px;line-height:1.7;">
بالدخول أنت توافق على لائحة النادي وشروط استخدام التطبيق
</p>
</div>
</div>
</div>
<!-- 2. HOME -->
<div class="scr" data-scr="home" style="display:none;flex-direction:column;height:100%;">
<div class="sbar on-dark"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd navy">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div>
<div class="sm">صباح الخير</div>
<h3>محمد أحمد سيد</h3>
</div>
<div style="position:relative;font-size:1.2rem;"><svg class="ico"><use href="#i-bell"/></svg><span style="position:absolute;top:-2px;right:-4px;width:8px;height:8px;background:var(--rose);border-radius:50%;"></span></div>
</div>
</div>
<div class="dcard">
<div style="display:flex;justify-content:space-between;align-items:flex-start;position:relative;z-index:2;">
<div>
<div class="who">محمد أحمد سيد إبراهيم</div>
<div class="meta">عضو عامل &mdash; سارية حتى <span class="num">2026/12/31</span></div>
</div>
<span class="chip ok">سارية</span>
</div>
<div class="idno num">10245</div>
</div>
<div class="mcard" style="border-color:rgba(224,85,107,.35);background:linear-gradient(180deg,#FFF6F7,#fff);">
<div class="mcard-t"><span>إجمالي المستحق عليك الآن</span><span class="chip due">متأخر يومين</span></div>
<div style="font-family:var(--f-head);font-size:1.9rem;font-weight:900;color:var(--rose);line-height:1.1;">
<span class="num">1,850</span> <span style="font-size:.9rem;color:var(--txt-3);">ج.م</span>
</div>
<p style="font-size:.72rem;color:var(--txt-3);margin:4px 0 14px;">٣ بنود مستحقة &mdash; قسط تجديد + فاتورة نشاط + غرامة</p>
<div class="mbtn">سدّد الآن</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:0 16px 14px;">
<div style="background:#fff;border:1px solid var(--line);border-radius:13px;padding:12px 4px;text-align:center;">
<div style="font-size:1.2rem;"><svg class="ico"><use href="#i-gate"/></svg></div><div style="font-size:.62rem;font-weight:700;color:var(--txt-2);margin-top:4px;">كود الدخول</div>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:13px;padding:12px 4px;text-align:center;">
<div style="font-size:1.2rem;"><svg class="ico"><use href="#i-mail"/></svg></div><div style="font-size:.62rem;font-weight:700;color:var(--txt-2);margin-top:4px;">الدعوات</div>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:13px;padding:12px 4px;text-align:center;">
<div style="font-size:1.2rem;"><svg class="ico"><use href="#i-activity"/></svg></div><div style="font-size:.62rem;font-weight:700;color:var(--txt-2);margin-top:4px;">الأنشطة</div>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:13px;padding:12px 4px;text-align:center;">
<div style="font-size:1.2rem;"><svg class="ico"><use href="#i-receipt"/></svg></div><div style="font-size:.62rem;font-weight:700;color:var(--txt-2);margin-top:4px;">الإيصالات</div>
</div>
</div>
<div class="mcard">
<div class="mcard-t"><span>اشتراكات أبنائك</span><span style="color:var(--gold);font-weight:700;">الكل</span></div>
<div class="mrow"><span class="l">يوسف &mdash; سباحة مبتدئين</span><span class="v"><span class="chip ok">نشط</span></span></div>
<div class="mrow"><span class="l">لُجين &mdash; جمباز فني</span><span class="v"><span class="chip soon">تُجدَّد خلال ٥ أيام</span></span></div>
</div>
<div class="mcard" style="margin-bottom:20px;">
<div class="mcard-t"><span>رصيد الدعوات هذا الشهر</span></div>
<div style="display:flex;align-items:center;gap:10px;">
<div style="flex:1;height:8px;background:var(--paper-3);border-radius:99px;overflow:hidden;">
<div style="width:25%;height:100%;background:linear-gradient(90deg,var(--gold),var(--gold-2));"></div>
</div>
<span style="font-size:.78rem;font-weight:800;color:var(--navy);"><span class="num">1</span> من <span class="num">4</span></span>
</div>
</div>
</div>
<div class="tabbar">
<div class="tab on"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div>
<div class="tab"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div>
<div class="tab"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
<!-- 3. DUES -->
<div class="scr" data-scr="dues" style="display:none;flex-direction:column;height:100%;">
<div class="sbar"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd" style="padding-top:4px;">
<div style="display:flex;align-items:center;gap:12px;">
<span class="app-back"></span><h3 style="color:var(--navy);">المستحقات</h3>
</div>
</div>
<div class="seg">
<button class="on">مستحق (٣)</button><button>مسدَّد</button><button>الكل</button>
</div>
<div class="mcard" style="border-inline-start:3px solid var(--rose);">
<div class="mcard-t"><span>قسط تجديد العضوية</span><span class="chip due">متأخر</span></div>
<div class="mrow"><span class="l">عن شهر</span><span class="v">فبراير 2026</span></div>
<div class="mrow"><span class="l">تاريخ الاستحقاق</span><span class="v num">2026/02/28</span></div>
<div class="mrow"><span class="l">القسط ٢ من ٣</span><span class="v" style="color:var(--rose);font-size:1rem;"><span class="num">1,500</span> ج.م</span></div>
</div>
<div class="mcard" style="border-inline-start:3px solid var(--gold);">
<div class="mcard-t"><span>اشتراك سباحة مبتدئين &mdash; يوسف</span><span class="chip soon">يستحق قريبًا</span></div>
<div class="mrow"><span class="l">عن شهر</span><span class="v">مارس 2026</span></div>
<div class="mrow"><span class="l">عدد الحصص</span><span class="v"><span class="num">12</span> حصة</span></div>
<div class="mrow"><span class="l">القيمة</span><span class="v"><span class="num">900</span> ج.م</span></div>
</div>
<div class="mcard" style="border-inline-start:3px solid var(--rose);">
<div class="mcard-t"><span>غرامة تأخير سداد</span><span class="chip due">متأخر</span></div>
<div class="mrow"><span class="l">السبب</span><span class="v">تأخر قسط يناير</span></div>
<div class="mrow"><span class="l">القيمة</span><span class="v"><span class="num">350</span> ج.م</span></div>
</div>
<div style="margin:18px 16px 22px;background:#fff;border:1px solid var(--line);border-radius:16px;padding:16px;">
<div class="mrow"><span class="l">إجمالي المحدد</span><span class="v" style="font-size:1.15rem;color:var(--navy);"><span class="num">2,750</span> ج.م</span></div>
<div style="height:10px;"></div>
<div class="mbtn">سدّد المحدد</div>
<div style="height:8px;"></div>
<div class="mbtn ghost">سدّد بندًا واحدًا فقط</div>
</div>
</div>
<div class="tabbar">
<div class="tab on"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div><div class="tab"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div><div class="tab"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
<!-- 4. PAY -->
<div class="scr" data-scr="pay" style="display:none;flex-direction:column;height:100%;">
<div class="sbar"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd" style="padding-top:4px;">
<div style="display:flex;align-items:center;gap:12px;">
<span class="app-back"></span><h3 style="color:var(--navy);">إتمام السداد</h3>
</div>
</div>
<div class="mcard" style="background:linear-gradient(150deg,#0C2247,#123566);border:none;color:#fff;">
<div style="font-size:.74rem;opacity:.7;">المبلغ المطلوب سداده</div>
<div style="font-family:var(--f-head);font-size:2.1rem;font-weight:900;color:var(--gold-2);line-height:1.2;">
<span class="num">2,750</span> <span style="font-size:.85rem;color:#C0D2EA;">ج.م</span>
</div>
<div style="font-size:.7rem;opacity:.65;margin-top:4px;">بندان مستحقان &mdash; عضوية 10245</div>
</div>
<div style="margin:0 16px 10px;font-size:.78rem;font-weight:700;color:var(--txt-2);">اختر وسيلة الدفع</div>
<div class="mcard" style="border:1.5px solid var(--gold);display:flex;align-items:center;gap:12px;">
<div class="sport-ico" style="background:var(--gold-dim);"><svg class="ico"><use href="#i-card"/></svg></div>
<div><div style="font-weight:700;font-size:.85rem;">بطاقة بنكية</div><div style="font-size:.7rem;color:var(--txt-3);">Visa &bull; Mastercard &bull; Meeza</div></div>
<div style="margin-inline-start:auto;width:20px;height:20px;border-radius:50%;background:var(--gold);color:#fff;display:grid;place-items:center;font-size:.65rem;"><svg class="ico" style="width:11px;height:11px;stroke-width:2.8"><use href="#i-check"/></svg></div>
</div>
<div class="mcard" style="display:flex;align-items:center;gap:12px;">
<div class="sport-ico"><svg class="ico"><use href="#i-wallet"/></svg></div>
<div><div style="font-weight:700;font-size:.85rem;">محفظة إلكترونية</div><div style="font-size:.7rem;color:var(--txt-3);">فودافون كاش &bull; إنستاباي</div></div>
</div>
<div class="mcard" style="display:flex;align-items:center;gap:12px;">
<div class="sport-ico"><svg class="ico"><use href="#i-bank"/></svg></div>
<div><div style="font-weight:700;font-size:.85rem;">الدفع في خزنة النادي</div><div style="font-size:.7rem;color:var(--txt-3);">يصدر كود سداد يقدَّم للخزنة</div></div>
</div>
<div style="margin:14px 16px 0;background:var(--mint-dim);border-radius:12px;padding:12px 14px;font-size:.73rem;color:#0E8C60;line-height:1.75;">
<svg class="ico"><use href="#i-check"/></svg> بمجرد نجاح العملية يُحدَّث رصيدك فورًا، ويصلك إيصال إلكتروني، ويُسجَّل التحصيل في تقرير الخزنة.
</div>
<div style="margin:16px;"><div class="mbtn">ادفع <span class="num">2,750</span> ج.م</div></div>
</div>
</div>
<!-- 5. QR -->
<div class="scr" data-scr="qr" style="display:none;flex-direction:column;height:100%;background:linear-gradient(170deg,#0C2247,#07142B);">
<div class="sbar on-dark"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body" style="color:#fff;padding:10px 0 20px;">
<div style="text-align:center;padding:6px 20px 20px;">
<h3 style="font-family:var(--f-head);font-size:1.1rem;">كود الدخول</h3>
<p style="font-size:.74rem;color:#9FB6D4;margin-top:4px;">وجّه الكود إلى قارئ البوابة</p>
</div>
<div class="qr-box"><div class="qr-grid" data-qr></div></div>
<div style="text-align:center;margin-top:16px;">
<div style="font-family:var(--f-head);letter-spacing:.2em;color:var(--gold-2);font-size:1rem;" class="num">10245</div>
<div style="font-size:.76rem;color:#B9CCE6;margin-top:2px;">محمد أحمد سيد &mdash; عضو عامل</div>
<div style="display:inline-flex;align-items:center;gap:7px;margin-top:12px;background:rgba(22,185,129,.16);border:1px solid rgba(22,185,129,.4);color:#5EE3B0;padding:5px 16px;border-radius:99px;font-size:.72rem;font-weight:700;">
● العضوية سارية &mdash; الدخول مسموح
</div>
<p style="font-size:.7rem;color:#7E96B8;margin-top:14px;">يتجدد الكود تلقائيًا كل <span class="num">60</span> ثانية لمنع إعادة الاستخدام</p>
</div>
<div style="margin:24px 16px 0;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:16px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:.78rem;font-weight:700;">آخر عمليات الدخول</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:.74rem;color:#B9CCE6;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.08);">
<span>البوابة الرئيسية</span><span class="num">اليوم 08:14</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:.74rem;color:#B9CCE6;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.08);">
<span>بوابة حمام السباحة</span><span class="num">أمس 17:02</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:.74rem;color:#B9CCE6;padding:6px 0;">
<span>البوابة الرئيسية &mdash; ومعه ضيف</span><span class="num">أمس 16:58</span>
</div>
</div>
</div>
<div class="tabbar" style="background:#0A1A33;border-top-color:rgba(255,255,255,.1);">
<div class="tab" style="color:#6F87A8;"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div>
<div class="tab" style="color:#6F87A8;"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab on" style="color:var(--gold-2);"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div>
<div class="tab" style="color:#6F87A8;"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
<!-- 6. INVITES -->
<div class="scr" data-scr="invites" style="display:none;flex-direction:column;height:100%;">
<div class="sbar"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd" style="padding-top:4px;">
<div style="display:flex;align-items:center;gap:12px;">
<span class="app-back"></span><h3 style="color:var(--navy);">الدعوات</h3>
</div>
</div>
<div class="mcard" style="text-align:center;background:linear-gradient(150deg,#FFFBEF,#fff);border-color:rgba(201,162,39,.35);">
<div style="font-size:.76rem;color:var(--txt-3);">رصيدك المتاح لشهر مارس</div>
<div style="font-family:var(--f-head);font-size:2.4rem;font-weight:900;color:var(--gold);line-height:1.2;">
<span class="num">3</span> <span style="font-size:.9rem;color:var(--txt-3);">من <span class="num">4</span> دعوات</span>
</div>
<div style="display:flex;gap:6px;justify-content:center;margin:10px 0 14px;">
<span style="width:34px;height:6px;border-radius:99px;background:var(--gold);"></span>
<span style="width:34px;height:6px;border-radius:99px;background:var(--gold);"></span>
<span style="width:34px;height:6px;border-radius:99px;background:var(--gold);"></span>
<span style="width:34px;height:6px;border-radius:99px;background:var(--paper-3);"></span>
</div>
<p style="font-size:.7rem;color:var(--txt-3);">يتجدد الرصيد أول كل شهر ميلادي حسب نوع عضويتك</p>
</div>
<div style="margin:0 16px 12px;"><div class="mbtn navy">إصدار دعوة جديدة</div></div>
<div class="mcard" style="border:1.5px dashed rgba(201,162,39,.5);background:#FFFDF6;">
<div class="mcard-t"><span>نفد رصيدك؟ اشترِ دعوات إضافية</span></div>
<div class="mrow"><span class="l">دعوة إضافية (بالغ)</span><span class="v"><span class="num">150</span> ج.م</span></div>
<div class="mrow"><span class="l">دعوة إضافية (طفل تحت ١٢)</span><span class="v"><span class="num">75</span> ج.م</span></div>
<div class="mrow"><span class="l">الحد الأقصى للشراء شهريًا</span><span class="v"><span class="num">10</span> دعوات</span></div>
<div style="height:12px;"></div>
<div class="mbtn">اشترِ دعوات</div>
</div>
<div class="mcard" style="margin-bottom:22px;">
<div class="mcard-t"><span>دعوات هذا الشهر</span></div>
<div class="mrow"><span class="l">أحمد سامي &mdash; <span class="num">15/03</span></span><span class="v"><span class="chip ok">تم الدخول</span></span></div>
<div class="mrow"><span class="l">دعوة مفتوحة &mdash; لم تُستخدم</span><span class="v"><span class="chip info">سارية اليوم</span></span></div>
</div>
</div>
<div class="tabbar">
<div class="tab"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div><div class="tab"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab on"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div><div class="tab"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
<!-- 7. SPORTS -->
<div class="scr" data-scr="sports" style="display:none;flex-direction:column;height:100%;">
<div class="sbar"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd" style="padding-top:4px;">
<h3 style="color:var(--navy);">الأنشطة الرياضية</h3>
<div class="sm" style="color:var(--txt-3);">اشترك مباشرة &mdash; الأسعار معلنة</div>
</div>
<div class="seg"><button class="on">الكل</button><button>سباحة</button><button>كرة</button><button>فردي</button></div>
<div class="sport-item">
<div class="sport-ico" style="background:rgba(62,140,224,.12);"><svg class="ico"><use href="#i-swim"/></svg></div>
<div><div class="nm">سباحة مبتدئين</div><div class="ds">٦ - ٩ سنوات &bull; <span class="num">12</span> حصة/شهر</div></div>
<div class="pr"><b class="num">900</b><span>ج.م / شهر</span></div>
</div>
<div class="sport-item">
<div class="sport-ico" style="background:rgba(62,140,224,.12);"><svg class="ico"><use href="#i-swim"/></svg></div>
<div><div class="nm">سباحة متقدم</div><div class="ds">١٠ - ١٤ سنة &bull; <span class="num">16</span> حصة/شهر</div></div>
<div class="pr"><b class="num">1,200</b><span>ج.م / شهر</span></div>
</div>
<div class="sport-item">
<div class="sport-ico" style="background:var(--mint-dim);"><svg class="ico"><use href="#i-football"/></svg></div>
<div><div class="nm">كرة القدم &mdash; الموهبة</div><div class="ds">٨ - ١١ سنة &bull; <span class="num">12</span> حصة/شهر</div></div>
<div class="pr"><b class="num">1,100</b><span>ج.م / شهر</span></div>
</div>
<div class="sport-item">
<div class="sport-ico" style="background:var(--rose-dim);"><svg class="ico"><use href="#i-karate"/></svg></div>
<div><div class="nm">كاراتيه مبتدئين</div><div class="ds">٦ - ١٢ سنة &bull; <span class="num">8</span> حصص/شهر</div></div>
<div class="pr"><b class="num">700</b><span>ج.م / شهر</span></div>
</div>
<div class="sport-item" style="opacity:.6;">
<div class="sport-ico" style="background:var(--gold-dim);"><svg class="ico"><use href="#i-tennis"/></svg></div>
<div><div class="nm">تنس أرضي &mdash; أطفال</div><div class="ds"><span class="chip due">اكتمل العدد &mdash; قائمة انتظار</span></div></div>
<div class="pr"><b class="num">1,400</b><span>ج.م / شهر</span></div>
</div>
<div class="sport-item">
<div class="sport-ico" style="background:rgba(201,162,39,.12);"><svg class="ico"><use href="#i-gym"/></svg></div>
<div><div class="nm">جمباز فني &mdash; سيدات</div><div class="ds">١٢ - ١٨ سنة &bull; <span class="num">12</span> حصة/شهر</div></div>
<div class="pr"><b class="num">1,000</b><span>ج.م / شهر</span></div>
</div>
<div style="margin:14px 16px 22px;background:var(--paper-3);border-radius:12px;padding:12px 14px;font-size:.73rem;color:var(--txt-2);line-height:1.75;">
عند الاشتراك تختار الابن، ويصدر النظام فاتورة الشهر الأول + رسوم الانضمام فورًا، ثم فاتورة شهرية تلقائية بعدها.
</div>
</div>
<div class="tabbar">
<div class="tab"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div><div class="tab on"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div><div class="tab"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
<!-- 8. SCHEDULE -->
<div class="scr" data-scr="schedule" style="display:none;flex-direction:column;height:100%;">
<div class="sbar"><span class="num">9:41</span><span>▮▮▮ ⏻</span></div>
<div class="scr-body">
<div class="app-hd" style="padding-top:4px;">
<div style="display:flex;align-items:center;gap:12px;">
<span class="app-back"></span>
<div><h3 style="color:var(--navy);">جدول التدريب</h3>
<div class="sm" style="color:var(--txt-3);">يوسف محمد &mdash; سباحة مبتدئين</div></div>
</div>
</div>
<div class="mcard" style="display:flex;gap:12px;align-items:center;">
<div class="sport-ico" style="background:rgba(62,140,224,.12);"><svg class="ico"><use href="#i-swim"/></svg></div>
<div style="flex:1;">
<div style="font-weight:700;font-size:.85rem;">مجموعة أ &mdash; صباحي</div>
<div style="font-size:.7rem;color:var(--txt-3);">كابتن هشام صلاح &bull; حمام السباحة الأولمبي</div>
</div>
<span class="chip ok">نشط</span>
</div>
<div class="tt">
<div class="tt-d">السبت</div>
<div class="tt-r"><span class="tm">09:00 &mdash; 10:00</span><span>حمام السباحة الأولمبي</span></div>
</div>
<div class="tt">
<div class="tt-d">الاثنين</div>
<div class="tt-r"><span class="tm">09:00 &mdash; 10:00</span><span>حمام السباحة الأولمبي</span></div>
</div>
<div class="tt">
<div class="tt-d">الأربعاء</div>
<div class="tt-r"><span class="tm">09:00 &mdash; 10:00</span><span>حمام السباحة الأولمبي</span></div>
<div class="tt-r" style="background:var(--gold-dim);"><span class="tm"></span><span style="font-size:.72rem;color:#9A7B12;">ملاحظة المدرب: حصة قياس مستوى</span></div>
</div>
<div class="mcard" style="margin-top:6px;margin-bottom:22px;background:var(--paper-2);">
<div class="mcard-t"><span>تغييرات على الجدول</span></div>
<div style="font-size:.75rem;color:var(--txt-2);line-height:1.8;">
▸ أُلغيت حصة الاثنين <span class="num">10/03</span> &mdash; صيانة حمام السباحة.<br>
▸ حصة تعويضية الجمعة <span class="num">14/03</span> الساعة <span class="num">10:00</span>.
</div>
</div>
</div>
<div class="tabbar">
<div class="tab"><i><svg class="ico"><use href="#i-home"/></svg></i>الرئيسية</div><div class="tab on"><i><svg class="ico"><use href="#i-activity"/></svg></i>الأنشطة</div>
<div class="tab"><i><svg class="ico"><use href="#i-gate"/></svg></i>الدخول</div><div class="tab"><i><svg class="ico"><use href="#i-user"/></svg></i>حسابي</div>
</div>
</div>
</div>
<iframe id="proto" class="screen" src="app/index.html#/home"
title="النموذج التفاعلي لتطبيق أعضاء نادي الصيد المصري"></iframe>
</div>
<div style="flex:1;min-width:260px;max-width:360px;">
<div id="scr-note"></div>
<div class="proto-tools">
<button class="pbtn ghost" id="proto-theme">تبديل المظهر</button>
<button class="pbtn ghost" id="proto-reset">إعادة ضبط النموذج</button>
<a class="pbtn" href="app/index.html#/home" target="_blank" rel="noopener">فتح بملء الشاشة</a>
</div>
</div>
</div>
</div>
......
......@@ -42,51 +42,52 @@
g.appendChild(frag);
});
/* ---------- mobile screen switcher ---------- */
/* ---------- live prototype controller ----------
The phone is an iframe running the real prototype. Clicks in the side list
drive it over postMessage; the prototype reports its route back so the list
and the annotation stay in sync when someone navigates inside the phone. */
var NOTES = {
login: {
t: 'الدخول برقم الموبايل',
p: 'يدخل العضو برقم موبايله المسجَّل في بيانات النادي، دون إنشاء حساب أو كلمة مرور، ودون تفعيل يدوي من أي موظف.',
b: ['التعرف الفوري على العضو من ملف الأعضاء المرفوع', 'كود تحقق OTP صالح لدقيقتين', 'جهاز واحد نشط لكل عضوية لمنع تداول الحساب بين أكثر من شخص']
},
home: {
t: 'الشاشة الرئيسية',
p: 'تعرض الشاشة حالة العضوية وإجمالي المستحق وطريقة السداد في موضع واحد، وتغني البطاقة الرقمية عن الكارنيه الورقي.',
b: ['حالة العضوية وتاريخ سريانها', 'إجمالي المستحق مع تنبيه التأخير', 'اشتراكات الأبناء ورصيد الدعوات']
},
dues: {
t: 'بيان المستحقات',
p: 'يظهر كل بند مستحق على حدة: عن أي شهر هو، وسببه، وتاريخ استحقاقه، بدلًا من مبلغ إجمالي غير مفصَّل يدفع العضو إلى الاستفسار وتأجيل السداد.',
b: ['تجديدات وأقساط وغرامات وفواتير أنشطة', 'سداد بند واحد أو عدة بنود معًا', 'أرشيف كامل للمسدَّد مع إيصالاته']
},
pay: {
t: 'وسائل السداد',
p: 'بطاقة بنكية أو محفظة إلكترونية، أو كود سداد يُقدَّم لخزنة النادي لمن يفضّل الدفع نقدًا. وتُسجَّل الوسائل الثلاث في التقرير نفسه.',
b: ['Visa و Mastercard و Meeza', 'فودافون كاش و إنستاباي', 'كود سداد للخزنة يربط الدفع النقدي بالنظام']
},
qr: {
t: 'الدخول بكود QR',
p: 'يتجدد الكود كل ستين ثانية، فلا يصلح تصويره وإرساله لغير العضو، ويعرض القارئ على البوابة حالة العضوية فور المسح.',
b: ['منع الدخول عند إيقاف العضوية أو تجاوز حد التأخير', 'سجل دخول لحظي لكل بوابة', 'يعمل حتى لو انقطع الإنترنت عن هاتف العضو']
},
invites: {
t: 'رصيد الدعوات وبيع الإضافي',
p: 'رصيد شهري محدد لكل نوع عضوية، وبيع منظَّم للدعوات الإضافية من التطبيق بدلًا من التقدير الشخصي عند البوابة.',
b: ['رصيد مجاني يتجدد شهريًا', 'شراء دعوات إضافية بحد أقصى', 'ربط اختياري بين الدعوة وانتظام السداد']
},
sports: {
t: 'كتالوج الأنشطة والاشتراك',
p: 'يعطّل الاشتراكَ في الأنشطة أن ولي الأمر لا يعرف السعر ولا الأماكن المتاحة، ويعالج الكتالوج المعلن هذه النقطة.',
b: ['أسعار وفئات سنية وعدد حصص معلن', 'السعة المتاحة وقائمة انتظار عند اكتمال العدد', 'فاتورة شهرية تلقائية بعد الاشتراك']
},
schedule: {
t: 'جداول التدريب',
p: 'يرفع المدرب جدول مجموعته من قالب موحَّد، فيظهر لكل مشترك في تطبيقه، ويصل تنبيه فوري عند أي إلغاء أو حصة تعويضية.',
b: ['جدول أسبوعي لكل لاعب على حدة', 'ملاحظات المدرب على الحصة', 'إشعار فوري بالتغييرات والحصص التعويضية']
}
login: { t:'الدخول برقم الموبايل',
p:'يدخل العضو برقم موبايله المسجَّل في بيانات النادي، دون إنشاء حساب أو كلمة مرور، ودون تفعيل يدوي من أي موظف.',
b:['التعرف الفوري على العضو من ملف الأعضاء المرفوع','كود تحقق OTP صالح لدقيقتين','جهاز واحد نشط لكل عضوية لمنع تداول الحساب'] },
home: { t:'الشاشة الرئيسية',
p:'تعرض حالة العضوية وإجمالي المستحق وطريقة السداد في موضع واحد، وتغني البطاقة الرقمية عن الكارنيه الورقي.',
b:['حالة العضوية وتاريخ سريانها','إجمالي المستحق مع تنبيه التأخير','اشتراكات الأبناء ورصيد الدعوات'] },
dues: { t:'بيان المستحقات',
p:'يظهر كل بند مستحق على حدة بسببه وتاريخه. حدّدوا البنود داخل الشاشة وسيتغير الإجمالي أمامكم قبل السداد.',
b:['تجديدات وأقساط وغرامات وفواتير أنشطة','سداد بند واحد أو عدة بنود معًا','الإجمالي يُحتسب لحظيًا حسب ما تختارونه'] },
pay: { t:'وسائل السداد',
p:'بطاقة بنكية أو محفظة إلكترونية، أو كود سداد يُقدَّم لخزنة النادي لمن يفضّل الدفع نقدًا. وتُسجَّل الوسائل الثلاث في التقرير نفسه.',
b:['Visa و Mastercard و Meeza','فودافون كاش و إنستاباي','كود سداد للخزنة يربط الدفع النقدي بالنظام'] },
qr: { t:'الدخول بكود QR',
p:'يتجدد الكود كل ستين ثانية أمامكم في النموذج، فلا يصلح تصويره وإرساله لغير العضو، ويعرض القارئ حالة العضوية فور المسح.',
b:['منع الدخول عند إيقاف العضوية أو تجاوز حد التأخير','سجل دخول لحظي لكل بوابة','يعمل حتى لو انقطع الإنترنت عن هاتف العضو'] },
invites: { t:'رصيد الدعوات وبيع الإضافي',
p:'رصيد شهري محدد لكل نوع عضوية. جرّبوا إصدار دعوة أو شراء دعوات إضافية وسيتغير الرصيد فورًا.',
b:['رصيد مجاني يتجدد شهريًا','شراء دعوات إضافية بحد أقصى','ربط اختياري بين الدعوة وانتظام السداد'] },
activities: { t:'كتالوج الأنشطة والاشتراك',
p:'يعطّل الاشتراكَ في الأنشطة أن ولي الأمر لا يعرف السعر ولا الأماكن المتاحة، ويعالج الكتالوج المعلن هذه النقطة.',
b:['أسعار وفئات سنية وعدد حصص معلن','السعة المتاحة وقائمة انتظار عند اكتمال العدد','تصفية حسب نوع النشاط'] },
activity: { t:'تفاصيل النشاط والاشتراك',
p:'اضغطوا «اشترك الآن» داخل النموذج: يختار ولي الأمر الابن، فتصدر فاتورة الشهر الأول ورسوم الانضمام وتظهر مباشرة في المستحقات.',
b:['اختيار الابن المشترك من أبناء العضوية','فاتورة فورية بالشهر الأول + رسوم الانضمام','تحديث السعة والأماكن المتاحة'] },
schedule: { t:'جداول التدريب',
p:'يرفع المدرب جدول مجموعته من قالب موحَّد، فيظهر لكل مشترك في تطبيقه، ويصل تنبيه فوري عند أي إلغاء أو حصة تعويضية.',
b:['جدول أسبوعي لكل لاعب على حدة','ملاحظات المدرب على الحصة','إشعار فوري بالتغييرات والحصص التعويضية'] },
receipts: { t:'الإيصالات',
p:'كل عملية سداد تُنشئ إيصالًا فوريًا داخل التطبيق. جرّبوا السداد من شاشة المستحقات وسيظهر الإيصال هنا.',
b:['إيصال فوري بعد كل عملية','أرشيف كامل بوسيلة الدفع والتاريخ','قابل للتحميل بصيغة PDF'] },
notifications: { t:'الإشعارات',
p:'تذكير قبل موعد الاستحقاق وبعده، وتنبيه بتغييرات جدول التدريب وبتجديد اشتراكات الأبناء.',
b:['تذكير بالاستحقاق قبل موعده وبعده','تنبيه بتغييرات الجداول','إشعار بتأكيد السداد'] },
profile: { t:'حسابي والإعدادات',
p:'بطاقة العضوية الرقمية ومداخل الإيصالات والدعوات والجداول، مع تبديل المظهر بين الفاتح والداكن.',
b:['بطاقة عضوية رقمية','مظهر فاتح وداكن','إعادة ضبط النموذج للعرض من جديد'] }
};
var noteBox = document.getElementById('scr-note');
var frame = document.getElementById('proto');
var pnBtns = Array.prototype.slice.call(document.querySelectorAll('.pn-item[data-route]'));
function renderNote(k) {
var n = NOTES[k];
......@@ -94,26 +95,47 @@
noteBox.innerHTML =
'<div class="card" style="border-color:var(--line-2);">' +
'<div class="card-ico"><svg class="ico"><use href="#i-note"/></svg></div>' +
'<h4>' + n.t + '</h4>' +
'<p>' + n.p + '</p>' +
'<h4>' + n.t + '</h4><p>' + n.p + '</p>' +
'<ul class="scope-list" style="margin-top:14px;">' +
n.b.map(function (x) { return '<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>' + x + '</span></li>'; }).join('') +
'</ul>' +
'</div>';
n.b.map(function (x) {
return '<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>' + x + '</span></li>';
}).join('') +
'</ul></div>';
}
var scrBtns = document.querySelectorAll('.pn-item[data-scr]');
scrBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
var k = btn.dataset.scr;
scrBtns.forEach(function (b) { b.classList.toggle('on', b === btn); });
document.querySelectorAll('.scr').forEach(function (s) {
s.style.display = (s.dataset.scr === k) ? 'flex' : 'none';
});
renderNote(k);
function syncNav(route) {
var base = String(route).replace(/^#\//, '').split('/')[0];
pnBtns.forEach(function (b) {
b.classList.toggle('on', b.dataset.route.split('/')[0] === base);
});
renderNote(base);
}
pnBtns.forEach(function (b) {
b.addEventListener('click', function () {
var r = '#/' + b.dataset.route;
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ type: 'sayd:goto', route: r }, '*');
syncNav(r);
});
});
window.addEventListener('message', function (e) {
var d = e.data || {};
if (d.type === 'sayd:route') syncNav(d.route);
});
var themeBtn = document.getElementById('proto-theme');
var resetBtn = document.getElementById('proto-reset');
var protoDark = true;
if (themeBtn) themeBtn.addEventListener('click', function () {
protoDark = !protoDark;
frame.contentWindow.postMessage({ type: 'sayd:theme', value: protoDark ? 'dark' : 'light' }, '*');
});
renderNote('login');
if (resetBtn) resetBtn.addEventListener('click', function () {
frame.contentWindow.postMessage({ type: 'sayd:reset' }, '*');
});
renderNote('home');
/* ---------- portal screen switcher ---------- */
var URLS = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment