my0919175 commited on
Commit
11610b5
·
verified ·
1 Parent(s): d36aa0c

Upload 15 files

Browse files
static/favicon.svg ADDED
static/index.html CHANGED
@@ -3,6 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
6
  <title>نيّة — سوق نوايا الاستثمار المحلي</title>
7
  <style>
8
  @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');
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
7
  <title>نيّة — سوق نوايا الاستثمار المحلي</title>
8
  <style>
9
  @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');
static/login.html CHANGED
@@ -3,6 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
6
  <title>تسجيل الدخول — نيّة</title>
7
  <style>
8
  @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');
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
7
  <title>تسجيل الدخول — نيّة</title>
8
  <style>
9
  @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');
static/signup.html CHANGED
@@ -3,6 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
6
  <title>حساب جديد — نيّة</title>
7
  <style>
8
  @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');
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
7
  <title>حساب جديد — نيّة</title>
8
  <style>
9
  @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');
static/verify-email.html ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
7
+ <title>تأكيد الإيميل — نيّة</title>
8
+ <style>
9
+ @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');
10
+
11
+ :root{
12
+ --bg:#FFFFFF;
13
+ --ink:#0A0A0A;
14
+ --line: rgba(10,10,10,0.12);
15
+ --line-strong: rgba(10,10,10,0.22);
16
+ --muted: rgba(10,10,10,0.55);
17
+ --muted-soft: rgba(10,10,10,0.4);
18
+ --surface: rgba(10,10,10,0.03);
19
+ --surface-hover: rgba(10,10,10,0.05);
20
+ --radius: 14px;
21
+ --max-w: 1080px;
22
+ }
23
+ *{ box-sizing:border-box; }
24
+ html,body{ margin:0; padding:0; }
25
+
26
+ /* ---------- app polish: smooth motion + native app feel ---------- */
27
+ @media (prefers-reduced-motion: no-preference){
28
+ body{ animation: pageFadeIn .28s ease both; }
29
+ .card, .interest-item, .grid > *{ animation: cardRise .32s ease both; }
30
+ .overlay.show .modal{ animation: modalPop .22s cubic-bezier(.2,.8,.3,1.2) both; }
31
+ .toast.show{ animation: toastRise .3s ease both; }
32
+ }
33
+ @keyframes pageFadeIn{ from{ opacity:0; transform:translateY(6px);} to{ opacity:1; transform:none; } }
34
+ @keyframes cardRise{ from{ opacity:0; transform:translateY(8px);} to{ opacity:1; transform:none; } }
35
+ @keyframes modalPop{ from{ opacity:0; transform:scale(.96) translateY(8px);} to{ opacity:1; transform:none; } }
36
+ @keyframes toastRise{ from{ opacity:0; transform:translateX(-50%) translateY(14px);} to{ opacity:1; transform:translateX(-50%) translateY(0);} }
37
+ .btn, .nav-link, .file-drop, .interest-item, .card{ transition: all .18s ease; }
38
+ .btn:active{ transform: scale(.96); }
39
+ .btn-primary:active{ opacity:.85; }
40
+ a.card:hover, .card:hover{ box-shadow: 0 4px 18px rgba(10,10,10,0.06); }
41
+ input, textarea, select{ transition: border-color .15s ease, box-shadow .15s ease; }
42
+ input:focus, textarea:focus, select:focus{ box-shadow: 0 0 0 3px rgba(10,10,10,0.06); }
43
+ ::-webkit-scrollbar{ width:8px; height:8px; }
44
+ ::-webkit-scrollbar-thumb{ background: rgba(10,10,10,0.15); border-radius:8px; }
45
+ @supports (padding: max(0px)){
46
+ body{ padding-bottom: env(safe-area-inset-bottom); }
47
+ .topbar{ padding-top: max(18px, env(safe-area-inset-top)); }
48
+ }
49
+ @media (max-width:720px){
50
+ .btn{ font-size:14px; }
51
+ .card{ border-radius:16px; }
52
+ .modal{ border-radius:20px 20px 0 0; position:fixed; bottom:0; left:0; right:0; max-width:100%; max-height:85vh; margin:0; }
53
+ .overlay{ align-items:flex-end; padding:0; }
54
+ .overlay.show .modal{ animation: sheetUp .25s cubic-bezier(.2,.8,.3,1) both; }
55
+ }
56
+ @keyframes sheetUp{ from{ transform:translateY(100%);} to{ transform:translateY(0);} }
57
+ body{
58
+ background: var(--bg);
59
+ color: var(--ink);
60
+ font-family:'IBM Plex Sans Arabic', sans-serif;
61
+ min-height:100vh;
62
+ -webkit-font-smoothing: antialiased;
63
+ line-height:1.6;
64
+ }
65
+ h1,h2,h3,.display,.btn,.brand-name{ font-family:'Cairo', sans-serif; }
66
+ a{ color: inherit; text-decoration:none; }
67
+ img{ max-width:100%; display:block; }
68
+
69
+ /* ---------- layout ---------- */
70
+ .page{ max-width: var(--max-w); margin: 0 auto; padding: 0 20px 60px; }
71
+ .center-page{ max-width: 440px; margin: 0 auto; padding: 20px 20px 60px; }
72
+
73
+ /* ---------- topbar ---------- */
74
+ .topbar{
75
+ display:flex; justify-content:space-between; align-items:center;
76
+ padding: 18px 24px; border-bottom: 1px solid var(--line);
77
+ position: sticky; top:0; background: var(--bg); z-index: 40;
78
+ }
79
+ .brand{ display:flex; align-items:center; gap:9px; }
80
+ .brand-mark{
81
+ width:30px; height:30px; border-radius:50%;
82
+ border: 2px solid var(--ink); position:relative; flex-shrink:0;
83
+ }
84
+ .brand-mark::after{
85
+ content:''; position:absolute; inset:7px; border-radius:50%; background: var(--ink);
86
+ }
87
+ .brand-name{ font-weight:800; font-size:20px; letter-spacing:0.3px; }
88
+ .nav-links{ display:flex; align-items:center; gap:6px; }
89
+ .nav-link{
90
+ padding:9px 14px; border-radius:999px; font-size:14px; font-weight:600; color: var(--muted);
91
+ transition: all .15s ease;
92
+ }
93
+ .nav-link:hover{ background: var(--surface); color: var(--ink); }
94
+ .nav-link.active{ background: var(--ink); color: var(--bg); }
95
+ .hamburger{ display:none; }
96
+
97
+ @media (max-width:720px){
98
+ .nav-links{ display:none; }
99
+ .hamburger{ display:block; }
100
+ }
101
+
102
+ /* ---------- buttons ---------- */
103
+ .btn{
104
+ display:inline-flex; align-items:center; justify-content:center; gap:8px;
105
+ padding: 13px 26px; border-radius: 11px; border: 1.5px solid var(--ink); cursor:pointer;
106
+ font-weight:700; font-size:14.5px; transition: all .15s ease; background: transparent; color: var(--ink);
107
+ }
108
+ .btn-primary{ background: var(--ink); color: var(--bg); }
109
+ .btn-primary:hover{ opacity:0.82; transform: translateY(-1px); }
110
+ .btn-ghost{ background: transparent; color: var(--ink); }
111
+ .btn-ghost:hover{ background: var(--surface); }
112
+ .btn-block{ width:100%; }
113
+ .btn:disabled{ opacity:0.4; cursor:not-allowed; }
114
+ .btn-sm{ padding: 9px 16px; font-size:13px; border-radius:9px; }
115
+
116
+ /* ---------- cards ---------- */
117
+ .card{
118
+ background: var(--bg); border: 1.5px solid var(--line); border-radius: var(--radius);
119
+ padding: 22px; transition: border-color .15s ease, transform .15s ease;
120
+ }
121
+ .card:hover{ border-color: var(--line-strong); }
122
+
123
+ /* ---------- forms ---------- */
124
+ .field{ margin-bottom:18px; }
125
+ .field label{ display:block; font-size:13.5px; font-weight:700; margin-bottom:8px; }
126
+ .field .hint{ font-size:12px; color: var(--muted-soft); margin-top:6px; line-height:1.6; }
127
+ input, textarea, select{
128
+ width:100%; padding: 13px 15px; border-radius:10px; border: 1.5px solid var(--line);
129
+ background: var(--bg); color: var(--ink); font-family:inherit; font-size:14.5px;
130
+ }
131
+ input:focus, textarea:focus, select:focus{ outline:none; border-color: var(--ink); }
132
+ textarea{ resize:vertical; min-height:100px; }
133
+ .field-row{ display:flex; gap:14px; }
134
+ .field-row .field{ flex:1; }
135
+ @media (max-width:520px){ .field-row{ flex-direction:column; gap:0; } }
136
+
137
+ .file-drop{
138
+ border: 1.5px dashed var(--line-strong); border-radius:12px; padding: 26px 16px;
139
+ text-align:center; cursor:pointer; transition: all .15s ease; font-size:13.5px; color: var(--muted);
140
+ }
141
+ .file-drop:hover{ border-color: var(--ink); background: var(--surface); }
142
+ .file-list{ display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; }
143
+ .file-thumb{
144
+ width:64px; height:64px; border-radius:8px; overflow:hidden; border:1px solid var(--line); position:relative;
145
+ }
146
+ .file-thumb img{ width:100%; height:100%; object-fit:cover; }
147
+ .file-chip{
148
+ display:flex; align-items:center; gap:6px; padding:6px 12px; border:1px solid var(--line);
149
+ border-radius:999px; font-size:12px;
150
+ }
151
+
152
+ /* ---------- grid / misc ---------- */
153
+ .grid{ display:grid; grid-template-columns: repeat(auto-fill, minmax(290px,1fr)); gap:18px; }
154
+ .section-head{ display:flex; justify-content:space-between; align-items:flex-end; margin: 30px 0 20px; flex-wrap:wrap; gap:10px; }
155
+ .section-head h2{ font-size:22px; font-weight:800; margin:0; }
156
+ .muted{ color: var(--muted); }
157
+ .badge{
158
+ display:inline-block; font-size:11px; font-weight:700; padding:4px 11px; border-radius:999px;
159
+ border: 1px solid var(--line-strong); color: var(--muted);
160
+ }
161
+ .badge-ink{ background: var(--ink); color: var(--bg); border-color: var(--ink); }
162
+ .empty{ text-align:center; padding: 60px 20px; color: var(--muted-soft); border: 1.5px dashed var(--line); border-radius: var(--radius); }
163
+ .divider{ height:1px; background: var(--line); margin: 24px 0; border:none; }
164
+
165
+ .progress-track{ height:8px; background: var(--surface); border-radius:999px; overflow:hidden; }
166
+ .progress-fill{ height:100%; background: var(--ink); border-radius:999px; transition: width .4s ease; }
167
+
168
+ .toast{
169
+ position:fixed; bottom:26px; left:50%; transform:translateX(-50%) translateY(20px);
170
+ background: var(--ink); color: var(--bg); padding:14px 24px; border-radius:12px;
171
+ font-family:'Cairo',sans-serif; font-weight:700; font-size:14px; opacity:0; transition: all .3s ease;
172
+ pointer-events:none; z-index:100; white-space:nowrap; max-width: 90vw; text-align:center;
173
+ }
174
+ .toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
175
+
176
+ .spinner{
177
+ width:20px; height:20px; border:2.5px solid var(--line); border-top-color: var(--ink);
178
+ border-radius:50%; animation: spin .7s linear infinite; margin: 0 auto;
179
+ }
180
+ @keyframes spin{ to{ transform: rotate(360deg); } }
181
+
182
+ .loading-row{ display:flex; justify-content:center; padding: 40px 0; }
183
+
184
+ /* ---------- modal ---------- */
185
+ .overlay{
186
+ position:fixed; inset:0; background: rgba(10,10,10,0.5);
187
+ display:none; align-items:center; justify-content:center; z-index:60; padding:20px;
188
+ }
189
+ .overlay.show{ display:flex; }
190
+ .modal{
191
+ background: var(--bg); border: 1.5px solid var(--ink); border-radius: 18px;
192
+ width:100%; max-width:460px; padding:26px; max-height:88vh; overflow-y:auto; position:relative;
193
+ }
194
+ .modal h3{ font-size:19px; margin:0 0 16px; }
195
+ .close-x{
196
+ position:absolute; top:18px; left:18px; background:none; border:none; font-size:19px; cursor:pointer; opacity:0.5;
197
+ }
198
+ .close-x:hover{ opacity:1; }
199
+
200
+ /* footer */
201
+ .footer-note{ text-align:center; padding: 40px 20px 30px; color: var(--muted-soft); font-size:12.5px; }
202
+
203
+ @media (max-width: 640px){
204
+ .topbar{ padding:14px 16px; }
205
+ .page{ padding: 0 14px 50px; }
206
+ }
207
+
208
+
209
+ /* ---------- mobile app shell (bottom tab bar, thndr-style) ---------- */
210
+ .bottom-nav{ display:none; }
211
+ @media (max-width:720px){
212
+ body{ padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
213
+ .bottom-nav{
214
+ display:flex; position:fixed; bottom:0; left:0; right:0; z-index:50;
215
+ background: var(--bg); border-top:1px solid var(--line);
216
+ padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
217
+ box-shadow: 0 -2px 14px rgba(10,10,10,0.05);
218
+ transform: translateY(0); transition: transform .28s cubic-bezier(.4,0,.2,1);
219
+ will-change: transform;
220
+ }
221
+ .bottom-nav.bn-hidden{ transform: translateY(100%); }
222
+ .bn-item{
223
+ flex:1; display:flex; flex-direction:column; align-items:center; gap:3px;
224
+ padding:6px 2px; border-radius:12px; color: var(--muted-soft);
225
+ font-size:10.5px; font-weight:600; transition: all .15s ease; -webkit-tap-highlight-color:transparent;
226
+ }
227
+ .bn-item svg{ width:22px; height:22px; stroke: currentColor; fill:none; stroke-width:1.8; transition: transform .15s ease; }
228
+ .bn-item.active{ color: var(--ink); }
229
+ .bn-item.active svg{ transform: translateY(-1px) scale(1.05); }
230
+ .bn-item:active{ transform: scale(.92); }
231
+ .topbar .nav-links{ display:none !important; }
232
+ .hamburger{ display:none !important; }
233
+ .topbar{ padding:14px 18px; }
234
+ .brand-name{ font-size:18px; }
235
+ }
236
+
237
+ /* ---------- final QA polish: no overflow, no overlap, no glitches ---------- */
238
+ html{ overflow-x:hidden; }
239
+ body{ overflow-x:hidden; }
240
+ img,video{ max-width:100%; height:auto; }
241
+ *{ word-wrap:break-word; overflow-wrap:break-word; }
242
+ .page, .center-page{ overflow-x:hidden; }
243
+ @media (max-width:720px){
244
+ .toast{ bottom: calc(78px + env(safe-area-inset-bottom)) !important; max-width: calc(100vw - 32px); }
245
+ .modal{ max-height: calc(85vh - env(safe-area-inset-bottom)); padding-bottom: calc(26px + env(safe-area-inset-bottom)); }
246
+ .overlay.show{ z-index:70; }
247
+ table{ display:block; overflow-x:auto; -webkit-overflow-scrolling:touch; }
248
+ }
249
+ </style>
250
+ </head>
251
+ <body>
252
+
253
+ <div class="topbar" id="topbar"></div>
254
+
255
+ <div class="center-page">
256
+ <div style="text-align:center; margin: 40px 0 30px;">
257
+ <div class="brand-mark" style="margin:0 auto 14px;"></div>
258
+ <h1 id="ve-title" style="font-size:26px; font-weight:800; margin:0 0 8px;">جاري تأكيد إيميلك...</h1>
259
+ <p class="muted" id="ve-sub" style="font-size:14px;">لحظة وهندخلك المنصة على طول</p>
260
+ </div>
261
+
262
+ <div class="card" style="text-align:center;">
263
+ <div class="spinner" id="ve-spinner"></div>
264
+ <p id="ve-msg" style="display:none; font-size:14.5px; margin:0;"></p>
265
+ </div>
266
+
267
+ <p style="text-align:center; margin-top:20px; font-size:14px;" class="muted">
268
+ <a href="login.html" style="font-weight:700; color:var(--ink); text-decoration:underline;">رجوع لتسجيل الدخول</a>
269
+ </p>
270
+ </div>
271
+
272
+ <script>
273
+ // ---------- نيّة: طبقة الاتصال بالـ API + إدارة الجلسة ----------
274
+ const API = {
275
+ base: '',
276
+
277
+ token(){ return localStorage.getItem('neya_token'); },
278
+ user(){ try{ return JSON.parse(localStorage.getItem('neya_user') || 'null'); }catch(e){ return null; } },
279
+ isLoggedIn(){ return !!this.token(); },
280
+
281
+ setSession(token, user){
282
+ localStorage.setItem('neya_token', token);
283
+ localStorage.setItem('neya_user', JSON.stringify(user));
284
+ },
285
+ clearSession(){
286
+ localStorage.removeItem('neya_token');
287
+ localStorage.removeItem('neya_user');
288
+ },
289
+
290
+ async _fetch(path, opts = {}){
291
+ const headers = opts.headers || {};
292
+ if(this.token()) headers['Authorization'] = 'Bearer ' + this.token();
293
+ if(opts.body && !(opts.body instanceof FormData)){
294
+ headers['Content-Type'] = 'application/json';
295
+ }
296
+ const res = await fetch(this.base + path, { ...opts, headers });
297
+ let data = null;
298
+ try{ data = await res.json(); }catch(e){ /* no body */ }
299
+ if(!res.ok){
300
+ const msg = (data && data.detail) ? data.detail : 'حصل خطأ، حاول تاني';
301
+ throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
302
+ }
303
+ return data;
304
+ },
305
+
306
+ signup(payload){ return this._fetch('/api/auth/signup', { method:'POST', body: JSON.stringify(payload) }); },
307
+ login(payload){ return this._fetch('/api/auth/login', { method:'POST', body: JSON.stringify(payload) }); },
308
+ me(){ return this._fetch('/api/auth/me'); },
309
+ forgotPassword(email){ return this._fetch('/api/auth/forgot-password', { method:'POST', body: JSON.stringify({email}) }); },
310
+ resetPassword(token, new_password){ return this._fetch('/api/auth/reset-password', { method:'POST', body: JSON.stringify({token, new_password}) }); },
311
+ verifyEmail(token){ return this._fetch('/api/auth/verify-email', { method:'POST', body: JSON.stringify({token}) }); },
312
+
313
+ listProjects(){ return this._fetch('/api/projects'); },
314
+ createProject(payload){ return this._fetch('/api/projects', { method:'POST', body: JSON.stringify(payload) }); },
315
+ uploadFile(projectId, kind, file){
316
+ const fd = new FormData();
317
+ fd.append('kind', kind);
318
+ fd.append('file', file);
319
+ return this._fetch(`/api/projects/${projectId}/files`, { method:'POST', body: fd });
320
+ },
321
+
322
+ listInterests(projectId){ return this._fetch(`/api/projects/${projectId}/interests`); },
323
+ addInterest(projectId, payload){ return this._fetch(`/api/projects/${projectId}/interests`, { method:'POST', body: JSON.stringify(payload) }); },
324
+
325
+ bookAppointment(projectId, payload){ return this._fetch(`/api/projects/${projectId}/appointments`, { method:'POST', body: JSON.stringify(payload) }); },
326
+ listAppointments(projectId){ return this._fetch(`/api/projects/${projectId}/appointments`); },
327
+
328
+ fileUrl(filename){ return `/api/uploads/${filename}`; },
329
+
330
+ listConversations(){ return this._fetch('/api/conversations'); },
331
+ getThread(projectId, withUser){ return this._fetch(`/api/projects/${projectId}/messages?with_user=${encodeURIComponent(withUser)}`); },
332
+ sendMessage(projectId, payload){ return this._fetch(`/api/projects/${projectId}/messages`, { method:'POST', body: JSON.stringify(payload) }); },
333
+
334
+ getUserProfile(userId){ return this._fetch(`/api/users/${userId}/profile`); },
335
+ getUserProjects(userId){ return this._fetch(`/api/users/${userId}/projects`); },
336
+ listReviews(userId){ return this._fetch(`/api/users/${userId}/reviews`); },
337
+ addReview(userId, payload){ return this._fetch(`/api/users/${userId}/reviews`, { method:'POST', body: JSON.stringify(payload) }); },
338
+ submitReport(payload){ return this._fetch('/api/reports', { method:'POST', body: JSON.stringify(payload) }); },
339
+ uploadAvatar(file){
340
+ const fd = new FormData();
341
+ fd.append('file', file);
342
+ return this._fetch('/api/auth/me/avatar', { method:'POST', body: fd });
343
+ },
344
+ };
345
+
346
+ function formatMoney(n){ return Number(n || 0).toLocaleString('en-US'); }
347
+
348
+ function escapeHtml(str){
349
+ const div = document.createElement('div');
350
+ div.textContent = str || '';
351
+ return div.innerHTML;
352
+ }
353
+
354
+ function showToast(msg){
355
+ let toast = document.getElementById('global-toast');
356
+ if(!toast){
357
+ toast = document.createElement('div');
358
+ toast.id = 'global-toast';
359
+ toast.className = 'toast';
360
+ document.body.appendChild(toast);
361
+ }
362
+ toast.textContent = msg;
363
+ toast.classList.add('show');
364
+ clearTimeout(toast._t);
365
+ toast._t = setTimeout(() => toast.classList.remove('show'), 2800);
366
+ }
367
+
368
+ // يبني التوب بار الموحّد في كل الصفحات ويعكس حالة تسجيل الدخول
369
+
370
+
371
+ function renderBottomNav(active){
372
+ if(window.innerWidth > 720) return;
373
+ if(document.getElementById('bottom-nav')) return;
374
+ const user = API.user();
375
+ const icons = {
376
+ browse: '<svg viewBox="0 0 24 24"><path d="M12 3l9 8h-2v9h-5v-6H10v6H5v-9H3z"/></svg>',
377
+ create: '<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
378
+ 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>',
379
+ 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>',
380
+ 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>',
381
+ 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>',
382
+ about: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>'
383
+ };
384
+ let tabs = [
385
+ { key:'browse', href:'index.html', label:'\u0627\u0633\u062a\u0643\u0634\u0641' },
386
+ { key:'create', href:'create.html', label:'\u0627\u0639\u0631\u0636' },
387
+ ];
388
+ if(user){
389
+ tabs.push({ key:'dashboard', href:'dashboard.html', label:'\u0644\u0648\u062d\u062a\u064a' });
390
+ tabs.push({ key:'messages', href:'messages.html', label:'\u0631\u0633\u0627\u0626\u0644' });
391
+ tabs.push({ key:'profile', href:'profile.html?id='+user.id, label:'\u062d\u0633\u0627\u0628\u064a' });
392
+ } else {
393
+ tabs.push({ key:'login', href:'login.html', label:'\u062f\u062e\u0648\u0644' });
394
+ }
395
+ tabs.push({ key:'about', href:'about.html', label:'\u062a\u0648\u0627\u0635\u0644' });
396
+ const nav = document.createElement('div');
397
+ nav.id = 'bottom-nav';
398
+ nav.className = 'bottom-nav';
399
+ 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('');
400
+ document.body.appendChild(nav);
401
+ initBottomNavAutoHide(nav);
402
+ }
403
+
404
+ // اخفاء شريط التنقل السفلي عند التمرير لتحت، وإظهاره عند التمرير لفوق (زي التطبيقات الأصلية)
405
+ function initBottomNavAutoHide(nav){
406
+ let lastY = window.scrollY, ticking = false;
407
+ window.addEventListener('scroll', () => {
408
+ if(ticking) return;
409
+ ticking = true;
410
+ requestAnimationFrame(() => {
411
+ const y = window.scrollY;
412
+ const delta = y - lastY;
413
+ if(y < 40){ nav.classList.remove('bn-hidden'); }
414
+ else if(delta > 6){ nav.classList.add('bn-hidden'); }
415
+ else if(delta < -6){ nav.classList.remove('bn-hidden'); }
416
+ lastY = y;
417
+ ticking = false;
418
+ });
419
+ }, { passive:true });
420
+ }
421
+
422
+ function renderFooter(){
423
+ if(document.getElementById('global-footer-note')) return;
424
+ const el = document.createElement('p');
425
+ el.id = 'global-footer-note';
426
+ el.className = 'footer-note';
427
+ el.style.cssText = 'margin-top:8px;';
428
+ 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>';
429
+ document.body.appendChild(el);
430
+ }
431
+
432
+ function renderTopbar(activePage){
433
+ const el = document.getElementById('topbar');
434
+ if(!el) return;
435
+ const user = API.user();
436
+ const links = [
437
+ { href:'index.html', label:'استكشف المشاريع', key:'browse' },
438
+ { href:'create.html', label:'اعرض مشروعك', key:'create' },
439
+ ];
440
+ if(user){
441
+ links.push({ href:'dashboard.html', label:'لوحتي', key:'dashboard' });
442
+ links.push({ href:'messages.html', label:'الرسائل', key:'messages' });
443
+ }
444
+
445
+ const authArea = user
446
+ ? `<a class="nav-link" href="profile.html?id=${user.id}">أهلاً، ${escapeHtml(user.name.split(' ')[0])}</a>
447
+ <button class="btn btn-ghost btn-sm" onclick="API.clearSession(); location.href='index.html';">خروج</button>`
448
+ : `<a class="btn btn-ghost btn-sm" href="login.html">دخول</a>
449
+ <a class="btn btn-primary btn-sm" href="signup.html">حساب جديد</a>`;
450
+
451
+ el.innerHTML = `
452
+ <a class="brand" href="index.html">
453
+ <div class="brand-mark"></div>
454
+ <div class="brand-name">نيّة</div>
455
+ </a>
456
+ <div class="nav-links">
457
+ ${links.map(l => `<a class="nav-link ${l.key===activePage?'active':''}" href="${l.href}">${l.label}</a>`).join('')}
458
+ ${authArea}
459
+ </div>
460
+ `;
461
+ }
462
+
463
+ // حماية صفحات لازم تسجيل دخول لها
464
+ function requireAuth(){
465
+ if(!API.isLoggedIn()){
466
+ location.href = 'login.html';
467
+ return false;
468
+ }
469
+ return true;
470
+ }
471
+
472
+ </script>
473
+ <script>
474
+ renderTopbar(null); renderFooter(); renderBottomNav('login');
475
+
476
+ function veDone(ok, title, sub){
477
+ document.getElementById('ve-spinner').style.display = 'none';
478
+ document.getElementById('ve-title').textContent = title;
479
+ document.getElementById('ve-sub').textContent = sub;
480
+ const msg = document.getElementById('ve-msg');
481
+ msg.style.display = 'block';
482
+ msg.textContent = ok ? 'حسابك اتفعّل بنجاح ✅' : '';
483
+ msg.style.color = ok ? '#0a0' : '#c00';
484
+ }
485
+
486
+ (async function verify(){
487
+ const token = new URLSearchParams(location.search).get('token');
488
+ if(!token){ veDone(false, 'رابط غير صالح', 'تأكد إنك فاتح الرابط اللي جالك في الإيميل كامل'); return; }
489
+ try{
490
+ const res = await API.verifyEmail(token);
491
+ API.setSession(res.token, res.user);
492
+ veDone(true, 'اتأكّد إيميلك 🎉', 'هندخلك المنصة دلوقتي...');
493
+ setTimeout(()=>location.href='index.html', 1200);
494
+ }catch(e){
495
+ veDone(false, 'الرابط منتهي أو غير صالح', e.message || 'اطلب رابط تأكيد جديد من صفحة الدخول');
496
+ }
497
+ })();
498
+ </script>
499
+ </body>
500
+ </html>