diff --git a/package-lock.json b/package-lock.json
index a0c011d..6e85085 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,8 +44,11 @@
"@babel/cli": "^7.26.4",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
+ "autoprefixer": "^10.4.21",
"babel-loader": "^9.2.1",
"copy-webpack-plugin": "^12.0.2",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.17",
"terser-webpack-plugin": "^5.3.11",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
@@ -1827,9 +1830,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -2531,6 +2534,44 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -2991,9 +3032,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001692",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
- "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==",
+ "version": "1.0.30001754",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
+ "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"dev": true,
"funding": [
{
@@ -3867,9 +3908,9 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.18.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
- "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4620,6 +4661,20 @@
"node": ">= 0.6"
}
},
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -6498,6 +6553,25 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"license": "ISC"
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -6676,6 +6750,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@@ -7138,6 +7222,42 @@
"node": ">=8"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -8186,6 +8306,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -8352,6 +8482,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwindcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
diff --git a/public/css/va-register.css b/public/css/va-register.css
index fa53161..a90ec99 100644
--- a/public/css/va-register.css
+++ b/public/css/va-register.css
@@ -1,70 +1,357 @@
-body {
- background-color: #212529;
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ }
+
+ body {
+ background-color: #fff;
+ color: #333;
+ }
+
+ .container {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+ }
+
+ /* Sidebar Styles */
+ .sidebar {
+ display: none;
+ flex-direction: column;
+ width: 344px;
+ background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7);
+ padding: 24px;
+ border-radius: 14px;
+ margin: 20px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ }
+
+ .logo {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 32px;
+ }
+ .logo-placeholder img{
+ width: 100%;
+ height: auto;
+ max-width: 196px;
+ }
+
+ .progress-steps {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .step {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ border-radius: 12px;
+ border: 2px solid transparent;
+ cursor: pointer;
+ position: relative;
+ }
+
+ .step.active {
+ border-color: #070065;
+ }
+
+ .step.completed .step-number {
+ background-color: #070065;
+ border-color: #070065;
+ color: white;
+ }
+
+ .step-number {
+ width: 34px;
+ height: 34px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 2px solid #a1a1aa;
+ font-weight: 600;
+ color: #a1a1aa;
+ transition: all 0.3s ease;
+ }
+
+ .step.active .step-number {
+ border-color: #000;
+ color: #000;
+ }
+
+ .step-text {
+ font-size: 14px;
+ color: #a1a1aa;
+ }
+
+ .step.active .step-text {
+ color: #000;
+ }
+
+ .step-connector {
+ height: 24px;
+ width: 2px;
+ background-color: #a1a1aa;
+ margin-left: 16px;
+ position: absolute;
+ top: 46px;
+ left: 12px;
+ }
+
+ .step.active .step-connector {
+ background-color: #000;
+ }
+
+ /* Main Content Styles */
+ .main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 20px;
+ overflow-y: auto;
+ }
+
+ .mobile-progress {
+ background-color: #f4f4f5;
+ padding: 16px;
+ border-radius: 12px;
+ margin-bottom: 20px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ overflow-x: auto;
+ }
+
+ .mobile-progress ol {
+ display: flex;
+ list-style: none;
+ gap: 12px;
+ min-width: max-content;
+ }
+
+ .mobile-progress .step {
+ flex-shrink: 0;
+ }
+
+ .form-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 40px 0;
+ }
+
+ .form-card {
+ width: 100%;
+ max-width: 450px;
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+ padding: 24px;
+ }
+
+ .form-title {
+ text-align: center;
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ color: #000;
+ }
+
+ .form-step {
+ display: none;
+ }
+
+ .form-step.active {
+ display: block;
+ }
+
+ .form-group {
+ margin-bottom: 16px;
+ }
+
+ .input-container {
+ position: relative;
+ background-color: #f9fafb;
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .input-label {
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 14px;
+ color: #6b7280;
+ transition: all 0.2s ease;
+ pointer-events: none;
+ }
+
+ .input-field, .select-field {
+ width: 100%;
+ padding: 16px 12px;
+ border: none;
+ background: transparent;
+ outline: none;
+ font-size: 14px;
+ }
+
+ .select-field {
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 12px center;
+ background-size: 16px;
}
-.card-registration {
- border-radius: 15px;
+ .input-field:focus + .input-label,
+ .input-field:not(:placeholder-shown) + .input-label,
+ .select-field:focus + .input-label,
+ .select-field:valid + .input-label {
+ top: 2px;
+ font-size: 12px;
+ transform: none;
+ }
+
+ .checkbox-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+
+ .checkbox-group input {
+ width: 16px;
+ height: 16px;
+ }
+
+ .checkbox-group label {
+ font-size: 14px;
+ }
+
+ .form-actions {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 24px;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .btn-primary {
+ background-color: #070065;
+ color: white;
+ }
+
+ .btn-primary:hover {
+ opacity: 0.9;
+ }
+
+ .btn-secondary {
+ background-color: #f3f4f6;
+ color: #374151;
+ }
+
+ .btn-secondary:hover {
+ background-color: #e5e7eb;
+ }
+
+ .btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ .payment-option {
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 12px;
+ cursor: pointer;
+ }
+
+ .payment-option.selected {
+ border-color: #070065;
+ background-color: #f0f9ff;
+ }
+
+ .payment-option h4 {
+ margin-bottom: 8px;
+ font-size: 16px;
+ }
+
+ .payment-option p {
+ font-size: 14px;
+ color: #6b7280;
+ }
+
+ .payment-details {
+ display: none;
+ margin-top: 16px;
+ padding: 16px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background-color: #f9fafb;
}
-.card-registration .form-control {
- border-radius: 0.5rem;
+.payment-details.active {
+ display: block;
}
-.form-label {
- font-weight: 500;
+ .success-message {
+ text-align: center;
+ padding: 40px 20px;
+ }
+
+ .success-icon {
+ font-size: 48px;
+ color: #10b981;
+ margin-bottom: 16px;
+ }
+
+ .success-message h3 {
+ font-size: 24px;
+ margin-bottom: 8px;
+ }
+
+ .success-message p {
+ color: #6b7280;
+ margin-bottom: 24px;
+ }
+
+.location-loading {
+ display: none;
+ font-size: 12px;
+ color: #6b7280;
+ margin-top: 4px;
}
-.btn-warning {
- background-color: #ffb300;
- border: none;
- color: #000;
- font-weight: 600;
+.row {
+ display: flex;
+ gap: 12px;
}
-.btn-warning:hover {
- background-color: #e6a000;
+.row .form-group {
+ flex: 1;
}
-.form-outline .form-control+.form-label {
- position: absolute;
- top: 6px;
- max-width: 90%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- left: .75rem;
- padding-top: .37rem;
- pointer-events: none;
- transform-origin: 0 0;
- transition: all .2s ease-out;
- color: var(--mdb-form-control-label-color);
- margin-bottom: 0;
- background-color: #ffffff;
-}
-.form-outline .form-control {
- outline: none;
- box-shadow: none;
-}
-
-.form-outline {
- position: relative;
- width: 100%;
-}
-.form-outline .form-control.not-required:placeholder-shown + .form-label,
-.form-outline .form-control.not-required:empty + .form-label {
- top: 6px;
-}
-
-.form-outline .form-control:focus + .form-label,
-.form-outline .form-control:valid + .form-label,
-.form-outline .form-control.not-required:not(:placeholder-shown) + .form-label,
-.form-outline .form-control.not-required:focus + .form-label {
- top: -17px;
-}
-
-.card-registration .select-input.form-control[readonly]:not([disabled]) {
- font-size: 1rem;
- line-height: 2.15;
- padding-left: .75em;
- padding-right: .75em;
-}
\ No newline at end of file
+ /* Responsive styles */
+ @media (min-width: 1024px) {
+ .sidebar {
+ display: flex;
+ }
+ .mobile-progress {
+ display: none;
+ }
+ }
\ No newline at end of file
diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js
new file mode 100644
index 0000000..2fd6a43
--- /dev/null
+++ b/public/js/va-client-register.js
@@ -0,0 +1,531 @@
+// // Example: Basic client-side validation or UX improvements
+// document.addEventListener("DOMContentLoaded", function () {
+// const form = document.querySelector("form");
+
+// form.addEventListener("submit", async (e) => {
+// e.preventDefault();
+
+// handleSubmit();
+// });
+// });
+
+// function handleSubmit() {
+
+// // Example: collect form data manually
+// const form = document.querySelector("form");
+// const formData = new FormData(form);
+// const payload = { userDetails: {} };
+
+// formData.forEach((value, key) => {
+// // handle nested names like userDetails[firstName]
+// if (key.startsWith("userDetails[")) {
+// const innerKey = key.match(/userDetails\[(.*?)\]/)[1];
+// payload.userDetails[innerKey] = value;
+// } else {
+// payload[key] = value;
+// }
+// });
+// // You could then send it via fetch:
+// fetch("/api/v1/va/client/register", {
+// method: "POST",
+// body: JSON.stringify(payload),
+// })
+// .then((res) => res.json())
+// .then((data) => {
+// console.log("Server Response:", data);
+// alert("Form submitted successfully!");
+// })
+// .catch((err) => console.error("Error:", err));
+// }
+
+const formData = {
+ userDetails: {}
+};
+
+// Current step tracking
+let currentStep = 1;
+
+// const statesByCountry = {
+// 'US': [
+// 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware',
+// 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky',
+// 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
+// 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico',
+// 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
+// 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
+// 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'
+// ],
+// 'CA': [
+// 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador',
+// 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan'
+// ],
+// 'UK': [
+// 'England', 'Scotland', 'Wales', 'Northern Ireland'
+// ],
+// 'AU': [
+// 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia'
+// ]
+// };
+
+// Function to navigate to any step (for tab navigation)
+function goToStep(step) {
+ // Only allow navigation to completed steps or the next step
+ if (step > currentStep && step > getMaxCompletedStep() + 1) {
+ return;
+ }
+
+ // Hide current step
+ document.getElementById(`step-${currentStep}`).classList.remove('active');
+ document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
+ el.classList.remove('active');
+ });
+
+ // Show target step
+ document.getElementById(`step-${step}`).classList.add('active');
+ document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
+ el.classList.add('active');
+ });
+
+ currentStep = step;
+}
+
+// Function to get the maximum completed step
+function getMaxCompletedStep() {
+ const completedSteps = document.querySelectorAll('.step.completed');
+ let maxStep = 0;
+ completedSteps.forEach(step => {
+ const stepNum = parseInt(step.getAttribute('data-step'));
+ if (stepNum > maxStep) maxStep = stepNum;
+ });
+ return maxStep;
+}
+
+// Function to navigate to next step
+function nextStep(step) {
+ // Validate current step before proceeding
+ if (!validateStep(currentStep)) {
+ return;
+ }
+
+ // Save current step data
+ saveStepData(currentStep);
+
+ // Mark current step as completed
+ document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
+ el.classList.add('completed');
+ const stepNumber = el.querySelector('.step-number');
+ stepNumber.textContent = '✓';
+ stepNumber.style.backgroundColor = '#070065';
+ stepNumber.style.borderColor = '#070065';
+ stepNumber.style.color = 'white';
+ });
+
+ // Hide current step
+ document.getElementById(`step-${currentStep}`).classList.remove('active');
+ document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
+ el.classList.remove('active');
+ });
+
+ // Show next step
+ document.getElementById(`step-${step}`).classList.add('active');
+ document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
+ el.classList.add('active');
+ });
+
+ currentStep = step;
+}
+
+// Function to navigate to previous step
+function prevStep(step) {
+ // Hide current step
+ document.getElementById(`step-${currentStep}`).classList.remove('active');
+ document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
+ el.classList.remove('active');
+ });
+
+ // Show previous step
+ document.getElementById(`step-${step}`).classList.add('active');
+ document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
+ el.classList.add('active');
+ });
+
+ currentStep = step;
+}
+
+// Function to validate current step
+function validateStep(step) {
+ let isValid = true;
+ const form = document.getElementById(getFormId(step));
+
+ if (form) {
+ const requiredFields = form.querySelectorAll('[required]');
+ requiredFields.forEach(field => {
+ if (!field.value.trim()) {
+ isValid = false;
+ field.style.border = '1px solid #ef4444';
+ } else {
+ field.style.border = '';
+ }
+ });
+ }
+
+ return isValid;
+}
+
+// Function to get form ID based on step
+function getFormId(step) {
+ switch(step) {
+ case 1: return 'account-form';
+ case 2: return 'business-form';
+ case 3: return 'billing-form';
+ case 4: return 'signatory-form';
+ case 5: return 'payment-form';
+ default: return '';
+ }
+}
+
+// Function to save step data to formData object
+function saveStepData(step) {
+ switch(step) {
+ case 1:
+ formData.userDetails.name = document.getElementById('company-name').value;
+ formData.userDetails.email = document.getElementById('email').value;
+ break;
+ case 2:
+ formData.userDetails.address = document.getElementById('address').value;
+ formData.userDetails.city = document.getElementById('city').value;
+ formData.userDetails.state = document.getElementById('state').value;
+ formData.userDetails.zip = document.getElementById('zip').value;
+ break;
+ case 3:
+ formData.userDetails.billingName = document.getElementById('billing-name').value;
+ formData.userDetails.billingAddress = document.getElementById('billing-address').value;
+ formData.userDetails.billingCity = document.getElementById('billing-city').value;
+ formData.userDetails.billingState = document.getElementById('billing-state').value;
+ formData.userDetails.billingZip = document.getElementById('billing-zip').value;
+ formData.userDetails.billingEmail = document.getElementById('billing-email').value;
+ break;
+ case 4:
+ formData.userDetails.position = document.getElementById('position').value;
+ formData.userDetails.signatoryName = document.getElementById('signatory-name').value;
+ formData.userDetails.signatoryEmail = document.getElementById('signatory-email').value;
+ break;
+ case 5:
+ // Payment method already handled in selectPaymentOption
+ break;
+ case 6:
+ formData.userDetails.paymentMethod = formData.payment?.type || '';
+ break;
+ }
+}
+
+// Function to select payment option
+function selectPaymentOption(type) {
+ // Remove selected class from all options
+ document.querySelectorAll('.payment-option').forEach(option => {
+ option.classList.remove('selected');
+ });
+
+ document.querySelectorAll('.payment-details').forEach(details => {
+ details.classList.remove('active');
+ });
+
+ // Add selected class to clicked option
+ event.currentTarget.classList.add('selected');
+
+ if (type === 'card') {
+ document.getElementById('card-details').classList.add('active');
+ formData.userDetails.paymentMethod = 'card';
+ formData.userDetails.cardNumber = '';
+ formData.userDetails.expiryDate = '';
+ formData.userDetails.securityCode = '';
+ formData.userDetails.cardZip = '';
+ } else if (type === 'bank') {
+ document.getElementById('bank-details').classList.add('active');
+ formData.userDetails.paymentMethod = 'bank';
+ formData.userDetails.bankName = '';
+ formData.userDetails.accountNumber = '';
+ formData.userDetails.routingNumber = '';
+ formData.userDetails.accountType = '';
+ formData.userDetails.accountHolderName = '';
+ }
+}
+
+// Function to submit the form
+async function submitForm() {
+ // Validate current step
+ if (!validateStep(currentStep)) {
+ return;
+ }
+
+ // Save current step data
+ saveStepData(currentStep);
+
+ // Save payment details based on selected method
+ if (formData.userDetails.paymentMethod === 'card') {
+ formData.userDetails.cardNumber = document.getElementById('card-number').value;
+ formData.userDetails.expiryDate = document.getElementById('expiry-date').value;
+ formData.userDetails.securityCode = document.getElementById('security-code').value;
+ formData.userDetails.cardZip = document.getElementById('card-zip').value;
+ } else if (formData.userDetails.paymentMethod === 'bank') {
+ formData.userDetails.bankName = document.getElementById('bank-name').value;
+ formData.userDetails.accountNumber = document.getElementById('account-number').value;
+ formData.userDetails.routingNumber = document.getElementById('routing-number').value;
+ formData.userDetails.accountType = document.getElementById('account-type').value;
+ formData.userDetails.accountHolderName = document.getElementById('account-holder-name').value;
+ }
+
+ // Show loading state
+ const submitBtn = document.querySelector('#payment-form .btn-primary');
+ const originalText = submitBtn.textContent;
+ submitBtn.textContent = 'Submitting...';
+ submitBtn.disabled = true;
+
+ try {
+ // Send data to server using fetch
+ const response = await fetch('/api/v1/va/client/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(formData)
+ });
+
+ if (response.ok) {
+ // Success - move to success step
+ // Mark current step as completed
+ document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
+ el.classList.add('completed');
+ const stepNumber = el.querySelector('.step-number');
+ stepNumber.textContent = '✓';
+ stepNumber.style.backgroundColor = '#070065';
+ stepNumber.style.borderColor = '#070065';
+ stepNumber.style.color = 'white';
+ });
+ nextStep(6);
+ } else {
+ // Handle server errors
+ alert('There was an error submitting your registration. Please try again.');
+ }
+ } catch (error) {
+ // Handle network errors
+ console.error('Error:', error);
+ alert('There was a network error. Please check your connection and try again.');
+ } finally {
+ // Restore button state
+ submitBtn.textContent = originalText;
+ submitBtn.disabled = false;
+ }
+}
+
+// Function to reset the form
+function resetForm() {
+ // Reset all form fields
+ document.querySelectorAll('input').forEach(input => {
+ input.value = '';
+ input.style.border = '';
+ });
+
+ document.querySelectorAll('select').forEach(select => {
+ select.value = '';
+ select.style.border = '';
+ });
+
+ // Reset payment selection
+ document.querySelectorAll('.payment-option').forEach(option => {
+ option.classList.remove('selected');
+ });
+
+ document.querySelectorAll('.payment-details').forEach(details => {
+ details.classList.remove('active');
+ });
+
+ // Reset form data
+ formData.userDetails = {};
+
+ document.querySelectorAll('.step').forEach(step => {
+ step.classList.remove('completed', 'active');
+ const stepNumber = step.querySelector('.step-number');
+ if (stepNumber) {
+ stepNumber.textContent = step.getAttribute('data-step');
+ stepNumber.style.backgroundColor = '';
+ stepNumber.style.borderColor = '#a1a1aa';
+ stepNumber.style.color = '#a1a1aa';
+ }
+ });
+
+ // Go back to step 1
+ document.getElementById('step-1').classList.add('active');
+ document.querySelectorAll('.step[data-step="1"]').forEach(el => {
+ el.classList.add('active');
+ });
+
+ currentStep = 1;
+}
+
+// Function to fetch states from API
+async function fetchStates(countryCode) {
+ try {
+ // Replace with your actual API endpoint
+ const response = await fetch(`/api/states/${countryCode}`);
+ if (response.ok) {
+ const states = await response.json();
+ return states;
+ } else {
+ console.error('Failed to fetch states');
+ return [];
+ }
+ } catch (error) {
+ console.error('Error fetching states:', error);
+ return [];
+ }
+}
+
+// Function to populate states dropdown
+function populateStates(countryCode, selectElementId) {
+ const selectElement = document.getElementById(selectElementId);
+ const states = statesByCountry[countryCode] || [];
+
+ // Clear existing options except the first empty one
+ while (selectElement.options.length > 1) {
+ selectElement.remove(1);
+ }
+
+ // Add states as options
+ states.forEach(state => {
+ const option = document.createElement('option');
+ option.value = state;
+ option.textContent = state;
+ selectElement.appendChild(option);
+ });
+}
+
+// Function to detect user location
+function detectLocation() {
+ // Show loading indicators
+ document.getElementById('state-loading').style.display = 'block';
+
+ // Using a free IP geolocation API
+ fetch('https://ipapi.co/json/')
+ .then(response => response.json())
+ .then(data => {
+ // Hide loading indicators
+ document.getElementById('state-loading').style.display = 'none';
+
+ // Populate city and state based on detected location
+ document.getElementById('city').value = data.city || '';
+ document.getElementById('zip').value = data.postal || '';
+
+ // Populate states dropdown for the detected country
+ populateStates(data.country_code, 'state');
+
+ // Set the detected state
+ if (data.region) {
+ document.getElementById('state').value = data.region;
+ }
+
+ // Also populate billing states dropdown
+ populateStates(data.country_code, 'billing-state');
+ })
+ .catch(error => {
+ console.error('Error detecting location:', error);
+
+ // Hide loading indicators
+ document.getElementById('state-loading').style.display = 'none';
+
+ // Fallback to US states if detection fails
+ populateStates('US', 'state');
+ populateStates('US', 'billing-state');
+ });
+}
+
+
+// Initialize form interactions
+document.addEventListener('DOMContentLoaded', function() {
+ // Add input event listeners to handle label animation
+ document.querySelectorAll('.input-field').forEach(input => {
+ input.addEventListener('input', function() {
+ if (this.value) {
+ this.nextElementSibling.style.top = '2px';
+ this.nextElementSibling.style.fontSize = '12px';
+ this.nextElementSibling.style.transform = 'none';
+ } else {
+ this.nextElementSibling.style.top = '50%';
+ this.nextElementSibling.style.fontSize = '14px';
+ this.nextElementSibling.style.transform = 'translateY(-50%)';
+ }
+ });
+
+ input.addEventListener('focus', function() {
+ this.nextElementSibling.style.top = '2px';
+ this.nextElementSibling.style.fontSize = '12px';
+ this.nextElementSibling.style.transform = 'none';
+ });
+
+ input.addEventListener('blur', function() {
+ if (!this.value) {
+ this.nextElementSibling.style.top = '50%';
+ this.nextElementSibling.style.fontSize = '14px';
+ this.nextElementSibling.style.transform = 'translateY(-50%)';
+ }
+ });
+ });
+
+ // Handle select field label animation
+ document.querySelectorAll('.select-field').forEach(select => {
+ select.addEventListener('change', function() {
+ if (this.value) {
+ this.nextElementSibling.style.top = '2px';
+ this.nextElementSibling.style.fontSize = '12px';
+ this.nextElementSibling.style.transform = 'none';
+ } else {
+ this.nextElementSibling.style.top = '50%';
+ this.nextElementSibling.style.fontSize = '14px';
+ this.nextElementSibling.style.transform = 'translateY(-50%)';
+ }
+ });
+
+ select.addEventListener('focus', function() {
+ this.nextElementSibling.style.top = '2px';
+ this.nextElementSibling.style.fontSize = '12px';
+ this.nextElementSibling.style.transform = 'none';
+ });
+
+ select.addEventListener('blur', function() {
+ if (!this.value) {
+ this.nextElementSibling.style.top = '50%';
+ this.nextElementSibling.style.fontSize = '14px';
+ this.nextElementSibling.style.transform = 'translateY(-50%)';
+ }
+ });
+ });
+
+ // Handle use business address checkbox
+ document.getElementById('use-business-address').addEventListener('change', function() {
+ if (this.checked) {
+ document.getElementById('billing-name').value = formData.userDetails.name || '';
+ document.getElementById('billing-address').value = formData.userDetails.address || '';
+ document.getElementById('billing-city').value = formData.userDetails.city || '';
+ document.getElementById('billing-state').value = formData.userDetails.state || '';
+ document.getElementById('billing-zip').value = formData.userDetails.zip || '';
+
+ // Trigger input events to update labels
+ document.querySelectorAll('#billing-form .input-field').forEach(input => {
+ input.dispatchEvent(new Event('input'));
+ });
+
+ // Trigger change event for select
+ document.getElementById('billing-state').dispatchEvent(new Event('change'));
+ }
+ });
+
+ // Handle use account email checkbox
+ document.getElementById('use-account-email').addEventListener('change', function() {
+ if (this.checked) {
+ document.getElementById('billing-email').value = formData.account.email || '';
+ document.getElementById('billing-email').dispatchEvent(new Event('input'));
+ }
+ });
+ // Detect user location when the page loads
+ detectLocation();
+});
\ No newline at end of file
diff --git a/src/controllers/va/vaClient/vaClient.controller.js b/src/controllers/va/vaClient/vaClient.controller.js
new file mode 100644
index 0000000..b29fdec
--- /dev/null
+++ b/src/controllers/va/vaClient/vaClient.controller.js
@@ -0,0 +1,43 @@
+import { ClientModel } from "../../../models/vaModel.js";
+
+// Register a new VA
+async function va_register_client(req, res) {
+ try {
+ const payload = req.body ? JSON.parse(req.body) : {};
+ const { userDetails } = payload;
+
+ if (!userDetails || !userDetails.email || !userDetails.name) {
+ return res.status(400).json({ success: false, message: "Missing required fields" });
+ }
+
+ // ✅ Check if user already exists by email
+ const existingUser = await ClientModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() });
+ if (existingUser) {
+ return res.status(409).json({ success: false, message: "User already exists with this email" });
+ }
+
+ // sanitize/construct allowed fields
+ const toCreate = {
+ userDetails: {
+ name: userDetails.name,
+ email: userDetails.email,
+ dob: userDetails.dob || null,
+ bankName: userDetails.bankName || null,
+ accountHolderName: userDetails.accountHolderName || null,
+ accountNumber: userDetails.accountNumber || null,
+ routingNumber: userDetails.routingNumber || null,
+ },
+ vaIds: payload.vaIds || [],
+ projectIds: payload.projectIds || [],
+ };
+
+ const created = await ClientModel.create(toCreate);
+
+ return res.status(201).json({ success: true, data: created });
+ } catch (error) {
+ console.error("va_register_client error:", error);
+ return res.status(500).json({ success: false, message: "Internal Server Error" });
+ }
+}
+
+export { va_register_client };
\ No newline at end of file
diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js
index 360aaed..9354858 100644
--- a/src/controllers/va/vaProfile/vaRegister.controller.js
+++ b/src/controllers/va/vaProfile/vaRegister.controller.js
@@ -4,7 +4,7 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA
async function va_register(req, res) {
try {
- const payload = req.body || {};
+ const payload = req.body ? JSON.parse(req.body) : {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index 15e9105..3b84daa 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -73,16 +73,36 @@ const projectSchema = new Schema({
],
});
+/* ---------------------- Client Details Schema ---------------------- */
+const clientDetailsSchema = new Schema({
+ name: {
+ type: String,
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ dob: {
+ type: String,
+ },
+ bankName: {
+ type: String,
+ },
+ accountHolderName: {
+ type: String,
+ },
+ accountNumber: {
+ type: String,
+ },
+ routingNumber: {
+ type: String,
+ },
+});
+
/* ---------------------- Client Schema ---------------------- */
const clientSchema = new Schema({
- userDetails: {
- name: {
- type: String,
- },
- email: {
- type: String,
- },
- },
+ userDetails: clientDetailsSchema,
vaIds: [
{
type: Types.ObjectId,
@@ -95,9 +115,14 @@ const clientSchema = new Schema({
ref: "Project",
},
],
- totalLoggedHours: {
- type: Number,
+ accessToken: {
+ type: String,
+ default: null
},
+ refreshToken: {
+ type: String,
+ default: null
+ }
});
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index 2baadd2..abe7a84 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -3,6 +3,7 @@ const vaRoutes = express.Router();
import { va_username_callback } from "../../controllers/va/vaUsername.controller.js";
import { va_register } from "../../controllers/va/vaProfile/vaRegister.controller.js";
+import { va_register_client } from "../../controllers/va/vaClient/vaClient.controller.js";
import { va_profile_callback } from "../../controllers/va/vaProfile/vaProfile.controller.js";
import { va_profile_update_callback } from "../../controllers/va/vaProfile/vaProfileUpdate.controller.js";
import { va_login_callback } from "../../controllers/va/vaAuth/vaLogin.controller.js";
@@ -27,21 +28,22 @@ vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA
+vaRoutes.post("/client/register", va_register_client); // Register a new VA
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
-vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
+vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC)
-// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31
+// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
vaRoutes.get("/search", va_filter_search);
-vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID
-vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback
+vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC)
+vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
diff --git a/src/server.js b/src/server.js
index f71f4ed..44efca8 100644
--- a/src/server.js
+++ b/src/server.js
@@ -36,6 +36,10 @@ app.get('/varegister', (req, res) => {
res.render('va_register', { title: "VA Registration" });
});
+app.get('/clientregister', (req, res) => {
+ res.render('va_client_register', { title: "Client Registration" });
+});
+
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})
diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs
new file mode 100644
index 0000000..d8742a4
--- /dev/null
+++ b/views/va_client_register.ejs
@@ -0,0 +1,416 @@
+
+
+
+
+
+ VA Registration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
1
+
+ Create Account
+
+ -
+
2
+ Business Info
+
+ -
+
3
+ Billing
+
+ -
+
4
+ Signatory
+
+ -
+
5
+ Payment
+
+ -
+
6
+ Done
+
+
+
+
+
+
+
+
+
+
+