RajalashmiNagarajan commited on
Commit
e94898e
·
1 Parent(s): b1efa3a
src.zip CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1eb45ebf23c16a409ed8f287fdfefe3435482492afdf4dcd2b6d1632174437f8
3
- size 34011524
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1819db358c2c3e8e40f189817c01f35259ca5c611a797cd3ba3a62189f5c5a30
3
+ size 34011296
src/app/homepage/homepage.component.css CHANGED
@@ -673,9 +673,42 @@ label {
673
  .card p { font-size: 16px; line-height: 1.6; color: #000; }
674
 
675
  /* Mission */
676
- .mission { background: transparent; color: #fff; padding: 60px 10%; text-align: center; }
677
- .mission-row { display: flex; justify-content: space-between; gap: 30px; margin-top: 20px; margin-left: 7vw; flex-wrap: wrap; }
678
- .mission-left, .mission-right { flex: 1; min-width: 300px; text-align: left; font-size: 16px; line-height: 1.6; color: #f0f4f8; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
  /* Features */
681
  .features { padding: 60px 10%; background: #011329; color: #010207; }
@@ -683,7 +716,7 @@ label {
683
  .feature-row.reverse { flex-direction: row-reverse; }
684
  .feature-text { flex: 1 1 320px; padding: 20px; }
685
  .feature-text h2 { color: #c62828; margin-bottom: 15px; }
686
- .feature-text p { font-size: 20px; line-height: 1.6; }
687
  .feature-image { flex: 1 1 320px; padding: 20px; }
688
  .feature-image img { width: 100%; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,.2); }
689
 
@@ -873,10 +906,9 @@ footer .social-icons .si.ig:hover { filter:brightness(1.15); color:#fff; box-sha
873
 
874
 
875
  .how-to-use .section-title {
876
- text-align: center;
877
- margin-left: auto;
878
- margin-right: auto;
879
- display: block;
880
  }
881
 
882
  .how-to-use .section-subtitle {
@@ -889,67 +921,42 @@ footer .social-icons .si.ig:hover { filter:brightness(1.15); color:#fff; box-sha
889
  display: flex;
890
  justify-content: space-between;
891
  flex-wrap: wrap;
892
- gap: 30px;
 
893
  }
894
 
895
  .use-card {
896
  background: #fff;
897
  color: #000;
898
- flex: 11 calc(25% - 30px);
899
- min-width: 260px;
900
  border-radius: 12px;
901
- padding: 28px 24px;
902
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
903
  transition: transform 0.3s;
904
  }
905
 
906
- /* Make the 5th card span entire row */
907
- .use-card:nth-child(5) {
908
- flex: 11 100%;
909
- min-width: 100%;
910
- text-align: center;
911
- padding: 36px 32px;
912
- }
913
-
914
- .use-card:nth-child(5) .icon {
915
- font-size: 46px;
916
- color: #5a35d6;
917
- }
918
-
919
- .use-card:nth-child(5) h3 {
920
- font-size: 22px;
921
- color: #5a35d6;
922
- margin-top: 6px;
923
- }
924
-
925
- .use-card:nth-child(5) p {
926
- max-width: 1100px;
927
- margin: 10px auto 0;
928
- }
929
-
930
- /* Keep icon/title styling for first row */
931
- .use-card .icon {
932
- font-size: 40px;
933
- margin-bottom: 15px;
934
- color: #3328c6;
935
- }
936
 
937
- .use-card h3 {
938
- font-size: 18px;
939
- margin-bottom: 10px;
940
- color: #3328c6;
941
- }
942
 
943
- .use-card p {
944
- font-size: 16px;
945
- line-height: 1.5;
946
- color: #000;
947
- }
948
 
949
- /* Hover lift */
950
- .use-card:hover {
951
- transform: translateY(-6px);
952
- }
 
953
 
954
  .social-icons {
955
  display: flex;
@@ -1303,7 +1310,7 @@ footer .social-icons {
1303
  .auth-topright .auth-btn[data-tooltip]:focus-visible::after { opacity:1; transform: translateX(50%) translateY(0); }
1304
  .hero .hero-cta {
1305
  margin-top: 12px;
1306
- background: linear-gradient(90deg, #38bdf80%, #23272b100%);
1307
  color: #18181b;
1308
  border: none;
1309
  border-radius: 999px;
@@ -1345,8 +1352,6 @@ footer .social-icons {
1345
  width: min(1000px,92vw);
1346
  height:380px;
1347
  border-radius:24px;
1348
- background: linear-gradient(180deg, rgba(7,16,28,0.95), rgba(5,12,22,0.95)); /* darker decorative layer */
1349
- box-shadow:0 20px 60px rgba(2,6,23,0.45), inset 0 0 1px rgba(56,189,248,0.15);
1350
  z-index:0; /* behind card content */
1351
  }
1352
  .how-it-works .section-title,
@@ -1380,32 +1385,54 @@ footer .social-icons {
1380
  }
1381
 
1382
  /* How It Works: blue background card and2x3 grid */
1383
- .how-wrapper { position: relative; max-width:1200px; margin:0 auto; padding:12px 12px 24px; }
1384
  .how-bg {
1385
  position:absolute; inset:0; margin:auto; border-radius:12px;
1386
  background: linear-gradient(135deg, #0b3a6b0%, #0d5aa8100%);
1387
  opacity:0.22; z-index:0; box-shadow:0 12px 40px rgba(0,0,0,.25);
1388
  }
1389
- .how-tiles { position:relative; z-index:1; display:grid; grid-template-columns: repeat(3,1fr); gap:22px; }
 
 
 
 
 
 
 
 
1390
  .how-tile { background:#fff; color:#222; border-radius:8px; box-shadow:0 8px 22px rgba(0,0,0,.06); border:1px solid rgba(0,0,0,.06); overflow:hidden; padding-bottom:6px; }
1391
  .how-tile:hover { transform: translateY(-3px); box-shadow:0 12px 26px rgba(0,0,0,.12); transition:transform .2s, box-shadow .2s; }
1392
  .how-illustration {
1393
  background: linear-gradient(180deg,#fcf7ea,#3aa6d9);
1394
- height: 160px;
1395
  display: flex;
1396
  align-items: center;
1397
  justify-content: center;
1398
  }
1399
  .how-illustration i { font-size:62px; }
1400
  .how-tile-title {
1401
- text-align: center;
1402
  margin: 14px 6px 0;
1403
  color: #3328c6;
1404
  font-weight: 800;
1405
  }
1406
- .how-divider { height:3px; width:72%; margin:8px auto 8px; border-bottom:2px dotted #f59e9e; }
1407
- .how-tile p, .how-tile ul { padding:0 18px 12px; margin:0; color:#333; }
1408
- .how-tile ul { padding-left:34px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1409
 
1410
  @media (max-width:1024px) { .how-tiles { grid-template-columns: repeat(2,1fr); } }
1411
  @media (max-width:640px) { .how-tiles { grid-template-columns:1fr; } .how-illustration { height:130px; } }
@@ -1426,3 +1453,6 @@ footer .social-icons {
1426
 
1427
 
1428
 
 
 
 
 
673
  .card p { font-size: 16px; line-height: 1.6; color: #000; }
674
 
675
  /* Mission */
676
+ .mission {
677
+ background: transparent;
678
+ color: #fff;
679
+ padding: 60px 10%;
680
+ text-align: center;
681
+ /* Ensure the mission section forms a centered block and prevents overflow */
682
+ box-sizing: border-box;
683
+ }
684
+
685
+ /* Center the mission row and keep content within the blue section */
686
+ .mission-row {
687
+ display: flex;
688
+ justify-content: space-between;
689
+ gap: 30px;
690
+ margin-top: 20px;
691
+ /* Remove the left offset which was pushing content outside */
692
+ margin-left: 0;
693
+ /* Constrain width and center horizontally */
694
+ max-width: 1100px;
695
+ margin-right: 36px;
696
+ margin-left: auto;
697
+ flex-wrap: wrap;
698
+ box-sizing: border-box;
699
+ }
700
+
701
+ .mission-left, .mission-right {
702
+ flex: 1;
703
+ min-width: 300px;
704
+ text-align: left;
705
+ font-size: 16px;
706
+ line-height: 1.6;
707
+ color: #f0f4f8;
708
+ /* Add inner padding so text doesn't touch edges of blue card */
709
+ padding: 20px;
710
+ box-sizing: border-box;
711
+ }
712
 
713
  /* Features */
714
  .features { padding: 60px 10%; background: #011329; color: #010207; }
 
716
  .feature-row.reverse { flex-direction: row-reverse; }
717
  .feature-text { flex: 1 1 320px; padding: 20px; }
718
  .feature-text h2 { color: #c62828; margin-bottom: 15px; }
719
+ .feature-text p { font-size: 16px; line-height: 1.6; }
720
  .feature-image { flex: 1 1 320px; padding: 20px; }
721
  .feature-image img { width: 100%; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,.2); }
722
 
 
906
 
907
 
908
  .how-to-use .section-title {
909
+ font-size: 28px;
910
+ color: #0d9de3; /* blue highlight */
911
+ margin-bottom: 10px;
 
912
  }
913
 
914
  .how-to-use .section-subtitle {
 
921
  display: flex;
922
  justify-content: space-between;
923
  flex-wrap: wrap;
924
+ gap: 25px;
925
+ line-height: 1.6;
926
  }
927
 
928
  .use-card {
929
  background: #fff;
930
  color: #000;
931
+ flex: 1 1 calc(20% - 20px); /* 5 cards in a row */
932
+ min-width: 220px;
933
  border-radius: 12px;
934
+ padding: 25px 20px;
935
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
936
  transition: transform 0.3s;
937
  }
938
 
939
+ .use-card:hover {
940
+ transform: translateY(-6px);
941
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
 
943
+ .use-card .icon {
944
+ font-size: 40px;
945
+ margin-bottom: 15px;
946
+ color: #3328c6; /* red accent */
947
+ }
948
 
949
+ .use-card h3 {
950
+ font-size: 18px;
951
+ margin-bottom: 10px;
952
+ color: #3328c6;
953
+ }
954
 
955
+ .use-card p {
956
+ font-size: 16px;
957
+ line-height: 1.5;
958
+ color: #000;
959
+ }
960
 
961
  .social-icons {
962
  display: flex;
 
1310
  .auth-topright .auth-btn[data-tooltip]:focus-visible::after { opacity:1; transform: translateX(50%) translateY(0); }
1311
  .hero .hero-cta {
1312
  margin-top: 12px;
1313
+ background: linear-gradient(90deg, #38bdf8 0%, #23272b 100%);
1314
  color: #18181b;
1315
  border: none;
1316
  border-radius: 999px;
 
1352
  width: min(1000px,92vw);
1353
  height:380px;
1354
  border-radius:24px;
 
 
1355
  z-index:0; /* behind card content */
1356
  }
1357
  .how-it-works .section-title,
 
1385
  }
1386
 
1387
  /* How It Works: blue background card and2x3 grid */
1388
+ .how-wrapper { position: relative; max-width:7000px; margin:0 auto; padding:12px 12px 24px; }
1389
  .how-bg {
1390
  position:absolute; inset:0; margin:auto; border-radius:12px;
1391
  background: linear-gradient(135deg, #0b3a6b0%, #0d5aa8100%);
1392
  opacity:0.22; z-index:0; box-shadow:0 12px 40px rgba(0,0,0,.25);
1393
  }
1394
+ .how-tiles {
1395
+ position: relative;
1396
+ z-index: 1;
1397
+ display: grid;
1398
+ grid-template-columns: repeat(3,1fr);
1399
+ gap: 22px;
1400
+ font-size: 16px;
1401
+ line-height: 1.6;
1402
+ }
1403
  .how-tile { background:#fff; color:#222; border-radius:8px; box-shadow:0 8px 22px rgba(0,0,0,.06); border:1px solid rgba(0,0,0,.06); overflow:hidden; padding-bottom:6px; }
1404
  .how-tile:hover { transform: translateY(-3px); box-shadow:0 12px 26px rgba(0,0,0,.12); transition:transform .2s, box-shadow .2s; }
1405
  .how-illustration {
1406
  background: linear-gradient(180deg,#fcf7ea,#3aa6d9);
1407
+ height: 100px;
1408
  display: flex;
1409
  align-items: center;
1410
  justify-content: center;
1411
  }
1412
  .how-illustration i { font-size:62px; }
1413
  .how-tile-title {
1414
+ text-align: left !important;
1415
  margin: 14px 6px 0;
1416
  color: #3328c6;
1417
  font-weight: 800;
1418
  }
1419
+ .how-divider {
1420
+ height: 3px;
1421
+ width: 81%;
1422
+ margin: 8px auto 8px;
1423
+ border-bottom: 2px dotted #3328c6;
1424
+ margin-right: 84px;
1425
+ }
1426
+ .how-tile p, .how-tile ul {
1427
+ text-align: left !important;
1428
+ padding:0 18px 12px;
1429
+ margin:0;
1430
+ color:#333;
1431
+ }
1432
+ .how-tile ul { padding-left:24px; }
1433
+
1434
+ /* Ensure illustration remains centered but not affecting text */
1435
+ .how-illustration { display:flex; align-items:center; justify-content:center; }
1436
 
1437
  @media (max-width:1024px) { .how-tiles { grid-template-columns: repeat(2,1fr); } }
1438
  @media (max-width:640px) { .how-tiles { grid-template-columns:1fr; } .how-illustration { height:130px; } }
 
1453
 
1454
 
1455
 
1456
+
1457
+
1458
+
src/app/homepage/homepage.component.html CHANGED
@@ -43,9 +43,6 @@
43
  <a class="dropdown-item" [class.active]="selectedNav==='how-it-works'" href="#how-it-works" (click)="scrollToHowItWorks($event)">
44
  <i class="fas fa-cogs nav-icon" aria-hidden="true"></i> How it works
45
  </a>
46
- <a class="dropdown-item" href="#" (click)="openInfoDialog(); markSelected('about'); $event.preventDefault()">
47
- <i class="fas fa-book-open nav-icon" aria-hidden="true"></i> Know more
48
- </a>
49
  </div>
50
  </div>
51
  <span class="nav-sep">|</span>
@@ -106,7 +103,7 @@
106
  </div>
107
  <div class="mission-right">
108
  <p>
109
- Through advanced behavioural analysis—examining verbal responses, facial cues, and body language—the platform provides clear insight into consistency, credibility, and potential risks. This enables teams to act with accuracy, clarity, and confidence across a wide range of situations.
110
  </p>
111
  </div>
112
  </div>
@@ -201,7 +198,7 @@
201
 
202
  <!-- How It Works Section (two-row tiles with blue background) -->
203
  <section id="how-it-works" class="how-it-works">
204
- <h2 class="section-title how-title">How It Works</h2>
205
  <div class="how-wrapper">
206
  <div class="how-bg"></div>
207
  <div class="how-tiles">
@@ -211,7 +208,7 @@
211
  <h3 class="how-tile-title">Context‑Aware Questioning</h3>
212
  <div class="how-divider"></div>
213
  <p class="how-tile-text">
214
- Py‑Detect reads case details and adapts followup questions based on prior answers to keep the interview relevant and continuous.
215
  </p>
216
  </div>
217
 
@@ -221,12 +218,11 @@
221
  <h3 class="how-tile-title">Voice Pattern Analysis</h3>
222
  <div class="how-divider"></div>
223
  <ul>
224
- <li>Tone variations, stress points</li>
225
- <li>Speech pace, hesitations</li>
226
- <li>Sudden pitch changes</li>
227
  </ul>
228
- <p class="how-tile-text">These cues help identify emotional states and potential inconsistencies.</p>
229
- </div>
230
 
231
  <!--3: Facial & Micro‑Expression Tracking -->
232
  <div class="how-tile">
@@ -234,12 +230,11 @@
234
  <h3 class="how-tile-title">Facial & Micro‑Expressions</h3>
235
  <div class="how-divider"></div>
236
  <ul>
237
- <li>Eye shifts and gaze changes</li>
238
- <li>Lip compression, brow tension</li>
239
- <li>Nervous micro‑gestures</li>
240
  </ul>
241
- <p class="how-tile-text">Signals that contribute to behavioural interpretation.</p>
242
- </div>
243
 
244
  <!--4: Body‑Language Pattern Detection -->
245
  <div class="how-tile">
@@ -247,7 +242,7 @@
247
  <h3 class="how-tile-title">Body‑Language Detection</h3>
248
  <div class="how-divider"></div>
249
  <p class="how-tile-text">
250
- Posture, hand movements, head position, and timing are evaluated to understand engagement, confidence, and anxiety levels.
251
  </p>
252
  </div>
253
 
@@ -257,22 +252,21 @@
257
  <h3 class="how-tile-title">Combined Insight Summary</h3>
258
  <div class="how-divider"></div>
259
  <ul>
260
- <li>Behavioural indicators and stress markers</li>
261
- <li>Consistency score and highlights</li>
262
- <li>Clear, structured summary for review</li>
263
  </ul>
264
  </div>
265
 
266
  <!--6: Workflow, Who, Why & Key Features (condensed) -->
267
  <div class="how-tile">
268
  <div class="how-illustration"><i class="fas fa-project-diagram"></i></div>
269
- <h3 class="how-tile-title">Workflow • Users • Benefits</h3>
270
  <div class="how-divider"></div>
271
  <ul>
272
- <li><strong>Workflow:</strong> Admin creates case → Investigator records interviewAI analyses → Summary & dashboards.</li>
273
- <li><strong>Who:</strong> Law Enforcement, Legal, HR/Security, Private Investigation, Education/Counselling.</li>
274
- <li><strong>Why:</strong> Fast, noninvasive, reliable, configurable, secure.</li>
275
- <li><strong>Key Features:</strong> Case Management, Auto Questioning, Voice/Video/Behaviour Analysis, Truth Score, Admin & Investigator Panels.</li>
276
  </ul>
277
  </div>
278
  </div>
@@ -361,24 +355,6 @@
361
  </ng-container>
362
  </div>
363
 
364
- <!-- ===== Info Dialog ===== -->
365
- <div class="modal-backdrop modal-backdrop--fade" *ngIf="showInfoDialog" (click)="closeInfoDialog()"></div>
366
- <div class="modal dialog-modal dialog-modal--zoom" *ngIf="showInfoDialog" role="dialog" aria-modal="true" aria-label="About Py-Detect">
367
- <div class="dialog-content" (click)="$event.stopPropagation()">
368
- <button class="dialog-close" (click)="closeInfoDialog()" aria-label="Close">×</button>
369
 
370
- <h3>How It Works</h3>
371
- <p>
372
- The platform asks context-aware questions and analyses verbal, facial, and behavioral cues using computer vision.
373
- It then generates a clear summary with risk indicators and consistency metrics.
374
- </p>
375
-
376
- <h3>Who Can Use Py-Detect?</h3>
377
- <p>Investigators, law enforcement teams, legal professionals, HR and security departments, educators, and any organization that needs accurate behavioral insights.</p>
378
-
379
- <h3>Why Py-Detect?</h3>
380
- <p>It is fast, non-invasive, configurable for different scenarios, and supports decision-making with reliable, data-driven analysis.</p>
381
- </div>
382
- </div>
383
 
384
 
 
43
  <a class="dropdown-item" [class.active]="selectedNav==='how-it-works'" href="#how-it-works" (click)="scrollToHowItWorks($event)">
44
  <i class="fas fa-cogs nav-icon" aria-hidden="true"></i> How it works
45
  </a>
 
 
 
46
  </div>
47
  </div>
48
  <span class="nav-sep">|</span>
 
103
  </div>
104
  <div class="mission-right">
105
  <p>
106
+ Through advanced behavioural analysis—examining verbal responses, facial cues, and body language—the platform provides clear insight into consistency, credibility, and <br /> potential risks. This enables teams to act with accuracy, <br /> clarity, and confidence across a wide range of situations.
107
  </p>
108
  </div>
109
  </div>
 
198
 
199
  <!-- How It Works Section (two-row tiles with blue background) -->
200
  <section id="how-it-works" class="how-it-works">
201
+ <h2 class="section-title how-title">How It Works?</h2>
202
  <div class="how-wrapper">
203
  <div class="how-bg"></div>
204
  <div class="how-tiles">
 
208
  <h3 class="how-tile-title">Context‑Aware Questioning</h3>
209
  <div class="how-divider"></div>
210
  <p class="how-tile-text">
211
+ The system reviews case details and presents structured, relevant follow-up questions based on previous responses to keep the interview natural and continuous.
212
  </p>
213
  </div>
214
 
 
218
  <h3 class="how-tile-title">Voice Pattern Analysis</h3>
219
  <div class="how-divider"></div>
220
  <ul>
221
+ <li>Tone variations and stress points</li>
222
+ <li>Speech pace, pauses, and hesitations</li>
223
+ <li>Sudden pitch changes</li>
224
  </ul>
225
+ </div>
 
226
 
227
  <!--3: Facial & Micro‑Expression Tracking -->
228
  <div class="how-tile">
 
230
  <h3 class="how-tile-title">Facial & Micro‑Expressions</h3>
231
  <div class="how-divider"></div>
232
  <ul>
233
+ <li>Eye movement and gaze behaviour</li>
234
+ <li>Lip tension and brow compression</li>
235
+ <li>Small, involuntary gestures</li>
236
  </ul>
237
+ </div>
 
238
 
239
  <!--4: Body‑Language Pattern Detection -->
240
  <div class="how-tile">
 
242
  <h3 class="how-tile-title">Body‑Language Detection</h3>
243
  <div class="how-divider"></div>
244
  <p class="how-tile-text">
245
+ Posture, hand movements, head position, and timing are assessed to interpret confidence, engagement, and anxiety levels.
246
  </p>
247
  </div>
248
 
 
252
  <h3 class="how-tile-title">Combined Insight Summary</h3>
253
  <div class="how-divider"></div>
254
  <ul>
255
+ <li>Key behavioural indicators and stress markers</li>
256
+ <li>Consistency highlights</li>
257
+ <li>A clear, structured summary for review and decision-makin</li>
258
  </ul>
259
  </div>
260
 
261
  <!--6: Workflow, Who, Why & Key Features (condensed) -->
262
  <div class="how-tile">
263
  <div class="how-illustration"><i class="fas fa-project-diagram"></i></div>
264
+ <h3 class="how-tile-title">Workflow Overview</h3>
265
  <div class="how-divider"></div>
266
  <ul>
267
+ <li><strong>Process:</strong> Admin creates the case → Assigned user records the session Behavioural analysis is generated → Summary available for review.</li>
268
+ <li><strong>Who Uses It:</strong> Law enforcement, legal professionals, HR and recruitment teams, private investigators, counselling/education professionals.</li>
269
+ <li><strong>Benefits:</strong> Simple to use, non-invasive, reliable, configurable, and secure.</li>
 
270
  </ul>
271
  </div>
272
  </div>
 
355
  </ng-container>
356
  </div>
357
 
 
 
 
 
 
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
 
src/app/homepage/sign-in/sign-in.component.css CHANGED
@@ -1127,7 +1127,7 @@ form {
1127
  border-radius: 10px !important;
1128
  font-weight: 700 !important;
1129
  cursor: pointer !important;
1130
- margin-top: 300px !important;
1131
  margin-left: 25px;
1132
  width: 293px;
1133
  height: 58px;
@@ -1196,7 +1196,7 @@ form {
1196
  border-radius: 10px !important;
1197
  font-weight: 700 !important;
1198
  cursor: pointer !important;
1199
- margin-bottom: 25px !important;
1200
  margin-left: 50px;
1201
  width: 293px;
1202
  height: 58px;
 
1127
  border-radius: 10px !important;
1128
  font-weight: 700 !important;
1129
  cursor: pointer !important;
1130
+ margin-top: 357px !important;
1131
  margin-left: 25px;
1132
  width: 293px;
1133
  height: 58px;
 
1196
  border-radius: 10px !important;
1197
  font-weight: 700 !important;
1198
  cursor: pointer !important;
1199
+ margin-top: 30px !important;
1200
  margin-left: 50px;
1201
  width: 293px;
1202
  height: 58px;
src/app/homepage/sign-up-1.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cdf75cd3553399579911dd6c64ecb0163f18019d2c6fbb7c5731f8d708e274f7
3
+ size 17423
src/app/homepage/sign-up-1/sign-up/sign-up.component.css ADDED
@@ -0,0 +1,1018 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ min-height: 100vh;
5
+ }
6
+
7
+ .signup-bg {
8
+ min-height: 100vh;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ background: #f7fafd;
13
+ }
14
+
15
+ .signup-container {
16
+ display: flex;
17
+ width: 100vw;
18
+ max-width: 1200px;
19
+ min-height: 600px;
20
+ border-radius: 0;
21
+ box-shadow: none;
22
+ overflow: hidden;
23
+ align-items: center;
24
+ justify-content: center;
25
+ }
26
+
27
+ .signup-panel-right {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ padding: 32px 0;
32
+ }
33
+
34
+ .signup-panel-right {
35
+ flex: 1 1 0;
36
+ background: #f7fafd;
37
+ }
38
+
39
+ .create-card {
40
+ background: #fff;
41
+ width: 90%;
42
+ max-width: 540px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
46
+ padding: 38px 38px 28px 38px;
47
+ border-radius: 18px;
48
+ box-shadow: 0 12px 48px rgba(2, 6, 23, 0.18), 0 0 0 2px rgba(56, 189, 248, 0.08);
49
+ margin: 0 auto;
50
+ position: relative;
51
+ z-index: 2;
52
+ transform: translateY(-8px);
53
+ box-shadow: 0 14px 40px rgba(2, 6, 23, 0.18);
54
+ transition: transform 220ms ease, box-shadow 220ms ease;
55
+ }
56
+
57
+ .create-card:hover {
58
+ transform: translateY(-12px);
59
+ box-shadow: 0 20px 60px rgba(2, 6, 23, 0.22);
60
+ }
61
+
62
+ @media (max-width: 700px) {
63
+ .create-card {
64
+ transform: none;
65
+ box-shadow: 0 8px 24px rgba(2, 6, 23, 0.12);
66
+ }
67
+
68
+ .create-card:hover {
69
+ transform: none;
70
+ box-shadow: 0 8px 24px rgba(2, 6, 23, 0.12);
71
+ }
72
+ }
73
+
74
+ @media (prefers-reduced-motion: reduce) {
75
+ .create-card,
76
+ .create-card:hover {
77
+ transition: none;
78
+ transform: none;
79
+ }
80
+ }
81
+
82
+ .create-title {
83
+ font-size: 2.1rem;
84
+ font-weight: 900;
85
+ text-align: center;
86
+ margin-bottom: 28px;
87
+ letter-spacing: 0.6px;
88
+ color: #23395d;
89
+ }
90
+
91
+ .signup-title.center-title {
92
+ text-align: center;
93
+ margin-bottom: 32px;
94
+ width: 100%;
95
+ font-size: 2.1rem;
96
+ font-weight: 800;
97
+ letter-spacing: 1px;
98
+ color: #23395d;
99
+ text-shadow: 0 2px 8px #0008;
100
+ margin-top:24px;
101
+ /* animation: logoGlow 3.5s ease-in-out infinite; */
102
+ }
103
+
104
+ @keyframes logoGlow {
105
+ 0% {
106
+ text-shadow: 0 2px 8px #0008, 0 0 12px #38bdf8, 0 0 6px #13bfa6;
107
+ }
108
+
109
+ 100% {
110
+ text-shadow: 0 2px 8px #0008, 0 0 32px #38bdf8, 0 0 18px #13bfa6;
111
+ }
112
+ }
113
+
114
+ .create-form {
115
+ width: 100%;
116
+ max-width: 580px;
117
+ display: grid;
118
+ grid-template-columns: 1fr 1fr;
119
+ gap: 16px 16px;
120
+ align-items: start;
121
+ margin-bottom: 10px;
122
+ padding: 25px;
123
+ padding-top: 0px;
124
+ }
125
+
126
+ .terms-info {
127
+ color: #137ec4;
128
+ font-size: 1.08rem;
129
+ font-weight: 600;
130
+ text-align: left;
131
+ margin: 12px 0 0 0;
132
+ letter-spacing: 0.5px;
133
+ display: block;
134
+ margin-left: 25px !important; /* align with .twofa-methods and .form-checkbox */
135
+ }
136
+
137
+ .form-row {
138
+ display: contents;
139
+ }
140
+
141
+ .form-field {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 8px;
145
+ width: 100%;
146
+ }
147
+
148
+ .form-field label {
149
+ color: #e6f7f9;
150
+ font-size: 1rem;
151
+ font-weight: 600;
152
+ color: #23395d;
153
+ letter-spacing: 0.5px;
154
+ }
155
+
156
+ .form-field input,
157
+ .form-field select {
158
+ background: #fff;
159
+ color: #23395d;
160
+ border: none;
161
+ border-radius: 8px;
162
+ padding: 3px 10px 3px 10px;
163
+ font-size: 1rem;
164
+ margin-bottom: 2px;
165
+ box-shadow: 0 1px 4px #0002;
166
+ transition: border 0.2s, box-shadow 0.2s;
167
+ width: 100%;
168
+ min-width: 0;
169
+ max-width: 100%;
170
+ height: 36px;
171
+ box-sizing: border-box;
172
+ }
173
+
174
+ .form-field input:focus,
175
+ .form-field select:focus {
176
+ outline: 2px solid #1de9b6;
177
+ border-color: #1de9b6;
178
+ box-shadow: 0 0 6px rgba(56, 189, 248, 0.5), 0 0 0 2px #1de9b688;
179
+ }
180
+
181
+ .form-field input:focus {
182
+ box-shadow: 006px rgba(14,165,164,0.08);
183
+ border-color: #0ea5a4;
184
+ }
185
+
186
+ .form-field input::placeholder {
187
+ color: #b0b8c1;
188
+ opacity: 1;
189
+ }
190
+
191
+ /* Reserve extra space when an eye toggle is present so text doesn't overlap and layout stays stable */
192
+ .form-field.has-eye input {
193
+ padding-right: 46px; /* enough for the eye button + spacing */
194
+ }
195
+
196
+ /* Anchor wrapper for inputs that include an eye toggle */
197
+ .input-with-eye {
198
+ position: relative;
199
+ display: block;
200
+ height: 46px; /* match input height so the eye is vertically stable */
201
+ box-sizing: border-box;
202
+ }
203
+
204
+ /* Ensure the input fills the wrapper and reserves space for the eye */
205
+ .input-with-eye input {
206
+ padding-right: 50px; /* space for the eye */
207
+ height: 79%;
208
+ box-sizing: border-box;
209
+ }
210
+
211
+ /* More specific selector so the eye is positioned relative to the wrapper and won't shift */
212
+ .input-with-eye .eye-toggle {
213
+ position: absolute;
214
+ right: 10px;
215
+ top: 41%;
216
+ transform: translateY(-50%);
217
+ border: none;
218
+ width: 32px;
219
+ height: 32px;
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ border-radius: 6px;
224
+ cursor: pointer;
225
+ color: #23395d;
226
+ z-index: 2; /* keep above input */
227
+ pointer-events: auto;
228
+ }
229
+
230
+ /* Keep the original .form-field .eye-toggle as a fallback but prefer the wrapper-based positioning */
231
+ .form-field .eye-toggle {
232
+ /* fallthrough for any other usages; do not change */
233
+ }
234
+
235
+ /* Eye toggle inside password field for sign-up: vertically center and avoid movement */
236
+ .form-field .eye-toggle {
237
+ position: absolute;
238
+ right: 0px;
239
+ top: 41%;
240
+ transform: translateY(-50%);
241
+ /* background: #fff;*/
242
+ border: transparent;
243
+ width: 32px;
244
+ height: 32px;
245
+ display: flex;
246
+ background: transparent;
247
+ align-items: center;
248
+ justify-content: center;
249
+ border-radius: 6px;
250
+ cursor: pointer;
251
+ color: #23395d;
252
+ z-index: 0;
253
+ pointer-events: auto;
254
+ }
255
+ .form-field .eye-toggle i {
256
+ font-size: 0.95rem;
257
+ line-height: 1;
258
+ }
259
+
260
+ .form-field .eye-toggle:focus {
261
+ outline: 2px solid rgba(29,233,182,0.12);
262
+ }
263
+
264
+ .form-checkbox {
265
+ display: flex;
266
+ gap: 10px;
267
+ align-items: center;
268
+ color: #2b5160;
269
+ margin-top: -19px; /* normalized spacing */
270
+ margin-left: 25px !important; /* enforce consistent left alignment */
271
+ }
272
+
273
+ .form-checkbox input[type="checkbox"] {
274
+ width: 20px;
275
+ height: 20px;
276
+ min-width: 20px;
277
+ min-height: 20px;
278
+ margin: 0; /* gap handles spacing */
279
+ padding: 0;
280
+ box-sizing: border-box;
281
+ vertical-align: middle;
282
+ appearance: none;
283
+ -webkit-appearance: none;
284
+ background: #fff;
285
+ border: 2px solid #cbd5e1; /* subtle border */
286
+ border-radius: 4px;
287
+ display: inline-block;
288
+ position: relative;
289
+ cursor: pointer;
290
+ margin-bottom: 0px;
291
+ }
292
+ input#twoFAOptIn {
293
+ margin-top: 0; /* remove extra offset which caused misalignment */
294
+ }
295
+
296
+ /* Checked state - show a simple tick using box-shadow trick (keeps no extra elements) */
297
+ .form-checkbox input[type="checkbox"]:checked {
298
+ background: linear-gradient(180deg, #38bdf8, #137ec4);
299
+ border-color: #137ec4;
300
+ }
301
+
302
+ .form-checkbox input[type="checkbox"]:checked::after {
303
+ content: '\2713'; /* check mark */
304
+ color: #083344;
305
+ font-weight: 700;
306
+ font-size: 14px;
307
+ position: absolute;
308
+ top: 50%;
309
+ left: 50%;
310
+ transform: translate(-50%, -58%);
311
+ line-height: 1;
312
+ }
313
+
314
+ /* Keep label aligned and allow wrapping */
315
+ .form-checkbox label {
316
+ display: inline-block;
317
+ line-height: 1.2;
318
+ cursor: pointer;
319
+ }
320
+
321
+ /* New: 2FA styles moved from inline */
322
+ .twofa-field {
323
+ grid-column: 1 / -1;
324
+ padding-top: 0px;
325
+ margin-left: 0;
326
+ }
327
+
328
+ .twofa-section {
329
+ margin-top: 6px;
330
+ }
331
+
332
+ .twofa-label-bold {
333
+ font-weight: 700;
334
+ display: block;
335
+ margin-bottom: 6px;
336
+ margin-left: 25px; /* match .form-checkbox and form padding */
337
+ }
338
+
339
+ .twofa-methods {
340
+ display: flex;
341
+ gap: 12px;
342
+ align-items: center;
343
+ margin-left: 25px !important; /* start at same x as checkbox label */
344
+ }
345
+
346
+ .inline-control {
347
+ display: flex;
348
+ gap: 6px;
349
+ align-items: center;
350
+ }
351
+
352
+ .twofa-email-options .twofa-alt-email,
353
+ .twofa-sms-options {
354
+ margin-top: 8px;
355
+ margin-left: 25px !important; /* align alt inputs with checkbox label */
356
+ }
357
+
358
+ .twofa-alt-email input,
359
+ .twofa-sms-options input {
360
+ width: 100%;
361
+ background: #fff;
362
+ border: none;
363
+ border-radius: 8px;
364
+ padding: 6px 10px;
365
+ height: 36px;
366
+ box-shadow: 0 1px 4px #0002;
367
+ }
368
+
369
+ .twofa-alt-email input:focus,
370
+ .twofa-sms-options input:focus {
371
+ outline: 2px solid #1de9b6;
372
+ box-shadow: 0 0 6px rgba(56, 189, 248, 0.5);
373
+ }
374
+
375
+ /* Reduce SMS input width only (override the earlier100% width) */
376
+ .twofa-sms-options input {
377
+ width:260px; /* reduced fixed width for SMS input */
378
+ max-width:100%;
379
+ }
380
+
381
+ .twofa-sms-options label {
382
+ display: block;
383
+ margin-bottom:6px; /* small gap -- reduced */
384
+ font-weight:600;
385
+ color: #23395d;
386
+ }
387
+
388
+ .twofa-sms-options input {
389
+ margin-top:4px; /* tiny gap to bring input closer to label */
390
+ }
391
+
392
+ .twofa-error {
393
+ display: block;
394
+ margin-top: 8px;
395
+ }
396
+
397
+ .twofa-sms-options input[readonly] {
398
+ background: #f3f4f6;
399
+ }
400
+
401
+ /* 2FA single-line row: checkbox + method radios aligned */
402
+ .twofa-row {
403
+ display: flex;
404
+ align-items: center;
405
+ gap:14px; /* small gap between checkbox label and method controls */
406
+ }
407
+
408
+ /* ensure checkbox block has same spacing as other checkboxes */
409
+ .twofa-checkbox {
410
+ margin-left:0px !important;
411
+ margin-top:0px;
412
+ }
413
+
414
+ /* plain control: no extra background, keep text and control inline */
415
+ .plain-control {
416
+ background: transparent;
417
+ padding:0;
418
+ margin:0;
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap:8px;
422
+ color: inherit;
423
+ }
424
+
425
+ /* ensure radios align vertically with checkbox center */
426
+ .plain-control input[type="radio"] {
427
+ width:16px;
428
+ height:16px;
429
+ margin:0;
430
+ }
431
+
432
+ /* Small responsive tweaks */
433
+ @media (max-width: 900px) {
434
+ .twofa-methods {
435
+ flex-direction: column;
436
+ align-items: flex-start;
437
+ }
438
+
439
+ .twofa-field .form-checkbox {
440
+ margin-left: 0;
441
+ }
442
+ }
443
+
444
+ .create-btn {
445
+ width: 50%;
446
+ max-width: 320px;
447
+ background: #23395d;
448
+ color: #fff;
449
+ padding: 12px 18px;
450
+ border-radius: 10px;
451
+ font-weight: 800;
452
+ border: none;
453
+ box-shadow: 0 10px 30px rgba(3, 20, 36, 0.32);
454
+ cursor: pointer;
455
+ font-size: 1.15rem;
456
+ margin-top: 100px;
457
+ margin-left: 0; /* remove previous fixed offset */
458
+ margin-right: 0;
459
+ margin-bottom: 0;
460
+ display: block;
461
+ margin-inline: auto; /* center horizontally */
462
+ }
463
+
464
+ .create-btn:hover {
465
+ background: #38bdf8;
466
+ color: #fff;
467
+ }
468
+
469
+ /* Grey out disabled create button */
470
+ .create-btn[disabled] {
471
+ background: #b8c6d6;
472
+ color: #fff;
473
+ cursor: not-allowed;
474
+ box-shadow: none;
475
+ }
476
+
477
+ .create-login-link {
478
+ grid-column: 1 / -1;
479
+ text-align: center;
480
+ color: #137ec4;
481
+ margin-top: 0px;
482
+ }
483
+
484
+ .create-login-link a {
485
+ color: #137ec4;
486
+ font-weight: 700;
487
+ }
488
+
489
+ .create-footer {
490
+ grid-column: 1 / -1;
491
+ text-align: center;
492
+ color: #010207;
493
+ font-size: 0.9rem;
494
+ margin-top: 8px;
495
+ }
496
+
497
+ .form-field .error {
498
+ color: #ff5252;
499
+ font-size: 0.85rem;
500
+ margin-top: 0px;
501
+ margin-left: 10px;
502
+ }
503
+
504
+
505
+
506
+ .welcome-info-box {
507
+ position: absolute;
508
+ top: 32px;
509
+ left: 0;
510
+ width: 100%;
511
+ padding: 0 32px;
512
+ z-index: 2;
513
+ text-align: left;
514
+ }
515
+
516
+ .welcome-info-title {
517
+ font-size: 1.35rem;
518
+ font-weight: 800;
519
+ color: #fff;
520
+ margin-bottom: 6px;
521
+ letter-spacing: 0.5px;
522
+ }
523
+
524
+ .welcome-info-desc {
525
+ font-size: 1.08rem;
526
+ color: #fff;
527
+ margin-bottom: 8px;
528
+ }
529
+
530
+ .welcome-info-link {
531
+ color: #fff;
532
+ font-weight: 700;
533
+ text-decoration: underline;
534
+ cursor: pointer;
535
+ transition: color 0.2s;
536
+ }
537
+
538
+ .welcome-info-link:hover {
539
+ color: #23395d;
540
+ }
541
+
542
+ .rotation-container {
543
+ display: flex;
544
+ align-items: center;
545
+ justify-content: center;
546
+ position: absolute;
547
+ inset: 0;
548
+ pointer-events: none;
549
+ }
550
+
551
+ .rotation-item {
552
+ display: inline-block;
553
+ will-change: transform;
554
+ pointer-events: auto;
555
+ position: relative;
556
+ }
557
+
558
+ @keyframes rotateAnimation {
559
+ 0% {
560
+ transform: rotate(0deg);
561
+ }
562
+
563
+ 100% {
564
+ transform: rotate(1turn);
565
+ }
566
+ }
567
+
568
+ .rotation-item:nth-child(1) {
569
+ animation: rotateAnimation 24s linear infinite;
570
+ z-index: 1;
571
+ }
572
+
573
+ .rotation-item:nth-child(2) {
574
+ animation: rotateAnimation 36s linear infinite;
575
+ }
576
+
577
+ /* Extra whitespace and centering for small screens */
578
+ @media (max-width: 900px) {
579
+ .signup-container {
580
+ flex-direction: column;
581
+ width: 98vw;
582
+ align-items: center;
583
+ justify-content: center;
584
+ }
585
+
586
+ .signup-panel-right {
587
+ padding: 18px 6vw;
588
+ }
589
+
590
+ .create-card {
591
+ padding: 18px 6vw;
592
+ margin: 0 auto;
593
+ }
594
+
595
+ .create-form {
596
+ grid-template-columns: 1fr;
597
+ gap: 18px;
598
+ }
599
+
600
+ .create-btn {
601
+ width: 100%;
602
+ }
603
+ }
604
+
605
+ @media (max-width: 600px) {
606
+ .create-card {
607
+ padding: 10px 2vw;
608
+ }
609
+
610
+ .signup-panel-right {
611
+ padding: 10px 2vw;
612
+ }
613
+ }
614
+
615
+ .signin-close {
616
+ position: absolute;
617
+ top: 5px;
618
+ right: 5px;
619
+ width: 38px;
620
+ height: 38px;
621
+ border: none;
622
+ background: #14263c;
623
+ color: #fff;
624
+ border-radius: 50%;
625
+ font-size: 2rem;
626
+ font-weight: bold;
627
+ display: flex;
628
+ align-items: center;
629
+ justify-content: center;
630
+ cursor: pointer;
631
+ z-index: 10;
632
+ transition: background 0.2s, color 0.2s;
633
+ box-shadow: 0 2px 8px #0005;
634
+ }
635
+
636
+ .signin-close:hover {
637
+ background: #38bdf8;
638
+ color: #18314a;
639
+ }
640
+
641
+ .side-bg-shapes,
642
+ .bg-circle,
643
+ .bg-ring,
644
+ .circle,
645
+ .circle-large1,
646
+ .circle-large2,
647
+ .circle-large3,
648
+ .circle-large4 {
649
+ display: none !important;
650
+ opacity:0 !important;
651
+ pointer-events: none !important;
652
+ animation: none !important;
653
+ }
654
+
655
+ @keyframes floatSlow { }
656
+ @keyframes floatSlowReverse { }
657
+ @keyframes ringPulse { }
658
+
659
+ .side-panel.side-right .side-img { display:block !important; position:absolute; top:0; left:0; width:100%; height:100%; object-fit:cover; z-index:0; }
660
+ .side-panel.side-right .signup-panel-right, .side-panel.side-right .signup-panel-left { position: relative; z-index:1; }
661
+
662
+ .welcome-back-title,
663
+ .welcome-info-title {
664
+ color: #ffffff !important;
665
+ }
666
+
667
+ .side-panel.side-right .side-img {
668
+ z-index:0 !important; /* image underlay */
669
+ }
670
+
671
+ .side-panel.side-right .side-info-box,
672
+ .side-panel.side-right .side-welcome-overlay,
673
+ .welcome-info-box,
674
+ .welcome-back-title,
675
+ .welcome-info-title,
676
+ .side-panel.side-right .welcome-back-title {
677
+ position: relative !important;
678
+ z-index:5 !important; /* bring text above image */
679
+ color: #ffffff !important; /* white text */
680
+ text-shadow: 0 2px 8px rgba(0,0,0,0.6) !important; /* improve contrast */
681
+ }
682
+
683
+ .side-panel.side-right .side-info-box p,
684
+ .side-panel.side-right .side-welcome-overlay p,
685
+ .welcome-info-box p,
686
+ .welcome-info-desc {
687
+ color: #ffffff !important;
688
+ text-shadow: 0 1px 6px rgba(0,0,0,0.45) !important;
689
+ }
690
+
691
+ .side-panel.side-right .action-btn,
692
+ .side-panel.side-right .panel-cta,
693
+ .welcome-line-4,
694
+ .side-panel.side-right .action-btn {
695
+ position: relative;
696
+ z-index:6 !important;
697
+ }
698
+
699
+ .side-panel.side-right .side-welcome-overlay,
700
+ .side-panel.side-right .side-info-box,
701
+ .welcome-info-box {
702
+ position: relative !important;
703
+ z-index:10 !important;
704
+ color: #ffffff !important;
705
+ }
706
+
707
+ .side-panel.side-right .side-welcome-overlay .welcome-back-title,
708
+ .side-panel.side-right .side-info-box .welcome-back-title,
709
+ .welcome-info-box .welcome-back-title,
710
+ .side-panel.side-right .side-welcome-overlay .welcome-back-desc,
711
+ .side-panel.side-right .side-info-box .welcome-back-desc,
712
+ .welcome-info-box .welcome-info-desc,
713
+ .side-panel.side-right .side-welcome-overlay .welcome-line-3,
714
+ .side-panel.side-right .side-info-box .welcome-line-3 {
715
+ color: #ffffff !important;
716
+ text-shadow: 02px 8px rgba(0,0,0,0.6) !important;
717
+ }
718
+
719
+ .side-panel.side-right .side-welcome-overlay a,
720
+ .side-panel.side-right .side-info-box a,
721
+ .welcome-info-box a {
722
+ color: #ffffff !important;
723
+ text-decoration: underline;
724
+ }
725
+
726
+
727
+ @media (max-width:900px) {
728
+ .side-panel.side-right .side-info-box,
729
+ .welcome-info-box { max-width:92% !important; padding:12px !important; }
730
+ .side-panel.side-right .side-info-box .side-info-title,
731
+ .welcome-info-box .welcome-info-title { font-size:1.25rem !important; }
732
+ .side-panel.side-right .side-info-box .side-info-desc,
733
+ .welcome-info-box .welcome-info-desc { font-size:0.98rem !important; }
734
+ .side-panel.side-right .side-info-box .action-btn { padding:8px 12px !important; }
735
+ }
736
+
737
+
738
+ .info-btn {
739
+ background: #23395d;
740
+ color: #fff;
741
+ border: none;
742
+ border-radius: 20%;
743
+ width: 20px;
744
+ height: 20px;
745
+ font-size: 1.1rem;
746
+ font-weight: bold;
747
+ cursor: pointer;
748
+ margin-left: 1px;
749
+ }
750
+
751
+ .info-popup-bg {
752
+ position: fixed;
753
+ inset: 0;
754
+ background: rgba(30,41,59,0.45);
755
+ backdrop-filter: blur(2px);
756
+ z-index:;
757
+ display: flex;
758
+ align-items: center;
759
+ justify-content: center;
760
+ }
761
+
762
+ .info-popup {
763
+ background: rgba(255,255,255,0.85);
764
+ border-radius: 16px;
765
+ box-shadow: 08px 32px #38bdf844, 0024px #1e293b88;
766
+ padding: 24px 28px 18px 28px;
767
+ min-width: 320px;
768
+ max-width: 90vw;
769
+ text-align: left;
770
+ font-size: 0.98rem;
771
+ color: #23395d;
772
+ position: relative;
773
+ font-family: inherit;
774
+ left: 840px;
775
+ }
776
+
777
+ .info-title {
778
+ font-size: 1.08rem;
779
+ font-weight: 700;
780
+ margin-bottom: 8px;
781
+ color: #38bdf8;
782
+ letter-spacing: 0.5px;
783
+ }
784
+
785
+ .info-text {
786
+ font-size: 0.95rem;
787
+ color: #23395d;
788
+ opacity: 0.95;
789
+ }
790
+
791
+ .info-close {
792
+ position: absolute;
793
+ top: 8px;
794
+ right: 10px;
795
+ background: #38bdf8;
796
+ color: #fff;
797
+ border: none;
798
+ width: 26px;
799
+ height: 26px;
800
+ border-radius: 50%;
801
+ font-size: 1rem;
802
+ line-height: 1;
803
+ cursor: pointer;
804
+ }
805
+
806
+ /* Floating info popup (small) */
807
+ .info-popup-bg {
808
+ position: fixed;
809
+ inset: 0;
810
+ display: flex;
811
+ align-items: center;
812
+ justify-content: center;
813
+ z-index: 1200;
814
+ }
815
+
816
+ .info-popup {
817
+ background: rgba(255,255,255,0.98);
818
+ width: 420px;
819
+ max-width: calc(100% -48px);
820
+ border-radius: 12px;
821
+ padding: 14px 18px;
822
+ box-shadow: 0 12px 30px rgba(2,6,23,0.18);
823
+ position: relative;
824
+ color: #23395d;
825
+ font-size: 0.95rem;
826
+ }
827
+
828
+ .info-close {
829
+ position: absolute;
830
+ top: 8px;
831
+ right: 10px;
832
+ background: #38bdf8;
833
+ color: #fff;
834
+ border: none;
835
+ width: 26px;
836
+ height: 26px;
837
+ border-radius: 50%;
838
+ font-size: 1rem;
839
+ line-height: 1;
840
+ cursor: pointer;
841
+ }
842
+
843
+ .info-title {
844
+ color: #38bdf8;
845
+ font-weight: 800;
846
+ margin-bottom: 8px;
847
+ padding-left: 6px;
848
+ }
849
+
850
+ .info-text ul {
851
+ list-style: disc;
852
+ padding-left: 22px;
853
+ margin: 6px 0;
854
+ }
855
+
856
+ .info-text li {
857
+ margin-bottom: 8px;
858
+ line-height: 1.35;
859
+ }
860
+
861
+ .info-text li strong {
862
+ display: inline-block;
863
+ width: 120px;
864
+ font-weight: 800;
865
+ }
866
+
867
+ /* Ensure popup scales on small screens */
868
+ @media (max-width:520px) {
869
+ .role-help-modal, .info-popup {
870
+ width: auto;
871
+ margin: 016px;
872
+ padding: 12px;
873
+ }
874
+
875
+ .role-help-list li strong, .info-text li strong {
876
+ display: block;
877
+ width: auto;
878
+ margin-bottom: 4px;
879
+ }
880
+ }
881
+
882
+ /* Email display for2FA: plain text and label aligned with other controls */
883
+ .twofa-email-display {
884
+ margin-left:25px;
885
+ margin-top:8px;
886
+ display: flex;
887
+ gap:12px;
888
+ align-items: center;
889
+ }
890
+ .twofa-email-label {
891
+ font-weight:600;
892
+ color: #23395d;
893
+ }
894
+ .twofa-email-value {
895
+ color: #516b78;
896
+ font-weight:600;
897
+ }
898
+
899
+ /* remove any residual styles for alternate email inputs */
900
+ .twofa-alt-email { display: none !important; }
901
+
902
+ /* Cross indicator under invalid fields */
903
+ .field-cross {
904
+ color: #ff5252;
905
+ font-weight:800;
906
+ margin-top:4px;
907
+ font-size:1rem;
908
+ }
909
+
910
+ /* Ensure input wrapper has room for cross under it */
911
+ .form-field { position: relative; margin-bottom:8px; }
912
+
913
+ /* Ensure main-panel is a positioned container so pinned controls can be placed inside it */
914
+ .main-panel {
915
+ position: relative;
916
+ /* reserve space so form content doesn't overlap pinned controls */
917
+ padding-bottom:120px; /* space for button + footer */
918
+ }
919
+
920
+ /* Pin the create button to the bottom center of the viewport (fixed) so it does not move when form content changes */
921
+ .main-panel .create-btn {
922
+ position: fixed; /* was absolute - changed to fixed */
923
+ left:86%;
924
+ transform: translateX(-50%);
925
+ bottom:56px; /* distance above footer */
926
+ width:48%;
927
+ max-width:360px;
928
+ margin:0;
929
+ z-index:50;
930
+ }
931
+
932
+ /* Pin the footer/version text to the bottom center of the viewport (fixed) */
933
+ .main-panel .create-footer {
934
+ position: fixed; /* was absolute - changed to fixed */
935
+ left:86%;
936
+ transform: translateX(-50%);
937
+ bottom:16px;
938
+ z-index:50;
939
+ width:100%;
940
+ text-align: center;
941
+ pointer-events: none;
942
+ }
943
+
944
+ /* Make sure Google row and other form elements don't get hidden behind the pinned controls */
945
+ .main-panel .google-signup-row {
946
+ margin-bottom:96px;
947
+ }
948
+
949
+ /* Remove any large flow margin on create button and rely on pinned positioning */
950
+ .create-btn { margin-top:0; }
951
+
952
+ /* Responsive adjustments */
953
+ @media (max-width:900px) {
954
+ .main-panel { padding-bottom:150px; }
955
+ .main-panel .create-btn {
956
+ bottom:86px;
957
+ width:90%;
958
+ max-width: none;
959
+ }
960
+ .main-panel .create-footer { bottom:12px; }
961
+ .main-panel .google-signup-row { margin-bottom:120px; }
962
+ }
963
+
964
+ /* Keep disabled styling intact */
965
+ .create-btn[disabled] { opacity:0.9; }
966
+ /* Cross shown inside confirm password input wrapper when mismatch */
967
+ .input-with-eye { position: relative; }
968
+ /* Removed the inside-cross element */
969
+ /* .input-with-eye .inside-cross { ... } */
970
+
971
+ /* Password field layout: label + gap + cross */
972
+ .password-field {
973
+ display: flex;
974
+ flex-direction: column;
975
+ }
976
+ .password-field label {
977
+ display: flex;
978
+ align-items: center;
979
+ gap:8px;
980
+ }
981
+
982
+ /* Keep label-cross visible when rendered by *ngIf */
983
+ .label-cross { display: inline-block; }
984
+
985
+ /* Force label cross visible and prominent */
986
+ .password-field label { padding-right:48px; }
987
+ .password-field .label-cross {
988
+ display: inline-block !important;
989
+ position: absolute !important;
990
+ right:8px !important;
991
+ top:50% !important;
992
+ transform: translateY(-50%) !important;
993
+ color: #ff5252 !important;
994
+ font-weight:900 !important;
995
+ font-size:1.1rem !important;
996
+ z-index:9999 !important;
997
+ pointer-events: none !important;
998
+ }
999
+
1000
+ /* Ensure inside-cross sits above everything */
1001
+ /* .input-with-eye .inside-cross { ... } */
1002
+
1003
+ /* Bring eye toggle slightly below inside-cross */
1004
+ .input-with-eye .eye-toggle { z-index:9000 !important; }
1005
+
1006
+ /* Strong outline and color on mismatch */
1007
+ .input-with-eye.password-mismatch input {
1008
+ outline:2px solid rgba(255,82,82,0.18) !important;
1009
+ box-shadow:0002px rgba(255,82,82,0.08) !important;
1010
+ color: #b91c1c !important;
1011
+ }
1012
+
1013
+ /* Ensure eye toggle never overlays popups */
1014
+ .input-with-eye .eye-toggle { z-index:20 !important; }
1015
+
1016
+ /* Keep the role info popup above all form controls */
1017
+ .info-popup-bg { z-index:1200 !important; }
1018
+ .info-popup { z-index:1201 !important; }
src/app/homepage/sign-up-1/sign-up/sign-up.component.html ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <section class="signup-popup">
2
+ <div class="ai-particle-bg"></div>
3
+ <div class="signup-header">
4
+ <div class="signup-logo">
5
+
6
+ </div>
7
+ </div>
8
+
9
+ <div class="auth-card">
10
+ <div class="card-inner">
11
+ <!-- FRONT: sign-up on main panel, image side on left -->
12
+ <div class="card-front">
13
+ <button class="signin-close" type="button" (click)="closePopup()" aria-label="Close">&times;</button>
14
+
15
+ <div class="card-content">
16
+ <div class="side-panel side-left">
17
+ <div class="signup-panel-left">
18
+
19
+ </div>
20
+ <div class="main-panel" [class.pwd-mismatch]="submitted && pwdMismatch">
21
+ <h2 class="signup-title center-title">Create An Account</h2>
22
+
23
+ <!-- aria-live region for screen readers (visually hidden) -->
24
+ <div class="sr-only" aria-live="assertive" *ngIf="submitted && invalidFieldsMessage">{{ invalidFieldsMessage }}</div>
25
+
26
+ <!-- Extend form to include terms, reCAPTCHA, and submit button -->
27
+ <form [formGroup]="form" (ngSubmit)="submit()" class="create-form" novalidate autocomplete="off">
28
+ <div class="form-row">
29
+ <div class="form-field">
30
+ <label for="firstName">First Name</label>
31
+ <input id="firstName" type="text" placeholder="First Name" formControlName="name" [attr.aria-invalid]="controlHasError('name')" [class.input-invalid]="submitted && (controlHasError('name') || nameHasDigits('name'))" />
32
+ <!-- Name errors intentionally hidden until Create Account and per user request no name error texts -->
33
+ </div>
34
+ <div class="form-field">
35
+ <label for="lastName">Last Name</label>
36
+ <input id="lastName" type="text" placeholder="Last Name" formControlName="lastName" [attr.aria-invalid]="controlHasError('lastName')" [class.input-invalid]="submitted && (controlHasError('lastName') || nameHasDigits('lastName'))" />
37
+ <!-- Last name errors intentionally hidden until Create Account and per user request no name error texts -->
38
+ </div>
39
+ </div>
40
+ <div class="form-row">
41
+ <div class="form-field email-field" [class.email-invalid]="submitted && controlHasError('email')">
42
+ <label for="email">Email</label>
43
+ <input id="email" type="text" placeholder="email@gmail.com" formControlName="email" [attr.aria-invalid]="controlHasError('email')" [class.input-invalid]="submitted && controlHasError('email')" autocomplete="off" autocapitalize="off" spellcheck="false" />
44
+ <small *ngIf="submitted && controlHasError('email','required')" class="error">Email is required.</small>
45
+ <small *ngIf="submitted && controlHasError('email','pattern')" class="error">Enter a valid email/phone.</small>
46
+ <small *ngIf="submitted && controlHasError('email','emailExists')" class="error">Email already exists.</small>
47
+ </div>
48
+ <div class="form-field role-field-wrapper">
49
+ <label for="roleGroup">Role Group
50
+ <span class="label-cross" *ngIf="submitted && controlHasError('roleGroup')" aria-hidden="true">✖</span>
51
+ <button type="button" class="info-btn" (click)="showInfo = true" aria-label="Role info">i</button>
52
+ </label>
53
+ <select id="roleGroup" formControlName="roleGroup" (change)="onRoleGroupChange($any($event.target).value)" aria-label="Role group" [class.input-invalid]="submitted && controlHasError('roleGroup')">
54
+ <option value="">-- Select role group --</option>
55
+ <option *ngFor="let g of roleGroups" [value]="g.key">{{ g.label }}</option>
56
+ </select>
57
+ <small *ngIf="submitted && controlHasError('roleGroup','required')" class="error">Please select a role group.</small>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <div class="form-field password-field">
63
+ <label for="password">Create Password
64
+ </label>
65
+ <!-- wrap input and eye in a positioned wrapper so the eye is anchored to the input only -->
66
+ <div class="input-with-eye">
67
+ <input id="password" [type]="showPassword ? 'text' : 'password'" placeholder="••••••••" formControlName="password" (keydown)="onPasswordKey($event)" (keyup)="onPasswordKey($event)" [class.input-invalid]="submitted && controlHasError('password')" autocomplete="new-password" autocapitalize="off" spellcheck="false" />
68
+ <!-- local eye toggle button (sign-up only) -->
69
+ <button type="button" class="eye-toggle variant-signup" aria-label="Show password" [attr.aria-pressed]="showPassword" (click)="togglePasswordVisibility()">
70
+ <i class="fa-solid" [ngClass]="showPassword ? 'fa-eye' : 'fa-eye-slash'" aria-hidden="true"></i>
71
+ </button>
72
+ </div>
73
+ <div *ngIf="capsLockOn" class="caps-lock-warning">Caps Lock is on</div>
74
+ <small *ngIf="submitted && controlHasError('password','required')" class="error">Password is required.</small>
75
+ <small *ngIf="submitted && form.get('password')?.hasError('passwordPolicy')" class="policy-info">
76
+ Required at least 8 characters using letters, numbers, and special character.
77
+ </small>
78
+ </div>
79
+
80
+ <div class="form-field password-field">
81
+ <label for="confirmPassword">Confirm Password
82
+ </label>
83
+ <!-- wrap confirm input and eye similarly -->
84
+ <div class="input-with-eye" [class.password-mismatch]="submitted && pwdMismatch" [class.confirm-cross]="submitted && pwdMismatch">
85
+ <input id="confirmPassword" [type]="showConfirmPassword ? 'text' : 'password'" placeholder="••••••••" formControlName="confirmPassword" (keydown)="onPasswordKey($event)" (keyup)="onPasswordKey($event)" [class.input-invalid]="submitted && (controlHasError('confirmPassword') || showPwdMismatch())" autocomplete="new-password" autocapitalize="off" spellcheck="false" />
86
+ <!-- local eye toggle for confirm (sign-up only) -->
87
+ <button type="button" class="eye-toggle variant-signup" aria-label="Show confirm password" [attr.aria-pressed]="showConfirmPassword" (click)="toggleConfirmPasswordVisibility()">
88
+ <i class="fa-solid" [ngClass]="showConfirmPassword ? 'fa-eye' : 'fa-eye-slash'" aria-hidden="true"></i>
89
+ </button>
90
+ </div>
91
+ <div *ngIf="capsLockOn" class="caps-lock-warning">Caps Lock is on</div>
92
+ <small *ngIf="submitted && controlHasError('confirmPassword','required')" class="error">Confirm password is required.</small>
93
+ <small *ngIf="submitted && showPwdMismatch()" class="error">Passwords do not match.</small>
94
+ </div>
95
+ </div>
96
+
97
+ <!--2FA opt-in and method selection -->
98
+ <div class="form-row">
99
+ <div class="form-field twofa-field">
100
+ <!-- single row: checkbox + methods -->
101
+ <div class="twofa-row">
102
+ <div class="form-checkbox twofa-checkbox">
103
+ <input type="checkbox" id="twoFAOptIn" formControlName="twoFAOptIn" />
104
+ <label for="twoFAOptIn">Enable Two-Factor Authentication (2FA)</label>
105
+ </div>
106
+
107
+ <div *ngIf="form.get('twoFAOptIn')?.value" class="twofa-methods">
108
+ <label class="inline-control plain-control"><input type="radio" formControlName="twoFAMethod" value="email" /> Email</label>
109
+ <label class="inline-control plain-control"><input type="radio" formControlName="twoFAMethod" value="sms" /> SMS</label>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Email-specific display: show account email (plain text) -->
114
+ <div *ngIf="form.get('twoFAOptIn')?.value && form.get('twoFAMethod')?.value === 'email'" class="twofa-email-display">
115
+ <label class="twofa-email-label">2FA will be sent to</label>
116
+ <div class="twofa-email-value">{{ form.get('email')?.value || '—' }}</div>
117
+ </div>
118
+
119
+ <!-- SMS-specific input -->
120
+ <div *ngIf="form.get('twoFAOptIn')?.value && form.get('twoFAMethod')?.value === 'sms'" class="twofa-sms-options">
121
+ <label for="twoFAPhone">Phone number for SMS2FA</label>
122
+ <input id="twoFAPhone" type="tel" placeholder="+1234567890" formControlName="twoFAPhone" (input)="formatPhone($event)" inputmode="tel" autocomplete="tel" [class.input-invalid]="submitted && form.get('twoFAPhone')?.invalid" />
123
+ <small *ngIf="submitted && form.get('twoFAPhone')?.invalid" class="error">Enter a valid phone number for SMS 2FA.</small>
124
+ </div>
125
+
126
+ <small *ngIf="twoFAError" class="error twofa-error">{{ twoFAError }}</small>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- reCAPTCHA -->
131
+ <div id="recaptcha-container" class="recaptcha-container"></div>
132
+ <small *ngIf="recaptchaError" class="error">{{ recaptchaError }}</small>
133
+
134
+ <!-- Submit -->
135
+ <button class="create-btn ai-pulse" type="submit" [disabled]="loading || !isFormFilled()">
136
+ <ng-container *ngIf="!loading; else creatingAccount">
137
+ Create Account
138
+ </ng-container>
139
+ <ng-template #creatingAccount>
140
+ <span class="spinner"></span> Creating Account...
141
+ </ng-template>
142
+ </button>
143
+ </form>
144
+
145
+ <!-- Terms & Privacy moved outside form group, aligned with2FA -->
146
+ <div [formGroup]="form">
147
+ <div class="form-checkbox">
148
+ <input type="checkbox" id="terms" formControlName="terms" />
149
+ <label for="terms">Agree to our <a href="/legal/terms" target="_blank" rel="_noopener noreferrer">Terms &amp; Conditions</a> and <a href="/legal/privacy" target="_blank" rel="_noopener noreferrer">Privacy Policy</a>.</label>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- Google Sign-In button -->
154
+ <div class="google-signup-row">
155
+ <div id="google-signup-btn-div">
156
+ <div class="g-signin2" data-width="240" data-height="50" data-longtitle="true"></div>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="create-footer"><b>Version1.0.0 | © Pykara Technologies</b></div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ <!-- Detached overlay/modal so layout doesn't shift -->
167
+ <div class="role-help-overlay" *ngIf="showRoleInfo" (click)="hideRoleInfo()">
168
+ <div class="role-help-modal" role="dialog" aria-label="Role descriptions" (click)="$event.stopPropagation()">
169
+ <button type="button" class="role-help-close" aria-label="Close role info" (click)="hideRoleInfo()">×</button>
170
+ <h3 class="role-help-title">Role Information</h3>
171
+ <ul class="role-help-list">
172
+ <li><strong>Law Enforcement</strong><br/>Investigator, Supervisor — for users handling case investigation, interviews, evidence review, and workflow supervision. These roles may require admin approval or identity verification.</li>
173
+ <li><strong>Legal</strong><br/>Lawyer — for legal professionals involved in reviewing case summaries, preparing legal notes, analyzing evidence, or assisting in compliance processes.</li>
174
+ <li><strong>Administration</strong><br/>Admin — for users who manage system settings, user access, roles, case assignments, and overall application operations.</li>
175
+ <li><strong>General Access</strong><br/>Other — for users who require limited or basic access, such as support staff, trainees, or external collaborators.</li>
176
+ </ul>
177
+ <p class="role-help-tip">If you are not sure which role to choose, select <strong>Other</strong>, or contact: <a href="mailto:support@pykara.ai">support@pykara.ai</a></p>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Floating Info Popup -->
182
+ <div *ngIf="showInfo" class="info-popup-bg" (click)="showInfo = false">
183
+ <div class="info-popup" (click)="$event.stopPropagation()">
184
+ <button class="info-close" type="button" (click)="showInfo = false" aria-label="Close">&times;</button>
185
+ <div class="info-title">Role Information</div>
186
+
187
+ <div class="info-text">
188
+ <ul>
189
+ <li><strong>Law Enforcement</strong><br/>Investigator, Supervisor — for users handling case investigation, interviews, evidence review, and workflow supervision. These roles may require admin approval or identity verification.</li>
190
+ <li><strong>Legal</strong><br/>Lawyer — for legal professionals involved in reviewing case summaries, preparing legal notes, analyzing evidence, or assisting in compliance processes.</li>
191
+ <li><strong>Administration</strong><br/>Admin — for users who manage system settings, user access, roles, case assignments, and overall application operations.</li>
192
+ <li><strong>General Access</strong><br/>Other — for users who require limited or basic access, such as support staff, trainees, or external collaborators.</li>
193
+ </ul>
194
+ <p>If you are not sure which role to choose, select <strong>Other</strong>, or contact: <a href="mailto:info@pykara.ai">info@pykara.ai</a></p>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
src/app/homepage/sign-up-1/sign-up/sign-up.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { SignUpComponent } from './sign-up.component';
4
+
5
+ describe('SignUpComponent', () => {
6
+ let component: SignUpComponent;
7
+ let fixture: ComponentFixture<SignUpComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [SignUpComponent]
12
+ });
13
+ fixture = TestBed.createComponent(SignUpComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/homepage/sign-up-1/sign-up/sign-up.component.ts ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CommonModule } from '@angular/common';
2
+ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { SignUpService } from './sign-up.service'; // Import the SignUpService
5
+ // Fix import paths to point to root app services/components
6
+ import { AuthService } from '../../../auth.service';
7
+ import { trigger, transition, style, animate } from '@angular/animations';
8
+ import { ChangeDetectorRef, Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
9
+ import { EyeToggleComponent } from '../../../shared/eye-toggle/eye-toggle.component';
10
+ import { Subscription } from 'rxjs';
11
+
12
+ // allow grecaptcha global variable
13
+ declare global {
14
+ interface Window { grecaptcha: any; }
15
+ }
16
+ declare const grecaptcha: any;
17
+
18
+ export function nameValidator(control: AbstractControl): ValidationErrors | null {
19
+ const value = control.value || '';
20
+ // Only allow alphabets and spaces, min2 chars
21
+ if (!/^[A-Za-z ]{2,}$/.test(value)) {
22
+ return { invalidName: true };
23
+ }
24
+ return null;
25
+ }
26
+
27
+ @Component({
28
+ selector: 'app-sign-up',
29
+ standalone: true,
30
+ imports: [CommonModule, ReactiveFormsModule, RouterLink, EyeToggleComponent],
31
+ templateUrl: './sign-up.component.html',
32
+ styleUrls: ['./sign-up.component.css'],
33
+ animations: [
34
+ trigger('fadeInOut', [
35
+ transition(':enter', [
36
+ style({ opacity:0 }),
37
+ animate('600ms', style({ opacity:1 }))
38
+ ]),
39
+ transition(':leave', [
40
+ animate('600ms', style({ opacity:0 }))
41
+ ])
42
+ ])
43
+ ]
44
+ })
45
+ export class SignUpComponent implements OnInit, OnDestroy {
46
+ @Input() embedded = false; // when true, render only inner panel (for embedding in auth-card)
47
+ @Input() cardState?: 'signup' | 'signin';
48
+ @Output() switchToSignIn = new EventEmitter<void>();
49
+ form: FormGroup;
50
+ private isSubmitting = false;
51
+
52
+ // Role info popover logic preserved
53
+ showRoleInfo = false;
54
+ toggleRoleInfo(ev?: Event) { ev?.stopPropagation(); this.showRoleInfo = !this.showRoleInfo; }
55
+ hideRoleInfo() { this.showRoleInfo = false; }
56
+
57
+ @Output() close = new EventEmitter<void>();
58
+
59
+ showPassword = false;
60
+ showConfirmPassword = false;
61
+ errorMessage = '';
62
+
63
+ isSignUpActive = true; //  Added state for sign-up panel activation
64
+ public loading = false; // Used to disable the button during sign-up
65
+ submitted = false; // Track form submission status
66
+ // Explicit flag to drive mismatch UI (set on submit)
67
+ public pwdMismatch: boolean = false;
68
+
69
+ // Added: terms & conditions error handling
70
+ termsError: string = '';
71
+ twoFAError: string = ''; // validation message for2FA
72
+
73
+ // reCAPTCHA
74
+ private recaptchaSiteKey = 'YOUR_RECAPTCHA_SITE_KEY'; // <-- Replace with your real site key
75
+ private recaptchaWidgetId: number | null = null;
76
+ recaptchaError = '';
77
+
78
+ facts: string[] = [
79
+ '🧠 Py-Detect AI analyzes tone, emotion, and consistency.',
80
+ '🎥 Supports video and audio interrogation analysis.',
81
+ '📊 Generates instant investigation summary reports.'
82
+ ];
83
+ currentFact: string = this.facts[0];
84
+ private factIndex =0;
85
+ private factInterval: any;
86
+
87
+ showInfo = false;
88
+
89
+ // Caps Lock state
90
+ capsLockOn = false;
91
+
92
+ // Role grouping options
93
+ roleGroups = [
94
+ { key: 'lawenforcement', label: 'Law Enforcement', subs: [ { key: 'investigator', label: 'Investigator' }, { key: 'supervisor', label: 'Supervisor' } ] },
95
+ { key: 'legal', label: 'Legal', subs: [ { key: 'lawyer', label: 'Lawyer' } ] },
96
+ { key: 'adminothers', label: 'Admin & Others', subs: [ { key: 'admin', label: 'Admin' }, { key: 'other', label: 'Other' } ] }
97
+ ];
98
+
99
+ availableSubRoles: Array<{key:string,label:string}> = [];
100
+
101
+ // subscriptions for2FA opt-in/method/phone changes
102
+ private twoFASubscriptions: Subscription[] = [];
103
+ private savedTwoFAMethod: string | null = null;
104
+ private savedTwoFAPhone: string | null = null;
105
+
106
+ // Additional property to track form enable state
107
+ public canEnable: boolean = false;
108
+ private formSubscription?: Subscription;
109
+
110
+ constructor(
111
+ private fb: FormBuilder,
112
+ private router: Router,
113
+ private signUpService: SignUpService,
114
+ private cdr: ChangeDetectorRef
115
+ ) {
116
+ this.form = this.fb.group({
117
+ name: ['', [Validators.required, Validators.minLength(2), nameValidator]],
118
+ lastName: ['', [Validators.required, Validators.minLength(2), nameValidator]],
119
+ email: ['', [
120
+ Validators.required,
121
+ Validators.pattern(/(^[^@]+@[^@]+\.[^@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)
122
+ ]],
123
+ password: ['', [Validators.required, Validators.minLength(8), passwordPolicyValidator]],
124
+ confirmPassword: ['', [Validators.required]],
125
+ roleGroup: ['', [Validators.required]],
126
+ role: ['', [Validators.required]],
127
+ terms: [false, Validators.requiredTrue], // Added terms control with requiredTrue validator
128
+ // reCAPTCHA token
129
+ // recaptcha: ['', [Validators.required]],
130
+
131
+ //2FA controls
132
+ twoFAOptIn: [false],
133
+ twoFAMethod: [''], // 'email' or 'sms'
134
+ twoFAUseSameEmail: [true],
135
+ twoFAAltEmail: ['', [Validators.pattern(/^[^@]+@[^@]+\.[^@]+$/)]],
136
+ twoFAPhone: ['', [Validators.pattern(/^\+?\d[\d\-\s]{8,14}\d$/)]]
137
+
138
+ }, { validators: [this.passwordsMatchValidator] });
139
+
140
+ // Close popover when clicking anywhere in document (capture phase not needed here)
141
+ document.addEventListener('click', () => this.hideRoleInfo());
142
+ }
143
+
144
+ ngOnInit() {
145
+ this.startFactRotation();
146
+ this.loadRecaptcha();
147
+
148
+ // update initial canEnable state
149
+ this.canEnable = this.isFormFilled();
150
+
151
+ // keep canEnable updated whenever form values change
152
+ this.formSubscription = this.form.valueChanges.subscribe(() => {
153
+ this.canEnable = this.isFormFilled();
154
+ // debug log to help diagnose why button remains disabled
155
+ // eslint-disable-next-line no-console
156
+ console.log('form value changed, isFormFilled=', this.canEnable, 'form values:', this.form.value);
157
+ try { this.cdr.detectChanges(); } catch (e) { /* ignore */ }
158
+ // Do NOT compute pwdMismatch here; only compute on submit so crosses appear after clicking Create Account
159
+ });
160
+
161
+ // Persist2FA selection when toggling opt-in
162
+ const optInCtrl = this.form.get('twoFAOptIn');
163
+ const methodCtrl = this.form.get('twoFAMethod');
164
+ const phoneCtrl = this.form.get('twoFAPhone');
165
+
166
+ if (optInCtrl) {
167
+ const sub = optInCtrl.valueChanges.subscribe((val: boolean) => {
168
+ if (!val) {
169
+ // save current selections when user unchecks
170
+ this.savedTwoFAMethod = methodCtrl?.value || null;
171
+ this.savedTwoFAPhone = phoneCtrl?.value || null;
172
+ } else {
173
+ // restore previous selections when user re-checks
174
+ if (this.savedTwoFAMethod) {
175
+ methodCtrl?.setValue(this.savedTwoFAMethod, { emitEvent: false });
176
+ }
177
+ if (this.savedTwoFAPhone) {
178
+ phoneCtrl?.setValue(this.savedTwoFAPhone, { emitEvent: false });
179
+ }
180
+ }
181
+ });
182
+ this.twoFASubscriptions.push(sub);
183
+ }
184
+
185
+ if (this.form.get('twoFAMethod')) {
186
+ const sub2 = this.form.get('twoFAMethod')!.valueChanges.subscribe((m: string) => {
187
+ // update saved value whenever user changes method while visible
188
+ this.savedTwoFAMethod = m || this.savedTwoFAMethod;
189
+ });
190
+ this.twoFASubscriptions.push(sub2);
191
+ }
192
+
193
+ if (this.form.get('twoFAPhone')) {
194
+ const sub3 = this.form.get('twoFAPhone')!.valueChanges.subscribe((p: string) => {
195
+ this.savedTwoFAPhone = p || this.savedTwoFAPhone;
196
+ });
197
+ this.twoFASubscriptions.push(sub3);
198
+ }
199
+ }
200
+
201
+ ngOnDestroy() {
202
+ if (this.factInterval) {
203
+ clearInterval(this.factInterval);
204
+ }
205
+ // cleanup subscriptions
206
+ this.twoFASubscriptions.forEach(s => s.unsubscribe());
207
+ if (this.formSubscription) {
208
+ this.formSubscription.unsubscribe();
209
+ }
210
+ }
211
+
212
+ private loadRecaptcha() {
213
+ // Don't attempt to load if no site key configured
214
+ if (!this.recaptchaSiteKey || this.recaptchaSiteKey === 'YOUR_RECAPTCHA_SITE_KEY') {
215
+ // skip loading but keep control present so dev can replace key
216
+ console.warn('reCAPTCHA site key not configured. Please replace recaptchaSiteKey with your site key.');
217
+ return;
218
+ }
219
+
220
+ const existing = document.querySelector('script[src*="recaptcha/api.js"]');
221
+ const renderWhenReady = () => {
222
+ try {
223
+ // render explicit
224
+ this.recaptchaWidgetId = window.grecaptcha.render('recaptcha-container', {
225
+ sitekey: this.recaptchaSiteKey,
226
+ callback: (token: string) => this.onRecaptchaSuccess(token),
227
+ 'expired-callback': () => this.onRecaptchaExpired()
228
+ });
229
+ } catch (e) {
230
+ console.error('Failed to render grecaptcha', e);
231
+ }
232
+ };
233
+
234
+ if (existing) {
235
+ if (window.grecaptcha && window.grecaptcha.render) {
236
+ renderWhenReady();
237
+ } else {
238
+ // wait for onload
239
+ existing.addEventListener('load', renderWhenReady);
240
+ }
241
+ return;
242
+ }
243
+
244
+ const script = document.createElement('script');
245
+ script.src = 'https://www.google.com/recaptcha/api.js?onload=__onRecaptchaLoadCallback&render=explicit';
246
+ script.async = true;
247
+ script.defer = true;
248
+ (window as any).__onRecaptchaLoadCallback = () => {
249
+ renderWhenReady();
250
+ };
251
+ document.head.appendChild(script);
252
+ }
253
+
254
+ onRecaptchaSuccess(token: string) {
255
+ this.form.get('recaptcha')?.setValue(token);
256
+ this.recaptchaError = '';
257
+ this.cdr.markForCheck();
258
+ }
259
+
260
+ onRecaptchaExpired() {
261
+ this.form.get('recaptcha')?.setValue('');
262
+ this.recaptchaError = 'reCAPTCHA expired. Please verify again.';
263
+ this.cdr.markForCheck();
264
+ }
265
+
266
+ resetRecaptcha() {
267
+ try {
268
+ if (window.grecaptcha && this.recaptchaWidgetId != null) {
269
+ window.grecaptcha.reset(this.recaptchaWidgetId);
270
+ }
271
+ this.form.get('recaptcha')?.setValue('');
272
+ this.cdr.markForCheck();
273
+ } catch (e) { /* ignore */ }
274
+ }
275
+
276
+ startFactRotation() {
277
+ this.factInterval = setInterval(() => {
278
+ this.factIndex = (this.factIndex +1) % this.facts.length;
279
+ this.currentFact = this.facts[this.factIndex];
280
+ },5000);
281
+ }
282
+
283
+ control(path: string): AbstractControl | null { return this.form.get(path); }
284
+
285
+ controlHasError(path: string, error?: string): boolean {
286
+ const c = this.control(path);
287
+ if (!c) return false;
288
+ // Only show validation state after the user has attempted submission
289
+ if (!this.submitted) return false;
290
+ return error ? !!(c.errors?.[error]) : !!(c.invalid);
291
+ }
292
+
293
+ showPwdMismatch(): boolean {
294
+ return !!this.pwdMismatch;
295
+ }
296
+
297
+ passwordsMatchValidator(group: AbstractControl) {
298
+ const pw = group.get('password')?.value;
299
+ const cpw = group.get('confirmPassword')?.value;
300
+ // don't mark mismatch unless both fields have a value
301
+ if (!pw || !cpw) return null;
302
+ return pw === cpw ? null : { passwordMismatch: true };
303
+ }
304
+
305
+ togglePasswordVisibility() {
306
+ this.showPassword = !this.showPassword;
307
+ }
308
+ toggleConfirmPasswordVisibility() {
309
+ this.showConfirmPassword = !this.showConfirmPassword;
310
+ }
311
+
312
+ invalidFieldsMessage: string = ''; // ARIA live message for screen reader
313
+
314
+ submit() {
315
+ // Confirm button click
316
+ this.submitted = true; // Track the form submission attempt
317
+
318
+ // Mark all form controls as touched to trigger validation
319
+ this.form.markAllAsTouched();
320
+ // Ensure validators (including group validator) run so form.errors is populated
321
+ this.form.updateValueAndValidity({ onlySelf: false, emitEvent: true });
322
+ // Trigger change detection so template picks up updated form errors immediately
323
+ try { this.cdr.detectChanges(); } catch (e) { this.cdr.markForCheck(); }
324
+
325
+ // Ensure the group validator ran and update the pwdMismatch flag used by template bindings
326
+ try { this.cdr.detectChanges(); } catch (e) { this.cdr.markForCheck(); }
327
+ // update pwdMismatch from the form-level validator result so existing template bindings work
328
+ this.pwdMismatch = !!this.form.hasError('passwordMismatch');
329
+ // Debug logs to verify validation state
330
+ // eslint-disable-next-line no-console
331
+ console.log('submit: form.errors=', this.form.errors, 'pwdMismatch=', this.pwdMismatch);
332
+ // eslint-disable-next-line no-console
333
+ console.log('submit: email.errors=', this.form.get('email')?.errors, 'email.invalid=', this.form.get('email')?.invalid);
334
+
335
+ // Build invalid fields message for screen readers
336
+ const missing: string[] = [];
337
+ const checks: Array<[string, string]> = [ ['name','First name'], ['lastName','Last name'], ['email','Email'], ['roleGroup','Role'], ['password','Password'], ['confirmPassword','Confirm password'] ];
338
+ for (const [k,label] of checks) {
339
+ if (this.form.get(k)?.invalid) missing.push(label);
340
+ }
341
+ if (this.form.get('twoFAOptIn')?.value && this.form.get('twoFAMethod')?.value === 'sms') {
342
+ if (this.form.get('twoFAPhone')?.invalid) missing.push('2FA phone');
343
+ }
344
+ if (!this.form.get('terms')?.value) missing.push('Terms and Privacy');
345
+ this.invalidFieldsMessage = missing.length ? 'Please correct the following fields: ' + missing.join(', ') : '';
346
+
347
+ // Check if the form is invalid
348
+ if (this.form.invalid) {
349
+ return;
350
+ }
351
+
352
+ // Check terms & conditions acceptance
353
+ if (!this.form.get('terms')?.value) {
354
+ this.termsError = 'Please accept Terms & Conditions.';
355
+ return;
356
+ }
357
+ this.termsError = '';
358
+
359
+ // Additional2FA validation if opted-in
360
+ this.twoFAError = '';
361
+ if (this.form.get('twoFAOptIn')?.value) {
362
+ const method = this.form.get('twoFAMethod')?.value;
363
+ if (!method) {
364
+ this.twoFAError = 'Please select a2FA method (Email or SMS).';
365
+ return;
366
+ }
367
+ if (method === 'email') {
368
+ const useSame = this.form.get('twoFAUseSameEmail')?.value;
369
+ if (!useSame) {
370
+ const alt = this.form.get('twoFAAltEmail');
371
+ if (!alt || alt.invalid) {
372
+ this.twoFAError = 'Please provide a valid alternate email for2FA.';
373
+ return;
374
+ }
375
+ }
376
+ }
377
+ if (method === 'sms') {
378
+ const phone = this.form.get('twoFAPhone');
379
+ if (!phone || phone.invalid) {
380
+ this.twoFAError = 'Please provide a valid phone number for SMS2FA.';
381
+ return;
382
+ }
383
+ }
384
+ }
385
+
386
+ // recaptcha check
387
+ // if (!this.form.get('recaptcha')?.value) {
388
+ // this.recaptchaError = 'Please verify that you are not a robot.';
389
+ // }
390
+
391
+ // if (this.form.invalid || this.recaptchaError) {
392
+ if (this.form.invalid) {
393
+ return;
394
+ }
395
+
396
+ this.loading = true; // Set loading to true when starting submission
397
+ try {
398
+ // Prepare the payload to send to the backend
399
+ const payload: any = {
400
+ name: this.control('name')?.value,
401
+ lastName: this.control('lastName')?.value,
402
+ email: this.control('email')?.value,
403
+ password: this.control('password')?.value,
404
+ role: this.control('role')?.value
405
+ };
406
+
407
+ // Include2FA settings when opted-in
408
+ if (this.form.get('twoFAOptIn')?.value) {
409
+ payload.twoFAEnabled = true;
410
+ payload.twoFAMethod = this.form.get('twoFAMethod')?.value;
411
+ if (payload.twoFAMethod === 'email') {
412
+ payload.twoFAContact = this.form.get('twoFAUseSameEmail')?.value ? payload.email : this.form.get('twoFAAltEmail')?.value;
413
+ } else if (payload.twoFAMethod === 'sms') {
414
+ payload.twoFAContact = this.form.get('twoFAPhone')?.value;
415
+ }
416
+ } else {
417
+ payload.twoFAEnabled = false;
418
+ }
419
+
420
+ // Make the HTTP request
421
+ this.signUpService.signUp(payload).subscribe(
422
+ (response) => {
423
+ this.errorMessage = '';
424
+ console.log("Sign-up request sent successfully!");
425
+ this.loading = false; // Reset loading on success
426
+ // Wait for loader to finish, then navigate
427
+ setTimeout(() => {
428
+ this.switchToSignIn.emit();
429
+ },500);
430
+ },
431
+ (error) => {
432
+ // If backend indicates duplicate email/user, set a specific error on the email control
433
+ const emailCtrl = this.form.get('email');
434
+ if (error && (error.status ===409 || error.status ===400)) {
435
+ this.errorMessage = 'Email already exists!';
436
+ // set reactive form error on email so template can render inline message
437
+ if (emailCtrl) {
438
+ const currentErrors = emailCtrl.errors || {};
439
+ emailCtrl.setErrors({ ...currentErrors, emailExists: true });
440
+ }
441
+ } else {
442
+ this.errorMessage = 'An error occurred. Please try again.';
443
+ }
444
+ this.loading = false; // Reset loading on error
445
+ this.cdr.markForCheck();
446
+ // keep the error visible briefly
447
+ setTimeout(() => {
448
+ this.errorMessage = '';
449
+ this.cdr.markForCheck();
450
+ },3000);
451
+ }
452
+ );
453
+ } catch (error) {
454
+ console.error("Error occurred during sign-up:", error); // Log any errors from the API call
455
+ this.loading = false; // Reset loading on exception
456
+ }
457
+ }
458
+
459
+ navigateHome() { this.router.navigateByUrl('/'); }
460
+
461
+ goToLogin() {
462
+ this.switchToSignIn.emit(); // ← Emit event instead of router navigation
463
+ }
464
+
465
+ closePopup() {
466
+ try {
467
+ // dispatch a global event so parent or other listeners always can close modals
468
+ window.dispatchEvent(new CustomEvent('auth-close'));
469
+ } catch (e) {
470
+ // ignore
471
+ }
472
+
473
+ this.close.emit();
474
+ // Defensive: remove modal/backdrop if parent didn't hide them
475
+ try {
476
+ const modal = document.querySelector('.modal');
477
+ if (modal && modal.parentElement) modal.parentElement.removeChild(modal);
478
+ const backdrop = document.querySelector('.modal-backdrop');
479
+ if (backdrop && backdrop.parentElement) backdrop.parentElement.removeChild(backdrop);
480
+ } catch (e) {
481
+ console.warn('Failed to remove modal/backdrop DOM elements', e);
482
+ }
483
+ // Ensure change detection updates
484
+ this.cdr.markForCheck();
485
+ }
486
+
487
+ tr(key: string): string {
488
+ const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
489
+ return map[key] || '';
490
+ }
491
+
492
+ goToSignIn() {
493
+ // Emit to parent when embedded so the card can slide back
494
+ this.switchToSignIn.emit();
495
+ }
496
+
497
+ goToSignUp() {
498
+ // no-op when embedded
499
+ }
500
+
501
+ onPasswordKey(event: KeyboardEvent) {
502
+ const caps = event.getModifierState && event.getModifierState('CapsLock');
503
+ this.capsLockOn = !!caps;
504
+ this.cdr.markForCheck();
505
+ }
506
+
507
+ // Called when user changes top-level role group
508
+ onRoleGroupChange(groupKey: string) {
509
+ const group = this.roleGroups.find(g => g.key === groupKey);
510
+ this.availableSubRoles = group ? group.subs : [];
511
+ // set the form's role to the selected group key so backend receives the grouping
512
+ this.form.get('role')?.setValue(groupKey);
513
+ this.cdr.markForCheck();
514
+ }
515
+
516
+ // Simple phone formatting: keep '+' then digits, group by spaces for readability
517
+ formatPhone(event: Event) {
518
+ const input = event.target as HTMLInputElement;
519
+ if (!input) return;
520
+ // strip non-digit except leading +
521
+ let v = input.value.trim();
522
+ const hasPlus = v.startsWith('+');
523
+ v = v.replace(/[^0-9]/g, '');
524
+ // group digits in blocks of3 for readability
525
+ const parts: string[] = [];
526
+ while (v.length) {
527
+ parts.push(v.substring(0,3));
528
+ v = v.substring(3);
529
+ }
530
+ input.value = (hasPlus ? '+' : '') + parts.join(' ');
531
+ // update reactive form control value without emitting extra events
532
+ const control = this.form.get('twoFAPhone');
533
+ if (control) {
534
+ control.setValue(input.value, { emitEvent: false });
535
+ }
536
+ }
537
+
538
+ // Return true if all mandatory fields have a non-empty value (used to enable the Create Account button)
539
+ isFormFilled(): boolean {
540
+ try {
541
+ const req = ['name', 'lastName', 'email', 'password', 'confirmPassword', 'roleGroup'];
542
+ let allFilled = true;
543
+ for (const k of req) {
544
+ const v = (this.form.get(k)?.value || '').toString().trim();
545
+ if (!v) { allFilled = false; break; }
546
+ }
547
+ if (!allFilled) {
548
+ const domIds = { name: 'firstName', lastName: 'lastName', email: 'email', password: 'password', confirmPassword: 'confirmPassword', roleGroup: 'roleGroup' };
549
+ let domAll = true;
550
+ for (const k of Object.keys(domIds)) {
551
+ const el = document.getElementById((domIds as any)[k]) as HTMLInputElement | HTMLSelectElement | null;
552
+ if (!el) { domAll = false; break; }
553
+ const val = (el as HTMLInputElement).value || '';
554
+ if (!val.toString().trim()) { domAll = false; break; }
555
+ }
556
+ if (domAll) {
557
+ try {
558
+ this.form.get('name')?.setValue((document.getElementById('firstName') as HTMLInputElement).value, { emitEvent: false });
559
+ this.form.get('lastName')?.setValue((document.getElementById('lastName') as HTMLInputElement).value, { emitEvent: false });
560
+ this.form.get('email')?.setValue((document.getElementById('email') as HTMLInputElement).value, { emitEvent: false });
561
+ this.form.get('password')?.setValue((document.getElementById('password') as HTMLInputElement).value, { emitEvent: false });
562
+ this.form.get('confirmPassword')?.setValue((document.getElementById('confirmPassword') as HTMLInputElement).value, { emitEvent: false });
563
+ const rg = (document.getElementById('roleGroup') as HTMLSelectElement).value;
564
+ if (rg) this.form.get('roleGroup')?.setValue(rg, { emitEvent: false });
565
+ } catch (e) { }
566
+ allFilled = true;
567
+ }
568
+ }
569
+ if (!allFilled) return false;
570
+ const termsChecked = !!this.form.get('terms')?.value || !!(document.getElementById('terms') as HTMLInputElement)?.checked;
571
+ if (!termsChecked) return false;
572
+ const twoFAOptIn = !!this.form.get('twoFAOptIn')?.value;
573
+ if (twoFAOptIn) {
574
+ const method = (this.form.get('twoFAMethod')?.value || '').toString().trim();
575
+ if (!method) return false;
576
+ if (method === 'sms') {
577
+ const phoneVal = (this.form.get('twoFAPhone')?.value || '').toString().trim() || (document.getElementById('twoFAPhone') as HTMLInputElement)?.value || '';
578
+ if (!phoneVal.toString().trim()) return false;
579
+ }
580
+ }
581
+ return true;
582
+ } catch (e) {
583
+ return false;
584
+ }
585
+ }
586
+
587
+ // Ensure we trigger change detection when controls change so template reevaluates the method
588
+ // (formSubscription added in ngOnInit keeps canEnable updated)
589
+
590
+ // Visual helpers used by the template
591
+ // Return true if the name contains any digits (show cross after submit)
592
+ public nameHasDigits(controlName: string): boolean {
593
+ if (!this.submitted) return false;
594
+ const v = (this.form.get(controlName)?.value || '').toString();
595
+ return /\d/.test(v);
596
+ }
597
+
598
+ // Return true when password mismatch should show the cross (after submit)
599
+ public passwordMismatchClass(): boolean {
600
+ return !!this.pwdMismatch;
601
+ }
602
+ }
603
+
604
+ // standalone validator follows
605
+ function passwordPolicyValidator(control: AbstractControl): ValidationErrors | null {
606
+ const value = control.value || '';
607
+ // Policy: min8 chars,1 uppercase,1 lowercase,1 number,1 special char
608
+ const policy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>\/?]).{8,}$/;
609
+ if (!policy.test(value)) {
610
+ return { passwordPolicy: true };
611
+ }
612
+ return null;
613
+ }
src/app/homepage/sign-up-1/sign-up/sign-up.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { SignUpService } from './sign-up.service';
4
+
5
+ describe('SignUpService', () => {
6
+ let service: SignUpService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(SignUpService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/homepage/sign-up-1/sign-up/sign-up.service.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+ import { environment } from 'src/environments/environment'; // adjust path if needed
5
+
6
+ @Injectable({
7
+ providedIn: 'root'
8
+ })
9
+ export class SignUpService {
10
+ private apiUrl = environment.pyDetectApiUrl;
11
+
12
+ constructor(private http: HttpClient) { }
13
+
14
+ signUp(payload: any): Observable<any> {
15
+ return this.http.post(`${this.apiUrl}/sign-up`, payload);
16
+ }
17
+ }
src/app/homepage/sign-up/sign-up.component.css CHANGED
@@ -979,33 +979,7 @@ input#twoFAOptIn {
979
  gap:8px;
980
  }
981
 
982
- /* Keep label-cross visible when rendered by *ngIf */
983
- .label-cross { display: inline-block; }
984
-
985
- /* Force label cross visible and prominent */
986
- .password-field label { padding-right:48px; }
987
- .password-field .label-cross {
988
- display: inline-block !important;
989
- position: absolute !important;
990
- right:8px !important;
991
- top:50% !important;
992
- transform: translateY(-50%) !important;
993
- color: #ff5252 !important;
994
- font-weight:900 !important;
995
- font-size:1.1rem !important;
996
- z-index:9999 !important;
997
- pointer-events: none !important;
998
- }
999
-
1000
- /* Ensure inside-cross sits above everything */
1001
- /* .input-with-eye .inside-cross { ... } */
1002
-
1003
- /* Bring eye toggle slightly below inside-cross */
1004
- .input-with-eye .eye-toggle { z-index:9000 !important; }
1005
-
1006
- /* Strong outline and color on mismatch */
1007
- .input-with-eye.password-mismatch input {
1008
- outline:2px solid rgba(255,82,82,0.18) !important;
1009
- box-shadow:0002px rgba(255,82,82,0.08) !important;
1010
- color: #b91c1c !important;
1011
- }
 
979
  gap:8px;
980
  }
981
 
982
+ /* Hide validation cross labels globally on sign-up without affecting form logic */
983
+ .label-cross { display: none !important; visibility: hidden !important; }
984
+ /* If any confirm-password cross indicator relies on a class, neutralize it */
985
+ .input-with-eye.confirm-cross::after { content: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/homepage/sign-up/sign-up.component.html CHANGED
@@ -27,14 +27,14 @@
27
  <form [formGroup]="form" (ngSubmit)="submit()" class="create-form" novalidate autocomplete="off">
28
  <div class="form-row">
29
  <div class="form-field">
30
- <label for="firstName">First Name <span class="required-star" *ngIf="true">*</span>
31
  <span class="label-cross" *ngIf="submitted && controlHasError('name')" aria-hidden="true">✖</span>
32
  </label>
33
  <input id="firstName" type="text" placeholder="First Name" formControlName="name" [attr.aria-invalid]="controlHasError('name')" [class.input-invalid]="submitted && (controlHasError('name') || nameHasDigits('name'))" />
34
  <!-- Name errors intentionally hidden until Create Account and per user request no name error texts -->
35
  </div>
36
  <div class="form-field">
37
- <label for="lastName">Last Name <span class="required-star" *ngIf="true">*</span>
38
  <span class="label-cross" *ngIf="submitted && controlHasError('lastName')" aria-hidden="true">✖</span>
39
  </label>
40
  <input id="lastName" type="text" placeholder="Last Name" formControlName="lastName" [attr.aria-invalid]="controlHasError('lastName')" [class.input-invalid]="submitted && (controlHasError('lastName') || nameHasDigits('lastName'))" />
@@ -43,7 +43,7 @@
43
  </div>
44
  <div class="form-row">
45
  <div class="form-field email-field" [class.email-invalid]="submitted && controlHasError('email')">
46
- <label for="email">Email <span class="required-star">*</span>
47
  </label>
48
  <input id="email" type="text" placeholder="email@gmail.com" formControlName="email" [attr.aria-invalid]="controlHasError('email')" [class.input-invalid]="submitted && controlHasError('email')" autocomplete="off" autocapitalize="off" spellcheck="false" />
49
  <small *ngIf="submitted && controlHasError('email','required')" class="error">Email is required.</small>
@@ -51,7 +51,7 @@
51
  <small *ngIf="submitted && controlHasError('email','emailExists')" class="error">Email already exists.</small>
52
  </div>
53
  <div class="form-field role-field-wrapper">
54
- <label for="roleGroup">Role Group <span class="required-star">*</span>
55
  <span class="label-cross" *ngIf="submitted && controlHasError('roleGroup')" aria-hidden="true">✖</span>
56
  <button type="button" class="info-btn" (click)="showInfo = true" aria-label="Role info">i</button>
57
  </label>
@@ -65,7 +65,7 @@
65
 
66
  <div class="form-row">
67
  <div class="form-field password-field">
68
- <label for="password">Create Password <span class="required-star">*</span>
69
  </label>
70
  <!-- wrap input and eye in a positioned wrapper so the eye is anchored to the input only -->
71
  <div class="input-with-eye">
@@ -83,7 +83,7 @@
83
  </div>
84
 
85
  <div class="form-field password-field">
86
- <label for="confirmPassword">Confirm Password <span class="required-star">*</span>
87
  </label>
88
  <!-- wrap confirm input and eye similarly -->
89
  <div class="input-with-eye" [class.password-mismatch]="submitted && pwdMismatch" [class.confirm-cross]="submitted && pwdMismatch">
 
27
  <form [formGroup]="form" (ngSubmit)="submit()" class="create-form" novalidate autocomplete="off">
28
  <div class="form-row">
29
  <div class="form-field">
30
+ <label for="firstName">First Name
31
  <span class="label-cross" *ngIf="submitted && controlHasError('name')" aria-hidden="true">✖</span>
32
  </label>
33
  <input id="firstName" type="text" placeholder="First Name" formControlName="name" [attr.aria-invalid]="controlHasError('name')" [class.input-invalid]="submitted && (controlHasError('name') || nameHasDigits('name'))" />
34
  <!-- Name errors intentionally hidden until Create Account and per user request no name error texts -->
35
  </div>
36
  <div class="form-field">
37
+ <label for="lastName">Last Name
38
  <span class="label-cross" *ngIf="submitted && controlHasError('lastName')" aria-hidden="true">✖</span>
39
  </label>
40
  <input id="lastName" type="text" placeholder="Last Name" formControlName="lastName" [attr.aria-invalid]="controlHasError('lastName')" [class.input-invalid]="submitted && (controlHasError('lastName') || nameHasDigits('lastName'))" />
 
43
  </div>
44
  <div class="form-row">
45
  <div class="form-field email-field" [class.email-invalid]="submitted && controlHasError('email')">
46
+ <label for="email">Email
47
  </label>
48
  <input id="email" type="text" placeholder="email@gmail.com" formControlName="email" [attr.aria-invalid]="controlHasError('email')" [class.input-invalid]="submitted && controlHasError('email')" autocomplete="off" autocapitalize="off" spellcheck="false" />
49
  <small *ngIf="submitted && controlHasError('email','required')" class="error">Email is required.</small>
 
51
  <small *ngIf="submitted && controlHasError('email','emailExists')" class="error">Email already exists.</small>
52
  </div>
53
  <div class="form-field role-field-wrapper">
54
+ <label for="roleGroup">Role Group
55
  <span class="label-cross" *ngIf="submitted && controlHasError('roleGroup')" aria-hidden="true">✖</span>
56
  <button type="button" class="info-btn" (click)="showInfo = true" aria-label="Role info">i</button>
57
  </label>
 
65
 
66
  <div class="form-row">
67
  <div class="form-field password-field">
68
+ <label for="password">Create Password
69
  </label>
70
  <!-- wrap input and eye in a positioned wrapper so the eye is anchored to the input only -->
71
  <div class="input-with-eye">
 
83
  </div>
84
 
85
  <div class="form-field password-field">
86
+ <label for="confirmPassword">Confirm Password
87
  </label>
88
  <!-- wrap confirm input and eye similarly -->
89
  <div class="input-with-eye" [class.password-mismatch]="submitted && pwdMismatch" [class.confirm-cross]="submitted && pwdMismatch">
src/app/homepage/sign-up/sign-up.component.ts CHANGED
@@ -23,6 +23,12 @@ export function nameValidator(control: AbstractControl): ValidationErrors | null
23
  return null;
24
  }
25
 
 
 
 
 
 
 
26
  @Component({
27
  selector: 'app-sign-up',
28
  standalone: true,
@@ -114,7 +120,8 @@ export class SignUpComponent implements OnInit, OnDestroy {
114
  ) {
115
  this.form = this.fb.group({
116
  name: ['', [Validators.required, Validators.minLength(2), nameValidator]],
117
- lastName: ['', [Validators.required, Validators.minLength(2), nameValidator]],
 
118
  email: ['', [
119
  Validators.required,
120
  Validators.pattern(/(^[^@]+@[^@]+\.[^@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)
 
23
  return null;
24
  }
25
 
26
+ // Relaxed validator for last name: allow any non-empty trimmed value
27
+ export function lastNameMinValidator(control: AbstractControl): ValidationErrors | null {
28
+ const value = (control.value || '').toString().trim();
29
+ return value.length >=1 ? null : { lastNameTooShort: true };
30
+ }
31
+
32
  @Component({
33
  selector: 'app-sign-up',
34
  standalone: true,
 
120
  ) {
121
  this.form = this.fb.group({
122
  name: ['', [Validators.required, Validators.minLength(2), nameValidator]],
123
+ // Allow single-character last name
124
+ lastName: ['', [Validators.required, lastNameMinValidator]],
125
  email: ['', [
126
  Validators.required,
127
  Validators.pattern(/(^[^@]+@[^@]+\.[^@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)