RajalashmiNagarajan commited on
Commit ·
e94898e
1
Parent(s): b1efa3a
homepage
Browse files- src.zip +2 -2
- src/app/homepage/homepage.component.css +96 -66
- src/app/homepage/homepage.component.html +19 -43
- src/app/homepage/sign-in/sign-in.component.css +2 -2
- src/app/homepage/sign-up-1.zip +3 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.component.css +1018 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.component.html +198 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.component.spec.ts +21 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.component.ts +613 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.service.spec.ts +16 -0
- src/app/homepage/sign-up-1/sign-up/sign-up.service.ts +17 -0
- src/app/homepage/sign-up/sign-up.component.css +4 -30
- src/app/homepage/sign-up/sign-up.component.html +6 -6
- src/app/homepage/sign-up/sign-up.component.ts +8 -1
src.zip
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 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 {
|
| 677 |
-
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
| 877 |
-
|
| 878 |
-
margin-
|
| 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:
|
|
|
|
| 893 |
}
|
| 894 |
|
| 895 |
.use-card {
|
| 896 |
background: #fff;
|
| 897 |
color: #000;
|
| 898 |
-
flex:
|
| 899 |
-
min-width:
|
| 900 |
border-radius: 12px;
|
| 901 |
-
padding:
|
| 902 |
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
| 903 |
transition: transform 0.3s;
|
| 904 |
}
|
| 905 |
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 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
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
}
|
| 942 |
|
| 943 |
-
.use-card
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
}
|
| 948 |
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 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, #
|
| 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:
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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:
|
| 1402 |
margin: 14px 6px 0;
|
| 1403 |
color: #3328c6;
|
| 1404 |
font-weight: 800;
|
| 1405 |
}
|
| 1406 |
-
.how-divider {
|
| 1407 |
-
|
| 1408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
</ul>
|
| 228 |
-
|
| 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 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
</ul>
|
| 241 |
-
|
| 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 |
-
|
| 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 |
-
|
| 261 |
-
|
| 262 |
-
|
| 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
|
| 270 |
<div class="how-divider"></div>
|
| 271 |
<ul>
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 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:
|
| 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-
|
| 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">×</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 & 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">×</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 |
-
/*
|
| 983 |
-
.label-cross { display:
|
| 984 |
-
|
| 985 |
-
|
| 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
|
| 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,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
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
| 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$)/)
|