Compare commits

...
Author SHA1 Message Date
Joaquin 480ea236ec fix/small fixes to page layouts 2025-05-16 00:41:17 -07:00
Joaquin 6ecd1ee848 implemented more page 2025-05-16 00:22:13 -07:00
Joaquin c839d023a7 fix/removed scrolling from login/signup pages 2025-05-15 21:46:58 -07:00
Joaquin cfcfd7778d feature/more page in nav bar completed 2025-05-15 21:30:58 -07:00
Joaquin f7377f6844 feature/most functionality created 2025-05-14 12:42:50 -07:00
Joaquin 370d5d3292 merge dev into feature/challenge1_error_message 2025-05-14 09:41:39 -07:00
Joaquin c12bbe8336 removed debugging code 2025-05-14 09:40:06 -07:00
Joaquin 0d59c60a28 merge dev into feature/challenge1_error_message 2025-05-14 09:39:12 -07:00
Joaquin 5e2ac94212 fully implemented error handling for challenge 1 2025-05-14 09:37:09 -07:00
SowinskiBraeden f5bc2de24a Merge pull request #26 from JoaquinPar/fix/session_issues
fix/login signup logout session bugs
2025-05-14 09:36:38 -07:00
SowinskiBraeden 3771288123 fix/login+signup+logout session bugs 2025-05-14 09:34:32 -07:00
Nícolas Agostini ccae6a51ff Merge pull request #25 from JoaquinPar/feature/ai-challenge2
Feature/ai challenge2
2025-05-13 14:19:17 -07:00
nicoagostini 206b31e785 feature/ Surprise challenge #2 - AI pun button added and basic suggestion integrated in the plans 2025-05-13 14:18:21 -07:00
Joaquin 8166245ab1 feature/basic functionality added 2025-05-13 13:17:18 -07:00
Joaquin Paredes 79f0caa3c1 Merge pull request #23 from JoaquinPar/dev
End of Sprint 2
2025-05-10 16:35:08 -07:00
Nícolas Agostini 4f73959cfb Merge pull request #22 from JoaquinPar/fix/16-hovering-hides-progress
Progress bar background changed to black to avoid disappearing
2025-05-09 15:54:45 -07:00
nicoagostini 6444d7afa7 Progress bar background changed to black to avoid disappearing 2025-05-09 15:53:40 -07:00
Nícolas Agostini daa984a48f Merge pull request #21 from JoaquinPar/fix/questionnaireFix
Issue 19: Questionnaire more than once, fixed
2025-05-09 14:04:31 -07:00
nicoagostini 383bd15263 Issue 19: Questionnaire more than once, fixed 2025-05-09 14:03:35 -07:00
knighthawk4227 1e44e8ddfb moving resetLink post to forgotpass.js 2025-05-09 11:25:36 -07:00
knighthawk4227 cc875b4e22 Merge branch 'dev' of github.com:JoaquinPar/2800-202510-BBY14 into dev 2025-05-09 11:24:03 -07:00
knighthawk4227 f3dbbebb43 moving resetLink post to forgotpass.js 2025-05-09 11:18:25 -07:00
Joaquin 4ee885ac57 fix/reorganized scripts called in ejs files 2025-05-09 11:06:13 -07:00
knighthawk4227 f3b121fc71 Merge branch 'feature/forgotPass' into dev 2025-05-09 11:03:02 -07:00
knighthawk4227 07824e6355 merge cnflicts with dev 2025-05-09 11:02:31 -07:00
Joaquin 22850ab2a2 fix/removed redundant code and fixed UI challenge 1 problem 2025-05-09 10:55:02 -07:00
SowinskiBraeden 69fd776c0f fix/enhanced user input validation 2025-05-09 10:29:20 -07:00
knighthawk4227 671b67be9d Merge branch 'dev' into feature/forgotPass 2025-05-09 10:27:33 -07:00
knighthawk4227 99b36d3370 fixing conflict with dashboard.ejs 2025-05-09 10:26:18 -07:00
SowinskiBraeden 1f742d41c3 fix/signup using existing email 2025-05-09 10:23:34 -07:00
SowinskiBraeden 18bee21941 Merge branch 'dev' of github.com:JoaquinPar/2800-202510-BBY14 into dev 2025-05-09 10:07:16 -07:00
knighthawk4227 1fcd12c6da fixing merge conflicts 2025-05-09 10:04:24 -07:00
SowinskiBraeden 0a9c176274 fix/default asset icon creation 2025-05-09 10:00:51 -07:00
knighthawk4227 1587f363f7 fixing reset link session 2025-05-09 09:50:38 -07:00
SowinskiBraeden 540e191d27 refactor/remove debug statements 2025-05-09 09:49:14 -07:00
SowinskiBraeden 1e01e022bf Merge pull request #14 from JoaquinPar/feature/asset-select-icon
update+fix/asset icons + load geolocation scripts
2025-05-08 23:32:40 -07:00
knighthawk4227 c64da1de46 fixing conflicts and merging dev 2025-05-08 10:44:38 -07:00
knighthawk4227 7a5d2e7490 fixing conflicts merging dev into this 2025-05-08 10:40:31 -07:00
knighthawk4227 fc39bcf693 nodemailer dependencies 2025-05-08 10:32:10 -07:00
knighthawk4227 29d682c4e8 fixing joi validation 2025-05-07 19:26:00 -07:00
knighthawk4227 15dd4e0d20 forgot password validatation using joi 2025-05-07 19:09:25 -07:00
knighthawk4227 cb59e86d8a fixing merge conflicts and forgot password functionality 2025-05-07 17:44:06 -07:00
knighthawk4227 088f9ea55a reset pass logic 2025-05-07 13:03:26 -07:00
knighthawk4227 617fc8d877 indentation of dashboard 2025-05-07 10:49:08 -07:00
knighthawk4227 f0b1089e15 routes for forgotPass 2025-05-07 00:52:01 -07:00
knighthawk4227 a40b6d514b forgot password js 2025-05-06 23:33:05 -07:00
Joaquin Paredes 5819b0d151 Merge pull request #1 from JoaquinPar/dev
Merging Dev into Main Sprint #0
2025-04-29 16:07:03 -07:00
30 changed files with 1372 additions and 320 deletions

No files matched your search

