From 4362db39460ada67d2a23d7b28d8f1eefb8a576d Mon Sep 17 00:00:00 2001
From: devzeeh <148837352+devzeeh@users.noreply.github.com>
Date: Wed, 29 Apr 2026 14:31:38 +0800
Subject: [PATCH 1/3] feat: implement signup flow with OTP verification, card
validation, and backend API handlers
---
backend/cmd/app/main.go | 3 +
backend/internal/auth/signup_verification.go | 136 ++++++++
backend/internal/pkg/handler/jsonWrite.go | 1 +
frontend/assets/js/signup.js | 349 +++++++++++--------
frontend/assets/style/index.css | 4 -
frontend/assets/style/style.css | 19 +
frontend/templates/signup.html | 175 +++++++---
7 files changed, 478 insertions(+), 209 deletions(-)
create mode 100644 backend/internal/auth/signup_verification.go
delete mode 100644 frontend/assets/style/index.css
create mode 100644 frontend/assets/style/style.css
diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go
index 7ed6673..8d3d454 100644
--- a/backend/cmd/app/main.go
+++ b/backend/cmd/app/main.go
@@ -71,6 +71,9 @@ func main() {
// POST Request: JSON API endpoints
mux.HandleFunc("POST /v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint
mux.HandleFunc("POST /v1/signupauth", authHandler.SignupHandler)
+ mux.HandleFunc("POST /v1/signup/check-details", authHandler.CheckDetailsHandler)
+ mux.HandleFunc("POST /v1/signup/verify-otp", authHandler.VerifyOTPHandler)
+ mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler)
mux.HandleFunc("GET /login", authHandler.LoginView)
mux.HandleFunc("GET /signup", authHandler.SignupView)
mux.HandleFunc("GET /dashboard", userHandler.DashboardHandler)
diff --git a/backend/internal/auth/signup_verification.go b/backend/internal/auth/signup_verification.go
new file mode 100644
index 0000000..8c8de97
--- /dev/null
+++ b/backend/internal/auth/signup_verification.go
@@ -0,0 +1,136 @@
+package authentication
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "sync"
+ "time"
+ "unicard-go/backend/internal/pkg/account"
+ jsonwrite "unicard-go/backend/internal/pkg/handler"
+)
+
+// In-memory store for OTPs (In production, use Redis or DB)
+var (
+ otpStore = make(map[string]string)
+ otpMutex sync.RWMutex
+)
+
+type CheckDetailsRequest struct {
+ Email string `json:"email"`
+ ContactNumber string `json:"contact_number"`
+}
+
+type VerifyOTPRequest struct {
+ Email string `json:"email"`
+ OTP string `json:"otp"`
+}
+
+type CheckCardRequest struct {
+ CardNumber string `json:"card_number"`
+}
+
+// CheckDetailsHandler checks if email and phone are available and sends OTP
+func (h *Handler) CheckDetailsHandler(w http.ResponseWriter, r *http.Request) {
+ var req CheckDetailsRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid request"})
+ return
+ }
+
+ // Check Email
+ exists, err := account.IsEmailExist(h.DB, req.Email)
+ if err != nil {
+ jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "Database error"})
+ return
+ }
+ if exists {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Email already registered", Field: "email"})
+ return
+ }
+
+ // Check Phone
+ exists, err = h.isPhoneExist(req.ContactNumber)
+ if err != nil {
+ jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "Database error"})
+ return
+ }
+ if exists {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Phone number already registered", Field: "phone"})
+ return
+ }
+
+ // Generate OTP
+ otp := fmt.Sprintf("%06d", rand.Intn(1000000))
+ otpMutex.Lock()
+ otpStore[req.Email] = otp
+ otpMutex.Unlock()
+
+ // Log OTP for development (since we don't have SMTP configured here)
+ log.Printf("SIGNUP OTP for %s: %s", req.Email, otp)
+
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "OTP sent successfully",
+ })
+}
+
+// VerifyOTPHandler checks the entered OTP
+func (h *Handler) VerifyOTPHandler(w http.ResponseWriter, r *http.Request) {
+ var req VerifyOTPRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid request"})
+ return
+ }
+
+ otpMutex.RLock()
+ storedOTP, ok := otpStore[req.Email]
+ otpMutex.RUnlock()
+
+ if !ok || storedOTP != req.OTP {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid or expired OTP"})
+ return
+ }
+
+ // Optional: Clear OTP after successful verification
+ // otpMutex.Lock()
+ // delete(otpStore[req.Email])
+ // otpMutex.Unlock()
+
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "OTP verified successfully",
+ })
+}
+
+// CheckCardHandler checks if card is valid for registration
+func (h *Handler) CheckCardHandler(w http.ResponseWriter, r *http.Request) {
+ var req CheckCardRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid request"})
+ return
+ }
+
+ var status string
+ err := h.DB.QueryRow("SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&status)
+ if err != nil {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Card not found"})
+ return
+ }
+
+ if status != "Inactive" {
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Card is already active or restricted"})
+ return
+ }
+
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "Card is valid",
+ })
+}
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
diff --git a/backend/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go
index 46ce8b9..17caeb1 100644
--- a/backend/internal/pkg/handler/jsonWrite.go
+++ b/backend/internal/pkg/handler/jsonWrite.go
@@ -9,6 +9,7 @@ import (
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
+ Field string `json:"field,omitempty"`
}
// Login specific response — returns user data after login
diff --git a/frontend/assets/js/signup.js b/frontend/assets/js/signup.js
index 1019e2c..d970ba6 100644
--- a/frontend/assets/js/signup.js
+++ b/frontend/assets/js/signup.js
@@ -1,41 +1,61 @@
-// Global helper functions for input restriction
-function isValidEmail(email) {
- const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
- return regex.test(email);
-}
-
function isNumber(evt) {
evt = (evt) ? evt : window.event;
var charCode = (evt.which) ? evt.which : evt.keyCode;
- // Allow only numbers
- if (charCode > 31 && (charCode < 48 || charCode > 57)) {
- return false;
- }
+ if (charCode > 31 && (charCode < 48 || charCode > 57)) return false;
return true;
}
function isAlpha(evt) {
evt = (evt) ? evt : window.event;
var charCode = (evt.which) ? evt.which : evt.keyCode;
- // Allow letters and spaces. Also allow control characters < 32.
if (charCode < 32) return true;
if ((charCode >= 65 && charCode <= 90) ||
(charCode >= 97 && charCode <= 122) ||
- charCode === 32) {
+ charCode === 32) return true;
+ return false;
+}
+
+function validateInput(el) {
+ const errorEl = document.getElementById(`error-${el.id}`);
+ if (!el.checkValidity() || el.value.trim() === "") {
+ el.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ el.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ if (errorEl) {
+ if (el.value.trim() === "") {
+ errorEl.textContent = "This field is required.";
+ } else if (el.type === "email") {
+ errorEl.textContent = "Please enter a valid email address.";
+ } else if (el.id === "contact_number") {
+ errorEl.textContent = "Please enter a valid 11-digit contact number.";
+ } else if (el.id === "card_id") {
+ errorEl.textContent = "Please enter a valid 16-digit card number.";
+ } else {
+ errorEl.textContent = "Invalid format.";
+ }
+ errorEl.classList.remove('hidden');
+ }
+ return false;
+ } else {
+ el.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ el.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ if (errorEl) errorEl.classList.add('hidden');
return true;
}
- return false;
}
document.addEventListener("DOMContentLoaded", function () {
// STEP ELEMENTS
const step1 = document.getElementById('step-1');
+ const stepOTP = document.getElementById('step-otp');
const step2 = document.getElementById('step-2');
const step3 = document.getElementById('step-3');
const stepSubtitle = document.getElementById('step-subtitle');
+ const stepProgress = document.getElementById('step-progress');
// BUTTONS
const btnStep1 = document.getElementById('btn-step-1');
+ const btnBackOTP = document.getElementById('btn-back-otp');
+ const btnStepOTP = document.getElementById('btn-step-otp');
const btnBack2 = document.getElementById('btn-back-2');
const btnStep2 = document.getElementById('btn-step-2');
const btnBack3 = document.getElementById('btn-back-3');
@@ -47,58 +67,53 @@ document.addEventListener("DOMContentLoaded", function () {
const lastNameInput = document.getElementById('last_name');
const emailInput = document.getElementById('email');
const contactNumberInput = document.getElementById('contact_number');
-
- // INPUTS
+ const otpInput = document.getElementById('otp');
const cardIdInput = document.getElementById('card_id');
- const cardIdError = document.getElementById('card-id-error');
-
- // INPUTS
const passwordInput = document.getElementById('password');
const confirmPasswordInput = document.getElementById('confirm_password');
+
+ // FEEDBACK
+ const cardIdError = document.getElementById('card-id-error');
+ const errorMessage = document.getElementById('error-message');
const checklist = document.getElementById('validation-checklist');
const lengthCheck = document.getElementById('length-check');
const matchCheck = document.getElementById('match-check');
- // MODAL (ADDED)
+ // MODAL
const successModal = document.getElementById('success-modal');
const modalCloseBtn = document.getElementById('modal-close-btn');
- // GLOBAL
- const errorMessage = document.getElementById('error-message');
-
// FORM DATA STORAGE
const formData = {
firstName: '',
lastName: '',
email: '',
+ contactNumber: '',
cardId: '',
password: '',
};
- // ROBUSTNESS CHECK
- if (!step1 || !step2 || !step3 || !stepSubtitle || !btnStep1 || !btnBack2 || !btnStep2 || !btnBack3 || !createAccountBtn || !signupForm || !firstNameInput || !emailInput || !cardIdInput || !cardIdError || !passwordInput || !confirmPasswordInput || !checklist || !lengthCheck || !matchCheck || !errorMessage || !successModal || !modalCloseBtn) {
- console.error("Signup Script Error: Not all required HTML elements were found on the page.");
- return; // Stop the script
- }
-
- // INITIALIZATION
-
// HELPER FUNCTIONS
function showStep(stepNumber) {
- step1.classList.add('hidden');
- step2.classList.add('hidden');
- step3.classList.add('hidden');
+ [step1, stepOTP, step2, step3].forEach(s => s?.classList.add('hidden'));
errorMessage.classList.add('hidden');
if (stepNumber === 1) {
step1.classList.remove('hidden');
- stepSubtitle.textContent = 'Step 1 of 3: Your Details';
+ stepSubtitle.textContent = 'Step 1 of 4: Your Details';
+ if (stepProgress) stepProgress.style.width = '25%';
+ } else if (stepNumber === 'otp') {
+ stepOTP.classList.remove('hidden');
+ stepSubtitle.textContent = 'Step 2 of 4: Verify Email';
+ if (stepProgress) stepProgress.style.width = '50%';
} else if (stepNumber === 2) {
step2.classList.remove('hidden');
- stepSubtitle.textContent = 'Step 2 of 3: Card Verification';
+ stepSubtitle.textContent = 'Step 3 of 4: Card Verification';
+ if (stepProgress) stepProgress.style.width = '75%';
} else if (stepNumber === 3) {
step3.classList.remove('hidden');
- stepSubtitle.textContent = 'Step 3 of 3: Create Password';
+ stepSubtitle.textContent = 'Step 4 of 4: Create Password';
+ if (stepProgress) stepProgress.style.width = '100%';
}
}
@@ -118,78 +133,128 @@ document.addEventListener("DOMContentLoaded", function () {
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.remove('hidden');
+ errorMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
- // VALIDATION LOGIC
-
- // Real-time validation for Name and Email fields
- function validateStep1Realtime() {
- const firstName = firstNameInput.value.trim();
- const lastName = lastNameInput.value.trim();
- const email = emailInput.value.trim();
- const contactNumber = contactNumberInput.value.trim();
-
- const isNameValid = firstName !== '' && lastName !== '';
- const isEmailValid = email !== '' && isValidEmail(email);
- const isContactValid = contactNumber !== '';
-
- if (isNameValid && isEmailValid && isContactValid) {
- errorMessage.classList.add('hidden');
- }
- }
+ // VALIDATION & API CALLS
+
+ async function validateStep1() {
+ const inputs = [firstNameInput, lastNameInput, emailInput, contactNumberInput];
+ let isValid = true;
+ inputs.forEach(input => {
+ if (!validateInput(input)) isValid = false;
+ });
+
+ if (!isValid) return false;
+
+ // Backend check for Email/Phone availability
+ try {
+ btnStep1.disabled = true;
+ btnStep1.textContent = 'Checking...';
+
+ const response = await fetch('/v1/signup/check-details', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ email: emailInput.value.trim(),
+ contact_number: contactNumberInput.value.trim()
+ })
+ });
+
+ const data = await response.json();
+ if (!data.success) {
+ showError(data.message);
+ if (data.field === 'email') {
+ emailInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ emailInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ }
+ if (data.field === 'phone') {
+ contactNumberInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ contactNumberInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ }
+ return false;
+ }
- // Click validation (shows error)
- function validateStep1() {
- const firstName = firstNameInput.value.trim();
- const lastName = lastNameInput.value.trim();
- const email = emailInput.value.trim();
- const contactNumber = contactNumberInput.value.trim();
-
- if (firstName === '' || lastName === '' || email === '' || !isValidEmail(email) || contactNumber === '') {
- showError('Please fill all fields and provide a valid email address.');
+ formData.firstName = firstNameInput.value.trim();
+ formData.lastName = lastNameInput.value.trim();
+ formData.email = emailInput.value.trim();
+ formData.contactNumber = contactNumberInput.value.trim();
+ return true;
+ } catch (err) {
+ showError('Network error. Please try again.');
return false;
+ } finally {
+ btnStep1.disabled = false;
+ btnStep1.textContent = 'Next';
}
-
- // TODO: Backend check for email
- formData.firstName = firstName;
- formData.lastName = lastName;
- formData.email = email;
- formData.contactNumber = contactNumber;
- return true;
}
- // Validate Card ID (real-time and click)
- function validateStep2() {
- const cardId = cardIdInput.value.trim();
+ async function validateOTP() {
+ if (!validateInput(otpInput)) return false;
- if (cardId === "") {
- cardIdError.textContent = 'Please enter your Card ID.';
- cardIdError.classList.remove('hidden');
+ try {
+ btnStepOTP.disabled = true;
+ btnStepOTP.textContent = 'Verifying...';
+
+ const response = await fetch('/v1/signup/verify-otp', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ email: formData.email,
+ otp: otpInput.value.trim()
+ })
+ });
+
+ const data = await response.json();
+ if (!data.success) {
+ showError(data.message);
+ otpInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ otpInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ return false;
+ }
+ return true;
+ } catch (err) {
+ showError('Network error. Please try again.');
return false;
+ } finally {
+ btnStepOTP.disabled = false;
+ btnStepOTP.textContent = 'Verify';
}
+ }
- const isCardIdOnlyNumbers = /^\d+$/.test(cardId);
- const isCardIdValidLength = cardId.length === 16;
-
- if (!isCardIdOnlyNumbers) {
- cardIdError.textContent = 'Card ID must contain only numbers.';
- cardIdError.classList.remove('hidden');
- return false;
- } else if (!isCardIdValidLength) {
- cardIdError.textContent = 'Card ID must be exactly 10 digits long.';
- cardIdError.classList.remove('hidden');
+ async function validateStep2() {
+ if (!validateInput(cardIdInput)) return false;
+
+ try {
+ btnStep2.disabled = true;
+ btnStep2.textContent = 'Verifying...';
+
+ const response = await fetch('/v1/signup/check-card', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ card_number: cardIdInput.value.trim() })
+ });
+
+ const data = await response.json();
+ if (!data.success) {
+ showError(data.message);
+ cardIdInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ cardIdInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+ return false;
+ }
+
+ formData.cardId = cardIdInput.value.trim();
+ return true;
+ } catch (err) {
+ showError('Network error. Please try again.');
return false;
+ } finally {
+ btnStep2.disabled = false;
+ btnStep2.textContent = 'Next';
}
-
- // TODO: Backend check for card ID
-
- cardIdError.classList.add('hidden');
- formData.cardId = cardId;
- return true;
}
- // Validate Password
- function validateStep3() {
+ function updatePasswordChecklist() {
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
@@ -198,10 +263,21 @@ document.addEventListener("DOMContentLoaded", function () {
updateChecklistItem(lengthCheck, isLengthValid);
updateChecklistItem(matchCheck, passwordsMatch);
+ }
+
+ function validateStep3() {
+ const p1 = validateInput(passwordInput);
+ const p2 = validateInput(confirmPasswordInput);
- const allValid = isLengthValid && passwordsMatch;
+ updatePasswordChecklist();
- if (allValid) {
+ const password = passwordInput.value;
+ const confirmPassword = confirmPasswordInput.value;
+
+ const isLengthValid = password.length >= 8;
+ const passwordsMatch = password === confirmPassword && password.length > 0;
+
+ if (p1 && p2 && isLengthValid && passwordsMatch) {
formData.password = password;
return true;
}
@@ -209,49 +285,34 @@ document.addEventListener("DOMContentLoaded", function () {
}
// EVENT LISTENERS
-
- // Add real-time listeners for Step 1
- firstNameInput.addEventListener('input', validateStep1Realtime);
- lastNameInput.addEventListener('input', validateStep1Realtime);
- contactNumberInput.addEventListener('input', validateStep1Realtime);
-
- // Next from Step 1
- btnStep1.addEventListener('click', function () {
- if (validateStep1()) {
- showStep(2);
- }
+ btnStep1.addEventListener('click', async () => {
+ if (await validateStep1()) showStep('otp');
});
- // Back from Step 2
- btnBack2.addEventListener('click', function () {
- showStep(1);
+ btnBackOTP.addEventListener('click', () => showStep(1));
+ btnStepOTP.addEventListener('click', async () => {
+ if (await validateOTP()) showStep(2);
});
- // Next from Step 2
- btnStep2.addEventListener('click', function () {
- if (validateStep2()) {
- showStep(3);
- }
+ btnBack2.addEventListener('click', () => showStep('otp'));
+ btnStep2.addEventListener('click', async () => {
+ if (await validateStep2()) showStep(3);
});
-
- // Real-time validation for Card ID as user types
- cardIdInput.addEventListener('input', validateStep2);
- // Back from Step 3
- btnBack3.addEventListener('click', function () {
- showStep(2);
- });
+ btnBack3.addEventListener('click', () => showStep(2));
- // Real-time validation for Password fields
- passwordInput.addEventListener('input', validateStep3);
- confirmPasswordInput.addEventListener('input', validateStep3);
+ passwordInput.addEventListener('input', updatePasswordChecklist);
+ confirmPasswordInput.addEventListener('input', updatePasswordChecklist);
- // Final Form Submission
- signupForm.addEventListener('submit', function (event) {
+ signupForm.addEventListener('submit', async (event) => {
event.preventDefault();
+ if (!validateStep3()) return;
- if (validateStep3()) {
- fetch("/v1/signupauth", {
+ try {
+ createAccountBtn.disabled = true;
+ createAccountBtn.textContent = 'Creating...';
+
+ const response = await fetch("/v1/signupauth", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
@@ -262,27 +323,21 @@ document.addEventListener("DOMContentLoaded", function () {
card_number: formData.cardId,
password: formData.password
})
- })
- .then(res => res.json())
- .then(data => {
- if (data.success) {
- successModal.classList.remove('hidden');
- } else {
- showError(data.message || 'Failed to create account');
- }
- })
- .catch(err => {
- console.error(err);
- showError('Network error occurred.');
});
- } else {
- showError('Please correct the errors in the password fields.');
+
+ const data = await response.json();
+ if (data.success) {
+ successModal.classList.remove('hidden');
+ } else {
+ showError(data.message || 'Failed to create account');
+ }
+ } catch (err) {
+ showError('Network error occurred.');
+ } finally {
+ createAccountBtn.disabled = false;
+ createAccountBtn.textContent = 'Create Account';
}
});
- // MODAL BUTTON (ADDED)
- // Add event listener for the modal's "Go to Login" button
- modalCloseBtn.addEventListener('click', function() {
- window.location.href = "/login";
- });
+ modalCloseBtn.addEventListener('click', () => window.location.href = "/login");
});
\ No newline at end of file
diff --git a/frontend/assets/style/index.css b/frontend/assets/style/index.css
deleted file mode 100644
index 40fa6e9..0000000
--- a/frontend/assets/style/index.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.required {
- color: red;
- margin-left: 3px;
-}
\ No newline at end of file
diff --git a/frontend/assets/style/style.css b/frontend/assets/style/style.css
new file mode 100644
index 0000000..c1ed756
--- /dev/null
+++ b/frontend/assets/style/style.css
@@ -0,0 +1,19 @@
+.required label::after {
+ content: " *";
+ color: #ef4444;
+}
+
+input.error {
+ border-color: #ef4444 !important;
+ outline: 2px solid transparent !important;
+ outline-offset: 2px !important;
+ --tw-ring-color: #ef4444 !important;
+ ring-width: 2px !important;
+}
+
+.error-text {
+ color: #ef4444;
+ font-size: 0.75rem;
+ margin-top: 0.25rem;
+ display: block;
+}
\ No newline at end of file
diff --git a/frontend/templates/signup.html b/frontend/templates/signup.html
index 473abac..ea7250e 100644
--- a/frontend/templates/signup.html
+++ b/frontend/templates/signup.html
@@ -10,48 +10,57 @@
-
+
-
+
Create your account
- Step 1 of 3: Your Details
+ Step 1 of 4: Your Details
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -188,13 +240,20 @@
Create your account
-