|
|
<!DOCTYPE html> |
|
|
<html lang="en" data-bs-theme="dark"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>GAKR AI - Login/Signup</title> |
|
|
<link rel="stylesheet" href="https://cdn.replit.com/agent/bootstrap-agent-dark-theme.min.css"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> |
|
|
<style> |
|
|
:root { |
|
|
--gakr-blue: #4285F4; |
|
|
--gakr-blue-dark: #1a73e8; |
|
|
--gakr-blue-light: #e8f0fe; |
|
|
--gakr-grey-text: #5f6368; |
|
|
--gakr-grey-hover-bg: rgba(95, 99, 104, 0.1); |
|
|
} |
|
|
|
|
|
:root[data-bs-theme="dark"] { |
|
|
--gakr-blue: #8ab4f8; |
|
|
--gakr-blue-dark: #669df6; |
|
|
--gakr-blue-light: rgba(138, 180, 248, 0.1); |
|
|
--gakr-grey-text: #bdc1c6; |
|
|
--gakr-grey-hover-bg: rgba(189, 193, 198, 0.1); |
|
|
} |
|
|
|
|
|
@import url('https://fonts.googleapis.com/css?family=Poppins:400,500,600,700&display=swap'); |
|
|
|
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
font-family: 'Poppins', sans-serif; |
|
|
} |
|
|
|
|
|
html, body { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
height: 100%; |
|
|
width: 100%; |
|
|
background: -webkit-linear-gradient(left, #003366,#004080,#0059b3 , #0073e6); |
|
|
color: var(--bs-body-color); |
|
|
padding-top: calc(64px + 5vh); |
|
|
padding-bottom: 5vh; |
|
|
min-height: 100vh; |
|
|
|
|
|
position: relative; |
|
|
} |
|
|
|
|
|
::selection { |
|
|
background: var(--gakr-blue); |
|
|
color: #fff; |
|
|
} |
|
|
|
|
|
.wrapper { |
|
|
overflow: hidden; |
|
|
max-width: 440px; |
|
|
width: 90%; |
|
|
background: var(--bs-body-bg); |
|
|
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5); |
|
|
padding: 30px; |
|
|
border-radius: 15px; |
|
|
margin-top: 10px; |
|
|
margin-bottom: 10px; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.wrapper .title-text { |
|
|
display: flex; |
|
|
width: 200%; |
|
|
} |
|
|
|
|
|
.wrapper .title { |
|
|
width: 50%; |
|
|
font-size: 35px; |
|
|
font-weight: 600; |
|
|
text-align: center; |
|
|
transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
|
|
|
.wrapper .slide-controls { |
|
|
position: relative; |
|
|
display: flex; |
|
|
height: 50px; |
|
|
width: 100%; |
|
|
overflow: hidden; |
|
|
margin: 30px 0 10px 0; |
|
|
justify-content: space-between; |
|
|
border: 1px solid var(--bs-border-color); |
|
|
border-radius: 15px; |
|
|
} |
|
|
|
|
|
.slide-controls .slide { |
|
|
height: 100%; |
|
|
width: 100%; |
|
|
color: var(--bs-body-color); |
|
|
font-size: 18px; |
|
|
font-weight: 500; |
|
|
text-align: center; |
|
|
line-height: 48px; |
|
|
cursor: pointer; |
|
|
z-index: 1; |
|
|
transition: all 0.6s ease; |
|
|
} |
|
|
|
|
|
.slide-controls label.signup{ |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
|
|
|
.slide-controls .slider-tab { |
|
|
position: absolute; |
|
|
height: 100%; |
|
|
width: 50%; |
|
|
left: 0; |
|
|
z-index: 0; |
|
|
border-radius: 15px; |
|
|
background: var(--gakr-blue); |
|
|
transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); |
|
|
} |
|
|
|
|
|
input[type="radio"]{ |
|
|
display: none; |
|
|
} |
|
|
|
|
|
#signup:checked ~ .slider-tab{ |
|
|
left: 50%; |
|
|
} |
|
|
#signup:checked ~ label.signup{ |
|
|
color: #fff; |
|
|
cursor: default; |
|
|
user-select: none; |
|
|
} |
|
|
#signup:checked ~ label.login{ |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
#login:checked ~ label.signup{ |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
#login:checked ~ label.login{ |
|
|
color: #fff; |
|
|
cursor: default; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
.wrapper .form-container { |
|
|
width: 100%; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.form-container .form-inner { |
|
|
display: flex; |
|
|
width: 200%; |
|
|
} |
|
|
|
|
|
.form-container .form-inner form { |
|
|
width: 50%; |
|
|
transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); |
|
|
} |
|
|
|
|
|
.form-inner form .field { |
|
|
height: 50px; |
|
|
width: 100%; |
|
|
margin-top: 20px; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.form-inner form .field:has(+ .field.btn) { |
|
|
margin-bottom: 25px; |
|
|
} |
|
|
|
|
|
.form-inner form .field input{ |
|
|
height: 100%; |
|
|
width: 100%; |
|
|
outline: none; |
|
|
padding-left: 15px; |
|
|
border-radius: 15px; |
|
|
border: 1px solid var(--bs-border-color); |
|
|
border-bottom-width: 2px; |
|
|
font-size: 17px; |
|
|
transition: all 0.3s ease; |
|
|
color: var(--bs-body-color); |
|
|
background-color: var(--bs-body-bg); |
|
|
} |
|
|
|
|
|
.form-inner form .field input.error { |
|
|
border-color: red; |
|
|
} |
|
|
|
|
|
.form-inner form .field input::placeholder{ |
|
|
color: var(--bs-secondary-color); |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
form .field input:focus::placeholder{ |
|
|
color: var(--gakr-blue); |
|
|
} |
|
|
|
|
|
.form-inner form .pass-link{ |
|
|
margin-top: 5px; |
|
|
font-size: 0.9rem; |
|
|
text-align: right; |
|
|
} |
|
|
|
|
|
.form-inner form .signup-link{ |
|
|
text-align: center; |
|
|
margin-top: 30px; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.form-inner form .pass-link a, |
|
|
.form-inner form .signup-link a{ |
|
|
color: var(--gakr-blue); |
|
|
text-decoration: none; |
|
|
transition: color 0.2s ease-in-out; |
|
|
} |
|
|
|
|
|
.form-inner form .pass-link a:hover, |
|
|
form-inner form .signup-link a:hover{ |
|
|
text-decoration: underline; |
|
|
color: var(--gakr-blue-dark); |
|
|
} |
|
|
|
|
|
form .btn { |
|
|
height: 50px; |
|
|
width: 100%; |
|
|
margin-top: 20px; |
|
|
border-radius: 15px; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
background: none; |
|
|
transition: none; |
|
|
} |
|
|
|
|
|
form .btn input[type="submit"] { |
|
|
height: 100%; |
|
|
width: 100%; |
|
|
z-index: 1; |
|
|
position: relative; |
|
|
background: var(--gakr-blue); |
|
|
border: none; |
|
|
color: #fff; |
|
|
padding: 0; |
|
|
border-radius: 15px; |
|
|
font-size: 20px; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
form .btn input[type="submit"]:hover { |
|
|
background-color: var(--gakr-blue-dark); |
|
|
} |
|
|
|
|
|
form .btn input[type="submit"]:active { |
|
|
transform: scale(0.98); |
|
|
transition: background-color 0s, transform 0.1s; |
|
|
} |
|
|
|
|
|
.error-message { |
|
|
color: red; |
|
|
font-size: 0.8rem; |
|
|
margin-top: 4px; |
|
|
position: absolute; |
|
|
bottom: -18px; |
|
|
left: 15px; |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.form-message { |
|
|
text-align: center; |
|
|
margin-top: 20px; |
|
|
font-size: 0.9rem; |
|
|
min-height: 1.2em; |
|
|
} |
|
|
|
|
|
.form-message.success { |
|
|
color: green; |
|
|
} |
|
|
|
|
|
.form-message.error { |
|
|
color: red; |
|
|
} |
|
|
|
|
|
.loading-spinner { |
|
|
display: inline-block; |
|
|
width: 18px; |
|
|
height: 18px; |
|
|
border: 3px solid rgba(255, 255, 255, .3); |
|
|
border-radius: 50%; |
|
|
border-top-color: #fff; |
|
|
animation: spin 1s ease-in-out infinite; |
|
|
margin-left: 10px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
@keyframes spin { |
|
|
to { -webkit-transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
.form-inner form .btn input[type="submit"]:disabled { |
|
|
opacity: 0.7; |
|
|
cursor: not-allowed; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
|
|
|
.modal-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0, 0, 0, 0.6); |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
z-index: 100; |
|
|
visibility: hidden; |
|
|
opacity: 0; |
|
|
transition: visibility 0s, opacity 0.3s ease-in-out; |
|
|
} |
|
|
|
|
|
.modal-overlay.show { |
|
|
visibility: visible; |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
background-color: var(--bs-body-bg); |
|
|
padding: 30px; |
|
|
border-radius: 15px; |
|
|
box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.7); |
|
|
text-align: center; |
|
|
max-width: 350px; |
|
|
width: 80%; |
|
|
transform: translateY(-20px); |
|
|
transition: transform 0.3s ease-in-out; |
|
|
} |
|
|
|
|
|
.modal-overlay.show .modal-content { |
|
|
transform: translateY(0); |
|
|
} |
|
|
|
|
|
.modal-message { |
|
|
font-size: 1.1rem; |
|
|
margin-bottom: 25px; |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
.modal-message p { |
|
|
margin-bottom: 10px; |
|
|
line-height: 1.4; |
|
|
} |
|
|
.modal-message strong { |
|
|
font-size: 1.2rem; |
|
|
display: block; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.modal-message ul { |
|
|
list-style: none; |
|
|
padding: 0; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.modal-message ul li { |
|
|
margin-bottom: 8px; |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
|
|
|
.modal-button { |
|
|
background-color: var(--gakr-blue); |
|
|
color: #fff; |
|
|
border: none; |
|
|
padding: 10px 25px; |
|
|
border-radius: 10px; |
|
|
font-size: 1rem; |
|
|
cursor: pointer; |
|
|
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out; |
|
|
} |
|
|
|
|
|
.modal-button:hover { |
|
|
background-color: var(--gakr-blue-dark); |
|
|
} |
|
|
|
|
|
.modal-button:active { |
|
|
transform: scale(0.98); |
|
|
} |
|
|
|
|
|
.modal-buttons-stacked { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.modal-buttons-stacked .modal-button { |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.modal-buttons-inline { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
gap: 15px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
.modal-buttons-inline .modal-button { |
|
|
flex-grow: 1; |
|
|
max-width: none; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.field .toggle-password { |
|
|
position: absolute; |
|
|
right: 15px; |
|
|
top: 50%; |
|
|
transform: translateY(-50%); |
|
|
cursor: pointer; |
|
|
color: var(--bs-secondary-color); |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.field .toggle-password:hover { |
|
|
color: var(--gakr-blue); |
|
|
} |
|
|
|
|
|
|
|
|
.modal-form { |
|
|
text-align: left; |
|
|
margin-top: 20px; |
|
|
} |
|
|
.modal-form .field { |
|
|
margin-bottom: 15px; |
|
|
height: auto; |
|
|
} |
|
|
.modal-form label { |
|
|
display: block; |
|
|
margin-bottom: 5px; |
|
|
font-size: 0.9rem; |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
.otp-input-container { |
|
|
display: flex; |
|
|
gap: 5px; |
|
|
justify-content: center; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.otp-digit { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
text-align: center; |
|
|
border: 1px solid var(--bs-border-color); |
|
|
border-radius: 5px; |
|
|
font-size: 1.2rem; |
|
|
background-color: var(--bs-body-bg); |
|
|
color: var(--bs-body-color); |
|
|
} |
|
|
.otp-digit:focus { |
|
|
border-color: var(--gakr-blue); |
|
|
outline: none; |
|
|
} |
|
|
.modal-form .error-message { |
|
|
position: static; |
|
|
margin-top: 5px; |
|
|
margin-left: 0; |
|
|
white-space: normal; |
|
|
} |
|
|
.modal-buttons { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 15px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
.modal-buttons button { |
|
|
width: 100%; |
|
|
max-width: 150px; |
|
|
} |
|
|
|
|
|
|
|
|
.gakr-chat-header { |
|
|
padding: 0.75rem 1.5rem; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
border-bottom: 1px solid var(--bs-border-color); |
|
|
height: 64px; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
width: 100%; |
|
|
background-color: var(--bs-body-bg); |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.gakr-logo-area { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.75rem; |
|
|
} |
|
|
|
|
|
.gakr-brand-logo { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.gakr-brand-text { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 500; |
|
|
color: var(--gakr-blue); |
|
|
} |
|
|
|
|
|
.gakr-nav-controls { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.gakr-login-button { |
|
|
color: var(--gakr-blue); |
|
|
text-decoration: none; |
|
|
background: transparent; |
|
|
border: 1px solid var(--gakr-blue); |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 50px; |
|
|
font-size: 0.9rem; |
|
|
transition: background-color 0.2s; |
|
|
} |
|
|
|
|
|
.gakr-login-button:hover { |
|
|
background-color: rgba(66, 133, 244, 0.1); |
|
|
} |
|
|
|
|
|
.gakr-profile-button { |
|
|
color: var(--gakr-blue); |
|
|
text-decoration: none; |
|
|
background: transparent; |
|
|
border: none; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 50px; |
|
|
font-size: 0.9rem; |
|
|
transition: background-color 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.gakr-profile-button:hover { |
|
|
background-color: rgba(66, 133, 244, 0.1); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<header class="gakr-chat-header"> |
|
|
<div class="gakr-logo-area"> |
|
|
<a href="/" class="gakr-brand-logo"> |
|
|
<i class="fas fa-robot" style="color: var(--gakr-blue);"></i> |
|
|
</a> |
|
|
<a href="/" class="gakr-brand-text" style="text-decoration: none;">GAKR AI</a> |
|
|
</div> |
|
|
|
|
|
<div class="gakr-nav-controls" id="authButtonContainer"> |
|
|
<a href="/auth" class="gakr-login-button">Sign in / Register</a> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<div class="wrapper"> |
|
|
<div class="title-text"> |
|
|
<div class="title login">Login Form</div> |
|
|
<div class="title signup">Signup Form</div> |
|
|
</div> |
|
|
<div class="form-container"> |
|
|
<div class="slide-controls"> |
|
|
<input type="radio" name="slide" id="login" checked> |
|
|
<input type="radio" name="slide" id="signup"> |
|
|
<label for="login" class="slide login">Login</label> |
|
|
<label for="signup" class="slide signup">Signup</label> |
|
|
<div class="slider-tab"></div> |
|
|
</div> |
|
|
<div class="form-inner"> |
|
|
<form action="#" class="login" id="loginForm"> |
|
|
<div class="field"> |
|
|
<input type="text" placeholder="Username or Email" required id="loginUsernameOrEmail" name="username_or_email"> |
|
|
<div class="error-message" id="loginUsernameOrEmailError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<input type="password" placeholder="Password" required id="loginPassword" name="password"> |
|
|
<span class="toggle-password fa-solid fa-eye-slash"></span> |
|
|
<div class="error-message" id="loginPasswordError"></div> |
|
|
</div> |
|
|
<div class="pass-link"><a href="#" id="forgotPasswordLink">Forgot password?</a></div> |
|
|
<div class="field btn"> |
|
|
<input type="submit" value="Login"> |
|
|
</div> |
|
|
<div class="signup-link">Not a member? <a href="">Signup now</a></div> |
|
|
</form> |
|
|
<form action="#" class="signup" id="signupForm"> |
|
|
<div class="field"> |
|
|
<input type="text" placeholder="Name" required id="signupName" name="name"> |
|
|
<div class="error-message" id="signupNameError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<input type="text" placeholder="Email Address" required id="signupEmail" name="email"> |
|
|
<div class="error-message" id="signupEmailError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<input type="password" placeholder="Password" required id="signupPassword" name="password"> |
|
|
<span class="toggle-password fa-solid fa-eye-slash"></span> |
|
|
<div class="error-message" id="signupPasswordError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<input type="password" placeholder="Confirm password" required id="signupConfirmPassword" name="confirm_password"> |
|
|
<span class="toggle-password fa-solid fa-eye-slash"></span> |
|
|
<div class="error-message" id="signupConfirmPasswordError"></div> |
|
|
</div> |
|
|
<div class="field btn"> |
|
|
<input type="submit" value="Signup"> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
<div class="form-message" id="generalFormMessage"></div> |
|
|
</div> |
|
|
|
|
|
<div class="modal-overlay" id="customModal"> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-message" id="modalMessage"></div> |
|
|
<div id="modalButtonsContainer" class="modal-buttons"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal-overlay" id="forgotPasswordRequestModal"> |
|
|
<div class="modal-content" style="max-width: 450px;"> |
|
|
<h3 style="text-align: center; margin-bottom: 20px; color: var(--gakr-blue);">Reset Password</h3> |
|
|
<p class="modal-message" id="forgotModalMessage" style="text-align: center; margin-bottom: 25px;">Enter your registered email address to receive a password reset code.</p> |
|
|
|
|
|
<form id="forgotPasswordRequestForm" class="modal-form"> |
|
|
<div class="field" style="margin-bottom: 30px;"> |
|
|
<label for="forgotEmail" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--bs-body-color);">Email Address:</label> |
|
|
<input type="email" id="forgotEmail" name="email" required style="width: 100%; padding: 12px; border: 2px solid var(--bs-border-color); border-radius: 8px; font-size: 16px; background-color: var(--bs-body-bg); color: var(--bs-body-color);" placeholder="Enter your email"> |
|
|
<div class="error-message" id="forgotEmailError" style="margin-top: 8px;"></div> |
|
|
</div> |
|
|
<div class="modal-buttons-stacked"> |
|
|
<button type="submit" class="modal-button" id="forgotSubmitButton" style="width: 100%; padding: 12px; font-size: 16px;">Send Reset Code</button> |
|
|
<button type="button" class="modal-button" id="forgotCancelButton" style="width: 100%; padding: 12px; font-size: 16px; background-color: var(--bs-secondary-color); margin-top: 10px;">Cancel</button> |
|
|
</div> |
|
|
</form> |
|
|
|
|
|
<div id="otpSection" style="display: none;"> |
|
|
<h3 style="text-align: center; margin-bottom: 20px; color: var(--gakr-blue);">Enter Verification Code</h3> |
|
|
<p style="text-align: center; margin-bottom: 25px; color: var(--bs-secondary-color);">We've sent a 8-digit code to your email. Enter it below to continue.</p> |
|
|
<form id="verifyOtpForm" class="modal-form"> |
|
|
<div class="field" style="margin-bottom: 30px;"> |
|
|
<label style="display: block; margin-bottom: 15px; font-weight: 500; color: var(--bs-body-color); text-align: center;">Verification Code:</label> |
|
|
<div class="otp-input-container" style="display: flex; justify-content: center; gap: 8px; margin-bottom: 10px;"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
<input type="text" class="otp-digit" maxlength="1" pattern="\d" required style="width: 45px; height: 50px; text-align: center; font-size: 20px; font-weight: bold; border: 2px solid var(--bs-border-color); border-radius: 8px; background-color: var(--bs-body-bg); color: var(--bs-body-color);"> |
|
|
</div> |
|
|
<input type="hidden" id="otpInput" name="otp"> |
|
|
<div class="error-message" id="otpError" style="text-align: center; margin-top: 10px;"></div> |
|
|
</div> |
|
|
<div class="modal-buttons-stacked"> |
|
|
<button type="submit" class="modal-button" id="verifyOtpButton" style="width: 100%; padding: 12px; font-size: 16px;">Verify Code</button> |
|
|
<button type="button" class="modal-button" id="resendOtpButton" style="width: 100%; padding: 12px; font-size: 16px; margin-top: 10px;">Send Code Again</button> |
|
|
<button type="button" class="modal-button" id="otpCancelButton" style="width: 100%; padding: 12px; font-size: 16px; background-color: var(--bs-secondary-color); margin-top: 10px;">Cancel</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal-overlay" id="resetPasswordModal"> |
|
|
<div class="modal-content"> |
|
|
<p class="modal-message">Enter your new password.</p> |
|
|
<form id="resetPasswordForm" class="modal-form"> |
|
|
<div class="field"> |
|
|
<label for="resetEmail">Email:</label> |
|
|
<input type="email" id="resetEmail" name="email" required readonly style="background-color: var(--bs-secondary-bg); cursor: not-allowed;"> |
|
|
<div class="error-message" id="resetEmailError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<label for="newPassword">New Password:</label> |
|
|
<input type="password" id="newPassword" name="new_password" required> |
|
|
<span class="toggle-password fa-solid fa-eye-slash"></span> |
|
|
<div class="error-message" id="newPasswordError"></div> |
|
|
</div> |
|
|
<div class="field"> |
|
|
<label for="confirmNewPassword">Confirm New Password:</label> |
|
|
<input type="password" id="confirmNewPassword" name="confirm_new_password" required> |
|
|
<span class="toggle-password fa-solid fa-eye-slash"></span> |
|
|
<div class="error-message" id="confirmNewPasswordError"></div> |
|
|
</div> |
|
|
<div class="modal-buttons-stacked"> |
|
|
<button type="submit" class="modal-button" id="resetSubmitButton">Reset Password</button> |
|
|
<button type="button" class="modal-button" id="resetCancelButton" style="background-color: var(--bs-secondary-color);">Cancel</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
function updateAuthButton() { |
|
|
const authButtonContainer = document.getElementById('authButtonContainer'); |
|
|
const userIsLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; |
|
|
|
|
|
if (userIsLoggedIn) { |
|
|
authButtonContainer.innerHTML = ` |
|
|
<a href="/profile" class="gakr-profile-button"> |
|
|
<i class="fas fa-user-circle me-2"></i>Profile |
|
|
</a> |
|
|
`; |
|
|
const profileButton = authButtonContainer.querySelector('.gakr-profile-button'); |
|
|
if (profileButton) { |
|
|
profileButton.addEventListener('click', function(event) { |
|
|
console.log("Profile button clicked!"); |
|
|
}); |
|
|
} |
|
|
} else { |
|
|
authButtonContainer.innerHTML = ` |
|
|
<a href="/auth" class="gakr-login-button">Sign in / Register</a> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
updateAuthButton(); |
|
|
window.addEventListener('focus', updateAuthButton); |
|
|
window.addEventListener('storage', updateAuthButton); |
|
|
|
|
|
const loginText = document.querySelector(".title-text .login"); |
|
|
const loginFormDiv = document.querySelector("form.login"); |
|
|
const signupLink = document.querySelector("form .signup-link a"); |
|
|
const loginRadio = document.getElementById('login'); |
|
|
const signupRadio = document.getElementById('signup'); |
|
|
|
|
|
const loginForm = document.getElementById('loginForm'); |
|
|
const signupForm = document.getElementById('signupForm'); |
|
|
|
|
|
|
|
|
const loginUsernameOrEmailInput = document.getElementById('loginUsernameOrEmail'); |
|
|
const loginPasswordInput = document.getElementById('loginPassword'); |
|
|
const loginSubmitButton = loginForm ? loginForm.querySelector('input[type="submit"]') : null; |
|
|
|
|
|
const signupNameInput = document.getElementById('signupName'); |
|
|
const signupEmailInput = document.getElementById('signupEmail'); |
|
|
const signupPasswordInput = document.getElementById('signupPassword'); |
|
|
const signupConfirmPasswordInput = document.getElementById('signupConfirmPassword'); |
|
|
const signupSubmitButton = signupForm ? signupForm.querySelector('input[type="submit"]') : null; |
|
|
|
|
|
const loginUsernameOrEmailError = document.getElementById('loginUsernameOrEmailError'); |
|
|
const loginPasswordError = document.getElementById('loginPasswordError'); |
|
|
|
|
|
const signupNameError = document.getElementById('signupNameError'); |
|
|
const signupEmailError = document.getElementById('signupEmailError'); |
|
|
const signupPasswordError = document.getElementById('signupPasswordError'); |
|
|
const signupConfirmPasswordError = document.getElementById('signupConfirmPasswordError'); |
|
|
|
|
|
const generalFormMessage = document.getElementById('generalFormMessage'); |
|
|
|
|
|
|
|
|
const customModal = document.getElementById('customModal'); |
|
|
const modalMessageDiv = document.getElementById('modalMessage'); |
|
|
const modalButtonsContainer = document.getElementById('modalButtonsContainer'); |
|
|
|
|
|
|
|
|
const otpDigits = document.querySelectorAll('.otp-digit'); |
|
|
otpDigits.forEach((input, index) => { |
|
|
input.addEventListener('input', function() { |
|
|
if (this.value.length === 1 && index < otpDigits.length - 1) { |
|
|
otpDigits[index + 1].focus(); |
|
|
} |
|
|
}); |
|
|
input.addEventListener('keydown', function(e) { |
|
|
if (e.key === 'Backspace' && this.value === '' && index > 0) { |
|
|
otpDigits[index - 1].focus(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
const forgotPasswordLink = document.getElementById('forgotPasswordLink'); |
|
|
const forgotPasswordRequestModal = document.getElementById('forgotPasswordRequestModal'); |
|
|
const forgotModalMessage = document.getElementById('forgotModalMessage'); |
|
|
|
|
|
|
|
|
const forgotPasswordRequestForm = document.getElementById('forgotPasswordRequestForm'); |
|
|
const forgotEmailInput = document.getElementById('forgotEmail'); |
|
|
const forgotEmailError = document.getElementById('forgotEmailError'); |
|
|
const forgotSubmitButton = document.getElementById('forgotSubmitButton'); |
|
|
const forgotCancelButton = document.getElementById('forgotCancelButton'); |
|
|
|
|
|
|
|
|
const otpSection = document.getElementById('otpSection'); |
|
|
const verifyOtpForm = document.getElementById('verifyOtpForm'); |
|
|
const otpInput = document.getElementById('otpInput'); |
|
|
const otpError = document.getElementById('otpError'); |
|
|
const verifyOtpButton = document.getElementById('verifyOtpButton'); |
|
|
const resendOtpButton = document.getElementById('resendOtpButton'); |
|
|
const otpCancelButton = document.getElementById('otpCancelButton'); |
|
|
|
|
|
|
|
|
const resetPasswordModal = document.getElementById('resetPasswordModal'); |
|
|
const resetPasswordForm = document.getElementById('resetPasswordForm'); |
|
|
const resetEmailInput = document.getElementById('resetEmail'); |
|
|
const newPasswordInput = document.getElementById('newPassword'); |
|
|
const confirmNewPasswordInput = document.getElementById('confirmNewPassword'); |
|
|
const resetEmailError = document.getElementById('resetEmailError'); |
|
|
const newPasswordError = document.getElementById('newPasswordError'); |
|
|
const confirmNewPasswordError = document.getElementById('confirmNewPasswordError'); |
|
|
const resetSubmitButton = document.getElementById('resetSubmitButton'); |
|
|
const resetCancelButton = document.getElementById('resetCancelButton'); |
|
|
|
|
|
|
|
|
let loginAttempts = 0; |
|
|
const MAX_LOGIN_ATTEMPTS = 3; |
|
|
|
|
|
|
|
|
let currentForgotEmail = ''; |
|
|
|
|
|
|
|
|
let resendCooldown = 0; |
|
|
let resendInterval = null; |
|
|
|
|
|
|
|
|
function startResendCooldown() { |
|
|
resendCooldown = 60; |
|
|
resendOtpButton.disabled = true; |
|
|
resendOtpButton.textContent = `Send Again (60s)`; |
|
|
resendOtpButton.style.backgroundColor = 'var(--bs-secondary-color)'; |
|
|
|
|
|
resendInterval = setInterval(() => { |
|
|
resendCooldown--; |
|
|
if (resendCooldown > 0) { |
|
|
resendOtpButton.textContent = `Send Again (${resendCooldown}s)`; |
|
|
} else { |
|
|
clearInterval(resendInterval); |
|
|
resendInterval = null; |
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.textContent = 'Send Code Again'; |
|
|
resendOtpButton.style.backgroundColor = ''; |
|
|
} |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
|
|
|
function clearErrors(formType = 'all') { |
|
|
if (formType === 'all' || formType === 'main') { |
|
|
document.querySelectorAll('.error-message').forEach(el => el.textContent = ''); |
|
|
document.querySelectorAll('.form-inner form .field input').forEach(el => el.classList.remove('error')); |
|
|
if (generalFormMessage) { |
|
|
generalFormMessage.textContent = ''; |
|
|
generalFormMessage.className = 'form-message'; |
|
|
} |
|
|
} |
|
|
if (formType === 'all' || formType === 'forgot') { |
|
|
if (forgotEmailError) forgotEmailError.textContent = ''; |
|
|
if (forgotEmailInput) forgotEmailInput.classList.remove('error'); |
|
|
|
|
|
if (forgotModalMessage) forgotModalMessage.innerHTML = 'Enter your registered email to request a password reset.'; |
|
|
} |
|
|
if (formType === 'all' || formType === 'otp') { |
|
|
if (otpError) otpError.textContent = ''; |
|
|
if (otpInput) otpInput.classList.remove('error'); |
|
|
} |
|
|
if (formType === 'all' || formType === 'reset') { |
|
|
if (resetEmailError) resetEmailError.textContent = ''; |
|
|
if (newPasswordError) newPasswordError.textContent = ''; |
|
|
if (confirmNewPasswordError) confirmNewPasswordError.textContent = ''; |
|
|
if (resetEmailInput) resetEmailInput.classList.remove('error'); |
|
|
if (newPasswordInput) newPasswordInput.classList.remove('error'); |
|
|
if (confirmNewPasswordInput) confirmNewPasswordInput.classList.remove('error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function displayFormMessage(message, type = 'info') { |
|
|
if (generalFormMessage) { |
|
|
generalFormMessage.textContent = message; |
|
|
generalFormMessage.className = 'form-message ' + type; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showCustomModal(modalElement) { |
|
|
modalElement.classList.add('show'); |
|
|
} |
|
|
|
|
|
|
|
|
function hideCustomModal(modalElement) { |
|
|
modalElement.classList.remove('show'); |
|
|
clearErrors(); |
|
|
} |
|
|
|
|
|
|
|
|
function showFlexibleModal(messageHTML, buttonsConfig, callback = null) { |
|
|
modalMessageDiv.innerHTML = messageHTML; |
|
|
modalButtonsContainer.innerHTML = ''; |
|
|
|
|
|
if (buttonsConfig && buttonsConfig.length > 0) { |
|
|
const isInline = buttonsConfig.every(btn => !btn.newline); |
|
|
modalButtonsContainer.className = isInline ? 'modal-buttons-inline' : 'modal-buttons-stacked'; |
|
|
|
|
|
buttonsConfig.forEach(btnConf => { |
|
|
const button = document.createElement('button'); |
|
|
button.className = 'modal-button'; |
|
|
button.textContent = btnConf.text; |
|
|
if (btnConf.style) { |
|
|
button.style = btnConf.style; |
|
|
} |
|
|
button.onclick = function() { |
|
|
hideCustomModal(customModal); |
|
|
if (btnConf.action && typeof btnConf.action === 'function') { |
|
|
btnConf.action(); |
|
|
} |
|
|
if (callback && typeof callback === 'function') { |
|
|
callback(); |
|
|
} |
|
|
}; |
|
|
modalButtonsContainer.appendChild(button); |
|
|
}); |
|
|
} else { |
|
|
const okButton = document.createElement('button'); |
|
|
okButton.className = 'modal-button'; |
|
|
okButton.textContent = 'OK'; |
|
|
okButton.onclick = function() { |
|
|
hideCustomModal(customModal); |
|
|
if (callback && typeof callback === 'function') { |
|
|
callback(); |
|
|
} |
|
|
}; |
|
|
modalButtonsContainer.classList.remove('modal-buttons-inline', 'modal-buttons-stacked'); |
|
|
modalButtonsContainer.classList.add('modal-buttons'); |
|
|
modalButtonsContainer.appendChild(okButton); |
|
|
} |
|
|
showCustomModal(customModal); |
|
|
} |
|
|
|
|
|
|
|
|
function slideToSignup() { |
|
|
if (loginFormDiv && loginText) { |
|
|
loginFormDiv.style.marginLeft = "-50%"; |
|
|
loginText.style.marginLeft = "-50%"; |
|
|
clearErrors('main'); |
|
|
} |
|
|
} |
|
|
|
|
|
function slideToLogin() { |
|
|
if (loginFormDiv && loginText) { |
|
|
loginFormDiv.style.marginLeft = "0%"; |
|
|
loginText.style.marginLeft = "0%"; |
|
|
clearErrors('main'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (signupRadio) { |
|
|
signupRadio.addEventListener('change', function() { |
|
|
if (this.checked) { |
|
|
slideToSignup(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (loginRadio) { |
|
|
loginRadio.addEventListener('change', function() { |
|
|
if (this.checked) { |
|
|
slideToLogin(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (signupLink && signupRadio) { |
|
|
signupLink.onclick = ((e) => { |
|
|
e.preventDefault(); |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
const showSignup = urlParams.get('signup'); |
|
|
|
|
|
if (showSignup === 'true') { |
|
|
if (signupRadio) { |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
} |
|
|
} else { |
|
|
if (loginRadio) { |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function setupPasswordToggle(passwordInput, toggleIcon) { |
|
|
if (passwordInput && toggleIcon) { |
|
|
toggleIcon.addEventListener('click', function() { |
|
|
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; |
|
|
passwordInput.setAttribute('type', type); |
|
|
this.classList.toggle('fa-eye'); |
|
|
this.classList.toggle('fa-eye-slash'); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
setupPasswordToggle(loginPasswordInput, document.querySelector('#loginForm .toggle-password')); |
|
|
setupPasswordToggle(signupPasswordInput, document.querySelector('#signupForm #signupPassword + .toggle-password')); |
|
|
setupPasswordToggle(signupConfirmPasswordInput, document.querySelector('#signupForm #signupConfirmPassword + .toggle-password')); |
|
|
setupPasswordToggle(newPasswordInput, document.querySelector('#resetPasswordForm #newPassword + .toggle-password')); |
|
|
setupPasswordToggle(confirmNewPasswordInput, document.querySelector('#resetPasswordForm #confirmNewPassword + .toggle-password')); |
|
|
|
|
|
|
|
|
|
|
|
if (loginForm) { |
|
|
loginForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('main'); |
|
|
|
|
|
|
|
|
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) { |
|
|
showLoginAttemptsLimitModal(); |
|
|
return; |
|
|
} |
|
|
|
|
|
let isValid = true; |
|
|
|
|
|
const usernameOrEmail = loginUsernameOrEmailInput.value.trim(); |
|
|
const password = loginPasswordInput.value.trim(); |
|
|
|
|
|
if (usernameOrEmail === '') { |
|
|
isValid = false; |
|
|
loginUsernameOrEmailError.textContent = 'Username or Email is required'; |
|
|
loginUsernameOrEmailInput.classList.add('error'); |
|
|
} |
|
|
if (password === '') { |
|
|
isValid = false; |
|
|
loginPasswordError.textContent = 'Password is required'; |
|
|
loginPasswordInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (isValid) { |
|
|
|
|
|
try { |
|
|
const checkResponse = await fetch('/api/check_user', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ identifier: usernameOrEmail }) |
|
|
}); |
|
|
const checkData = await checkResponse.json(); |
|
|
if (!checkData.exists) { |
|
|
loginUsernameOrEmailError.textContent = 'User does not exist.'; |
|
|
loginUsernameOrEmailInput.classList.add('error'); |
|
|
return; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error checking user:', error); |
|
|
|
|
|
} |
|
|
submitFormData(event.target, loginSubmitButton, '/api/login', 'main'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (signupForm) { |
|
|
signupForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('main'); |
|
|
|
|
|
let isValid = true; |
|
|
|
|
|
const name = signupNameInput.value.trim(); |
|
|
const email = signupEmailInput.value.trim(); |
|
|
const password = signupPasswordInput.value.trim(); |
|
|
const confirmPassword = signupConfirmPasswordInput.value.trim(); |
|
|
|
|
|
if (name === '') { |
|
|
isValid = false; |
|
|
signupNameError.textContent = 'Name is required'; |
|
|
signupNameInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (email === '') { |
|
|
isValid = false; |
|
|
signupEmailError.textContent = 'Email is required'; |
|
|
signupEmailInput.classList.add('error'); |
|
|
} else if (!isValidEmail(email)) { |
|
|
isValid = false; |
|
|
signupEmailError.textContent = 'Enter a valid email address'; |
|
|
signupEmailInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (password === '') { |
|
|
isValid = false; |
|
|
signupPasswordError.textContent = 'Password is required'; |
|
|
signupPasswordInput.classList.add('error'); |
|
|
} else if (password.length < 6) { |
|
|
isValid = false; |
|
|
signupPasswordError.textContent = 'Password must be at least 6 characters'; |
|
|
signupPasswordInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (confirmPassword === '') { |
|
|
isValid = false; |
|
|
signupConfirmPasswordError.textContent = 'Confirm password is required'; |
|
|
signupConfirmPasswordInput.classList.add('error'); |
|
|
} else if (password !== confirmPassword) { |
|
|
isValid = false; |
|
|
signupConfirmPasswordError.textContent = 'Passwords do not match'; |
|
|
signupConfirmPasswordInput.classList.add('error'); |
|
|
signupPasswordInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (isValid) { |
|
|
|
|
|
try { |
|
|
const checkResponse = await fetch('/api/check_user', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ identifier: email }) |
|
|
}); |
|
|
const checkData = await checkResponse.json(); |
|
|
if (checkData.exists) { |
|
|
signupEmailError.textContent = 'Email already registered.'; |
|
|
signupEmailInput.classList.add('error'); |
|
|
return; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error checking user:', error); |
|
|
|
|
|
} |
|
|
submitFormData(event.target, signupSubmitButton, '/api/signup', 'main'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (forgotPasswordLink) { |
|
|
forgotPasswordLink.addEventListener('click', function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('forgot'); |
|
|
clearErrors('otp'); |
|
|
|
|
|
forgotPasswordRequestForm.reset(); |
|
|
otpInput.value = ''; |
|
|
|
|
|
forgotModalMessage.innerHTML = 'Enter your registered email to request a password reset.'; |
|
|
|
|
|
|
|
|
forgotPasswordRequestForm.style.display = 'block'; |
|
|
if (otpSection) otpSection.style.display = 'none'; |
|
|
|
|
|
|
|
|
const modalContent = forgotPasswordRequestModal.querySelector('.modal-content'); |
|
|
if (modalContent) { |
|
|
modalContent.style.maxWidth = '450px'; |
|
|
} |
|
|
|
|
|
showCustomModal(forgotPasswordRequestModal); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (forgotPasswordRequestForm) { |
|
|
forgotPasswordRequestForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('forgot'); |
|
|
|
|
|
let isValid = true; |
|
|
const email = forgotEmailInput.value.trim(); |
|
|
|
|
|
if (email === '') { |
|
|
isValid = false; |
|
|
forgotEmailError.textContent = 'Email is required.'; |
|
|
forgotEmailInput.classList.add('error'); |
|
|
} else if (!isValidEmail(email)) { |
|
|
isValid = false; |
|
|
forgotEmailError.textContent = 'Enter a valid email address.'; |
|
|
forgotEmailInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (isValid) { |
|
|
forgotSubmitButton.disabled = true; |
|
|
forgotSubmitButton.innerHTML = '<span class="loading-spinner"></span> Sending OTP...'; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/forgot_password_request', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ email: email }) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (response.ok) { |
|
|
currentForgotEmail = email; |
|
|
|
|
|
|
|
|
forgotPasswordRequestForm.style.display = 'none'; |
|
|
if (otpSection) otpSection.style.display = 'block'; |
|
|
otpInput.value = ''; |
|
|
|
|
|
|
|
|
const modalContent = forgotPasswordRequestModal.querySelector('.modal-content'); |
|
|
if (modalContent) { |
|
|
modalContent.style.maxWidth = '550px'; |
|
|
} |
|
|
|
|
|
|
|
|
let messageText = `<p>An OTP has been sent to <strong>${email}</strong>. Please enter it below.</p>`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
forgotModalMessage.innerHTML = messageText; |
|
|
startResendCooldown(); |
|
|
|
|
|
} else { |
|
|
|
|
|
if (response.status === 404) { |
|
|
|
|
|
hideCustomModal(forgotPasswordRequestModal); |
|
|
showFlexibleModal( |
|
|
'<p>The email address you entered is not registered yet.</p><p>Please sign up to create an account.</p>', |
|
|
[ |
|
|
{ text: 'Sign Up', action: () => { |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
|
|
|
signupEmailInput.value = email; |
|
|
}}, |
|
|
{ text: 'Try Again', style: 'background-color: var(--bs-secondary-color);' } |
|
|
] |
|
|
); |
|
|
} else { |
|
|
|
|
|
result.status = response.status; |
|
|
displayModalFormErrors(result, forgotPasswordRequestForm, 'forgot'); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
showFlexibleModal('<p>Network error. Could not request OTP. Please try again.</p>', [{ text: 'OK' }]); |
|
|
} finally { |
|
|
forgotSubmitButton.disabled = false; |
|
|
forgotSubmitButton.innerHTML = 'Request OTP'; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (verifyOtpForm) { |
|
|
verifyOtpForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('otp'); |
|
|
|
|
|
let isValid = true; |
|
|
const otpDigits = document.querySelectorAll('.otp-digit'); |
|
|
const otp = Array.from(otpDigits).map(d => d.value).join(''); |
|
|
|
|
|
if (otp.length !== 8 || !/^\d{8}$/.test(otp)) { |
|
|
isValid = false; |
|
|
otpError.textContent = 'Please enter all 8 digits of the OTP.'; |
|
|
otpDigits.forEach(d => d.classList.add('error')); |
|
|
} |
|
|
|
|
|
if (isValid) { |
|
|
otpInput.value = otp; |
|
|
verifyOtpButton.disabled = true; |
|
|
verifyOtpButton.innerHTML = '<span class="loading-spinner"></span> Verifying...'; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/verify_otp', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ email: currentForgotEmail, otp: otp }) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (response.ok) { |
|
|
hideCustomModal(forgotPasswordRequestModal); |
|
|
resetEmailInput.value = currentForgotEmail; |
|
|
newPasswordInput.value = ''; |
|
|
confirmNewPasswordInput.value = ''; |
|
|
showCustomModal(resetPasswordModal); |
|
|
|
|
|
} else { |
|
|
otpError.textContent = result.message || 'OTP verification failed.'; |
|
|
otpInput.classList.add('error'); |
|
|
otpInput.classList.add('error'); |
|
|
|
|
|
if (result.message) { |
|
|
forgotModalMessage.innerHTML = `<p>${result.message}</p>`; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
showFlexibleModal('<p>Network error. Could not verify OTP. Please try again.</p>', [{ text: 'OK' }]); |
|
|
} finally { |
|
|
verifyOtpButton.disabled = false; |
|
|
verifyOtpButton.innerHTML = 'Verify OTP'; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (resendOtpButton) { |
|
|
resendOtpButton.addEventListener('click', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('otp'); |
|
|
resendOtpButton.disabled = true; |
|
|
resendOtpButton.innerHTML = '<span class="loading-spinner"></span> Resending...'; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/forgot_password_request', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ email: currentForgotEmail }) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (response.ok) { |
|
|
let messageText = `<p>New OTP sent to <strong>${currentForgotEmail}</strong>. Please enter it below.</p>`; |
|
|
|
|
|
|
|
|
|
|
|
forgotModalMessage.innerHTML = messageText; |
|
|
otpInput.value = ''; |
|
|
otpInput.classList.remove('error'); |
|
|
otpError.textContent = ''; |
|
|
startResendCooldown(); |
|
|
} else { |
|
|
|
|
|
otpError.textContent = result.message || 'Failed to resend OTP.'; |
|
|
otpInput.classList.add('error'); |
|
|
if (result.message) { |
|
|
forgotModalMessage.innerHTML = `<p>${result.message}</p>`; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
showFlexibleModal('<p>Network error. Could not resend OTP. Please try again.</p>', [{ text: 'OK' }]); |
|
|
} finally { |
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.innerHTML = 'Send Again'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (forgotCancelButton) { |
|
|
forgotCancelButton.addEventListener('click', function() { |
|
|
hideCustomModal(forgotPasswordRequestModal); |
|
|
forgotPasswordRequestForm.reset(); |
|
|
currentForgotEmail = ''; |
|
|
clearErrors('forgot'); |
|
|
|
|
|
if (resendInterval) { |
|
|
clearInterval(resendInterval); |
|
|
resendInterval = null; |
|
|
resendCooldown = 0; |
|
|
} |
|
|
|
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.textContent = 'Send Code Again'; |
|
|
resendOtpButton.style.backgroundColor = ''; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (otpCancelButton) { |
|
|
otpCancelButton.addEventListener('click', function() { |
|
|
hideCustomModal(forgotPasswordRequestModal); |
|
|
verifyOtpForm.reset(); |
|
|
currentForgotEmail = ''; |
|
|
clearErrors('otp'); |
|
|
|
|
|
if (resendInterval) { |
|
|
clearInterval(resendInterval); |
|
|
resendInterval = null; |
|
|
resendCooldown = 0; |
|
|
} |
|
|
|
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.textContent = 'Send Code Again'; |
|
|
resendOtpButton.style.backgroundColor = ''; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (resetCancelButton) { |
|
|
resetCancelButton.addEventListener('click', function() { |
|
|
hideCustomModal(resetPasswordModal); |
|
|
resetPasswordForm.reset(); |
|
|
currentForgotEmail = ''; |
|
|
clearErrors('reset'); |
|
|
|
|
|
if (resendInterval) { |
|
|
clearInterval(resendInterval); |
|
|
resendInterval = null; |
|
|
resendCooldown = 0; |
|
|
} |
|
|
|
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.textContent = 'Send Code Again'; |
|
|
resendOtpButton.style.backgroundColor = ''; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (resetPasswordForm) { |
|
|
resetPasswordForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
clearErrors('reset'); |
|
|
|
|
|
let isValid = true; |
|
|
const email = resetEmailInput.value.trim(); |
|
|
const newPassword = newPasswordInput.value.trim(); |
|
|
const confirmNewPassword = confirmNewPasswordInput.value.trim(); |
|
|
|
|
|
|
|
|
if (email === '') { |
|
|
isValid = false; |
|
|
resetEmailError.textContent = 'Email is required.'; |
|
|
resetEmailInput.classList.add('error'); |
|
|
} else if (!isValidEmail(email)) { |
|
|
isValid = false; |
|
|
resetEmailError.textContent = 'Invalid email format.'; |
|
|
resetEmailInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (newPassword === '') { |
|
|
isValid = false; |
|
|
newPasswordError.textContent = 'New password is required.'; |
|
|
newPasswordInput.classList.add('error'); |
|
|
} else if (newPassword.length < 6) { |
|
|
isValid = false; |
|
|
newPasswordError.textContent = 'New password must be at least 6 characters.'; |
|
|
newPasswordInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
if (confirmNewPassword === '') { |
|
|
isValid = false; |
|
|
confirmNewPasswordError.textContent = 'Confirm new password is required.'; |
|
|
confirmNewPasswordInput.classList.add('error'); |
|
|
} else if (newPassword !== confirmNewPassword) { |
|
|
isValid = false; |
|
|
confirmNewPasswordError.textContent = 'Passwords do not match.'; |
|
|
confirmNewPasswordInput.classList.add('error'); |
|
|
newPasswordInput.classList.add('error'); |
|
|
} |
|
|
|
|
|
|
|
|
if (isValid) { |
|
|
resetSubmitButton.disabled = true; |
|
|
resetSubmitButton.innerHTML = '<span class="loading-spinner"></span> Resetting...'; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/reset_password', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ email: email, new_password: newPassword }) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (response.ok) { |
|
|
hideCustomModal(resetPasswordModal); |
|
|
resetPasswordForm.reset(); |
|
|
currentForgotEmail = ''; |
|
|
|
|
|
if (resendInterval) { |
|
|
clearInterval(resendInterval); |
|
|
resendInterval = null; |
|
|
resendCooldown = 0; |
|
|
} |
|
|
|
|
|
resendOtpButton.disabled = false; |
|
|
resendOtpButton.textContent = 'Send Code Again'; |
|
|
resendOtpButton.style.backgroundColor = ''; |
|
|
showFlexibleModal( |
|
|
'<p>Password reset successful. You can now login with your new password.</p>', |
|
|
[{ text: 'OK', action: () => { |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
}}] |
|
|
); |
|
|
} else { |
|
|
|
|
|
result.status = response.status; |
|
|
displayModalFormErrors(result, resetPasswordForm, 'reset'); |
|
|
if (result.message && (result.message.includes('OTP not verified') || result.message.includes('not initiated'))) { |
|
|
showFlexibleModal( |
|
|
`<p>${result.message}</p><p>Please start the password reset process again.</p>`, |
|
|
[{ text: 'Start Over', action: () => { |
|
|
hideCustomModal(resetPasswordModal); |
|
|
forgotPasswordLink.click(); |
|
|
}}], |
|
|
null |
|
|
); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
showFlexibleModal('<p>Network error. Could not reset password. Please try again.</p>', [{ text: 'OK' }]); |
|
|
} finally { |
|
|
resetSubmitButton.disabled = false; |
|
|
resetSubmitButton.innerHTML = 'Reset Password'; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function handleMainFormErrors(result, formElement, endpoint) { |
|
|
if (endpoint === '/api/login') { |
|
|
if (result.message && (result.message.includes('Account not found') || result.message.includes('Invalid credentials'))) { |
|
|
showFlexibleModal( |
|
|
`<p>${result.message}</p><p>If you are not registered, please sign up.</p>`, |
|
|
[{ text: 'Sign Up', action: () => { |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
}}, { text: 'OK', style: 'background-color: var(--bs-secondary-color);' }] |
|
|
); |
|
|
} |
|
|
} else if (endpoint === '/api/signup') { |
|
|
if (result.message && (result.message.includes('Email already registered') || result.message.includes('Username already taken'))) { |
|
|
showFlexibleModal( |
|
|
`<p>${result.message}</p><p>If you are already registered, please log in.</p>`, |
|
|
[{ text: 'Login', action: () => { |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
}}, { text: 'OK', style: 'background-color: var(--bs-secondary-color);' }] |
|
|
); |
|
|
} |
|
|
} |
|
|
if (result.errors) { |
|
|
for (const field in result.errors) { |
|
|
let inputElement, errorElement; |
|
|
|
|
|
if (formElement.id === 'loginForm' && field === 'username_or_email') { |
|
|
inputElement = loginUsernameOrEmailInput; |
|
|
errorElement = loginUsernameOrEmailError; |
|
|
} else if (formElement.id === 'signupForm' && field === 'name') { |
|
|
inputElement = signupNameInput; |
|
|
errorElement = signupNameError; |
|
|
} |
|
|
else { |
|
|
|
|
|
|
|
|
inputElement = formElement.querySelector(`[name="${field}"]`); |
|
|
|
|
|
const inputId = inputElement ? inputElement.id : null; |
|
|
errorElement = inputId ? document.getElementById(`${inputId}Error`) : null; |
|
|
} |
|
|
|
|
|
if (errorElement) errorElement.textContent = result.errors[field]; |
|
|
if (inputElement) inputElement.classList.add('error'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function displayModalFormErrors(result, formElement, formContext) { |
|
|
|
|
|
clearErrors(formContext); |
|
|
|
|
|
if (result.errors) { |
|
|
for (const field in result.errors) { |
|
|
|
|
|
const inputElement = formElement.querySelector(`[name="${field}"]`); |
|
|
|
|
|
const errorElement = inputElement ? document.getElementById(`${inputElement.id}Error`) : null; |
|
|
|
|
|
if (errorElement) errorElement.textContent = result.errors[field]; |
|
|
if (inputElement) inputElement.classList.add('error'); |
|
|
} |
|
|
} |
|
|
|
|
|
const targetModalMessage = formElement.closest('.modal-content').querySelector('.modal-message'); |
|
|
if (targetModalMessage && result.message) { |
|
|
|
|
|
const isError = result.status >= 400 && result.status < 600; |
|
|
const messageClass = isError ? 'error-message' : ''; |
|
|
targetModalMessage.innerHTML = `<p class="${messageClass}">${result.message}</p>`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function submitFormData(formElement, submitButton, endpoint, formContext) { |
|
|
if (!formElement || !submitButton || !endpoint) { |
|
|
console.error("submitFormData called with missing arguments."); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(formElement); |
|
|
const data = {}; |
|
|
for (let [key, value] of formData.entries()) { |
|
|
data[key] = value; |
|
|
} |
|
|
|
|
|
submitButton.disabled = true; |
|
|
if (formContext === 'main') { |
|
|
displayFormMessage("Submitting...", 'info'); |
|
|
} else { |
|
|
|
|
|
submitButton.innerHTML = '<span class="loading-spinner"></span> Submitting...'; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(endpoint, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify(data), |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (response.ok) { |
|
|
if (formContext === 'main') { |
|
|
clearErrors('main'); |
|
|
formElement.reset(); |
|
|
displayFormMessage('', 'info'); |
|
|
loginAttempts = 0; |
|
|
|
|
|
if (endpoint === '/api/signup') { |
|
|
showFlexibleModal( |
|
|
'<p>Registration successful! Please log in.</p>', |
|
|
[{ text: 'OK', action: () => { |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
const url = new URL(window.location); |
|
|
url.searchParams.delete('signup'); |
|
|
window.history.replaceState({}, '', url.toString()); |
|
|
}}] |
|
|
); |
|
|
} else if (endpoint === '/api/login') { |
|
|
showFlexibleModal( |
|
|
'<p>Login successful!</p>', |
|
|
[{ text: 'OK', action: () => { |
|
|
localStorage.setItem('isLoggedIn', 'true'); |
|
|
localStorage.setItem('user', JSON.stringify(result.user)); |
|
|
window.location.href = '/'; |
|
|
}}] |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} else { |
|
|
if (formContext === 'main') { |
|
|
if (endpoint === '/api/login' && result.message && result.message.includes('Invalid credentials')) { |
|
|
loginAttempts++; |
|
|
loginPasswordInput.classList.add('error'); |
|
|
loginPasswordError.textContent = result.message; |
|
|
|
|
|
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) { |
|
|
showLoginAttemptsLimitModal(); |
|
|
loginPasswordInput.value = ''; |
|
|
} else { |
|
|
displayFormMessage(`Incorrect password. You have ${MAX_LOGIN_ATTEMPTS - loginAttempts} attempts left.`, 'error'); |
|
|
} |
|
|
} else { |
|
|
displayFormMessage(result.message || 'An error occurred.', 'error'); |
|
|
handleMainFormErrors(result, formElement, endpoint); |
|
|
} |
|
|
} else if (formContext === 'forgot') { |
|
|
result.status = response.status; |
|
|
displayModalFormErrors(result, formElement, 'forgot'); |
|
|
} else if (formContext === 'otp') { |
|
|
result.status = response.status; |
|
|
displayModalFormErrors(result, formElement, 'otp'); |
|
|
} else if (formContext === 'reset') { |
|
|
result.status = response.status; |
|
|
displayModalFormErrors(result, formElement, 'reset'); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
if (formContext === 'main') { |
|
|
displayFormMessage('Network error. Please try again.', 'error'); |
|
|
} else { |
|
|
showFlexibleModal( |
|
|
'<p>Network error. Please check your internet connection and try again.</p>', |
|
|
[{ text: 'OK' }] |
|
|
); |
|
|
} |
|
|
} finally { |
|
|
submitButton.disabled = false; |
|
|
|
|
|
if (formContext === 'forgot') { |
|
|
submitButton.innerHTML = 'Send Reset Code'; |
|
|
} else if (formContext === 'otp') { |
|
|
submitButton.innerHTML = 'Verify Code'; |
|
|
} else if (formContext === 'reset') { |
|
|
submitButton.innerHTML = 'Reset Password'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showLoginAttemptsLimitModal() { |
|
|
const messageHtml = ` |
|
|
<strong>You have made three incorrect login attempts.</strong> |
|
|
<ul> |
|
|
<li>1. If you are not registered, please <a href="#" id="modalSignupLink">sign up now</a>.</li> |
|
|
<li>2. If you are already registered, please <a href="#" id="modalLoginLink">login now</a> with correct credentials.</li> |
|
|
<li>3. If you have forgotten your password, click <a href="#" id="modalForgotPasswordLink">forgot password</a> to reset it.</li> |
|
|
</ul> |
|
|
`; |
|
|
|
|
|
const buttons = [ |
|
|
{ text: 'Login', action: () => { |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
loginAttempts = 0; |
|
|
loginPasswordInput.value = ''; |
|
|
}}, |
|
|
{ text: 'Signup', action: () => { |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
loginAttempts = 0; |
|
|
loginPasswordInput.value = ''; |
|
|
}} |
|
|
]; |
|
|
|
|
|
showFlexibleModal(messageHtml, buttons, () => { |
|
|
const modalSignupLink = document.getElementById('modalSignupLink'); |
|
|
const modalLoginLink = document.getElementById('modalLoginLink'); |
|
|
const modalForgotPasswordLink = document.getElementById('modalForgotPasswordLink'); |
|
|
|
|
|
if (modalSignupLink) { |
|
|
modalSignupLink.onclick = (e) => { |
|
|
e.preventDefault(); |
|
|
hideCustomModal(customModal); |
|
|
signupRadio.checked = true; |
|
|
slideToSignup(); |
|
|
loginAttempts = 0; |
|
|
loginPasswordInput.value = ''; |
|
|
}; |
|
|
} |
|
|
if (modalLoginLink) { |
|
|
modalLoginLink.onclick = (e) => { |
|
|
e.preventDefault(); |
|
|
hideCustomModal(customModal); |
|
|
loginRadio.checked = true; |
|
|
slideToLogin(); |
|
|
loginAttempts = 0; |
|
|
loginPasswordInput.value = ''; |
|
|
}; |
|
|
} |
|
|
if (modalForgotPasswordLink) { |
|
|
modalForgotPasswordLink.onclick = (e) => { |
|
|
e.preventDefault(); |
|
|
hideCustomModal(customModal); |
|
|
forgotPasswordLink.click(); |
|
|
loginAttempts = 0; |
|
|
loginPasswordInput.value = ''; |
|
|
}; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function isValidEmail(email) { |
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
|
|
return emailRegex.test(email); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|