| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>البروفايل — نيّة</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600&display=swap'); |
| |
| :root{ |
| --bg:#FFFFFF; |
| --ink:#0A0A0A; |
| --line: rgba(10,10,10,0.12); |
| --line-strong: rgba(10,10,10,0.22); |
| --muted: rgba(10,10,10,0.55); |
| --muted-soft: rgba(10,10,10,0.4); |
| --surface: rgba(10,10,10,0.03); |
| --surface-hover: rgba(10,10,10,0.05); |
| --radius: 14px; |
| --max-w: 1080px; |
| } |
| *{ box-sizing:border-box; } |
| html,body{ margin:0; padding:0; } |
| |
| |
| @media (prefers-reduced-motion: no-preference){ |
| body{ animation: pageFadeIn .28s ease both; } |
| .card, .interest-item, .grid > *{ animation: cardRise .32s ease both; } |
| .overlay.show .modal{ animation: modalPop .22s cubic-bezier(.2,.8,.3,1.2) both; } |
| .toast.show{ animation: toastRise .3s ease both; } |
| } |
| @keyframes pageFadeIn{ from{ opacity:0; transform:translateY(6px);} to{ opacity:1; transform:none; } } |
| @keyframes cardRise{ from{ opacity:0; transform:translateY(8px);} to{ opacity:1; transform:none; } } |
| @keyframes modalPop{ from{ opacity:0; transform:scale(.96) translateY(8px);} to{ opacity:1; transform:none; } } |
| @keyframes toastRise{ from{ opacity:0; transform:translateX(-50%) translateY(14px);} to{ opacity:1; transform:translateX(-50%) translateY(0);} } |
| .btn, .nav-link, .file-drop, .interest-item, .card{ transition: all .18s ease; } |
| .btn:active{ transform: scale(.96); } |
| .btn-primary:active{ opacity:.85; } |
| a.card:hover, .card:hover{ box-shadow: 0 4px 18px rgba(10,10,10,0.06); } |
| input, textarea, select{ transition: border-color .15s ease, box-shadow .15s ease; } |
| input:focus, textarea:focus, select:focus{ box-shadow: 0 0 0 3px rgba(10,10,10,0.06); } |
| ::-webkit-scrollbar{ width:8px; height:8px; } |
| ::-webkit-scrollbar-thumb{ background: rgba(10,10,10,0.15); border-radius:8px; } |
| @supports (padding: max(0px)){ |
| body{ padding-bottom: env(safe-area-inset-bottom); } |
| .topbar{ padding-top: max(18px, env(safe-area-inset-top)); } |
| } |
| @media (max-width:720px){ |
| .btn{ font-size:14px; } |
| .card{ border-radius:16px; } |
| .modal{ border-radius:20px 20px 0 0; position:fixed; bottom:0; left:0; right:0; max-width:100%; max-height:85vh; margin:0; } |
| .overlay{ align-items:flex-end; padding:0; } |
| .overlay.show .modal{ animation: sheetUp .25s cubic-bezier(.2,.8,.3,1) both; } |
| } |
| @keyframes sheetUp{ from{ transform:translateY(100%);} to{ transform:translateY(0);} } |
| body{ |
| background: var(--bg); |
| color: var(--ink); |
| font-family:'IBM Plex Sans Arabic', sans-serif; |
| min-height:100vh; |
| -webkit-font-smoothing: antialiased; |
| line-height:1.6; |
| } |
| h1,h2,h3,.display,.btn,.brand-name{ font-family:'Cairo', sans-serif; } |
| a{ color: inherit; text-decoration:none; } |
| img{ max-width:100%; display:block; } |
| |
| |
| .page{ max-width: var(--max-w); margin: 0 auto; padding: 0 20px 60px; } |
| .center-page{ max-width: 440px; margin: 0 auto; padding: 20px 20px 60px; } |
| |
| |
| .topbar{ |
| display:flex; justify-content:space-between; align-items:center; |
| padding: 18px 24px; border-bottom: 1px solid var(--line); |
| position: sticky; top:0; background: var(--bg); z-index: 40; |
| } |
| .brand{ display:flex; align-items:center; gap:9px; } |
| .brand-mark{ |
| width:30px; height:30px; border-radius:50%; |
| border: 2px solid var(--ink); position:relative; flex-shrink:0; |
| } |
| .brand-mark::after{ |
| content:''; position:absolute; inset:7px; border-radius:50%; background: var(--ink); |
| } |
| .brand-name{ font-weight:800; font-size:20px; letter-spacing:0.3px; } |
| .nav-links{ display:flex; align-items:center; gap:6px; } |
| .nav-link{ |
| padding:9px 14px; border-radius:999px; font-size:14px; font-weight:600; color: var(--muted); |
| transition: all .15s ease; |
| } |
| .nav-link:hover{ background: var(--surface); color: var(--ink); } |
| .nav-link.active{ background: var(--ink); color: var(--bg); } |
| .hamburger{ display:none; } |
| |
| @media (max-width:720px){ |
| .nav-links{ display:none; } |
| .hamburger{ display:block; } |
| } |
| |
| |
| .btn{ |
| display:inline-flex; align-items:center; justify-content:center; gap:8px; |
| padding: 13px 26px; border-radius: 11px; border: 1.5px solid var(--ink); cursor:pointer; |
| font-weight:700; font-size:14.5px; transition: all .15s ease; background: transparent; color: var(--ink); |
| } |
| .btn-primary{ background: var(--ink); color: var(--bg); } |
| .btn-primary:hover{ opacity:0.82; transform: translateY(-1px); } |
| .btn-ghost{ background: transparent; color: var(--ink); } |
| .btn-ghost:hover{ background: var(--surface); } |
| .btn-block{ width:100%; } |
| .btn:disabled{ opacity:0.4; cursor:not-allowed; } |
| .btn-sm{ padding: 9px 16px; font-size:13px; border-radius:9px; } |
| |
| |
| .card{ |
| background: var(--bg); border: 1.5px solid var(--line); border-radius: var(--radius); |
| padding: 22px; transition: border-color .15s ease, transform .15s ease; |
| } |
| .card:hover{ border-color: var(--line-strong); } |
| |
| |
| .field{ margin-bottom:18px; } |
| .field label{ display:block; font-size:13.5px; font-weight:700; margin-bottom:8px; } |
| .field .hint{ font-size:12px; color: var(--muted-soft); margin-top:6px; line-height:1.6; } |
| input, textarea, select{ |
| width:100%; padding: 13px 15px; border-radius:10px; border: 1.5px solid var(--line); |
| background: var(--bg); color: var(--ink); font-family:inherit; font-size:14.5px; |
| } |
| input:focus, textarea:focus, select:focus{ outline:none; border-color: var(--ink); } |
| textarea{ resize:vertical; min-height:100px; } |
| .field-row{ display:flex; gap:14px; } |
| .field-row .field{ flex:1; } |
| @media (max-width:520px){ .field-row{ flex-direction:column; gap:0; } } |
| |
| .file-drop{ |
| border: 1.5px dashed var(--line-strong); border-radius:12px; padding: 26px 16px; |
| text-align:center; cursor:pointer; transition: all .15s ease; font-size:13.5px; color: var(--muted); |
| } |
| .file-drop:hover{ border-color: var(--ink); background: var(--surface); } |
| .file-list{ display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; } |
| .file-thumb{ |
| width:64px; height:64px; border-radius:8px; overflow:hidden; border:1px solid var(--line); position:relative; |
| } |
| .file-thumb img{ width:100%; height:100%; object-fit:cover; } |
| .file-chip{ |
| display:flex; align-items:center; gap:6px; padding:6px 12px; border:1px solid var(--line); |
| border-radius:999px; font-size:12px; |
| } |
| |
| |
| .grid{ display:grid; grid-template-columns: repeat(auto-fill, minmax(290px,1fr)); gap:18px; } |
| .section-head{ display:flex; justify-content:space-between; align-items:flex-end; margin: 30px 0 20px; flex-wrap:wrap; gap:10px; } |
| .section-head h2{ font-size:22px; font-weight:800; margin:0; } |
| .muted{ color: var(--muted); } |
| .badge{ |
| display:inline-block; font-size:11px; font-weight:700; padding:4px 11px; border-radius:999px; |
| border: 1px solid var(--line-strong); color: var(--muted); |
| } |
| .badge-ink{ background: var(--ink); color: var(--bg); border-color: var(--ink); } |
| .empty{ text-align:center; padding: 60px 20px; color: var(--muted-soft); border: 1.5px dashed var(--line); border-radius: var(--radius); } |
| .divider{ height:1px; background: var(--line); margin: 24px 0; border:none; } |
| |
| .progress-track{ height:8px; background: var(--surface); border-radius:999px; overflow:hidden; } |
| .progress-fill{ height:100%; width:0; background: var(--ink); border-radius:999px; transition: width .4s ease; } |
| |
| .toast{ |
| position:fixed; bottom:26px; left:50%; transform:translateX(-50%) translateY(20px); |
| background: var(--ink); color: var(--bg); padding:14px 24px; border-radius:12px; |
| font-family:'Cairo',sans-serif; font-weight:700; font-size:14px; opacity:0; transition: all .3s ease; |
| pointer-events:none; z-index:100; white-space:nowrap; max-width: 90vw; text-align:center; |
| } |
| .toast.show{ opacity:1; transform:translateX(-50%) translateY(0); } |
| |
| .spinner{ |
| width:20px; height:20px; border:2.5px solid var(--line); border-top-color: var(--ink); |
| border-radius:50%; animation: spin .7s linear infinite; margin: 0 auto; |
| } |
| @keyframes spin{ to{ transform: rotate(360deg); } } |
| |
| .loading-row{ display:flex; justify-content:center; padding: 40px 0; } |
| |
| |
| .overlay{ |
| position:fixed; inset:0; background: rgba(10,10,10,0.5); |
| display:none; align-items:center; justify-content:center; z-index:60; padding:20px; |
| } |
| .overlay.show{ display:flex; } |
| .modal{ |
| background: var(--bg); border: 1.5px solid var(--ink); border-radius: 18px; |
| width:100%; max-width:460px; padding:26px; max-height:88vh; overflow-y:auto; position:relative; |
| } |
| .modal h3{ font-size:19px; margin:0 0 16px; } |
| .close-x{ |
| position:absolute; top:18px; left:18px; background:none; border:none; font-size:19px; cursor:pointer; opacity:0.5; |
| } |
| .close-x:hover{ opacity:1; } |
| |
| |
| .footer-note{ text-align:center; padding: 40px 20px 30px; color: var(--muted-soft); font-size:12.5px; } |
| |
| @media (max-width: 640px){ |
| .topbar{ padding:14px 16px; } |
| .page{ padding: 0 14px 50px; } |
| } |
| |
| |
| |
| .bottom-nav{ display:none; } |
| @media (max-width:720px){ |
| body{ padding-bottom: calc(64px + env(safe-area-inset-bottom)); } |
| .bottom-nav{ |
| display:flex; position:fixed; bottom:0; left:0; right:0; z-index:50; |
| background: var(--bg); border-top:1px solid var(--line); |
| padding: 6px 4px calc(6px + env(safe-area-inset-bottom)); |
| box-shadow: 0 -2px 14px rgba(10,10,10,0.05); |
| transform: translateY(0); transition: transform .28s cubic-bezier(.4,0,.2,1); |
| will-change: transform; |
| } |
| .bottom-nav.bn-hidden{ transform: translateY(100%); } |
| .bn-item{ |
| flex:1; display:flex; flex-direction:column; align-items:center; gap:3px; |
| padding:6px 2px; border-radius:12px; color: var(--muted-soft); |
| font-size:10.5px; font-weight:600; transition: all .15s ease; -webkit-tap-highlight-color:transparent; |
| } |
| .bn-item svg{ width:22px; height:22px; stroke: currentColor; fill:none; stroke-width:1.8; transition: transform .15s ease; } |
| .bn-item.active{ color: var(--ink); } |
| .bn-item.active svg{ transform: translateY(-1px) scale(1.05); } |
| .bn-item:active{ transform: scale(.92); } |
| .topbar .nav-links{ display:none !important; } |
| .hamburger{ display:none !important; } |
| .topbar{ padding:14px 18px; } |
| .brand-name{ font-size:18px; } |
| } |
| |
| |
| html{ overflow-x:hidden; } |
| body{ overflow-x:hidden; } |
| img,video{ max-width:100%; height:auto; } |
| *{ word-wrap:break-word; overflow-wrap:break-word; } |
| .page, .center-page{ overflow-x:hidden; } |
| @media (max-width:720px){ |
| .toast{ bottom: calc(78px + env(safe-area-inset-bottom)) !important; max-width: calc(100vw - 32px); } |
| .modal{ max-height: calc(85vh - env(safe-area-inset-bottom)); padding-bottom: calc(26px + env(safe-area-inset-bottom)); } |
| .overlay.show{ z-index:70; } |
| table{ display:block; overflow-x:auto; -webkit-overflow-scrolling:touch; } |
| } |
| |
| |
| :root{ |
| --radius: 16px; --radius-sm: 10px; --radius-lg: 22px; |
| --ease: cubic-bezier(.22,1,.36,1); |
| --shadow-sm: 0 1px 3px rgba(10,10,10,0.05), 0 1px 2px rgba(10,10,10,0.04); |
| --shadow-md: 0 6px 20px rgba(10,10,10,0.07), 0 2px 6px rgba(10,10,10,0.04); |
| --shadow-lg: 0 14px 40px rgba(10,10,10,0.12), 0 4px 12px rgba(10,10,10,0.06); |
| } |
| *{ -webkit-tap-highlight-color: transparent; } |
| html{ scroll-behavior:smooth; } |
| body{ text-rendering:optimizeLegibility; -webkit-font-smoothing:antialiased; } |
| ::selection{ background: var(--ink); color: var(--bg); } |
| ::-webkit-scrollbar-thumb{ border-radius:999px; } |
| a, button, input, textarea, select{ font-family:inherit; } |
| :focus-visible{ outline: 2px solid rgba(10,10,10,0.5); outline-offset:2px; border-radius:6px; } |
| |
| @keyframes fadeSlideUp{ from{ opacity:0; transform:translateY(12px); } to{ opacity:1; transform:none; } } |
| @keyframes fadeIn{ from{ opacity:0; } to{ opacity:1; } } |
| @keyframes popIn{ from{ opacity:0; transform:scale(.96) translateY(6px); } to{ opacity:1; transform:none; } } |
| |
| .topbar{ backdrop-filter:saturate(1.5) blur(10px); -webkit-backdrop-filter:saturate(1.5) blur(10px); background: rgba(255,255,255,0.86); } |
| |
| .btn{ |
| border-radius: var(--radius-sm) !important; transition: transform .18s var(--ease), box-shadow .18s var(--ease), background-color .18s var(--ease), border-color .18s var(--ease), opacity .18s var(--ease); |
| } |
| .btn:hover{ box-shadow: var(--shadow-sm); } |
| .btn:active{ transform: scale(.96); } |
| .btn-primary{ box-shadow: 0 2px 8px rgba(10,10,10,0.16); } |
| .btn-primary:hover{ box-shadow: var(--shadow-md); transform: translateY(-1.5px); } |
| .btn-ghost:hover{ background: var(--surface); } |
| |
| .card{ |
| border-radius: var(--radius) !important; transition: box-shadow .28s var(--ease), transform .28s var(--ease), border-color .28s var(--ease); |
| animation: fadeSlideUp .5s var(--ease) both; |
| } |
| .card:hover{ box-shadow: var(--shadow-md); border-color: var(--line-strong); } |
| .project-card:hover .thumb img{ transform: scale(1.04); } |
| .thumb img{ transition: transform .5s var(--ease); } |
| |
| input, textarea, select{ |
| border-radius: var(--radius-sm) !important; transition: border-color .2s var(--ease), box-shadow .2s var(--ease), background-color .2s var(--ease); |
| } |
| input:focus, textarea:focus, select:focus{ |
| outline:none !important; border-color: var(--ink) !important; box-shadow: 0 0 0 3.5px rgba(10,10,10,0.07); |
| } |
| |
| .badge{ border-radius:999px !important; transition: transform .15s var(--ease); } |
| .modal{ animation: popIn .3s var(--ease) both; border-radius: var(--radius-lg) !important; } |
| .overlay{ transition: opacity .22s var(--ease); } |
| .overlay.show{ animation: fadeIn .22s var(--ease) both; } |
| .toast{ animation: fadeSlideUp .35s var(--ease) both; } |
| |
| .progress-track, .progress-fill{ border-radius:999px !important; } |
| .progress-fill{ transition: width .6s var(--ease) !important; } |
| |
| .stat-box, .stat{ transition: transform .2s var(--ease), box-shadow .2s var(--ease); border-radius: var(--radius-sm) !important; } |
| .stat-box:hover, .stat:hover{ transform: translateY(-2px); box-shadow: var(--shadow-sm); } |
| |
| .filter-chip, .role-card{ transition: all .2s var(--ease); } |
| .role-card:hover{ transform: translateY(-2px); box-shadow: var(--shadow-sm); } |
| |
| @media (max-width:720px){ |
| .btn{ min-height:44px; } |
| input, textarea, select{ font-size:16px !important; } |
| .card{ border-radius: var(--radius-sm) !important; } |
| } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div class="topbar" id="topbar"></div> |
| <div class="page" id="content" style="display:none; max-width:760px;"> |
| <div class="card" id="profile-header" style="margin:24px 0;"></div> |
| <div id="profile-badges" style="margin-bottom:24px;"></div> |
|
|
| <div id="matches-section" style="display:none;"> |
| <div class="section-head"><h2 style="font-size:18px; display:flex; align-items:center; gap:7px;"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="0.6" fill="currentColor" stroke="none"/></svg> مقترحات ليك</h2></div> |
| <div id="matches-content"></div> |
| </div> |
|
|
| <div class="section-head"><h2 style="font-size:18px;">مشاريعه</h2></div> |
| <div class="grid" id="profile-projects"></div> |
|
|
| <div class="section-head"><h2 style="font-size:18px;">التقييمات</h2><button class="btn btn-ghost btn-sm" id="add-review-btn" onclick="openModal('review-overlay')">أضف تقييم</button></div> |
| <div id="profile-reviews"></div> |
| </div> |
|
|
| <div class="loading-row" id="loading"><div class="spinner"></div></div> |
|
|
| |
| <div class="overlay" id="avatar-overlay"> |
| <div class="modal"> |
| <button class="close-x" onclick="closeModal('avatar-overlay')">✕</button> |
| <h3>تغيير صورة البروفايل</h3> |
| <input type="file" id="avatar-file" accept="image/*"> |
| <button class="btn btn-primary btn-block" style="margin-top:14px;" onclick="submitAvatar()">رفع الصورة</button> |
| </div> |
| </div> |
|
|
| |
| <div class="overlay" id="idcard-overlay"> |
| <div class="modal"> |
| <button class="close-x" onclick="closeModal('idcard-overlay')">✕</button> |
| <h3>وثّق حسابك</h3> |
| <p class="muted" style="font-size:13px; margin-top:-8px;">صوّر بطاقة الرقم القومي مباشرة بالكاميرا (وشك ظاهر عليها). هنراجعها يدوياً وتاخد علامة "موثّق" بعد التأكيد.</p> |
| <div id="cam-wrap" style="border-radius:12px; overflow:hidden; background:#000; position:relative;"> |
| <video id="cam-video" autoplay playsinline style="width:100%; display:block;"></video> |
| <img id="cam-shot" style="width:100%; display:none;"> |
| </div> |
| <canvas id="cam-canvas" style="display:none;"></canvas> |
| <p id="cam-error" class="muted" style="font-size:12.5px; color:#c00; display:none; margin-top:8px;"></p> |
| <div style="display:flex; gap:10px; margin-top:14px;"> |
| <button class="btn btn-ghost btn-block" id="cam-retake" style="display:none;" onclick="retakePhoto()">إعادة التصوير</button> |
| <button class="btn btn-primary btn-block" id="cam-capture" onclick="capturePhoto()"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13.5" r="3.2"/></svg> صوّر دلوقتي</button> |
| </div> |
| <button class="btn btn-primary btn-block" id="cam-send" style="display:none; margin-top:10px;" onclick="submitIdCard()">إرسال للمراجعة</button> |
| </div> |
| </div> |
|
|
| |
| <div class="overlay" id="review-overlay"> |
| <div class="modal"> |
| <button class="close-x" onclick="closeModal('review-overlay')">✕</button> |
| <h3>قيّم المستخدم</h3> |
| <div class="field"> |
| <label>التقييم (1-5)</label> |
| <select id="rv-rating"> |
| <option value="5">5 — ممتاز</option> |
| <option value="4">4 — جيد جداً</option> |
| <option value="3">3 — متوسط</option> |
| <option value="2">2 — ضعيف</option> |
| <option value="1">1 — سيء</option> |
| </select> |
| </div> |
| <div class="field"><label>تعليق (اختياري)</label><textarea id="rv-body"></textarea></div> |
| <button class="btn btn-primary btn-block" onclick="submitReview()">إرسال التقييم</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const API = { |
| base: '', |
| |
| token(){ return localStorage.getItem('neya_token'); }, |
| user(){ try{ return JSON.parse(localStorage.getItem('neya_user') || 'null'); }catch(e){ return null; } }, |
| isLoggedIn(){ return !!this.token(); }, |
| |
| setSession(token, user){ |
| localStorage.setItem('neya_token', token); |
| localStorage.setItem('neya_user', JSON.stringify(user)); |
| }, |
| clearSession(){ |
| localStorage.removeItem('neya_token'); |
| localStorage.removeItem('neya_user'); |
| }, |
| |
| async _fetch(path, opts = {}){ |
| const headers = opts.headers || {}; |
| if(this.token()) headers['Authorization'] = 'Bearer ' + this.token(); |
| if(opts.body && !(opts.body instanceof FormData)){ |
| headers['Content-Type'] = 'application/json'; |
| } |
| const res = await fetch(this.base + path, { ...opts, headers }); |
| let data = null; |
| try{ data = await res.json(); }catch(e){ } |
| if(!res.ok){ |
| const msg = (data && data.detail) ? data.detail : 'حصل خطأ، حاول تاني'; |
| const err = new Error(typeof msg === 'string' ? msg : JSON.stringify(msg)); |
| err.status = res.status; |
| throw err; |
| } |
| return data; |
| }, |
| |
| signup(payload){ return this._fetch('/api/auth/signup', { method:'POST', body: JSON.stringify(payload) }); }, |
| login(payload){ return this._fetch('/api/auth/login', { method:'POST', body: JSON.stringify(payload) }); }, |
| me(){ return this._fetch('/api/auth/me'); }, |
| |
| listProjects(){ return this._fetch('/api/projects'); }, |
| createProject(payload){ return this._fetch('/api/projects', { method:'POST', body: JSON.stringify(payload) }); }, |
| uploadFile(projectId, kind, file){ |
| const fd = new FormData(); |
| fd.append('kind', kind); |
| fd.append('file', file); |
| return this._fetch(`/api/projects/${projectId}/files`, { method:'POST', body: fd }); |
| }, |
| |
| listInterests(projectId){ return this._fetch(`/api/projects/${projectId}/interests`); }, |
| addInterest(projectId, payload){ return this._fetch(`/api/projects/${projectId}/interests`, { method:'POST', body: JSON.stringify(payload) }); }, |
| |
| bookAppointment(projectId, payload){ return this._fetch(`/api/projects/${projectId}/appointments`, { method:'POST', body: JSON.stringify(payload) }); }, |
| listAppointments(projectId){ return this._fetch(`/api/projects/${projectId}/appointments`); }, |
| |
| fileUrl(filename){ return `/api/uploads/${filename}`; }, |
| |
| listConversations(){ return this._fetch('/api/conversations'); }, |
| getThread(projectId, withUser){ return this._fetch(`/api/projects/${projectId}/messages?with_user=${encodeURIComponent(withUser)}`); }, |
| sendMessage(projectId, payload){ return this._fetch(`/api/projects/${projectId}/messages`, { method:'POST', body: JSON.stringify(payload) }); }, |
| |
| getUserProfile(userId){ return this._fetch(`/api/users/${userId}/profile`); }, |
| getUserProjects(userId){ return this._fetch(`/api/users/${userId}/projects`); }, |
| listReviews(userId){ return this._fetch(`/api/users/${userId}/reviews`); }, |
| myMatches(){ return this._fetch('/api/users/me/matches'); }, |
| addReview(userId, payload){ return this._fetch(`/api/users/${userId}/reviews`, { method:'POST', body: JSON.stringify(payload) }); }, |
| submitReport(payload){ return this._fetch('/api/reports', { method:'POST', body: JSON.stringify(payload) }); }, |
| deactivateAccount(){ return this._fetch('/api/auth/me/deactivate', { method:'POST' }); }, |
| uploadAvatar(file){ |
| const fd = new FormData(); |
| fd.append('file', file); |
| return this._fetch('/api/auth/me/avatar', { method:'POST', body: fd }); |
| }, |
| uploadIdCard(file){ |
| const fd = new FormData(); |
| fd.append('file', file, 'id-card.jpg'); |
| return this._fetch('/api/auth/me/id-card', { method:'POST', body: fd }); |
| }, |
| }; |
| |
| function formatMoney(n){ return Number(n || 0).toLocaleString('en-US'); } |
| |
| function escapeHtml(str){ |
| const div = document.createElement('div'); |
| div.textContent = str || ''; |
| return div.innerHTML; |
| } |
| |
| function showToast(msg){ |
| let toast = document.getElementById('global-toast'); |
| if(!toast){ |
| toast = document.createElement('div'); |
| toast.id = 'global-toast'; |
| toast.className = 'toast'; |
| document.body.appendChild(toast); |
| } |
| toast.innerHTML = msg; |
| toast.classList.add('show'); |
| clearTimeout(toast._t); |
| toast._t = setTimeout(() => toast.classList.remove('show'), 2800); |
| } |
| |
| |
| |
| |
| function renderBottomNav(active){ |
| if(window.innerWidth > 720) return; |
| if(document.getElementById('bottom-nav')) return; |
| const user = API.user(); |
| const icons = { |
| browse: '<svg viewBox="0 0 24 24"><path d="M12 3l9 8h-2v9h-5v-6H10v6H5v-9H3z"/></svg>', |
| create: '<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>', |
| dashboard: '<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>', |
| messages: '<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H8l-5 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>', |
| profile: '<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>', |
| login: '<svg viewBox="0 0 24 24"><path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg>', |
| about: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>' |
| }; |
| let tabs = [ |
| { key:'browse', href:'index.html', label:'\u0627\u0633\u062a\u0643\u0634\u0641' }, |
| { key:'create', href:'create.html', label:'\u0627\u0639\u0631\u0636' }, |
| ]; |
| if(user){ |
| tabs.push({ key:'dashboard', href:'dashboard.html', label:'\u0644\u0648\u062d\u062a\u064a' }); |
| tabs.push({ key:'messages', href:'messages.html', label:'\u0631\u0633\u0627\u0626\u0644' }); |
| tabs.push({ key:'profile', href:'profile.html?id='+user.id, label:'\u062d\u0633\u0627\u0628\u064a' }); |
| } else { |
| tabs.push({ key:'login', href:'login.html', label:'\u062f\u062e\u0648\u0644' }); |
| } |
| tabs.push({ key:'about', href:'about.html', label:'\u062a\u0648\u0627\u0635\u0644' }); |
| const nav = document.createElement('div'); |
| nav.id = 'bottom-nav'; |
| nav.className = 'bottom-nav'; |
| nav.innerHTML = tabs.map(t => `<a class="bn-item ${t.key===active?'active':''}" href="${t.href}">${icons[t.key]}<span>${t.label}</span></a>`).join(''); |
| document.body.appendChild(nav); |
| initBottomNavAutoHide(nav); |
| } |
| |
| |
| function initBottomNavAutoHide(nav){ |
| let lastY = window.scrollY, ticking = false; |
| window.addEventListener('scroll', () => { |
| if(ticking) return; |
| ticking = true; |
| requestAnimationFrame(() => { |
| const y = window.scrollY; |
| const delta = y - lastY; |
| if(y < 40){ nav.classList.remove('bn-hidden'); } |
| else if(delta > 6){ nav.classList.add('bn-hidden'); } |
| else if(delta < -6){ nav.classList.remove('bn-hidden'); } |
| lastY = y; |
| ticking = false; |
| }); |
| }, { passive:true }); |
| } |
| |
| function renderFooter(){ |
| if(document.getElementById('global-footer-note')) return; |
| const el = document.createElement('p'); |
| el.id = 'global-footer-note'; |
| el.className = 'footer-note'; |
| el.style.cssText = 'margin-top:8px;'; |
| el.innerHTML = '\u0646\u064a\u0651\u0629 \u0648\u0633\u064a\u0637 \u062a\u0648\u0627\u0635\u0644 \u0641\u0642\u0637 \u00b7 <a href="terms.html" style="text-decoration:underline;">\u0627\u0644\u0634\u0631\u0648\u0637 \u0648\u0627\u0644\u0623\u062d\u0643\u0627\u0645</a> \u00b7 <a href="about.html" style="text-decoration:underline;">\u062a\u0648\u0627\u0635\u0644 \u0645\u0639\u0627\u0646\u0627</a>'; |
| document.body.appendChild(el); |
| } |
| |
| function renderTopbar(activePage){ |
| const el = document.getElementById('topbar'); |
| if(!el) return; |
| const user = API.user(); |
| const links = [ |
| { href:'index.html', label:'استكشف المشاريع', key:'browse' }, |
| { href:'create.html', label:'اعرض مشروعك', key:'create' }, |
| ]; |
| if(user){ |
| links.push({ href:'dashboard.html', label:'لوحتي', key:'dashboard' }); |
| links.push({ href:'messages.html', label:'الرسائل', key:'messages' }); |
| } |
| |
| const authArea = user |
| ? `<a class="nav-link" href="profile.html?id=${user.id}">أهلاً، ${escapeHtml(user.name.split(' ')[0])}</a> |
| <button class="btn btn-ghost btn-sm" onclick="API.clearSession(); location.href='index.html';">خروج</button>` |
| : `<a class="btn btn-ghost btn-sm" href="login.html">دخول</a> |
| <a class="btn btn-primary btn-sm" href="signup.html">حساب جديد</a>`; |
| |
| el.innerHTML = ` |
| <a class="brand" href="index.html"> |
| <div class="brand-mark"></div> |
| <div class="brand-name">نيّة</div> |
| </a> |
| <div class="nav-links"> |
| ${links.map(l => `<a class="nav-link ${l.key===activePage?'active':''}" href="${l.href}">${l.label}</a>`).join('')} |
| ${authArea} |
| </div> |
| `; |
| } |
| |
| |
| function requireAuth(){ |
| if(!API.isLoggedIn()){ |
| location.href = 'login.html'; |
| return false; |
| } |
| return true; |
| } |
| |
| </script> |
| <script> |
| const params = new URLSearchParams(location.search); |
| const userId = params.get('id'); |
| const _me = API.user(); |
| renderTopbar(null); renderFooter(); renderBottomNav(_me && String(_me.id)===String(userId) ? 'profile' : null); |
| let profileData = null; |
| |
| async function load(){ |
| if(!userId){ location.href = 'index.html'; return; } |
| try{ |
| const [profile, projects, reviews] = await Promise.all([ |
| API.getUserProfile(userId), API.getUserProjects(userId), API.listReviews(userId) |
| ]); |
| profileData = profile; |
| renderHeader(profile); |
| renderBadges(profile); |
| renderProjects(projects); |
| renderReviews(reviews); |
| |
| const me = API.user(); |
| if(me && String(me.id) === String(userId)){ |
| loadMatches(); |
| } |
| }catch(e){ |
| showToast('البروفايل مش موجود'); |
| setTimeout(()=>location.href='index.html', 1200); |
| return; |
| } |
| document.getElementById('loading').style.display = 'none'; |
| document.getElementById('content').style.display = 'block'; |
| } |
| |
| async function loadMatches(){ |
| try{ |
| const m = await API.myMatches(); |
| const hasAny = (m.projectMatches && m.projectMatches.length) || (m.investorMatches && m.investorMatches.length); |
| if(!hasAny) return; // مفيش داعي نعرض قسم فاضي — لو معملش بياناته يكفي، مش هيلاقي مقترحات أصلاً |
| document.getElementById('matches-section').style.display = 'block'; |
| renderMatches(m); |
| }catch(e){ console.error(e); /* المقترحات ثانوية، مش لازم توقف الصفحة لو فشلت */ } |
| } |
| |
| function matchScoreChip(score){ |
| const pct = Math.round(score); |
| return `<span class="badge badge-ink" style="font-size:10.5px;">تطابق ${pct}%</span>`; |
| } |
| |
| function renderMatches(m){ |
| const parts = []; |
| if(m.projectMatches && m.projectMatches.length){ |
| parts.push(`<div class="muted" style="font-size:12.5px; margin:0 0 10px;">مشاريع تناسب اهتماماتك وميزانيتك:</div>`); |
| parts.push(`<div class="grid" style="margin-bottom:22px;">` + m.projectMatches.map(p => ` |
| <a class="card" href="project.html?id=${p.id}"> |
| <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;"> |
| <div style="font-weight:700;">${escapeHtml(p.name)}</div> |
| ${matchScoreChip(p.matchScore)} |
| </div> |
| <div class="muted" style="font-size:12.5px; margin-top:6px;">${escapeHtml(p.sector)} · ${escapeHtml(p.loc||'')}</div> |
| <div class="muted" style="font-size:12px; margin-top:8px; line-height:1.7;">${p.matchReasons.map(r=>'• '+escapeHtml(r)).join('<br>')}</div> |
| </a> |
| `).join('') + `</div>`); |
| } |
| if(m.investorMatches && m.investorMatches.length){ |
| parts.push(`<div class="muted" style="font-size:12.5px; margin:0 0 10px;">مستثمرين مهتمين بمجال مشاريعك:</div>`); |
| parts.push(`<div class="grid">` + m.investorMatches.map(inv => ` |
| <a class="card" href="profile.html?id=${inv.id}"> |
| <div style="display:flex; align-items:center; gap:10px;"> |
| ${inv.avatar ? `<img src="${API.fileUrl(inv.avatar)}" style="width:38px;height:38px;border-radius:50%;object-fit:cover;">` |
| : `<div style="width:38px;height:38px;border-radius:50%;background:var(--surface);display:flex;align-items:center;justify-content:center;font-weight:800;">${escapeHtml((inv.name||'?')[0])}</div>`} |
| <div style="flex:1;"> |
| <div style="font-weight:700; font-size:13.5px;">${escapeHtml(inv.name)}</div> |
| </div> |
| ${matchScoreChip(inv.matchScore)} |
| </div> |
| <div class="muted" style="font-size:12px; margin-top:8px; line-height:1.7;">${inv.matchReasons.map(r=>'• '+escapeHtml(r)).join('<br>')}</div> |
| </a> |
| `).join('') + `</div>`); |
| } |
| document.getElementById('matches-content').innerHTML = parts.join(''); |
| } |
| |
| function renderHeader(p){ |
| const me = API.user(); |
| const isMe = me && me.id === p.id; |
| const roleLabel = p.role === 'owner' ? 'صاحب مشاريع' : p.role === 'investor' ? 'مستثمر' : p.role === 'broker' ? 'عارض (وسيط)' : 'صاحب مشاريع ومستثمر'; |
| const stars = p.ratingAvg ? `<svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M12 3l2.6 5.7 6.2.6-4.7 4.2 1.4 6.2L12 16.9 6.5 19.7l1.4-6.2L3.2 9.3l6.2-.6z"/></svg> ${p.ratingAvg} (${p.ratingCount} تقييم)` : 'لسه من غير تقييمات'; |
| document.getElementById('profile-header').innerHTML = ` |
| <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;"> |
| ${p.avatar ? `<img src="${API.fileUrl(p.avatar)}" style="width:76px;height:76px;border-radius:50%;object-fit:cover;">` |
| : `<div style="width:76px;height:76px;border-radius:50%;background:var(--surface);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:26px;">${escapeHtml((p.name||'?')[0])}</div>`} |
| <div style="flex:1; min-width:180px;"> |
| <div style="font-weight:800; font-size:20px;">${escapeHtml(p.name)} ${p.idVerified ? '<span class="badge badge-ink"><svg width="12" height="12" viewBox="0 0 20 20" style="display:inline-block;vertical-align:-2px;margin-left:3px;"><circle cx="10" cy="10" r="10" fill="currentColor"/><path d="M6 10.3l2.6 2.6L14.4 7" fill="none" stroke="var(--ink)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>موثّق</span>' : ''}</div> |
| <div class="muted" style="font-size:13px; margin-top:4px;">${roleLabel} · ${stars} · ${p.projectsCount} مشروع</div> |
| ${p.bio ? `<p style="font-size:13.5px; margin-top:10px; line-height:1.8;">${escapeHtml(p.bio)}</p>` : ''} |
| ${isMe ? `<a href="#" onclick="deactivateAccount();return false;" style="font-size:12px; color:var(--muted-soft); text-decoration:underline; display:inline-block; margin-top:10px;">تعطيل الحساب</a>` : ''} |
| </div> |
| ${isMe ? `<button class="btn btn-ghost btn-sm" onclick="openModal('avatar-overlay')">تغيير الصورة</button> |
| ${p.idStatus === 'approved' ? '' : `<button class="btn btn-ghost btn-sm" onclick="openModal('idcard-overlay')">${idBadge(p.idStatus)}</button>`}` |
| : `<button class="btn btn-ghost btn-sm" onclick="reportUser()"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 3v18"/><path d="M5 4h13l-3 4 3 4H5"/></svg> إبلاغ</button>`} |
| </div>`; |
| const reviewBtn = document.getElementById('add-review-btn'); |
| if(reviewBtn) reviewBtn.style.display = isMe ? 'none' : ''; |
| } |
| |
| function renderBadges(p){ |
| const el = document.getElementById('profile-badges'); |
| // أيقونات SVG بسيطة بنفس أسلوب الموقع (outline، بدون أي إيموجي) بدل رموز نصية |
| const ICONS = { |
| check: '<svg viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round"><path d="M5 13l4 4L19 7"/></svg>', |
| star: '<svg viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px;fill:currentColor;stroke:none"><path d="M12 2l2.9 6.3 6.9.6-5.2 4.6 1.6 6.8L12 16.9 5.8 20.3l1.6-6.8L2.2 8.9l6.9-.6z"/></svg>', |
| bolt: '<svg viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px;fill:currentColor;stroke:none"><path d="M13 2L4 14h6l-1 8 9-12h-6z"/></svg>', |
| clock: '<svg viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>', |
| deal: '<svg viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.5 2.5L16 9.5"/></svg>', |
| target: '<svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="0.6" fill="currentColor" stroke="none"/></svg>', |
| bulb: '<svg viewBox="0 0 24 24" style="width:12px;height:12px;vertical-align:-1px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M9 18h6M10 22h4"/><path d="M12 2a6 6 0 00-3 11.2c.5.4.8 1 .8 1.8h4.4c0-.8.3-1.4.8-1.8A6 6 0 0012 2z"/></svg>', |
| }; |
| const badgeChips = (p.trustBadges||[]).map(b => ` |
| <span class="badge" style="display:inline-flex; align-items:center; gap:5px; padding:6px 12px;"> |
| ${ICONS[b.icon] || ''} ${escapeHtml(b.label)} |
| </span> |
| `).join(''); |
| |
| const breakdown = p.ratingBreakdown || {}; |
| const totalRatings = p.ratingCount || 0; |
| const breakdownRows = totalRatings ? [5,4,3,2,1].map(star => { |
| const count = breakdown[String(star)] || 0; |
| const pct = totalRatings ? Math.round(count/totalRatings*100) : 0; |
| return ` |
| <div style="display:flex; align-items:center; gap:8px; font-size:11.5px;"> |
| <span style="width:14px;">${star}</span> |
| <div class="progress-track" style="flex:1;"><div class="progress-fill" style="width:${pct}%; background:var(--ink);"></div></div> |
| <span class="muted" style="width:24px; text-align:left;">${count}</span> |
| </div>`; |
| }).join('') : ''; |
| |
| // شرح كل عنصر في درجة الثقة — مش رقم غامض، كل سطر بيوضح ليه اتحسب كده |
| // ولو ناقص نقاط، بيوريك بالظبط تعمل إيه عشان تزوّدها |
| const trustRows = (p.trustBreakdown || []).map(item => { |
| const pct = item.max ? Math.round(item.earned / item.max * 100) : 0; |
| const full = item.earned >= item.max; |
| return ` |
| <div style="padding:10px 0; border-bottom:1px solid var(--line);"> |
| <div style="display:flex; justify-content:space-between; align-items:center; gap:8px;"> |
| <div style="font-weight:700; font-size:13px; display:flex; align-items:center; gap:6px;"> |
| ${full ? '<span style="color:#1a7d3a;">●</span>' : '<span style="color:var(--muted-soft);">○</span>'} |
| ${escapeHtml(item.label)} |
| </div> |
| <div style="font-size:12px; font-weight:700;">${item.earned}<span class="muted" style="font-weight:500;">/${item.max}</span></div> |
| </div> |
| <div class="progress-track" style="margin-top:6px; height:5px;"><div class="progress-fill" style="width:${pct}%; background:var(--ink);"></div></div> |
| <div class="muted" style="font-size:11.5px; margin-top:5px;">${escapeHtml(item.reason)}</div> |
| ${item.howToImprove ? `<div style="font-size:11.5px; margin-top:3px; color:#a3690a; display:flex; align-items:flex-start; gap:5px;">${ICONS.bulb}<span>${escapeHtml(item.howToImprove)}</span></div>` : ''} |
| </div>`; |
| }).join(''); |
| |
| el.innerHTML = ` |
| <div class="card"> |
| <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;"> |
| <div style="font-weight:700; font-size:14px;">درجة الثقة</div> |
| <div style="font-weight:800; font-size:16px;">${p.trustScore}<span class="muted" style="font-size:12px; font-weight:600;">/100</span></div> |
| </div> |
| <div class="progress-track" style="margin-top:10px;"><div class="progress-fill" style="width:${p.trustScore}%; background:var(--ink);"></div></div> |
| |
| ${badgeChips ? `<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:16px;">${badgeChips}</div>` : ''} |
| |
| ${trustRows ? ` |
| <div style="margin-top:16px;"> |
| <button class="btn btn-ghost btn-sm" style="width:100%;" onclick="const b=document.getElementById('trust-detail'); const open=b.style.display!=='none'; b.style.display=open?'none':'block'; this.textContent = open ? 'تفاصيل درجة الثقة ▾' : 'اخفاء التفاصيل ▴';">تفاصيل درجة الثقة ▾</button> |
| <div id="trust-detail" style="display:none; margin-top:6px;">${trustRows}</div> |
| </div> |
| ` : ''} |
| |
| ${p.responseRate !== null && p.responseRate !== undefined ? ` |
| <div class="muted" style="font-size:12.5px; margin-top:14px; display:flex; align-items:center; gap:5px;">${ICONS.bolt}<span>بيرد على ${p.responseRate}% من الرسائل اللي توصله</span></div> |
| ` : ''} |
| |
| ${(p.projectsCount || p.confirmedDealsAsOwner || p.confirmedDealsAsInvestor) ? ` |
| <div style="display:flex; gap:20px; flex-wrap:wrap; margin-top:14px; padding-top:14px; border-top:1px solid var(--line);"> |
| <div><div style="font-weight:800; font-size:16px;">${p.projectsCount}</div><div class="muted" style="font-size:11px;">مشروع</div></div> |
| <div><div style="font-weight:800; font-size:16px;">${p.closedProjectsCount||0}</div><div class="muted" style="font-size:11px;">مكتمل</div></div> |
| ${p.confirmedDealsAsOwner ? `<div><div style="font-weight:800; font-size:16px;">${p.confirmedDealsAsOwner}</div><div class="muted" style="font-size:11px;">صفقة مؤكدة على مشاريعه</div></div>` : ''} |
| ${p.confirmedDealsAsInvestor ? `<div><div style="font-weight:800; font-size:16px;">${p.confirmedDealsAsInvestor}</div><div class="muted" style="font-size:11px;">صفقة مؤكدة كمستثمر</div></div>` : ''} |
| </div> |
| ` : ''} |
| |
| ${breakdownRows ? `<div style="margin-top:14px; padding-top:14px; border-top:1px solid var(--line); display:flex; flex-direction:column; gap:5px;">${breakdownRows}</div>` : ''} |
| |
| ${p.investorSectorsList && p.investorSectorsList.length ? ` |
| <div style="margin-top:14px; padding-top:14px; border-top:1px solid var(--line);"> |
| <div class="muted" style="font-size:11.5px; margin-bottom:8px;">مهتم بمجالات:</div> |
| <div style="display:flex; flex-wrap:wrap; gap:6px;">${p.investorSectorsList.map(s=>`<span class="badge">${escapeHtml(s)}</span>`).join('')}</div> |
| </div> |
| ` : ''} |
| </div> |
| `; |
| } |
| |
| async function deactivateAccount(){ |
| if(!confirm('متأكد إنك عايز تعطّل حسابك؟ هيتخفي من المنصة ومش هتقدر تدخل تاني إلا لو تواصلت معانا.')) return; |
| try{ |
| await API.deactivateAccount(); |
| API.clearSession(); |
| location.href = 'index.html'; |
| }catch(e){ showToast(e.message); } |
| } |
| |
| function idBadge(status){ |
| if(status === 'approved') return '<svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg> موثّق'; |
| if(status === 'pending') return '⏳ طلبك قيد المراجعة'; |
| if(status === 'rejected') return 'وثّق حسابك (اترفض قبل كده)'; |
| return 'وثّق حسابك'; |
| } |
| |
| function renderProjects(projects){ |
| const el = document.getElementById('profile-projects'); |
| el.innerHTML = projects.length ? projects.map(p => ` |
| <a class="card" href="project.html?id=${p.id}"> |
| <div style="font-weight:700; margin-bottom:6px;">${escapeHtml(p.name)}</div> |
| <div class="muted" style="font-size:13px;">${escapeHtml(p.sector)} · ${escapeHtml(p.loc || '')}</div> |
| <div class="muted" style="font-size:13px; margin-top:6px;">الهدف: ${formatMoney(p.target)} جنيه</div> |
| </a> |
| `).join('') : '<div class="empty">لسه مفيش مشاريع معروضة</div>'; |
| } |
| |
| function renderReviews(reviews){ |
| const el = document.getElementById('profile-reviews'); |
| el.innerHTML = reviews.length ? reviews.map(r => ` |
| <div class="card" style="margin-bottom:12px;"> |
| <div style="display:flex; justify-content:space-between; align-items:center;"> |
| <a href="profile.html?id=${r.authorId}" style="font-weight:700;">${escapeHtml(r.authorName)}</a> |
| <span>${'<svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M12 3l2.6 5.7 6.2.6-4.7 4.2 1.4 6.2L12 16.9 6.5 19.7l1.4-6.2L3.2 9.3l6.2-.6z"/></svg>'.repeat(r.rating)}</span> |
| </div> |
| ${r.body ? `<p style="font-size:13.5px; margin-top:8px; color:var(--muted);">${escapeHtml(r.body)}</p>` : ''} |
| </div> |
| `).join('') : '<div class="empty">لسه مفيش تقييمات</div>'; |
| } |
| |
| async function submitAvatar(){ |
| const file = document.getElementById('avatar-file').files[0]; |
| if(!file){ showToast('اختار صورة'); return; } |
| try{ |
| const res = await API.uploadAvatar(file); |
| const user = API.user(); |
| user.avatar = res.avatar; |
| localStorage.setItem('neya_user', JSON.stringify(user)); |
| closeModal('avatar-overlay'); |
| showToast('اتغيرت الصورة <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg>'); |
| load(); |
| }catch(e){ showToast(e.message); } |
| } |
| |
| async function submitIdCard(){ |
| if(!capturedBlob){ showToast('صوّر البطاقة الأول'); return; } |
| try{ |
| await API.uploadIdCard(capturedBlob); |
| stopCamera(); |
| closeModal('idcard-overlay'); |
| showToast('اتبعت بطاقتك، هنراجعها ونرد عليك <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg>'); |
| load(); |
| }catch(e){ showToast(e.message); } |
| } |
| |
| let camStream = null, capturedBlob = null; |
| |
| async function startCamera(){ |
| const err = document.getElementById('cam-error'); |
| err.style.display = 'none'; |
| document.getElementById('cam-video').style.display = 'block'; |
| document.getElementById('cam-shot').style.display = 'none'; |
| document.getElementById('cam-capture').style.display = 'block'; |
| document.getElementById('cam-retake').style.display = 'none'; |
| document.getElementById('cam-send').style.display = 'none'; |
| capturedBlob = null; |
| try{ |
| camStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); |
| document.getElementById('cam-video').srcObject = camStream; |
| }catch(e){ |
| err.textContent = 'مش قادرين نوصل للكاميرا. لازم تسمح بالإذن من المتصفح.'; |
| err.style.display = 'block'; |
| } |
| } |
| |
| function stopCamera(){ |
| if(camStream){ camStream.getTracks().forEach(t => t.stop()); camStream = null; } |
| } |
| |
| function capturePhoto(){ |
| const video = document.getElementById('cam-video'); |
| if(!video.videoWidth){ showToast('استنى الكاميرا لسه بتفتح'); return; } |
| const canvas = document.getElementById('cam-canvas'); |
| canvas.width = video.videoWidth; canvas.height = video.videoHeight; |
| canvas.getContext('2d').drawImage(video, 0, 0); |
| canvas.toBlob(blob => { |
| if(!blob){ showToast('حصل مشكلة في التصوير، جرب تاني'); return; } |
| capturedBlob = blob; |
| document.getElementById('cam-shot').src = URL.createObjectURL(blob); |
| document.getElementById('cam-shot').style.display = 'block'; |
| document.getElementById('cam-video').style.display = 'none'; |
| document.getElementById('cam-capture').style.display = 'none'; |
| document.getElementById('cam-retake').style.display = 'block'; |
| document.getElementById('cam-send').style.display = 'block'; |
| }, 'image/jpeg', 0.92); |
| } |
| |
| function retakePhoto(){ |
| document.getElementById('cam-shot').style.display = 'none'; |
| document.getElementById('cam-video').style.display = 'block'; |
| document.getElementById('cam-capture').style.display = 'block'; |
| document.getElementById('cam-retake').style.display = 'none'; |
| document.getElementById('cam-send').style.display = 'none'; |
| capturedBlob = null; |
| } |
| |
| async function submitReview(){ |
| if(!requireAuth()) return; |
| const rating = Number(document.getElementById('rv-rating').value); |
| const body = document.getElementById('rv-body').value.trim(); |
| try{ |
| await API.addReview(userId, { rating, body: body || null }); |
| closeModal('review-overlay'); |
| showToast('اتسجل تقييمك <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg>'); |
| load(); |
| }catch(e){ showToast(e.message); } |
| } |
| |
| async function reportUser(){ |
| if(!requireAuth()) return; |
| const reason = prompt('سبب البلاغ:'); |
| if(!reason) return; |
| try{ |
| await API.submitReport({ target_user_id: userId, reason }); |
| showToast('اتسجل بلاغك، شكراً لتنبيهنا <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg>'); |
| }catch(e){ showToast(e.message); } |
| } |
| |
| function openModal(id){ |
| document.getElementById(id).classList.add('show'); |
| if(id === 'idcard-overlay') startCamera(); |
| } |
| function closeModal(id){ |
| document.getElementById(id).classList.remove('show'); |
| if(id === 'idcard-overlay') stopCamera(); |
| } |
| |
| load(); |
| </script> |
| </body> |
| </html> |
|
|