Swetha commited on
Commit
4c34168
·
1 Parent(s): edd1151

frontend update

Browse files
Files changed (48) hide show
  1. package-lock.json +0 -0
  2. package.json +3 -1
  3. src/app/api.service.ts +3 -2
  4. src/app/app-routing.module.ts +21 -5
  5. src/app/app.component.css +331 -0
  6. src/app/app.component.html +70 -0
  7. src/app/app.component.ts +117 -68
  8. src/app/auth/sign-in/sign-in.component.css +1 -0
  9. src/app/auth/sign-up/sign-up.component.css +1 -0
  10. src/app/auth/sign-up/sign-up.component.ts +14 -3
  11. src/app/auth/sign-up/sign-up.service.ts +19 -0
  12. src/app/intro-page/intro-page.component.css +872 -674
  13. src/app/intro-page/intro-page.component.html +244 -187
  14. src/app/intro-page/intro-page.component.ts +145 -19
  15. src/app/llm-quiz/llm-quiz.component.css +634 -176
  16. src/app/llm-quiz/llm-quiz.component.html +104 -108
  17. src/app/llm-quiz/llm-quiz.component.ts +133 -170
  18. src/app/matching-list/matching-list.component.css +586 -0
  19. src/app/matching-list/matching-list.component.html +169 -0
  20. src/app/{quiz/quiz.component.spec.ts → matching-list/matching-list.component.spec.ts} +6 -6
  21. src/app/matching-list/matching-list.component.ts +481 -0
  22. src/app/matching.service.spec.ts +59 -0
  23. src/app/matching.service.ts +65 -0
  24. src/app/question-answer/question-answer-service.service.ts +18 -12
  25. src/app/question-answer/question-answer.component.css +602 -226
  26. src/app/question-answer/question-answer.component.html +161 -53
  27. src/app/question-answer/question-answer.component.ts +455 -91
  28. src/app/quiz/quiz.component.css +0 -126
  29. src/app/quiz/quiz.component.html +0 -35
  30. src/app/quiz/quiz.component.ts +0 -107
  31. src/app/quiz/quiz.service.spec.ts +0 -16
  32. src/app/quiz/quiz.service.ts +0 -36
  33. src/app/services/llm-qa.service.ts +11 -13
  34. src/app/user-preferences/user-preferences.component.css +438 -0
  35. src/app/user-preferences/user-preferences.component.html +135 -0
  36. src/app/user-preferences/user-preferences.component.ts +280 -0
  37. src/app/user-preferences/user-preferences.service.ts +63 -0
  38. src/assets/favicon.ico +0 -0
  39. src/assets/interview1.png +0 -3
  40. src/assets/marriage-match/{11.JPG → 11.png} +0 -0
  41. src/assets/marriage-match/{14.JPG → 14.png} +0 -0
  42. src/assets/marriage-match/{16.JPG → 16.png} +0 -0
  43. src/assets/quiz-bg.png +0 -3
  44. src/assets/signup.png +0 -3
  45. src/assets/{1.png → wedding.png} +2 -2
  46. src/index.html +4 -3
  47. src/main.ts +15 -6
  48. src/styles.css +18 -62
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -10,11 +10,13 @@
10
  },
11
  "private": true,
12
  "dependencies": {
13
- "@angular/animations": "^16.1.0",
 
14
  "@angular/common": "^16.1.0",
15
  "@angular/compiler": "^16.1.0",
16
  "@angular/core": "^16.1.0",
17
  "@angular/forms": "^16.1.0",
 
18
  "@angular/platform-browser": "^16.1.0",
19
  "@angular/platform-browser-dynamic": "^16.1.0",
20
  "@angular/router": "^16.1.0",
 
10
  },
11
  "private": true,
12
  "dependencies": {
13
+ "@angular/animations": "^16.2.12",
14
+ "@angular/cdk": "^16.2.14",
15
  "@angular/common": "^16.1.0",
16
  "@angular/compiler": "^16.1.0",
17
  "@angular/core": "^16.1.0",
18
  "@angular/forms": "^16.1.0",
19
+ "@angular/material": "^16.2.14",
20
  "@angular/platform-browser": "^16.1.0",
21
  "@angular/platform-browser-dynamic": "^16.1.0",
22
  "@angular/router": "^16.1.0",
src/app/api.service.ts CHANGED
@@ -5,6 +5,7 @@ import { environment } from '../environments/environment';
5
  @Injectable({ providedIn: 'root' })
6
  export class ApiService {
7
 
 
8
  // Decide backend base URL at runtime
9
  private readonly base = location.hostname.endsWith('hf.space')
10
  ? 'https://pykara-py-match-backend.hf.space'
@@ -14,10 +15,10 @@ export class ApiService {
14
  constructor(private http: HttpClient) { }
15
 
16
  start(body = { n_questions: 10, batch_size: 5, domain: 'marriage' }) {
17
- return this.http.post<any>(`${this.base}/llm/start`, body);
18
  }
19
 
20
  next(session_id: string, selected_color: 'blue' | 'green' | 'red' | 'yellow') {
21
- return this.http.post<any>(`${this.base}/llm/next`, { session_id, selected_color });
22
  }
23
  }
 
5
  @Injectable({ providedIn: 'root' })
6
  export class ApiService {
7
 
8
+
9
  // Decide backend base URL at runtime
10
  private readonly base = location.hostname.endsWith('hf.space')
11
  ? 'https://pykara-py-match-backend.hf.space'
 
15
  constructor(private http: HttpClient) { }
16
 
17
  start(body = { n_questions: 10, batch_size: 5, domain: 'marriage' }) {
18
+ return this.http.post<any>(`${this.base}/q/start`, body);
19
  }
20
 
21
  next(session_id: string, selected_color: 'blue' | 'green' | 'red' | 'yellow') {
22
+ return this.http.post<any>(`${this.base}/q/next`, { session_id, selected_color });
23
  }
24
  }
src/app/app-routing.module.ts CHANGED
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
2
  import { RouterModule, Routes } from '@angular/router';
3
  import { IntroPageComponent } from './intro-page/intro-page.component';
4
  import { QuestionAnswerComponent } from './question-answer/question-answer.component';
5
- import { QuizComponent } from './quiz/quiz.component';
 
6
 
7
  const routes: Routes = [
 
 
 
8
  {
9
  path: 'auth',
10
  children: [
@@ -21,12 +25,24 @@ const routes: Routes = [
21
  ],
22
  },
23
 
24
- { path: '', component: IntroPageComponent, pathMatch: 'full' },
25
-
26
  { path: 'sign-in', redirectTo: 'auth/signin', pathMatch: 'full' },
27
  { path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
 
28
  { path: 'question-answer', component: QuestionAnswerComponent },
29
- { path: 'quiz', component: QuizComponent },
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  { path: '**', redirectTo: '' },
31
  ];
32
 
@@ -34,4 +50,4 @@ const routes: Routes = [
34
  imports: [RouterModule.forRoot(routes)],
35
  exports: [RouterModule],
36
  })
37
- export class AppRoutingModule { }
 
2
  import { RouterModule, Routes } from '@angular/router';
3
  import { IntroPageComponent } from './intro-page/intro-page.component';
4
  import { QuestionAnswerComponent } from './question-answer/question-answer.component';
5
+ import { LlmQuizComponent } from './llm-quiz/llm-quiz.component';
6
+ import { MatchingListComponent } from './matching-list/matching-list.component';
7
 
8
  const routes: Routes = [
9
+ { path: '', component: IntroPageComponent, pathMatch: 'full' },
10
+
11
+ // Auth lazy standalone components
12
  {
13
  path: 'auth',
14
  children: [
 
25
  ],
26
  },
27
 
 
 
28
  { path: 'sign-in', redirectTo: 'auth/signin', pathMatch: 'full' },
29
  { path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
30
+
31
  { path: 'question-answer', component: QuestionAnswerComponent },
32
+ { path: 'llmquiz', component: LlmQuizComponent },
33
+
34
+ // Preferences (standalone component lazy-loaded)
35
+ {
36
+ path: 'user-preferences',
37
+ loadComponent: () =>
38
+ import('./user-preferences/user-preferences.component').then(m => m.UserPreferencesComponent),
39
+ },
40
+
41
+ // Matching list routes (must be BEFORE wildcard). Provide optional id.
42
+ { path: 'matchinglist', component: MatchingListComponent },
43
+ { path: 'matchinglist/:id', component: MatchingListComponent },
44
+
45
+ // Wildcard LAST
46
  { path: '**', redirectTo: '' },
47
  ];
48
 
 
50
  imports: [RouterModule.forRoot(routes)],
51
  exports: [RouterModule],
52
  })
53
+ export class AppRoutingModule {}
src/app/app.component.css CHANGED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .header-bar {
2
+ background: #000 !important;
3
+ box-shadow: 0 2px 0 rgba(255,255,255,.04);
4
+ display: flex !important;
5
+ align-items: center !important;
6
+ justify-content: flex-start;
7
+ min-height: 82px;
8
+ padding: 14px clamp(1.4rem,4vw,3rem) !important;
9
+ gap: clamp(1.5rem,3vw,4rem);
10
+ position: fixed;
11
+ top: 0;
12
+ z-index: 2002;
13
+ width:100%;
14
+ }
15
+
16
+ .header-bar .brand {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ gap: 1.5vw;
20
+ cursor: pointer;
21
+ }
22
+
23
+ .header-bar .brand__logo-img {
24
+ background: #fff;
25
+ width: 74px;
26
+ min-width: 74px;
27
+ max-width: 74px;
28
+ padding: 6px;
29
+ border-radius: 50%;
30
+ box-shadow: 0 4px 10px rgba(0,0,0,.25);
31
+ }
32
+
33
+ .header-bar .brand__name {
34
+ font-family: Roliana;
35
+ font-size: 2.5vw;
36
+ font-weight: 300;
37
+ letter-spacing: 3.2px;
38
+ color: #fff;
39
+ text-shadow: 0 2px 4px rgba(0,0,0,.35);
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 2px;
43
+ }
44
+
45
+ .header-bar .brand__name strong {
46
+ color: #f3a54c;
47
+ font-weight: 300;
48
+ }
49
+
50
+ .header__nav {
51
+ flex: 1 1 auto !important;
52
+ display: flex !important;
53
+ align-items: center;
54
+ justify-content: center;
55
+ gap: clamp(2.2rem,3.4vw,4rem) !important;
56
+ margin: 0 !important;
57
+ white-space: nowrap;
58
+ }
59
+
60
+ .nav-link--icon {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ font-size: 0.9vw;
65
+ letter-spacing: 1px;
66
+ font-weight: 700;
67
+ color: #fff;
68
+ opacity: .92;
69
+ text-decoration: none;
70
+ padding: 6px 10px;
71
+ position: relative;
72
+ transition: .25s ease;
73
+ height: 46px;
74
+ line-height: 1;
75
+ cursor: pointer;
76
+ }
77
+
78
+ .nav-link--icon i {
79
+ font-size: .85rem;
80
+ opacity: .95;
81
+ }
82
+
83
+ .nav-link--icon:hover {
84
+ color: #f3a54c;
85
+ opacity: 1;
86
+ }
87
+
88
+ .nav-link--icon::after {
89
+ content: "";
90
+ position: absolute;
91
+ left: 8px;
92
+ right: 8px;
93
+ bottom: 2px;
94
+ height: 2px;
95
+ background: linear-gradient(90deg,#f3a54c,#ffcb7a);
96
+ transform: scaleX(0);
97
+ transform-origin: left;
98
+ transition: transform .35s ease;
99
+ border-radius: 2px;
100
+ }
101
+
102
+ .nav-link--icon:hover::after {
103
+ transform: scaleX(1);
104
+ }
105
+
106
+ .header__actions {
107
+ flex: 0 0 auto;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: flex-end;
111
+ gap: 1rem;
112
+ margin: 0 !important;
113
+ }
114
+
115
+ .btn-signup, .btn-signin {
116
+ border-radius: 10px;
117
+ font-size: .8rem;
118
+ padding: .65rem 1.25rem;
119
+ font-weight: 600;
120
+ letter-spacing: .5px;
121
+ line-height: 1;
122
+ cursor: pointer;
123
+ }
124
+
125
+ .btn-signup {
126
+ background: #f3a54c;
127
+ color: #0b0f1a;
128
+ border: none;
129
+ box-shadow: 0 4px 10px -2px rgba(243,165,76,.4);
130
+ }
131
+
132
+ .btn-signup:hover {
133
+ filter: brightness(1.08);
134
+ }
135
+
136
+ .btn-signin {
137
+ background: #0b0f1a;
138
+ color: #fff;
139
+ border: 2px solid #f3a54c;
140
+ }
141
+
142
+ .btn-signin:hover {
143
+ background: #121b27;
144
+ }
145
+
146
+ /* Marriage Sub Navigation */
147
+ .nav-item-wrapper {
148
+ position: relative;
149
+ display: inline-block;
150
+ }
151
+
152
+ .sub-nav {
153
+ position: absolute;
154
+ top: 100%;
155
+ left: 0;
156
+ background: #0b0f1a;
157
+ border: 1px solid rgba(255,255,255,.12);
158
+ border-radius: 8px;
159
+ padding: 8px 0;
160
+ min-width: 200px;
161
+ box-shadow: 0 8px 24px rgba(0,0,0,.4);
162
+ backdrop-filter: blur(14px);
163
+ z-index: 1000;
164
+ animation: subnavFadeIn 0.2s ease-out;
165
+ }
166
+
167
+ .sub-nav-link {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 10px;
171
+ padding: 10px 16px;
172
+ color: #e5e7eb;
173
+ text-decoration: none;
174
+ font-size: 0.75vw;
175
+ font-weight: 600;
176
+ letter-spacing: 0.5px;
177
+ transition: all 0.25s ease;
178
+ border: none;
179
+ background: none;
180
+ width: 100%;
181
+ text-align: left;
182
+ cursor: pointer;
183
+ }
184
+
185
+ .sub-nav-link:hover {
186
+ background: rgba(243, 165, 76, 0.1);
187
+ color: #f3a54c;
188
+ }
189
+
190
+ .sub-nav-link i {
191
+ font-size: 0.85rem;
192
+ width: 16px;
193
+ text-align: center;
194
+ }
195
+
196
+ @keyframes subnavFadeIn {
197
+ from {
198
+ opacity: 0;
199
+ transform: translateY(-8px);
200
+ }
201
+
202
+ to {
203
+ opacity: 1;
204
+ transform: translateY(0);
205
+ }
206
+ }
207
+
208
+ /* Ensure the main nav items have proper z-index */
209
+ .header__nav {
210
+ position: relative;
211
+ z-index: 1001;
212
+ }
213
+
214
+ @media (max-width:1100px) {
215
+ .header__nav {
216
+ gap: 1.6rem;
217
+ }
218
+ }
219
+
220
+ @media (max-width:880px) {
221
+ .header-bar {
222
+ flex-wrap: wrap;
223
+ min-height: unset;
224
+ }
225
+
226
+ .header__nav {
227
+ order: 3;
228
+ flex: 1 1 100%;
229
+ justify-content: center;
230
+ flex-wrap: wrap;
231
+ gap: .85rem 1.4rem;
232
+ padding-top: .35rem;
233
+ }
234
+
235
+ .header__actions {
236
+ order: 2;
237
+ }
238
+ }
239
+
240
+ @media (max-width:900px) {
241
+ .header-bar .brand__name {
242
+ font-size: 8vw;
243
+ }
244
+ }
245
+
246
+ @media (max-width:640px) {
247
+ .nav-link--icon {
248
+ font-size: .64rem;
249
+ }
250
+ }
251
+
252
+ /* Auth Modal (global) */
253
+ .modal-backdrop {
254
+ position: fixed;
255
+ inset: 0;
256
+ background: rgba(10,14,25,0.72);
257
+ backdrop-filter: blur(6px);
258
+ display: flex;
259
+ align-items: center; /* center vertically */
260
+ justify-content: center; /* center horizontally */
261
+ z-index: 4000; /* above header */
262
+ padding: 2rem 1.2rem;
263
+ }
264
+
265
+ .modal-panel {
266
+ /* width: 100%;
267
+ max-width: 540px;
268
+ background: #111a24;
269
+ border: 1px solid rgba(255,255,255,0.08);
270
+ border-radius: 22px;*/
271
+ /* box-shadow: 0 20px 48px -12px rgba(0,0,0,.65), 0 4px 18px -4px rgba(0,0,0,.45);*/
272
+ position: relative;
273
+ padding: 2.25rem 2.4rem 2.4rem;
274
+ color: #fff;
275
+ animation: modalPop .35s cubic-bezier(.4,.8,.2,1);
276
+ }
277
+
278
+ /*.modal-panel--white {*/ /* allow reuse if needed */
279
+ /*background: linear-gradient(145deg,#ffffff,#f7f9fb);
280
+ color: #0b0f1a;
281
+ }*/
282
+
283
+ .modal-close {
284
+ position: absolute;
285
+ top: 10px;
286
+ right: 12px;
287
+ background: none;
288
+ border: none;
289
+ font-size: 1.6rem;
290
+ line-height: 1;
291
+ cursor: pointer;
292
+ color: inherit;
293
+ opacity: .6;
294
+ transition: .25s ease;
295
+ }
296
+ .modal-close:hover { opacity: 1; }
297
+
298
+ @keyframes modalPop {
299
+ 0% { opacity: 0; transform: translateY(28px) scale(.94); }
300
+ 60% { opacity: 1; transform: translateY(-4px) scale(1.01); }
301
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
302
+ }
303
+
304
+ /* Make forms inside auth modal look nice if not already styled */
305
+ .modal-panel form .field label { font-weight: 600; letter-spacing:.4px; }
306
+ .modal-panel form input { width:100%; border:1px solid #d6dde3; border-radius:8px; padding:.7rem .85rem; font-size:.95rem; }
307
+ .modal-panel form input:focus { outline:2px solid #f3a54c; border-color:#f3a54c; }
308
+ .modal-panel h2.title { margin-top:0; font-size:1.65rem; font-weight:700; letter-spacing:.5px; }
309
+
310
+ /* Override dark text if using white variant */
311
+ .modal-panel--white form input { background:#fff; }
312
+
313
+ @media (max-width:640px) {
314
+ .modal-panel { padding:1.75rem 1.4rem 2rem; border-radius:18px; }
315
+ .modal-panel h2.title { font-size:1.4rem; }
316
+ }
317
+
318
+ /* Liked button on matching page */
319
+ .btn-liked {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ gap: .5rem;
323
+ background: rgba(243,165,76,.12);
324
+ color: #f3a54c;
325
+ border: 1px solid rgba(243,165,76,.4);
326
+ padding: .55rem .9rem;
327
+ border-radius: 10px;
328
+ font-weight: 700;
329
+ text-decoration: none;
330
+ }
331
+ .btn-liked:hover { background: rgba(243,165,76,.18); }
src/app/app.component.html CHANGED
@@ -1 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <router-outlet></router-outlet>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Top Navigation -->
2
+ <header class="topbar header-bar" role="banner" aria-label="Top navigation">
3
+ <a class="brand brand__link" (click)="goHome()" style="cursor: pointer;">
4
+ <img src="assets/pykara-logo.png" alt="Pykara Technologies logo" class="brand__logo-img" />
5
+ <span class="brand__name"><strong>Py</strong>-Match</span>
6
+ </a>
7
+
8
+ <!-- Center Navigation -->
9
+ <nav class="primary-nav header__nav" aria-label="Primary">
10
+ <div class="nav-item-wrapper">
11
+ <a (click)="toggleMarriageSubnav()" class="nav-link nav-link--icon">
12
+ <i class="fa-solid fa-ring"></i>
13
+ <span>MARRIAGE</span>
14
+ </a>
15
+
16
+ <!-- Marriage Sub Navigation -->
17
+ <div class="sub-nav" *ngIf="showMarriageSubnav" (mouseleave)="hideMarriageSubnav()">
18
+ <a (click)="selectRole('marriage'); navigateToProfile()" class="sub-nav-link">
19
+ <i class="fa-solid fa-user"></i>
20
+ <span>Profile</span>
21
+ </a>
22
+ <a (click)="navigateToExpectations()" class="sub-nav-link">
23
+ <i class="fa-solid fa-bullseye"></i>
24
+ <span>Your Expectations</span>
25
+ </a>
26
+ <a (click)="navigateToAssessment()" class="sub-nav-link">
27
+ <i class="fa-solid fa-clipboard-list"></i>
28
+ <span>Take Assessment</span>
29
+ </a>
30
+ <a (click)="navigateToMatchingProfile()" class="sub-nav-link">
31
+ <i class="fa-solid fa-chart-pie"></i>
32
+ <span>Your Matching Profile</span>
33
+ </a>
34
+ </div>
35
+ </div>
36
+ <a (click)="onSelectRole('interview')" class="nav-link nav-link--icon"><i class="fa-solid fa-user-tie"></i><span>INTERVIEW</span></a>
37
+ <a (click)="onSelectRole('partnership')" class="nav-link nav-link--icon"><i class="fa-solid fa-handshake"></i><span>PARTNERSHIP</span></a>
38
+ <a *ngIf="isIntroPage" (click)="scrollToFeatures()" class="nav-link nav-link--icon" aria-label="Jump to features section"><i class="fa-solid fa-grid"></i><span>FEATURES</span></a>
39
+ <a *ngIf="isIntroPage" (click)="scrollToJourney()" class="nav-link nav-link--icon" aria-label="Jump to how it works section"><i class="fa-solid fa-timeline"></i><span>HOW IT WORKS</span></a>
40
+ </nav>
41
+
42
+ <!-- Right Actions -->
43
+ <nav class="actions header__actions" aria-label="User actions">
44
+ <ng-container *ngIf="!isSignedIn; else signedInBlock">
45
+ <button type="button" class="btn btn-signup" (click)="onOpenSignUp()">Sign up</button>
46
+ <button type="button" class="btn btn-signin" (click)="onOpenSignIn()">Sign in</button>
47
+ </ng-container>
48
+ <ng-template #signedInBlock>
49
+ <button type="button" class="profile-btn" aria-label="Profile">
50
+ <i class="fa-solid fa-user" aria-hidden="true"></i>
51
+ </button>
52
+ <button type="button" class="btn btn-signin" (click)="onSignOut()">Sign out</button>
53
+ </ng-template>
54
+ </nav>
55
+ </header>
56
+
57
+ <!-- Main Content -->
58
  <router-outlet></router-outlet>
59
+
60
+ <!-- Global Auth Modal -->
61
+ <div *ngIf="authModal" class="modal-backdrop" (click)="closeAuthModal($event)">
62
+ <div class="modal-panel modal-panel--white" role="dialog" aria-modal="true" [attr.aria-label]="authModal === 'signin' ? 'Sign in' : 'Sign up'" (click)="$event.stopPropagation()">
63
+ <button type="button" class="modal-close" (click)="closeAuthModal()" aria-label="Close">×</button>
64
+ <app-sign-in *ngIf="authModal === 'signin'"
65
+ (switchToSignUp)="onOpenSignUp()"
66
+ (signInSuccess)="onSignInSuccess()"></app-sign-in>
67
+ <app-sign-up *ngIf="authModal === 'signup'"
68
+ (switchToSignIn)="onOpenSignIn()"
69
+ (signUpSuccess)="onSignUpSuccess()"></app-sign-up>
70
+ </div>
71
+ </div>
src/app/app.component.ts CHANGED
@@ -1,79 +1,128 @@
1
-
2
- //import { Component } from '@angular/core';
3
- //import { FormsModule } from '@angular/forms'; // Import FormsModule
4
- //import { CommonModule } from '@angular/common'; // Import CommonModule
5
-
6
- //import { IntroPageComponent } from './intro-page/intro-page.component';
7
- //import { SignInComponent } from './auth/sign-in/sign-in.component';
8
- //import { SignUpComponent } from './auth/sign-up/sign-up.component';
9
- //import { QuestionAnswerComponent } from './question-answer/question-answer.component';
10
- //import { QuizComponent } from './quiz/quiz.component';
11
- //import { ApiService } from './api.service';
12
- //import { RouterOutlet } from '@angular/router';
13
- //type Color = 'blue' | 'green' | 'red' | 'yellow';
14
-
15
- //@Component({
16
- // selector: 'app-root',
17
- // standalone: true,
18
- // imports: [RouterOutlet, QuestionAnswerComponent, IntroPageComponent, QuizComponent,
19
- // SignInComponent,
20
- // SignUpComponent,
21
- // FormsModule, // Add FormsModule here
22
- // CommonModule] , // Add CommonModule here],
23
- // templateUrl: './app.component.html',
24
- // styleUrls: ['./app.component.css']
25
- //})
26
- //export class AppComponent {
27
- // title = 'py-match';
28
- // sessionId: string | null = null;
29
- // index = 0;
30
- // total = 0;
31
- // question = '';
32
- // options: { text: string, color: Color }[] = [];
33
- // progress?: Record<Color, number>; // percentages
34
-
35
- // constructor(private api: ApiService) { }
36
-
37
- // start() {
38
- // this.api.start({ n_questions: 10, batch_size: 5, domain: 'marriage' })
39
- // .subscribe(res => {
40
- // this.sessionId = res.session_id;
41
- // this.index = res.index;
42
- // this.total = res.total;
43
- // this.question = res.question;
44
- // this.options = res.options;
45
- // this.progress = undefined; // first question has no progress yet
46
- // });
47
- // }
48
-
49
- // answer(color: Color) {
50
- // if (!this.sessionId) { return; }
51
- // this.api.next(this.sessionId, color).subscribe(res => {
52
- // if (res.done) {
53
- // alert('Finished! Check console for final mix.');
54
- // console.log('Final mix (percent):', res.mix);
55
- // return;
56
- // }
57
- // this.index = res.index;
58
- // this.total = res.total;
59
- // this.question = res.question;
60
- // this.options = res.options;
61
- // this.progress = res.progress; // already in percentages (from your backend change)
62
- // });
63
- // }
64
- //}
65
-
66
-
67
  import { Component } from '@angular/core';
68
- import { RouterOutlet } from '@angular/router';
 
 
 
 
69
 
70
  @Component({
71
  selector: 'app-root',
72
  standalone: true,
73
- imports: [RouterOutlet],
74
  templateUrl: './app.component.html',
75
  styleUrls: ['./app.component.css']
76
  })
77
  export class AppComponent {
78
  title = 'py-match';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { Component } from '@angular/core';
2
+ import { Router, RouterOutlet } from '@angular/router';
3
+ import { CommonModule } from '@angular/common';
4
+ import { SignupService } from './auth/sign-up/sign-up.service';
5
+ import { SignInComponent } from './auth/sign-in/sign-in.component';
6
+ import { SignUpComponent } from './auth/sign-up/sign-up.component';
7
 
8
  @Component({
9
  selector: 'app-root',
10
  standalone: true,
11
+ imports: [RouterOutlet, CommonModule, SignInComponent, SignUpComponent],
12
  templateUrl: './app.component.html',
13
  styleUrls: ['./app.component.css']
14
  })
15
  export class AppComponent {
16
  title = 'py-match';
17
+ showMarriageSubnav = false;
18
+ isSignedIn = false;
19
+ authModal: 'signin' | 'signup' | null = null; // global auth modal state
20
+
21
+ constructor(private router: Router, private signupService: SignupService) {
22
+ // Check if user is signed in
23
+ this.isSignedIn = !!localStorage.getItem('auth_token');
24
+ }
25
+
26
+ // Show intro-only nav items (Features, How it works) only on intro page
27
+ get isIntroPage(): boolean {
28
+ const url = (this.router.url || '').split('?')[0];
29
+ return url === '/' || url.startsWith('/intro');
30
+ }
31
+
32
+ // Show liked button only on matching list page
33
+ get isMatchingPage(): boolean {
34
+ const url = (this.router.url || '').split('?')[0];
35
+ return url.startsWith('/matchinglist');
36
+ }
37
+
38
+ toggleMarriageSubnav(): void {
39
+ this.showMarriageSubnav = !this.showMarriageSubnav;
40
+ }
41
+
42
+ hideMarriageSubnav(): void {
43
+ this.showMarriageSubnav = false;
44
+ }
45
+
46
+ onSelectRole(role: string): void {
47
+ const userId = localStorage.getItem('user_id');
48
+ if (!userId) {
49
+ alert('Please sign in first.');
50
+ return;
51
+ }
52
+
53
+ // Navigate based on role
54
+ if (role === 'marriage') {
55
+ this.router.navigate(['/question-answer'], { queryParams: { role: 'marriage' } });
56
+ } else {
57
+ this.router.navigate(['/question-answer'], { queryParams: { role } });
58
+ }
59
+ this.hideMarriageSubnav();
60
+ }
61
+
62
+ // Open auth modals instead of routing
63
+ onOpenSignIn(): void { this.authModal = 'signin'; }
64
+ onOpenSignUp(): void { this.authModal = 'signup'; }
65
+ closeAuthModal(evt?: MouseEvent): void {
66
+ if (evt && (evt.target as HTMLElement).classList.contains('modal-backdrop')) {
67
+ this.authModal = null;
68
+ } else if (!evt) {
69
+ this.authModal = null;
70
+ }
71
+ }
72
+
73
+ // Sign out
74
+ onSignOut(): void {
75
+ this.isSignedIn = false;
76
+ localStorage.removeItem('auth_token');
77
+ localStorage.removeItem('user_id');
78
+ this.router.navigate(['/']);
79
+ }
80
+
81
+ navigateToProfile(): void {
82
+ const userId = localStorage.getItem('user_id');
83
+ if (!userId) { alert('Please sign in first.'); return; }
84
+ this.router.navigate(['/question-answer'], { queryParams: { role: 'marriage' } });
85
+ this.hideMarriageSubnav();
86
+ }
87
+ navigateToExpectations(): void { this.hideMarriageSubnav(); this.router.navigate(['/userPreference']); }
88
+ navigateToAssessment(): void { this.hideMarriageSubnav(); this.router.navigate(['/llmquiz']); }
89
+ navigateToMatchingProfile(): void { this.hideMarriageSubnav(); this.router.navigate(['/matchinglist']); }
90
+
91
+ navigateToLikedProfiles(): void {
92
+ this.router.navigate(['/matchinglist'], { queryParams: { view: 'liked' } });
93
+ }
94
+
95
+ scrollToFeatures(): void { document.getElementById('features')?.scrollIntoView({ behavior: 'smooth' }); }
96
+ scrollToJourney(): void { document.getElementById('journey')?.scrollIntoView({ behavior: 'smooth' }); }
97
+ goHome(): void { this.router.navigate(['/']); }
98
+
99
+ selectRole(role: string): void {
100
+ const userId = localStorage.getItem('user_id');
101
+ if (!userId) { alert('Please sign in first.'); return; }
102
+ this.signupService.assignRole(+userId, role).subscribe({
103
+ next: (res: any) => {
104
+ console.log('Role saved:', res);
105
+ this.router.navigate(['/question-answer'], { queryParams: { role } });
106
+ },
107
+ error: (err: any) => {
108
+ console.error('Role assign failed:', err);
109
+ alert('Failed to assign role.');
110
+ }
111
+ });
112
+ }
113
+
114
+ // Auth modal child callbacks
115
+ onSignInSuccess(): void {
116
+ this.isSignedIn = true;
117
+ // Placeholder: set token if backend integrates
118
+ if (!localStorage.getItem('auth_token')) {
119
+ localStorage.setItem('auth_token', 'true');
120
+ }
121
+ this.authModal = null;
122
+ }
123
+
124
+ onSignUpSuccess(): void {
125
+ // After signup, show sign-in form
126
+ this.authModal = 'signin';
127
+ }
128
  }
src/app/auth/sign-in/sign-in.component.css CHANGED
@@ -43,6 +43,7 @@
43
  margin-bottom: 14px;
44
  border: 2px solid #b1b1b17d;
45
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
 
46
  }
47
 
48
  .title {
 
43
  margin-bottom: 14px;
44
  border: 2px solid #b1b1b17d;
45
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
46
+ border-radius:50%;
47
  }
48
 
49
  .title {
src/app/auth/sign-up/sign-up.component.css CHANGED
@@ -56,6 +56,7 @@
56
  margin-bottom: 14px;
57
  border: 2px solid #b1b1b17d;
58
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
 
59
  }
60
 
61
  .title {
 
56
  margin-bottom: 14px;
57
  border: 2px solid #b1b1b17d;
58
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
59
+ border-radius:50%;
60
  }
61
 
62
  .title {
src/app/auth/sign-up/sign-up.component.ts CHANGED
@@ -67,7 +67,13 @@ export class SignUpComponent {
67
  const res = await this.signupService.signup(payload).toPromise();
68
  if (res && res.message) {
69
  alert('Success: ' + res.message);
70
- this.signUpSuccess.emit(); // Emit success instead of navigating
 
 
 
 
 
 
71
  } else if (res && res.error) {
72
  alert('Signup failed: ' + res.error);
73
  } else {
@@ -87,14 +93,19 @@ export class SignUpComponent {
87
  return error ? !!(c.touched && c.errors?.[error]) : !!(c.touched && c.invalid);
88
  }
89
 
90
- navigateHome() { this.router.navigateByUrl('/'); }
 
 
91
 
92
  goToLogin() {
93
  this.switchToSignIn.emit();
94
  }
95
 
96
  tr(key: string): string {
97
- const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
 
 
 
98
  return map[key] || '';
99
  }
100
  }
 
67
  const res = await this.signupService.signup(payload).toPromise();
68
  if (res && res.message) {
69
  alert('Success: ' + res.message);
70
+
71
+ // Save user_id from backend for role assignment
72
+ if (res.user_id) {
73
+ localStorage.setItem('user_id', res.user_id.toString());
74
+ }
75
+
76
+ this.signUpSuccess.emit(); // Emit success
77
  } else if (res && res.error) {
78
  alert('Signup failed: ' + res.error);
79
  } else {
 
93
  return error ? !!(c.touched && c.errors?.[error]) : !!(c.touched && c.invalid);
94
  }
95
 
96
+ navigateHome() {
97
+ this.router.navigateByUrl('/');
98
+ }
99
 
100
  goToLogin() {
101
  this.switchToSignIn.emit();
102
  }
103
 
104
  tr(key: string): string {
105
+ const map: Record<string, string> = {
106
+ title: 'Create your account',
107
+ subtitle: 'Join to continue'
108
+ };
109
  return map[key] || '';
110
  }
111
  }
src/app/auth/sign-up/sign-up.service.ts CHANGED
@@ -13,6 +13,7 @@ export interface SignupPayload {
13
  export interface SignupResponse {
14
  message?: string; // success message from backend
15
  error?: string; // error message from backend
 
16
  }
17
 
18
  @Injectable({
@@ -20,6 +21,7 @@ export interface SignupResponse {
20
  })
21
  export class SignupService {
22
 
 
23
  // Exactly the same style as your other service:
24
  // hf.space => Hugging Face backend; otherwise => local Flask
25
  private baseUrl = location.hostname.endsWith('hf.space')
@@ -36,8 +38,25 @@ export class SignupService {
36
  catchError(this.handleError)
37
  );
38
  }
 
 
 
 
 
 
 
39
 
40
  private handleError(error: HttpErrorResponse) {
41
  return throwError(() => new Error(error.message || 'Something went wrong'));
42
  }
 
 
 
 
 
 
 
 
 
 
43
  }
 
13
  export interface SignupResponse {
14
  message?: string; // success message from backend
15
  error?: string; // error message from backend
16
+ user_id?: number; // <-- add this line
17
  }
18
 
19
  @Injectable({
 
21
  })
22
  export class SignupService {
23
 
24
+ public user_id: number | null = null;
25
  // Exactly the same style as your other service:
26
  // hf.space => Hugging Face backend; otherwise => local Flask
27
  private baseUrl = location.hostname.endsWith('hf.space')
 
38
  catchError(this.handleError)
39
  );
40
  }
41
+ assignRole(userId: number, roleName: string) {
42
+ return this.http.post(`${this.apiUrl}/questions/select-role`, {
43
+ user_id: userId,
44
+ role_name: roleName,
45
+ assigned_at: new Date().toISOString()
46
+ });
47
+ }
48
 
49
  private handleError(error: HttpErrorResponse) {
50
  return throwError(() => new Error(error.message || 'Something went wrong'));
51
  }
52
+
53
+ // Method to store user_id after successful signup
54
+ setUserId(userId: number): void {
55
+ this.user_id = userId;
56
+ }
57
+ // Get user_id (to use in other components)
58
+ getUserId(): number | null {
59
+ console.log(this.user_id)
60
+ return this.user_id;
61
+ }
62
  }
src/app/intro-page/intro-page.component.css CHANGED
@@ -2,904 +2,1102 @@
2
  display: block;
3
  }
4
 
5
- * {
6
- box-sizing: border-box;
7
  }
8
-
9
- .sr-only {
10
- position: absolute;
11
  width: 1px;
12
  height: 1px;
13
- margin: -1px;
14
  padding: 0;
 
15
  overflow: hidden;
16
- clip: rect(0,0,0,0);
 
17
  border: 0;
18
  }
19
-
20
- .skip-link {
21
- position: absolute;
22
- left: -9999px;
23
- top: auto;
24
- width: 1px;
25
- height: 1px;
26
- overflow: hidden;
27
  }
28
-
29
- .skip-link:focus {
30
- left: 16px;
31
- top: 12px;
32
- width: auto;
33
- height: auto;
34
- padding: 8px 10px;
35
- background: #fff;
36
- color: #000;
37
- border-radius: 6px;
38
- z-index: 10;
39
- }
40
-
41
- .page {
42
  height: 100vh;
43
  display: flex;
44
- flex-direction: column;
45
- background: url(/assets/11.png) no-repeat center center fixed;
46
- background-size: cover;
47
- color: #e5e7eb;
48
- justify-content: space-evenly;
49
- gap: 12vw;
50
  }
51
 
52
- /* Topbar aligned like QA */
53
- .topbar {
54
- position: fixed;
55
- top: 0;
56
- left: 0;
57
- z-index: 100;
58
- background: #000000d4;
59
- -webkit-backdrop-filter: blur(8px);
60
- backdrop-filter: blur(8px);
61
- padding: 17px 97px;
62
- justify-content: space-between;
63
- display: flex;
64
- width: 100%;
65
  align-items: center;
 
66
  }
67
 
68
- .brand {
69
- display: inline-flex;
70
- align-items: center;
71
- gap: 1.5vw;
72
- text-decoration: none;
73
- cursor: pointer;
74
- user-select: none;
75
  }
76
 
77
- /* Match QA brand logo sizing/behavior */
78
- .brand__logo-img {
79
- background: #ffffff;
80
- max-width: 4.2vw;
81
- min-width: 56px;
82
- height: auto;
83
- border-radius: 50%;
84
- padding: 4px;
85
- box-shadow: 0 4px 10px rgba(0,0,0,0.25);
86
- transition: transform .25s ease;
87
  }
88
 
89
- .brand:hover .brand__logo-img {
90
- transform: scale(1.05);
 
 
 
 
 
91
  }
92
 
93
- .brand__name {
94
- letter-spacing: 3.2px;
95
- font-size: 2.5vw;
96
- font-family: Roliana;
97
- font-weight: lighter;
98
  }
99
 
100
- /* Color the "Py" like QA */
101
- .brand__name strong {
102
- font-weight: unset;
103
- color: #f3a54c;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
 
106
- .brand__link {
107
- display: inline-flex;
108
- align-items: center;
109
- color: white;
 
 
 
 
 
110
  }
111
 
112
- .actions {
113
  display: flex;
114
- align-items: center;
115
- gap: 8px;
116
  }
117
 
118
- /* Controls / buttons */
119
- .select {
120
- background: #0b0f1a;
121
- color: #e5e7eb;
122
- border: 1px solid #2b3246;
123
- border-radius: 10px;
124
- padding: 8px 10px;
 
 
 
 
 
125
  }
126
 
127
- .btn {
128
- display: inline-flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  align-items: center;
130
  justify-content: center;
131
- gap: 8px;
132
- background: #0b0f1a;
133
- color: #e5e7eb;
134
- border: 1px solid #2b3246;
135
- border-radius: 10px;
136
- padding: 10px 14px;
137
- cursor: pointer;
138
- text-decoration: none;
139
- border: 2px solid #f3a54c;
140
  }
141
 
142
- .btn:hover {
143
- border-color: #00ff88;
 
 
 
 
 
 
 
 
144
  }
 
 
 
 
 
145
 
146
- .btn-primary {
147
- background: #f3a54c;
148
- color: #0b0f1a;
149
- border: none;
150
- font-weight: 700;
 
 
151
  }
152
 
153
- .btn-ghost {
154
- background: #0b0f1a;
 
 
 
 
 
 
 
 
155
  }
156
 
157
- .btn-disabled:hover {
158
- border-color: #f3a54c !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
 
161
- .hero {
162
- margin-left: 5vw;
163
- position: absolute;
164
- top: 50%;
165
- left: 16.3%;
166
- transform: translate(-50%, -16.3%);
167
  }
168
 
169
- .hero__content {
170
- max-width: 620px;
 
 
 
 
 
 
 
 
 
 
171
  }
172
 
173
- .hero__title {
174
- margin: 0 0 25px;
175
- font-size: 2.3vw;
176
- line-height: 1.2;
177
- font-weight: bold;
 
 
 
 
 
 
178
  }
179
 
180
- .hero__text {
181
- color: #b9c2d0;
182
- margin: 0 0 34px;
183
- font-size: 0.9vw;
184
- font-weight: 500;
185
- line-height: 1.5vw;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
- .cta {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  display: flex;
190
- gap: 10px;
191
- flex-direction: row-reverse;
192
- justify-content: flex-end;
193
  }
194
 
195
- /* Footer */
196
- .footer {
 
 
 
 
 
197
  display: flex;
198
- align-items: center;
199
- gap: 16px;
200
- margin-left: 5vw;
201
- font-size: 0.7vw;
202
  }
203
 
204
- /* remove styles targeting removed footer link */
205
- .footer a {
206
- all: unset;
 
 
207
  }
208
 
209
- .about-btn {
210
- display: inline-flex;
211
- align-items: center;
212
- gap: 8px;
213
- padding: 6px 8px;
214
- border-radius: 8px;
215
- border: 1px solid rgba(255,255,255,.25);
216
- background: rgba(11, 15, 26, 0.6);
217
- color: #cfe8ff;
218
- cursor: pointer;
219
  }
220
 
221
- .about-btn:hover, .footer a:hover {
222
- border-color: #00ff88;
 
 
 
 
 
 
223
  }
224
 
225
- /* High contrast mode */
226
- .page.high-contrast {
227
- background: #000;
228
- color: #fff;
 
 
 
 
 
 
 
 
229
  }
230
 
231
- .page.high-contrast a {
232
- color: #fff;
233
- text-decoration: underline;
234
- }
 
 
 
235
 
236
- .page.high-contrast .card, .page.high-contrast .banner {
237
- background: #000;
238
- border: 2px solid #fff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  }
240
 
241
- .page.high-contrast .btn, .page.high-contrast .select {
242
- border-color: #fff !important;
 
 
 
 
243
  }
244
 
245
- /* AUTH Modal overlay */
246
- .modal-backdrop {
247
- position: fixed;
248
- inset: 0;
249
- background: rgb(255 255 255 / 34%);
250
- display: grid;
251
- place-items: center;
252
- z-index: 1000;
253
- animation: fadeIn 0.2s ease-out;
254
- backdrop-filter: blur(8px);
255
  }
256
 
257
- .modal-panel {
258
- animation: slideIn 0.3s ease-out;
259
- }
 
 
260
 
261
- @keyframes fadeIn {
262
- from {
263
- opacity: 0;
264
  }
265
 
266
- to {
267
- opacity: 1;
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
  }
 
 
 
 
 
 
 
270
 
271
- @keyframes slideIn {
272
- from {
273
- opacity: 0;
274
- transform: translateY(-20px);
 
275
  }
276
 
277
- to {
 
 
 
 
 
 
 
 
278
  opacity: 1;
279
- transform: translateY(0);
280
  }
 
 
 
 
 
 
 
 
 
281
  }
282
 
283
- button.modal-close {
284
- background: white;
285
- color: black;
286
- position: relative;
287
- left: 48.2vw;
288
- top: 0.7vw;
289
- z-index: 11;
290
- border-radius: 50%;
291
- cursor: pointer;
292
- font-size: 1vw;
293
- font-weight: bold;
294
  }
295
 
296
- /* Ensure the auth components fit properly in the modal */
297
- .modal-panel app-sign-in,
298
- .modal-panel app-sign-up {
299
- display: block;
300
- width: 100%;
301
  }
302
 
303
- /* Enhanced ABOUT Modal backdrop */
304
- .about-backdrop {
305
- position: fixed;
306
- inset: 0;
307
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 100%);
308
- backdrop-filter: blur(8px);
309
- -webkit-backdrop-filter: blur(8px);
310
- z-index: 1100; /* above auth backdrop */
311
- animation: fadeIn 0.3s ease-out;
312
  display: flex;
313
  align-items: center;
 
 
 
 
314
  justify-content: center;
315
  }
316
 
317
- /* Add a subtle pattern/texture to the backdrop */
318
- .about-backdrop::before {
319
- content: "";
320
- position: absolute;
321
- inset: 0;
322
- background-image: radial-gradient(circle at 25% 25%, rgba(243, 165, 76, 0.1) 0%, transparent 50%), radial-gradient(circle at 75% 75%, rgba(0, 255, 136, 0.1) 0%, transparent 50%);
323
- pointer-events: none;
324
- }
325
-
326
- /* Add a subtle animation to the backdrop pattern */
327
- @keyframes subtleShift {
328
- 0%, 100% {
329
- transform: translate(0, 0);
330
  }
331
 
332
- 50% {
333
- transform: translate(3px, 3px);
334
- }
335
- }
336
 
337
- .about-backdrop::before {
338
- animation: subtleShift 15s ease-in-out infinite;
339
- }
 
 
 
 
 
 
 
 
340
 
341
- /* Enhanced about modal */
342
- .about-modal {
343
- position: fixed;
344
- z-index: 1101; /* above auth modal */
345
- inset: 50% auto auto 50%;
346
- transform: translate(-50%, -50%);
347
- width: min(720px, 92vw);
348
- max-height: 80vh;
349
- overflow: auto;
350
- background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
351
- color: #111;
352
- border-radius: 16px;
353
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
354
- outline: none;
355
- animation: modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
356
  }
357
 
358
- @keyframes modalSlideIn {
359
- from {
360
- opacity: 0;
361
- transform: translate(-50%, -48%);
 
 
 
 
 
 
 
 
 
362
  }
363
 
364
- to {
365
- opacity: 1;
366
- transform: translate(-50%, -50%);
 
 
 
 
 
 
367
  }
368
- }
369
 
370
- .about-modal__header {
371
- padding: 20px 24px;
372
- border-bottom: 1px solid rgba(0, 0, 0, 0.08);
373
- display: flex;
374
- align-items: center;
375
- background: #dedee7;
376
- color: #0b0f1a;
377
- border-radius: 16px 16px 0 0;
378
- }
379
 
380
- .about-modal__header h2 {
381
- margin: 0;
382
- font-size: 1.5rem;
383
- font-weight: 700;
384
  }
385
 
386
- .about-modal__body {
387
- padding: 24px;
388
- line-height: 1.6;
389
- background: #ffffff;
390
- text-align: justify;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
 
 
 
 
 
392
 
393
- .about-modal__body p {
394
- margin-bottom: 16px;
 
 
 
 
 
 
395
  }
396
 
397
- .about-modal__footer {
398
- padding: 16px 24px;
399
- border-top: 1px solid rgba(0, 0, 0, 0.08);
400
- display: flex;
401
- justify-content: flex-end;
402
- gap: 12px;
403
- background: #f8f9fa;
404
- border-radius: 0 0 16px 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  }
406
 
407
- /* Enhanced close button for the about modal */
408
- .about-close {
409
- margin-left: auto;
410
- border: none;
411
- color: #000000;
412
- border: 2px solid #f3a54c;
413
- width: 28px;
414
- height: 28px;
415
- border-radius: 50%;
416
- font-size: 22px;
417
- cursor: pointer;
418
- display: flex;
419
  align-items: center;
420
- justify-content: center;
421
- transition: all 0.2s ease;
422
- font-weight: bold;
423
- background: transparent;
 
 
 
 
 
 
 
 
424
  }
425
 
426
- .about-close:hover {
427
- border: 2px solid #00ff88;
428
- /*transform: scale(0.8);*/
429
  }
430
 
431
- .about-close:focus {
432
- outline: 2px solid #00ff88;
433
- outline-offset: 2px;
434
  }
435
 
436
- /* High contrast mode adjustments */
437
- .page.high-contrast .about-backdrop {
438
- background: rgba(0, 0, 0, 0.8);
439
- }
 
 
 
 
 
 
 
 
 
440
 
441
- .page.high-contrast .about-backdrop::before {
442
- display: none;
443
  }
444
 
445
- .page.high-contrast .about-modal {
446
- border: 2px solid #fff;
447
- background: #000;
448
- color: #fff;
 
 
 
449
  }
450
 
451
- .page.high-contrast .about-modal__header {
452
- border-bottom: 2px solid #fff;
453
- background: #000;
 
 
 
 
454
  }
455
 
456
- .page.high-contrast .about-modal__footer {
457
- border-top: 2px solid #fff;
458
- background: #000;
 
 
459
  }
460
 
461
- /* Reduced motion support */
462
- @media (prefers-reduced-motion: reduce) {
463
- .about-backdrop,
464
- .about-modal {
465
- animation: none;
466
  }
467
 
468
- .about-backdrop::before {
469
- animation: none;
470
- }
 
 
471
 
472
- .about-close:hover {
473
- transform: none;
474
  }
 
 
 
 
 
 
 
 
475
  }
476
 
477
- /* Enhanced tooltip for Get Started when not signed in */
478
- .tooltip {
479
- position: relative;
480
- display: inline-block;
481
- /* theme tokens */
482
- --t-bg: #cfe8ff; /* requested background */
483
- --t-border: #2b3246;
484
- --t-text: #0b0f1a; /* dark text for contrast */
485
- --t-shadow: rgba(0,0,0,.25);
486
- --t-glow: rgba(0,255,136,.18); /* brand neon */
487
- }
488
-
489
- /* Bubble positioned to the right of trigger */
490
- .tooltip__bubble {
491
- transform: translateY(-50%) translateX(-6px) scale(0.98);
492
- padding: 10px 14px;
493
- opacity: 0;
494
- pointer-events: none;
495
- transition: opacity .18s ease, transform .18s ease;
496
- transition-delay: .06s;
497
- z-index: 20;
498
  backdrop-filter: blur(6px);
499
  -webkit-backdrop-filter: blur(6px);
500
- position: absolute;
501
- top: 50%;
502
- left: calc(100% + 12px);
503
- background: #cfe8ffb5;
504
- color: var(--t-text);
505
- padding: 10px 14px;
506
- border-radius: 12px;
507
- font-size: 10px;
508
- transition: opacity .18s ease, transform .18s ease;
509
- width: max-content;
510
  }
511
 
512
- /* subtle top highlight */
513
- .tooltip__bubble::before {
514
- content: "";
515
- position: absolute;
516
- inset: 0;
517
- border-radius: inherit;
518
- pointer-events: none;
519
- background: radial-gradient(120px 40px at 50% -20px, rgba(255,255,255,.35), transparent 70%);
520
- }
521
-
522
- /* Arrow on the left side pointing to trigger */
523
- .tooltip__bubble::after {
524
- content: "";
525
- position: absolute;
526
- top: 50%;
527
- left: -16px;
528
- transform: translateY(-50%);
529
- border: 8px solid transparent;
530
- border-right-color: var(--t-bg);
531
- filter: drop-shadow(0 2px 0 rgba(0,0,0,.12));
532
- }
533
-
534
- .tooltip.tooltip--active:hover .tooltip__bubble,
535
- .tooltip.tooltip--active:focus-within .tooltip__bubble,
536
- .tooltip.tooltip--active .tooltip__bubble:hover,
537
- .tooltip.tooltip--active .tooltip__bubble:focus {
538
- opacity: 1;
539
- transform: translateY(-50%) translateX(0) scale(1);
540
- pointer-events: auto;
541
- animation: tooltip-pop-right .22s cubic-bezier(.2,.8,.25,1);
542
- }
543
-
544
- @keyframes tooltip-pop-right {
545
- 0% {
546
- opacity: 0;
547
- transform: translateY(-50%) translateX(-10px) scale(.96);
548
- }
549
-
550
- 60% {
551
- opacity: 1;
552
- transform: translateY(-50%) translateX(2px) scale(1.02);
553
  }
554
 
555
- 100% {
556
- opacity: 1;
557
- transform: translateY(-50%) translateX(0) scale(1);
558
  }
559
  }
560
 
561
- /* High contrast friendly tooltip */
562
- .page.high-contrast .tooltip__bubble {
563
- background: #000;
564
- color: #fff;
565
- border-color: #fff;
566
- box-shadow: 0 0 0 2px #fff;
 
 
567
  }
568
 
569
- .page.high-contrast .tooltip__bubble::after {
570
- border-right-color: #000;
571
- }
 
572
 
573
- /* Keep right-side placement on small screens as requested */
574
- @media (prefers-reduced-motion: reduce) {
575
- .tooltip__bubble {
576
- transition: none;
577
  }
578
 
579
- .tooltip.tooltip--active:hover .tooltip__bubble,
580
- .tooltip.tooltip--active:focus-within .tooltip__bubble {
581
- animation: none;
582
  }
583
  }
584
 
585
- /* Cards Section */
586
- .cards {
 
 
 
 
 
 
 
 
 
 
 
587
  display: flex;
588
- justify-content: space-around;
589
- padding: 60px 10%;
590
- background: #f9f9f9;
591
- flex-wrap: wrap;
592
  }
593
 
594
- .card {
595
- width: 30%;
596
  background: #fff;
597
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
598
- margin-bottom: 20px;
599
- border-radius: 12px;
600
- overflow: hidden;
601
- text-align: left;
602
  }
603
 
604
- .card img {
605
- width: 100%;
606
- height: 200px;
607
- object-fit: cover;
608
- }
609
-
610
- .card h3 {
611
- margin: 15px;
612
- font-size: 20px;
613
- color: #444;
614
- }
615
-
616
- .card p {
617
- margin: 0 15px 20px 15px;
618
- font-size: 0.89vw;
619
- line-height: 1.5;
620
- text-align: justify;
621
- color: black;
622
- }
623
-
624
- /* Scrolling Images */
625
- .scroll-container {
626
- overflow: hidden;
627
- white-space: nowrap;
628
- padding: 20px 0;
629
- position: relative;
630
  }
631
 
632
- /* Use flex track with two identical sequences for seamless loop */
633
- .scroll-images {
634
- display: flex;
635
- align-items: center;
636
- animation: scroll-left 40s linear infinite;
637
- width: max-content;
638
  }
639
 
640
- .scroll-images img {
641
- width: 10vw;
642
- height: 10vw;
643
- margin: 0 10px;
644
- border-radius: 8px;
645
- object-fit: cover;
646
- flex: 0 0 auto;
647
- }
648
-
649
- /* Pause on hover (optional UX improvement) */
650
- .scroll-container:hover .scroll-images {
651
- animation-play-state: paused;
652
  }
653
 
654
- @keyframes scroll-left {
655
- 0% {
656
- transform: translateX(0);
657
- }
658
-
659
- 100% {
660
- transform: translateX(-50%);
661
- }
662
- /* shift exactly one set width (because duplicated) */
663
  }
664
 
665
- /* Special Content */
666
- .special-content {
667
- background: #dedee7;
668
- padding: 50px 15%;
669
- text-align: center;
670
  }
671
 
672
- .special-content h2 {
673
- font-size: 28px;
674
- margin-bottom: 20px;
 
675
  }
676
 
677
- .special-content p {
678
- font-size: 18px;
679
- line-height: 1.6;
 
 
 
 
680
  }
681
 
682
- /* Footer */
683
- footer {
684
- background: black;
685
- color: #fff;
686
- text-align: center;
687
- padding: 15px 10px;
688
  }
689
 
690
- footer a {
691
- color: #fff;
692
- text-decoration: none;
693
- margin: 0 10px;
694
  }
 
695
 
696
- footer .social-icons {
697
- margin-top: 10px;
 
698
  }
 
699
 
700
- footer .social-icons img {
701
- width: 25px;
702
- margin: 0 8px;
703
- vertical-align: middle;
704
- }
705
-
706
- /* Responsive adjustments */
707
- @media (max-width: 768px) {
708
- .card {
709
- width: 100%;
710
- }
711
 
712
- .brand__name {
713
- font-size: 1.5rem;
714
- }
715
 
716
- .topbar {
717
- gap: 20px;
718
- justify-content: space-between;
719
- }
720
 
721
- .cards {
722
- padding: 30px 5%;
723
- }
724
 
725
- .special-content {
726
- padding: 30px 5%;
727
- }
728
  }
729
 
730
- /* Adjust page padding to account for fixed header/footer */
731
- .page {
732
- padding-top: 70px; /* Space for fixed header */
733
- padding-bottom: 80px; /* Space for fixed footer */
734
- height: auto;
735
- min-height: 100vh;
736
  }
737
 
738
- /* Adjust cards section to account for fixed elements */
739
- .cards {
740
- padding: 80px 51px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  }
742
 
743
- /* Adjust scrolling images section */
744
- .scroll-container {
745
- margin: 20px 0;
 
746
  }
747
 
748
- /* Adjust special content section */
749
- .special-content {
750
- padding: 60px 15% 80px;
 
 
751
  }
752
 
753
- /* Responsive adjustments for fixed elements */
754
- @media (max-width: 768px) {
755
- .topbar {
756
- gap: 20px;
757
- padding: 8px 12px;
758
- }
759
-
760
- .page {
761
- padding-top: 60px;
762
- padding-bottom: 70px;
763
- }
764
-
765
- .cards {
766
- padding: 70px 5% 30px;
767
- }
768
 
769
- .special-content {
770
- padding: 40px 5% 60px;
 
771
  }
772
 
773
- footer .social-icons img {
774
- width: 20px;
775
- margin: 0 5px;
776
- }
777
  }
778
 
779
- /* Social Media Icons */
780
- .social-icons {
781
  display: flex;
782
- justify-content: center;
783
- gap: 15px;
784
- margin-top: 10px;
785
  }
786
 
787
- .social-icon {
788
- display: inline-flex;
789
- align-items: center;
790
- justify-content: center;
791
- width: 36px;
792
- height: 36px;
793
- border-radius: 50%;
794
- background: rgba(255, 255, 255, 0.1);
795
- color: white;
796
- text-decoration: none;
797
- font-size: 18px;
798
- transition: all 0.3s ease;
799
- border: 1px solid rgba(255, 255, 255, 0.2);
800
  }
801
 
802
- .social-icon:hover {
803
- transform: translateY(-3px);
804
- background: rgba(255, 255, 255, 0.2);
805
  }
806
 
807
- /* Specific colors for each platform */
808
- .social-icon.facebook:hover {
809
- background: #3b5998;
810
- box-shadow: 0 4px 8px rgba(59, 89, 152, 0.3);
811
  }
812
 
813
- .social-icon.youtube:hover {
814
- background: #ff0000;
815
- box-shadow: 0 4px 8px rgba(255, 0, 0, 0.3);
816
  }
 
817
 
818
- .social-icon.linkedin:hover {
819
- background: #0077b5;
820
- box-shadow: 0 4px 8px rgba(0, 119, 181, 0.3);
 
821
  }
822
 
823
- .social-icon.instagram:hover {
824
- background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
825
- box-shadow: 0 4px 8px rgba(188, 24, 136, 0.3);
826
  }
 
827
 
828
- /* Responsive adjustments for social icons */
829
- @media (max-width: 768px) {
830
- .social-icons {
831
- gap: 10px;
832
- }
833
 
834
- .social-icon {
835
- width: 32px;
836
- height: 32px;
837
- font-size: 16px;
838
- }
 
 
 
 
 
 
 
 
839
  }
840
 
841
- /* New special-card styles */
842
- .special-card {
843
- width: min(840px, 90%);
844
- margin: 0 auto;
845
- background: #fff;
846
- padding: 28px 32px 34px;
847
- box-shadow: 0 12px 32px -8px rgba(0,0,0,0.15);
848
- border: 1px solid rgba(0,0,0,0.06);
849
- border-radius: 20px;
 
 
 
 
 
850
  text-align: left;
 
851
  }
852
 
853
- .special-card h3 {
854
- margin: 0 0 16px;
855
- font-size: 28px;
856
- background: linear-gradient(90deg, #f3a54c, #ffcb7a);
857
- -webkit-background-clip: text;
858
- color: transparent;
859
- letter-spacing: .5px;
860
- color: black;
861
  text-align: center;
862
  }
863
 
864
- .special-card p {
865
- margin: 0;
866
- line-height: 1.6;
867
- color: #000000;
868
  }
869
 
870
- .primary-nav {
871
- display: flex;
872
- gap: 1.8rem;
 
873
  }
874
 
875
- .nav-link {
 
876
  position: relative;
877
- font-size: 0.9rem;
878
- letter-spacing: 1.5px;
879
- text-transform: uppercase;
880
- font-weight: 600;
881
- color: #e5e7eb;
882
- text-decoration: none;
883
- padding: 6px 4px;
884
- transition: color .25s ease;
885
  }
886
-
887
- .nav-link::after {
888
- content: "";
889
- position: absolute;
890
- left: 0;
891
- bottom: 0;
892
- width: 0;
893
- height: 2px;
894
- background: linear-gradient(90deg,#f3a54c,#ffcb7a);
895
- transition: width .3s ease;
896
- border-radius: 2px;
897
- }
898
-
899
- .nav-link:hover {
900
- color: #f3a54c;
901
- }
902
-
903
- .nav-link:hover::after {
904
- width: 100%;
905
- }
 
2
  display: block;
3
  }
4
 
5
+ #journeyTitle {
6
+ color: black !important;
7
  }
8
+ /* Utility */
9
+ .visually-hidden {
10
+ position: absolute !important;
11
  width: 1px;
12
  height: 1px;
 
13
  padding: 0;
14
+ margin: -1px;
15
  overflow: hidden;
16
+ clip: rect(0 0 0 0);
17
+ white-space: nowrap;
18
  border: 0;
19
  }
20
+ /* Layout container */
21
+ .container {
22
+ width: 80%;
23
+ margin: 0 auto;
 
 
 
 
24
  }
25
+ /* HERO */
26
+ .hero {
27
+ position: relative;
 
 
 
 
 
 
 
 
 
 
 
28
  height: 100vh;
29
  display: flex;
30
+ align-items: center;
31
+ background: radial-gradient(circle at 20% 30%,rgba(243,165,76,.18),transparent 60%),radial-gradient(circle at 75% 55%,rgba(0,255,136,.12),transparent 55%);
 
 
 
 
32
  }
33
 
34
+ .hero__layout {
35
+ display: grid;
36
+ grid-template-columns: repeat(auto-fit,minmax(320px,1fr));
 
 
 
 
 
 
 
 
 
 
37
  align-items: center;
38
+ gap: clamp(2.2rem,5vw,5rem);
39
  }
40
 
41
+ .hero__content {
42
+ position: relative;
43
+ z-index: 2;
 
 
 
 
44
  }
45
 
46
+ .hero__title {
47
+ font-size: clamp(2.2rem,3.6vw,3.8rem);
48
+ line-height: 1.1;
49
+ margin: 0 0 1.2rem;
50
+ font-weight: 700;
51
+ background: linear-gradient(90deg,#fff,#ffe2c5 45%,#fff);
52
+ -webkit-background-clip: text;
53
+ color: transparent;
54
+ filter: drop-shadow(0 4px 18px rgba(0,0,0,.35));
 
55
  }
56
 
57
+ .hero__text {
58
+ font-size: clamp(.95rem,1.15vw,1.2rem);
59
+ max-width: 46ch;
60
+ line-height: 1.55;
61
+ margin: 0 0 1.6rem;
62
+ color: #d8dee7;
63
+ font-weight: 500;
64
  }
65
 
66
+ .hero__badges {
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ gap: .5rem .75rem;
70
+ margin: 0 0 1.75rem;
71
  }
72
 
73
+ .badge {
74
+ position: relative;
75
+ padding: .4rem .85rem .45rem;
76
+ border-radius: 2rem;
77
+ font-size: .65rem;
78
+ letter-spacing: .8px;
79
+ font-weight: 600;
80
+ text-transform: uppercase;
81
+ background: #f3a54c;
82
+ color: #0b0f1a;
83
+ box-shadow: 0 4px 12px -2px rgba(0,0,0,.4);
84
+ }
85
+
86
+ .hero__sub-cta {
87
+ margin: 1.25rem 0 0;
88
+ font-size: .8rem;
89
+ letter-spacing: .6px;
90
+ color: #b8c4d2;
91
+ }
92
+ /* Features */
93
+ .features {
94
+ padding: 2vw;
95
+ background: linear-gradient(180deg,#0b0f1a,#111c29);
96
+ position: relative;
97
+ }
98
+
99
+ .features::before {
100
+ content: "";
101
+ position: absolute;
102
+ inset: 0;
103
+ background: radial-gradient(circle at 70% 30%,rgba(0,255,136,.12),transparent 60%);
104
+ pointer-events: none;
105
+ opacity: .65;
106
  }
107
 
108
+ .section-title {
109
+ font-size: clamp(1.8rem,2.9vw,2.6rem);
110
+ text-align: center;
111
+ font-weight: 700;
112
+ letter-spacing: .5px;
113
+ background: linear-gradient(90deg,#fff,#e6f9ff);
114
+ -webkit-background-clip: text;
115
+ color: transparent;
116
+ line-height: 4vw;
117
  }
118
 
119
+ .features__grid {
120
  display: flex;
121
+ gap: 2vw;
122
+ width: 100%;
123
  }
124
 
125
+ .feature {
126
+ background: linear-gradient(135deg,rgba(255,255,255,.07),rgba(255,255,255,.02));
127
+ border: 1px solid rgba(255,255,255,.12);
128
+ padding: 1.2rem 1.15rem 1.25rem;
129
+ border-radius: 1.1rem;
130
+ backdrop-filter: blur(14px);
131
+ position: relative;
132
+ overflow: hidden;
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: .55rem;
136
+ min-height: 170px;
137
  }
138
 
139
+ .feature::after {
140
+ content: "";
141
+ position: absolute;
142
+ inset: 0;
143
+ background: radial-gradient(circle at 25% 15%,rgba(243,165,76,.15),transparent 70%);
144
+ opacity: 0;
145
+ transition: opacity .4s ease;
146
+ }
147
+
148
+ .feature:hover::after {
149
+ opacity: 1;
150
+ }
151
+
152
+ .feature h3 {
153
+ margin: .2rem 0 .15rem;
154
+ font-size: .95rem;
155
+ letter-spacing: .6px;
156
+ font-weight: 600;
157
+ color: #fff;
158
+ }
159
+
160
+ .feature p {
161
+ margin: 0;
162
+ font-size: .72rem;
163
+ line-height: 1.35;
164
+ color: #d1d8e3;
165
+ max-width: 36ch;
166
+ }
167
+
168
+ .feature__icon {
169
+ width: 44px;
170
+ height: 44px;
171
+ display: flex;
172
  align-items: center;
173
  justify-content: center;
174
+ font-size: 1.1rem;
175
+ border-radius: 14px;
176
+ background: linear-gradient(135deg,#182330,#1f2f40);
177
+ color: #ffe2c5;
178
+ box-shadow: 0 4px 14px -4px rgba(0,0,0,.65),0 0 0 1px rgba(255,255,255,.06);
179
+ }
180
+
181
+ .gradient-ring {
182
+ position: relative;
183
  }
184
 
185
+ .gradient-ring::before {
186
+ content: "";
187
+ position: absolute;
188
+ inset: -2px;
189
+ border-radius: inherit;
190
+ padding: 1px;
191
+ background: linear-gradient(120deg,#f3a54c,#00ff88);
192
+ -webkit-mask: linear-gradient(#000,#000) content-box,linear-gradient(#000,#000);
193
+ -webkit-mask-composite: xor;
194
+ mask-composite: exclude;
195
  }
196
+ /* Journey */
197
+ .journey {
198
+ padding-top: 4vw;
199
+ background: #f7f9fc;
200
+ }
201
 
202
+ .journey__steps {
203
+ list-style: none;
204
+ margin: 0;
205
+ padding: 0;
206
+ display: grid;
207
+ gap: 1.4rem;
208
+ grid-template-columns: repeat(auto-fit,minmax(180px,1fr));
209
  }
210
 
211
+ .step {
212
+ background: #fff;
213
+ border: 1px solid #eceff3;
214
+ padding: 1.15rem 1rem 1.3rem;
215
+ border-radius: 1rem;
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: .45rem;
219
+ box-shadow: 0 6px 16px -6px rgba(28,40,56,.18);
220
+ transition: transform .35s ease,box-shadow .35s ease;
221
  }
222
 
223
+ .step:hover {
224
+ transform: translateY(-4px);
225
+ box-shadow: 0 10px 28px -10px rgba(28,40,56,.35);
226
+ }
227
+
228
+ .step__num {
229
+ width: 38px;
230
+ height: 38px;
231
+ border-radius: 50%;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ font-weight: 600;
236
+ font-size: .85rem;
237
+ background: linear-gradient(135deg,#f3a54c,#ffcb7a);
238
+ color: #111;
239
+ box-shadow: 0 4px 10px -2px rgba(0,0,0,.25);
240
  }
241
 
242
+ .step h3 {
243
+ margin: 0;
244
+ font-size: .85rem;
245
+ letter-spacing: .5px;
246
+ color: #222;
247
+ font-weight: 600;
248
  }
249
 
250
+ .step p {
251
+ margin: 0;
252
+ font-size: .68rem;
253
+ line-height: 1.35;
254
+ color: #4a5663;
255
+ }
256
+ /* Buttons */
257
+ .btn-secondary {
258
+ background: linear-gradient(135deg,#f3a54c,#ffcb7a);
259
+ color: #111;
260
+ border: none;
261
+ font-weight: 600;
262
  }
263
 
264
+ .btn-secondary.disabled, .btn-secondary[aria-disabled='true'] {
265
+ filter: grayscale(.7) brightness(.6);
266
+ cursor: not-allowed;
267
+ }
268
+ /* Solutions */
269
+ .cards {
270
+ background: #f7f7f7;
271
+ padding: 70px clamp(2.5rem,6vw,5rem);
272
+ display: grid !important;
273
+ grid-template-columns: repeat(auto-fit,minmax(340px,1fr));
274
+ gap: 60px clamp(2rem,3.2vw,3rem);
275
  }
276
 
277
+ .cards > .card {
278
+ background: #fff;
279
+ border: 1px solid #e3e5e8;
280
+ border-radius: 8px;
281
+ padding: 36px 34px 42px;
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 1px;
285
+ box-shadow: 0 6px 22px -8px rgba(0,0,0,.12);
286
+ transition: .35s ease;
287
+ position: relative;
288
+ isolation: isolate;
289
+ }
290
+
291
+ .cards > .card::before {
292
+ content: "";
293
+ position: absolute;
294
+ inset: 0;
295
+ border-radius: inherit;
296
+ background: linear-gradient(135deg,rgba(255,255,255,.2),rgba(255,255,255,0));
297
+ opacity: 0;
298
+ transition: opacity .45s ease;
299
+ pointer-events: none;
300
+ }
301
+
302
+ .cards > .card:hover {
303
+ transform: translateY(-6px);
304
+ box-shadow: 0 14px 38px -12px rgba(0,0,0,.22);
305
+ }
306
+
307
+ .cards > .card:hover::before {
308
+ opacity: 1;
309
+ }
310
+
311
+ .cards > .card img {
312
+ width: 100%;
313
+ height: 200px;
314
+ object-fit: cover;
315
+ border-radius: 4px;
316
+ margin: -8px 0 6px;
317
+ box-shadow: 0 4px 10px -4px rgba(0,0,0,.18);
318
+ }
319
+
320
+ .cards > .card h3 {
321
+ margin: 10px 0 6px;
322
+ font-size: 22px;
323
+ font-weight: 700;
324
+ letter-spacing: .4px;
325
+ color: #1d2433;
326
+ }
327
+
328
+ .cards > .card p {
329
+ margin: 0;
330
+ font-size: 14px;
331
+ line-height: 1.55;
332
+ color: #2a313d;
333
+ letter-spacing: .25px;
334
+ }
335
+
336
+ @media (max-width:880px) {
337
+ .cards {
338
+ gap: 40px 28px;
339
+ padding: 60px clamp(1.5rem,5vw,3rem)
340
+ }
341
+
342
+ .cards > .card {
343
+ padding: 30px 26px 36px
344
+ }
345
  }
346
 
347
+ @media (max-width:560px) {
348
+ .cards {
349
+ grid-template-columns: 1fr;
350
+ padding: 50px 1.25rem 60px
351
+ }
352
+
353
+ .cards > .card {
354
+ padding: 26px 22px 32px
355
+ }
356
+
357
+ .cards > .card img {
358
+ height: 180px
359
+ }
360
+ }
361
+ /* Special + bottom CTA */
362
+ .special-content {
363
+ background: #e5e5ea;
364
+ position: relative;
365
+ padding: 110px clamp(2rem,6vw,5rem) 60px;
366
  display: flex;
367
+ flex-direction: column;
368
+ align-items: center;
369
+ gap: 70px;
370
  }
371
 
372
+ .special-card {
373
+ max-width: 860px;
374
+ width: 100%;
375
+ border-radius: 26px;
376
+ padding: 54px 56px 60px;
377
+ background: #fff;
378
+ box-shadow: 0 18px 42px -14px rgba(0, 0, 0, .12);
379
  display: flex;
380
+ flex-direction: column;
381
+ gap: 2vw;
 
 
382
  }
383
 
384
+ .special-card h2 {
385
+ font-size: 2.05rem;
386
+ text-align: center;
387
+ color: #111;
388
+ font-weight: 700;
389
  }
390
 
391
+ .special-card p {
392
+ font-size: 1rem;
393
+ line-height: 1.55;
394
+ color: #111;
395
+ margin: 0;
396
+ }
397
+ .secondaryCard {
398
+ display: flex;
399
+ flex-direction: column;
400
+ gap: 1vw;
401
  }
402
 
403
+
404
+ .secondaryCard button {
405
+ width: max-content;
406
+ color: #f3a54c;
407
+ background: black;
408
+ padding: 0.8vw;
409
+ border-radius: 1vw;
410
+ font-weight: 900;
411
  }
412
 
413
+
414
+ .cta-bottom {
415
+ text-align: center;
416
+ max-width: 760px;
417
+ width: 100%;
418
+ background: #fff;
419
+ padding: 54px 48px 60px;
420
+ border-radius: 26px;
421
+ box-shadow: 0 18px 42px -14px rgba(0,0,0,.12);
422
+ display: flex;
423
+ flex-direction: column;
424
+ gap: 22px;
425
  }
426
 
427
+ .cta-bottom__title {
428
+ margin: 0;
429
+ font-size: 1.9rem;
430
+ font-weight: 700;
431
+ letter-spacing: .5px;
432
+ color: #111;
433
+ }
434
 
435
+ .cta-bottom__text {
436
+ margin: 0;
437
+ font-size: 1.05rem;
438
+ line-height: 1.55;
439
+ color: #222;
440
+ }
441
+
442
+ .cta-bottom__btn {
443
+ align-self: center;
444
+ margin-top: 6px;
445
+ font-size: .95rem;
446
+ padding: .9rem 2.2rem;
447
+ }
448
+
449
+ @media (max-width:860px) {
450
+ .special-card, .cta-bottom {
451
+ padding: 46px 40px 52px
452
  }
453
 
454
+ .special-card h2 {
455
+ font-size: 1.85rem
456
+ }
457
+
458
+ .cta-bottom__title {
459
+ font-size: 1.75rem
460
  }
461
 
462
+ .cta-bottom__text {
463
+ font-size: 1rem
464
+ }
 
 
 
 
 
 
 
465
  }
466
 
467
+ @media (max-width:560px) {
468
+ .special-content {
469
+ padding: 80px 1.25rem 50px;
470
+ gap: 50px
471
+ }
472
 
473
+ .special-card, .cta-bottom {
474
+ padding: 40px 30px 46px
 
475
  }
476
 
477
+ .special-card h2 {
478
+ font-size: 1.6rem
479
+ }
480
+
481
+ .cta-bottom__title {
482
+ font-size: 1.55rem
483
+ }
484
+
485
+ .cta-bottom__text {
486
+ font-size: .95rem
487
+ }
488
+
489
+ .cta-bottom__btn {
490
+ width: 100%
491
  }
492
  }
493
+ /* Footer */
494
+ footer {
495
+ position: relative;
496
+ background: #05080d;
497
+ padding: 2.2rem 1rem 2.6rem;
498
+ margin: 0;
499
+ }
500
 
501
+ footer p {
502
+ font-size: .65rem;
503
+ letter-spacing: .5px;
504
+ margin: 0 0 .9rem;
505
+ color: #9aa6b5;
506
  }
507
 
508
+ .footer-links a {
509
+ font-size: .6rem;
510
+ letter-spacing: .6px;
511
+ color: #d5dee9;
512
+ opacity: .85;
513
+ transition: opacity .3s ease;
514
+ }
515
+
516
+ .footer-links a:hover {
517
  opacity: 1;
 
518
  }
519
+ /* Footer redesign */
520
+ .site-footer {
521
+ background: #000;
522
+ padding: 38px clamp(1.5rem,4vw,3.5rem) 54px;
523
+ text-align: center;
524
+ display: flex;
525
+ flex-direction: column;
526
+ align-items: center;
527
+ gap: 26px;
528
  }
529
 
530
+ .site-footer__copy {
531
+ margin: 0;
532
+ font-size: .78rem;
533
+ letter-spacing: .5px;
534
+ color: #e5e5e5;
535
+ font-weight: 500;
 
 
 
 
 
536
  }
537
 
538
+ .site-footer__row {
539
+ display: flex;
540
+ flex-direction: column;
541
+ align-items: center;
542
+ gap: 22px;
543
  }
544
 
545
+ .site-footer__links {
 
 
 
 
 
 
 
 
546
  display: flex;
547
  align-items: center;
548
+ gap: 30px;
549
+ font-size: .8rem;
550
+ font-weight: 500;
551
+ flex-wrap: wrap;
552
  justify-content: center;
553
  }
554
 
555
+ .site-footer__links a {
556
+ color: #e5e5e5;
557
+ text-decoration: none;
558
+ position: relative;
559
+ letter-spacing: .4px;
560
+ transition: color .25s ease;
 
 
 
 
 
 
 
561
  }
562
 
563
+ .site-footer__links a:hover {
564
+ color: #f3a54c;
565
+ }
 
566
 
567
+ .site-footer__links a + a::before {
568
+ content: "";
569
+ position: absolute;
570
+ left: -15px;
571
+ top: 50%;
572
+ width: 2px;
573
+ height: 16px;
574
+ background: #333;
575
+ transform: translateY(-50%);
576
+ border-radius: 2px;
577
+ }
578
 
579
+ .site-footer__social {
580
+ display: flex;
581
+ gap: 34px;
582
+ align-items: center;
583
+ justify-content: center;
 
 
 
 
 
 
 
 
 
 
584
  }
585
 
586
+ .site-footer__social a {
587
+ width: 42px;
588
+ height: 42px;
589
+ border-radius: 50%;
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: center;
593
+ background: #111;
594
+ border: 1px solid #32363d;
595
+ color: #e5e5e5;
596
+ font-size: 16px;
597
+ transition: .3s ease;
598
+ box-shadow: 0 4px 10px -4px rgba(0,0,0,.45);
599
  }
600
 
601
+ .site-footer__social a:hover {
602
+ color: #f3a54c;
603
+ border-color: #f3a54c;
604
+ transform: translateY(-3px);
605
+ }
606
+
607
+ @media (max-width:680px) {
608
+ .site-footer {
609
+ padding: 46px 1.25rem 70px;
610
  }
 
611
 
612
+ .site-footer__links {
613
+ gap: 20px;
614
+ font-size: .72rem;
615
+ }
 
 
 
 
 
616
 
617
+ .site-footer__social {
618
+ gap: 20px;
 
 
619
  }
620
 
621
+ .site-footer__social a {
622
+ width: 38px;
623
+ height: 38px;
624
+ font-size: 15px;
625
+ }
626
+ }
627
+ /* Background */
628
+ .page {
629
+ display: block;
630
+ min-height: 100vh;
631
+ background: linear-gradient(rgba(5,8,13,.65),rgba(5,8,13,.65)),url('/assets/11.png') center/cover fixed no-repeat;
632
+ }
633
+ /* Header */
634
+ .header-bar {
635
+ background: #000 !important;
636
+ box-shadow: 0 2px 0 rgba(255,255,255,.04);
637
+ display: flex !important;
638
+ align-items: center !important;
639
+ justify-content: flex-start;
640
+ min-height: 82px;
641
+ padding: 14px clamp(1.4rem,4vw,3rem) !important;
642
+ gap: clamp(1.5rem,3vw,4rem);
643
+ position: sticky;
644
+ top: 0;
645
+ z-index: 2002;
646
  }
647
+ .header-bar .brand {
648
+ display: inline-flex;
649
+ align-items: center;
650
+ gap: 1.5vw;
651
+ }
652
 
653
+ .header-bar .brand__logo-img {
654
+ background: #fff;
655
+ width: 74px;
656
+ min-width: 74px;
657
+ max-width: 74px;
658
+ padding: 6px;
659
+ border-radius: 50%;
660
+ box-shadow: 0 4px 10px rgba(0,0,0,.25);
661
  }
662
 
663
+ .header-bar .brand__name {
664
+ font-family: Roliana;
665
+ font-size: 2.5vw;
666
+ font-weight: 300;
667
+ letter-spacing: 3.2px;
668
+ color: #fff;
669
+ text-shadow: 0 2px 4px rgba(0,0,0,.35);
670
+ display: flex;
671
+ align-items: center;
672
+ gap: 2px;
673
+ }
674
+
675
+ .header-bar .brand__name strong {
676
+ color: #f3a54c;
677
+ font-weight: 300;
678
+ }
679
+
680
+ .header__nav {
681
+ flex: 1 1 auto !important;
682
+ display: flex !important;
683
+ align-items: center;
684
+ justify-content: center;
685
+ gap: clamp(2.2rem,3.4vw,4rem) !important;
686
+ margin: 0 !important;
687
+ white-space: nowrap;
688
  }
689
 
690
+ .nav-link--icon {
691
+ display: inline-flex;
 
 
 
 
 
 
 
 
 
 
692
  align-items: center;
693
+ gap: 8px;
694
+ font-size: 0.9vw;
695
+ letter-spacing: 1px;
696
+ font-weight: 700;
697
+ color: #fff;
698
+ opacity: .92;
699
+ text-decoration: none;
700
+ padding: 6px 10px;
701
+ position: relative;
702
+ transition: .25s ease;
703
+ height: 46px;
704
+ line-height: 1;
705
  }
706
 
707
+ .nav-link--icon i {
708
+ font-size: .85rem;
709
+ opacity: .95;
710
  }
711
 
712
+ .nav-link--icon:hover {
713
+ color: #f3a54c;
714
+ opacity: 1;
715
  }
716
 
717
+ .nav-link--icon::after {
718
+ content: "";
719
+ position: absolute;
720
+ left: 8px;
721
+ right: 8px;
722
+ bottom: 2px;
723
+ height: 2px;
724
+ background: linear-gradient(90deg,#f3a54c,#ffcb7a);
725
+ transform: scaleX(0);
726
+ transform-origin: left;
727
+ transition: transform .35s ease;
728
+ border-radius: 2px;
729
+ }
730
 
731
+ .nav-link--icon:hover::after {
732
+ transform: scaleX(1);
733
  }
734
 
735
+ .header__actions {
736
+ flex: 0 0 auto;
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: flex-end;
740
+ gap: 1rem;
741
+ margin: 0 !important;
742
  }
743
 
744
+ .btn-signup, .btn-signin {
745
+ border-radius: 10px;
746
+ font-size: .8rem;
747
+ padding: .65rem 1.25rem;
748
+ font-weight: 600;
749
+ letter-spacing: .5px;
750
+ line-height: 1;
751
  }
752
 
753
+ .btn-signup {
754
+ background: #f3a54c;
755
+ color: #0b0f1a;
756
+ border: none;
757
+ box-shadow: 0 4px 10px -2px rgba(243,165,76,.4);
758
  }
759
 
760
+ .btn-signup:hover {
761
+ filter: brightness(1.08);
 
 
 
762
  }
763
 
764
+ .btn-signin {
765
+ background: #0b0f1a;
766
+ color: #fff;
767
+ border: 2px solid #f3a54c;
768
+ }
769
 
770
+ .btn-signin:hover {
771
+ background: #121b27;
772
  }
773
+ /* Tagline line break styling */
774
+ .tagline-line2 {
775
+ display: inline-block;
776
+ margin-top: .35rem;
777
+ font-weight: 700;
778
+ background: linear-gradient(90deg,#ffe2c5,#ffffff);
779
+ -webkit-background-clip: text;
780
+ color: transparent;
781
  }
782
 
783
+ .hero__title br {
784
+ content: "";
785
+ }
786
+ /* Auth Modal */
787
+ .modal-backdrop {
788
+ position: fixed;
789
+ inset: 0;
790
+ background: rgba(0,0,0,.55);
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  backdrop-filter: blur(6px);
792
  -webkit-backdrop-filter: blur(6px);
793
+ display: flex;
794
+ align-items: center;
795
+ justify-content: center;
796
+ padding: 24px clamp(12px,4vw,40px);
797
+ z-index: 2000;
798
+ animation: modalFade .25s ease;
 
 
 
 
799
  }
800
 
801
+ @keyframes modalFade {
802
+ from {
803
+ opacity: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  }
805
 
806
+ to {
807
+ opacity: 1
 
808
  }
809
  }
810
 
811
+ .modal-panel {
812
+ position: relative;
813
+ width: 56%;
814
+ border-radius: 22px;
815
+ background: #0f1522;
816
+ color: #e5e7eb;
817
+ padding: 52px 44px 48px;
818
+ animation: panelIn .35s cubic-bezier(.16,1,.3,1);
819
  }
820
 
821
+ .modal-panel--white {
822
+ background: transparent;
823
+ color: #111;
824
+ }
825
 
826
+ @keyframes panelIn {
827
+ from {
828
+ transform: translateY(22px) scale(.95);
829
+ opacity: 0
830
  }
831
 
832
+ to {
833
+ transform: translateY(0) scale(1);
834
+ opacity: 1
835
  }
836
  }
837
 
838
+ button.modal-close {
839
+ position: absolute;
840
+ top: 14px;
841
+ right: 14px;
842
+ width: 38px;
843
+ height: 38px;
844
+ border-radius: 50%;
845
+ background: #0b0f1a;
846
+ color: #fff;
847
+ border: 2px solid #f3a54c;
848
+ font-size: 20px;
849
+ font-weight: 600;
850
+ cursor: pointer;
851
  display: flex;
852
+ align-items: center;
853
+ justify-content: center;
854
+ transition: .25s ease;
 
855
  }
856
 
857
+ .modal-panel--white button.modal-close {
 
858
  background: #fff;
859
+ color: #111;
860
+ border: 2px solid #f3a54c;
 
 
 
861
  }
862
 
863
+ button.modal-close:hover {
864
+ border-color: #00ff88;
865
+ transform: scale(.92);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866
  }
867
 
868
+ button.modal-close:focus-visible {
869
+ outline: 3px solid #00ff88;
870
+ outline-offset: 2px;
 
 
 
871
  }
872
 
873
+ .modal-panel app-sign-in, .modal-panel app-sign-up {
874
+ display: block;
 
 
 
 
 
 
 
 
 
 
875
  }
876
 
877
+ .page, .hero, .features, .journey, .cards, .special-content {
878
+ position: relative;
879
+ z-index: 1;
 
 
 
 
 
 
880
  }
881
 
882
+ @media (max-width:1100px) {
883
+ .header__nav {
884
+ gap: 1.6rem;
885
+ }
 
886
  }
887
 
888
+ @media (max-width:880px) {
889
+ .header-bar {
890
+ flex-wrap: wrap;
891
+ min-height: unset;
892
  }
893
 
894
+ .header__nav {
895
+ order: 3;
896
+ flex: 1 1 100%;
897
+ justify-content: center;
898
+ flex-wrap: wrap;
899
+ gap: .85rem 1.4rem;
900
+ padding-top: .35rem;
901
  }
902
 
903
+ .header__actions {
904
+ order: 2;
905
+ }
 
 
 
906
  }
907
 
908
+ @media (max-width:900px) {
909
+ .header-bar .brand__name {
910
+ font-size: 8vw;
 
911
  }
912
+ }
913
 
914
+ @media (max-width:640px) {
915
+ .nav-link--icon {
916
+ font-size: .64rem;
917
  }
918
+ }
919
 
 
 
 
 
 
 
 
 
 
 
 
920
 
 
 
 
921
 
922
+ a.brand.brand__link {
923
+ text-decoration: unset;
924
+ }
 
925
 
 
 
 
926
 
927
+ .cta {
928
+ display: flex;
929
+ gap: 1vw;
930
  }
931
 
932
+ .about-backdrop {
933
+ position: fixed;
934
+ inset: 0;
935
+ background: rgba(0, 0, 0, 0.7);
936
+ z-index: 1000;
 
937
  }
938
 
939
+ .about-modal {
940
+ position: fixed;
941
+ top: 50%;
942
+ left: 50%;
943
+ transform: translate(-50%, -50%);
944
+ background: #ffffff;
945
+ border-radius: 12px;
946
+ padding: 2.5rem;
947
+ width: 45%;
948
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
949
+ z-index: 1001;
950
+ display: flex;
951
+ flex-direction: column;
952
+ gap: 1.5rem;
953
+ animation: _ngcontent-ng-c221623785_modalFadeIn 0.4s ease-out;
954
+ text-align: justify;
955
+ margin: 0 auto;
956
  }
957
 
958
+ .about-modal__header {
959
+ display: flex;
960
+ justify-content: space-between;
961
+ align-items: center;
962
  }
963
 
964
+ .about-modal__title {
965
+ font-size: 1.5rem;
966
+ font-weight: 700;
967
+ margin: 0;
968
+ color: #f3a54c;
969
  }
970
 
971
+ .about-modal__close {
972
+ background: transparent;
973
+ border: 2px solid black;
974
+ color: #f3a54c;
975
+ font-size: 1.2rem;
976
+ cursor: pointer;
977
+ transition: color 0.3s ease;
978
+ border-radius: 0.3vw;
979
+ }
 
 
 
 
 
 
980
 
981
+ .about-modal__close:hover {
982
+ background: #f3a54c;
983
+ color: black;
984
  }
985
 
986
+ .about-modal__content {
987
+ color: #e5e7eb;
988
+ line-height: 1.6;
 
989
  }
990
 
991
+ .about-modal__footer {
 
992
  display: flex;
993
+ justify-content: flex-end;
994
+ align-items: center;
995
+ gap: 1rem;
996
  }
997
 
998
+ .about-modal__btn {
999
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
1000
+ color: #111;
1001
+ border: none;
1002
+ padding: 0.8rem 1.5rem;
1003
+ border-radius: 8px;
1004
+ font-weight: 600;
1005
+ cursor: pointer;
1006
+ transition: transform 0.3s ease;
 
 
 
 
1007
  }
1008
 
1009
+ .about-modal__btn:hover {
1010
+ transform: translateY(-2px);
 
1011
  }
1012
 
1013
+ @keyframes modalFadeIn {
1014
+ from {
1015
+ opacity: 0;
1016
+ transform: translate(-50%, -40%);
1017
  }
1018
 
1019
+ to {
1020
+ opacity: 1;
1021
+ transform: translate(-50%, -50%);
1022
  }
1023
+ }
1024
 
1025
+ @keyframes modalFadeOut {
1026
+ from {
1027
+ opacity: 1;
1028
+ transform: translate(-50%, -50%);
1029
  }
1030
 
1031
+ to {
1032
+ opacity: 0;
1033
+ transform: translate(-50%, -60%);
1034
  }
1035
+ }
1036
 
1037
+ /* Marriage Sub Navigation */
1038
+ .nav-item-wrapper {
1039
+ position: relative;
1040
+ display: inline-block;
1041
+ }
1042
 
1043
+ .sub-nav {
1044
+ position: absolute;
1045
+ top: 100%;
1046
+ left: 0;
1047
+ background: #0b0f1a;
1048
+ border: 1px solid rgba(255,255,255,.12);
1049
+ border-radius: 8px;
1050
+ padding: 8px 0;
1051
+ min-width: 200px;
1052
+ box-shadow: 0 8px 24px rgba(0,0,0,.4);
1053
+ backdrop-filter: blur(14px);
1054
+ z-index: 1000;
1055
+ animation: subnavFadeIn 0.2s ease-out;
1056
  }
1057
 
1058
+ .sub-nav-link {
1059
+ display: flex;
1060
+ align-items: center;
1061
+ gap: 10px;
1062
+ padding: 10px 16px;
1063
+ color: #e5e7eb;
1064
+ text-decoration: none;
1065
+ font-size: 0.75vw;
1066
+ font-weight: 600;
1067
+ letter-spacing: 0.5px;
1068
+ transition: all 0.25s ease;
1069
+ border: none;
1070
+ background: none;
1071
+ width: 100%;
1072
  text-align: left;
1073
+ cursor: pointer;
1074
  }
1075
 
1076
+ .sub-nav-link:hover {
1077
+ background: rgba(243, 165, 76, 0.1);
1078
+ color: #f3a54c;
1079
+ }
1080
+
1081
+ .sub-nav-link i {
1082
+ font-size: 0.85rem;
1083
+ width: 16px;
1084
  text-align: center;
1085
  }
1086
 
1087
+ @keyframes subnavFadeIn {
1088
+ from {
1089
+ opacity: 0;
1090
+ transform: translateY(-8px);
1091
  }
1092
 
1093
+ to {
1094
+ opacity: 1;
1095
+ transform: translateY(0);
1096
+ }
1097
  }
1098
 
1099
+ /* Ensure the main nav items have proper z-index */
1100
+ .header__nav {
1101
  position: relative;
1102
+ z-index: 1001;
 
 
 
 
 
 
 
1103
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/intro-page/intro-page.component.html CHANGED
@@ -1,67 +1,189 @@
1
  <a class="skip-link" href="#main">Skip to content</a>
2
 
3
  <section class="page">
4
- <!-- Top bar (now fixed) -->
5
- <header class="topbar" role="banner" aria-label="Top navigation">
6
- <a class="brand brand__link"
7
- href="https://pykara.net/"
8
- target="_blank"
9
- rel="noopener noreferrer"
10
- aria-label="Visit Pykara website">
11
- <img src="assets/pykara-logo.png" alt="Pykara Technologies logo" class="brand__logo-img" />
12
- <span class="brand__name"><strong>Py</strong>-Match</span>
13
  </a>
 
14
 
15
- <!-- Primary navigation for assessment types -->
16
- <nav class="primary-nav" aria-label="Assessment navigation">
17
- <a routerLink="/question-answer" [queryParams]="{ role: 'marriage' }" class="nav-link">
18
- <i class="fa-solid fa-ring nav-link__icon" aria-hidden="true"></i>
19
- <span>Marriage</span>
20
- </a>
21
- <a routerLink="/question-answer" [queryParams]="{ role: 'interview' }" class="nav-link">
22
- <i class="fa-solid fa-user-tie nav-link__icon" aria-hidden="true"></i>
23
- <span>Interview</span>
24
- </a>
25
- <a routerLink="/question-answer" [queryParams]="{ role: 'partnership' }" class="nav-link">
26
- <i class="fa-solid fa-handshake nav-link__icon" aria-hidden="true"></i>
27
- <span>Partnership</span>
28
- </a>
29
- </nav>
30
-
31
- <nav class="actions" aria-label="Actions">
32
- <ng-container *ngIf="!isSignedIn; else signedInBlock">
33
- <button type="button" class="btn btn-primary" (click)="openSignUp()">{{ tr('signup') }}</button>
34
- <button type="button" class="btn btn-ghost" (click)="openSignIn()">{{ tr('signin') }}</button>
35
- </ng-container>
36
- <ng-template #signedInBlock>
37
- <button type="button" class="profile-btn" aria-label="Profile">
38
- <i class="fa-solid fa-user" aria-hidden="true"></i>
39
- </button>
40
- <button type="button" class="btn btn-ghost" (click)="signOut()">Sign out</button>
41
- </ng-template>
42
- </nav>
43
- </header>
44
-
45
- <!-- Hero (unchanged) -->
46
  <main id="main" class="hero" role="main">
47
- <div class="hero__content">
48
- <h1 class="hero__title">{{ tr('tagline') }}</h1>
49
- <p class="hero__text">{{ tr('subtext') }}</p>
50
- <div class="cta">
51
- <button type="button"
52
- class="btn btn-ghost"
53
- (click)="openAbout()"
54
- aria-haspopup="dialog"
55
- aria-controls="aboutDialog"
56
- [attr.aria-expanded]="isAboutOpen ? 'true' : 'false'">
57
- Know more
58
- </button>
 
 
 
 
 
 
 
 
 
59
  </div>
60
  </div>
61
  </main>
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- <!-- AUTH MODAL OVERLAY (unchanged) -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  <div class="modal-backdrop" *ngIf="modal" (click)="backdropClick($event)">
66
  <div class="modal-panel modal-panel--white"
67
  role="dialog"
@@ -74,13 +196,12 @@
74
  (signInSuccess)="handleSignInSuccess()"></app-sign-in>
75
  <app-sign-up *ngIf="modal === 'signup'"
76
  (switchToSignIn)="openSignIn()"
77
- (signUpSuccess)="handleSignUpSuccess()"></app-sign-up> <!-- Add this line -->
78
  </div>
79
  </div>
80
 
81
- <!-- ABOUT MODAL OVERLAY (new) -->
82
  <div class="about-backdrop" *ngIf="isAboutOpen" (click)="closeAbout()" tabindex="-1"></div>
83
-
84
  <div class="about-modal"
85
  *ngIf="isAboutOpen"
86
  role="dialog"
@@ -89,147 +210,83 @@
89
  id="aboutDialog"
90
  tabindex="-1">
91
  <div class="about-modal__header">
92
- <h2 id="aboutTitle">About Py-Match</h2>
93
- <button class="about-close" (click)="closeAbout()" aria-label="Close dialog">×</button>
94
  </div>
95
-
96
  <div class="about-modal__body">
97
- <h1>Your Ideal Match Awaits</h1>
98
  <p><strong>Py-Match</strong> is an advanced personality assessment tool designed to help individuals find the ideal match based on their core personality traits. The system uses a series of adaptive questions that are tailored to each user's profile. Each response is mapped to one of four core personality profiles, which represent different dominant behavioral tendencies. As users progress through the assessment, the platform continuously adapts, ensuring that the questions remain relevant and engaging, ultimately providing a more accurate personality profile.</p>
99
-
100
  <p>The primary goal of <strong>Py-Match</strong> is to offer data-driven, personalized matches for various scenarios such as marriage compatibility, business partnerships, and leadership fit. By comparing the individual's personality profile to role-specific templates, the platform ensures that the match is based on both complementary and compatible traits. This sophisticated matching engine provides an in-depth, personalized experience while maintaining strict privacy standards. With a focus on both accuracy and user confidentiality, <strong>Py-Match</strong> guarantees that sensitive information is securely stored, offering users peace of mind while they explore the platform's potential.</p>
101
  </div>
 
 
 
102
  </div>
103
 
104
- </section>
105
-
106
- <!-- Cards Section -->
107
- <section class="cards">
108
- <div class="card">
109
- <img src="assets/ring.png" alt="Marriage Match">
110
- <h3><b>Marriage Match</b></h3>
111
- <p>
112
- Find the right life partner, beyond surface-level details.<br>
113
- This analyzes values, habits, and personality traits such as analytical, organized, decisive, and creative.<br>
114
- Understand true compatibility.<br>
115
- See strengths and differences clearly.<br>
116
- Make informed decisions for a lasting relationship.
117
- </p>
118
- </div>
119
- <div class="card">
120
- <img src="assets/Hiring.png" alt="Interview & Hiring Match">
121
- <h3><b>Interview & Hiring Match</b></h3>
122
- <p>
123
- Recruit with confidence, powered by science.<br>
124
- Spot hidden strengths and leadership potential.<br>
125
- Match candidates to roles where they'll thrive.<br>
126
- Build balanced, high-performing teams.
127
- </p>
128
- </div>
129
- <div class="card">
130
- <img src="assets/business.png" alt="Business Partner Match">
131
- <h3><b>Business Partner Match</b></h3>
132
- <p>
133
- Choose partners who complement your strengths.<br>
134
- Identify complementary skills and leadership styles.<br>
135
- Reduce risks of conflict in decision-making.<br>
136
- Build stronger, trust-based collaborations.
137
- </p>
138
  </div>
139
- </section>
140
 
141
- <!-- Scrolling Images -->
142
- <div class="scroll-container">
143
- <div class="scroll-images">
144
- <!-- Sequence A -->
145
- <img src="assets/small-img/1.png" alt="smallImage">
146
- <img src="assets/small-img/2.png" alt="smallImage">
147
- <img src="assets/small-img/3.png" alt="smallImage">
148
- <img src="assets/small-img/4.png" alt="smallImage">
149
- <img src="assets/small-img/5.png" alt="smallImage">
150
- <img src="assets/small-img/6.png" alt="smallImage">
151
- <img src="assets/small-img/7.png" alt="smallImage">
152
- <img src="assets/small-img/8.png" alt="smallImage">
153
- <img src="assets/small-img/9.png" alt="smallImage">
154
- <img src="assets/small-img/10.png" alt="smallImage">
155
- <img src="assets/small-img/11.png" alt="smallImage">
156
- <img src="assets/small-img/12.png" alt="smallImage">
157
- <img src="assets/small-img/13.png" alt="smallImage">
158
- <img src="assets/small-img/14.png" alt="smallImage">
159
- <img src="assets/small-img/15.png" alt="smallImage">
160
- <img src="assets/small-img/16.png" alt="smallImage">
161
- <img src="assets/small-img/17.png" alt="smallImage">
162
- <img src="assets/small-img/18.png" alt="smallImage">
163
- <img src="assets/small-img/19.png" alt="smallImage">
164
- <img src="assets/small-img/20.png" alt="smallImage">
165
- <img src="assets/small-img/21.png" alt="smallImage">
166
- <img src="assets/small-img/22.png" alt="smallImage">
167
- <img src="assets/small-img/24.png" alt="smallImage">
168
- <img src="assets/small-img/26.png" alt="smallImage">
169
- <img src="assets/small-img/28.png" alt="smallImage">
170
- <!-- Sequence B (duplicate for seamless loop) -->
171
- <img src="assets/small-img/1.png" alt="smallImage">
172
- <img src="assets/small-img/2.png" alt="smallImage">
173
- <img src="assets/small-img/3.png" alt="smallImage">
174
- <img src="assets/small-img/4.png" alt="smallImage">
175
- <img src="assets/small-img/5.png" alt="smallImage">
176
- <img src="assets/small-img/6.png" alt="smallImage">
177
- <img src="assets/small-img/7.png" alt="smallImage">
178
- <img src="assets/small-img/8.png" alt="smallImage">
179
- <img src="assets/small-img/9.png" alt="smallImage">
180
- <img src="assets/small-img/10.png" alt="smallImage">
181
- <img src="assets/small-img/11.png" alt="smallImage">
182
- <img src="assets/small-img/12.png" alt="smallImage">
183
- <img src="assets/small-img/13.png" alt="smallImage">
184
- <img src="assets/small-img/14.png" alt="smallImage">
185
- <img src="assets/small-img/15.png" alt="smallImage">
186
- <img src="assets/small-img/16.png" alt="smallImage">
187
- <img src="assets/small-img/17.png" alt="smallImage">
188
- <img src="assets/small-img/18.png" alt="smallImage">
189
- <img src="assets/small-img/19.png" alt="smallImage">
190
- <img src="assets/small-img/20.png" alt="smallImage">
191
- <img src="assets/small-img/21.png" alt="smallImage">
192
- <img src="assets/small-img/22.png" alt="smallImage">
193
- <img src="assets/small-img/24.png" alt="smallImage">
194
- <img src="assets/small-img/26.png" alt="smallImage">
195
- <img src="assets/small-img/28.png" alt="smallImage">
196
  </div>
197
- </div>
198
 
199
- <!-- Special Content as Card -->
200
- <section class="special-content">
201
- <div class="card special-card">
202
- <h3><b>Why Py-Match is Special?</b></h3>
203
- <p>
204
- <strong>Py-Match</strong> isn't just about data — it's about people.<br>
205
- Using cutting-edge technology and psychology-based personality models, <strong>Py-Match</strong> decodes human compatibility.<br>
206
- Whether in love, business, or recruitment, <strong>Py-Match</strong> gives you clarity and confidence.<br>
207
- It's science made simple, to help you make life's most important decisions wisely.
208
- </p>
209
  </div>
210
  </section>
211
-
212
- <!-- Footer (now fixed) -->
213
- <footer>
214
- <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
215
- <div class="footer-links">
216
- <a href="#">About Us</a> |
217
- <a href="#">Contact</a> |
218
- <a href="#">Privacy Policy</a> |
219
- <a href="#">Terms & Conditions</a>
220
- </div>
221
- <div class="social-icons">
222
- <a href="#" class="social-icon facebook" aria-label="Facebook">
223
- <i class="fab fa-facebook-f"></i>
224
- </a>
225
- <a href="#" class="social-icon youtube" aria-label="YouTube">
226
- <i class="fab fa-youtube"></i>
227
- </a>
228
- <a href="#" class="social-icon linkedin" aria-label="LinkedIn">
229
- <i class="fab fa-linkedin-in"></i>
230
- </a>
231
- <a href="#" class="social-icon instagram" aria-label="Instagram">
232
- <i class="fab fa-instagram"></i>
233
- </a>
234
- </div>
235
- </footer>
 
1
  <a class="skip-link" href="#main">Skip to content</a>
2
 
3
  <section class="page">
4
+ <!-- Logo link added below -->
5
+ <div class="intro-logo">
6
+ <a href="https://pykara.net/" target="_blank" rel="noopener" aria-label="Pykara Technologies Home">
7
+ <img src="assets/logo.png" alt="Pykara Technologies Logo" style="height: 60px;" />
 
 
 
 
 
8
  </a>
9
+ </div>
10
 
11
+ <!-- Hero -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  <main id="main" class="hero" role="main">
13
+ <div class="hero__bg" aria-hidden="true"></div>
14
+ <div class="container hero__layout">
15
+ <div class="hero__content">
16
+ <h1 class="hero__title" [innerHTML]="tr('tagline')"></h1>
17
+ <p class="hero__text">{{ tr('subtext') }}</p>
18
+ <div class="hero__badges">
19
+ <span class="badge">AI Adaptive</span>
20
+ <span class="badge">Psychometrics</span>
21
+ <span class="badge">Privacy First</span>
22
+ </div>
23
+ <div class="cta">
24
+ <button type="button"
25
+ class="btn btn-ghost"
26
+ (click)="openAbout()"
27
+ aria-haspopup="dialog"
28
+ aria-controls="aboutDialog"
29
+ [attr.aria-expanded]="isAboutOpen ? 'true' : 'false'">
30
+ Know more
31
+ </button>
32
+ </div>
33
+ <p class="hero__sub-cta" *ngIf="!isSignedIn">Create a free profile to begin your first assessment.</p>
34
  </div>
35
  </div>
36
  </main>
37
 
38
+ <!-- Feature Differentiators -->
39
+ <section id="features" class="features" aria-labelledby="featuresTitle">
40
+ <div class="container">
41
+ <h2 id="featuresTitle" class="section-title">What Makes Py-Match Different</h2>
42
+ <div class="features__grid">
43
+ <div class="feature">
44
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-layer-group"></i></div>
45
+ <h3>Adaptive Questioning</h3>
46
+ <p>Dynamic assessments that evolve with every answer to sharpen accuracy.</p>
47
+ </div>
48
+ <div class="feature">
49
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-dna"></i></div>
50
+ <h3>Behavioral Modeling</h3>
51
+ <p>Maps responses to validated personality archetypes for deep insight.</p>
52
+ </div>
53
+ <div class="feature">
54
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-chart-line"></i></div>
55
+ <h3>Role Intelligence</h3>
56
+ <p>Aligns your profile with scenario-specific success templates.</p>
57
+ </div>
58
+ <div class="feature">
59
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-shield-halved"></i></div>
60
+ <h3>Privacy by Design</h3>
61
+ <p>Secure data handling and minimal exposure at every interaction.</p>
62
+ </div>
63
+ <div class="feature">
64
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-people-group"></i></div>
65
+ <h3>Multi-Context Matching</h3>
66
+ <p>Marriage, hiring, leadership and partnerships in one unified engine.</p>
67
+ </div>
68
+ <div class="feature">
69
+ <div class="feature__icon gradient-ring"><i class="fa-solid fa-bolt"></i></div>
70
+ <h3>Actionable Insights</h3>
71
+ <p>Clear strengths, gaps and complementarity—not vague labels.</p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </section>
76
+
77
+ <!-- Journey / How it works -->
78
+ <section id="journey" class="journey" aria-labelledby="journeyTitle">
79
+ <div class="container">
80
+ <h2 id="journeyTitle" class="section-title">Your Matching Journey</h2>
81
+ <ol class="journey__steps">
82
+ <li class="step">
83
+ <div class="step__num">1</div>
84
+ <h3>Create Profile</h3>
85
+ <p>Sign up and select your objective: marriage, hiring, or partnership.</p>
86
+ </li>
87
+ <li class="step">
88
+ <div class="step__num">2</div>
89
+ <h3>Adaptive Assessment</h3>
90
+ <p>Answer evolving questions tailored to sharpen trait precision.</p>
91
+ </li>
92
+ <li class="step">
93
+ <div class="step__num">3</div>
94
+ <h3>Profile Modeling</h3>
95
+ <p>Your responses map into a multi-dimensional personality structure.</p>
96
+ </li>
97
+ <li class="step">
98
+ <div class="step__num">4</div>
99
+ <h3>Intelligent Matching</h3>
100
+ <p>Compare against role or compatibility templates for data-driven fit.</p>
101
+ </li>
102
+ <li class="step">
103
+ <div class="step__num">5</div>
104
+ <h3>Insight & Action</h3>
105
+ <p>Receive transparent recommendations and next-step guidance.</p>
106
+ </li>
107
+ </ol>
108
+ </div>
109
+ </section>
110
+
111
+ <!-- Solutions (Cards) -->
112
+ <section class="cards" aria-labelledby="solutionsTitle">
113
+ <h2 id="solutionsTitle" class="visually-hidden">Solution Scenarios</h2>
114
+ <div class="card">
115
+ <img src="assets/ring.png" alt="Marriage Match">
116
+ <h3>Marriage Match</h3>
117
+ <p>Find the right life partner, beyond surface-level details.</p>
118
+ <p>This analyzes values, habits, and personality traits such as analytical, organized, decisive, and creative.</p>
119
+ <p>Understand true compatibility.</p>
120
+ <p>See strengths and differences clearly.</p>
121
+ <p>Make informed decisions for a lasting relationship.</p>
122
+ </div>
123
+ <div class="card">
124
+ <img src="assets/Hiring.png" alt="Interview & Hiring Match">
125
+ <h3>Interview & Hiring Match</h3>
126
+ <p>Recruit with confidence, powered by science.</p>
127
+ <p>Spot hidden strengths and leadership potential.</p>
128
+ <p>Match candidates to roles where they'll thrive.</p>
129
+ <p>Build balanced, high-performing teams.</p>
130
+ </div>
131
+ <div class="card">
132
+ <img src="assets/business.png" alt="Business Partner Match">
133
+ <h3>Business Partner Match</h3>
134
+ <p>Choose partners who complement your strengths.</p>
135
+ <p>Identify complementary skills and leadership styles.</p>
136
+ <p>Reduce risks of conflict in decision-making.</p>
137
+ <p>Build stronger, trust-based collaborations.</p>
138
+ </div>
139
+ </section>
140
+
141
+ <!-- Special Content + Bottom CTA -->
142
+ <section class="special-content" aria-labelledby="specialTitle">
143
+ <div class="card special-card">
144
+ <h2 id="specialTitle">Why Py-Match is Special?</h2>
145
+ <p>
146
+ <strong>Py-Match</strong> isn't just about data — it's about people.
147
+ Using cutting-edge technology and psychology-based personality models, <strong>Py-Match</strong> decodes human compatibility.
148
+ Whether in love, business, or recruitment, <strong>Py-Match</strong> gives you clarity and confidence.
149
+ It's science made simple, to help you make life's most important decisions wisely.
150
+ </p>
151
+ <div class="secondaryCard">
152
+ <h3 class="cta-bottom__title">Have a question or need guidance?</h3>
153
+ <p class="cta-bottom__text">Contact us — we’re happy to assist you on your journey.</p>
154
+ <button>Contact Us</button>
155
+ </div>
156
+ </div>
157
 
158
+ <!--<div class="cta-bottom" *ngIf="!isSignedIn">
159
+ </div>-->
160
+ <div class="cta-bottom" *ngIf="isSignedIn">
161
+ <h3 class="cta-bottom__title">Ready to continue matching?</h3>
162
+ <p class="cta-bottom__text">Jump back in and explore intelligent compatibility insights.</p>
163
+ <a class="btn btn-secondary cta-bottom__btn" [routerLink]="['/matchinglist', userId]" [class.disabled]="!canMatch" [attr.aria-disabled]="!canMatch ? true : null">Continue Matching</a>
164
+ </div>
165
+ </section>
166
+
167
+ <!-- Footer -->
168
+ <footer class="site-footer" role="contentinfo">
169
+ <p class="site-footer__copy">© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
170
+ <div class="site-footer__row">
171
+ <nav class="site-footer__links" aria-label="Footer navigation">
172
+ <a href="#" (click)="$event.preventDefault(); openPrivacyModal();">Privacy Policy</a>
173
+ <a href="https://pykara.net/" target="_blank" rel="noopener">About Us</a>
174
+ <a href="https://pykara.net/contact-us/" target="_blank" rel="noopener">Contact</a>
175
+ <a href="#" (click)="$event.preventDefault(); openTermsModal();">Terms & Conditions</a>
176
+ </nav>
177
+ <div class="site-footer__social" aria-label="Social media">
178
+ <a href="https://www.facebook.com/profile.php?id=100087653675803" target="_blank" rel="noopener" aria-label="Facebook"><i class="fa-brands fa-facebook-f"></i></a>
179
+ <a href="https://www.youtube.com/@PykaraTechnologies" target="_blank" rel="noopener" aria-label="YouTube"><i class="fa-brands fa-youtube"></i></a>
180
+ <a href="https://www.linkedin.com/in/pykara-technologies" target="_blank" rel="noopener" aria-label="LinkedIn"><i class="fa-brands fa-linkedin-in"></i></a>
181
+ <a href="https://www.instagram.com/pykaratechnologie/" target="_blank" rel="noopener" aria-label="Instagram"><i class="fa-brands fa-instagram"></i></a>
182
+ </div>
183
+ </div>
184
+ </footer>
185
+
186
+ <!-- AUTH MODAL -->
187
  <div class="modal-backdrop" *ngIf="modal" (click)="backdropClick($event)">
188
  <div class="modal-panel modal-panel--white"
189
  role="dialog"
 
196
  (signInSuccess)="handleSignInSuccess()"></app-sign-in>
197
  <app-sign-up *ngIf="modal === 'signup'"
198
  (switchToSignIn)="openSignIn()"
199
+ (signUpSuccess)="handleSignUpSuccess()"></app-sign-up>
200
  </div>
201
  </div>
202
 
203
+ <!-- ABOUT MODAL (restyled to match Privacy Policy modal) -->
204
  <div class="about-backdrop" *ngIf="isAboutOpen" (click)="closeAbout()" tabindex="-1"></div>
 
205
  <div class="about-modal"
206
  *ngIf="isAboutOpen"
207
  role="dialog"
 
210
  id="aboutDialog"
211
  tabindex="-1">
212
  <div class="about-modal__header">
213
+ <h2 id="aboutTitle" class="about-modal__title">About Py-Match</h2>
214
+ <button class="about-modal__close" (click)="closeAbout()" aria-label="Close dialog">×</button>
215
  </div>
 
216
  <div class="about-modal__body">
217
+ <p><strong>Your Ideal Match Awaits</strong></p>
218
  <p><strong>Py-Match</strong> is an advanced personality assessment tool designed to help individuals find the ideal match based on their core personality traits. The system uses a series of adaptive questions that are tailored to each user's profile. Each response is mapped to one of four core personality profiles, which represent different dominant behavioral tendencies. As users progress through the assessment, the platform continuously adapts, ensuring that the questions remain relevant and engaging, ultimately providing a more accurate personality profile.</p>
 
219
  <p>The primary goal of <strong>Py-Match</strong> is to offer data-driven, personalized matches for various scenarios such as marriage compatibility, business partnerships, and leadership fit. By comparing the individual's personality profile to role-specific templates, the platform ensures that the match is based on both complementary and compatible traits. This sophisticated matching engine provides an in-depth, personalized experience while maintaining strict privacy standards. With a focus on both accuracy and user confidentiality, <strong>Py-Match</strong> guarantees that sensitive information is securely stored, offering users peace of mind while they explore the platform's potential.</p>
220
  </div>
221
+ <div class="about-modal__footer">
222
+ <button class="about-modal__btn" (click)="closeAbout()">Close</button>
223
+ </div>
224
  </div>
225
 
226
+ <!-- Privacy Policy Modal (styled like About modal) -->
227
+ <div class="about-backdrop" *ngIf="showPrivacyModal" (click)="closePrivacyModal()" tabindex="-1"></div>
228
+ <div class="about-modal"
229
+ *ngIf="showPrivacyModal"
230
+ role="dialog"
231
+ aria-modal="true"
232
+ aria-labelledby="privacyTitle"
233
+ id="privacyDialog"
234
+ tabindex="-1">
235
+ <div class="about-modal__header">
236
+ <h2 id="privacyTitle" class="about-modal__title">Privacy Policy</h2>
237
+ <button class="about-modal__close" (click)="closePrivacyModal()" aria-label="Close dialog">×</button>
238
+ </div>
239
+ <div class="about-modal__body">
240
+ <p><strong>Py-Match Privacy Policy</strong></p>
241
+ <p>Your privacy is important to us. Py-Match securely stores your data and only uses it for personality assessment and matching purposes. We do not sell or share your personal information with third parties. All responses are encrypted and handled according to strict privacy standards. You can request deletion of your data at any time by contacting support.</p>
242
+ <ul>
243
+ <li>We collect only the information necessary for matching and assessment.</li>
244
+ <li>Your data is never sold or shared outside Py-Match.</li>
245
+ <li>All communications are encrypted and confidential.</li>
246
+ <li>You may request data deletion at any time.</li>
247
+ </ul>
248
+ <p>By using Py-Match, you agree to our privacy practices. For questions, contact support@pykara.net.</p>
249
+ </div>
250
+ <div class="about-modal__footer">
251
+ <button class="about-modal__btn" (click)="acceptPrivacyPolicy()">Accept Privacy Policy</button>
252
+ </div>
 
 
 
 
 
 
 
253
  </div>
 
254
 
255
+ <!-- Terms & Conditions Modal (styled like About modal) -->
256
+ <div class="about-backdrop" *ngIf="showTermsModal" (click)="closeTermsModal()" tabindex="-1"></div>
257
+ <div class="about-modal"
258
+ *ngIf="showTermsModal"
259
+ role="dialog"
260
+ aria-modal="true"
261
+ aria-labelledby="termsTitle"
262
+ id="termsDialog"
263
+ tabindex="-1">
264
+ <div class="about-modal__header">
265
+ <h2 id="termsTitle" class="about-modal__title">Terms & Conditions</h2>
266
+ <button class="about-modal__close" (click)="closeTermsModal()" aria-label="Close dialog">×</button>
267
+ </div>
268
+ <div class="about-modal__body">
269
+ <p><strong>Py-Match Terms & Conditions</strong></p>
270
+ <ul>
271
+ <li>By using Py-Match, you agree to provide accurate information for assessments and matching.</li>
272
+ <li>Py-Match is intended for personal, business, and recruitment use only. Misuse or unauthorized access is prohibited.</li>
273
+ <li>All intellectual property, including algorithms, content, and branding, belongs to Pykara Technologies Pvt. Ltd.</li>
274
+ <li>Py-Match does not guarantee match outcomes; results are based on provided data and scientific models.</li>
275
+ <li>Users are responsible for maintaining the confidentiality of their login credentials.</li>
276
+ <li>Py-Match reserves the right to update these terms at any time. Continued use constitutes acceptance of changes.</li>
277
+ <li>For questions or concerns, contact support@pykara.net.</li>
278
+ </ul>
279
+ <p>By continuing to use Py-Match, you acknowledge and accept these terms and conditions.</p>
280
+ </div>
281
+ <div class="about-modal__footer">
282
+ <button class="about-modal__btn" (click)="closeTermsModal()">Close</button>
283
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  </div>
 
285
 
286
+ <!-- Loader Overlay (same as llm-quiz) -->
287
+ <div class="loader-overlay" *ngIf="isLoading">
288
+ <div class="loader-container">
289
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
290
+ </div>
 
 
 
 
 
291
  </div>
292
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/intro-page/intro-page.component.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { Component, ChangeDetectionStrategy, HostListener, ChangeDetectorRef } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
- import { RouterLink } from '@angular/router';
 
4
  import { SignInComponent } from '../auth/sign-in/sign-in.component';
5
  import { SignUpComponent } from '../auth/sign-up/sign-up.component';
 
6
 
7
  type AuthModal = 'signin' | 'signup' | null;
8
 
@@ -14,12 +16,87 @@ type AuthModal = 'signin' | 'signup' | null;
14
  styleUrls: ['./intro-page.component.css'],
15
  changeDetection: ChangeDetectionStrategy.OnPush
16
  })
17
- export class IntroPageComponent {
18
  modal: AuthModal = null;
19
  isSignedIn = false;
20
  isAboutOpen = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- constructor(private cdr: ChangeDetectorRef) { }
23
  // --- Auth modal handlers ---
24
  openSignIn(): void {
25
  this.modal = 'signin';
@@ -37,25 +114,19 @@ export class IntroPageComponent {
37
  this.cdr.markForCheck();
38
  }
39
 
40
-
41
- // Called by child components on success
42
  handleSignInSuccess(): void {
43
  this.isSignedIn = true;
44
  this.closeModal();
45
  }
46
 
47
- // Handle sign-up success - switch to sign-in modal
48
  handleSignUpSuccess(): void {
49
- // Close sign-up and open sign-in
50
  this.modal = 'signin';
51
- // You can optionally show a success message here
52
  alert('Sign up successful! Please sign in to continue.');
53
  this.cdr.markForCheck();
54
  }
55
 
56
  signOut(): void {
57
  this.isSignedIn = false;
58
- // Clear any persisted auth if used later
59
  localStorage.removeItem('auth_token');
60
  this.cdr.markForCheck();
61
  }
@@ -70,41 +141,96 @@ export class IntroPageComponent {
70
  openAbout(): void {
71
  this.isAboutOpen = true;
72
  document.body.style.overflow = 'hidden';
73
- // Move focus into the dialog for accessibility
74
  setTimeout(() => document.getElementById('aboutDialog')?.focus(), 0);
75
  this.cdr.markForCheck();
76
  }
77
 
78
  closeAbout(): void {
79
  this.isAboutOpen = false;
80
- // If Auth is also open, keep scroll locked. Otherwise restore.
81
  if (!this.modal) document.body.style.overflow = '';
82
  this.cdr.markForCheck();
83
  }
84
 
85
- // --- Global ESC handler closes whichever is open ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  @HostListener('document:keydown.escape')
87
  onEsc(): void {
88
  if (this.modal) this.closeModal();
89
  else if (this.isAboutOpen) this.closeAbout();
 
90
  }
91
- scrollToContent() {
 
 
 
 
 
 
 
 
 
 
 
 
92
  const element = document.getElementById('content-sections');
93
  if (element) {
94
  element.scrollIntoView({ behavior: 'smooth' });
95
  }
96
  }
97
- // Your existing i18n helper, keep as-is if already present:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  tr(key: string): string {
99
  const dict = {
100
  brand: 'Py-Match',
101
  signup: 'Sign up',
102
  signin: 'Sign in',
103
- tagline: 'Smarter matches, powered by personality science.',
104
  subtext: 'Discover compatibility in love, business, and careers with AI-driven personality insights.',
105
- /* getStarted: 'Get started',*/
106
  exploreFeatures: 'Explore our features below'
107
- };
108
- return dict[key as keyof typeof dict] ?? key;
109
  }
110
  }
 
1
+ import { Component, ChangeDetectionStrategy, HostListener, ChangeDetectorRef, OnInit, AfterViewInit } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { HttpClient } from '@angular/common/http';
5
  import { SignInComponent } from '../auth/sign-in/sign-in.component';
6
  import { SignUpComponent } from '../auth/sign-up/sign-up.component';
7
+ import { SignupService } from '../auth/sign-up/sign-up.service';
8
 
9
  type AuthModal = 'signin' | 'signup' | null;
10
 
 
16
  styleUrls: ['./intro-page.component.css'],
17
  changeDetection: ChangeDetectionStrategy.OnPush
18
  })
19
+ export class IntroPageComponent implements OnInit {
20
  modal: AuthModal = null;
21
  isSignedIn = false;
22
  isAboutOpen = false;
23
+ canMatch: boolean = false;
24
+ userId: number | null = null;
25
+ showMarriageSubnav = false;
26
+ showPrivacyModal = false;
27
+ privacyAccepted = false;
28
+ showTermsModal = false;
29
+ isLoading = false;
30
+
31
+ constructor(
32
+ private cdr: ChangeDetectorRef,
33
+ private signupService: SignupService,
34
+ private router: Router,
35
+ private http: HttpClient
36
+ ) { }
37
+
38
+ ngOnInit(): void {
39
+ window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
40
+ this.userId = this.signupService.getUserId();
41
+ if (this.userId) {
42
+ // Check if the user has completed LLM questions
43
+ this.http.get(`http://127.0.0.1:5000/api/match/${this.userId}?role=marriage`)
44
+ .subscribe({
45
+ next: () => {
46
+ this.canMatch = true;
47
+ this.cdr.markForCheck();
48
+ },
49
+ error: () => {
50
+ this.canMatch = false;
51
+ this.cdr.markForCheck();
52
+ }
53
+ });
54
+ }
55
+ // Only check if accepted, do not show modal automatically
56
+ this.privacyAccepted = localStorage.getItem('privacyAccepted') === 'true';
57
+ }
58
+
59
+ // --- Marriage Sub Navigation Methods ---
60
+ toggleMarriageSubnav(): void {
61
+ this.showMarriageSubnav = !this.showMarriageSubnav;
62
+ this.cdr.markForCheck();
63
+ }
64
+
65
+ hideMarriageSubnav(): void {
66
+ this.showMarriageSubnav = false;
67
+ this.cdr.markForCheck();
68
+ }
69
+
70
+ navigateToProfile(): void {
71
+ const userId = localStorage.getItem('user_id');
72
+ if (!userId) {
73
+ alert('Please sign in first.');
74
+ return;
75
+ }
76
+
77
+ this.router.navigate(['/question-answer'], { queryParams: { role: 'marriage' } });
78
+ this.showMarriageSubnav = false;
79
+ }
80
+
81
+ navigateToExpectations(): void {
82
+ this.showMarriageSubnav = false;
83
+ this.router.navigate(['/userPreference']);
84
+ }
85
+
86
+ navigateToAssessment(): void {
87
+ this.showMarriageSubnav = false;
88
+ this.router.navigate(['/llmquiz']);
89
+ }
90
+
91
+ navigateToMatchingProfile(): void {
92
+ this.showMarriageSubnav = false;
93
+ this.router.navigate(['/matchinglist']);
94
+ }
95
+ //} else {
96
+ // alert('User ID not found. Please sign in first.');
97
+ //}
98
+ //}
99
 
 
100
  // --- Auth modal handlers ---
101
  openSignIn(): void {
102
  this.modal = 'signin';
 
114
  this.cdr.markForCheck();
115
  }
116
 
 
 
117
  handleSignInSuccess(): void {
118
  this.isSignedIn = true;
119
  this.closeModal();
120
  }
121
 
 
122
  handleSignUpSuccess(): void {
 
123
  this.modal = 'signin';
 
124
  alert('Sign up successful! Please sign in to continue.');
125
  this.cdr.markForCheck();
126
  }
127
 
128
  signOut(): void {
129
  this.isSignedIn = false;
 
130
  localStorage.removeItem('auth_token');
131
  this.cdr.markForCheck();
132
  }
 
141
  openAbout(): void {
142
  this.isAboutOpen = true;
143
  document.body.style.overflow = 'hidden';
 
144
  setTimeout(() => document.getElementById('aboutDialog')?.focus(), 0);
145
  this.cdr.markForCheck();
146
  }
147
 
148
  closeAbout(): void {
149
  this.isAboutOpen = false;
 
150
  if (!this.modal) document.body.style.overflow = '';
151
  this.cdr.markForCheck();
152
  }
153
 
154
+ openPrivacyModal(): void {
155
+ this.showPrivacyModal = true;
156
+ this.cdr.markForCheck();
157
+ }
158
+
159
+ closePrivacyModal(): void {
160
+ this.showPrivacyModal = false;
161
+ this.cdr.markForCheck();
162
+ }
163
+
164
+ acceptPrivacyPolicy(): void {
165
+ localStorage.setItem('privacyAccepted', 'true');
166
+ this.privacyAccepted = true;
167
+ this.closePrivacyModal();
168
+ }
169
+
170
+ openTermsModal(): void {
171
+ this.showTermsModal = true;
172
+ this.cdr.markForCheck();
173
+ }
174
+
175
+ closeTermsModal(): void {
176
+ this.showTermsModal = false;
177
+ this.cdr.markForCheck();
178
+ }
179
+
180
  @HostListener('document:keydown.escape')
181
  onEsc(): void {
182
  if (this.modal) this.closeModal();
183
  else if (this.isAboutOpen) this.closeAbout();
184
+ else if (this.showMarriageSubnav) this.hideMarriageSubnav();
185
  }
186
+
187
+ @HostListener('document:click', ['$event'])
188
+ onDocumentClick(event: MouseEvent): void {
189
+ // Close subnav when clicking outside
190
+ if (this.showMarriageSubnav) {
191
+ const target = event.target as HTMLElement;
192
+ if (!target.closest('.nav-item-wrapper')) {
193
+ this.hideMarriageSubnav();
194
+ }
195
+ }
196
+ }
197
+
198
+ scrollToContent(): void {
199
  const element = document.getElementById('content-sections');
200
  if (element) {
201
  element.scrollIntoView({ behavior: 'smooth' });
202
  }
203
  }
204
+
205
+ selectRole(role: string): void {
206
+ const userId = localStorage.getItem('user_id');
207
+ if (!userId) {
208
+ alert('Please sign in first.');
209
+ return;
210
+ }
211
+
212
+ this.signupService.assignRole(+userId, role).subscribe({
213
+ next: (res) => {
214
+ console.log('Role saved:', res);
215
+ this.router.navigate(['/question-answer'], { queryParams: { role } });
216
+ },
217
+ error: (err) => {
218
+ console.error('Role assign failed:', err);
219
+ alert('Failed to assign role.');
220
+ }
221
+ });
222
+ }
223
+
224
+ // i18n helper
225
  tr(key: string): string {
226
  const dict = {
227
  brand: 'Py-Match',
228
  signup: 'Sign up',
229
  signin: 'Sign in',
230
+ tagline: 'Smarter matches,<br><span class="tagline-line2">powered by personality science.</span>',
231
  subtext: 'Discover compatibility in love, business, and careers with AI-driven personality insights.',
 
232
  exploreFeatures: 'Explore our features below'
233
+ } as const;
234
+ return (dict as Record<string, string>)[key] ?? key;
235
  }
236
  }
src/app/llm-quiz/llm-quiz.component.css CHANGED
@@ -1,274 +1,732 @@
1
- /*.quiz-wrap {
2
- max-width: 880px;
3
- margin: 20px auto;
4
- padding: 12px;
5
- font-family: system-ui, Arial, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
- h2 {
9
- margin: 0 0 12px;
 
 
 
 
 
 
10
  }
11
 
12
- .panel, .qcard, .result {
13
- border: 1px solid #ddd;
14
- border-radius: 8px;
15
- padding: 12px;
16
- background: #fff;
17
- margin-bottom: 14px;
18
  }
19
 
20
- .row {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  display: flex;
22
  align-items: center;
23
  gap: 12px;
24
- margin-bottom: 10px;
25
  }
26
 
27
- .row.column {
28
- flex-direction: column;
29
- align-items: stretch;
30
- }
31
 
32
- .row label {
33
- width: 180px;
34
- font-weight: 600;
35
- }
 
 
 
36
 
37
- .row input[type="number"], .row select, textarea {
38
- flex: 1;
39
- padding: 8px;
40
- border: 1px solid #ccc;
41
- border-radius: 6px;
42
- }
43
 
44
- .actions {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  display: flex;
46
- gap: 10px;
47
- margin-top: 8px;
 
48
  }
49
 
50
- .actions button {
51
- padding: 8px 14px;
52
- border: 0;
53
- border-radius: 6px;
54
- background: #111827;
55
- color: #fff;
56
- cursor: pointer;
57
- }
 
 
 
 
 
 
 
58
 
59
- .actions button[disabled] {
60
- opacity: 0.5;
61
- cursor: not-allowed;
62
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- .error {
65
- color: #b91c1c;
66
- margin-top: 8px;
67
  }
68
 
69
- .loading {
70
- margin-top: 10px;
 
71
  }
72
 
73
- .meta {
 
74
  display: flex;
75
  justify-content: space-between;
76
- margin-bottom: 8px;
77
- color: #374151;
78
- font-size: 13px;
 
79
  }
80
 
81
- .question {
82
- font-size: 18px;
83
- font-weight: 700;
84
- margin-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
 
 
87
  .options {
88
  display: grid;
89
- gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
 
92
- .opt {
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  align-items: center;
95
- gap: 10px;
96
- padding: 8px;
97
- border: 1px solid #eee;
98
- border-radius: 6px;
99
  }
100
 
101
- .opt input {
102
- transform: scale(1.1);
103
- }
 
 
 
 
 
 
 
104
 
105
- .badge {
106
- display: inline-block;
107
- padding: 3px 8px;
108
- border-radius: 999px;
109
- font-size: 12px;
110
- color: #fff;
111
  }
112
 
113
- .badge.blue {
114
- background: #2563eb;
115
- }
 
116
 
117
- .badge.green {
118
- background: #059669;
119
- }
 
 
120
 
121
- .badge.red {
122
- background: #dc2626;
123
- }
 
124
 
125
- .badge.yellow {
126
- background: #d97706;
127
- }
 
 
 
 
128
 
129
- .mix, .result {
130
- margin-top: 14px;
 
 
 
 
 
131
  }
132
 
133
  .bar {
134
  display: grid;
135
- grid-template-columns: 90px 1fr 48px;
136
  align-items: center;
137
- gap: 8px;
138
- margin: 6px 0;
139
  }
140
 
141
  .bar-label {
 
 
 
142
  font-weight: 600;
 
 
143
  }
144
 
145
- .bar-track {
146
- height: 10px;
147
- background: #f3f4f6;
148
- border-radius: 999px;
149
- overflow: hidden;
150
- }
151
 
152
- .bar-fill {
153
- height: 100%;
 
 
 
 
154
  }
155
 
156
- .bar-fill.blue {
157
- background: #93c5fd;
158
- }
159
-
160
- .bar-fill.green {
161
- background: #86efac;
162
- }
163
 
164
- .bar-fill.red {
165
- background: #fca5a5;
166
- }
167
-
168
- .bar-fill.yellow {
169
- background: #fde68a;
170
- }
171
 
172
- .bar-val {
173
- text-align: right;
174
- font-variant-numeric: tabular-nums;
 
 
 
175
  }
176
- */
177
 
 
 
 
 
 
 
 
178
 
179
- .panel {
180
- padding: 12px;
181
- border: 1px solid #ddd;
182
- border-radius: 8px;
183
- margin-bottom: 16px;
184
  }
185
 
186
- .row {
187
- display: flex;
188
- gap: 8px;
189
  align-items: center;
190
- margin: 8px 0;
 
 
 
 
191
  }
192
 
193
- .row label {
194
- width: 160px;
195
- }
 
196
 
197
- .actions {
198
- margin-top: 12px;
199
- display: flex;
200
- gap: 8px;
 
 
 
 
 
 
201
  }
202
 
 
203
  .loader-overlay {
204
  position: fixed;
205
  inset: 0;
 
206
  display: grid;
207
  place-items: center;
208
- background: rgba(0,0,0,0.08);
 
209
  }
210
 
211
- .question-card {
212
- padding: 16px;
213
- border: 1px solid #eee;
214
- border-radius: 8px;
215
- }
216
 
217
- .q-meta {
218
- display: flex;
219
- gap: 16px;
220
- color: #666;
221
- margin-bottom: 8px;
222
  }
223
 
224
- .q-text {
225
- margin: 8px 0 16px;
 
 
226
  }
227
 
228
- .options {
229
- display: grid;
230
- gap: 8px;
 
231
  }
232
 
233
- .option {
234
  display: flex;
235
  align-items: center;
236
- gap: 8px;
237
- }
238
-
239
- .opt-color {
240
- color: #888;
241
- }
242
-
243
- .progress, .result {
244
- margin-top: 16px;
245
  }
246
 
247
- .bar {
248
- display: grid;
249
- grid-template-columns: 80px 1fr 60px;
250
- align-items: center;
251
- gap: 8px;
252
- margin: 6px 0;
253
  }
254
 
255
- .bar-track {
256
- height: 10px;
257
- background: #eee;
258
- border-radius: 8px;
259
- overflow: hidden;
 
 
 
260
  }
261
 
262
- .bar-fill {
263
- height: 100%;
264
- background: #7dafff;
 
265
  }
266
 
267
- .bar-val {
268
- text-align: right;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
 
271
- .empty {
272
- color: #777;
273
- margin-top: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
 
 
1
+ /*[file name]: llm-quiz.component.css
2
+ [file content begin]*/
3
+ /* Enhanced brain-themed UI */
4
+ :host {
5
+ display: block;
6
+ min-height: 100vh;
7
+ background:
8
+ radial-gradient(circle at 15% 20%, rgba(45, 75, 120, 0.8) 0%, transparent 40%),
9
+ radial-gradient(circle at 85% 80%, rgba(120, 75, 180, 0.6) 0%, transparent 40%),
10
+ radial-gradient(circle at 50% 50%, rgba(30, 45, 75, 1) 0%, #121a23 100%);
11
+ background-attachment: fixed;
12
+ position: relative;
13
+ overflow-x: hidden;
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15
+ }
16
+
17
+ /* Neural network background animation */
18
+ .brain-bg {
19
+ position: fixed;
20
+ top: 0;
21
+ left: 0;
22
+ width: 100%;
23
+ height: 100%;
24
+ pointer-events: none;
25
+ z-index: 0;
26
+ opacity: 0.4;
27
  }
28
 
29
+ .neuron {
30
+ position: absolute;
31
+ width: 6px;
32
+ height: 6px;
33
+ border-radius: 50%;
34
+ background: radial-gradient(circle, #f3a54c, #e67e22);
35
+ box-shadow: 0 0 12px 4px rgba(243, 165, 76, 0.7);
36
+ animation: neuronPulse 3s ease-in-out infinite;
37
  }
38
 
39
+ .neuron-connection {
40
+ position: absolute;
41
+ height: 2px;
42
+ background: linear-gradient(90deg, transparent, rgba(243, 165, 76, 0.6), transparent);
43
+ transform-origin: left center;
44
+ animation: connectionFlow 4s linear infinite;
45
  }
46
 
47
+ @keyframes neuronPulse {
48
+ 0%, 100% { transform: scale(1); opacity: 0.7; }
49
+ 50% { transform: scale(1.3); opacity: 1; }
50
+ }
51
+
52
+ @keyframes connectionFlow {
53
+ 0% { opacity: 0; transform: scaleX(0); }
54
+ 50% { opacity: 0.8; transform: scaleX(1); }
55
+ 100% { opacity: 0; transform: scaleX(0); }
56
+ }
57
+
58
+ /* Enhanced topbar with brain theme */
59
+ .quiz-topbar {
60
+ position: fixed;
61
+ top: 0;
62
+ left: 0;
63
+ width: 100%;
64
+ padding: 14px 38px;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ background: rgba(18, 26, 35, 0.9);
69
+ backdrop-filter: blur(12px);
70
+ z-index: 2000;
71
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
72
+ border-bottom: 1px solid rgba(243, 165, 76, 0.2);
73
+ }
74
+
75
+ .brand {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 16px;
79
+ cursor: pointer;
80
+ user-select: none;
81
+ transition: transform 0.3s ease;
82
+ }
83
+
84
+ .brand:hover {
85
+ transform: translateY(-2px);
86
+ }
87
+
88
+ .brand__logo-img {
89
+ background: linear-gradient(135deg, #f3a54c, #e67e22);
90
+ width: 54px;
91
+ height: 54px;
92
+ border-radius: 50%;
93
+ padding: 6px;
94
+ box-shadow: 0 4px 15px rgba(243, 165, 76, 0.4);
95
+ transition: all 0.3s ease;
96
+ }
97
+
98
+ .brand__name {
99
+ letter-spacing: 2px;
100
+ color: #ffffff;
101
+ font-size: 28px;
102
+ font-weight: 300;
103
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
104
+ }
105
+
106
+ .brand__name strong {
107
+ color: #f3a54c;
108
+ font-weight: 400;
109
+ }
110
+
111
+ .top-progress {
112
  display: flex;
113
  align-items: center;
114
  gap: 12px;
115
+ color: #f0f0f0;
116
  }
117
 
118
+ .tp-label {
119
+ font-size: 14px;
120
+ font-weight: 500;
121
+ }
122
 
123
+ .tp-track {
124
+ width: 120px;
125
+ height: 6px;
126
+ background: rgba(255, 255, 255, 0.1);
127
+ border-radius: 3px;
128
+ overflow: hidden;
129
+ }
130
 
131
+ .tp-fill {
132
+ height: 100%;
133
+ background: linear-gradient(90deg, #f3a54c, #e67e22);
134
+ border-radius: 3px;
135
+ transition: width 0.5s ease;
136
+ }
137
 
138
+ /* Brain-themed hero section */
139
+ .quiz-hero {
140
+ max-width: 880px;
141
+ margin: 100px auto 30px;
142
+ padding: 50px 40px;
143
+ background:
144
+ radial-gradient(circle at 20% 30%, rgba(243, 165, 76, 0.15) 0%, transparent 50%),
145
+ linear-gradient(135deg, rgba(30, 42, 60, 0.9) 0%, rgba(40, 55, 80, 0.9) 100%);
146
+ border-radius: 24px;
147
+ position: relative;
148
+ overflow: hidden;
149
+ box-shadow:
150
+ 0 20px 40px rgba(0, 0, 0, 0.4),
151
+ 0 8px 24px rgba(0, 0, 0, 0.3);
152
+ border: 1px solid rgba(243, 165, 76, 0.2);
153
+ color: #f5f8fb;
154
+ text-align: center;
155
+ }
156
+
157
+ .quiz-hero::before {
158
+ content: "";
159
+ position: absolute;
160
+ inset: 0;
161
+ background:
162
+ radial-gradient(circle at 70% 20%, rgba(243, 165, 76, 0.25) 0%, transparent 40%),
163
+ radial-gradient(circle at 30% 80%, rgba(120, 75, 180, 0.2) 0%, transparent 40%);
164
+ mix-blend-mode: overlay;
165
+ z-index: 1;
166
+ }
167
+
168
+ .hero-inner {
169
+ position: relative;
170
+ z-index: 2;
171
  display: flex;
172
+ flex-direction: column;
173
+ align-items: center;
174
+ gap: 20px;
175
  }
176
 
177
+ .hero-icon {
178
+ width: 100px;
179
+ height: 100px;
180
+ border-radius: 50%;
181
+ background: linear-gradient(135deg, #f3a54c, #e67e22);
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ font-size: 46px;
186
+ color: #1e2532;
187
+ box-shadow:
188
+ 0 10px 30px rgba(243, 165, 76, 0.5),
189
+ 0 0 0 4px rgba(243, 165, 76, 0.2);
190
+ animation: iconFloat 4s ease-in-out infinite;
191
+ }
192
 
193
+ @keyframes iconFloat {
194
+ 0%, 100% { transform: translateY(0px) rotate(0deg); }
195
+ 50% { transform: translateY(-10px) rotate(5deg); }
196
+ }
197
+
198
+ .hero-title {
199
+ margin: 0;
200
+ font-size: clamp(36px, 4vw, 48px);
201
+ line-height: 1.1;
202
+ font-weight: 700;
203
+ letter-spacing: 0.5px;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 16px;
207
+ flex-wrap: wrap;
208
+ justify-content: center;
209
+ }
210
+
211
+ .hero-title .fa-brain {
212
+ color: #f3a54c;
213
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.5));
214
+ animation: brainPulse 2s ease-in-out infinite;
215
+ }
216
+
217
+ @keyframes brainPulse {
218
+ 0%, 100% { transform: scale(1); }
219
+ 50% { transform: scale(1.1); }
220
+ }
221
+
222
+ .hero-sub {
223
+ margin: 0;
224
+ max-width: 680px;
225
+ font-size: clamp(1rem, 1.1vw, 1.1rem);
226
+ line-height: 1.6;
227
+ opacity: 0.9;
228
+ font-weight: 400;
229
+ }
230
+
231
+ /* Enhanced quiz container */
232
+ .quiz {
233
+ position: absolute;
234
+ z-index: 2;
235
+ width: 80%;
236
+ margin: 0 auto 40px;
237
+ background: rgba(255, 255, 255, 0.88);
238
+ -webkit-backdrop-filter: blur(15px);
239
+ backdrop-filter: blur(15px);
240
+ border-radius: 20px;
241
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25), 0 8px 24px rgba(0, 0, 0, 0.15);
242
+ padding: 30px;
243
+ border: 1px solid rgba(255, 255, 255, 0.3);
244
+ top: 50%;
245
+ left: 50%;
246
+ transform: translate(-50%, -50%);
247
+ }
248
+
249
+ /* Enhanced cards with brain-inspired design */
250
+ .question-card, .result, .panel {
251
+ background: rgba(255, 255, 255, 0.95);
252
+ border-radius: 16px;
253
+ box-shadow:
254
+ 0 8px 24px rgba(0, 0, 0, 0.08),
255
+ 0 2px 8px rgba(0, 0, 0, 0.04);
256
+ padding: 28px;
257
+ margin-bottom: 24px;
258
+ border: 1px solid rgba(243, 165, 76, 0.1);
259
+ animation: cardAppear 0.5s ease-out;
260
+ position: relative;
261
+ overflow: hidden;
262
+ }
263
+
264
+ .question-card::before, .result::before, .panel::before {
265
+ content: "";
266
+ position: absolute;
267
+ top: 0;
268
+ left: 0;
269
+ right: 0;
270
+ height: 4px;
271
+ background: linear-gradient(90deg, #f3a54c, #e67e22, #f3a54c);
272
+ background-size: 200% 100%;
273
+ animation: shimmer 3s ease-in-out infinite;
274
+ }
275
 
276
+ @keyframes cardAppear {
277
+ from { opacity: 0; transform: translateY(20px) scale(0.98); }
278
+ to { opacity: 1; transform: translateY(0) scale(1); }
279
  }
280
 
281
+ @keyframes shimmer {
282
+ 0% { background-position: -200% 0; }
283
+ 100% { background-position: 200% 0; }
284
  }
285
 
286
+ /* Question styling */
287
+ .q-meta {
288
  display: flex;
289
  justify-content: space-between;
290
+ align-items: center;
291
+ margin-bottom: 16px;
292
+ color: #666;
293
+ font-size: 0.9rem;
294
  }
295
 
296
+ .q-count {
297
+ font-weight: 600;
298
+ color: #2C3E50;
299
+ background: rgba(243, 165, 76, 0.1);
300
+ padding: 6px 12px;
301
+ border-radius: 20px;
302
+ }
303
+
304
+ .q-role .badge {
305
+ display: inline-flex;
306
+ align-items: center;
307
+ gap: 6px;
308
+ padding: 6px 14px;
309
+ background: linear-gradient(135deg, rgba(243, 165, 76, 0.15), rgba(230, 126, 34, 0.1));
310
+ color: #e67e22;
311
+ border: 1px solid rgba(230, 126, 34, 0.3);
312
+ border-radius: 20px;
313
+ font-size: 0.85rem;
314
+ font-weight: 500;
315
+ }
316
+
317
+ .q-text {
318
+ font-size: 1.7rem;
319
+ font-weight: 600;
320
+ margin: 20px 0;
321
+ line-height: 1.4;
322
+ color: #2C3E50;
323
+ text-align: center;
324
+ }
325
+
326
+ .q-brain {
327
+ color: #f3a54c;
328
+ margin-right: 10px;
329
+ animation: thoughtBubble 2s ease-in-out infinite;
330
+ }
331
+
332
+ @keyframes thoughtBubble {
333
+ 0%, 100% { transform: scale(1) rotate(0deg); }
334
+ 25% { transform: scale(1.1) rotate(5deg); }
335
+ 75% { transform: scale(1.1) rotate(-5deg); }
336
  }
337
 
338
+ /* Enhanced options with brain-inspired colors */
339
  .options {
340
  display: grid;
341
+ grid-template-columns: repeat(2, 1fr);
342
+ gap: 16px;
343
+ margin: 24px 0;
344
+ }
345
+
346
+ .option {
347
+ position: relative;
348
+ transition: transform 0.3s ease;
349
+ }
350
+
351
+ .option:hover {
352
+ transform: translateY(-3px);
353
+ }
354
+
355
+ .option input[type=radio] {
356
+ position: absolute;
357
+ opacity: 0;
358
+ width: 100%;
359
+ height: 100%;
360
+ cursor: pointer;
361
+ z-index: 2;
362
+ }
363
+
364
+ .opt-text {
365
+ display: block;
366
+ padding: 20px;
367
+ border: 2px solid #e2e7ee;
368
+ border-radius: 12px;
369
+ background: #fbfcfe;
370
+ color: #2C3E50;
371
+ font-weight: 500;
372
+ transition: all 0.3s ease;
373
+ position: relative;
374
+ overflow: hidden;
375
+ }
376
+
377
+ .opt-text::before {
378
+ content: "";
379
+ position: absolute;
380
+ top: 0;
381
+ left: -100%;
382
+ width: 100%;
383
+ height: 100%;
384
+ background: linear-gradient(90deg, transparent, rgba(243, 165, 76, 0.1), transparent);
385
+ transition: left 0.5s ease;
386
+ }
387
+
388
+ .option:hover .opt-text::before {
389
+ left: 100%;
390
+ }
391
+
392
+ .option input:checked + .opt-text {
393
+ border-color: #f3a54c;
394
+ background: rgba(243, 165, 76, 0.05);
395
+ box-shadow: 0 4px 12px rgba(243, 165, 76, 0.2);
396
+ transform: scale(1.02);
397
  }
398
 
399
+ /* Color chips for options */
400
+ .color-chip {
401
+ width: 20px;
402
+ height: 20px;
403
+ border-radius: 6px;
404
+ margin-right: 10px;
405
+ display: inline-block;
406
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
407
+ vertical-align: middle;
408
+ }
409
+
410
+ /* Enhanced buttons */
411
+ .actions {
412
+ margin-top: 24px;
413
  display: flex;
414
+ gap: 14px;
415
+ flex-wrap: wrap;
416
+ }
417
+
418
+ button {
419
+ padding: 14px 24px;
420
+ font-size: 1rem;
421
+ font-weight: 600;
422
+ border: none;
423
+ border-radius: 12px;
424
+ cursor: pointer;
425
+ transition: all 0.3s ease;
426
+ display: inline-flex;
427
  align-items: center;
428
+ gap: 8px;
429
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
430
+ position: relative;
431
+ overflow: hidden;
432
  }
433
 
434
+ button::before {
435
+ content: "";
436
+ position: absolute;
437
+ top: 0;
438
+ left: -100%;
439
+ width: 100%;
440
+ height: 100%;
441
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
442
+ transition: left 0.5s ease;
443
+ }
444
 
445
+ button:hover::before {
446
+ left: 100%;
 
 
 
 
447
  }
448
 
449
+ button:active {
450
+ transform: translateY(2px);
451
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
452
+ }
453
 
454
+ button.primary, button.submit-btn {
455
+ background: linear-gradient(135deg, #f3a54c, #e67e22);
456
+ color: white;
457
+ flex: 1;
458
+ }
459
 
460
+ button.primary:hover, button.submit-btn:hover {
461
+ background: linear-gradient(135deg, #ffb347, #e67e22);
462
+ transform: translateY(-2px);
463
+ }
464
 
465
+ button[disabled] {
466
+ background: #cfd8e3;
467
+ color: #6b7280;
468
+ cursor: not-allowed;
469
+ box-shadow: none;
470
+ transform: none;
471
+ }
472
 
473
+ button[disabled]::before {
474
+ display: none;
475
+ }
476
+
477
+ /* Enhanced progress bars */
478
+ .progress {
479
+ margin: 24px 0;
480
  }
481
 
482
  .bar {
483
  display: grid;
484
+ grid-template-columns: 140px 1fr 60px;
485
  align-items: center;
486
+ gap: 14px;
487
+ margin: 14px 0;
488
  }
489
 
490
  .bar-label {
491
+ display: inline-flex;
492
+ align-items: center;
493
+ gap: 10px;
494
  font-weight: 600;
495
+ color: #2C3E50;
496
+ text-transform: capitalize;
497
  }
498
 
499
+ /* Strengthened specificity for dimension colors */
500
+ .progress .bar .bar-label.dim-blue, .result .bar .bar-label.dim-blue { color:#2980b9 !important; }
501
+ .progress .bar .bar-label.dim-green, .result .bar .bar-label.dim-green { color:#1d7f44 !important; }
502
+ .progress .bar .bar-label.dim-red, .result .bar .bar-label.dim-red { color:#b93528 !important; }
503
+ .progress .bar .bar-label.dim-yellow, .result .bar .bar-label.dim-yellow { color:#b56a00 !important; }
 
504
 
505
+ .dot {
506
+ width: 12px;
507
+ height: 12px;
508
+ border-radius: 50%;
509
+ display: inline-block;
510
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
511
  }
512
 
513
+ .dot.blue { background:#3498db; }
514
+ .dot.green { background:#2ecc71; }
515
+ .dot.red { background:#e74c3c; }
516
+ .dot.yellow { background:#f1c40f; }
 
 
 
517
 
518
+ /* Add missing bar-fill color classes */
519
+ .bar-fill.blue { background:linear-gradient(90deg,#5dade2,#2e86c1); }
520
+ .bar-fill.green { background:linear-gradient(90deg,#58d68d,#239b56); }
521
+ .bar-fill.red { background:linear-gradient(90deg,#ec7063,#cb4335); }
522
+ .bar-fill.yellow { background:linear-gradient(90deg,#f8c471,#d68910); }
 
 
523
 
524
+ /* Fallback if gradient not supported */
525
+ @supports not (background:linear-gradient(red,blue)) {
526
+ .bar-fill.blue { background:#2e86c1; }
527
+ .bar-fill.green { background:#239b56; }
528
+ .bar-fill.red { background:#cb4335; }
529
+ .bar-fill.yellow { background:#d68910; }
530
  }
 
531
 
532
+ /* Enhanced results section */
533
+ .result h2 {
534
+ color: #2C3E50;
535
+ text-align: center;
536
+ margin-bottom: 24px;
537
+ font-size: 2rem;
538
+ }
539
 
540
+ .result-sub {
541
+ text-align: center;
542
+ margin-bottom: 20px;
543
+ font-size: 1.1rem;
544
+ color: #666;
545
  }
546
 
547
+ .pill {
548
+ display: inline-flex;
 
549
  align-items: center;
550
+ gap: 6px;
551
+ padding: 8px 16px;
552
+ border-radius: 20px;
553
+ font-weight: 600;
554
+ text-transform: capitalize;
555
  }
556
 
557
+ .pill-blue { background: rgba(52, 152, 219, 0.15); color: #2980b9; }
558
+ .pill-green { background: rgba(46, 204, 113, 0.15); color: #27ae60; }
559
+ .pill-red { background: rgba(231, 76, 60, 0.15); color: #c0392b; }
560
+ .pill-yellow { background: rgba(241, 196, 15, 0.15); color: #d35400; }
561
 
562
+ .dom-desc {
563
+ text-align: center;
564
+ font-size: 1.1rem;
565
+ font-style: italic;
566
+ color: #7f8c8d;
567
+ margin-bottom: 30px;
568
+ padding: 16px;
569
+ background: rgba(243, 165, 76, 0.05);
570
+ border-radius: 12px;
571
+ border-left: 4px solid #f3a54c;
572
  }
573
 
574
+ /* Loader overlay */
575
  .loader-overlay {
576
  position: fixed;
577
  inset: 0;
578
+ background: rgba(12, 18, 28, 0.7);
579
  display: grid;
580
  place-items: center;
581
+ z-index: 9999;
582
+ backdrop-filter: blur(5px);
583
  }
584
 
585
+ .loader-container { text-align:center; }
586
+ .loader-gif { width: 50%; height: 50%; }
587
+ /* Keep spinner styles in case referenced elsewhere */
588
+ .loader { display:none; }
589
+ .spinner { display:none; }
590
 
591
+ /* Form elements in panel */
592
+ .panel {
593
+ text-align: center;
 
 
594
  }
595
 
596
+ .panel-title {
597
+ color: #2C3E50;
598
+ margin-bottom: 16px;
599
+ font-size: 1.8rem;
600
  }
601
 
602
+ .panel-hint {
603
+ color: #666;
604
+ margin-bottom: 28px;
605
+ line-height: 1.5;
606
  }
607
 
608
+ .row {
609
  display: flex;
610
  align-items: center;
611
+ justify-content: space-between;
612
+ margin-bottom: 18px;
613
+ text-align: left;
 
 
 
 
 
 
614
  }
615
 
616
+ .row label {
617
+ font-weight: 600;
618
+ color: #2C3E50;
619
+ min-width: 140px;
 
 
620
  }
621
 
622
+ .row input, .row select {
623
+ flex: 1;
624
+ padding: 12px 16px;
625
+ border: 2px solid #e2e7ee;
626
+ border-radius: 10px;
627
+ font-size: 1rem;
628
+ transition: border-color 0.3s ease;
629
+ background: white;
630
  }
631
 
632
+ .row input:focus, .row select:focus {
633
+ outline: none;
634
+ border-color: #f3a54c;
635
+ box-shadow: 0 0 0 3px rgba(243, 165, 76, 0.2);
636
  }
637
 
638
+ /* Watermark */
639
+ /*:host::after {
640
+ content: "NeuroMatch";
641
+ position: fixed;
642
+ right: 20px;
643
+ bottom: 20px;
644
+ z-index: 2;
645
+ pointer-events: none;
646
+ user-select: none;
647
+ font-weight: 700;
648
+ font-size: 14px;
649
+ letter-spacing: 1px;
650
+ color: rgba(255, 255, 255, 0.7);
651
+ background: rgba(30, 42, 60, 0.7);
652
+ backdrop-filter: blur(4px);
653
+ padding: 8px 14px;
654
+ border-radius: 10px;
655
+ border: 1px solid rgba(243, 165, 76, 0.3);
656
+ }*/
657
+
658
+ /* Responsive design */
659
+ @media (max-width: 900px) {
660
+ .quiz-hero {
661
+ padding: 40px 28px;
662
+ margin-top: 110px;
663
+ }
664
+
665
+ .hero-icon {
666
+ width: 80px;
667
+ height: 80px;
668
+ font-size: 38px;
669
+ }
670
+
671
+ .quiz {
672
+ margin: 0 16px 30px;
673
+ padding: 24px;
674
+ }
675
+
676
+ .options {
677
+ grid-template-columns: 1fr;
678
+ }
679
  }
680
 
681
+ @media (max-width: 600px) {
682
+ .quiz-topbar {
683
+ padding: 12px 20px;
684
+ }
685
+
686
+ .brand__logo-img {
687
+ width: 44px;
688
+ height: 44px;
689
+ }
690
+
691
+ .brand__name {
692
+ font-size: 22px;
693
+ }
694
+
695
+ .quiz-hero {
696
+ padding: 30px 20px;
697
+ margin-top: 90px;
698
+ }
699
+
700
+ .hero-title {
701
+ font-size: 28px;
702
+ }
703
+
704
+ .q-text {
705
+ font-size: 1.4rem;
706
+ }
707
+
708
+ .bar {
709
+ grid-template-columns: 1fr;
710
+ gap: 8px;
711
+ text-align: center;
712
+ }
713
+
714
+ .bar-val {
715
+ text-align: center;
716
+ }
717
+
718
+ .row {
719
+ flex-direction: column;
720
+ align-items: flex-start;
721
+ gap: 8px;
722
+ }
723
+
724
+ .row input, .row select {
725
+ width: 100%;
726
+ }
727
+
728
+ .actions {
729
+ flex-direction: column;
730
+ }
731
  }
732
+ /*[file content end]*/
src/app/llm-quiz/llm-quiz.component.html CHANGED
@@ -1,142 +1,138 @@
1
- <!-- Control panel -->
2
- <!--<section class="panel">
3
- <div class="row">
4
- <label>User ID</label>
5
- <input type="text" [(ngModel)]="userId" placeholder="e.g. 1" />
6
- </div>
7
-
8
- <div class="row">
9
- <label>Role</label>
10
- <select [(ngModel)]="role">
11
- <option value="interview">interview</option>
12
- <option value="marriage">marriage</option>
13
- <option value="partnership">partnership</option>
14
- <option value="team">team</option>
15
- <option value="general">general</option>
16
- <option value="assistant">assistant</option>
17
- <option value="ceo">ceo</option>
18
- </select>
19
- </div>
20
-
21
- <div class="row">
22
- <label>No. of questions</label>
23
- <input type="number" [(ngModel)]="nQuestions" min="1" max="50" />
24
- </div>
25
-
26
- <div class="row">
27
- <label>Batch size</label>
28
- <input type="number" [(ngModel)]="batchSize" min="1" max="20" />
29
- </div>
30
-
31
- <div class="actions">
32
- <button type="button" (click)="start()" [disabled]="loading">Start</button>
33
- <button type="button" (click)="restart()" [disabled]="loading">Reset</button>
34
- </div>
35
-
36
- <p class="error" *ngIf="errorMsg">{{ errorMsg }}</p>
37
- </section>-->
38
 
39
- <!-- Loader -->
40
  <div class="loader-overlay" *ngIf="loading">
41
- <div class="loader">Loading…</div>
 
 
42
  </div>
43
 
44
  <!-- Quiz area -->
45
  <section class="quiz" *ngIf="!loading">
46
-
47
- <!-- When there is an active question -->
48
  <div class="question-card" *ngIf="!isDone && question">
49
  <div class="q-meta">
50
- <div class="q-count">Question {{ index }} / {{ total }}</div>
51
- <div class="q-role">Role: {{ role }}</div>
 
 
52
  </div>
53
 
54
- <h2 class="q-text">{{ question }}</h2>
55
 
56
  <form (ngSubmit)="submitAnswer()">
57
  <div class="options">
58
  <label class="option" *ngFor="let opt of options; let i = index">
59
- <input type="radio"
60
- name="answer"
61
- [value]="opt.color"
62
- [(ngModel)]="selectedColor"
63
- required />
64
- <span class="opt-text">{{ opt.text }}</span>
65
- <!--<small class="opt-color">({{ opt.color }})</small>-->
66
  </label>
67
  </div>
68
-
69
  <div class="actions">
70
- <button type="submit" [disabled]="!selectedColor || loading">Submit</button>
 
 
 
 
 
71
  </div>
72
  </form>
73
 
74
- <!-- In-progress mix (live) -->
75
- <!--<div class="progress" *ngIf="inProgressMix">
76
- <h3>Current mix</h3>
77
- <div class="bar">
78
- <span>Blue</span>
79
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('blue')"></div></div>
80
- <div class="bar-val">{{ asPercent(getInProgressMixValue('blue')) }}%</div>
81
- </div>
82
-
83
- <div class="bar">
84
- <span>Green</span>
85
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('green')"></div></div>
86
- <div class="bar-val">{{ asPercent(getInProgressMixValue('green')) }}%</div>
87
- </div>
88
-
89
- <div class="bar">
90
- <span>Red</span>
91
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('red')"></div></div>
92
- <div class="bar-val">{{ asPercent(getInProgressMixValue('red')) }}%</div>
93
- </div>
94
 
95
- <div class="bar">
96
- <span>Yellow</span>
97
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('yellow')"></div></div>
98
- <div class="bar-val">{{ asPercent(getInProgressMixValue('yellow')) }}%</div>
 
 
 
 
 
 
 
99
  </div>
100
- </div>-->
101
  </div>
102
 
103
  <!-- Final result -->
104
- <div>DONE</div>
105
- <!--<div class="result" *ngIf="isDone && finalMix">
106
- <h2>Final result</h2>
107
-
108
- <div class="bar">
109
- <span>Blue</span>
110
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('blue')"></div></div>-->
111
- <!--<div class="bar-val">{{ asPercent(getFinalMixValue('blue')) }}%</div>-->
112
- <!--</div>
 
 
 
 
 
 
 
 
113
 
114
- <div class="bar">
115
- <span>Green</span>
116
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('green')"></div></div>-->
117
- <!--<div class="bar-val">{{ asPercent(getFinalMixValue('green')) }}%</div>-->
118
- <!--</div>
 
 
 
 
 
 
 
 
119
 
120
- <div class="bar">
121
- <span>Red</span>
122
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('red')"></div></div>-->
123
- <!--<div class="bar-val">{{ asPercent(getFinalMixValue('red')) }}%</div>-->
124
- <!--</div>
125
 
126
- <div class="bar">
127
- <span>Yellow</span>
128
- <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('yellow')"></div></div>-->
129
- <!--<div class="bar-val">{{ asPercent(getFinalMixValue('yellow')) }}%</div>-->
130
- <!--</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  <div class="actions">
133
- <button type="button" (click)="restart()">Restart</button>
 
 
 
 
 
134
  </div>
135
- </div>-->
136
 
137
- <!-- Empty state -->
138
- <div class="empty" *ngIf="!isDone && !question && !errorMsg">
139
- <p>Press <strong>Start</strong> to begin the quiz.</p>
140
  </div>
141
  </section>
142
-
 
1
+ <!-- Removed local brand header (global top navigation present) -->
2
+ <div class="brain-bg" #brainBg></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ <!-- Loader (updated to match quiz component) -->
5
  <div class="loader-overlay" *ngIf="loading">
6
+ <div class="loader-container">
7
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
8
+ </div>
9
  </div>
10
 
11
  <!-- Quiz area -->
12
  <section class="quiz" *ngIf="!loading">
13
+ <!-- Active question area -->
 
14
  <div class="question-card" *ngIf="!isDone && question">
15
  <div class="q-meta">
16
+ <div class="q-count">Neural Query {{ index }} / {{ total }}</div>
17
+ <div class="q-role">
18
+ <span class="badge"><i [class]="quizIcon"></i> {{ role | titlecase }} Profile</span>
19
+ </div>
20
  </div>
21
 
22
+ <h2 class="q-text fade-in"><i class="fa-solid fa-brain q-brain"></i> {{ question }}</h2>
23
 
24
  <form (ngSubmit)="submitAnswer()">
25
  <div class="options">
26
  <label class="option" *ngFor="let opt of options; let i = index">
27
+ <input type="radio" name="answer" [value]="opt.color" [(ngModel)]="selectedColor" required />
28
+ <span class="opt-text">
29
+ <span class="color-chip {{opt.color}}"></span>
30
+ {{ opt.text }}
31
+ </span>
 
 
32
  </label>
33
  </div>
 
34
  <div class="actions">
35
+ <button type="submit" [disabled]="!selectedColor || loading" class="submit-btn">
36
+ <i class="fa-solid fa-brain"></i> Process Response
37
+ </button>
38
+ <button type="button" (click)="restart()" [disabled]="loading">
39
+ <i class="fa-solid fa-rotate-left"></i> Reset Assessment
40
+ </button>
41
  </div>
42
  </form>
43
 
44
+ <div class="inline-progress" *ngIf="total">
45
+ <div class="ip-track"><div class="ip-fill" [style.width.%]="progressPercent"></div></div>
46
+ <small class="ip-label">Neural Mapping: {{ progressPercent }}% Complete</small>
47
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ <div class="progress" *ngIf="inProgressMix">
50
+ <h3><i class="fa-solid fa-network-wired"></i> Current Neural Signature</h3>
51
+ <div class="bar" *ngFor="let color of ['blue', 'green', 'red', 'yellow']">
52
+ <span class="bar-label" [ngClass]="'dim-' + color">
53
+ <span class="dot" [ngClass]="color"></span>
54
+ {{ getDimensionName(color) }}
55
+ </span>
56
+ <div class="bar-track">
57
+ <div class="bar-fill {{color}}" [style.width.%]="getInProgressMixValue(color)"></div>
58
+ </div>
59
+ <div class="bar-val">{{ asPercent(getInProgressMixValue(color)) }}%</div>
60
  </div>
61
+ </div>
62
  </div>
63
 
64
  <!-- Final result -->
65
+ <div class="result" *ngIf="isDone && finalMix">
66
+ <h2 class="fade-in"><i class="fa-solid fa-brain"></i> Neural Profile Complete</h2>
67
+ <p class="result-sub" *ngIf="dominantColor">
68
+ Primary Cognitive Pattern: <span class="pill pill-{{dominantColor}}"><i [class]="quizIcon"></i> {{ getDimensionName(dominantColor) }}</span>
69
+ </p>
70
+ <p class="dom-desc" *ngIf="dominantDescription">{{ dominantDescription }}</p>
71
+
72
+ <div class="bar" *ngFor="let color of ['blue', 'green', 'red', 'yellow']">
73
+ <span class="bar-label" [ngClass]="'dim-' + color">
74
+ <span class="dot" [ngClass]="color"></span>
75
+ {{ getDimensionName(color) }}
76
+ </span>
77
+ <div class="bar-track">
78
+ <div class="bar-fill {{color}}" [style.width.%]="getFinalMixValue(color)"></div>
79
+ </div>
80
+ <div class="bar-val">{{ asPercent(getFinalMixValue(color)) }}%</div>
81
+ </div>
82
 
83
+ <div class="actions">
84
+ <button type="button" (click)="restart()" class="primary">
85
+ <i class="fa-solid fa-arrow-rotate-right"></i> New Assessment
86
+ </button>
87
+ <button type="button" (click)="goHome()">
88
+ <i class="fa-solid fa-house"></i> Return Home
89
+ </button>
90
+ <a [routerLink]="['/matchinglist', userId]"
91
+ class="btn btn-secondary">
92
+ Matching
93
+ </a>
94
+ </div>
95
+ </div>
96
 
97
+ <!-- Configuration panel -->
98
+ <div class="panel" *ngIf="!isDone && !question && !errorMsg">
99
+ <h2 class="panel-title"><i class="fa-solid fa-brain"></i> Configure Neural Assessment</h2>
100
+ <p class="panel-hint">Set up your cognitive profile assessment. Your existing profile will personalize the questions and results.</p>
 
101
 
102
+ <div class="row">
103
+ <label for="userId">ID</label>
104
+ <input id="userId" type="text" [(ngModel)]="userId" placeholder="Enter your unique identifier" />
105
+ </div>
106
+ <div class="row">
107
+ <label for="role">Assessment Context</label>
108
+ <select id="role" [(ngModel)]="role">
109
+ <option value="interview">Career Interview</option>
110
+ <option value="marriage">Relationship Compatibility</option>
111
+ <option value="partnership">Business Partnership</option>
112
+ <option value="team">Team Dynamics</option>
113
+ <option value="general">General Personality</option>
114
+ <option value="assistant">AI Assistant Matching</option>
115
+ <option value="ceo">Leadership Profile</option>
116
+ </select>
117
+ </div>
118
+ <div class="row">
119
+ <label for="nQuestions">Neural Queries</label>
120
+ <input id="nQuestions" type="number" [(ngModel)]="nQuestions" min="5" max="50" />
121
+ </div>
122
+ <div class="row">
123
+ <label for="batchSize">Processing Batch</label>
124
+ <input id="batchSize" type="number" [(ngModel)]="batchSize" min="1" max="20" />
125
+ </div>
126
 
127
  <div class="actions">
128
+ <button type="button" (click)="start()" [disabled]="loading" class="primary">
129
+ <i class="fa-solid fa-play"></i> Begin Neural Mapping
130
+ </button>
131
+ <button type="button" (click)="restart()" [disabled]="loading">
132
+ <i class="fa-solid fa-rotate-left"></i> Reset Configuration
133
+ </button>
134
  </div>
 
135
 
136
+ <p class="error" *ngIf="errorMsg">{{ errorMsg }}</p>
 
 
137
  </div>
138
  </section>
 
src/app/llm-quiz/llm-quiz.component.ts CHANGED
@@ -1,244 +1,207 @@
1
- import { Component, OnInit } from '@angular/core';
2
  import { FormsModule } from '@angular/forms';
3
  import { CommonModule } from '@angular/common';
4
- import { LlmQaService, Role, ColorKey, StartResponse, NextResponse } from '../services/llm-qa.service';
5
  import { ActivatedRoute, Router } from '@angular/router';
6
-
7
  type Mix = Record<ColorKey, number>;
8
 
9
  @Component({
10
  selector: 'app-llm-quiz',
11
  standalone: true,
12
- imports: [CommonModule, FormsModule],
13
  templateUrl: './llm-quiz.component.html',
14
  styleUrls: ['./llm-quiz.component.css'],
15
  })
16
- export class LlmQuizComponent implements OnInit {
 
 
17
  // --- Config bound to the UI ---
18
  role: Role = 'marriage';
19
- nQuestions = 5;
20
- batchSize = 5;
21
-
22
- /** Provide user id. Will auto-read from localStorage('user_id') if available. */
23
  userId = '';
24
 
25
  // --- Runtime state ---
26
  sessionId = '';
27
- index = 0;
28
- total = 0;
29
-
30
  question = '';
31
- options: { text: string; color: ColorKey }[] = [];
32
  selectedColor: ColorKey | '' = '';
33
 
34
  loading = false;
35
  errorMsg = '';
36
  isDone = false;
37
 
38
- inProgressMix: Mix | null = null; // live progress returned by backend
39
- finalMix: Mix | null = null;
40
 
41
- /*constructor(private api: LlmQaService) { }*/
42
-
 
 
 
43
 
44
- constructor(
45
- private api: LlmQaService,
46
- private route: ActivatedRoute,
47
- private router: Router
48
- ) { }
49
 
50
- ngOnInit(): void {
51
- const qp = this.route.snapshot.queryParamMap;
52
- const uid = (qp.get('user_id') || localStorage.getItem('user_id') || '').trim();
53
- const role = (qp.get('role') || localStorage.getItem('role') || '').toLowerCase();
54
- const autostart = qp.get('autostart') === '1';
55
 
56
- if(uid) this.userId = uid;
57
- if(role) this.role = role as any;
58
 
59
- if(autostart && this.userId && this.role) {
60
- this.start();
61
- }
62
- }
63
-
64
-
65
- // ---------- Helpers ----------
66
- asPercent(v: number | undefined | null): number {
67
- const n = Number(v);
68
- return Number.isFinite(n) ? Math.round(n) : 0;
69
  }
70
 
71
- /** Safe getter for in-progress mix values. */
72
- getInProgressMixValue(c: string): number {
73
- if (!this.inProgressMix) return 0;
74
- const key = this.toColorKey(c);
75
- return key ? this.inProgressMix[key] ?? 0 : 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
- /** Safe getter for final mix values. */
79
- getFinalMixValue(c: string): number {
80
- if (!this.finalMix) return 0;
81
- const key = this.toColorKey(c);
82
- return key ? this.finalMix[key] ?? 0 : 0;
 
 
 
 
 
83
  }
84
-
85
- private toColorKey(c: string): ColorKey | null {
86
- switch (c) {
87
- case 'blue':
88
- case 'green':
89
- case 'red':
90
- case 'yellow':
91
- return c;
92
- default:
93
- return null;
94
  }
95
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- // ---------- Flow ----------
98
- //start(): void {
99
- // this.errorMsg = '';
100
- // this.isDone = false;
101
- // this.finalMix = null;
102
- // this.inProgressMix = null;
103
- // this.selectedColor = '';
104
- // this.sessionId = '';
105
- // this.index = 0;
106
- // this.total = 0;
107
- // this.question = '';
108
- // this.options = [];
109
-
110
- // if (!this.userId?.trim()) {
111
- // this.errorMsg = 'User ID is required.';
112
- // return;
113
- // }
114
-
115
- // this.loading = true;
116
- // this.api .start({
117
-
118
- // user_id: this.userId.trim(),
119
- // role: this.role,
120
- // n_questions: this.nQuestions,
121
- // batch_size: this.batchSize,
122
- // })
123
- // .subscribe({
124
- // next: (res: StartResponse) => {
125
- // this.loading = false;
126
- // this.sessionId = res.session_id;
127
- // this.index = res.index;
128
- // this.total = res.total;
129
- // this.question = res.question;
130
- // this.options = (res.options || []) as any;
131
- // // Optional: inspect res.profile_used
132
- // if (res.profile_used === false) {
133
- // this.errorMsg =
134
- // 'No profile found for this user and role. Please complete the profile first.';
135
- // }
136
- // },
137
- // error: (err) => {
138
- // this.loading = false;
139
- // this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
140
- // },
141
- // });
142
- //}
143
  start(): void {
 
 
144
  this.errorMsg = '';
145
  this.isDone = false;
146
  this.finalMix = null;
147
  this.inProgressMix = null;
148
  this.selectedColor = '';
149
- this.sessionId = '';
150
- this.index = 0;
151
- this.total = 0;
152
- this.question = '';
153
- this.options = [];
154
 
155
- if (!this.userId?.trim()) {
156
- this.errorMsg = 'User ID is required.';
157
- return;
158
- }
159
-
160
- this.loading = true;
161
-
162
- // Log the payload before the API request
163
- const payload = {
164
- user_id: this.userId.trim(),
165
  role: this.role,
166
  n_questions: this.nQuestions,
167
- batch_size: this.batchSize,
168
- };
169
-
170
- console.log('Payload:', payload); // Add this line to log the payload
171
-
172
- this.api.start(payload).subscribe({
173
- next: (res: StartResponse) => {
174
- this.loading = false;
175
- this.sessionId = res.session_id;
176
- this.index = res.index;
177
- this.total = res.total;
178
- this.question = res.question;
179
- this.options = (res.options || []) as any;
180
- console.log("res", res);
181
- // Optional: inspect res.profile_used
182
- if (res.profile_used === false) {
183
- this.errorMsg =
184
- 'No profile found for this user and role. Please complete the profile first.';
185
- }
186
  },
187
  error: (err) => {
 
188
  this.loading = false;
189
- this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
190
  },
 
191
  });
192
  }
193
 
194
  submitAnswer(): void {
195
- if (!this.sessionId || !this.selectedColor) return;
196
  this.loading = true;
197
  this.errorMsg = '';
198
 
199
- this.api
200
- .next({ session_id: this.sessionId, selected_color: this.selectedColor })
201
  .subscribe({
202
- next: (res: NextResponse) => {
203
- this.loading = false;
204
-
205
- // Finished?
206
- if (res.done) {
207
- this.isDone = true;
208
- this.finalMix = (res.mix || {
209
- blue: 0,
210
- green: 0,
211
- red: 0,
212
- yellow: 0,
213
- }) as Mix;
214
- return;
215
- }
216
-
217
- // Continue
218
- this.index = res.index ?? this.index + 1;
219
- this.total = res.total ?? this.total;
220
- this.question = res.question || '';
221
- this.options = (res.options || []) as any;
222
- this.inProgressMix = (res.progress || null) as Mix;
223
- this.selectedColor = ''; // reset selection
224
  },
225
  error: (err) => {
 
226
  this.loading = false;
227
- this.errorMsg = err?.error?.error || 'Failed to fetch next question.';
228
  },
 
229
  });
230
  }
231
 
232
  restart(): void {
233
- this.isDone = false;
234
- this.finalMix = null;
235
- this.inProgressMix = null;
236
- this.selectedColor = '';
237
  this.sessionId = '';
238
  this.index = 0;
239
  this.total = 0;
240
  this.question = '';
241
  this.options = [];
 
 
242
  this.errorMsg = '';
 
 
 
243
  }
244
  }
 
1
+ import { Component, OnInit, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
2
  import { FormsModule } from '@angular/forms';
3
  import { CommonModule } from '@angular/common';
4
+ import { LlmQaService, Role, ColorKey, StartResponse, NextResponse, LlmOption } from '../services/llm-qa.service';
5
  import { ActivatedRoute, Router } from '@angular/router';
6
+ import { RouterLink } from '@angular/router';
7
  type Mix = Record<ColorKey, number>;
8
 
9
  @Component({
10
  selector: 'app-llm-quiz',
11
  standalone: true,
12
+ imports: [CommonModule, FormsModule, RouterLink],
13
  templateUrl: './llm-quiz.component.html',
14
  styleUrls: ['./llm-quiz.component.css'],
15
  })
16
+ export class LlmQuizComponent implements OnInit, AfterViewInit {
17
+ @ViewChild('brainBg') brainBg!: ElementRef;
18
+
19
  // --- Config bound to the UI ---
20
  role: Role = 'marriage';
21
+ nQuestions = 20;
22
+ batchSize = 10;
 
 
23
  userId = '';
24
 
25
  // --- Runtime state ---
26
  sessionId = '';
27
+ index = 0; // current question index (1‑based from backend)
28
+ total = 0; // total questions in session
 
29
  question = '';
30
+ options: LlmOption[] = [];
31
  selectedColor: ColorKey | '' = '';
32
 
33
  loading = false;
34
  errorMsg = '';
35
  isDone = false;
36
 
37
+ inProgressMix: Mix | null = null; // live progress (NextResponse.progress)
38
+ finalMix: Mix | null = null; // final mix (NextResponse.mix when done)
39
 
40
+ constructor(
41
+ private api: LlmQaService,
42
+ private route: ActivatedRoute,
43
+ private router: Router
44
+ ) { }
45
 
46
+ goHome(): void { this.router.navigate(['/']); }
 
 
 
 
47
 
48
+ ngOnInit(): void {
49
+ const qp = this.route.snapshot.queryParamMap;
50
+ const uid = (qp.get('user_id') || localStorage.getItem('user_id') || '').trim();
51
+ const role = (qp.get('role') || localStorage.getItem('role') || '').toLowerCase();
52
+ const autostart = qp.get('autostart') === '1';
53
 
54
+ if (uid) this.userId = uid;
55
+ if (role) this.role = role as any;
56
 
57
+ if (autostart && this.userId && this.role) {
58
+ this.start();
59
+ }
 
 
 
 
 
 
 
60
  }
61
 
62
+ ngAfterViewInit(): void { this.createNeuralNetwork(); }
63
+
64
+ // Decorative background (unchanged)
65
+ createNeuralNetwork(): void {
66
+ if (!this.brainBg) return;
67
+ const container = this.brainBg.nativeElement;
68
+ const neuronCount = 25;
69
+ const connectionCount = 40;
70
+ for (let i = 0; i < neuronCount; i++) {
71
+ const neuron = document.createElement('div');
72
+ neuron.className = 'neuron';
73
+ neuron.style.left = Math.random() * 100 + '%';
74
+ neuron.style.top = Math.random() * 100 + '%';
75
+ neuron.style.animationDelay = (Math.random() * 3) + 's';
76
+ container.appendChild(neuron);
77
+ }
78
+ for (let i = 0; i < connectionCount; i++) {
79
+ const connection = document.createElement('div');
80
+ connection.className = 'neuron-connection';
81
+ connection.style.left = Math.random() * 100 + '%';
82
+ connection.style.top = Math.random() * 100 + '%';
83
+ connection.style.width = (50 + Math.random() * 150) + 'px';
84
+ connection.style.transform = `rotate(${Math.random() * 360}deg)`;
85
+ connection.style.animationDelay = (Math.random() * 4) + 's';
86
+ container.appendChild(connection);
87
+ }
88
  }
89
 
90
+ // ---------- Helpers ----------
91
+ asPercent(v: number | undefined | null): number { const n = Number(v); return Number.isFinite(n) ? Math.round(n) : 0; }
92
+ getDimensionName(color: string): string {
93
+ switch (color) {
94
+ case 'blue': return 'Analytical';
95
+ case 'green': return 'Empathetic';
96
+ case 'red': return 'Assertive';
97
+ case 'yellow': return 'Creative';
98
+ default: return color;
99
+ }
100
  }
101
+ get quizIcon(): string {
102
+ switch (this.role) {
103
+ case 'interview': return 'fa-solid fa-briefcase';
104
+ case 'marriage': return 'fa-solid fa-heart';
105
+ case 'partnership': return 'fa-solid fa-handshake';
106
+ case 'team': return 'fa-solid fa-users';
107
+ case 'general': return 'fa-solid fa-user';
108
+ case 'assistant': return 'fa-solid fa-robot';
109
+ case 'ceo': return 'fa-solid fa-crown';
110
+ default: return 'fa-solid fa-brain';
111
  }
112
  }
113
+ get progressLabel(): string { return `${this.index} / ${this.total}`; }
114
+ get progressPercent(): number { return this.total ? Math.round((this.index / this.total) * 100) : 0; }
115
+ getInProgressMixValue(color: string): number { return this.inProgressMix?.[color as ColorKey] || 0; }
116
+ getFinalMixValue(color: string): number { return this.finalMix?.[color as ColorKey] || 0; }
117
+ get dominantColor(): ColorKey | null {
118
+ if (!this.finalMix) return null;
119
+ return (Object.entries(this.finalMix) as [ColorKey, number][])
120
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || null;
121
+ }
122
+ get dominantDescription(): string {
123
+ const color = this.dominantColor; if (!color) return '';
124
+ const descriptions: Record<ColorKey, string> = {
125
+ blue: 'Your thinking is primarily analytical and logical, focusing on data-driven decisions and systematic problem-solving.',
126
+ green: 'You lead with empathy and emotional intelligence, valuing relationships and harmony in your interactions.',
127
+ red: 'You approach situations with assertiveness and directness, prioritizing action and tangible results.',
128
+ yellow: 'Your cognitive style is creative and innovative, embracing new ideas and unconventional approaches.'
129
+ };
130
+ return descriptions[color];
131
+ }
132
 
133
+ // ---------- API calls (Observable based) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  start(): void {
135
+ if (!this.userId) { this.errorMsg = 'User ID required'; return; }
136
+ this.loading = true;
137
  this.errorMsg = '';
138
  this.isDone = false;
139
  this.finalMix = null;
140
  this.inProgressMix = null;
141
  this.selectedColor = '';
 
 
 
 
 
142
 
143
+ this.api.start({
144
+ user_id: this.userId,
 
 
 
 
 
 
 
 
145
  role: this.role,
146
  n_questions: this.nQuestions,
147
+ batch_size: this.batchSize
148
+ }).subscribe({
149
+ next: (resp: StartResponse) => {
150
+ this.sessionId = resp.session_id;
151
+ this.index = resp.index ?? 0;
152
+ this.total = resp.total ?? 0;
153
+ this.question = resp.question || '';
154
+ this.options = resp.options || [];
 
 
 
 
 
 
 
 
 
 
 
155
  },
156
  error: (err) => {
157
+ this.errorMsg = err?.error?.error || err.message || 'Failed to start quiz';
158
  this.loading = false;
 
159
  },
160
+ complete: () => { this.loading = false; }
161
  });
162
  }
163
 
164
  submitAnswer(): void {
165
+ if (!this.sessionId || !this.selectedColor || this.loading) return;
166
  this.loading = true;
167
  this.errorMsg = '';
168
 
169
+ this.api.next({ session_id: this.sessionId, selected_color: this.selectedColor as ColorKey })
 
170
  .subscribe({
171
+ next: (resp: NextResponse) => {
172
+ // Update progress
173
+ if (resp.index !== undefined) this.index = resp.index;
174
+ if (resp.total !== undefined) this.total = resp.total;
175
+ if (resp.progress) this.inProgressMix = resp.progress as Mix;
176
+
177
+ if (resp.done) {
178
+ this.isDone = true;
179
+ this.finalMix = (resp.mix || null) as Mix;
180
+ } else {
181
+ this.question = resp.question || '';
182
+ this.options = resp.options || [];
183
+ this.selectedColor = '';
184
+ }
 
 
 
 
 
 
 
 
185
  },
186
  error: (err) => {
187
+ this.errorMsg = err?.error?.error || err.message || 'Failed to submit answer';
188
  this.loading = false;
 
189
  },
190
+ complete: () => { this.loading = false; }
191
  });
192
  }
193
 
194
  restart(): void {
 
 
 
 
195
  this.sessionId = '';
196
  this.index = 0;
197
  this.total = 0;
198
  this.question = '';
199
  this.options = [];
200
+ this.selectedColor = '';
201
+ this.loading = false;
202
  this.errorMsg = '';
203
+ this.isDone = false;
204
+ this.inProgressMix = null;
205
+ this.finalMix = null;
206
  }
207
  }
src/app/matching-list/matching-list.component.css ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block;
3
+ height: 100vh;
4
+ background: #f5f7f9;
5
+ font-family: "Poppins", sans-serif;
6
+ margin: 0;
7
+ color: #333;
8
+ }
9
+
10
+ /* Loader overlay */
11
+ .loader-overlay {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ width: 100%;
16
+ height: 100%;
17
+ background: rgba(15, 19, 30, 0.95);
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ z-index: 2000;
22
+ }
23
+
24
+ .loader-container {
25
+ text-align: center;
26
+ }
27
+
28
+ .loader-gif {
29
+ width: 50%;
30
+ }
31
+
32
+ /* HEADER */
33
+ .header-bar {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ background: #fff;
38
+ padding: 15px 25px;
39
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
40
+ position: sticky;
41
+ top: 7vw;
42
+ z-index: 50;
43
+ }
44
+
45
+ .logo {
46
+ height: 40px;
47
+ margin-right: 10px;
48
+ }
49
+
50
+ .left-section {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 10px;
54
+ }
55
+
56
+ .search-section {
57
+ flex: 1;
58
+ text-align: center;
59
+ }
60
+
61
+ .search-section input,
62
+ .search-section select {
63
+ padding: 8px 12px;
64
+ border-radius: 6px;
65
+ border: 1px solid #ccc;
66
+ margin-right: 6px;
67
+ }
68
+
69
+ .filter-btn {
70
+ padding: 8px 14px;
71
+ border: none;
72
+ background: #00bfa6;
73
+ color: #fff;
74
+ border-radius: 6px;
75
+ cursor: pointer;
76
+ }
77
+
78
+ .like-section {
79
+ position: relative;
80
+ cursor: pointer;
81
+ padding: 8px 12px;
82
+ border-radius: 6px;
83
+ background: #f5f7f9;
84
+ }
85
+
86
+ .like-count {
87
+ margin-left: 5px;
88
+ font-weight: bold;
89
+ }
90
+
91
+ .liked-dropdown {
92
+ display: none;
93
+ position: absolute;
94
+ top: 100%;
95
+ right: 0;
96
+ background: white;
97
+ border: 1px solid #ccc;
98
+ border-radius: 6px;
99
+ padding: 10px;
100
+ min-width: 150px;
101
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
102
+ z-index: 100;
103
+ }
104
+
105
+ .liked-dropdown.show {
106
+ display: block;
107
+ }
108
+
109
+ .liked-dropdown ul {
110
+ list-style: none;
111
+ padding: 0;
112
+ margin: 0;
113
+ }
114
+
115
+ .liked-dropdown li {
116
+ padding: 5px 0;
117
+ border-bottom: 1px solid #eee;
118
+ }
119
+
120
+ .liked-dropdown li:last-child {
121
+ border-bottom: none;
122
+ }
123
+
124
+ /* MAIN LAYOUT */
125
+ .main-layout {
126
+ display: flex;
127
+ flex-direction: row;
128
+ height: calc(100vh - 70px);
129
+ position: relative;
130
+ top: 8vw;
131
+ }
132
+
133
+ .user-panel {
134
+ width: 260px;
135
+ background: #fff;
136
+ height: calc(100vh - 70px);
137
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
138
+ padding: 20px;
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ position: sticky;
143
+ top: 70px;
144
+ overflow-y: auto;
145
+ }
146
+
147
+ .user-photo {
148
+ width: 120px;
149
+ height: 120px;
150
+ border-radius: 50%;
151
+ object-fit: cover;
152
+ margin-bottom: 15px;
153
+ }
154
+
155
+ .user-name {
156
+ font-weight: bold;
157
+ font-size: 18px;
158
+ margin-bottom: 5px;
159
+ }
160
+
161
+ .user-city {
162
+ color: #666;
163
+ margin-bottom: 20px;
164
+ }
165
+
166
+ .user-stats {
167
+ width: 100%;
168
+ margin-bottom: 20px;
169
+ }
170
+
171
+ .user-stats div {
172
+ display: flex;
173
+ justify-content: space-between;
174
+ margin-bottom: 10px;
175
+ padding: 5px 0;
176
+ border-bottom: 1px solid #eee;
177
+ }
178
+
179
+ .user-stats .value {
180
+ font-weight: bold;
181
+ color: #00bfa6;
182
+ }
183
+
184
+ .edit-btn {
185
+ padding: 10px 20px;
186
+ border: none;
187
+ background: #f5f7f9;
188
+ border-radius: 6px;
189
+ cursor: pointer;
190
+ margin-bottom: 15px;
191
+ width: 100%;
192
+ }
193
+
194
+ .progress-bar-user {
195
+ width: 100%;
196
+ height: 8px;
197
+ border-radius: 6px;
198
+ background: #e5e5e5;
199
+ overflow: hidden;
200
+ margin-top: 10px;
201
+ }
202
+
203
+ .progress-fill {
204
+ height: 100%;
205
+ background: linear-gradient(90deg, #00bfa6, #2ecc71);
206
+ transition: width 0.3s ease;
207
+ }
208
+
209
+ .matching-container {
210
+ flex: 1;
211
+ padding: 20px;
212
+ overflow-y: auto;
213
+ }
214
+
215
+ .grid-view {
216
+ display: grid;
217
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
218
+ gap: 20px;
219
+ margin-top: 10px;
220
+ }
221
+
222
+ /* PROFILE CARD */
223
+ .profile-card {
224
+ background: #fff;
225
+ border-radius: 12px;
226
+ overflow: hidden;
227
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
228
+ transition: 0.3s;
229
+ cursor: pointer;
230
+ position: relative;
231
+ }
232
+
233
+ .profile-card:hover {
234
+ transform: translateY(-5px);
235
+ }
236
+
237
+ .image-wrapper {
238
+ position: relative;
239
+ height: 200px;
240
+ overflow: hidden;
241
+ }
242
+
243
+ .image-wrapper img {
244
+ width: 100%;
245
+ height: 100%;
246
+ object-fit: cover;
247
+ transition: 0.4s;
248
+ }
249
+
250
+ .image-wrapper img:hover {
251
+ transform: scale(1.05);
252
+ }
253
+
254
+ .online-dot {
255
+ position: absolute;
256
+ top: 10px;
257
+ left: 10px;
258
+ width: 10px;
259
+ height: 10px;
260
+ border-radius: 50%;
261
+ background: #0f0;
262
+ box-shadow: 0 0 6px #0f0;
263
+ }
264
+
265
+ .image-wrapper .badge {
266
+ position: absolute;
267
+ top: 10px;
268
+ right: 10px;
269
+ background: #00bfa6;
270
+ color: white;
271
+ padding: 4px 8px;
272
+ border-radius: 12px;
273
+ font-size: 12px;
274
+ font-weight: bold;
275
+ }
276
+
277
+ .profile-info {
278
+ padding: 15px;
279
+ }
280
+
281
+ .profile-info h3 {
282
+ margin: 0 0 5px 0;
283
+ font-size: 16px;
284
+ }
285
+
286
+ .profile-info p {
287
+ margin: 2px 0;
288
+ color: #666;
289
+ font-size: 14px;
290
+ }
291
+
292
+ .profession {
293
+ font-weight: 500;
294
+ color: #333 !important;
295
+ }
296
+
297
+ .progress-bar {
298
+ height: 6px;
299
+ background: #e5e5e5;
300
+ border-radius: 3px;
301
+ overflow: hidden;
302
+ margin: 10px 0;
303
+ }
304
+
305
+ .progress-bar-inner {
306
+ height: 100%;
307
+ background: linear-gradient(90deg, #00bfa6, #2ecc71);
308
+ transition: width 0.3s ease;
309
+ }
310
+
311
+ .actions {
312
+ display: flex;
313
+ justify-content: space-between;
314
+ margin-top: 10px;
315
+ }
316
+
317
+ .actions button {
318
+ border: none;
319
+ background: #f3f3f3;
320
+ border-radius: 6px;
321
+ padding: 8px 12px;
322
+ cursor: pointer;
323
+ transition: 0.2s;
324
+ flex: 1;
325
+ margin: 0 2px;
326
+ }
327
+
328
+ .actions button:hover,
329
+ .actions button.liked {
330
+ background: #00bfa6;
331
+ color: #fff;
332
+ }
333
+
334
+ /* MODAL */
335
+ .modal {
336
+ position: fixed;
337
+ top: 0;
338
+ left: 0;
339
+ width: 100%;
340
+ height: 100%;
341
+ background: rgba(0, 0, 0, 0.5);
342
+ display: none;
343
+ justify-content: center;
344
+ align-items: center;
345
+ z-index: 40;
346
+ }
347
+
348
+ .modal.show {
349
+ display: flex;
350
+ }
351
+
352
+ .modal-content {
353
+ background: #fff;
354
+ padding: 25px;
355
+ border-radius: 10px;
356
+ width: 450px;
357
+ max-height: 90vh;
358
+ overflow-y: auto;
359
+ text-align: center;
360
+ position: relative;
361
+ }
362
+
363
+ .ai-summary {
364
+ margin-top: 10px;
365
+ font-size: 14px;
366
+ color: #444;
367
+ text-align: left;
368
+ }
369
+
370
+ .close {
371
+ position: absolute;
372
+ right: 30px;
373
+ top: 20px;
374
+ font-size: 28px;
375
+ cursor: pointer;
376
+ color: #555;
377
+ }
378
+
379
+ /* Compatibility Report */
380
+ .compatibility-report {
381
+ background: #f8f9fa;
382
+ padding: 16px 20px;
383
+ border-radius: 10px;
384
+ font-size: 14px;
385
+ line-height: 1.6;
386
+ }
387
+
388
+ .quadratic-section h4 {
389
+ color: #007bff;
390
+ margin-bottom: 6px;
391
+ margin-top: 12px;
392
+ }
393
+
394
+ .quadratic-section p {
395
+ margin: 0 0 10px 0;
396
+ color: #333;
397
+ }
398
+
399
+ .quadratic-section ul {
400
+ padding-left: 20px;
401
+ }
402
+
403
+ .quadratic-section li {
404
+ margin-bottom: 4px;
405
+ line-height: 1.4;
406
+ }
407
+
408
+ /* CHAT WINDOW */
409
+ .chat-window {
410
+ position: fixed;
411
+ bottom: 20px;
412
+ right: 20px;
413
+ width: 350px;
414
+ height: 400px;
415
+ background: white;
416
+ border-radius: 10px;
417
+ box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
418
+ display: none;
419
+ flex-direction: column;
420
+ z-index: 100;
421
+ }
422
+
423
+ .chat-window.show {
424
+ display: flex;
425
+ }
426
+
427
+ .chat-header {
428
+ padding: 15px;
429
+ background: #00bfa6;
430
+ color: white;
431
+ border-radius: 10px 10px 0 0;
432
+ display: flex;
433
+ justify-content: space-between;
434
+ align-items: center;
435
+ }
436
+
437
+ .chat-body {
438
+ flex: 1;
439
+ padding: 15px;
440
+ overflow-y: auto;
441
+ display: flex;
442
+ flex-direction: column;
443
+ gap: 10px;
444
+ }
445
+
446
+ .chat-msg {
447
+ padding: 10px 15px;
448
+ border-radius: 18px;
449
+ max-width: 80%;
450
+ font-size: 14px;
451
+ position: relative;
452
+ }
453
+
454
+ .user-msg {
455
+ align-self: flex-end;
456
+ background: #00bfa6;
457
+ color: white;
458
+ border-bottom-right-radius: 4px;
459
+ }
460
+
461
+ .other-msg {
462
+ align-self: flex-start;
463
+ background: #f1f1f1;
464
+ color: #333;
465
+ border-bottom-left-radius: 4px;
466
+ }
467
+
468
+ .timestamp {
469
+ font-size: 10px;
470
+ opacity: 0.7;
471
+ margin-top: 4px;
472
+ }
473
+
474
+ .typing {
475
+ font-style: italic;
476
+ color: #666;
477
+ font-size: 12px;
478
+ align-self: flex-start;
479
+ }
480
+
481
+ .chat-input {
482
+ display: flex;
483
+ padding: 15px;
484
+ border-top: 1px solid #eee;
485
+ }
486
+
487
+ .chat-input input {
488
+ flex: 1;
489
+ padding: 10px;
490
+ border: 1px solid #ddd;
491
+ border-radius: 20px;
492
+ margin-right: 10px;
493
+ }
494
+
495
+ .chat-input button {
496
+ padding: 10px 20px;
497
+ border: none;
498
+ background: #00bfa6;
499
+ color: white;
500
+ border-radius: 20px;
501
+ cursor: pointer;
502
+ }
503
+
504
+ /* Skeleton Loader */
505
+ .skeleton {
506
+ background: #e0e0e0;
507
+ border-radius: 12px;
508
+ height: 300px;
509
+ animation: pulse 1.5s ease-in-out infinite;
510
+ }
511
+
512
+ @keyframes pulse {
513
+ 0% {
514
+ opacity: 1;
515
+ }
516
+
517
+ 50% {
518
+ opacity: 0.5;
519
+ }
520
+
521
+ 100% {
522
+ opacity: 1;
523
+ }
524
+ }
525
+
526
+ /* Responsive Design */
527
+ @media (max-width: 768px) {
528
+ .main-layout {
529
+ flex-direction: column;
530
+ }
531
+
532
+ .user-panel {
533
+ width: 100%;
534
+ height: auto;
535
+ position: static;
536
+ }
537
+
538
+ .grid-view {
539
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
540
+ }
541
+
542
+ .header-bar {
543
+ flex-direction: column;
544
+ gap: 10px;
545
+ }
546
+
547
+ .search-section {
548
+ width: 100%;
549
+ display: flex;
550
+ flex-wrap: wrap;
551
+ gap: 5px;
552
+ }
553
+
554
+ .search-section input,
555
+ .search-section select {
556
+ flex: 1;
557
+ min-width: 120px;
558
+ }
559
+
560
+ .modal-content {
561
+ width: 90%;
562
+ margin: 20px;
563
+ }
564
+
565
+ .chat-window {
566
+ width: 300px;
567
+ height: 350px;
568
+ }
569
+ }
570
+
571
+ @media (max-width: 480px) {
572
+ .grid-view {
573
+ grid-template-columns: 1fr;
574
+ }
575
+
576
+ .search-section input,
577
+ .search-section select {
578
+ min-width: 100px;
579
+ }
580
+
581
+ .chat-window {
582
+ width: 280px;
583
+ right: 10px;
584
+ bottom: 10px;
585
+ }
586
+ }
src/app/matching-list/matching-list.component.html ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Loader Overlay -->
2
+ <div class="loader-overlay" *ngIf="loading">
3
+ <div class="loader-container">
4
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
5
+ </div>
6
+ </div>
7
+
8
+ <!-- Main Content -->
9
+ <div *ngIf="!loading" class="main-container">
10
+ <!-- HEADER -->
11
+ <div class="header-bar">
12
+ <div class="left-section">
13
+ <img src="https://cdn-icons-png.flaticon.com/512/168/168724.png" class="logo" alt="logo">
14
+ <h2>Py-Match</h2>
15
+ </div>
16
+ <div class="search-section">
17
+ <input type="text" id="searchBox" placeholder="Search name or city"
18
+ [(ngModel)]="searchTerm" (input)="onSearchChange()">
19
+ <select id="filterLocation" (change)="onLocationFilterChange($event)">
20
+ <option value="">All Locations</option>
21
+ <option *ngFor="let location of availableLocations" [value]="location">{{ location }}</option>
22
+ </select>
23
+ <select id="filterAge" (change)="onAgeFilterChange($event)">
24
+ <option value="">All Ages</option>
25
+ <option *ngFor="let ageRange of availableAgeRanges" [value]="ageRange">{{ ageRange }}</option>
26
+ </select>
27
+ <select id="filterProfession" (change)="onProfessionFilterChange($event)">
28
+ <option value="">All Professions</option>
29
+ <option *ngFor="let profession of availableProfessions" [value]="profession">{{ profession }}</option>
30
+ </select>
31
+ <select id="scoreFilter" (change)="onScoreFilterChange($event)">
32
+ <option value="0">All Scores</option>
33
+ <option value="80">80%+</option>
34
+ <option value="90">90%+</option>
35
+ </select>
36
+ <select id="sortBy" (change)="onSortChange($event)">
37
+ <option value="compatibility">Top Compatibility</option>
38
+ <option value="newest">Newest</option>
39
+ <option value="location">Closest Location</option>
40
+ </select>
41
+ <button class="filter-btn" (click)="applyFilters()">Apply</button>
42
+ </div>
43
+ <div class="like-section" (click)="toggleLikedDropdown()">
44
+ ❤️
45
+ <span class="like-count">{{ likedProfiles.length }}</span>
46
+ <div class="liked-dropdown" [class.show]="showLikedDropdown">
47
+ <ul>
48
+ <li *ngFor="let profile of likedProfiles">{{ profile.name }}</li>
49
+ <li *ngIf="likedProfiles.length === 0">No liked profiles</li>
50
+ </ul>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- MAIN LAYOUT -->
56
+ <div class="main-layout">
57
+ <!-- LEFT USER PANEL -->
58
+ <div class="user-panel">
59
+ <img [src]="userPhoto" class="user-photo" alt="User Photo">
60
+ <div class="user-name">{{ userName }}</div>
61
+ <div class="user-city">{{ userCity }}</div>
62
+ <div class="user-stats">
63
+ <div><span>Matches</span><span class="value">{{ totalMatches }}</span></div>
64
+ <div><span>Likes</span><span class="value">{{ likedProfiles.length }}</span></div>
65
+ <div><span>Compatibility</span><span class="value">87%</span></div>
66
+ </div>
67
+ <button class="edit-btn" (click)="openEditModal()">⚙️ Edit Profile</button>
68
+ <div class="progress-bar-user">
69
+ <div class="progress-fill" [style.width]="userProfileCompletion + '%'"></div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- RIGHT MATCHING CONTAINER -->
74
+ <div class="matching-container">
75
+ <div class="grid-view" id="profilesContainer">
76
+ <!-- Skeleton Loaders -->
77
+ <div class="skeleton" *ngIf="displayedMatches.length === 0 && !loading"></div>
78
+ <div class="skeleton" *ngIf="displayedMatches.length === 0 && !loading"></div>
79
+ <div class="skeleton" *ngIf="displayedMatches.length === 0 && !loading"></div>
80
+
81
+ <!-- Profile Cards -->
82
+ <div *ngFor="let profile of displayedMatches"
83
+ class="profile-card"
84
+ (click)="openProfileModal(profile)">
85
+ <div class="image-wrapper">
86
+ <div class="online-dot" *ngIf="profile.isOnline"></div>
87
+ <img [src]="profile.photo_url" [alt]="profile.name"
88
+ (error)="handleImageError($event, profile)">
89
+ <span class="badge">{{ profile.match_score || profile.score }}%</span>
90
+ </div>
91
+ <div class="profile-info">
92
+ <h3>{{ profile.name }}</h3>
93
+ <p>{{ profile.age }} • {{ profile.city }}</p>
94
+ <p class="profession">{{ profile.job }}</p>
95
+ <div class="progress-bar">
96
+ <div class="progress-bar-inner" [style.width]="(profile.match_score || profile.score) + '%'"></div>
97
+ </div>
98
+ <div class="actions">
99
+ <button [class.liked]="isLiked(profile)"
100
+ (click)="$event.stopPropagation(); toggleLike(profile)">
101
+ ❤️
102
+ </button>
103
+ <button (click)="$event.stopPropagation(); openChat(profile)">
104
+ 💬
105
+ </button>
106
+ <button (click)="$event.stopPropagation(); openModal(profile)">
107
+ 👁️
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- PROFILE MODAL -->
117
+ <div class="modal" [class.show]="showProfileModal">
118
+ <div class="modal-content" *ngIf="selectedProfile">
119
+ <span class="close" (click)="closeModal()">&times;</span>
120
+ <img [src]="selectedProfile.photo_url" [alt]="selectedProfile.name"
121
+ (error)="handleImageError($event, selectedProfile)">
122
+ <h3>{{ selectedProfile.name }} ({{ selectedProfile.gender || 'Male' }})</h3>
123
+ <p>{{ selectedProfile.age }} • {{ selectedProfile.city }}</p>
124
+ <p>{{ selectedProfile.job }}</p>
125
+ <p>Overall Match: {{ selectedProfile.match_score || selectedProfile.score }}%</p>
126
+
127
+ <div class="ai-summary" [innerHTML]="getCompatibilityReport(selectedProfile)"></div>
128
+
129
+ <canvas id="aiChart" style="display: none;"></canvas>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- EDIT PROFILE MODAL -->
134
+ <div class="modal" [class.show]="showEditModal">
135
+ <div class="modal-content">
136
+ <span class="close" (click)="closeEditModal()">&times;</span>
137
+ <h3>Edit Profile</h3>
138
+ <input [(ngModel)]="editName" placeholder="Name" style="margin:6px;padding:6px;width:90%">
139
+ <input [(ngModel)]="editCity" placeholder="City" style="margin:6px;padding:6px;width:90%">
140
+ <button class="filter-btn" (click)="saveProfile()">Save</button>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- CHAT WINDOW -->
145
+ <div class="chat-window" [class.show]="showChatWindow">
146
+ <div class="chat-header">
147
+ <span>Chat with {{ currentChatUser?.name }}</span>
148
+ <span style="cursor:pointer" (click)="closeChat()">✖</span>
149
+ </div>
150
+ <div class="chat-body" #chatBody>
151
+ <div *ngFor="let message of chatMessages"
152
+ class="chat-msg"
153
+ [class.user-msg]="message.sender === 'me'"
154
+ [class.other-msg]="message.sender === 'them'">
155
+ {{ message.text }}
156
+ <div class="timestamp">{{ message.time }}</div>
157
+ </div>
158
+ <div *ngIf="showTyping" class="typing" id="typingMsg">Typing...</div>
159
+ </div>
160
+ <div class="chat-input">
161
+ <input type="text"
162
+ [(ngModel)]="newMessage"
163
+ placeholder="Type a message..."
164
+ (keyup.enter)="sendMessage()"
165
+ (keypress)="typingIndicator()">
166
+ <button (click)="sendMessage()">Send</button>
167
+ </div>
168
+ </div>
169
+ </div>
src/app/{quiz/quiz.component.spec.ts → matching-list/matching-list.component.spec.ts} RENAMED
@@ -1,16 +1,16 @@
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
 
3
- import { QuizComponent } from './quiz.component';
4
 
5
- describe('QuizComponent', () => {
6
- let component: QuizComponent;
7
- let fixture: ComponentFixture<QuizComponent>;
8
 
9
  beforeEach(() => {
10
  TestBed.configureTestingModule({
11
- declarations: [QuizComponent]
12
  });
13
- fixture = TestBed.createComponent(QuizComponent);
14
  component = fixture.componentInstance;
15
  fixture.detectChanges();
16
  });
 
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
 
3
+ import { MatchingListComponent } from './matching-list.component';
4
 
5
+ describe('MatchingListComponent', () => {
6
+ let component: MatchingListComponent;
7
+ let fixture: ComponentFixture<MatchingListComponent>;
8
 
9
  beforeEach(() => {
10
  TestBed.configureTestingModule({
11
+ imports: [MatchingListComponent]
12
  });
13
+ fixture = TestBed.createComponent(MatchingListComponent);
14
  component = fixture.componentInstance;
15
  fixture.detectChanges();
16
  });
src/app/matching-list/matching-list.component.ts ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit, ElementRef, ViewChild, HostListener } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatchingService, ApiMatchItem, ApiMatchResponse } from '../matching.service';
5
+ import { CommonModule } from '@angular/common';
6
+
7
+ interface Profile extends ApiMatchItem {
8
+ id?: number;
9
+ name?: string;
10
+ age?: number | null;
11
+ city?: string | null;
12
+ job?: string | null;
13
+ isOnline?: boolean;
14
+ match_score?: number;
15
+ photo_url?: string;
16
+ blue: number;
17
+ green: number;
18
+ yellow: number;
19
+ red: number;
20
+ score: number;
21
+ user_id: number;
22
+ gender?: string;
23
+ profession?: string;
24
+ meetsExpectations?: boolean;
25
+ expectationMatchScore?: number;
26
+ aiExplanation?: string;
27
+ hobbies?: string[];
28
+ education?: string;
29
+ location?: string;
30
+ }
31
+
32
+ interface ChatMessage {
33
+ text: string;
34
+ sender: 'me' | 'them';
35
+ time: string;
36
+ }
37
+
38
+ @Component({
39
+ selector: 'app-matching-list',
40
+ templateUrl: './matching-list.component.html',
41
+ styleUrls: ['./matching-list.component.css'],
42
+ standalone: true,
43
+ imports: [FormsModule, CommonModule]
44
+ })
45
+ export class MatchingListComponent implements OnInit {
46
+ @ViewChild('chatBody') chatBody!: ElementRef;
47
+
48
+ loading = false;
49
+ error: string | null = null;
50
+
51
+ // Modal states
52
+ showProfileModal = false;
53
+ showEditModal = false;
54
+ showChatWindow = false;
55
+ showLikedDropdown = false;
56
+ showTyping = false;
57
+
58
+ // User data
59
+ userId: number = 1;
60
+ userPhoto = 'https://randomuser.me/api/portraits/men/15.jpg';
61
+ userName = 'Ravi Kumar';
62
+ userCity = 'Chennai, India';
63
+ userProfileCompletion = 80;
64
+
65
+ // Edit profile
66
+ editName = '';
67
+ editCity = '';
68
+
69
+ // Filter state
70
+ searchTerm = '';
71
+ locationFilter = '';
72
+ ageFilter = '';
73
+ professionFilter = '';
74
+ scoreFilter = 0;
75
+ sortBy = 'compatibility';
76
+
77
+ // Data
78
+ allMatches: Profile[] = [];
79
+ filteredMatches: Profile[] = [];
80
+ selectedProfile: Profile | null = null;
81
+ currentChatUser: Profile | null = null;
82
+
83
+ // Chat state
84
+ chatMessages: ChatMessage[] = [];
85
+ newMessage = '';
86
+ typingTimeout: any = null;
87
+
88
+ // Stats
89
+ totalMatches = 0;
90
+
91
+ // Available filters
92
+ availableLocations: string[] = ['Chennai', 'Bengaluru', 'Hyderabad', 'Mumbai', 'Delhi'];
93
+ availableAgeRanges: string[] = ['18-25', '26-30', '31-35', '36-40', '41+'];
94
+ availableProfessions: string[] = ['Software Engineer', 'Doctor', 'Teacher', 'Business', 'Student', 'Architect', 'Data Scientist', 'Marketing Analyst'];
95
+
96
+ // Like functionality
97
+ likedProfiles: Profile[] = [];
98
+
99
+ constructor(
100
+ private router: Router,
101
+ private matchingService: MatchingService
102
+ ) { }
103
+
104
+ async ngOnInit() {
105
+ this.loadMatches();
106
+ }
107
+
108
+ get displayedMatches(): Profile[] {
109
+ return this.filteredMatches;
110
+ }
111
+
112
+ async loadMatches() {
113
+ this.loading = true;
114
+ this.error = null;
115
+
116
+ try {
117
+ this.matchingService.getMatches(this.userId, {
118
+ role: 'marriage',
119
+ limit: 50,
120
+ excludeSelf: true
121
+ }).subscribe({
122
+ next: (response: ApiMatchResponse) => {
123
+ this.allMatches = response.matches.map((match: ApiMatchItem) => ({
124
+ ...match,
125
+ id: match.user_id,
126
+ user_id: match.user_id,
127
+ name: match.name || `User ${match.user_id}`,
128
+ match_score: match.score,
129
+ age: match.age || Math.floor(Math.random() * 15) + 25,
130
+ city: match.city || this.availableLocations[Math.floor(Math.random() * this.availableLocations.length)],
131
+ job: match.role || this.availableProfessions[Math.floor(Math.random() * this.availableProfessions.length)],
132
+ profession: match.role || this.availableProfessions[Math.floor(Math.random() * this.availableProfessions.length)],
133
+ isOnline: Math.random() > 0.5,
134
+ photo_url: match.photo_url || `https://randomuser.me/api/portraits/${Math.random() > 0.5 ? 'men' : 'women'}/${Math.floor(Math.random() * 50)}.jpg`,
135
+ gender: Math.random() > 0.5 ? 'Male' : 'Female',
136
+ meetsExpectations: false,
137
+ expectationMatchScore: 0,
138
+ aiExplanation: this.generateAIExplanation(match),
139
+ hobbies: ['Reading', 'Travel', 'Music'].slice(0, Math.floor(Math.random() * 3) + 1),
140
+ education: ['Bachelor', 'Master', 'PhD'][Math.floor(Math.random() * 3)],
141
+ location: match.city || 'Unknown'
142
+ }));
143
+
144
+ this.applyFilters();
145
+ this.totalMatches = this.allMatches.length;
146
+ this.loading = false;
147
+ },
148
+ error: (err: any) => {
149
+ console.error('Error loading matches:', err);
150
+ this.error = 'Failed to load matches. Please try again.';
151
+ this.loading = false;
152
+ }
153
+ });
154
+
155
+ } catch (err) {
156
+ this.error = 'Failed to load matches. Please try again.';
157
+ this.loading = false;
158
+ }
159
+ }
160
+
161
+ generateAIExplanation(profile: ApiMatchItem): string {
162
+ const traits = [];
163
+ if (profile.blue > 60) traits.push('analytical thinking');
164
+ if (profile.green > 60) traits.push('empathetic nature');
165
+ if (profile.yellow > 60) traits.push('creative approach');
166
+ if (profile.red > 60) traits.push('assertive communication');
167
+
168
+ if (traits.length > 0) {
169
+ return `Strong compatibility in ${traits.join(' and ')} based on personality assessment.`;
170
+ }
171
+
172
+ return 'Good overall compatibility with balanced personality traits.';
173
+ }
174
+
175
+ applyFilters() {
176
+ let filtered = [...this.allMatches];
177
+
178
+ // Apply search filter
179
+ if (this.searchTerm) {
180
+ const term = this.searchTerm.toLowerCase();
181
+ filtered = filtered.filter(profile =>
182
+ profile.name?.toLowerCase().includes(term) ||
183
+ profile.city?.toLowerCase().includes(term) ||
184
+ profile.job?.toLowerCase().includes(term) ||
185
+ profile.hobbies?.some(hobby => hobby.toLowerCase().includes(term))
186
+ );
187
+ }
188
+
189
+ // Apply location filter
190
+ if (this.locationFilter) {
191
+ filtered = filtered.filter(profile =>
192
+ profile.city?.toLowerCase().includes(this.locationFilter.toLowerCase())
193
+ );
194
+ }
195
+
196
+ // Apply age filter
197
+ if (this.ageFilter) {
198
+ filtered = filtered.filter(profile => {
199
+ if (!profile.age) return false;
200
+ return this.doesAgeMatch(profile.age, this.ageFilter);
201
+ });
202
+ }
203
+
204
+ // Apply profession filter
205
+ if (this.professionFilter) {
206
+ filtered = filtered.filter(profile =>
207
+ profile.profession?.toLowerCase().includes(this.professionFilter.toLowerCase())
208
+ );
209
+ }
210
+
211
+ // Apply score filter
212
+ if (this.scoreFilter > 0) {
213
+ filtered = filtered.filter(profile =>
214
+ (profile.match_score || profile.score) >= this.scoreFilter
215
+ );
216
+ }
217
+
218
+ // Apply sorting
219
+ filtered.sort((a, b) => {
220
+ switch (this.sortBy) {
221
+ case 'compatibility':
222
+ return (b.match_score || b.score) - (a.match_score || a.score);
223
+ case 'newest':
224
+ return Math.random() - 0.5; // Random for demo
225
+ case 'location':
226
+ return a.city?.localeCompare(b.city || '') || 0;
227
+ default:
228
+ return 0;
229
+ }
230
+ });
231
+
232
+ this.filteredMatches = filtered;
233
+ }
234
+
235
+ doesAgeMatch(age: number, ageRange: string): boolean {
236
+ const ranges: { [key: string]: [number, number] } = {
237
+ '18-25': [18, 25],
238
+ '26-30': [26, 30],
239
+ '31-35': [31, 35],
240
+ '36-40': [36, 40],
241
+ '41+': [41, 100]
242
+ };
243
+
244
+ const range = ranges[ageRange];
245
+ return range ? age >= range[0] && age <= range[1] : false;
246
+ }
247
+
248
+ // Filter handlers
249
+ onSearchChange() {
250
+ this.applyFilters();
251
+ }
252
+
253
+ onLocationFilterChange(event: any) {
254
+ this.locationFilter = event.target.value;
255
+ this.applyFilters();
256
+ }
257
+
258
+ onAgeFilterChange(event: any) {
259
+ this.ageFilter = event.target.value;
260
+ this.applyFilters();
261
+ }
262
+
263
+ onProfessionFilterChange(event: any) {
264
+ this.professionFilter = event.target.value;
265
+ this.applyFilters();
266
+ }
267
+
268
+ onScoreFilterChange(event: any) {
269
+ this.scoreFilter = parseInt(event.target.value);
270
+ this.applyFilters();
271
+ }
272
+
273
+ onSortChange(event: any) {
274
+ this.sortBy = event.target.value;
275
+ this.applyFilters();
276
+ }
277
+
278
+ // Profile actions
279
+ openModal(profile: Profile) {
280
+ this.selectedProfile = profile;
281
+ this.showProfileModal = true;
282
+ }
283
+
284
+ closeModal() {
285
+ this.showProfileModal = false;
286
+ this.selectedProfile = null;
287
+ }
288
+
289
+ openProfileModal(profile: Profile) {
290
+ this.selectedProfile = profile;
291
+ this.showProfileModal = true;
292
+ }
293
+
294
+ toggleLike(profile: Profile) {
295
+ const index = this.likedProfiles.findIndex(p => p.user_id === profile.user_id);
296
+
297
+ if (index > -1) {
298
+ this.likedProfiles.splice(index, 1);
299
+ } else {
300
+ this.likedProfiles.push(profile);
301
+ this.triggerConfetti();
302
+ }
303
+ }
304
+
305
+ isLiked(profile: Profile): boolean {
306
+ return this.likedProfiles.some(p => p.user_id === profile.user_id);
307
+ }
308
+
309
+ triggerConfetti() {
310
+ // Simple confetti effect - you can enhance this with a proper confetti library
311
+ console.log('Confetti effect triggered!');
312
+ }
313
+
314
+ toggleLikedDropdown() {
315
+ this.showLikedDropdown = !this.showLikedDropdown;
316
+ }
317
+
318
+ // Edit profile
319
+ openEditModal() {
320
+ this.editName = this.userName;
321
+ this.editCity = this.userCity.split(', ')[0]; // Remove country for edit
322
+ this.showEditModal = true;
323
+ }
324
+
325
+ closeEditModal() {
326
+ this.showEditModal = false;
327
+ }
328
+
329
+ saveProfile() {
330
+ this.userName = this.editName;
331
+ this.userCity = this.editCity + ', India';
332
+ this.userProfileCompletion = 100;
333
+ this.showEditModal = false;
334
+ }
335
+
336
+ // Chat functionality
337
+ openChat(profile: Profile) {
338
+ this.currentChatUser = profile;
339
+ this.showChatWindow = true;
340
+ this.loadChat(profile);
341
+ setTimeout(() => {
342
+ this.scrollChatToBottom();
343
+ }, 100);
344
+ }
345
+
346
+ closeChat() {
347
+ this.showChatWindow = false;
348
+ this.currentChatUser = null;
349
+ this.chatMessages = [];
350
+ }
351
+
352
+ typingIndicator() {
353
+ this.showTyping = true;
354
+ if (this.typingTimeout) {
355
+ clearTimeout(this.typingTimeout);
356
+ }
357
+ this.typingTimeout = setTimeout(() => {
358
+ this.showTyping = false;
359
+ }, 1500);
360
+ }
361
+
362
+ sendMessage() {
363
+ const text = this.newMessage.trim();
364
+ if (!text || !this.currentChatUser) return;
365
+
366
+ const message: ChatMessage = {
367
+ text,
368
+ sender: 'me',
369
+ time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
370
+ };
371
+
372
+ this.chatMessages.push(message);
373
+ this.saveMessage(this.currentChatUser, message);
374
+ this.newMessage = '';
375
+ this.scrollChatToBottom();
376
+
377
+ // Simulate reply after 1 second
378
+ setTimeout(() => {
379
+ const reply: ChatMessage = {
380
+ text: `${this.currentChatUser?.name}: Nice to hear from you!`,
381
+ sender: 'them',
382
+ time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
383
+ };
384
+ this.chatMessages.push(reply);
385
+ this.saveMessage(this.currentChatUser!, reply);
386
+ this.scrollChatToBottom();
387
+ }, 1000);
388
+ }
389
+
390
+ saveMessage(user: Profile, message: ChatMessage) {
391
+ const key = `chat_${user.user_id}`;
392
+ let arr = JSON.parse(localStorage.getItem(key) || '[]');
393
+ arr.push(message);
394
+ localStorage.setItem(key, JSON.stringify(arr));
395
+ }
396
+
397
+ loadChat(user: Profile) {
398
+ const key = `chat_${user.user_id}`;
399
+ const arr = JSON.parse(localStorage.getItem(key) || '[]');
400
+ this.chatMessages = arr;
401
+ }
402
+
403
+ scrollChatToBottom() {
404
+ if (this.chatBody) {
405
+ this.chatBody.nativeElement.scrollTop = this.chatBody.nativeElement.scrollHeight;
406
+ }
407
+ }
408
+
409
+ // Compatibility report
410
+ getCompatibilityReport(profile: Profile): string {
411
+ const colorSet = {
412
+ B: Math.floor(Math.random() * 40),
413
+ G: Math.floor(Math.random() * 40),
414
+ Y: Math.floor(Math.random() * 40),
415
+ R: Math.floor(Math.random() * 40),
416
+ };
417
+ const colorMatch = (Math.random() * (0.9 - 0.6) + 0.6).toFixed(2);
418
+ const expectationMatch = (Math.random() * (0.9 - 0.6) + 0.6).toFixed(2);
419
+
420
+ const differences = [
421
+ "Their religion or faith expectations differ (same faith vs no preference).",
422
+ "Both share similar preferences in smoking preference (Occasionally).",
423
+ "Their alcohol preference expectations differ (no preference vs never).",
424
+ "Their dietary habits expectations differ (vegetarian vs no preference).",
425
+ "Both share similar preferences in family type (Either).",
426
+ "Their fitness lifestyle expectations differ (high vs moderate).",
427
+ "Their preferred location expectations differ (pune vs kolkata).",
428
+ "Their preferred country expectations differ (other vs australia).",
429
+ ];
430
+
431
+ return `
432
+ <div class="compatibility-report">
433
+ <p><strong>${profile.name}</strong> (${profile.gender || 'Male'})</p>
434
+ <p><strong>Overall Match:</strong> ${profile.match_score || profile.score}%</p>
435
+ <p><strong>Colors:</strong> B${colorSet.B} • G${colorSet.G} • Y${colorSet.Y} • R${colorSet.R}</p>
436
+ <p><strong>Color Match:</strong> ${colorMatch} • <strong>Expectation Match:</strong> ${expectationMatch}</p>
437
+ <br>
438
+ <div class="quadratic-section">
439
+ <h4>1️⃣ How this profile matches to you</h4>
440
+ <p>${profile.name} shows high compatibility in emotional stability, career orientation, and communication approach. You both value similar ethics, decision-making styles, and emotional awareness.</p>
441
+
442
+ <h4>2️⃣ Why the remaining percentage does not match</h4>
443
+ <p>The mismatch comes mainly from lifestyle and belief areas. Factors like faith flexibility, preferred diet, and regional lifestyle contribute to the gap.</p>
444
+
445
+ <h4>3️⃣ What you may need to sacrifice</h4>
446
+ <p>For smoother compatibility, consider being more adaptive toward dietary and cultural choices, and show patience toward different viewpoints in lifestyle preferences.</p>
447
+
448
+ <h4>4️⃣ Expectation-wise Comparison</h4>
449
+ <ul>
450
+ ${differences
451
+ .map(
452
+ (d) =>
453
+ `<li style="color:${d.includes("similar") ? "green" : "red"
454
+ }">${d}</li>`
455
+ )
456
+ .join("")}
457
+ </ul>
458
+ </div>
459
+ </div>`;
460
+ }
461
+
462
+ // Utility functions
463
+ handleImageError(event: any, profile: Profile) {
464
+ event.target.src = 'https://via.placeholder.com/300?text=' + (profile.name || 'User');
465
+ }
466
+
467
+ @HostListener('document:click', ['$event'])
468
+ handleDocumentClick(event: MouseEvent) {
469
+ const target = event.target as HTMLElement;
470
+ if (!target.closest('.like-section')) {
471
+ this.showLikedDropdown = false;
472
+ }
473
+ }
474
+
475
+ @HostListener('document:keydown.escape')
476
+ handleEscape() {
477
+ this.closeModal();
478
+ this.closeEditModal();
479
+ this.closeChat();
480
+ }
481
+ }
src/app/matching.service.spec.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient, HttpParams } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+
5
+ export interface ApiMatchItem {
6
+ user_id: number;
7
+ role?: string | null;
8
+ name?: string;
9
+ gender?: string;
10
+ blue: number;
11
+ green: number;
12
+ yellow: number;
13
+ red: number;
14
+ score: number;
15
+ created_at?: string | null;
16
+ explanations?: string[];
17
+ // Optional display fields that may not come from API but used in templates
18
+ city?: string | null;
19
+ age?: number | null;
20
+ job?: string | null;
21
+ isOnline?: boolean; // optional presence indicator for UI
22
+ photo_url?: string; // optional photo URL for profile image
23
+ }
24
+
25
+ export interface ApiMatchResponse {
26
+ input_user: {
27
+ user_id: number;
28
+ role?: string | null;
29
+ blue: number;
30
+ green: number;
31
+ yellow: number;
32
+ red: number;
33
+ created_at?: string | null;
34
+ };
35
+ matches: ApiMatchItem[];
36
+ count: number;
37
+ }
38
+
39
+ @Injectable({ providedIn: 'root' })
40
+ export class MatchingService {
41
+ private readonly base = 'http://127.0.0.1:5000'; // Flask backend
42
+
43
+ constructor(private http: HttpClient) { }
44
+
45
+ getMatches(
46
+ userId: number,
47
+ opts?: { role?: string; limit?: number; excludeSelf?: boolean }
48
+ ): Observable<ApiMatchResponse> {
49
+ let params = new HttpParams()
50
+ .set('user_id', String(userId))
51
+ .set('limit', String(opts?.limit ?? 10))
52
+ .set('exclude_self', (opts?.excludeSelf ?? true) ? 'yes' : 'no');
53
+
54
+ // 👇 Default to "marriage" if role is not explicitly passed
55
+ params = params.set('role', opts?.role ?? 'marriage');
56
+
57
+ return this.http.get<ApiMatchResponse>(`${this.base}/match`, { params });
58
+ }
59
+ }
src/app/matching.service.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient, HttpParams } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+
5
+ export interface ApiMatchItem {
6
+ user_id: number;
7
+ role?: string | null;
8
+ name?: string;
9
+ gender?: string;
10
+ blue: number;
11
+ green: number;
12
+ yellow: number;
13
+ red: number;
14
+ score: number;
15
+ created_at?: string | null;
16
+ explanations?: string[];
17
+ // Optional display fields that may not come from API but used in templates
18
+ city?: string | null;
19
+ age?: number | null;
20
+ job?: string | null;
21
+ isOnline?: boolean; // optional presence indicator for UI
22
+ photo_url?: string; // optional photo URL for profile image
23
+ }
24
+
25
+ export interface ApiMatchResponse {
26
+ input_user: {
27
+ user_id: number;
28
+ role?: string | null;
29
+ blue: number;
30
+ green: number;
31
+ yellow: number;
32
+ red: number;
33
+ created_at?: string | null;
34
+ };
35
+ matches: ApiMatchItem[];
36
+ count: number;
37
+ }
38
+
39
+ @Injectable({ providedIn: 'root' })
40
+ export class MatchingService {
41
+
42
+ private baseUrl: string;
43
+
44
+ constructor(private http: HttpClient) {
45
+ // Initialize baseUrl based on the current hostname
46
+ this.baseUrl = typeof location !== 'undefined' && location.hostname.endsWith('hf.space')
47
+ ? 'https://pykara-py-match-backend.hf.space/api'
48
+ : 'http://localhost:5000/api';
49
+ }
50
+
51
+ getMatches(
52
+ userId: number,
53
+ opts?: { role?: string; limit?: number; excludeSelf?: boolean }
54
+ ): Observable<ApiMatchResponse> {
55
+ let params = new HttpParams()
56
+ .set('user_id', String(userId))
57
+ .set('limit', String(opts?.limit ?? 10))
58
+ .set('exclude_self', (opts?.excludeSelf ?? true) ? 'yes' : 'no');
59
+
60
+ // 👇 Default to "marriage" if role is not explicitly passed
61
+ params = params.set('role', opts?.role ?? 'marriage');
62
+
63
+ return this.http.get<ApiMatchResponse>(`${this.baseUrl}/match`, { params });
64
+ }
65
+ }
src/app/question-answer/question-answer-service.service.ts CHANGED
@@ -2,35 +2,41 @@ import { Injectable } from '@angular/core';
2
  import { HttpClient } from '@angular/common/http';
3
  import { environment } from '../../environments/environment';
4
  import { Observable } from 'rxjs';
 
5
  export interface QAItem {
6
  label: string;
7
- input_type: 'text' | 'date' | 'radio' | 'select';
8
  options?: string[];
9
  column_key: string;
 
 
 
10
  }
 
11
  @Injectable({ providedIn: 'root' })
12
  export class QuestionAnswerService {
13
- // hf.space => HF backend; else => local Flask
14
- private baseUrl = location.hostname.endsWith('hf.space')
15
- ? 'https://pykara-py-match-backend.hf.space/api/questions'
16
- : 'http://localhost:5000/api/questions';
17
 
18
- //private baseUrl = 'http://127.0.0.1:5000/api/questions';
19
-
20
- constructor(private http: HttpClient) { }
 
 
 
21
 
22
  // Assign role to user
23
- assignRole(payload: { user_id: number; role_name: string; assigned_at: string }): Observable<any> {
24
  return this.http.post<any>(`${this.baseUrl}/select-role`, payload);
25
  }
26
 
27
-
28
  getQuestions(role: string): Observable<QAItem[]> {
29
  return this.http.get<QAItem[]>(`${this.baseUrl}/${role}`);
30
  }
31
 
32
- // fields = { full_name: 'John', created_at: '...' }
33
  submitAnswers(role: string, userId: number, fields: Record<string, any>): Observable<any> {
34
- return this.http.post(`${this.baseUrl}/submit-answers/${role}`, { user_id: userId, ...fields });
 
 
35
  }
36
  }
 
2
  import { HttpClient } from '@angular/common/http';
3
  import { environment } from '../../environments/environment';
4
  import { Observable } from 'rxjs';
5
+
6
  export interface QAItem {
7
  label: string;
8
+ input_type: 'text' | 'date' | 'select' | 'radio' | 'multiselect';
9
  options?: string[];
10
  column_key: string;
11
+ category: string;
12
+ required?: boolean;
13
+ placeholder?: string;
14
  }
15
+
16
  @Injectable({ providedIn: 'root' })
17
  export class QuestionAnswerService {
18
+ private baseUrl: string;
 
 
 
19
 
20
+ constructor(private http: HttpClient) {
21
+ // Initialize baseUrl based on the current hostname
22
+ this.baseUrl = typeof location !== 'undefined' && location.hostname.endsWith('hf.space')
23
+ ? 'https://pykara-py-match-backend.hf.space/api/questions'
24
+ : 'http://127.0.0.1:5000/api/questions';
25
+ }
26
 
27
  // Assign role to user
28
+ assignRole(payload: { user_id: number; role_name: string; assigned_at?: string }): Observable<any> {
29
  return this.http.post<any>(`${this.baseUrl}/select-role`, payload);
30
  }
31
 
32
+ // Fetch questions by role (returns label/options/input_type/column_key/category)
33
  getQuestions(role: string): Observable<QAItem[]> {
34
  return this.http.get<QAItem[]>(`${this.baseUrl}/${role}`);
35
  }
36
 
 
37
  submitAnswers(role: string, userId: number, fields: Record<string, any>): Observable<any> {
38
+ const payload = { user_id: userId, ...fields };
39
+ console.log(payload);
40
+ return this.http.post(`${this.baseUrl}/submit-answers/${role}`, payload);
41
  }
42
  }
src/app/question-answer/question-answer.component.css CHANGED
@@ -1,317 +1,693 @@
1
- /* ---------- Page & Topbar ---------- */
2
- .qa-page {
3
- height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  position: relative;
5
- background-image: url('/assets/Q&A-BG.png'); /* Set the background image */
6
- background-size: cover; /* Ensure the image covers the entire area */
7
- background-position: center center; /* Center the background image */
8
- color: #1a1a1a;
9
- overflow: hidden;
10
  }
11
 
12
- .topbar {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  position: relative;
14
- z-index: 2;
15
- display: grid;
16
- grid-template-columns: auto 1fr auto;
 
 
 
 
 
 
 
 
 
17
  align-items: center;
18
- gap: 12px;
19
- padding: 14px 18px;
20
- background: transparent;
21
- }
22
-
23
- .home-btn {
24
- appearance: none;
25
- border: 1px solid rgba(0,0,0,0.08);
26
- background: rgba(255,255,255,0.9);
27
- backdrop-filter: blur(6px);
28
- padding: 8px 12px;
29
- border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  font-weight: 600;
32
- transition: transform .15s ease, box-shadow .15s ease;
 
 
 
 
 
33
  }
34
 
35
- .home-btn:hover {
36
- transform: translateY(-1px);
37
- box-shadow: 0 8px 20px rgba(0,0,0,0.08);
38
- border: 1px solid #f3a54c;
39
  }
40
 
41
- .home-btn:active {
42
- transform: translateY(0);
 
 
43
  }
44
 
45
- .home-btn:focus-visible {
46
- outline: 3px solid #7cc4ff;
47
- outline-offset: 2px;
 
48
  }
49
 
50
- .title {
51
- margin: 0;
52
- text-align: center;
53
- color: #3a3a3a;
54
- font-size: 3vw;
55
- color: white;
 
 
 
 
 
 
 
 
56
  }
57
 
58
- .topbar-spacer {
59
- width: 52px;
60
- height: 1px;
 
 
 
 
61
  }
62
 
63
- /* ---------- Ambient soft background (subtle stripes + light blobs) ---------- */
64
- .ambient {
65
- position: absolute;
66
- inset: 0;
67
- z-index: 0;
68
- pointer-events: none;
69
- background: #e9cfad57;
70
- opacity: 0.8;
71
- top: 50%;
72
- left: 50%;
73
- transform: translate(-50%, -50%);
74
- height: 28vw;
75
- margin: 0 auto;
76
- border-radius: 8vw;
77
- border: 2px solid #e9cfad;
78
  }
79
 
80
- .stage {
81
- position: absolute;
82
- z-index: 1;
83
- top: 50%;
84
- left: 50%;
85
- transform: translate(-50%, -50%);
86
  }
87
 
88
- /* Shared orb styles */
89
- .orb {
90
- --size: clamp(160px, 26vw, 260px);
91
- width: var(--size);
92
- height: var(--size);
93
- border-radius: 50%;
94
- display: grid;
95
- place-items: center;
96
- border: 10px solid rgba(255,255,255,0.9); /* white rim to mimic gaps */
97
- box-shadow: 0 28px 60px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.6);
98
- position: absolute;
99
- cursor: pointer;
100
- text-align: center;
101
- text-transform: uppercase;
102
- font-weight: 800;
103
- letter-spacing: .04em;
104
- transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
105
  }
106
 
107
- /* Color palette inspired by your reference image */
108
- .orb-top {
109
- background: #595a5e;
110
- color: #ffffff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
- /* dark grey */
113
- .orb-left {
114
- background: #f3a54c;
115
- color: #2b2316;
116
  }
117
- /* warm orange */
118
- .orb-right {
119
- background: #d2a784;
120
- color: #2a1e17;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
- /* soft tan */
123
 
124
- /* Placement to form a neat triad */
125
- .orb-top {
126
- bottom: -2vw;
127
- right: -6vw;
 
 
128
  }
129
 
130
- .orb-left {
131
- left: -2vw;
132
- top: -2vw;
 
 
133
  }
134
 
135
- .orb-right {
136
- right: 0vw;
137
- top: -2vw;
 
 
138
  }
139
 
140
- /* Hover / focus interactions */
141
- .orb:hover {
142
- /*transform: translate(-50%, 0) scale(1.02);*/
143
- box-shadow: 0 36px 80px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.65);
144
- transform: translate(0,0) scale(1.02);
 
 
 
 
145
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- .orb-left:hover {
148
- transform: translate(0,0) scale(1.02);
149
  }
150
 
151
- .orb-right:hover {
152
- transform: translate(0,0) scale(1.02);
 
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
- .orb:focus-visible {
156
- outline: 4px solid #7cc4ff;
157
- outline-offset: 3px;
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
- /* Text inside circles */
161
- .orb > span {
162
- line-height: 1.15;
163
- font-size: clamp(13px, 1.6vw, 18px);
164
- padding: 0 18px;
 
 
 
 
165
  }
166
 
 
 
 
167
 
168
- /* Modal Overlay */
169
- .modal-overlay {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  position: fixed;
171
  top: 0;
172
  left: 0;
173
- right: 0;
174
- bottom: 0;
175
- background: rgba(0, 0, 0, 0.7);
176
  display: flex;
177
  justify-content: center;
178
  align-items: center;
179
  z-index: 1000;
180
- animation: 0.3s ease-in-out;
181
  }
182
 
183
- /* Modal Content */
184
  .modal {
185
- display: flex;
186
  border-radius: 20px;
187
- width: 70%;
188
- max-height: 80%;
189
- overflow-y: auto;
190
- position: relative;
191
- animation: slideUp 0.3s ease-out;
 
192
  }
193
 
194
- /* Left Side: Header and Image */
195
- .modal .left-side {
196
- width: 50%;
197
- background: url('/assets/popup.png') no-repeat center center;
198
- background-size: cover;
199
- position: relative;
200
- padding: 20px;
201
- display: flex;
202
- justify-content: flex-start;
203
- align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
 
206
- .modal h2 {
207
- font-size: 24px;
208
- font-weight: 700;
209
- color: #fff;
210
- position: absolute;
211
- top: 20px;
212
- left: 20px;
213
- z-index: 2;
214
- background-color: rgba(0, 0, 0, 0.5); /* Background for readability */
215
- padding: 5px 10px;
216
- border-radius: 5px;
217
  }
218
 
219
- /* Right Side: Questions and Options */
220
- .modal .right-side {
221
- width: 50%;
222
- padding: 20px;
223
- color: #eb4814;
224
- background-color: #fbeccf;
225
- overflow-y: auto;
226
  }
 
227
 
228
- /* Modal Question Styling */
229
- .modal label {
230
- display: block;
231
- font-size: 18px;
232
- font-weight: 500;
233
- margin-bottom: 10px;
234
- color: #000000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
 
237
- .modal div {
238
- margin-bottom: 20px;
239
  }
240
 
241
- .modal input[type="radio"] {
242
- margin-right: 10px;
 
243
  }
244
 
245
- .modal input[type="text"] {
246
- padding: 10px;
247
- border-radius: 5px;
248
- border: 1px solid #ccc;
249
  width: 100%;
250
- margin-top: 10px;
251
  }
252
 
253
- /* Close Button */
254
- .close-btn {
255
- position: absolute;
256
- top: 10px;
257
- right: 25px;
258
- background: transparent;
259
- font-size: 24px;
260
- color: #aaa;
261
- cursor: pointer;
262
- transition: color 0.3s;
263
- width: 2vw;
264
- border: 3px solid #eb48149c;
265
- padding: 1px;
266
- }
267
 
268
- .close-btn:hover {
269
- color: #333;
 
 
270
  }
271
 
272
- /* Button Styling */
273
- button {
274
- padding: 12px 20px;
275
- background-color: #007bff;
276
- color: black;
277
- border: none;
278
- border-radius: 8px;
279
- font-size: 16px;
280
- cursor: pointer;
281
- transition: background-color 0.3s ease, transform 0.2s ease;
282
- width: 100%;
283
- text-align: center;
284
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
285
  }
286
 
287
- button:active {
288
- transform: translateY(1px);
 
289
  }
290
 
291
- button:focus-visible {
292
- outline: 3px solid #7cc4ff;
293
- outline-offset: 3px;
294
  }
295
 
296
- /* Animations for modal */
297
- @keyframes fadeIn {
298
- from {
299
- opacity: 0;
300
  }
301
 
302
- to {
303
- opacity: 1;
304
  }
305
- }
306
 
307
- @keyframes slideUp {
308
- from {
309
- transform: translateY(20px);
310
- opacity: 0;
311
  }
312
 
313
- to {
314
- transform: translateY(0);
315
- opacity: 1;
316
  }
317
  }
 
 
1
+ /* Professional Styles for Question Answer Component - Updated to match intro theme */
2
+ .professional-page {
3
+ min-height: 100vh;
4
+ background: #f8fafc;
5
+ }
6
+
7
+ /* Loader */
8
+ .loader-overlay {
9
+ position: fixed;
10
+ inset: 0;
11
+ background: rgba(12, 18, 28, 0.7);
12
+ display: grid;
13
+ place-items: center;
14
+ z-index: 9999;
15
+ backdrop-filter: blur(5px);
16
+ }
17
+
18
+ .loader-container {
19
+ text-align: center;
20
+ }
21
+
22
+ .loader-gif {width: 50%; height: 50%;
23
+ }
24
+
25
+ /* Hero Section */
26
+ .hero {
27
  position: relative;
28
+ width: 100%;
29
+ min-height: 70vh;
30
+ display: flex;
31
+ align-items: center;
 
32
  }
33
 
34
+ .hero .content {
35
+ width: 100%;
36
+ padding: 80px 5%;
37
+ color: white;
38
+ }
39
+
40
+ .info-card {
41
+ display: flex;
42
+ gap: 2rem;
43
+ margin: 0 auto;
44
+ background: rgb(255 255 255 / 52%);
45
+ border-radius: 24px;
46
+ padding: 3rem;
47
+ border: 1px solid rgba(255, 255, 255, 0.2);
48
+ align-items: center;
49
+ width: fit-content;
50
  position: relative;
51
+ top: 50%;
52
+ transform: translatey(-50%);
53
+ right: 24vw;
54
+ }
55
+
56
+ .info-card__icon {
57
+ flex: 0 0 100px;
58
+ width: 100px;
59
+ height: 100px;
60
+ border-radius: 50%;
61
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
62
+ display: flex;
63
  align-items: center;
64
+ justify-content: center;
65
+ color: #111;
66
+ font-size: 2.5rem;
67
+ box-shadow: 0 10px 30px rgba(243, 165, 76, 0.3);
68
+ }
69
+
70
+ .info-card__content {
71
+ flex: 1;
72
+ }
73
+
74
+ .info-card__title {
75
+ margin: 0 0 1rem 0;
76
+ font-size: 2.5rem;
77
+ font-weight: 700;
78
+ color: #1a202c;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 0.75rem;
82
+ }
83
+
84
+ .info-card__title i {
85
+ color: #f3a54c;
86
+ }
87
+
88
+ .info-card__text {
89
+ margin: 0 0 2rem 0;
90
+ font-size: 1.1rem;
91
+ line-height: 1.6;
92
+ color: black;
93
+ max-width: 600px;
94
+ font-weight: 500;
95
+ }
96
+
97
+ .info-card__actions {
98
+ display: flex;
99
+ gap: 1rem;
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ /* Buttons */
104
+ .btn {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ gap: 0.5rem;
108
+ padding: 0.875rem 1.75rem;
109
+ border: none;
110
+ border-radius: 12px;
111
+ font-weight: 600;
112
+ font-size: 0.95rem;
113
+ text-decoration: none;
114
  cursor: pointer;
115
+ transition: all 0.3s ease;
116
+ font-family: inherit;
117
+ }
118
+
119
+ .btn-primary {
120
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
121
+ color: #111;
122
+ box-shadow: 0 4px 15px rgba(243, 165, 76, 0.3);
123
+ }
124
+
125
+ .btn-primary:hover {
126
+ transform: translateY(-2px);
127
+ box-shadow: 0 8px 25px rgba(243, 165, 76, 0.4);
128
+ }
129
+
130
+ .btn-secondary {
131
+ background: white;
132
+ color: #f3a54c;
133
+ border: 2px solid #f3a54c;
134
+ }
135
+
136
+ .btn-secondary:hover {
137
+ background: #f3a54c;
138
+ color: white;
139
+ }
140
+
141
+ /* Questions Container */
142
+ .questions-container {
143
+ background: white;
144
+ padding: 5rem 0 1vw 0;
145
+ }
146
+
147
+ .container {
148
+ width: 80%;
149
+ margin: 0 auto;
150
+ /* padding: 0 2rem;*/
151
+ }
152
+
153
+ /* Category Navigation */
154
+ .category-nav-wrapper {
155
+ margin-bottom: 3rem;
156
+ }
157
+
158
+ .category-nav {
159
+ display: flex;
160
+ gap: 1rem;
161
+ flex-wrap: wrap;
162
+ justify-content: center;
163
+ }
164
+
165
+ .nav-chip {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 0.75rem;
169
+ background: white;
170
+ padding: 1rem 1.5rem;
171
+ border-radius: 12px;
172
  font-weight: 600;
173
+ color: #4a5568;
174
+ text-decoration: none;
175
+ border: 2px solid #e2e8f0;
176
+ cursor: pointer;
177
+ transition: all 0.3s ease;
178
+ font-family: inherit;
179
  }
180
 
181
+ .nav-chip:hover {
182
+ border-color: #f3a54c;
183
+ transform: translateY(-2px);
184
+ box-shadow: 0 4px 12px rgba(243, 165, 76, 0.15);
185
  }
186
 
187
+ .nav-chip.active {
188
+ background: #f3a54c;
189
+ color: white;
190
+ border-color: #f3a54c;
191
  }
192
 
193
+ .nav-chip.completed {
194
+ background: #00d68f;
195
+ color: #fff;
196
+ border-color: #00d68f;
197
  }
198
 
199
+ .nav-chip.completed i {
200
+ color: #fff;
201
+ }
202
+
203
+ .nav-chip i {
204
+ font-size: 1.1rem;
205
+ }
206
+
207
+ .chip-progress {
208
+ background: rgba(255, 255, 255, 0.2);
209
+ padding: 0.25rem 0.75rem;
210
+ border-radius: 20px;
211
+ font-size: 0.85rem;
212
+ font-weight: 600;
213
  }
214
 
215
+ /* Category Content */
216
+ .category-content {
217
+ background: white;
218
+ border-radius: 20px;
219
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
220
+ overflow: hidden;
221
+ border: 1px solid #e3e5e8;
222
  }
223
 
224
+ .category-section {
225
+ padding: 2vw 2vw 0.5vw 2vw;
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
 
228
+ .category-header {
229
+ display: flex;
230
+ gap: 3rem;
231
+ margin-bottom: 3rem;
232
+ align-items: center;
 
233
  }
234
 
235
+ .category-image {
236
+ flex: 0 0 300px;
237
+ border-radius: 16px;
238
+ overflow: hidden;
239
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
240
+ position: relative;
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
+ .category-image::before {
244
+ content: "";
245
+ position: absolute;
246
+ inset: 0;
247
+ background: linear-gradient(135deg, rgba(243, 165, 76, 0.1), rgba(0, 255, 136, 0.1));
248
+ z-index: 1;
249
+ }
250
+
251
+ .category-image img {
252
+ width: 100%;
253
+ height: 200px;
254
+ object-fit: cover;
255
+ position: relative;
256
+ z-index: 0;
257
+ }
258
+
259
+ .category-info {
260
+ flex: 1;
261
  }
262
+
263
+ .title-section {
264
+ margin-bottom: 2rem;
 
265
  }
266
+
267
+ .title-section h2 {
268
+ margin: 0 0 1rem 0;
269
+ font-size: 2rem;
270
+ font-weight: 700;
271
+ color: #1a202c;
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 0.75rem;
275
+ }
276
+
277
+ .title-section h2 i {
278
+ color: #f3a54c;
279
+ }
280
+
281
+ .progress-indicator {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 1rem;
285
  }
 
286
 
287
+ .progress-bar {
288
+ flex: 1;
289
+ height: 8px;
290
+ background: #e2e8f0;
291
+ border-radius: 4px;
292
+ overflow: hidden;
293
  }
294
 
295
+ .progress-fill {
296
+ height: 100%;
297
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
298
+ border-radius: 4px;
299
+ transition: width 0.4s ease;
300
  }
301
 
302
+ .progress-indicator small {
303
+ font-size: 0.9rem;
304
+ font-weight: 600;
305
+ color: #4a5568;
306
+ min-width: 80px;
307
  }
308
 
309
+ /* Questions Grid - Compact 3-column layout */
310
+ .questions-grid {
311
+ display: grid;
312
+ grid-template-columns: repeat(4, 1fr);
313
+ gap: 1.5rem;
314
+ }
315
+ .checkbox-options{
316
+ overflow-y:scroll;
317
+ height:3vh;
318
  }
319
+ .question-card {
320
+ background: white;
321
+ border: 2px solid #f7fafc;
322
+ border-radius: 16px;
323
+ padding: 0.2vw;
324
+ transition: all 0.3s ease;
325
+ position: relative;
326
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
327
+ width: 100%;
328
+ min-width: 0;
329
+ box-sizing: border-box;
330
+ height: auto;
331
+ color: #1a202c;
332
+ display: flex;
333
+ flex-direction: column;
334
+ justify-content: stretch;
335
+ }
336
+
337
+ .question-card:hover {
338
+ border-color: #f3a54c;
339
+ transform: translateY(-2px);
340
+ box-shadow: 0 8px 25px rgba(243, 165, 76, 0.15);
341
+ }
342
+
343
+ .question-card.answered {
344
+ border-color: #48bb78;
345
+ background: #f0fff4;
346
+ }
347
+
348
+ .question-card.error {
349
+ border-color: #f56565;
350
+ animation: shake 0.5s ease-in-out;
351
+ }
352
+
353
+ .question-card.required::after {
354
+ content: 'Required';
355
+ position: absolute;
356
+ top: 1rem;
357
+ right: 1rem;
358
+ background: #fed7d7;
359
+ color: #c53030;
360
+ padding: 0.25rem 0.75rem;
361
+ border-radius: 20px;
362
+ font-size: 0.75rem;
363
+ font-weight: 700;
364
+ }
365
+
366
+ .question-card label {
367
+ display: block;
368
+ margin-bottom: 0.75rem;
369
+ font-weight: 600;
370
+ color: #2d3748;
371
+ font-size: 0.95rem;
372
+ line-height: 1.4;
373
+ }
374
 
375
+ .required-indicator {
376
+ color: #f56565;
377
  }
378
 
379
+ .question-card select,
380
+ .question-card input[type="text"],
381
+ .question-card input[type="date"] {
382
+ width: 100%;
383
+ padding: 0.875rem 1rem;
384
+ border: 2px solid #e2e8f0;
385
+ border-radius: 8px;
386
+ font-size: 0.95rem;
387
+ transition: all 0.3s ease;
388
+ background: white;
389
+ font-family: inherit;
390
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
391
  }
392
 
393
+ .question-card select:focus,
394
+ .question-card input[type="text"]:focus,
395
+ .question-card input[type="date"]:focus {
396
+ outline: none;
397
+ border-color: #f3a54c;
398
+ box-shadow: 0 0 0 3px rgba(243, 165, 76, 0.1);
399
+ }
400
+
401
+ .radio-options {
402
+ display: flex;/*
403
+ flex-direction: column;*/
404
+ gap: 0.75rem;
405
  }
406
 
407
+ .radio-option {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 0.75rem;
411
+ padding: 0.75rem;
412
+ background: #f7fafc;
413
+ border-radius: 8px;
414
+ cursor: pointer;
415
+ transition: background 0.3s ease;
416
  }
417
 
418
+ .radio-option:hover {
419
+ background: #edf2f7;
420
+ }
421
 
422
+ .radio-option input {
423
+ margin: 0;
424
+ }
425
+
426
+ .hint {
427
+ font-size: 0.85rem;
428
+ color: #718096;
429
+ margin-top: 0.5rem;
430
+ line-height: 1.4;
431
+ }
432
+
433
+ .error-message {
434
+ font-size: 0.85rem;
435
+ color: #e53e3e;
436
+ font-weight: 600;
437
+ margin-top: 0.5rem;
438
+ }
439
+
440
+ /* Submit Section */
441
+ .submit-section {
442
+ text-align: center;
443
+ padding: 1vw;
444
+ border-top: 1px solid #e2e8f0;
445
+ /* margin-top: 2rem;*/
446
+ }
447
+
448
+ .submit-btn {
449
+ padding: 1rem 2.5rem;
450
+ font-size: 1.1rem;
451
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
452
+ color: #111;
453
+ border: none;
454
+ border-radius: 12px;
455
+ font-weight: 600;
456
+ cursor: pointer;
457
+ transition: all 0.3s ease;
458
+ box-shadow: 0 4px 15px rgba(243, 165, 76, 0.3);
459
+ }
460
+
461
+ .submit-btn:hover {
462
+ transform: translateY(-2px);
463
+ box-shadow: 0 8px 25px rgba(243, 165, 76, 0.4);
464
+ }
465
+
466
+ .submit-btn:disabled {
467
+ opacity: 0.6;
468
+ cursor: not-allowed;
469
+ transform: none !important;
470
+ }
471
+
472
+ .validation-message {
473
+ margin-top: 1rem;
474
+ color: #e53e3e;
475
+ font-weight: 600;
476
+ }
477
+
478
+ /* Empty State */
479
+ .empty-state {
480
+ padding: 4rem 2rem;
481
+ text-align: center;
482
+ }
483
+
484
+ .empty-state-text {
485
+ font-size: 1.1rem;
486
+ color: #718096;
487
+ }
488
+
489
+ /* Modal */
490
+ .modal-backdrop {
491
  position: fixed;
492
  top: 0;
493
  left: 0;
494
+ width: 100%;
495
+ height: 100%;
496
+ background: rgba(0, 0, 0, 0.6);
497
  display: flex;
498
  justify-content: center;
499
  align-items: center;
500
  z-index: 1000;
501
+ padding: 2rem;
502
  }
503
 
 
504
  .modal {
505
+ background: white;
506
  border-radius: 20px;
507
+ padding: 3rem;
508
+ max-width: 500px;
509
+ width: 100%;
510
+ text-align: center;
511
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
512
+ border: 1px solid #e3e5e8;
513
  }
514
 
515
+ .success-title {
516
+ color: #48bb78;
517
+ font-size: 1.5rem;
518
+ margin-bottom: 1rem;
519
+ display: flex;
520
+ align-items: center;
521
+ justify-content: center;
522
+ gap: 0.5rem;
523
+ }
524
+
525
+ /* Tip Card */
526
+ .tip-card {
527
+ background: linear-gradient(135deg, #fed7aa, #fdba74);
528
+ border-radius: 16px;
529
+ padding: 1.5rem;
530
+ margin: 2rem 0;
531
+ text-align: left;
532
+ }
533
+
534
+ .tip-content {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 1rem;
538
+ }
539
+
540
+ .tip-icon {
541
+ width: 50px;
542
+ height: 50px;
543
+ flex-shrink: 0;
544
+ }
545
+
546
+ .tip-text {
547
+ flex: 1;
548
+ }
549
+
550
+ .tip-message {
551
+ margin: 0;
552
+ color: #7c2d12;
553
+ font-weight: 500;
554
+ line-height: 1.5;
555
+ }
556
+
557
+ /* Modal Actions */
558
+ .modal-actions {
559
+ display: flex;
560
+ gap: 1rem;
561
+ justify-content: center;
562
+ margin-bottom: 2rem;
563
+ }
564
+
565
+ /* Countdown Timer */
566
+ .countdown-timer {
567
+ margin-top: 2rem;
568
+ }
569
+
570
+ .timer-bar {
571
+ width: 100%;
572
+ height: 6px;
573
+ background: #e2e8f0;
574
+ border-radius: 3px;
575
+ overflow: hidden;
576
+ margin-bottom: 0.5rem;
577
+ }
578
+
579
+ .timer-fill {
580
+ height: 100%;
581
+ background: linear-gradient(135deg, #48bb78, #38a169);
582
+ border-radius: 3px;
583
+ transition: width 0.1s linear;
584
+ }
585
+
586
+ .countdown-timer small {
587
+ color: #718096;
588
+ font-size: 0.85rem;
589
+ }
590
+
591
+ /* Animations */
592
+ @keyframes shake {
593
+ 0%, 100% {
594
+ transform: translateX(0);
595
  }
596
 
597
+ 25% {
598
+ transform: translateX(-5px);
 
 
 
 
 
 
 
 
 
599
  }
600
 
601
+ 75% {
602
+ transform: translateX(5px);
 
 
 
 
 
603
  }
604
+ }
605
 
606
+ /* Responsive Design */
607
+ @media (max-width: 1024px) {
608
+ .questions-grid {
609
+ grid-template-columns: repeat(2, 1fr);
610
+ }
611
+ }
612
+
613
+ @media (max-width: 768px) {
614
+ .hero .content {
615
+ padding: 60px 2rem;
616
+ }
617
+
618
+ .info-card {
619
+ flex-direction: column;
620
+ text-align: center;
621
+ padding: 2rem;
622
+ }
623
+
624
+ .info-card__icon {
625
+ width: 80px;
626
+ height: 80px;
627
+ font-size: 2rem;
628
+ }
629
+
630
+ .info-card__title {
631
+ font-size: 2rem;
632
+ justify-content: center;
633
  }
634
 
635
+ .info-card__actions {
636
+ justify-content: center;
637
  }
638
 
639
+ .category-header {
640
+ flex-direction: column;
641
+ gap: 2rem;
642
  }
643
 
644
+ .category-image {
645
+ flex: none;
 
 
646
  width: 100%;
 
647
  }
648
 
649
+ .questions-grid {
650
+ grid-template-columns: 1fr;
651
+ }
 
 
 
 
 
 
 
 
 
 
 
652
 
653
+ .category-nav {
654
+ justify-content: flex-start;
655
+ overflow-x: auto;
656
+ padding-bottom: 1rem;
657
  }
658
 
659
+ .nav-chip {
660
+ flex-shrink: 0;
661
+ }
662
+
663
+ .modal-actions {
664
+ flex-direction: column;
665
+ }
 
 
 
 
 
 
666
  }
667
 
668
+ @media (max-width: 480px) {
669
+ .container {
670
+ padding: 0 1rem;
671
  }
672
 
673
+ .category-section {
674
+ padding: 2rem 1rem;
 
675
  }
676
 
677
+ .modal {
678
+ padding: 2rem 1.5rem;
 
 
679
  }
680
 
681
+ .info-card__title {
682
+ font-size: 1.75rem;
683
  }
 
684
 
685
+ .questions-grid {
686
+ gap: 1rem;
 
 
687
  }
688
 
689
+ .question-card {
690
+ padding: 1.25rem;
 
691
  }
692
  }
693
+
src/app/question-answer/question-answer.component.html CHANGED
@@ -1,67 +1,175 @@
1
- <div class="qa-page">
2
- <header class="topbar">
3
- <button class="home-btn" type="button" (click)="goHome()" aria-label="Go to Home">Home</button>
4
- <h1 class="title">{{ heading }}</h1>
5
- </header>
6
-
7
- <main class="stage">
8
- <button class="orb orb-top" type="button" (click)="onSelect('marriage')" aria-label="Select MARRIAGE">
9
- <span><i class="fa-solid fa-ring"></i>&nbsp;MARRIAGE</span>
10
- </button>
11
- <button class="orb orb-left" type="button" (click)="onSelect('interview')" aria-label="Select INTERVIEW">
12
- <span><i class="fa-solid fa-user-tie"></i>&nbsp;INTERVIEW</span>
13
- </button>
14
- <button class="orb orb-right" type="button" (click)="onSelect('partnership')" aria-label="Select BUSINESS PARTNERS">
15
- <span><i class="fa-solid fa-handshake"></i>&nbsp;BUSINESS PARTNERS</span>
16
- </button>
17
- </main>
18
-
19
- <div *ngIf="isModalOpen" class="modal-overlay">
20
- <div class="modal">
21
- <button class="close-btn" (click)="closeModal()">×</button>
22
 
23
- <div class="left-side">
24
- <h2>Questions for {{ selectedOption | uppercase }}</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  </div>
 
 
26
 
27
- <div class="right-side">
28
- <div *ngIf="selectedQuestions.length > 0; else noQuestions">
29
- <form (ngSubmit)="submitAnswers()">
30
- <div *ngFor="let q of selectedQuestions; let i = index">
31
- <label>{{ q.label }}</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- <!-- text/date -->
34
- <div *ngIf="q.input_type === 'text' || q.input_type === 'date'">
35
- <input [type]="q.input_type === 'date' ? 'date' : 'text'"
36
- [(ngModel)]="answers[i]"
37
- name="answer{{ i }}"
38
- (ngModelChange)="onAnswerChange(i, $event)"
39
- placeholder="Enter your answer" />
 
 
 
 
 
 
 
 
 
 
 
40
  </div>
 
41
 
42
- <!-- radio/select -->
43
- <div *ngIf="(q.input_type === 'select' || q.input_type === 'radio') && q.options?.length">
44
- <label *ngFor="let option of q.options" class="option">
45
- <input type="radio"
46
- [name]="'answer' + i"
47
- [value]="option"
48
- [(ngModel)]="answers[i]"
49
- (change)="onAnswerChange(i, option)" />
50
- {{ option }}
51
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
  </div>
54
 
55
- <button type="submit">Submit Answers</button>
56
- </form>
57
- <!--<button [routerLink]="'/quiz'">Go to Quiz</button>-->
58
- <a routerLink="/llmquiz" class="btn btn-primary">{{ tr('Go to Quiz') }}</a>
59
- </div>
60
-
61
- <ng-template #noQuestions>
62
- <p>No questions available for this role.</p>
63
- </ng-template>
 
 
64
  </div>
65
  </div>
66
  </div>
 
 
 
 
67
  </div>
 
1
+ <div class="professional-page">
2
+ <!-- Loader Overlay (same as llm-quiz) -->
3
+ <div class="loader-overlay" *ngIf="isLoading">
4
+ <div class="loader-container">
5
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
6
+ </div>
7
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ <!-- Hero Section -->
10
+ <section class="hero">
11
+ <div class="content" [ngStyle]="heroContentStyle">
12
+ <div class="info-card">
13
+ <div class="info-card__icon">
14
+ <i [class]="headingIcon"></i>
15
+ </div>
16
+ <div class="info-card__content">
17
+ <h1 class="info-card__title">{{ selectedOption ? (selectedOption | titlecase) + ' Match' : 'Marriage Match' }}</h1>
18
+ <p class="info-card__text">
19
+ Discover meaningful connections beyond surface-level swipes.<br>
20
+ This helps to analyse compatibility deeply.<br>
21
+ Understand habits, values, and life goals.
22
+ Find harmony and balance for a lasting relationship.
23
+ </p>
24
+ <div class="info-card__actions">
25
+ <button class="btn btn-primary" (click)="scrollToQuestions()">
26
+ Fill Your Details
27
+ </button>
28
+ <button class="btn btn-secondary" (click)="goToPreferences()">
29
+ Set Preference
30
+ </button>
31
+ </div>
32
+ </div>
33
  </div>
34
+ </div>
35
+ </section>
36
 
37
+ <!-- Questions container -->
38
+ <div id="questions-section" class="questions-container" *ngIf="selectedQuestions.length">
39
+ <div class="container">
40
+ <!-- Category Navigation -->
41
+ <div class="category-nav-wrapper">
42
+ <nav class="category-nav" aria-label="Question categories">
43
+ <button *ngFor="let cat of categoriesMeta" type="button"
44
+ (click)="selectCategory(cat.key)"
45
+ [class.active]="selectedCategory === cat.key"
46
+ [class.completed]="getCategoryAnsweredCount(cat.key) === getCategoryRequiredCount(cat.key) && getCategoryRequiredCount(cat.key) > 0"
47
+ [attr.aria-selected]="selectedCategory === cat.key"
48
+ class="nav-chip">
49
+ <i class="fa-solid" [ngClass]="cat.icon"></i>
50
+ <span>{{ cat.title }}</span>
51
+ <span class="chip-progress" *ngIf="getCategoryQuestions(cat.key).length">
52
+ {{ getCategoryAnsweredCount(cat.key) }}/{{ getCategoryRequiredCount(cat.key) }}
53
+ </span>
54
+ </button>
55
+ </nav>
56
+ </div>
57
 
58
+ <!-- Single Category Rendering -->
59
+ <div class="category-content">
60
+ <ng-container *ngFor="let cat of categoriesMeta; let last = last">
61
+ <section *ngIf="selectedCategory === cat.key" [attr.id]="'cat-' + cat.key" class="category-section">
62
+ <div class="category-header">
63
+ <div class="category-image">
64
+ <img [src]="getCategoryImage(cat.key)" [alt]="cat.title" loading="lazy" />
65
+ </div>
66
+ <div class="category-info">
67
+ <div class="title-section">
68
+ <h2><i class="fa-solid" [ngClass]="cat.icon"></i> {{ cat.title }}</h2>
69
+ <div class="progress-indicator">
70
+ <div class="progress-bar">
71
+ <div class="progress-fill" [style.width.%]="getCategoryPercent(cat.key)"></div>
72
+ </div>
73
+ <small>{{ getCategoryProgressLabel(cat.key) }}</small>
74
+ </div>
75
+ </div>
76
  </div>
77
+ </div>
78
 
79
+ <div class="questions-grid">
80
+ <div *ngFor="let question of getCategoryQuestions(cat.key); trackBy: trackQuestion"
81
+ class="question-card"
82
+ [attr.id]="'q-' + question.column_key"
83
+ [class.required]="question.required"
84
+ [class.answered]="isAnswered(question)"
85
+ [class.error]="showError(question)">
86
+ <label [attr.for]="'answer' + question.column_key">
87
+ {{ question.label }} <span *ngIf="question.required" class="required-indicator">*</span>
88
  </label>
89
+ <ng-container [ngSwitch]="question.input_type">
90
+ <!-- Custom multi-select for Languages Spoken -->
91
+ <div *ngIf="question.label === 'Languages Spoken'" class="checkbox-options">
92
+ <label class="checkbox-option" *ngFor="let opt of question.options">
93
+ <input type="checkbox"
94
+ [value]="opt"
95
+ [checked]="answers[question.column_key]?.includes(opt)"
96
+ (change)="onLanguagesCheckboxChange($event, question.column_key)" />
97
+ <span class="checkmark"></span>
98
+ {{ opt }}
99
+ </label>
100
+ </div>
101
+
102
+ <!-- Default rendering for other types, only if not Languages Spoken -->
103
+ <ng-container *ngIf="question.label !== 'Languages Spoken'">
104
+ <select *ngSwitchCase="'select'" [id]="'answer' + question.column_key"
105
+ [(ngModel)]="answers[question.column_key]"
106
+ [name]="'answer' + question.column_key"
107
+ (ngModelChange)="onAnswerChange(question.column_key, $event)"
108
+ [required]="question.required || false">
109
+ <option value="" disabled>{{ getPlaceholder(question) }}</option>
110
+ <option *ngFor="let opt of question.options" [value]="opt">{{ opt }}</option>
111
+ </select>
112
+
113
+ <select *ngSwitchCase="'multiselect'" multiple [id]="'answer' + question.column_key"
114
+ [(ngModel)]="answers[question.column_key]"
115
+ [name]="'answer' + question.column_key"
116
+ (ngModelChange)="onAnswerChange(question.column_key, $event)"
117
+ [required]="question.required || false">
118
+ <option *ngFor="let opt of question.options" [value]="opt">{{ opt }}</option>
119
+ </select>
120
+ </ng-container>
121
+
122
+ <input *ngSwitchCase="'date'" type="date" [id]="'answer' + question.column_key"
123
+ [(ngModel)]="answers[question.column_key]"
124
+ [name]="'answer' + question.column_key"
125
+ (ngModelChange)="onAnswerChange(question.column_key, $event)"
126
+ [required]="question.required || false"
127
+ [placeholder]="getPlaceholder(question)" />
128
+
129
+ <input *ngSwitchCase="'text'" type="text" [id]="'answer' + question.column_key"
130
+ [(ngModel)]="answers[question.column_key]"
131
+ [name]="'answer' + question.column_key"
132
+ (ngModelChange)="onAnswerChange(question.column_key, $event)"
133
+ [required]="question.required || false"
134
+ [placeholder]="getPlaceholder(question)" />
135
+
136
+ <div *ngSwitchCase="'radio'" class="radio-options">
137
+ <label *ngFor="let opt of question.options" class="radio-option">
138
+ <input type="radio" [name]="'answer' + question.column_key" [value]="opt"
139
+ [(ngModel)]="answers[question.column_key]"
140
+ (ngModelChange)="onAnswerChange(question.column_key, $event)" />
141
+ {{ opt }}
142
+ </label>
143
+ </div>
144
+
145
+ <input *ngSwitchDefault type="text" [id]="'answer' + question.column_key"
146
+ [(ngModel)]="answers[question.column_key]"
147
+ [name]="'answer' + question.column_key"
148
+ (ngModelChange)="onAnswerChange(question.column_key, $event)"
149
+ [required]="question.required || false"
150
+ [placeholder]="getPlaceholder(question)" />
151
+ </ng-container>
152
+ <div class="hint" *ngIf="question.placeholder && !showError(question)">{{ question.placeholder }}</div>
153
+ <div class="error-message" *ngIf="showError(question)">This field is required.</div>
154
  </div>
155
  </div>
156
 
157
+ <div class="submit-section" *ngIf="last">
158
+ <button class="btn btn-primary submit-btn" (click)="submitAnswers()" [disabled]="isLoading || !allFieldsAnswered">
159
+ <span *ngIf="!isLoading"><i class="fa-solid fa-paper-plane"></i> Submit Answers</span>
160
+ <span *ngIf="isLoading"><i class="fa-solid fa-circle-notch fa-spin"></i> Submitting...</span>
161
+ </button>
162
+ <div class="validation-message" *ngIf="attemptedSubmit && !allFieldsAnswered">
163
+ Please complete all fields ({{ unansweredCount }} remaining).
164
+ </div>
165
+ </div>
166
+ </section>
167
+ </ng-container>
168
  </div>
169
  </div>
170
  </div>
171
+
172
+ <div class="empty-state" *ngIf="!selectedQuestions.length && !isLoading">
173
+ <p class="empty-state-text">Select a matching mode to begin answering questions.</p>
174
+ </div>
175
  </div>
src/app/question-answer/question-answer.component.ts CHANGED
@@ -1,157 +1,521 @@
1
- import { Component, OnInit } from '@angular/core';
2
  import { QuestionAnswerService, QAItem } from './question-answer-service.service';
3
- import { Router, RouterLink } from '@angular/router';
 
 
 
 
 
 
4
  import { FormsModule } from '@angular/forms';
5
  import { CommonModule } from '@angular/common';
6
- import { HttpErrorResponse } from '@angular/common/http';
7
 
8
  @Component({
9
  selector: 'app-question-answer',
10
  standalone: true,
11
- imports: [FormsModule, CommonModule, RouterLink],
12
  templateUrl: './question-answer.component.html',
13
  styleUrls: ['./question-answer.component.css']
14
  })
15
  export class QuestionAnswerComponent implements OnInit {
16
- heading: string = 'CHOOSE YOUR ROLE';
17
- selectedOption: string = ''; // 'marriage' | 'interview' | 'partnership'
18
- selectedQuestions: QAItem[] = []; // questions fetched for selected role
19
- isModalOpen: boolean = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- // If you already know the logged-in user id, set it here or load from localStorage in ngOnInit.
22
- userId: number = 1;
 
23
 
24
- // Template-driven answers storage (bound via [(ngModel)])
25
- answers: any[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  constructor(
28
  private qaService: QuestionAnswerService,
29
- private router: Router
 
30
  ) { }
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  ngOnInit(): void {
33
- // Optional: restore from localStorage if available
34
  const uid = localStorage.getItem('user_id');
35
  if (uid) {
36
- const n = Number(uid);
37
- if (!Number.isNaN(n)) this.userId = n;
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
- const savedRole = localStorage.getItem('role');
41
- if (savedRole) this.selectedOption = savedRole;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
- onSelect(role: string): void {
45
- this.selectedOption = role;
46
- this.fetchQuestions(role);
47
- this.isModalOpen = true;
48
- this.assignRoleToUser(role);
 
 
 
 
 
49
  }
50
 
51
- assignRoleToUser(role: string): void {
52
- const payload = {
53
- user_id: this.userId,
54
- role_name: role,
55
- assigned_at: new Date().toISOString()
56
- };
 
 
 
 
57
 
58
- this.qaService.assignRole(payload).subscribe({
59
- next: (res) => console.log('Role assigned successfully:', res),
60
- error: (err: HttpErrorResponse) => console.error('Error assigning role:', err)
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  });
62
  }
63
 
64
  fetchQuestions(role: string): void {
65
  this.qaService.getQuestions(role).subscribe({
66
  next: (data: QAItem[]) => {
 
 
67
  this.selectedQuestions = data;
68
- // Prepare answers array for template-driven binding
69
- this.answers = new Array(data.length).fill('');
70
- console.log('Fetched questions:', data);
 
 
 
 
 
 
 
 
 
 
 
71
  },
72
- error: (err: HttpErrorResponse) => console.error('Error fetching questions:', err)
 
 
 
73
  });
74
  }
75
 
76
- submitAnswers(): void {
77
- const role = (this.selectedOption || '').toLowerCase(); // 'marriage' | 'interview' | 'partnership'
78
- const userIdNum = Number(this.userId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- if (!role || !userIdNum) {
81
- console.error('Role and user id are required.');
 
 
 
 
 
 
82
  return;
83
  }
84
 
85
- // Build the "fields" object from questions + answers.
86
- // We prefer the backend-provided DB column name: r.column_key.
 
 
 
87
  const fields: Record<string, any> = {};
88
- this.selectedQuestions.forEach((q: QAItem, idx: number) => {
89
- const key =
90
- (q as any).column_key || // from RoleQuestions table
91
- (q as any).key || // fallback if present
92
- `q_${idx + 1}`; // final fallback
93
- fields[key] = this.answers[idx] ?? '';
94
  });
95
 
96
- // Many insert statements expect created_at. Add if your backend uses it.
97
- fields['created_at'] = new Date().toISOString();
 
 
 
 
98
 
99
- // Service signature: submitAnswers(role: string, userId: number, fields: Record<string, any>)
100
- this.qaService.submitAnswers(role, userIdNum, fields).subscribe({
101
  next: () => {
102
- // Close profile modal
103
- this.isModalOpen = false;
104
-
105
- // Persist for LLM Quiz
106
- localStorage.setItem('user_id', String(userIdNum));
107
- localStorage.setItem('role', role);
108
- // Log the values passed to router
109
- console.log('Navigating to LLM Quiz with queryParams:', fields);
110
- // Navigate to LLM Quiz with autostart
111
- // Prepare queryParams
112
- const queryParams = {
113
- user_id: String(userIdNum),
114
- role,
115
- fields, // Fields object (you might want to reconsider passing this depending on the size/complexity)
116
- autostart: '1'
117
- };
118
-
119
- // Log the queryParams before navigating
120
- this.router.navigate(['/llmquiz'], {
121
- queryParams: { user_id: String(userIdNum), role, fields, autostart: '1' }
122
  });
123
- console.log('queryParams:', queryParams);
124
  },
125
  error: (err: HttpErrorResponse) => {
126
- console.error('Error submitting answers:', err);
 
 
 
 
 
127
  }
128
  });
129
  }
130
 
131
- onAnswerChange(i: number, v: any): void {
132
- this.answers[i] = v;
133
  }
134
 
135
  closeModal(): void {
136
  this.isModalOpen = false;
 
137
  }
138
 
139
- goHome(): void {
140
- this.router.navigate(['/intro-page']);
 
 
 
 
 
 
 
141
  }
142
 
143
- // i18n helper (keep if used in template)
144
- tr(key: string): string {
145
- const dict = {
146
- brand: 'Pykara',
147
- signup: 'Sign up',
148
- signin: 'Sign in',
149
- tagline: 'Discover your profile. Match with purpose.',
150
- subtext: 'Py-Match runs a short, privacy-first assessment and builds a profile used for responsible matching. Color results are not shown to users.',
151
- getStarted: 'Get started',
152
- tos: 'Terms',
153
- privacy: 'Privacy'
154
  };
155
- return dict[key as keyof typeof dict] ?? key;
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
  }
 
1
+ import { Component, OnInit, ViewChild } from '@angular/core';
2
  import { QuestionAnswerService, QAItem } from './question-answer-service.service';
3
+ import { Router, ActivatedRoute } from '@angular/router';
4
+ import { HttpErrorResponse } from '@angular/common/http';
5
+ import { MatButtonModule } from '@angular/material/button';
6
+ import { MatIconModule } from '@angular/material/icon';
7
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8
+ import { MatTabsModule } from '@angular/material/tabs';
9
+ import { MatTabGroup } from '@angular/material/tabs';
10
  import { FormsModule } from '@angular/forms';
11
  import { CommonModule } from '@angular/common';
 
12
 
13
  @Component({
14
  selector: 'app-question-answer',
15
  standalone: true,
16
+ imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatTabsModule],
17
  templateUrl: './question-answer.component.html',
18
  styleUrls: ['./question-answer.component.css']
19
  })
20
  export class QuestionAnswerComponent implements OnInit {
21
+ @ViewChild('tabGroup') tabGroup!: MatTabGroup;
22
+
23
+ selectedQuestions: QAItem[] = [];
24
+ filteredQuestions: QAItem[] = [];
25
+ selectedOption = '';
26
+ currentTabIndex = 0;
27
+ isModalOpen = false;
28
+ displayMode: 'modal' | 'page' = 'modal';
29
+ isLoading = false;
30
+ userId: any;
31
+ answers: Record<string, any> = {};
32
+ attemptedSubmit = false;
33
+ showProceedModal = false;
34
+ selectedCategory: string = '';
35
+
36
+ // NEW: Add submission status variables
37
+ showSubmissionPopup = false;
38
+ submissionMessage = '';
39
+ isSubmissionSuccess = false;
40
+ redirectTimer: any = null;
41
+ countdownProgress = 100;
42
+ countdownInterval: any = null;
43
+
44
+ categories = ['personal', 'locationfamily', 'lifestyle', 'educationcareer'];
45
+ categoriesMeta = [
46
+ { key: 'personal', title: 'Personal Info', icon: 'fa-user', color: '#f3a54c' },
47
+ { key: 'locationfamily', title: 'Family & Location', icon: 'fa-people-roof', color: '#ffcb7a' },
48
+ { key: 'lifestyle', title: 'Lifestyle', icon: 'fa-seedling', color: '#f3a54c' },
49
+ { key: 'educationcareer', title: 'Education & Career', icon: 'fa-briefcase', color: '#ffcb7a' }
50
+ ];
51
+
52
+ private readonly allowedRoles = new Set(['marriage', 'interview', 'partnership']);
53
+
54
+ /** Columns that are BIT in SQL Server and require 1/0 values */
55
+ private readonly BIT_FIELDS = new Set<string>([
56
+ 'dual_citizenship',
57
+ 'live_with_parents',
58
+ 'support_parents_financially',
59
+ 'own_pets'
60
+ ]);
61
+
62
+ // Added getter for icon next to heading
63
+ get headingIcon(): string {
64
+ switch (this.selectedOption) {
65
+ case 'marriage':
66
+ return 'fa-solid fa-ring';
67
+ case 'interview':
68
+ return 'fa-solid fa-user-tie';
69
+ case 'partnership':
70
+ return 'fa-solid fa-handshake';
71
+ default:
72
+ return 'fa-solid fa-layer-group';
73
+ }
74
+ }
75
+
76
+ // Dynamic hero background style used in template
77
+ get heroBackgroundStyle() {
78
+ const role = this.selectedOption || 'marriage';
79
+ const img = this.getBackgroundForRole(role);
80
+ return { 'background-image': `linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)), url(${img})` } as { [k: string]: string };
81
+ }
82
+
83
+ // Combined content style for hero content div
84
+ get heroContentStyle() {
85
+ return {
86
+ ...this.heroBackgroundStyle,
87
+ 'background-repeat': 'no-repeat',
88
+ 'background-position': 'center center',
89
+ 'background-attachment': 'fixed',
90
+ 'background-size': 'cover',
91
+ 'width': '100%',
92
+ 'height': '100vh'
93
+ } as { [k: string]: string };
94
+ }
95
+
96
+ get totalRequired(): number {
97
+ return this.selectedQuestions.filter(q => q.required).length;
98
+ }
99
+
100
+ get answeredRequired(): number {
101
+ return this.selectedQuestions.filter(q => q.required && this.isAnswered(q)).length;
102
+ }
103
+
104
+ get progressPercent(): number {
105
+ return this.totalRequired ? Math.round((this.answeredRequired / this.totalRequired) * 100) : 0;
106
+ }
107
 
108
+ get progressLabel(): string {
109
+ return `${this.answeredRequired} / ${this.totalRequired || 0} Required`;
110
+ }
111
 
112
+ get allFieldsAnswered(): boolean {
113
+ return this.selectedQuestions.length > 0 && this.selectedQuestions.every(q => this.isAnswered(q));
114
+ }
115
+
116
+ get unansweredFields(): QAItem[] {
117
+ return this.selectedQuestions.filter(q => !this.isAnswered(q));
118
+ }
119
+
120
+ get unansweredCount(): number {
121
+ return this.unansweredFields.length;
122
+ }
123
+
124
+ // --- Category progress helpers ---
125
+ getCategoryQuestions(key: string): QAItem[] {
126
+ const norm = key.replace(/\s+/g, '').toLowerCase();
127
+ return this.selectedQuestions.filter(q => (q.category || '').replace(/\s+/g, '').toLowerCase() === norm);
128
+ }
129
+ getCategoryAnsweredCount(key: string): number {
130
+ return this.getCategoryQuestions(key).filter(q => this.isAnswered(q)).length;
131
+ }
132
+ getCategoryRequiredCount(key: string): number {
133
+ const qs = this.getCategoryQuestions(key);
134
+ const requiredCnt = qs.filter(q => q.required).length;
135
+ return requiredCnt || qs.length;
136
+ }
137
+ getCategoryPercent(key: string): number {
138
+ const denom = this.getCategoryRequiredCount(key) || 1;
139
+ return (this.getCategoryAnsweredCount(key) / denom) * 100;
140
+ }
141
+ getCategoryProgressLabel(key: string): string {
142
+ return `${this.getCategoryAnsweredCount(key)} / ${this.getCategoryRequiredCount(key)}`;
143
+ }
144
+
145
+ // NEW: Method to get the completed category key (only one at a time).
146
+ completedCategory(): string | null {
147
+ // Return the first category key that is fully completed
148
+ for (const cat of this.categoriesMeta) {
149
+ const total = this.getCategoryRequiredCount(cat.key);
150
+ const answered = this.getCategoryAnsweredCount(cat.key);
151
+ if (total > 0 && answered === total) {
152
+ return cat.key;
153
+ }
154
+ }
155
+ return null;
156
+ }
157
 
158
  constructor(
159
  private qaService: QuestionAnswerService,
160
+ private router: Router,
161
+ private route: ActivatedRoute
162
  ) { }
163
 
164
+ // NEW: Method to show submission popup and redirect
165
+ private showSubmissionSuccessPopup(): void {
166
+ console.log('Submission successful, preparing redirect to preferences');
167
+ console.log('User ID:', this.userId, 'Role:', this.selectedOption);
168
+
169
+ this.showSubmissionPopup = true;
170
+ this.isSubmissionSuccess = true;
171
+ // Updated message for next screen
172
+ this.submissionMessage = 'Profile submitted! Redirecting to preferences....';
173
+
174
+ // Reset countdown
175
+ this.countdownProgress = 100;
176
+
177
+ // Clear any existing timers
178
+ if (this.redirectTimer) {
179
+ clearTimeout(this.redirectTimer);
180
+ }
181
+ if (this.countdownInterval) {
182
+ clearInterval(this.countdownInterval);
183
+ }
184
+
185
+ // Start countdown animation
186
+ const countdownDuration = 5000; // 5 seconds
187
+ const intervalDuration = 50; // update every 50ms
188
+ const steps = countdownDuration / intervalDuration;
189
+ const progressDecrement = 100 / steps;
190
+
191
+ this.countdownInterval = setInterval(() => {
192
+ this.countdownProgress -= progressDecrement;
193
+ if (this.countdownProgress <= 0) {
194
+ this.countdownProgress = 0;
195
+ clearInterval(this.countdownInterval);
196
+ }
197
+ }, intervalDuration);
198
+
199
+ // Set redirect timer
200
+ this.redirectTimer = setTimeout(() => {
201
+ console.log('Auto-redirecting to preferences...');
202
+ this.redirectToPreferences();
203
+ }, countdownDuration);
204
+ }
205
+
206
+ private redirectToPreferences(): void {
207
+ this.showSubmissionPopup = false;
208
+ this.router.navigate(['/userPreference'], {
209
+ queryParams: {
210
+ user_id: this.userId,
211
+ role: this.selectedOption
212
+ }
213
+ });
214
+ }
215
+
216
+ // NEW: Navigate directly from hero to preferences
217
+ goToPreferences(): void {
218
+ if (!this.userId) {
219
+ const uid = localStorage.getItem('user_id');
220
+ if (uid) this.userId = Number(uid);
221
+ }
222
+ const role = this.selectedOption || 'marriage';
223
+ this.router.navigate(['/userPreference'], {
224
+ queryParams: {
225
+ user_id: this.userId,
226
+ role
227
+ }
228
+ });
229
+ }
230
+
231
+ onSelect(role: string, mode: 'modal' | 'page' = 'page'): void {
232
+ this.selectedOption = role;
233
+ this.displayMode = mode;
234
+ this.isModalOpen = mode === 'modal';
235
+ this.isLoading = true;
236
+ this.fetchQuestions(role);
237
+ }
238
+
239
  ngOnInit(): void {
 
240
  const uid = localStorage.getItem('user_id');
241
  if (uid) {
242
+ this.userId = Number(uid);
243
+ }
244
+
245
+ this.selectedCategory = this.categoriesMeta[0]?.key || '';
246
+
247
+ // Restore answers from localStorage if available
248
+ const savedAnswers = localStorage.getItem('qa_answers');
249
+ if (savedAnswers) {
250
+ try {
251
+ this.answers = JSON.parse(savedAnswers);
252
+ } catch { }
253
  }
254
 
255
+ // If navigated with role param -> show full page (not popup)
256
+ this.route.queryParamMap.subscribe(params => {
257
+ const role = (params.get('role') || '').toLowerCase();
258
+ if (this.allowedRoles.has(role)) {
259
+ this.onSelect(role, 'page');
260
+ }
261
+ // Scroll to questions-section if scrollTo param is present
262
+ const scrollTo = params.get('scrollTo');
263
+ if (scrollTo === 'questions-section') {
264
+ // Wait for questions to load, then scroll
265
+ const scrollAfterLoad = () => {
266
+ if (this.selectedQuestions.length) {
267
+ setTimeout(() => this.scrollToQuestions(), 200);
268
+ } else {
269
+ setTimeout(scrollAfterLoad, 100);
270
+ }
271
+ };
272
+ scrollAfterLoad();
273
+ }
274
+ });
275
  }
276
 
277
+ selectCategory(key: string): void { this.selectedCategory = key; }
278
+ scrollToCategory(key: string): void { this.selectCategory(key); }
279
+
280
+ private getScrollOffset(): number {
281
+ const header = document.querySelector('.topbar') as HTMLElement | null;
282
+ const categoryNav = document.querySelector('.category-nav-wrapper') as HTMLElement | null;
283
+ let offset = 0;
284
+ if (header) offset += header.offsetHeight;
285
+ if (categoryNav) offset += categoryNav.offsetHeight;
286
+ return offset + 16; // Add some extra spacing
287
  }
288
 
289
+ scrollToQuestions(): void {
290
+ setTimeout(() => {
291
+ const el = document.getElementById('questions-section');
292
+ if (el) {
293
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
294
+ }
295
+ }, 100); // Delay to ensure DOM is rendered
296
+ }
297
+
298
+ trackQuestion(index: number, item: QAItem): string { return item.column_key; }
299
 
300
+ private applyMarriageOptionMapping(questions: QAItem[]): void {
301
+ const map: Record<string, string[] | undefined> = {
302
+ 'gender': ['Male', 'Female', 'Other'],
303
+ 'marital status': ['Single', 'Divorced', 'Widowed'],
304
+ 'education level': ['School', 'Diploma', 'Bachelor', 'Master', 'Doctorate'],
305
+ 'employment status': ['Student', 'Employed', 'Self-employed', 'Homemaker', 'Not working'],
306
+ 'family type': ['Nuclear', 'Joint', 'Extended'],
307
+ 'hobbies/interests': ['Music', 'Reading', 'Sports', 'Travel', 'Cooking', 'Art'],
308
+ 'conflict approach': ['Avoid', 'Discuss calmly', 'Decide fast', 'Problem-solving'],
309
+ 'financial style': ['Budget-oriented', 'Balanced', 'Spend-oriented'],
310
+ 'relocation willingness': ['Yes', 'No', 'Maybe']
311
+ };
312
+ questions.forEach(q => {
313
+ const key = q.label.toLowerCase().trim();
314
+ const opts = map[key];
315
+ if (opts) { q.input_type = 'select'; q.options = opts; }
316
  });
317
  }
318
 
319
  fetchQuestions(role: string): void {
320
  this.qaService.getQuestions(role).subscribe({
321
  next: (data: QAItem[]) => {
322
+ // Process questions to ensure proper formatting
323
+ if (role === 'marriage') this.applyMarriageOptionMapping(data);
324
  this.selectedQuestions = data;
325
+ this.answers = {};
326
+ data.forEach(q => { this.answers[q.column_key] = q.input_type === 'multiselect' ? [] : ''; });
327
+ this.isLoading = false;
328
+ this.attemptedSubmit = false;
329
+ if (role === 'marriage') {
330
+ this.displayMode = 'page';
331
+ this.isModalOpen = false;
332
+ } else if (this.displayMode === 'modal') {
333
+ this.isModalOpen = true;
334
+ } else {
335
+ this.isModalOpen = false;
336
+ }
337
+ this.selectedCategory = this.categoriesMeta[0]?.key || '';
338
+ this.filterQuestionsByCategory('personal');
339
  },
340
+ error: (err: HttpErrorResponse) => {
341
+ console.error('Error fetching questions:', err);
342
+ this.isLoading = false;
343
+ }
344
  });
345
  }
346
 
347
+ filterQuestionsByCategory(category: string): void {
348
+ const norm = category.replace(/\s+/g, '').toLowerCase();
349
+ this.filteredQuestions = this.selectedQuestions.filter(q => (q.category || '').replace(/\s+/g, '').toLowerCase() === norm);
350
+ }
351
+
352
+ getQuestionsByCategory(category: string): QAItem[] {
353
+ const norm = category.replace(/\s+/g, '').toLowerCase();
354
+ return this.selectedQuestions.filter(q => (q.category || '').replace(/\s+/g, '').toLowerCase() === norm);
355
+ }
356
+
357
+ onAnswerChange(key: string, value: any): void {
358
+ this.answers[key] = value;
359
+ localStorage.setItem('qa_answers', JSON.stringify(this.answers));
360
+ // Auto-move to next category if current is completed and not last
361
+ const currentIndex = this.categoriesMeta.findIndex(cat => cat.key === this.selectedCategory);
362
+ const currentCat = this.categoriesMeta[currentIndex];
363
+ const total = this.getCategoryRequiredCount(currentCat.key);
364
+ const answered = this.getCategoryAnsweredCount(currentCat.key);
365
+ if (total > 0 && answered === total && currentIndex < this.categoriesMeta.length - 1) {
366
+ // Move to next category
367
+ this.selectedCategory = this.categoriesMeta[currentIndex + 1].key;
368
+ }
369
+ }
370
+
371
+ // Add handler for Languages Spoken checkbox group
372
+ onLanguagesCheckboxChange(event: Event, key: string): void {
373
+ const input = event.target as HTMLInputElement;
374
+ if (!Array.isArray(this.answers[key])) {
375
+ this.answers[key] = [];
376
+ }
377
+ if (input.checked) {
378
+ if (!this.answers[key].includes(input.value)) {
379
+ this.answers[key].push(input.value);
380
+ }
381
+ } else {
382
+ this.answers[key] = this.answers[key].filter((v: string) => v !== input.value);
383
+ }
384
+ localStorage.setItem('qa_answers', JSON.stringify(this.answers));
385
+ this.onAnswerChange(key, this.answers[key]);
386
+ }
387
+
388
+ isCurrentTabComplete(): boolean {
389
+ const cat = this.categories[this.currentTabIndex];
390
+ return this.getQuestionsByCategory(cat).every(q => !q.required || (this.answers[q.column_key] ?? '') !== '');
391
+ }
392
+
393
+ isAnswered(q: QAItem): boolean {
394
+ const v = this.answers[q.column_key];
395
+ if (q.input_type === 'multiselect') {
396
+ return Array.isArray(v) && v.length > 0;
397
+ }
398
+ return v !== undefined && v !== null && v !== '';
399
+ }
400
+
401
+ goToNextTab(group: MatTabGroup): void {
402
+ if (!group || group.selectedIndex == null) return;
403
+ const next = group.selectedIndex + 1;
404
+ if (next < (group as any)._tabs.length) { group.selectedIndex = next; this.currentTabIndex = next; }
405
+ }
406
+
407
+ private normalizeForBackend(obj: Record<string, any>): Record<string, any> {
408
+ const out: Record<string, any> = {};
409
+ for (const [k, v] of Object.entries(obj)) {
410
+ if (!this.BIT_FIELDS.has(k)) {
411
+ out[k] = v;
412
+ continue;
413
+ }
414
+ if (v === undefined || v === null || v === '') { out[k] = null; continue; }
415
+ if (typeof v === 'boolean') { out[k] = v ? 1 : 0; continue; }
416
+ if (typeof v === 'number') { out[k] = v !== 0 ? 1 : 0; continue; }
417
+ if (typeof v === 'string') {
418
+ const s = v.trim().toLowerCase();
419
+ if (['yes', 'true', 'y', '1'].includes(s)) { out[k] = 1; continue; }
420
+ if (['no', 'false', 'n', '0'].includes(s)) { out[k] = 0; continue; }
421
+ out[k] = v;
422
+ continue;
423
+ }
424
+ out[k] = v;
425
+ }
426
+ return out;
427
+ }
428
 
429
+ submitAnswers(): void {
430
+ this.attemptedSubmit = true;
431
+ // Validate required fields
432
+ const incompleteRequired = this.selectedQuestions.filter(q => q.required && !this.isAnswered(q));
433
+ if (incompleteRequired.length) {
434
+ const first = incompleteRequired[0];
435
+ const el = document.getElementById('q-' + first.column_key);
436
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
437
  return;
438
  }
439
 
440
+ this.isLoading = true;
441
+ // Normalize answers (only affects BIT fields)
442
+ const normalizedAnswers = this.normalizeForBackend(this.answers);
443
+
444
+ // Build fields by column_key
445
  const fields: Record<string, any> = {};
446
+ this.selectedQuestions.forEach((q) => {
447
+ fields[q.column_key] = normalizedAnswers[q.column_key] ?? null;
 
 
 
 
448
  });
449
 
450
+ // Build payload for Marriage table
451
+ const payload = {
452
+ user_id: this.userId,
453
+ ...fields,
454
+ created_at: new Date().toISOString()
455
+ };
456
 
457
+ this.qaService.submitAnswers(this.selectedOption, this.userId, payload).subscribe({
 
458
  next: () => {
459
+ this.isLoading = false;
460
+ // Redirect to user preferences after successful submit
461
+ this.router.navigate(['/userPreference'], {
462
+ queryParams: {
463
+ user_id: this.userId,
464
+ role: this.selectedOption || 'marriage'
465
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  });
 
467
  },
468
  error: (err: HttpErrorResponse) => {
469
+ this.isLoading = false;
470
+ const reason = err.status === 0
471
+ ? 'Network/CORS or server unreachable'
472
+ : (err.error?.error || err.message || 'Server error');
473
+ console.error('Submit failed:', err);
474
+ alert(`Submit failed: ${reason}`);
475
  }
476
  });
477
  }
478
 
479
+ dismissProceedModal(): void {
480
+ this.showProceedModal = false;
481
  }
482
 
483
  closeModal(): void {
484
  this.isModalOpen = false;
485
+ if (this.displayMode === 'modal') { this.selectedQuestions = []; this.answers = {}; this.selectedOption = ''; }
486
  }
487
 
488
+ goHome(): void { this.router.navigate(['/']); }
489
+
490
+ getBackgroundForRole(role: string): string {
491
+ const backgrounds: Record<string, string> = {
492
+ marriage: 'assets/wedding.png',
493
+ interview: 'assets/interview.png',
494
+ partnership: 'assets/partnership.png'
495
+ };
496
+ return backgrounds[role] || 'assets/default-bg.png';
497
  }
498
 
499
+ getCategoryImage(key: string): string {
500
+ const images: Record<string, string> = {
501
+ personal: 'assets/marriage-match/17.png',
502
+ locationfamily: 'assets/marriage-match/12.png',
503
+ lifestyle: 'assets/marriage-match/7.png',
504
+ educationcareer: 'assets/marriage-match/15.png'
 
 
 
 
 
505
  };
506
+ return images[key] || 'assets/marriage-match/9.png';
507
+ }
508
+
509
+ getPlaceholder(question: QAItem): string {
510
+ if (question.placeholder) return question.placeholder;
511
+ switch (question.input_type) {
512
+ case 'date': return 'Select date';
513
+ case 'select': return 'Select option';
514
+ default: return 'Enter your answer';
515
+ }
516
+ }
517
+
518
+ showError(q: QAItem): boolean {
519
+ return this.attemptedSubmit && !!q.required && !this.isAnswered(q);
520
  }
521
  }
src/app/quiz/quiz.component.css DELETED
@@ -1,126 +0,0 @@
1
- /* General styling for the quiz container */
2
- .quiz-container {
3
- display: flex;
4
- justify-content: center;
5
- align-items: center;
6
- height: 100vh; /* Full viewport height */
7
- background-image: url('src/assets/background.png');
8
- background-size: cover; /* Ensures the image covers the entire area */
9
- background-position: center; /* Center the image */
10
- background-repeat: no-repeat; /* Prevents the image from repeating */
11
- margin: 0; /* Remove default margin */
12
- }
13
-
14
-
15
- /* Overlay for dimming the background */
16
- .quiz-overlay {
17
- position: absolute;
18
- top: 0;
19
- left: 0;
20
- width: 100%;
21
- height: 100%;
22
- background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black overlay */
23
- display: flex;
24
- justify-content: center;
25
- align-items: center;
26
- }
27
-
28
- /* Card for the quiz with soft green background */
29
- .quiz-card {
30
- background-color: #eaf6e2; /* Soft light green background */
31
- border-radius: 15px;
32
- padding: 30px;
33
- text-align: center;
34
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
35
- max-width: 450px;
36
- width: 100%;
37
- position: relative;
38
- }
39
-
40
- /* Profile image */
41
- .profile-picture {
42
- width: 80px;
43
- height: 80px;
44
- border-radius: 50%;
45
- object-fit: cover;
46
- margin-bottom: 20px;
47
- }
48
-
49
- /* Title styling */
50
- .quiz-title {
51
- font-size: 22px;
52
- font-weight: bold;
53
- color: #4caf50; /* Green color */
54
- margin-bottom: 20px;
55
- }
56
-
57
- /* Question text styling */
58
- .question {
59
- font-size: 18px;
60
- color: #333;
61
- margin-bottom: 30px;
62
- }
63
-
64
- /* Button styling for options */
65
- .option-button {
66
- background-color: #4caf50;
67
- color: white;
68
- padding: 12px 20px;
69
- margin: 10px;
70
- border: none;
71
- border-radius: 10px;
72
- width: 100%;
73
- text-align: center;
74
- cursor: pointer;
75
- transition: background-color 0.3s ease;
76
- }
77
-
78
- .option-button:hover {
79
- background-color: #45a049;
80
- }
81
-
82
- /* Button styling for "Next" and "Submit" */
83
- .next-button, .submit-button {
84
- background-color: #4caf50;
85
- color: white;
86
- padding: 12px 20px;
87
- border: none;
88
- border-radius: 10px;
89
- margin-top: 20px;
90
- width: 100%;
91
- cursor: pointer;
92
- transition: background-color 0.3s ease;
93
- }
94
-
95
- .next-button:hover, .submit-button:hover {
96
- background-color: #45a049;
97
- }
98
-
99
- /* Success Popup */
100
- .success-popup {
101
- position: absolute;
102
- top: 50%;
103
- left: 50%;
104
- transform: translate(-50%, -50%);
105
- background: rgba(0, 0, 0, 0.8);
106
- color: white;
107
- padding: 20px;
108
- border-radius: 10px;
109
- text-align: center;
110
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
111
- }
112
-
113
- .success-popup button {
114
- background-color: #4caf50;
115
- color: white;
116
- padding: 10px 20px;
117
- border: none;
118
- border-radius: 5px;
119
- cursor: pointer;
120
- margin-top: 15px;
121
- transition: background-color 0.3s ease;
122
- }
123
-
124
- .success-popup button:hover {
125
- background-color: #45a049;
126
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/quiz/quiz.component.html DELETED
@@ -1,35 +0,0 @@
1
- <div class="quiz-container">
2
- <div class="quiz-overlay">
3
- <div class="quiz-card">
4
- <h1 class="quiz-title">QUIZ</h1>
5
- <div class="question-container">
6
- <p class="question">{{ questions[currentQuestionIndex].question }}</p>
7
- <div class="options">
8
- <button *ngFor="let option of questions[currentQuestionIndex].options"
9
- class="option-button"
10
- (click)="selectOption(option)">
11
- {{ option }}
12
- </button>
13
- </div>
14
- <button *ngIf="currentQuestionIndex < questions.length - 1"
15
- class="next-button"
16
- [disabled]="!isOptionSelected"
17
- (click)="nextQuestion()">
18
- Next
19
- </button>
20
- <button *ngIf="currentQuestionIndex === questions.length - 1"
21
- class="submit-button"
22
- [disabled]="!isOptionSelected"
23
- (click)="submitQuiz()">
24
- Submit
25
- </button>
26
- </div>
27
- </div>
28
- </div>
29
- </div>
30
-
31
- <!-- Success Popup -->
32
- <div *ngIf="isSubmitted" class="success-popup">
33
- <p>Your test is submitted!</p>
34
- <button (click)="closePopup()">Close</button>
35
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/quiz/quiz.component.ts DELETED
@@ -1,107 +0,0 @@
1
- import { Component, OnInit } from '@angular/core';
2
- import { Router } from '@angular/router';
3
- import { FormsModule } from '@angular/forms';
4
- import { CommonModule } from '@angular/common';
5
- import { RouterLink } from '@angular/router';
6
- interface QuizQuestion {
7
- question: string;
8
- options: string[];
9
- // store the selected answer; start as null and set to a string later
10
- answer: string | null;
11
- }
12
-
13
- @Component({
14
- selector: 'app-quiz',
15
- standalone: true,
16
- imports: [FormsModule, CommonModule, RouterLink],
17
- templateUrl: './quiz.component.html',
18
- styleUrls: ['./quiz.component.css'],
19
- })
20
- export class QuizComponent implements OnInit {
21
-
22
- constructor(public router: Router) { }
23
- ngOnInit(): void { }
24
- questions: QuizQuestion[] = [
25
- {
26
- question: 'You are leading a small team. A new member is quiet and avoids discussions. What will you do first?',
27
- options: [
28
- 'Assign a simple task and review later',
29
- 'Speak privately to understand concerns',
30
- 'Ask them to present in the next meeting',
31
- 'Ignore it and focus on deadlines'
32
- ],
33
- answer: null,
34
- },
35
- {
36
- question: 'A stakeholder requests a big change late in the sprint. What is your approach?',
37
- options: [
38
- 'Accept it immediately',
39
- 'Reject it immediately',
40
- 'Evaluate impact and negotiate scope/time',
41
- 'Ask the team to work overtime'
42
- ],
43
- answer: null,
44
- },
45
- {
46
- question: 'Two teammates disagree on a solution. How do you proceed?',
47
- options: [
48
- 'Pick the faster option',
49
- 'Let them resolve it alone',
50
- 'Facilitate a short decision session with facts',
51
- 'Escalate to management'
52
- ],
53
- answer: null,
54
- },
55
- {
56
- question: 'You find a recurring production bug. What is your first step?',
57
- options: [
58
- 'Hotfix every time',
59
- 'Add logs and create a root-cause ticket',
60
- 'Disable the feature',
61
- 'Rollback to an old release'
62
- ],
63
- answer: null,
64
- },
65
- {
66
- question: 'Your release is on time but test coverage is low. What will you do?',
67
- options: [
68
- 'Release as-is',
69
- 'Block the release',
70
- 'Risk-assess, add critical tests, and plan coverage',
71
- 'Let QA handle it later'
72
- ],
73
- answer: null,
74
- },
75
- ];
76
-
77
- currentQuestionIndex = 0;
78
- isOptionSelected = false; // controls Next/Submit button enabled state
79
- isSubmitted = false; // controls the success popup
80
-
81
- selectOption(option: string): void {
82
- this.questions[this.currentQuestionIndex].answer = option; // type-safe: string | null
83
- this.isOptionSelected = true;
84
- }
85
-
86
- nextQuestion(): void {
87
- if (!this.isOptionSelected) return;
88
- this.currentQuestionIndex++;
89
-
90
- // Reset the enable/disable state for the next question
91
- const next = this.questions[this.currentQuestionIndex];
92
- this.isOptionSelected = !!next?.answer; // true if already answered, else false
93
- }
94
-
95
- submitQuiz(): void {
96
- if (!this.isOptionSelected) return;
97
- this.isSubmitted = true; // shows the “Your test is submitted!” popup
98
- }
99
-
100
- closePopup(): void {
101
- this.isSubmitted = false;
102
- // Optionally navigate or reset after closing:
103
- // this.currentQuestionIndex = 0;
104
- // this.questions.forEach(q => (q.answer = null));
105
- // this.isOptionSelected = false;
106
- }
107
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/quiz/quiz.service.spec.ts DELETED
@@ -1,16 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
-
3
- import { QuizService } from './quiz.service';
4
-
5
- describe('QuizService', () => {
6
- let service: QuizService;
7
-
8
- beforeEach(() => {
9
- TestBed.configureTestingModule({});
10
- service = TestBed.inject(QuizService);
11
- });
12
-
13
- it('should be created', () => {
14
- expect(service).toBeTruthy();
15
- });
16
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/quiz/quiz.service.ts DELETED
@@ -1,36 +0,0 @@
1
- import { Injectable } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
3
- import { Observable } from 'rxjs';
4
-
5
- @Injectable({
6
- providedIn: 'root'
7
- })
8
- export class QuizService {
9
-
10
- // hf.space => HF backend; else => local Flask
11
- private baseUrl = location.hostname.endsWith('hf.space')
12
- ? 'https://pykara-py-match-backend.hf.space'
13
- : 'http://localhost:5000';
14
-
15
- //private apiUrl = 'http://localhost:5000'; // Your Flask backend URL
16
-
17
- constructor(private http: HttpClient) { }
18
-
19
- // Start the quiz by sending the profile and role
20
- startQuiz(nQuestions: number, batchSize: number, role: string, profile: any): Observable<any> {
21
- return this.http.post<any>(`${this.baseUrl}/q/start`, {
22
- n_questions: nQuestions,
23
- batch_size: batchSize,
24
- role: role,
25
- profile: profile
26
- });
27
- }
28
-
29
- // Fetch next question
30
- nextQuestion(sessionId: string, selectedColor: string): Observable<any> {
31
- return this.http.post<any>(`${this.baseUrl}/q/next`, {
32
- session_id: sessionId,
33
- selected_color: selectedColor
34
- });
35
- }
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/services/llm-qa.service.ts CHANGED
@@ -63,25 +63,23 @@ export interface NextResponse {
63
 
64
  @Injectable({ providedIn: 'root' })
65
  export class LlmQaService {
 
 
66
 
67
- private readonly api = location.hostname.endsWith('hf.space')
68
- ? 'https://pykara-py-match-backend.hf.space'
69
- : 'http://localhost:5000';
70
-
71
- private readonly baseUrl = `${this.api}/api/questions`;
72
- // Change this to your API origin if needed.
73
- //private readonly api = 'http://localhost:5000';
74
- //private baseUrl = 'http://127.0.0.1:5000/api/questions';
75
- //test
76
- constructor(private http: HttpClient) { }
77
-
78
 
 
 
79
 
80
- start(req: StartRequest) {
81
  return this.http.post<StartResponse>(`${this.api}/llm/start`, req);
82
  }
83
 
84
- next(req: NextRequest) {
85
  return this.http.post<NextResponse>(`${this.api}/llm/next`, req);
86
  }
87
  }
 
63
 
64
  @Injectable({ providedIn: 'root' })
65
  export class LlmQaService {
66
+ private readonly api: string;
67
+ private readonly baseUrl: string;
68
 
69
+ constructor(private http: HttpClient) {
70
+ // Initialize URLs based on the current hostname
71
+ this.api = typeof location !== 'undefined' && location.hostname.endsWith('hf.space')
72
+ ? 'https://pykara-py-match-backend.hf.space'
73
+ : 'http://localhost:5000';
 
 
 
 
 
 
74
 
75
+ this.baseUrl = `${this.api}/api/questions`;
76
+ }
77
 
78
+ start(req: StartRequest): Observable<StartResponse> {
79
  return this.http.post<StartResponse>(`${this.api}/llm/start`, req);
80
  }
81
 
82
+ next(req: NextRequest): Observable<NextResponse> {
83
  return this.http.post<NextResponse>(`${this.api}/llm/next`, req);
84
  }
85
  }
src/app/user-preferences/user-preferences.component.css ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reuse Question Answer styles for shared hero and info-card UI */
2
+ @import url('../question-answer/question-answer.component.css');
3
+
4
+ /* Professional Styles for User Preferences Component - Light theme to match question-answer */
5
+ .preferences-container {
6
+ background: #f8fafc;
7
+ padding: 7.5vw 0 1.5vw 0;
8
+ color: #1a202c;
9
+ }
10
+
11
+ .preferences-content {
12
+ background: white;
13
+ border-radius: 20px;
14
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
15
+ overflow: hidden;
16
+ border: 1px solid #e3e5e8;
17
+ }
18
+
19
+ .preferences-section {
20
+ padding: 3rem;
21
+ }
22
+
23
+ .preferences-header {
24
+ display: flex;
25
+ gap: 3rem;
26
+ margin-bottom: unset !important;
27
+ align-items: flex-start;
28
+ }
29
+
30
+ .preferences-image {
31
+ flex: 0 0 300px;
32
+ border-radius: 16px;
33
+ overflow: hidden;
34
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
35
+ position: relative;
36
+ }
37
+
38
+ .preferences-image::before {
39
+ content: "";
40
+ position: absolute;
41
+ inset: 0;
42
+ background: linear-gradient(135deg, rgba(243, 165, 76, 0.1), rgba(0, 255, 136, 0.1));
43
+ z-index: 1;
44
+ }
45
+
46
+ .preferences-image img {
47
+ width: 100%;
48
+ height: 200px;
49
+ object-fit: cover;
50
+ position: relative;
51
+ z-index: 0;
52
+ border-radius: 16px;
53
+ }
54
+
55
+ .preferences-info {
56
+ flex: 1;
57
+ }
58
+
59
+ .section-description {
60
+ color: #4a5568;
61
+ font-size: 1rem;
62
+ font-weight: 500;
63
+ line-height: 1.6;
64
+ margin: 0;
65
+ }
66
+
67
+ /* Progress Indicator (match QA) */
68
+ .progress-indicator {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 1rem;
72
+ margin-top: 1rem;
73
+ }
74
+
75
+ .progress-bar {
76
+ flex: 1;
77
+ height: 8px;
78
+ background: #e2e8f0;
79
+ border-radius: 4px;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .progress-fill {
84
+ height: 100%;
85
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
86
+ border-radius: 4px;
87
+ transition: width 0.4s ease;
88
+ }
89
+
90
+ .progress-indicator small {
91
+ color: #4a5568;
92
+ font-weight: 600;
93
+ min-width: 80px;
94
+ }
95
+
96
+ /* Category Sections */
97
+ .category-section {
98
+ margin-bottom: 3rem;
99
+ padding-bottom: 2rem;
100
+ border-bottom: 1px solid #e2e8f0;
101
+ }
102
+
103
+ .category-section:last-of-type {
104
+ border-bottom: none;
105
+ margin-bottom: 2rem;
106
+ }
107
+
108
+ /* category-title is a button; reset to look like a heading */
109
+ .category-title {
110
+ background: none;
111
+ border: none;
112
+ padding: 0;
113
+ margin: 0 0 1.5rem 0;
114
+ text-align: left;
115
+ font: inherit;
116
+ cursor: default;
117
+
118
+ font-size: 1.5rem;
119
+ font-weight: 700;
120
+ color: #2d3748;
121
+ padding-bottom: 0.5rem;
122
+ border-bottom: 2px solid #f3a54c;
123
+ display: inline-block;
124
+ }
125
+
126
+ .category-title:focus {
127
+ outline: none;
128
+ box-shadow: none;
129
+ }
130
+
131
+ /* Preferences Form */
132
+ .preferences-form {
133
+ margin-top: 2rem;
134
+ }
135
+
136
+ /* Form Controls */
137
+ .form-control {
138
+ width: 100%;
139
+ padding: 0.875rem 1rem;
140
+ border: 2px solid #e2e8f0;
141
+ border-radius: 12px;
142
+ font-size: 0.95rem;
143
+ transition: all 0.3s ease;
144
+ background: white;
145
+ color: #1a202c;
146
+ font-family: inherit;
147
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
148
+ }
149
+
150
+ .form-control:focus {
151
+ outline: none;
152
+ border-color: #f3a54c;
153
+ box-shadow: 0 0 0 3px rgba(243, 165, 76, 0.1);
154
+ }
155
+
156
+ .form-control::placeholder {
157
+ color: #718096;
158
+ }
159
+
160
+ /* Checkbox Options */
161
+ .checkbox-options {
162
+ display: flex;
163
+ flex-direction: column;
164
+ align-items: stretch;
165
+ }
166
+
167
+ .checkbox-options .question-card {
168
+ width: 100%;
169
+ min-width: 0;
170
+ box-sizing: border-box;
171
+ max-height: 260px;
172
+ overflow-y: auto;
173
+ overflow-x: hidden;
174
+ }
175
+
176
+ .checkbox-option {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 0.75rem;
180
+ padding: 0.2vw;
181
+ background: #f7fafc;
182
+ border-radius: 12px;
183
+ cursor: pointer;
184
+ transition: all 0.3s ease;
185
+ position: relative;
186
+ border: 2px solid transparent;
187
+ color: #2d3748;
188
+ margin-bottom: unset !important;
189
+ }
190
+ .checkbox-options {
191
+ overflow: scroll;
192
+ height: 8vh;
193
+ }
194
+ .checkbox-option:hover {
195
+ background: #edf2f7;
196
+ border-color: #f3a54c;
197
+ transform: translateY(-2px);
198
+ box-shadow: 0 4px 12px rgba(243, 165, 76, 0.15);
199
+ }
200
+
201
+ .checkbox-option input {
202
+ position: absolute;
203
+ opacity: 0;
204
+ cursor: pointer;
205
+ }
206
+
207
+ .checkmark {
208
+ position: relative;
209
+ height: 20px;
210
+ width: 20px;
211
+ background-color: white;
212
+ border: 2px solid #cbd5e0;
213
+ border-radius: 6px;
214
+ transition: all 0.3s ease;
215
+ }
216
+
217
+ .checkbox-option input:checked ~ .checkmark {
218
+ background-color: #f3a54c;
219
+ border-color: #f3a54c;
220
+ }
221
+
222
+ .checkmark:after {
223
+ content: "";
224
+ position: absolute;
225
+ display: none;
226
+ left: 6px;
227
+ top: 2px;
228
+ width: 4px;
229
+ height: 8px;
230
+ border: solid white;
231
+ border-width: 0 2px 2px 0;
232
+ transform: rotate(45deg);
233
+ }
234
+
235
+ .checkbox-option input:checked ~ .checkmark:after {
236
+ display: block;
237
+ }
238
+
239
+ /* Questions Grid - ensure consistent card widths */
240
+ .questions-grid {
241
+ display: grid;
242
+ grid-template-columns: repeat(4, minmax(0, 1fr));
243
+ gap: unset !important;
244
+ margin-bottom: 2rem;
245
+ align-items: start;
246
+ justify-items: stretch;
247
+ }
248
+
249
+ .question-card {
250
+ background: white;
251
+ border: 2px solid #f7fafc;
252
+ border-radius: 16px;
253
+ padding: 1.5rem;
254
+ transition: all 0.3s ease;
255
+ position: relative;
256
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
257
+ width: 100%;
258
+ box-sizing: border-box;
259
+ height: 18vh;
260
+ overflow-y: auto;
261
+ color: #1a202c;
262
+ }
263
+
264
+ .question-card:hover {
265
+ border-color: #f3a54c;
266
+ transform: translateY(-2px);
267
+ box-shadow: 0 8px 25px rgba(243, 165, 76, 0.15);
268
+ }
269
+
270
+ .question-card label {
271
+ display: flex;
272
+ margin-bottom: 0.75rem;
273
+ font-weight: 600;
274
+ color: #2d3748;
275
+ font-size: 0.95rem;
276
+ /*line-height: 1.4;*/
277
+ overflow-wrap: anywhere;
278
+ }
279
+
280
+ .question-card.answered {
281
+ border-color: #00d68f !important;
282
+ background: linear-gradient(135deg, #e6fff5 60%, #f7fafc 100%);
283
+ box-shadow: 0 4px 16px rgba(0, 214, 143, 0.12);
284
+ }
285
+
286
+ /* Submit Section */
287
+ .submit-section {
288
+ text-align: center;
289
+ padding: 2rem 0 0;
290
+ border-top: 1px solid #e2e8f0;
291
+ margin-top: 2rem;
292
+ }
293
+
294
+ .submit-btn {
295
+ padding: 1rem 2.5rem;
296
+ font-size: 1.1rem;
297
+ background: linear-gradient(135deg, #f3a54c, #ffcb7a);
298
+ color: #111;
299
+ border: none;
300
+ border-radius: 12px;
301
+ font-weight: 600;
302
+ cursor: pointer;
303
+ transition: all 0.3s ease;
304
+ box-shadow: 0 4px 15px rgba(243, 165, 76, 0.3);
305
+ }
306
+
307
+ .submit-btn:hover {
308
+ transform: translateY(-2px);
309
+ box-shadow: 0 8px 25px rgba(243, 165, 76, 0.4);
310
+ }
311
+
312
+ .submit-btn:disabled {
313
+ opacity: 0.6;
314
+ cursor: not-allowed;
315
+ transform: none !important;
316
+ }
317
+
318
+ /* Modal Styles - light */
319
+ .modal-backdrop {
320
+ position: fixed;
321
+ inset: 0;
322
+ background: rgba(0, 0, 0, 0.6);
323
+ display: flex;
324
+ justify-content: center;
325
+ align-items: center;
326
+ z-index: 1000;
327
+ }
328
+
329
+ .modal {
330
+ background: white;
331
+ padding: 3rem;
332
+ border-radius: 16px;
333
+ max-width: 460px;
334
+ text-align: center;
335
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
336
+ border: 1px solid #e3e5e8;
337
+ }
338
+
339
+ .success-title {
340
+ color: #48bb78;
341
+ font-size: 1.5rem;
342
+ margin-bottom: 1rem;
343
+ }
344
+
345
+ .error-title {
346
+ color: #e53e3e;
347
+ font-size: 1.5rem;
348
+ margin-bottom: 1rem;
349
+ }
350
+
351
+ .modal p {
352
+ color: #4a5568;
353
+ margin-bottom: 2rem;
354
+ line-height: 1.5;
355
+ }
356
+
357
+ .modal-actions {
358
+ display: flex;
359
+ justify-content: center;
360
+ gap: 1rem;
361
+ }
362
+
363
+ /* Empty State */
364
+ .empty-state {
365
+ text-align: center;
366
+ padding: 4rem 2rem;
367
+ color: #718096;
368
+ }
369
+
370
+ .empty-state-text {
371
+ font-size: 1.1rem;
372
+ }
373
+
374
+ /* Responsive Design for Preferences */
375
+ @media (max-width: 1024px) {
376
+ .questions-grid {
377
+ grid-template-columns: repeat(2, minmax(0, 1fr));
378
+ }
379
+ }
380
+
381
+ @media (max-width: 768px) {
382
+ .preferences-header {
383
+ flex-direction: column;
384
+ gap: 2rem;
385
+ }
386
+
387
+ .preferences-image {
388
+ flex: none;
389
+ width: 100%;
390
+ }
391
+
392
+ .preferences-section {
393
+ padding: 2rem;
394
+ }
395
+
396
+ .questions-grid {
397
+ grid-template-columns: 1fr;
398
+ }
399
+
400
+ .category-title {
401
+ font-size: 1.3rem;
402
+ }
403
+
404
+ .modal {
405
+ margin: 1rem;
406
+ padding: 2rem;
407
+ }
408
+ }
409
+
410
+ @media (max-width: 480px) {
411
+ .preferences-section {
412
+ padding: 1.5rem;
413
+ }
414
+
415
+ .questions-grid {
416
+ gap: 1rem;
417
+ }
418
+
419
+ .question-card {
420
+ padding: 1.25rem;
421
+ }
422
+
423
+ .category-title {
424
+ font-size: 1.2rem;
425
+ }
426
+
427
+ .progress-indicator {
428
+ flex-direction: column;
429
+ align-items: stretch;
430
+ gap: 0.5rem;
431
+ }
432
+
433
+ .modal {
434
+ padding: 1.5rem;
435
+ }
436
+ }
437
+
438
+
src/app/user-preferences/user-preferences.component.html ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="professional-page">
2
+ <!-- Loader Overlay -->
3
+ <div *ngIf="!questions.length" class="loader-overlay">
4
+ <div class="loader-container">
5
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
6
+ </div>
7
+ </div>
8
+
9
+ <!-- Loader Overlay (same as llm-quiz) -->
10
+ <div class="loader-overlay" *ngIf="isLoading">
11
+ <div class="loader-container">
12
+ <img src="assets/Loader.gif" alt="Loading..." class="loader-gif" />
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Hero Section -->
17
+ <section class="hero">
18
+ <div class="content" [ngStyle]="heroContentStyle">
19
+ <div class="info-card">
20
+ <div class="info-card__icon">
21
+ <i class="fa-solid fa-sliders"></i>
22
+ </div>
23
+ <div class="info-card__content">
24
+ <h1 class="info-card__title"><i class="fa-solid fa-sliders"></i> Preferences</h1>
25
+ <p class="info-card__text">
26
+ Tell us what matters to you most. These preferences guide better matching and help highlight alignment.
27
+ </p>
28
+ <div class="info-card__actions">
29
+ <button class="btn btn-primary" (click)="scrollToPrefSection()">
30
+ Set Preferences
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </section>
37
+
38
+ <!-- Preferences Form -->
39
+ <div id="pref-section" class="preferences-container" *ngIf="questions.length">
40
+ <div class="container">
41
+ <!-- Category Navigation (like QA) -->
42
+ <div class="category-nav-wrapper">
43
+ <button class="btn btn-secondary" style="margin-bottom: 1rem;" (click)="goBackToQuestions()">
44
+ <i class="fa fa-arrow-left"></i> Go Back
45
+ </button>
46
+ <nav class="category-nav" aria-label="Question categories">
47
+ <button *ngFor="let cat of categoriesMeta" type="button"
48
+ (click)="selectCategory(cat.key)"
49
+ [class.active]="selectedCategory === cat.key"
50
+ [class.completed]="getCategoryAnsweredCount(cat.key) === getQuestionsByCategory(cat.key).length && getQuestionsByCategory(cat.key).length > 0"
51
+ [attr.aria-selected]="selectedCategory === cat.key"
52
+ class="nav-chip">
53
+ <i class="fa-solid" [ngClass]="cat.icon"></i>
54
+ <span>{{ cat.title }}</span>
55
+ <span class="chip-progress" *ngIf="getQuestionsByCategory(cat.key).length">
56
+ {{ getCategoryAnsweredCount(cat.key) }}/{{ getQuestionsByCategory(cat.key).length }}
57
+ </span>
58
+ </button>
59
+ </nav>
60
+ </div>
61
+
62
+ <div class="preferences-content category-content">
63
+ <ng-container *ngFor="let cat of categoriesMeta; let last = last">
64
+ <section *ngIf="selectedCategory === cat.key" class="preferences-section category-section">
65
+ <div class="preferences-header category-header">
66
+ <div class="preferences-image category-image">
67
+ <img [src]="getCategoryImage(cat.key)" [alt]="cat.title" loading="lazy" />
68
+ </div>
69
+ <div class="preferences-info category-info">
70
+ <div class="title-section">
71
+ <h2><i class="fa-solid" [ngClass]="cat.icon"></i> {{ cat.title }}</h2>
72
+ <p class="section-description">Configure your matching preferences for this section.</p>
73
+ <div class="progress-indicator">
74
+ <div class="progress-bar">
75
+ <div class="progress-fill" [style.width.%]="getCategoryPercent(cat.key)"></div>
76
+ </div>
77
+ <small>{{ getCategoryProgressLabel(cat.key) }}</small>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <form [formGroup]="form" (ngSubmit)="submit()" class="preferences-form">
84
+ <div class="questions-grid">
85
+ <div class="question-card" *ngFor="let q of getQuestionsByCategory(cat.key)">
86
+ <label [attr.for]="q.column_key">{{ q.question }}</label>
87
+
88
+ <!-- Select -->
89
+ <select *ngIf="q.input_type === 'select'"
90
+ [formControlName]="q.column_key"
91
+ [id]="q.column_key"
92
+ class="form-control"
93
+ (change)="onInputChange(q.column_key)">
94
+ <option value="">Select an option</option>
95
+ <option *ngFor="let opt of getOptions(q)" [value]="opt">{{ opt }}</option>
96
+ </select>
97
+
98
+ <!-- Text -->
99
+ <input *ngIf="q.input_type === 'text'"
100
+ type="text"
101
+ [formControlName]="q.column_key"
102
+ [id]="q.column_key"
103
+ placeholder="Enter your preference"
104
+ class="form-control"
105
+ (input)="onInputChange(q.column_key)" />
106
+
107
+ <!-- Multi select (checkbox group) -->
108
+ <div *ngIf="q.input_type === 'multi_select'" class="checkbox-options">
109
+ <label class="checkbox-option" *ngFor="let opt of getOptions(q)">
110
+ <input type="checkbox"
111
+ [value]="opt"
112
+ (change)="onCheckboxChange($event, q.column_key); onInputChange(q.column_key)" />
113
+ <span class="checkmark"></span>
114
+ {{ opt }}
115
+ </label>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="submit-section" *ngIf="last">
121
+ <button class="btn submit-btn" type="submit">
122
+ <i class="fa-solid fa-floppy-disk"></i> Save Preferences
123
+ </button>
124
+ </div>
125
+ </form>
126
+ </section>
127
+ </ng-container>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="empty-state" *ngIf="!questions.length && !isLoading">
133
+ <p class="empty-state-text">Loading your preference questions...</p>
134
+ </div>
135
+ </div>
src/app/user-preferences/user-preferences.component.ts ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit, Renderer2, ElementRef } from '@angular/core';
2
+ import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { CommonModule } from '@angular/common';
4
+ import { Router, ActivatedRoute } from '@angular/router';
5
+ import { UserPreferencesService, ExpectationQuestion } from './user-preferences.service';
6
+
7
+ @Component({
8
+ selector: 'app-user-preferences',
9
+ standalone: true,
10
+ imports: [CommonModule, ReactiveFormsModule],
11
+ templateUrl: './user-preferences.component.html',
12
+ styleUrls: ['./user-preferences.component.css']
13
+ })
14
+ export class UserPreferencesComponent implements OnInit {
15
+ questions: ExpectationQuestion[] = [];
16
+ form!: FormGroup;
17
+ user_id = 1; // fallback; will be overridden by query/localStorage
18
+ role: string | null = null;
19
+ isLoading = true;
20
+ showModal = false;
21
+ progress = 0;
22
+ savedSuccessfully = false;
23
+
24
+ // Merge Family and Personality & Values into one tab
25
+ categoriesMeta: Array<{ key: string; title: string; icon: string }> = [
26
+ { key: 'personal_education', title: 'Personal & Education', icon: 'fa-user-graduate' },
27
+ { key: 'family_values', title: 'Family & Values', icon: 'fa-people-roof' },
28
+ { key: 'lifestyle', title: 'Lifestyle & Habits', icon: 'fa-heart' },
29
+ { key: 'finances_dealbreakers', title: 'Finances & Deal Breakers', icon: 'fa-sack-dollar' },
30
+ { key: 'interests', title: 'Interests & Social', icon: 'fa-bullseye' }
31
+ ];
32
+ selectedCategory: string = 'personal_education';
33
+
34
+ constructor(
35
+ private service: UserPreferencesService,
36
+ private fb: FormBuilder,
37
+ private router: Router,
38
+ private route: ActivatedRoute,
39
+ private renderer: Renderer2,
40
+ private el: ElementRef
41
+ ) { }
42
+
43
+ // Match QA: background helpers
44
+ get heroBackgroundStyle() {
45
+ const role = (this.role || 'marriage').toLowerCase();
46
+ const img = this.getBackgroundForRole(role);
47
+ return { 'background-image': `linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)), url(${img})` } as { [k: string]: string };
48
+ }
49
+
50
+ get heroContentStyle() {
51
+ return {
52
+ ...this.heroBackgroundStyle,
53
+ 'background-repeat': 'no-repeat',
54
+ 'background-position': 'center center',
55
+ 'background-attachment': 'fixed',
56
+ 'background-size': 'cover',
57
+ 'width': '100%',
58
+ 'height': '100vh'
59
+ } as { [k: string]: string };
60
+ }
61
+
62
+ private getBackgroundForRole(role: string): string {
63
+ const backgrounds: Record<string, string> = {
64
+ marriage: 'assets/wedding.png',
65
+ interview: 'assets/interview.png',
66
+ partnership: 'assets/partnership.png'
67
+ };
68
+ return backgrounds[role] || 'assets/default-bg.png';
69
+ }
70
+
71
+ ngOnInit(): void {
72
+ // Enhanced parameter capture - check both query params and localStorage
73
+ this.route.queryParamMap.subscribe(params => {
74
+ const qpUser = params.get('user_id');
75
+ const qpRole = params.get('role');
76
+
77
+ if (qpUser) {
78
+ this.user_id = Number(qpUser);
79
+ // Also store in localStorage for consistency
80
+ localStorage.setItem('user_id', qpUser);
81
+ } else {
82
+ // Fallback to localStorage
83
+ const lsUser = localStorage.getItem('user_id');
84
+ if (lsUser) this.user_id = Number(lsUser);
85
+ }
86
+
87
+ this.role = qpRole;
88
+
89
+ console.log('UserPreferences: user_id=', this.user_id, 'role=', this.role);
90
+ });
91
+
92
+ this.service.getQuestions().subscribe({
93
+ next: (data) => {
94
+ this.questions = data;
95
+ this.buildForm();
96
+ this.setInitialSelectedCategory();
97
+ this.isLoading = false;
98
+ },
99
+ error: () => {
100
+ this.isLoading = false;
101
+ }
102
+ });
103
+ }
104
+
105
+ buildForm(): void {
106
+ const group: any = {};
107
+ this.questions.forEach(q => {
108
+ if (q.input_type === 'multi_select') {
109
+ group[q.column_key] = this.fb.array([]);
110
+ } else {
111
+ group[q.column_key] = new FormControl('');
112
+ }
113
+ });
114
+ this.form = this.fb.group(group);
115
+
116
+ // Initialize progress after form is built
117
+ this.updateProgress();
118
+
119
+ // Keep progress up-to-date for all input types
120
+ this.form.valueChanges.subscribe(() => this.updateProgress());
121
+ }
122
+
123
+ // Normalize options to a string array for template binding
124
+ getOptions(q: ExpectationQuestion): string[] {
125
+ const opts = q.options;
126
+ if (Array.isArray(opts)) return (opts as string[]).filter(Boolean);
127
+ if (typeof opts === 'string') return opts.split(',').map(s => s.trim()).filter(Boolean);
128
+ return [];
129
+ }
130
+
131
+ // Get questions by category
132
+ getQuestionsByCategory(category: string): ExpectationQuestion[] {
133
+ if (category === 'personal_education') {
134
+ return this.questions.filter(q => q.category === 'demographics' || q.category === 'educationcareer');
135
+ }
136
+ if (category === 'finances_dealbreakers') {
137
+ return this.questions.filter(q => q.category === 'finances' || q.category === 'dealbreakers');
138
+ }
139
+ if (category === 'family_values') {
140
+ return this.questions.filter(q => q.category === 'family' || q.category === 'values');
141
+ }
142
+ return this.questions.filter(q => q.category === category);
143
+ }
144
+
145
+ selectCategory(key: string): void {
146
+ this.selectedCategory = key;
147
+ }
148
+
149
+ private setInitialSelectedCategory(): void {
150
+ const firstWithQuestions = this.categoriesMeta.find(c => this.getQuestionsByCategory(c.key).length > 0);
151
+ if (firstWithQuestions) {
152
+ this.selectedCategory = firstWithQuestions.key;
153
+ }
154
+ }
155
+
156
+ getCategoryAnsweredCount(key: string): number {
157
+ const qs = this.getQuestionsByCategory(key);
158
+ return qs.reduce((acc, q) => {
159
+ const ctrl = this.form?.get(q.column_key);
160
+ if (!ctrl) return acc;
161
+ const val = ctrl.value;
162
+ if (Array.isArray(val)) return acc + (val.length > 0 ? 1 : 0);
163
+ return acc + (!!val && val.toString().trim().length > 0 ? 1 : 0);
164
+ }, 0);
165
+ }
166
+
167
+ getCategoryPercent(key: string): number {
168
+ const total = this.getQuestionsByCategory(key).length || 1;
169
+ const answered = this.getCategoryAnsweredCount(key);
170
+ return Math.round((answered / total) * 100);
171
+ }
172
+
173
+ getCategoryProgressLabel(key: string): string {
174
+ const total = this.getQuestionsByCategory(key).length;
175
+ const answered = this.getCategoryAnsweredCount(key);
176
+ return `${answered}/${total} answered`;
177
+ }
178
+
179
+ getCategoryImage(key: string): string {
180
+ const images: Record<string, string> = {
181
+ personal_education: 'assets/marriage-match/1.png',
182
+ lifestyle: 'assets/marriage-match/3.png',
183
+ family_values: 'assets/marriage-match/4.png',
184
+ interests: 'assets/marriage-match/6.png',
185
+ finances_dealbreakers: 'assets/marriage-match/7.png'
186
+ };
187
+ return images[key] || 'assets/marriage-match/9.png';
188
+ }
189
+
190
+ onCheckboxChange(event: any, columnKey: string) {
191
+ const formArray: FormArray = this.form.get(columnKey) as FormArray;
192
+ if (event.target.checked) {
193
+ formArray.push(new FormControl(event.target.value));
194
+ } else {
195
+ const index = formArray.controls.findIndex(x => x.value === event.target.value);
196
+ formArray.removeAt(index);
197
+ }
198
+ this.updateProgress();
199
+ }
200
+
201
+ onInputChange(columnKey: string): void {
202
+ // Auto-move to next category if current is completed and not last
203
+ const currentIndex = this.categoriesMeta.findIndex(cat => cat.key === this.selectedCategory);
204
+ const currentCat = this.categoriesMeta[currentIndex];
205
+ const total = this.getQuestionsByCategory(currentCat.key).length;
206
+ const answered = this.getCategoryAnsweredCount(currentCat.key);
207
+ if (total > 0 && answered === total && currentIndex < this.categoriesMeta.length - 1) {
208
+ // Move to next category
209
+ this.selectedCategory = this.categoriesMeta[currentIndex + 1].key;
210
+ }
211
+ }
212
+
213
+ scrollToPrefSection(): void {
214
+ const el = document.getElementById('pref-section');
215
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
216
+ }
217
+
218
+ updateProgress(): void {
219
+ const total = Object.keys(this.form.controls).length || 1;
220
+ const filled = Object.values(this.form.value).filter(v => {
221
+ if (Array.isArray(v)) return v.length > 0;
222
+ return !!v && v.toString().trim().length > 0;
223
+ }).length;
224
+ this.progress = Math.round((filled / total) * 100);
225
+ }
226
+
227
+ submit(): void {
228
+ if (this.form.valid) {
229
+ this.savedSuccessfully = false;
230
+ this.showModal = true;
231
+
232
+ const payload = {
233
+ user_id: this.user_id,
234
+ ...this.form.value
235
+ };
236
+
237
+ this.service.saveResponse(payload).subscribe({
238
+ next: () => {
239
+ this.savedSuccessfully = true;
240
+ // Redirect to LLM quiz after successful save
241
+ this.router.navigate(['/llmquiz'], {
242
+ queryParams: {
243
+ user_id: this.user_id,
244
+ role: this.role || 'marriage',
245
+ autostart: '1'
246
+ }
247
+ });
248
+ },
249
+ error: (err) => {
250
+ this.savedSuccessfully = false;
251
+ console.error('Error saving preferences:', err);
252
+ }
253
+ });
254
+ }
255
+ }
256
+
257
+ closeModal(): void {
258
+ this.showModal = false;
259
+ if (this.savedSuccessfully) {
260
+ // Redirect to LLM quiz after successful save
261
+ this.router.navigate(['/llmquiz'], {
262
+ queryParams: {
263
+ user_id: this.user_id,
264
+ role: this.role || 'marriage',
265
+ autostart: '1'
266
+ }
267
+ });
268
+ }
269
+ }
270
+
271
+ goBackToQuestions(): void {
272
+ this.router.navigate(['/question-answer'], {
273
+ queryParams: {
274
+ user_id: this.user_id,
275
+ role: this.role || 'marriage',
276
+ scrollTo: 'questions-section'
277
+ }
278
+ });
279
+ }
280
+ }
src/app/user-preferences/user-preferences.service.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable, of } from 'rxjs';
4
+
5
+ interface ExpectationQuestion {
6
+ id: number;
7
+ question: string;
8
+ options?: string | string[];
9
+ input_type: string;
10
+ column_key: string;
11
+ category: string;
12
+ required?: boolean;
13
+ }
14
+
15
+ interface UserExpectations {
16
+ pref_age_range?: string;
17
+ pref_location?: string;
18
+ pref_education_level?: string;
19
+ pref_employment_status?: string;
20
+ pref_countries?: string[];
21
+ pref_languages?: string[];
22
+ [key: string]: any;
23
+ }
24
+
25
+ @Injectable({
26
+ providedIn: 'root'
27
+ })
28
+ export class UserPreferencesService {
29
+
30
+ private baseUrl: string;
31
+
32
+ constructor(private http: HttpClient) {
33
+ // Initialize baseUrl based on the current hostname
34
+ this.baseUrl = location.hostname.endsWith('hf.space')
35
+ ? 'https://pykara-py-match-backend.hf.space/api'
36
+ : 'http://localhost:5000/api';
37
+ }
38
+
39
+ getQuestions(): Observable<ExpectationQuestion[]> {
40
+ return this.http.get<ExpectationQuestion[]>(`${this.baseUrl}/expectation-questions`);
41
+ }
42
+
43
+ saveResponse(payload: any): Observable<any> {
44
+ return this.http.post(`${this.baseUrl}/expectation-response`, payload);
45
+ }
46
+
47
+ // Add the missing method that MatchingListComponent expects
48
+ getUserExpectations(userId: number): Observable<UserExpectations> {
49
+ // Replace with your actual API endpoint for getting user expectations
50
+ // return this.http.get<UserExpectations>(`${this.baseUrl}/user-expectations/${userId}`);
51
+
52
+ // Temporary fallback - return empty expectations
53
+ return of({});
54
+ }
55
+
56
+ // Optional: Add method to save expectations
57
+ saveUserExpectations(userId: number, expectations: UserExpectations): Observable<any> {
58
+ return this.http.post(`${this.baseUrl}/user-expectations/${userId}`, expectations);
59
+ }
60
+ }
61
+
62
+ // Only export once at the end of the file
63
+ export type { UserExpectations, ExpectationQuestion };
src/assets/favicon.ico DELETED
Binary file (791 Bytes)
 
src/assets/interview1.png DELETED

Git LFS Details

  • SHA256: 5a5595940d238d20589994e6869ab1ddd4a2b8ea84c8372935df901fa42dab84
  • Pointer size: 132 Bytes
  • Size of remote file: 3.64 MB
src/assets/marriage-match/{11.JPG → 11.png} RENAMED
File without changes
src/assets/marriage-match/{14.JPG → 14.png} RENAMED
File without changes
src/assets/marriage-match/{16.JPG → 16.png} RENAMED
File without changes
src/assets/quiz-bg.png DELETED

Git LFS Details

  • SHA256: 268deb8f6bf83c6aa52db70747870899906e2bd3a5f8bf534f4aa2815958b274
  • Pointer size: 132 Bytes
  • Size of remote file: 7.89 MB
src/assets/signup.png DELETED

Git LFS Details

  • SHA256: decc6c7664cbbebdeb3b5916b7c67b7639d6a16bc2ee61cfdbe1be94fea3a692
  • Pointer size: 131 Bytes
  • Size of remote file: 631 kB
src/assets/{1.png → wedding.png} RENAMED
File without changes
src/index.html CHANGED
@@ -2,11 +2,12 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8">
5
- <title>Py-Match</title>
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <link rel="icon" type="image/x-icon" href="pykara-favicon.ico">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
 
10
  <link rel="preconnect" href="https://fonts.gstatic.com">
11
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
12
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8">
5
+ <title>PyMatch</title>
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <!-- Font Awesome via CDN (ensures icons render immediately in dev) -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-4oV3h6Fh2bP7o4F1Yc7c5b8J9c1t1tO8S4ZV6Lr2d9Vd8YH9B6kFq5XvQG3vE1Z4w7xKJ7xk3hQxG8i3k8QqQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
11
  <link rel="preconnect" href="https://fonts.gstatic.com">
12
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
13
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
src/main.ts CHANGED
@@ -3,15 +3,18 @@ import { provideHttpClient } from '@angular/common/http';
3
  import { provideRouter, Routes } from '@angular/router';
4
  import { FormsModule } from '@angular/forms';
5
  import { importProvidersFrom } from '@angular/core';
 
 
6
 
7
  import { AppComponent } from './app/app.component';
8
  import { IntroPageComponent } from './app/intro-page/intro-page.component';
9
  import { QuestionAnswerComponent } from './app/question-answer/question-answer.component';
10
  import { LlmQuizComponent } from './app/llm-quiz/llm-quiz.component';
11
- import { QuizComponent } from './app/quiz/quiz.component';
12
- import { SignInComponent } from './app/auth/sign-in/sign-in.component';
13
- import { SignUpComponent } from './app/auth/sign-up/sign-up.component';
14
  import { QuestionAnswerService } from './app/question-answer/question-answer-service.service';
 
15
 
16
  const routes: Routes = [
17
  {
@@ -34,7 +37,12 @@ const routes: Routes = [
34
  { path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
35
  { path: 'question-answer', component: QuestionAnswerComponent },
36
  { path: 'llmquiz', component: LlmQuizComponent },
37
- { path: 'quiz', component: QuizComponent },
 
 
 
 
 
38
  { path: '**', redirectTo: '' }
39
  ];
40
 
@@ -43,6 +51,7 @@ bootstrapApplication(AppComponent, {
43
  provideHttpClient(),
44
  provideRouter(routes),
45
  importProvidersFrom(FormsModule),
46
- QuestionAnswerService
47
- ]
 
48
  });
 
3
  import { provideRouter, Routes } from '@angular/router';
4
  import { FormsModule } from '@angular/forms';
5
  import { importProvidersFrom } from '@angular/core';
6
+ import { MatchingListComponent } from './app/matching-list/matching-list.component';
7
+ import { UserPreferencesComponent } from './app/user-preferences/user-preferences.component';
8
 
9
  import { AppComponent } from './app/app.component';
10
  import { IntroPageComponent } from './app/intro-page/intro-page.component';
11
  import { QuestionAnswerComponent } from './app/question-answer/question-answer.component';
12
  import { LlmQuizComponent } from './app/llm-quiz/llm-quiz.component';
13
+ //import { SignInComponent } from './app/auth/sign-in/sign-in.component';
14
+ //import { SignUpComponent } from './app/auth/sign-up/sign-up.component';
15
+
16
  import { QuestionAnswerService } from './app/question-answer/question-answer-service.service';
17
+ import { provideAnimations } from '@angular/platform-browser/animations';
18
 
19
  const routes: Routes = [
20
  {
 
37
  { path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
38
  { path: 'question-answer', component: QuestionAnswerComponent },
39
  { path: 'llmquiz', component: LlmQuizComponent },
40
+ { path: 'matchinglist', component: MatchingListComponent },
41
+ { path: 'userPreference', component: UserPreferencesComponent },
42
+ {
43
+ path: 'matchinglist/:id',
44
+ component: MatchingListComponent
45
+ },
46
  { path: '**', redirectTo: '' }
47
  ];
48
 
 
51
  provideHttpClient(),
52
  provideRouter(routes),
53
  importProvidersFrom(FormsModule),
54
+ QuestionAnswerService,
55
+ provideAnimations()
56
+ ]
57
  });
src/styles.css CHANGED
@@ -1,17 +1,17 @@
 
 
1
  @font-face {
2
  font-family: 'Roliana';
3
  src: url('/assets/font/Roliana-Regular.otf') format('opentype');
4
  font-weight: normal;
5
  font-style: normal;
6
  }
7
-
8
  @font-face {
9
  font-family: 'EBGaramond-Regular';
10
  src: url('/assets/font/EBGaramond-Regular.ttf') format('truetype');
11
  font-weight: normal;
12
  font-style: normal;
13
  }
14
-
15
  @font-face {
16
  font-family: 'EBGaramond-Medium';
17
  src: url('/assets/font/EBGaramond-Medium.ttf') format('truetype');
@@ -95,22 +95,14 @@ html, body {
95
  letter-spacing: var(--ls);
96
  }
97
 
98
- .btn:hover {
99
- transform: translateY(-1px);
100
- }
101
-
102
- .btn:active {
103
- transform: translateY(0);
104
- }
105
-
106
- .btn.primary {
107
- background: var(--black);
108
- color: var(--white);
109
- }
110
 
111
  .btn-ghost {
112
- border-color: var(--border);
113
- color: var(--text);
 
114
  }
115
 
116
  /* Skip link for accessibility */
@@ -124,60 +116,24 @@ html, body {
124
  border-radius: 6px;
125
  letter-spacing: var(--ls);
126
  }
127
-
128
- .skip-link:focus {
129
- top: 8px;
130
- }
131
 
132
  /* Hero section */
133
  /*.hero { padding: 56px 0 72px; }*/
134
- .hero h1 {
135
- margin: 0 0 12px 0;
136
- font-size: clamp(28px, 4vw, 44px);
137
- line-height: 1.15;
138
- letter-spacing: var(--ls);
139
- }
140
-
141
- .hero p {
142
- margin: 0 0 28px 0;
143
- color: var(--muted);
144
- max-width: 60ch;
145
- }
146
 
147
  /* Card (optional info block) */
148
- .card {
149
- border: 1px solid var(--border);
150
- border-radius: 12px;
151
- padding: 20px;
152
- background: var(--white);
153
- letter-spacing: var(--ls);
154
- }
155
 
156
  /* Simple grid */
157
- .actions {
158
- display: flex;
159
- gap: 12px;
160
- flex-wrap: wrap;
161
- }
162
 
163
- html, body {
164
- height: 100%;
165
- }
166
 
167
- body {
168
- margin: 0;
169
- }
170
-
171
- span.mdc-tab__text-label {
172
- color: white !important;
173
- font-weight: bold !important; /*font-family: EBGaramond-Medium !important;*/
174
- letter-spacing: var(--ls) !important;
175
- }
176
 
177
- body.mat-typography { font-family: Roboto, sans-serif !important;
178
- letter-spacing: var(--ls);
179
- }
180
 
181
- .mat-h2, .mat-headline-6, .mat-typography .mat-h2, .mat-typography .mat-headline-6, .mat-typography h2, .mat-h3, .mat-subtitle-1, .mat-typography .mat-h3, .mat-typography .mat-subtitle-1, .mat-typography h3, .mat-h1, .mat-headline-5, .mat-typography .mat-h1, .mat-typography .mat-headline-5, .mat-typography h1 { /*font-family: EBGaramond-Medium !important; */
182
- letter-spacing: var(--ls) !important;
183
- }
 
1
+ @import url('../node_modules/@fortawesome/fontawesome-free/css/all.min.css');
2
+
3
  @font-face {
4
  font-family: 'Roliana';
5
  src: url('/assets/font/Roliana-Regular.otf') format('opentype');
6
  font-weight: normal;
7
  font-style: normal;
8
  }
 
9
  @font-face {
10
  font-family: 'EBGaramond-Regular';
11
  src: url('/assets/font/EBGaramond-Regular.ttf') format('truetype');
12
  font-weight: normal;
13
  font-style: normal;
14
  }
 
15
  @font-face {
16
  font-family: 'EBGaramond-Medium';
17
  src: url('/assets/font/EBGaramond-Medium.ttf') format('truetype');
 
95
  letter-spacing: var(--ls);
96
  }
97
 
98
+ .btn:hover { transform: translateY(-1px); }
99
+ .btn:active { transform: translateY(0); }
100
+ .btn.primary { background: var(--black); color: var(--white); }
 
 
 
 
 
 
 
 
 
101
 
102
  .btn-ghost {
103
+ border: 2px solid #f3a54c;
104
+ color: white;
105
+ background: black;
106
  }
107
 
108
  /* Skip link for accessibility */
 
116
  border-radius: 6px;
117
  letter-spacing: var(--ls);
118
  }
119
+ .skip-link:focus { top: 8px; }
 
 
 
120
 
121
  /* Hero section */
122
  /*.hero { padding: 56px 0 72px; }*/
123
+ .hero h1 { margin: 0 0 12px 0; font-size: clamp(28px, 4vw, 44px); line-height: 1.15; letter-spacing: var(--ls); }
124
+ .hero p { margin: 0 0 28px 0; color: var(--muted); max-width: 60ch; }
 
 
 
 
 
 
 
 
 
 
125
 
126
  /* Card (optional info block) */
127
+ .card { border: 1px solid var(--border); border-radius: 12px; padding: 20px; background: var(--white); letter-spacing: var(--ls); }
 
 
 
 
 
 
128
 
129
  /* Simple grid */
130
+ .actions { display: flex; gap: 12px; flex-wrap: wrap; }
 
 
 
 
131
 
132
+ html, body { height: 100%; }
133
+ body { margin: 0; }
 
134
 
135
+ span.mdc-tab__text-label { color: white !important; font-weight:bold !important; /*font-family: EBGaramond-Medium !important;*/ letter-spacing: var(--ls) !important; }
 
 
 
 
 
 
 
 
136
 
137
+ body.mat-typography { /*font-family: EBGaramond-Medium !important;*/ letter-spacing: var(--ls); }
 
 
138
 
139
+ .mat-h2, .mat-headline-6, .mat-typography .mat-h2, .mat-typography .mat-headline-6, .mat-typography h2, .mat-h3, .mat-subtitle-1, .mat-typography .mat-h3, .mat-typography .mat-subtitle-1, .mat-typography h3, .mat-h1, .mat-headline-5, .mat-typography .mat-h1, .mat-typography .mat-headline-5, .mat-typography h1 { /*font-family: EBGaramond-Medium !important; */letter-spacing: var(--ls) !important; }