+1
View File
@@ -5,3 +5,4 @@ SECRET='123456789'
GEOLOCATION_API='api_key' GEOLOCATION_API='api_key'
EMAIL_USER=mail@example.com EMAIL_USER=mail@example.com
EMAIL_PASS='password' EMAIL_PASS='password'
GOOGLE_API_KEY='api_key'
+3
View File
@@ -1,6 +1,9 @@
# Environment variables # Environment variables
.env .env
#vscode files
.vscode
# Node modules # Node modules
node_modules/ node_modules/
+51 -7
View File
@@ -3,6 +3,7 @@ const MongoStore = require("connect-mongo");
const session = require("express-session"); const session = require("express-session");
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const bcrypt = require('bcrypt');
const joi = require('joi'); const joi = require('joi');
require('dotenv').config(); require('dotenv').config();
@@ -11,7 +12,7 @@ const port = process.env.PORT || 3000;
const mongoURI = process.env.MONGO_URI; const mongoURI = process.env.MONGO_URI;
const database = process.env.DATABASE; // Database name const database = process.env.DATABASE; // Database name
const secret = process.env.SECRET || "123-secret-xyz"; const secret = process.env.SECRET || "123-secret-xyz";
/*** Sessions ***/ /*** Sessions ***/
app.use(session({ app.use(session({
@@ -37,11 +38,11 @@ let assets;
let plans; let plans;
async function initDatabase() { async function initDatabase() {
const db = await connectMongo(mongoURI, database); const db = await connectMongo(mongoURI, database);
// For any collection, init here // For any collection, init here
users = await getCollection(db, "users"); users = await getCollection(db, "users");
assets = await getCollection(db, "assets"); assets = await getCollection(db, "assets");
plans = await getCollection(db, "plans"); plans = await getCollection(db, "plans");
} }
/*** ROUTINGS ***/ /*** ROUTINGS ***/
@@ -69,10 +70,51 @@ app.get('/login', (req, res) => {
}); });
app.get('/aboutUs', (req, res) => { app.get('/aboutUs', (req, res) => {
res.render('aboutUs'); if (!req.session.authenticated) {
res.render('aboutUs', {
geoData: undefined
});
return res.status(status.Ok);
} else {
res.render('aboutUs', {
user: req.session.user,
geoData: req.session.geoData
});
return res.status(status.Ok);
}
});
app.get('/forgotPassword', (req, res) => {
const error = req.session.error;
const reset = req.session.reset;
delete req.session.reset;
delete req.session.error;
res.render('forgotPass', { error: error, reset: reset });
return res.status(status.Ok); return res.status(status.Ok);
}); });
// Reset with token given to user via email
app.get('/reset/:token', async (req, res) => {
const token = req.params.token;
const user = await users.findOne({
resetToken: token,
resetTokenExpires: { $gt: Date.now() },
});
if (!user) {
req.session.error = 'reset link not valid or has expired';
return res.redirect('/forgotPassword');
}
const error = req.session.error;
delete req.session.error;
res.render('resetPass', {
token: token,
errMessage: error,
});
});
// 404 handler - keep the actual notFound route please // 404 handler - keep the actual notFound route please
// REALLY DONT DELETE THIS // REALLY DONT DELETE THIS
app.get('/notFound', (req, res) => { app.get('/notFound', (req, res) => {
@@ -86,7 +128,9 @@ initDatabase().then(() => {
// Import authentication handler // Import authentication handler
app.use(require("./src/auth/authentication")(users)); app.use(require("./src/auth/authentication")(users));
app.use(require('./src/auth/forgotPass')(users));
// Import middleware & apply to user routes // Import middleware & apply to user routes
const middleware = require("./src/auth/middleware")(users); const middleware = require("./src/auth/middleware")(users);
app.use(require('./src/router/user')(middleware, users, plans, assets)); app.use(require('./src/router/user')(middleware, users, plans, assets));
@@ -96,7 +140,7 @@ initDatabase().then(() => {
res.render('notFound'); res.render('notFound');
return res.status(status.NotFound); return res.status(status.NotFound);
}); });
// Start app // Start app
app.listen(port, () => { app.listen(port, () => {
console.log(`Server listening on port ${port}`); console.log(`Server listening on port ${port}`);
+523 -1
View File
@@ -9,19 +9,38 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@google/genai": "^0.14.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"crypto": "^1.0.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongodb": "^6.16.0" "mongodb": "^6.16.0",
"nodemailer": "^7.0.3",
"sweetalert2": "^11.21.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
}, },
"node_modules/@google/genai": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.14.0.tgz",
"integrity": "sha512-nxXbRJZjCUBlPlfTXBMCgkSEpojcLzjxT2Ye3CDiqlXaiSscZ046bDEzztq6ONxUT3BfqHcGsQLhpbR5DU1Mcg==",
"dependencies": {
"google-auth-library": "^9.14.2",
"test-server-sdk": "^0.2.1",
"ws": "^8.18.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -87,6 +106,15 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@types/node": {
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
"optional": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/webidl-conversions": { "node_modules/@types/webidl-conversions": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -102,6 +130,15 @@
"@types/webidl-conversions": "*" "@types/webidl-conversions": "*"
} }
}, },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -209,12 +246,46 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@@ -229,6 +300,14 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -300,6 +379,19 @@
"node": ">=16.20.1" "node": ">=16.20.1"
} }
}, },
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": {
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -415,6 +507,17 @@
"color-support": "bin.js" "color-support": "bin.js"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -483,6 +586,12 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -500,6 +609,14 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -550,6 +667,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -586,6 +711,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -616,6 +749,20 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -713,6 +860,38 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -773,6 +952,58 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -866,6 +1097,71 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"optional": true,
"peer": true,
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/gcp-metadata/node_modules/gaxios": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
"optional": true,
"peer": true,
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^5.0.0",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -903,6 +1199,20 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -937,6 +1247,43 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -949,6 +1296,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -970,6 +1329,20 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1123,6 +1496,17 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jake": { "node_modules/jake": {
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
@@ -1154,6 +1538,33 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kruptein": { "node_modules/kruptein": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz", "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz",
@@ -1430,6 +1841,15 @@
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@@ -1598,6 +2018,11 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1624,6 +2049,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pstree.remy": { "node_modules/pstree.remy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -1631,6 +2061,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1990,6 +2429,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sweetalert2": {
"version": "11.21.0",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.21.0.tgz",
"integrity": "sha512-fiEK7SqRY/QD/wC2uqEHlfYGZ7qe2UcyQbJpbpj4YRVqplBgcI+euPZLZL+evLINcvbtXmL1SFUdZHKqBHGAAQ==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/tar": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -2007,6 +2455,17 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/test-server-sdk": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/test-server-sdk/-/test-server-sdk-0.2.1.tgz",
"integrity": "sha512-o9F0HEBZbdDMwZ5xwSafkVb0C2nlZ4hq/g/aMgyLniTs69J7zoQJzF5eA3ZN6MPRoYPhy/QsQ/BuRtqaXzawvw==",
"hasInstallScript": true,
"dependencies": {
"axios": "^1.6.0",
"extract-zip": "^2.0.1",
"tar": "^6.2.0"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2084,6 +2543,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"optional": true
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -2099,6 +2564,18 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -2145,11 +2622,56 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC" "license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zod": {
"version": "3.24.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.5",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
"integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
"peerDependencies": {
"zod": "^3.24.1"
}
} }
} }
} }
+5 -1
View File
@@ -18,14 +18,18 @@
}, },
"homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme",
"dependencies": { "dependencies": {
"@google/genai": "^0.14.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"crypto": "^1.0.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongodb": "^6.16.0" "mongodb": "^6.16.0",
"nodemailer": "^7.0.3",
"sweetalert2": "^11.21.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
+47 -14
View File
@@ -12,15 +12,23 @@ module.exports = (users) => {
const router = require("express").Router(); const router = require("express").Router();
router.get("/logout", (req, res) => { router.get("/logout", (req, res) => {
req.session.destroy(); req.session.destroy((err) => {
return res.redirect('/login'); if (err) {
console.error("Failed to destroy session: ", err);
res.status(status.InternalServerError);
return res.redirect("back");
}
res.status(status.Ok);
return res.redirect('/login');
});
}); });
router.post("/login", async (req, res) => { router.post("/login", async (req, res) => {
const credentialSchema = joi.object({ const credentialSchema = joi.object({
email: joi.string().email().required(), email: joi.string().email().required(),
password: joi.string().max(20).required(), password: joi.string().alphanum().max(20).required(),
}); });
const valid = credentialSchema.validate(req.body); const valid = credentialSchema.validate(req.body);
@@ -46,19 +54,28 @@ module.exports = (users) => {
req.session.authenticated = true; req.session.authenticated = true;
req.session.userId = user._id; req.session.userId = user._id;
req.session.email = req.body.email;
req.session.errMessage = ""; req.session.errMessage = "";
res.redirect("/home");
return res.status(status.Ok); req.session.save((err) => {
if (err) {
console.error("Failed to save session: ", err);
req.session.errMessage = "Failed to save session. Please try again.";
res.status(status.InternalServerError);
return res.redirect("/login");
}
res.status(status.Ok);
return res.redirect("/home");
});
}); });
}); });
router.post("/signup", async (req, res) => { router.post("/signup", async (req, res) => {
const userSchema = joi.object({ const userSchema = joi.object({
email: joi.string().email().required(), email: joi.string().email().required(),
name: joi.string().alphanum().max(20).required(), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20).required(),
password: joi.string().max(20).min(8).required(), password: joi.string().alphanum().max(20).min(8).required(),
repassword: joi.string().max(20).min(8).required(), repassword: joi.string().alphanum().max(20).min(8).required(),
}); });
const valid = userSchema.validate(req.body); const valid = userSchema.validate(req.body);
@@ -69,6 +86,13 @@ module.exports = (users) => {
return res.redirect("/signup"); return res.redirect("/signup");
} }
let exists = await users.findOne({ email: req.body.email }).then((exists) => exists);
if (exists) {
req.session.errMessage = "Email already in use";
res.status(status.BadRequest);
return res.redirect("/signup");
}
if (req.body.password != req.body.repassword) { if (req.body.password != req.body.repassword) {
req.session.errMessage = "Passwords must match"; req.session.errMessage = "Passwords must match";
res.status(status.BadRequest); res.status(status.BadRequest);
@@ -84,17 +108,26 @@ module.exports = (users) => {
financialData: false, financialData: false,
}).then((results, err) => { }).then((results, err) => {
if (err) { if (err) {
console.error(err); console.error("Error creating user on signup: ", err);
res.session.errMessage = "Internal server error"; res.session.errMessage = "Internal server error";
return res.status(status.InternalServerError).redirect("/signup"); return res.status(status.InternalServerError).redirect("/signup");
} }
req.session.authenticated = true; req.session.authenticated = true;
req.session.email = req.body.email;
req.session.userId = results.insertedId; req.session.userId = results.insertedId;
req.session.errMessage = ""; req.session.errMessage = "";
return res.status(status.Ok).redirect("/home");
req.session.save((err) => {
if (err) {
console.error("Failed to save session: ", err);
req.session.errMessage = "Please login";
res.status(status.Ok);
return res.redirect("/login");
}
res.status(status.Ok);
return res.redirect("/home");
});
}); });
}); });
+119
View File
@@ -0,0 +1,119 @@
const express = require('express');
const crypto = require('crypto');
const joi = require('joi');
const nodeMail = require('nodemailer');
const bcrypt = require('bcrypt');
require('dotenv').config();
const PORT = process.env.PORT;
const transporter = nodeMail.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
}
});
module.exports = (users) => {
const router = express.Router();
router.post('/auth/resetPass', async (req, res) => {
const resetSchema = joi.object({
email: joi.string().email().required(),
});
req.session.error = '';
req.session.reset = '';
const valid = resetSchema.validate(req.body);
if (valid.error) {
req.session.error = 'invalid email';
return res.redirect('/forgotPassword')
}
const { email } = req.body;
const user = await users.findOne({ email });
if (!user) {
req.session.error = 'No user found'
return res.redirect('/forgotPassword');
}
const token = crypto.randomBytes(32).toString('hex');
console.log(`The reset token is ${token}`)
const expiration = Date.now() + 360000;
await users.updateOne({ email }, {
$set: { resetToken: token, resetTokenExpires: expiration }
});
const resetUrl = `http://localhost:${PORT}/reset/${token}`;
const mailSend = {
from: process.env.EMAIL_USER,
to: email,
subject: 'Password reset',
text: `reset your password here ${resetUrl} this link will expire within 1 hour`,
};
try {
await transporter.sendMail(mailSend);
req.session.reset = 'Reset link sent Check your email';
res.redirect('/forgotPassword');
} catch (err) {
console.log('there was an error', err);
res.status(500).send('email failed to send try again');
}
});
router.post('/resetLink', async (req, res) => {
const { token, password, confirmPassword, } = req.body;
const passwordSchema = joi.object({
password: joi.string().max(20).required(),
confirmPassword: joi.string().max(20).required(),
});
const valid = passwordSchema.validate({ password, confirmPassword });
if (valid.error) {
console.log("houston we have a problem"); // nice
req.session.error = 'Invalid input';
res.status(status.BadRequest);
return res.redirect(`/reset/${token}`);
}
if (!token || !password || !confirmPassword) {
req.session.error = 'field may be missing';
return res.redirect(`/reset/${token}`);
}
if (password !== confirmPassword) {
req.session.error = 'passwords do not match';
return res.redirect(`/reset/${token}`);
}
const user = await users.findOne({
resetToken: token,
resetTokenExpires: { $gt: Date.now() },
});
if (!user) {
req.session.error = 'Reset link is invalid.';
return res.redirect(`/reset`);
}
const hashPassword = await bcrypt.hash(password, 12);
await users.updateOne(
{
email: user.email
},
{
$set: {
password: hashPassword,
resetToken: '',
resetTokenExpires: 0,
},
}
);
req.session.success = 'Password has been reset';
res.redirect('/login');
});
return router;
}
+35 -7
View File
@@ -1,5 +1,6 @@
const status = require("../util/statuses"); const status = require("../util/statuses");
const session = require("express-session"); const session = require("express-session");
const ObjectId = require("mongodb").ObjectId;
// Get all names of user routes // Get all names of user routes
let userRouter = require("../router/user")((req, res, next) => next(), null, null, null); let userRouter = require("../router/user")((req, res, next) => next(), null, null, null);
@@ -18,26 +19,53 @@ const createMiddleware = (users) => {
return res.status(status.NotFound).redirect("/notFound"); return res.status(status.NotFound).redirect("/notFound");
} }
if (!req.session.authenticated || !req.session.email) { if (!req.session.authenticated || !req.session.userId) {
req.session.errMessage = "Please login to view that resource"; req.session.errMessage = "Please login to view that resource";
res.redirect("/login"); res.redirect("/login");
return res.status(status.Unauthorized); return res.status(status.Unauthorized);
} }
if (!req.session.user) { if (!req.session.user) {
let user = await users.findOne({ "email": req.session.email }).then((user) => user); let user = await users.findOne({ _id: new ObjectId(req.session.userId) }).then((user) => user);
if (!user) { if (!user) {
req.session.errMessage = "User not found"; return req.session.destroy((err) => {
res.redirect("/login"); if (err) {
return res.status(status.Unauthorized); console.error("Failed to destroy session: ", err);
req.session.errMessage = "Failed to logout. Please try again";
res.status(status.InternalServerError);
return res.redirect("/home");
}
req.session.errMessage = "User not found";
res.status(status.NotFound);
res.redirect("/login");
});
} }
req.session.user = user; req.session.user = user;
return req.session.save((err) => {
if (err) {
console.error("Failed to save session: ", err);
return req.session.destroy((err) => {
req.session.errMessage = "An error occured, please login again.";
if (err) {
console.error("Failed to destroy session: ", err);
}
res.status(status.InternalServerError);
res.redirect("/login");
});
}
next();
});
} }
next(); next();
}; };
} };
module.exports = createMiddleware; module.exports = createMiddleware;
+48 -35
View File
@@ -5,40 +5,47 @@ function getLocation() {
} }
function update(data) { function update(data) {
document.getElementById("loading").style = "display: none"; if (data.data.message != "error") {
document.getElementById("currencyExchange").style = "display: flex"; document.getElementById("loading").style = "display: none";
document.getElementById("currencyExchange").style = "display: flex";
document.getElementById("yourFlag").src = `/static/svgs/flags/${data.data.country}.svg`
document.getElementById("yourFlag").alt = data.data.country;
document.getElementById("flagTag").innerHTML = `(${data.data.country})`;
document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`;
document.getElementById("exchangeFlag").alt = "USD";
document.getElementById("exFlagTag").innerHTML = "(USD)";
document.getElementById("dropdown-country-button").value = data.data.toCurrencyRates["USD"]; document.getElementById("yourFlag").src = `/static/svgs/flags/${data.data.country}.svg`
document.getElementById("yourFlag").alt = data.data.country;
updateExchange(document.getElementById("dropdown-country-button").value); document.getElementById("flagTag").innerHTML = `(${data.data.country})`;
const dropdown = document.getElementById("dropdown"); document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`;
let countries = Object.keys(data.data.toCurrencyRates); document.getElementById("exchangeFlag").alt = "USD";
document.getElementById("exFlagTag").innerHTML = "(USD)";
countries.forEach(item => {
if (dropdown.querySelector(`button[value="${data.data.toCurrencyRates[item]}"]`)) { document.getElementById("dropdown-country-button").value = data.data.toCurrencyRates["USD"];
return;
} updateExchange(document.getElementById("dropdown-country-button").value);
const listItem = document.createElement("li"); const dropdown = document.getElementById("dropdown");
listItem.innerHTML = `<button onclick="switchButton(this)" type="button" value="${data.data.toCurrencyRates[item]}" class="country Button inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"> let countries = Object.keys(data.data.toCurrencyRates);
<span class="inline-flex items-center">
<img src="/static/svgs/flags/${item}.svg" class="h-4 w-4 me-2" alt="${item}"> (${item}) countries.forEach(item => {
</span> if (dropdown.querySelector(`button[value="${data.data.toCurrencyRates[item]}"]`)) {
</button>`; return;
}
dropdown.appendChild(listItem);
}); const listItem = document.createElement("li");
listItem.innerHTML = `<button onclick="switchButton(this)" type="button" value="${data.data.toCurrencyRates[item]}" class="country Button inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
// console.log(data); <span class="inline-flex items-center">
<img src="/static/svgs/flags/${item}.svg" class="h-4 w-4 me-2" alt="${item}"> (${item})
</span>
</button>`;
dropdown.appendChild(listItem);
});
} else {
document.getElementById("loading").style = "display: none";
document.getElementById("exFrom").style = "display: none";
document.getElementById("dropdown-country-button").style = "display: none";
document.getElementById("currencyExchange").style = "display: flex";
document.getElementById("exchange").innerHTML = "Country not supported";
document.getElementById("exchange").className = "block rounded-lg text-center p-2.5 w-50 z-20 text-sm border bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-blue-500";
}
} }
function switchButton(clickedButton) { function switchButton(clickedButton) {
@@ -62,10 +69,16 @@ async function getLatestExchange(position) {
let lon = position.coords.longitude; let lon = position.coords.longitude;
const res = await fetch(`/exRates/${lat}/${lon}`); const res = await fetch(`/exRates/${lat}/${lon}`);
const data = await res.json(); const data = await res.json();
update(data); update(data);
} }
function error(err) { function error(err) {
console.error("Geolocation error: ", err); const data = {
data: {
message: "error"
}
}
update(data);
} }
+125 -108
View File
@@ -1,5 +1,6 @@
const getRates = require("../util/exchangeRate"); const getRates = require("../util/exchangeRate");
const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations");
const suggestions = require("../util/suggestions");
const status = require("../util/statuses"); const status = require("../util/statuses");
const ObjectId = require('mongodb').ObjectId; const ObjectId = require('mongodb').ObjectId;
const session = require("express-session"); const session = require("express-session");
@@ -79,14 +80,16 @@ module.exports = (middleware, users, plans, assets) => {
}; };
} }
res.render('dashboard', { user: req.user, geoData: req.session.geoData }); res.render('dashboard', {
user: req.session.user,
geoData: req.session.geoData });
return res.status(status.Ok); return res.status(status.Ok);
}); });
router.get('/assets', async (req, res) => { router.get('/assets', async (req, res) => {
let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).toArray();
res.render('assets', { res.render('assets', {
user: req.session.user, user: req.session.user,
errMessage: req.session.errMessage, errMessage: req.session.errMessage,
assets: userAssets, assets: userAssets,
@@ -98,18 +101,16 @@ module.exports = (middleware, users, plans, assets) => {
router.get('/plans', async (req, res) => { router.get('/plans', async (req, res) => {
try { try {
// console.log(new ObjectId(req.session.user._id)); const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray();
const userPlansFromDB = await plans.find({userId: new ObjectId(req.session.user._id) }).toArray();
// console.log(userPlansFromDB);
// Use a for...of loop for proper async/await behavior in series for updates // Use a for...of loop for proper async/await behavior in series for updates
for (const plan of userPlansFromDB) { for (const plan of userPlansFromDB) {
const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); const percentage = await calculatePlanProgress(plan, assets, req.session.userId);
await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection
} }
// Re-fetch plans to get updated progress for rendering // Re-fetch plans to get updated progress for rendering
const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray();
res.render('plans', { res.render('plans', {
user: req.session.user, user: req.session.user,
@@ -124,26 +125,24 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/plans/:id', async (req, res) => { router.get('/plans/:id', async (req, res) => {
try { try {
const planId = req.params.id; const planId = req.params.id;
let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).toArray();
if (!ObjectId.isValid(planId)) { if (!ObjectId.isValid(planId)) {
req.session.errMessage = "Invalid plan ID format."; req.session.errMessage = "Invalid plan ID format.";
return res.status(status.BadRequest).redirect('/plans'); return res.status(status.BadRequest).redirect('/plans');
} }
const plan = await plans.findOne({ userId: new ObjectId(req.session.user._id), _id: new ObjectId(planId) }); const plan = await plans.findOne({ userId: new ObjectId(req.session.userId), _id: new ObjectId(planId) });
if (!plan) { if (!plan) {
console.log(`Plan not found with ID: ${planId} for user: ${req.session.user.email}`); console.log(`Plan not found with ID: ${planId} for user: ${req.session.userId}`);
req.session.errMessage = "Plan not found or you do not have permission to view it."; req.session.errMessage = "Plan not found or you do not have permission to view it.";
return res.status(status.NotFound).redirect('/plans'); return res.status(status.NotFound).redirect('/plans');
} }
// console.log("Found plan:", plan);
// The plan.progress should be up-to-date from the database as it was updated in the /plans route // The plan.progress should be up-to-date from the database as it was updated in the /plans route
// or when assets/plans are modified. If an immediate recalculation for this specific view is absolutely needed, // or when assets/plans are modified. If an immediate recalculation for this specific view is absolutely needed,
// (e.g., if assets were modified without an immediate plan progress update elsewhere), // (e.g., if assets were modified without an immediate plan progress update elsewhere),
@@ -151,13 +150,14 @@ module.exports = (middleware, users, plans, assets) => {
// const currentProgress = await calculatePlanProgress(plan, assets, req.session.user._id); // const currentProgress = await calculatePlanProgress(plan, assets, req.session.user._id);
// plan.progress = currentProgress; // This would only update the 'plan' object for this render, not in DB // plan.progress = currentProgress; // This would only update the 'plan' object for this render, not in DB
res.render('planDetail', { res.render('planDetail', {
user: req.session.user, user: req.session.user,
plan: plan, // This plan object will have the progress from the database plan: plan, // This plan object will have the progress from the database
geoData: req.session.geoData, geoData: req.session.geoData,
assets: userAssets, assets: userAssets,
suggestions: await suggestions.generateSuggestions(),
}); });
} catch (err) { } catch (err) {
console.error("Error fetching plan:", err); console.error("Error fetching plan:", err);
req.session.errMessage = "Could not load your plan. Please try again."; req.session.errMessage = "Could not load your plan. Please try again.";
@@ -166,13 +166,13 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/newPlan', (req, res) => { router.get('/newPlan', (req, res) => {
if(!req.session.user.financialData){ if (!req.session.user.financialData || !req.session.user) {
req.session.errMessage = "Please complete your financial data before creating a plan."; req.session.errMessage = "Please complete your financial data before creating a plan.";
return res.status(status.Unauthorized).redirect('/questionnaire'); return res.status(status.Unauthorized).redirect('/questionnaire');
} }
const errMessage = req.session.errMessage; const errMessage = req.session.errMessage;
req.session.errMessage = ""; req.session.errMessage = "";
res.render('newPlan', { res.render('newPlan', {
user: req.session.user, user: req.session.user,
errMessage: errMessage, errMessage: errMessage,
geoData: req.session.geoData geoData: req.session.geoData
@@ -188,17 +188,17 @@ module.exports = (middleware, users, plans, assets) => {
retirementLiabilities: joi.number().min(0).required(), retirementLiabilities: joi.number().min(0).required(),
}); });
const validationOptions = { convert: true, abortEarly: false }; const validationOptions = { convert: true, abortEarly: false };
const { error, value } = planSchema.validate(req.body, validationOptions); const { error, value } = planSchema.validate(req.body, validationOptions);
if (error) { if (error) {
console.error("Plan validation error:", error.details); console.error("Plan validation error:", error.details);
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
res.status(status.BadRequest).redirect("/newPlan"); res.status(status.BadRequest).redirect("/newPlan");
return; return;
} }
const newPlan = { const newPlan = {
userId: new ObjectId(req.session.user._id), userId: new ObjectId(req.session.userId),
name: value.name, name: value.name,
retirementAge: value.retirementAge, retirementAge: value.retirementAge,
retirementExpenses: value.retirementExpenses, retirementExpenses: value.retirementExpenses,
@@ -207,20 +207,25 @@ module.exports = (middleware, users, plans, assets) => {
progress: "0" progress: "0"
}; };
try{ try {
await plans.insertOne({userId: new ObjectId(req.session.user._id), ...newPlan}); await plans.insertOne({ userId: new ObjectId(req.session.userId), ...newPlan });
req.session.errMessage = ""; req.session.errMessage = "";
res.redirect('/plans'); res.redirect('/plans');
} }
catch(err){ catch (err) {
console.error("Error saving plan:", err); console.error("Error saving plan:", err);
req.session.errMessage = "An error occurred while saving your plan. Please try again."; req.session.errMessage = "An error occurred while saving your plan. Please try again.";
res.status(status.InternalServerError).redirect("/newPlan"); res.status(status.InternalServerError).redirect("/newPlan");
} }
}); });
router.get('/pun', async (req, res) => {
const pun = await suggestions.generatePun();
return res.status(status.Ok).json({ pun });
});
router.get('/more', (req, res) => { router.get('/more', (req, res) => {
res.render('more', { res.render('more', {
user: req.session.user, user: req.session.user,
geoData: req.session.geoData geoData: req.session.geoData
}); });
@@ -228,7 +233,7 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/profile', (req, res) => { router.get('/profile', (req, res) => {
res.render('profile', { res.render('profile', {
user: req.session.user, user: req.session.user,
errMessage: req.session.errMessage, errMessage: req.session.errMessage,
geoData: req.session.geoData geoData: req.session.geoData
@@ -237,7 +242,7 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/settings', (req, res) => { router.get('/settings', (req, res) => {
res.render('settings', { res.render('settings', {
user: req.session.user, user: req.session.user,
geoData: req.session.geoData geoData: req.session.geoData
}); });
@@ -246,8 +251,8 @@ module.exports = (middleware, users, plans, assets) => {
router.get('/questionnaire', (req, res) => { router.get('/questionnaire', (req, res) => {
const errMessage = req.session.errMessage; const errMessage = req.session.errMessage;
req.session.errMessage = ""; req.session.errMessage = "";
res.render('questionnaire', { res.render('questionnaire', {
user: req.session.user, user: req.session.user,
errMessage: errMessage, errMessage: errMessage,
geoData: req.session.geoData geoData: req.session.geoData
@@ -255,8 +260,6 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.post('/questionnaire', (req, res) => { router.post('/questionnaire', (req, res) => {
// console.log("Questionnaire POST body:", req.body);
const questionnaireSchema = joi.object({ const questionnaireSchema = joi.object({
dob: joi.date().required(), dob: joi.date().required(),
education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(),
@@ -266,22 +269,22 @@ module.exports = (middleware, users, plans, assets) => {
assets: joi.number().min(0).required(), assets: joi.number().min(0).required(),
liabilities: joi.number().min(0).required(), liabilities: joi.number().min(0).required(),
}); });
const validationOptions = { convert: true, abortEarly: false }; const validationOptions = { convert: true, abortEarly: false };
const { error, value } = questionnaireSchema.validate(req.body, validationOptions); const { error, value } = questionnaireSchema.validate(req.body, validationOptions);
if (error) { if (error) {
console.error("Questionnaire validation error:", error.details); console.error("Questionnaire validation error:", error.details);
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
res.status(status.BadRequest).redirect("/questionnaire"); res.status(status.BadRequest).redirect("/questionnaire");
return; return;
} }
users.updateOne( users.updateOne(
{ _id: new ObjectId(req.session.user._id) }, { _id: new ObjectId(req.session.userId) },
{ {
$set: { $set: {
financialData: true, financialData: true,
dob: value.dob, dob: value.dob,
education: value.education, education: value.education,
maritalStatus: value.maritalStatus, maritalStatus: value.maritalStatus,
@@ -289,36 +292,42 @@ module.exports = (middleware, users, plans, assets) => {
expenses: value.expenses, expenses: value.expenses,
assets: value.assets, assets: value.assets,
liabilities: value.liabilities, liabilities: value.liabilities,
} }
} }
).then((result) => { ).then((result) => {
if (result.matchedCount === 0) { if (result.matchedCount === 0) {
console.log(`User not found during questionnaire update: ${req.session.user.email}`); console.log(`User not found during questionnaire update: ${req.session.userId}`);
req.session.errMessage = "User session invalid. Please log in again."; req.session.errMessage = "User session invalid. Please log in again.";
res.status(status.NotFound).redirect("/login"); res.status(status.NotFound).redirect("/login");
return; return;
} }
if (result.modifiedCount === 0 && result.matchedCount === 1) { if (result.modifiedCount === 0 && result.matchedCount === 1) {
console.log(`User questionnaire data unchanged (already up-to-date): ${req.session.user.email}`); console.log(`User questionnaire data unchanged (already up-to-date): ${req.session.userId}`);
} }
req.session.user.financialData = true; req.session.user.financialData = true;
req.session.errMessage = ""; req.session.errMessage = "";
res.status(status.Ok).redirect("/home");
req.session.save(err => {
}).catch(err => { if (err) {
res.status(status.InternalServerError).redirect("/plans");
}
res.status(status.Ok).redirect("/plans");
});
}).catch(err => {
console.error("Error updating questionnaire in database:", err); console.error("Error updating questionnaire in database:", err);
req.session.errMessage = "An error occurred while saving your information. Please try again."; req.session.errMessage = "An error occurred while saving your information. Please try again.";
res.status(status.InternalServerError).redirect("/questionnaire"); res.status(status.InternalServerError).redirect("/questionnaire");
}); });
}); });
router.post("/updateAccount", async (req, res) => { router.post("/updateAccount", async (req, res) => {
const accountSchema = joi.object({ const accountSchema = joi.object({
email: joi.string().email(), email: joi.string().email(),
name: joi.string().alphanum().max(20), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20),
password: joi.string().max(20).min(8), password: joi.string().alphanum().max(20).min(8),
repassword: joi.string().max(20).min(8), repassword: joi.string().alphanum().max(20).min(8),
}); });
const valid = accountSchema.validate(req.body); const valid = accountSchema.validate(req.body);
@@ -343,25 +352,25 @@ module.exports = (middleware, users, plans, assets) => {
} }
users.updateOne( users.updateOne(
{ email: req.session.email }, { _id: new ObjectId(req.session.userId) },
{ $set: update } { $set: update }
).then((result) => { ).then((result) => {
if (result.matchedCount === 0) { if (result.matchedCount === 0) {
console.log(`User not found during account update: ${req.session.email}`); console.log(`User not found during account update: ${req.session.userId}`);
req.session.errMessage = "User session invalid. Please log in again."; req.session.errMessage = "User session invalid. Please log in again.";
res.status(status.NotFound).redirect("/login"); res.status(status.NotFound).redirect("/login");
return; return;
} }
if (result.modifiedCount === 0 && result.matchedCount === 1) { if (result.modifiedCount === 0 && result.matchedCount === 1) {
console.log(`User account data unchanged (already up-to-date): ${req.session.email}`); console.log(`User account data unchanged (already up-to-date): ${req.session.userId}`);
} }
req.session.errMessage = ""; req.session.errMessage = "";
return res.status(status.Ok).redirect("/profile"); return res.status(status.Ok).redirect("/profile");
}).catch(err => { }).catch(err => {
console.error("Error updating account in database:", err); console.error("Error updating account in database:", err);
req.session.errMessage = "An error occurred while saving your information. Please try again."; req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/profile"); return res.status(status.InternalServerError).redirect("/profile");
}); });
}); });
@@ -380,12 +389,12 @@ module.exports = (middleware, users, plans, assets) => {
if (valid.err) { if (valid.err) {
req.session.errMessage = "Invalid input", req.session.errMessage = "Invalid input",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/assets"); return res.redirect("/assets");
} }
let newAsset = { let newAsset = {
userId: new ObjectId(req.session.user._id), userId: new ObjectId(req.session.userId),
...req.body, ...req.body,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -407,7 +416,7 @@ module.exports = (middleware, users, plans, assets) => {
} }
}); });
req.session.errMessage = ""; req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets"); return res.status(status.Ok).redirect("/assets");
}); });
@@ -420,18 +429,18 @@ module.exports = (middleware, users, plans, assets) => {
req.session.errMessage = "Invalid input"; req.session.errMessage = "Invalid input";
return res.status(status.BadRequest).redirect("/assets"); return res.status(status.BadRequest).redirect("/assets");
} }
const valid = assetSchema.validate(req.body); const valid = assetSchema.validate(req.body);
if (valid.err) { if (valid.err) {
req.session.errMessage = "Invalid input", req.session.errMessage = "Invalid input",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/assets"); return res.redirect("/assets");
} }
if (req.body.userId != req.session.user._id) { if (req.body.userId != req.session.userId) {
req.session.errMessage = "Cannot change asset owner", req.session.errMessage = "Cannot change asset owner",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/assets"); return res.redirect("/assets");
} }
@@ -445,11 +454,11 @@ module.exports = (middleware, users, plans, assets) => {
update.value = parseFloat(update.value); update.value = parseFloat(update.value);
delete update.id; delete update.id;
delete update.userId; delete update.userId;
assets.updateOne( assets.updateOne(
{ "_id": new ObjectId(req.body.id) }, { "_id": new ObjectId(req.body.id) },
{ $set: update }, { $set: update },
).then((result) => { ).then((result) => {
if (result.matchedCount === 0) { if (result.matchedCount === 0) {
console.log(`Asset not found: ${req.body.id}`); console.log(`Asset not found: ${req.body.id}`);
req.session.errMessage = "Unable to update asset"; req.session.errMessage = "Unable to update asset";
@@ -458,14 +467,14 @@ module.exports = (middleware, users, plans, assets) => {
if (result.modifiedCount === 0 && result.matchedCount === 1) { if (result.modifiedCount === 0 && result.matchedCount === 1) {
console.log(`Asset data unchanged (already up-to-date): ${req.body.id}`); console.log(`Asset data unchanged (already up-to-date): ${req.body.id}`);
} }
req.session.errMessage = ""; req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets"); return res.status(status.Ok).redirect("/assets");
}).catch((err) => { }).catch((err) => {
console.error("Error updating asset: ", err); console.error("Error updating asset: ", err);
req.session.errMessage = "An error occurred while saving your information. Please try again."; req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/assets"); return res.status(status.InternalServerError).redirect("/assets");
}); });
}); });
@@ -474,7 +483,7 @@ module.exports = (middleware, users, plans, assets) => {
assets.deleteOne( assets.deleteOne(
{ "_id": id } { "_id": id }
).then((result) => { ).then((result) => {
if (result.deletedCount === 0) { if (result.deletedCount === 0) {
console.error(`Asset not found: ${req.body.id}`); console.error(`Asset not found: ${req.body.id}`);
req.session.errMessage = "Unable to delete asset. Please try again."; req.session.errMessage = "Unable to delete asset. Please try again.";
@@ -486,11 +495,11 @@ module.exports = (middleware, users, plans, assets) => {
req.session.errMessage = "An error occurred while deleting an asset. Please try again."; req.session.errMessage = "An error occurred while deleting an asset. Please try again.";
return res.status(status.InternalServerError).redirect("/assets"); return res.status(status.InternalServerError).redirect("/assets");
} }
req.session.errMessage = ""; req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets"); return res.status(status.Ok).redirect("/assets");
}).catch((err) => { }).catch((err) => {
console.error("Error deleting asset: ", err); console.error("Error deleting asset: ", err);
req.session.errMessage = "An error occurred while deleting an asset. Please try again."; req.session.errMessage = "An error occurred while deleting an asset. Please try again.";
return res.status(status.InternalServerError).redirect("/assets"); return res.status(status.InternalServerError).redirect("/assets");
@@ -499,20 +508,20 @@ module.exports = (middleware, users, plans, assets) => {
router.post("/deleteUser", (req, res) => { router.post("/deleteUser", (req, res) => {
// not as critical if results aren't as expected only if crashing // not as critical if results aren't as expected only if crashing
assets.deleteMany({ userId: new ObjectId(req.session.user._id) }).catch((err) => { assets.deleteMany({ userId: new ObjectId(req.session.userId) }).catch((err) => {
console.error("Error deleting user assets: ", err); console.error("Error deleting user assets: ", err);
req.session.errMessage = "An error occured while deleting your account. Please try again."; req.session.errMessage = "An error occured while deleting your account. Please try again.";
return res.status(status.InternalServerError).redirect("/profile"); return res.status(status.InternalServerError).redirect("/profile");
}); });
plans.deleteMany({ userId: new ObjectId(req.session.user._id) }).catch((err) => { plans.deleteMany({ userId: new ObjectId(req.session.userId) }).catch((err) => {
console.error("Error deleting user assets: ", err); console.error("Error deleting user assets: ", err);
req.session.errMessage = "An error occured while deleting your account. Please try again."; req.session.errMessage = "An error occured while deleting your account. Please try again.";
return res.status(status.InternalServerError).redirect("/profile"); return res.status(status.InternalServerError).redirect("/profile");
}); });
users.deleteOne( users.deleteOne(
{ _id: new ObjectId(req.session.user._id) }, { _id: new ObjectId(req.session.userId) },
).then((result) => { ).then((result) => {
if (result.deletedCount === 0) { if (result.deletedCount === 0) {
console.error(`User not found: ${req.body.id}`); console.error(`User not found: ${req.body.id}`);
@@ -540,14 +549,22 @@ module.exports = (middleware, users, plans, assets) => {
if (!req.session.geoData.country) { if (!req.session.geoData.country) {
const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`); const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`);
const data = await response.json(); const data = await response.json();
country = data.results[0].formatted_address; country = data.results[0].formatted_address;
let results = await getRates(country); let results = await getRates(country);
req.session.geoData = {
country: results.abbreviation, if (!results.exRates) {
toCurrencyRates: results.exRates, req.session.geoData = {
geoData: req.session.geoData message: "error"
}
} else {
req.session.geoData = {
country: results.abbreviation,
toCurrencyRates: results.exRates,
geoData: req.session.geoData
}
} }
} }
return res.status(status.Ok).send({ data: req.session.geoData }); return res.status(status.Ok).send({ data: req.session.geoData });
+24
View File
@@ -0,0 +1,24 @@
const { GoogleGenAI } = require("@google/genai");
const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });
async function generatePun() {
const response = await ai.models.generateContent({
model: "gemini-2.0-flash",
contents: "Return a funny pun about investments, answer the pun only, no additional text.",
});
return response.text;
}
async function generateSuggestions() {
const response = await ai.models.generateContent({
model: "gemini-2.0-flash",
contents: "Return a mock investment suggestion 3-5 lines only",
});
return response.text;
}
module.exports = {
generatePun,
generateSuggestions
};
+23 -10
View File
@@ -1,7 +1,12 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/headerStart") %>
<main> <% if (!geoData) { %>
<%- include("./partials/headerStart") %>
<% } else { %>
<%- include("./partials/header") %>
<% } %>
<main class="pt-24 pb-16">
<section class="bg-white dark:bg-gray-900 py-12 md:py-20"> <section class="bg-white dark:bg-gray-900 py-12 md:py-20">
<div class="max-w-screen-lg mx-auto px-4 text-center"> <div class="max-w-screen-lg mx-auto px-4 text-center">
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">About RCalculator</h1> <h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">About RCalculator</h1>
@@ -25,15 +30,23 @@
</p> </p>
</div> </div>
</div> </div>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/signup" class="inline-flex justify-center items-center mt-4 py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-900"> <% if (!geoData) { %>
Get started <div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10"> <a href="/signup" class="inline-flex justify-center items-center mt-4 py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-900">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/> Get started
</svg> <svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
</a> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
</div> </svg>
</a>
</div>
<% } %>
</section> </section>
</main> </main>
<% if (!geoData) { %>
<% } else { %>
<%- include("./partials/navBar") %>
<% } %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+2 -2
View File
@@ -1,7 +1,7 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main class="pt-26">
<!-- Create Asset popup Modal --> <!-- Create Asset popup Modal -->
<dialog id="create-asset-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md"> <dialog id="create-asset-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
@@ -38,7 +38,7 @@
<!-- Create other asset form --> <!-- Create other asset form -->
<form method="POST" action="/createAsset" id="create-other-asset-form"> <form method="POST" action="/createAsset" id="create-other-asset-form">
<input style="display: none;" id="type" value="other" name="type" type="text"> <input style="display: none;" id="type" value="other" name="type" type="text">
<input style="display: none;" id="icon" value="other" name="icon" type="text"> <input style="display: none;" id="icon" value="Other" name="icon" type="text">
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Select an Icon</label> <label for="name" class="block text-sm font-medium text-gray-700 mt-2">Select an Icon</label>
<div class="flex items-center"> <div class="flex items-center">
+102 -79
View File
@@ -1,95 +1,118 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main class="pt-24 pb-14">
<!-- Buttons --> <!-- Buttons -->
<div class="flex justify-end gap-4 px-5 py-6"> <div class="flex justify-end gap-4 px-5 py-6">
<a href="/assets?popup" class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">Add Asset</a> <a href="/assets?popup"
<a href="/newPlan" class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">Create new plan </a> class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800"
type="button">Add Asset</a>
<a href="/newPlan"
class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800"
type="button">Create new plan </a>
</div> </div>
<!--This is the cards for plans and other things--> <!--This is the cards for plans and other things-->
<div class="mt-12"> <div class="mt-4">
<div class="mb-12 grid gap-y-10 gap-x-6 md:grid-cols-2 mr-3 ml-3 xl:grid-cols-4">
<!--box to put logo icon if we want on in a box--> <!--box to put logo icon if we want on in a box-->
<div class=" relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md"> <!-- <div -->
<!-- <div --> <!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> -->
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> --> <!-- </div> -->
<!-- </div> --> <!--This is the cards for plans and other things-->
<div class="p-4 flex items-center justify-between"> <div class="">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600"> <div class="mb-12 grid gap-y-10 gap-x-6 md:grid-cols-2 mr-3 ml-3 xl:grid-cols-4">
Retirement goal</p> <!--box to put logo icon if we want on in a box-->
<h4 <div class=" relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> <!-- <div -->
$53k</h4> <!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> -->
<!-- </div> -->
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p
class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or
something
</strong>&nbsp;than lastweek
</p>
</div>
</div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p
class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or
something
</strong>&nbsp;than last
week
</p>
</div>
</div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<!-- <div -->
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-green-600 to-green-400 text-white shadow-green-500/40 shadow-lg absolute -mt-4 grid h-16 w-16 place-items-center"> -->
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" -->
<!-- class="w-6 h-6 text-white"> -->
<!-- <path -->
<!-- d="M6.25 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM3.25 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM19.75 7.5a.75.75 0 00-1.5 0v2.25H16a.75.75 0 000 1.5h2.25v2.25a.75.75 0 001.5 0v-2.25H22a.75.75 0 000-1.5h-2.25V7.5z"> -->
<!-- </path> -->
<!-- </svg> -->
<!-- </div> -->
<!-- we can use this later if needed not sure what it shoudl be for right now. -->
<!-- <div class="p-4 text-right"> -->
<!-- <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">New Clients -->
<!-- </p> -->
<!-- <h4 -->
<!-- class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> -->
<!-- 3,462</h4> -->
<!-- </div> -->
<!-- <div class="border-t border-blue-gray-50 p-4"> -->
<!-- <p class="block antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> -->
<!-- <strong class="text-red-500">-2%</strong>&nbsp;than yesterday -->
<!-- </p> -->
<!-- </div> -->
<!-- </div> -->
<div
class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div
class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p
class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or
graph or something
</strong>&nbsp;than last
week
</p>
</div>
</div>
</div>
</div>
</div> </div>
<div class="border-t border-blue-gray-50 p-10"> <div class="text-blue-gray-600">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than lastweek
</p>
</div> </div>
</div> </div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last
week
</p>
</div>
</div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<!-- <div -->
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-green-600 to-green-400 text-white shadow-green-500/40 shadow-lg absolute -mt-4 grid h-16 w-16 place-items-center"> -->
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" -->
<!-- class="w-6 h-6 text-white"> -->
<!-- <path -->
<!-- d="M6.25 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM3.25 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM19.75 7.5a.75.75 0 00-1.5 0v2.25H16a.75.75 0 000 1.5h2.25v2.25a.75.75 0 001.5 0v-2.25H22a.75.75 0 000-1.5h-2.25V7.5z"> -->
<!-- </path> -->
<!-- </svg> -->
<!-- </div> -->
<!-- we can use this later if needed not sure what it shoudl be for right now. -->
<!-- <div class="p-4 text-right"> -->
<!-- <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">New Clients -->
<!-- </p> -->
<!-- <h4 -->
<!-- class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> -->
<!-- 3,462</h4> -->
<!-- </div> -->
<!-- <div class="border-t border-blue-gray-50 p-4"> -->
<!-- <p class="block antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> -->
<!-- <strong class="text-red-500">-2%</strong>&nbsp;than yesterday -->
<!-- </p> -->
<!-- </div> -->
<!-- </div> -->
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last
week
</p>
</div>
</div>
</div>
</div>
</div>
<div class="text-blue-gray-600">
</div> </div>
</main> </main>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %> <%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+38
View File
@@ -0,0 +1,38 @@
<%- include("./partials/fileHeader") %>
<main>
<div class="flex justify-center items-center h-screen">
<form action="/auth/resetPass" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Reset Password</h1>
<hr class="mt-3">
<% if (!reset ) { %>
<div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" />
</div>
<% } else { %>
<div class="mt-3">
<h1 class="text-2xl block text-center font-bold text-red-600"> Reset link sent. </h1>
</div>
<% } %>
<!---->
<div class="text-red-800 font-bold" id="forgor">
<%= error%>
</div>
<% if (!reset) { %>
<div class="mt-5">
<button type="submit"
class="cursor-pointer border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Send
reset Link</button>
</div>
<% } %>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+5 -4
View File
@@ -1,7 +1,8 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/headerStart") %>
<main> <main class="pt-24">
<div class="flex justify-center items-center h-screen"> <div class="flex justify-center items-center">
<form action="/login" method="POST"> <form action="/login" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md"> <div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Login</h1> <h1 class="text-3xl block text-center font-semibold">Login</h1>
@@ -25,7 +26,7 @@
<label>Remember me</label> <label>Remember me</label>
</div> </div>
<div> <div>
<a href="#" class="text-indigo-600 font-semibold">Forgot Password? </a> <a href="/forgotPassword" class="text-indigo-600 font-semibold">Forgot Password? </a>
</div> </div>
</div> </div>
<div class="mt-5"> <div class="mt-5">
@@ -41,4 +42,4 @@
</div> </div>
</main> </main>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+47 -4
View File
@@ -1,11 +1,54 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main class="pt-24 pb-10">
The More Page <div class="mt-6">
<div class="mb-12 grid gap-y-6 gap-x-6 md:grid-cols-2 mr-3 ml-3 xl:grid-cols-4">
<!--box to put logo icon if we want on in a box-->
<a href="/profile" class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Profile
</p>
<svg class="w-[40px] h-[40px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m10 16 4-4-4-4"/>
</svg>
</div>
</a>
<a href="/settings" class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Settings
</p>
<svg class="w-[40px] h-[40px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m10 16 4-4-4-4"/>
</svg>
</div>
</a>
<a href="/aboutUs" class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
About Us
</p>
<svg class="w-[40px] h-[40px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m10 16 4-4-4-4"/>
</svg>
</div>
</a>
<a href="/logout" class="mt-77 relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Log Out
</p>
<svg class="w-[40px] h-[40px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m10 16 4-4-4-4"/>
</svg>
</div>
</a>
</div>
</div>
</main> </main>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %> <%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+1 -1
View File
@@ -1,7 +1,7 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main class="container mx-auto p-4"> <main class="container mx-auto p-4 pt-30">
<h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2> <h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2>
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md"> <div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md">
<h3 class="text-lg font-medium mb-6">Retirement Plan</h3> <h3 class="text-lg font-medium mb-6">Retirement Plan</h3>
+14 -1
View File
@@ -6,5 +6,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>RCalculator</title> <title>RCalculator</title>
<style>
::-webkit-scrollbar {
display: none;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
</head> </head>
<body class="bg-gray-900"> <body class="bg-gray-900 overflow-y-auto">
+29 -7
View File
@@ -1,11 +1,33 @@
<div data-dial-init class="fixed end-6 bottom-20 group">
<footer class="mt-16"> <button type="button" id="punButton" class="flex items-center justify-center text-white bg-blue-700 rounded-full w-14 h-14 hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:outline-none dark:focus:ring-blue-800">
<div class="fixed bottom-0 left-0 z-50 hidden lg:block w-full h-16 bg-gray-800 border-t border-gray-700 p-6 text-center text-gray-400 text-sm"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
<div class="container mx-auto"> <span class="sr-only">Get a Pun</span>
<p>&copy; 2025 RCalculator. All rights reserved.</p> </button>
</div>
</div> </div>
</footer>
<footer>
<div class="fixed bottom-0 left-0 z-50 hidden lg:block w-full h-16 bg-gray-800 border-t border-gray-700 p-6 text-center text-gray-400 text-sm">
<div class="container mx-auto">
<p>&copy; 2025 RCalculator. All rights reserved.</p>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
const punButton = document.getElementById("punButton");
punButton.addEventListener("click", () => {
fetch("/pun")
.then(response => response.json())
.then(data => {
Swal.fire('Here is a pun!', data.pun, 'success');
})
.catch(error => {
console.error("Error fetching pun:", error);
Swal.fire('Error fetching pun', 'Please try again later', 'error');
});
});
</script>
</body> </body>
</html> </html>
+78 -24
View File
@@ -1,8 +1,8 @@
<header> <header>
<nav class="bg-gray-800 border-b border-gray-700"> <nav class="fixed bg-gray-800 border-b border-gray-700 z-50">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"> <div class="w-screen flex flex-col md:flex-row flex-wrap items-center justify-between p-3">
<a href="/home" class="flex items-center space-x-3 rtl:space-x-reverse"> <a href="/home" class="flex xl:w-[320px] items-center space-x-3 rtl:space-x-reverse">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png" class="h-8" alt="Flowbite Logo" /> <img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span> <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span>
</a> </a>
@@ -16,13 +16,13 @@
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
<div style="display: none;" class="flex items-center" id="currencyExchange"> <div style="display: none;" class="flex items-center pt-2 md:pt-0 lg:pt-0" id="currencyExchange">
<div class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"> <div id="exFrom" class="shrink-0 z-10 inline-flex w-[115px] items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600">
<img src="" id="yourFlag" class="h-4 w-4 me-2" alt=""> <img src="" id="yourFlag" class="h-4 w-4 me-2" alt="">
<p id="flagTag"></p> <p id="flagTag"></p>
</div> </div>
<div class="relative w-full"> <div class="relative w-full">
<p id="exchange" class="block text-center p-2.5 w-30 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"> <p id="exchange" class="block text-center p-2.5 w-35 z-20 text-sm border bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-blue-500">
</p> </p>
</div> </div>
<button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button"> <button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button">
@@ -32,24 +32,26 @@
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg> </svg>
</button> </button>
<div id="dropdown-country" class="overflow-y-auto h-40 z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-32 dark:bg-gray-700"> <div id="dropdown-country" class="overflow-y-auto w-[115px] h-40 z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-32 dark:bg-gray-700">
<ul id="dropdown" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-country-button"> <ul id="dropdown" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-country-button">
</ul> </ul>
</div> </div>
</div> </div>
<% } else if(geoData.message == "error") { %>
<p>error</p>
<% } else { %> <% } else { %>
<div class="flex items-center"> <div class="flex items-center pt-2 md:pt-0 lg:pt-0">
<div class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"> <div class="shrink-0 z-10 w-[115px] inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600">
<img src="/static/svgs/flags/<%= geoData.country %>.svg" id="yourFlag" class="h-4 w-4 me-2" alt="<%= geoData.country %>"> (<%= geoData.country %>) <img src="/static/svgs/flags/<%= geoData.country %>.svg" id="yourFlag" class="h-4 w-4 me-2" alt="<%= geoData.country %>"> (<%= geoData.country %>)
<p id="flagTag"></p> <p id="flagTag"></p>
</div> </div>
<div class="relative w-full"> <div class="relative w-full">
<p id="exchange" class="block text-center p-2.5 w-30 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"> <p id="exchange" class="block text-center p-2.5 w-35 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500">
$1.00 = $<%= (1 * geoData.toCurrencyRates["USD"]).toFixed(2) %> $1.00 = $<%= (1 * geoData.toCurrencyRates["USD"]).toFixed(2) %>
</p> </p>
</div> </div>
<button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button"> <button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 w-[115px] z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button">
<img src="/static/svgs/flags/USD.svg" class="h-4 w-4 me-2" alt="USD"> (USD) <img src="/static/svgs/flags/USD.svg" class="h-4 w-4 me-2" alt="USD"> (USD)
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6"> <svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
@@ -71,25 +73,77 @@
</div> </div>
</div> </div>
<% } %> <% } %>
<div class="hidden w-full lg:block lg:w-auto" id="navbar-solid-bg"> <div class="hidden w-full lg:block lg:w-[320px]" id="navbar-solid-bg">
<ul class="flex flex-col font-medium mt-4 rounded-lg bg-gray-50 lg:space-x-8 rtl:space-x-reverse lg:flex-row lg:mt-0 lg:border-0 lg:bg-transparent dark:bg-gray-800 lg:dark:bg-transparent dark:border-gray-700"> <ul class="flex flex-col font-medium mt-4 rounded-lg bg-gray-50 lg:space-x-8 rtl:space-x-reverse lg:flex-row lg:mt-0 lg:border-0 lg:bg-transparent dark:bg-gray-800 lg:dark:bg-transparent dark:border-gray-700">
<li> <li class="flex items-center justify-center">
<a href="/home" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Dashboard</a> <a href="/home" class="block w-full text-center py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Dashboard</a>
</li>
<li class="flex items-center justify-center">
<a href="/assets" class="block w-full text-center py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Assets</a>
</li>
<li class="flex items-center justify-center">
<a href="/plans" class="block w-full text-center py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Plans</a>
</li> </li>
<li> <li>
<a href="/assets" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Assets</a> <div class="text-center">
</li> <button class="flex flex-row text-white hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm focus:outline-none dark:focus:ring-blue-800" type="button" data-drawer-target="drawer-navigation" data-drawer-show="drawer-navigation" data-drawer-placement="right" aria-controls="drawer-navigation">
<li> <svg class="w-[30px] h-[30px] text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<a href="/plans" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Plans</a> <path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
</li> </svg>
<li> <svg class="w-[30px] h-[30px] text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<a href="/more" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">More</a> <path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M5 7h14M5 12h14M5 17h14"/>
</li> </svg>
<li>
<a href="/logout" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Logout</a> </button>
</div>
</li> </li>
</ul> </ul>
</div> </div>
<!-- drawer component -->
<div id="drawer-navigation" class="hidden lg:block fixed top-0 right-0 z-40 w-64 h-screen p-4 overflow-y-auto transition-transform translate-x-full bg-white dark:bg-gray-800" tabindex="-1" aria-labelledby="drawer-navigation-label">
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400"><%= user.name %></h5>
<button type="button" data-drawer-hide="drawer-navigation" aria-controls="drawer-navigation" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
<span class="sr-only">Close menu</span>
</button>
<div class="">
<div class="w-full pt-5 pl-2.5">
<a href="/logout" class="text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-20 py-2.5 text-center me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Logout
</a>
</div>
</div>
<div class="py-4 overflow-y-auto">
<ul class="space-y-2 font-medium">
<li>
<a href="/profile" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 22 21">
<path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
</svg>
<span class="ms-3">Profile</span>
</a>
</li>
<li>
<a href="/settings" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 22 21">
<path d="M16.975 11H10V4.025a1 1 0 0 0-1.066-.998 8.5 8.5 0 1 0 9.039 9.039.999.999 0 0 0-1-1.066h.002Z"/>
<path d="M12.5 0c-.157 0-.311.01-.565.027A1 1 0 0 0 11 1.02V10h8.975a1 1 0 0 0 1-.935c.013-.188.028-.374.028-.565A8.51 8.51 0 0 0 12.5 0Z"/>
</svg>
<span class="ms-3">Settings</span>
</a>
</li>
<li>
<a href="aboutUs" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class="shrink-0 w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
<path d="M14 2a3.963 3.963 0 0 0-1.4.267 6.439 6.439 0 0 1-1.331 6.638A4 4 0 1 0 14 2Zm1 9h-1.264A6.957 6.957 0 0 1 15 15v2a2.97 2.97 0 0 1-.184 1H19a1 1 0 0 0 1-1v-1a5.006 5.006 0 0 0-5-5ZM6.5 9a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 10H5a5.006 5.006 0 0 0-5 5v2a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-2a5.006 5.006 0 0 0-5-5Z"/>
</svg>
<span class="flex-1 ms-3 whitespace-nowrap">About Us</span>
</a>
</li>
</ul>
</div>
</div>
</div> </div>
</nav> </nav>
</header> </header>
+1 -1
View File
@@ -1,6 +1,6 @@
<header> <header>
<nav class="bg-gray-800 border-b border-gray-700"> <nav class="bg-gray-800 border-b border-gray-700">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"> <div class="w-screen flex flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse"> <a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png" class="h-8" alt="Flowbite Logo" /> <img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span> <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span>
+2 -2
View File
@@ -1,5 +1,5 @@
<div class="fixed bottom-0 z-50 lg:hidden w-full h-16 bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 text-sm"> <div class="fixed block bottom-0 left-0 z-50 lg:hidden w-full h-16 bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 text-sm">
<div class="grid h-16 max-w-lg grid-cols-4 mx-auto font-medium"> <div class="grid h-16 grid-cols-4 mx-auto font-medium">
<a href="/home" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group"> <a href="/home" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group">
<img src="/static/svgs/dashboard.svg" alt="dashboard"> <img src="/static/svgs/dashboard.svg" alt="dashboard">
<span class="text-gray-500 dark:text-gray-400 text-sm">Dashboard</span> <span class="text-gray-500 dark:text-gray-400 text-sm">Dashboard</span>
-1
View File
@@ -1,5 +1,4 @@
<!-- Get geolocation if not in session and load conversion rates --> <!-- Get geolocation if not in session and load conversion rates -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
<script src="/static/scripts/geolocation.js"></script> <script src="/static/scripts/geolocation.js"></script>
<script> <script>
// If no geoData provided thorugh EJS, send update request to backend // If no geoData provided thorugh EJS, send update request to backend
+1 -1
View File
@@ -62,7 +62,7 @@
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600"> <div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-700 dark:text-blue-300"> <p class="text-sm text-blue-700 dark:text-blue-300">
USE YOUR HEAD MOCK <%= suggestions %>
</p> </p>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,7 +1,7 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main class="container mx-auto p-4"> <main class="container mx-auto p-4 pt-28">
<h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2> <h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2>
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md space-y-4"> <div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md space-y-4">
<h3 class="text-lg font-medium mb-6">My Retirement Plans</h3> <h3 class="text-lg font-medium mb-6">My Retirement Plans</h3>
@@ -10,7 +10,7 @@
<% plans.forEach(plan => { %> <% plans.forEach(plan => { %>
<a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"> <a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"><%= plan.name %></h5> <h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"><%= plan.name %></h5>
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4 dark:bg-gray-700"> <div class="w-full bg-black rounded-full h-2.5 mb-4 dark:bg-black">
<div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %>%;"></div> <div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %>%;"></div>
</div> </div>
<p class="font-normal text-gray-700 dark:text-gray-400"><%= plan.description %></p> <p class="font-normal text-gray-700 dark:text-gray-400"><%= plan.description %></p>
+2 -2
View File
@@ -1,7 +1,7 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main class="pt-24">
<!-- Confirm delete account modal --> <!-- Confirm delete account modal -->
<dialog id="delete-warning-modal" class="w-lg mx-auto bg-transparent p-8 mt-10 rounded-lg shadow-md"> <dialog id="delete-warning-modal" class="w-lg mx-auto bg-transparent p-8 mt-10 rounded-lg shadow-md">
<div class="relative p-4 w-full max-w-md h-full md:h-auto"> <div class="relative p-4 w-full max-w-md h-full md:h-auto">
@@ -56,7 +56,7 @@
<form action="/updateAccount" method="post" class="space-y-4 mt-4" id="account-form"> <form action="/updateAccount" method="post" class="space-y-4 mt-4" id="account-form">
<label for="email" class="block text-sm font-medium text-gray-700">Email</label> <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input id="email" disabled type="text" name="email" value="<%= user.email %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none"> <input id="email" disabled type="email" name="email" value="<%= user.email %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
<label for="name" class="block text-sm font-medium text-gray-700">Name</label> <label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input id="name" disabled type="text" name="name" value="<%= user.name %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none"> <input id="name" disabled type="text" name="name" value="<%= user.name %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
+2 -2
View File
@@ -1,8 +1,8 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main class="container mx-auto p-4"> <main class="container mx-auto p-4 pt-28">
<h2 class="text-xl font-semibold mb-4">Welcome: <%= user.name %></h2> <h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2>
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md"> <div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md">
<h3 class="text-lg font-medium mb-6">Financial Questionnaire</h3> <h3 class="text-lg font-medium mb-6">Financial Questionnaire</h3>
<form action="/questionnaire" method="post" class="space-y-4"> <form action="/questionnaire" method="post" class="space-y-4">
+36
View File
@@ -0,0 +1,36 @@
<%- include("./partials/fileHeader") %>
<main>
<div class="flex justify-center items-center h-screen">
<form action="/resetLink" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Reset Password</h1>
<hr class="mt-3">
<input type="hidden" name="token" value="<%=token %>">
<div class="mt-3">
<label for="password" class="block text-base mb-2">New Password:</label>
<input type="password" id="password" name="password"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" />
</div>
<div class="mt-3">
<label for="password" class="block text-base mb-2">Confirm Password:</label>
<input type="password" id="Repassword" name="confirmPassword"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="ReType Password" />
</div>
<% if (typeof errMessage !=='undefined' ) { %>
<div class="text-red-800 font-bold" id="forgor">
<%= errMessage %>
</div>
<% } %>
<div class="mt-5">
<button type="submit"
class="cursor-pointer border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Change
Password</button>
</div>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %>
+6 -4
View File
@@ -1,8 +1,8 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/headerStart") %> <%- include("./partials/headerStart") %>
<main> <main class="pt-24">
<div class="flex justify-center items-center h-screen"> <div class="flex justify-center items-center">
<form action="/signup" method="POST"> <form action="/signup" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md"> <div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Signup</h1> <h1 class="text-3xl block text-center font-semibold">Signup</h1>
@@ -15,7 +15,7 @@
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label> <label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email" <input type="email" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" /> placeholder="Enter Email" />
</div> </div>
@@ -29,7 +29,9 @@
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" /> placeholder="Enter Password" />
</div> </div>
<div class="text-red-800 font-bold " id="forgor"> <%= errMessage %></div> <div class="text-red-800 font-bold " id="forgor">
<%= errMessage %>
</div>
<div class="mt-5"> <div class="mt-5">
<button type="submit" <button type="submit"
class="border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Signup</button> class="border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Signup</button>