Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1afcbd785 | ||
|
|
e320459955 | ||
|
|
8c0e07abb3 | ||
|
|
edb0021cc6 | ||
|
|
69624f859c | ||
|
|
53e72e2a19 | ||
|
|
043c24a417 | ||
|
|
629e4f8ae8 | ||
|
|
e2df35929f | ||
|
|
b1743d91c4 | ||
|
|
5d67f4f5ed | ||
|
|
505f0484bc | ||
|
|
cc63b83288 | ||
|
|
0eaa82b121 | ||
|
|
03b0dd8fa6 | ||
|
|
6faa951636 | ||
|
|
58a6e4724f | ||
|
|
f5f1516a76 | ||
|
|
692f195e27 | ||
|
|
5ec2de0631 | ||
|
|
59f8677e7c | ||
|
|
480ea236ec | ||
|
|
6ecd1ee848 | ||
|
|
c839d023a7 | ||
|
|
cfcfd7778d | ||
|
|
450e46f9e5 | ||
|
|
3940089fa6
|
||
|
|
0834723c99 | ||
|
|
1acb749f1f | ||
|
|
563fa502d1 | ||
|
|
aa9e412fa1 | ||
|
|
60238b7a9a
|
||
|
|
61db8e162f
|
||
|
|
1c5031f48c
|
||
|
|
b040f12eaf
|
||
|
|
4a02b75a4d | ||
|
|
f7377f6844 | ||
|
|
34c6dd2c66 | ||
|
|
1f3109a68f
|
||
|
|
9ae23ce25e
|
||
|
|
c41f38ce6b | ||
|
|
5771f1372f | ||
|
|
73d61d76de | ||
|
|
50e2d95fda | ||
|
|
370d5d3292 | ||
|
|
c12bbe8336 | ||
|
|
0d59c60a28 | ||
|
|
5e2ac94212 | ||
|
|
f5bc2de24a | ||
|
|
3771288123
|
||
|
|
ccae6a51ff | ||
|
|
206b31e785 | ||
|
|
8166245ab1 | ||
|
|
79f0caa3c1 | ||
|
|
4f73959cfb | ||
|
|
6444d7afa7 | ||
|
|
daa984a48f | ||
|
|
383bd15263 | ||
|
|
1e44e8ddfb | ||
|
|
cc875b4e22 | ||
|
|
f3dbbebb43 | ||
|
|
5819b0d151 |
No files matched your search
@@ -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'
|
||||||
@@ -70,8 +70,18 @@ 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);
|
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) => {
|
app.get('/forgotPassword', (req, res) => {
|
||||||
@@ -83,7 +93,6 @@ app.get('/forgotPassword', (req, res) => {
|
|||||||
return res.status(status.Ok);
|
return res.status(status.Ok);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Reset with token given to user via email
|
// Reset with token given to user via email
|
||||||
app.get('/reset/:token', async (req, res) => {
|
app.get('/reset/:token', async (req, res) => {
|
||||||
const token = req.params.token;
|
const token = req.params.token;
|
||||||
@@ -106,54 +115,6 @@ app.get('/reset/:token', async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.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");
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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) => {
|
||||||
|
|||||||
Generated
+529
-1
@@ -9,7 +9,9 @@
|
|||||||
"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",
|
||||||
|
"check-password-strength": "^3.0.0",
|
||||||
"connect-mongo": "^5.1.0",
|
"connect-mongo": "^5.1.0",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
@@ -18,12 +20,28 @@
|
|||||||
"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"
|
"nodemailer": "^7.0.3",
|
||||||
|
"sweetalert2": "^11.6.13"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -89,6 +107,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",
|
||||||
@@ -104,6 +131,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",
|
||||||
@@ -211,12 +247,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",
|
||||||
@@ -231,6 +301,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",
|
||||||
@@ -302,6 +380,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",
|
||||||
@@ -356,6 +447,15 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/check-password-strength": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XIBvWpb7/RI2DO05tMixE4WbFFvEC7ls/jr1VSrWgXvlCmdiH2fsdkixG0cNGs1uIXwoSqJ75T3s9H/TlJV6Cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -417,6 +517,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",
|
||||||
@@ -508,6 +619,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",
|
||||||
@@ -558,6 +677,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",
|
||||||
@@ -594,6 +721,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",
|
||||||
@@ -624,12 +759,38 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -721,6 +882,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",
|
||||||
@@ -781,6 +974,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",
|
||||||
@@ -874,6 +1119,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",
|
||||||
@@ -911,6 +1221,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",
|
||||||
@@ -945,6 +1269,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",
|
||||||
@@ -957,6 +1318,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",
|
||||||
@@ -978,6 +1351,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",
|
||||||
@@ -1131,6 +1518,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",
|
||||||
@@ -1162,6 +1560,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",
|
||||||
@@ -1615,6 +2040,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",
|
||||||
@@ -1641,6 +2071,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",
|
||||||
@@ -1648,6 +2083,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",
|
||||||
@@ -2007,6 +2451,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sweetalert2": {
|
||||||
|
"version": "11.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.6.13.tgz",
|
||||||
|
"integrity": "sha512-n5yVF0FNx1lm4XzpPyb1HIaiptzODfVyeCzmB809tpK+1bPdoKoevKOxYjrtId75DV7xuIp4r6cjn8xUAB8dPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"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",
|
||||||
@@ -2024,6 +2478,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",
|
||||||
@@ -2101,6 +2566,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",
|
||||||
@@ -2116,6 +2587,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",
|
||||||
@@ -2162,11 +2645,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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -18,7 +18,9 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
|
"check-password-strength": "^3.0.0",
|
||||||
"connect-mongo": "^5.1.0",
|
"connect-mongo": "^5.1.0",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
@@ -27,7 +29,8 @@
|
|||||||
"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"
|
"nodemailer": "^7.0.3",
|
||||||
|
"sweetalert2": "^11.6.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { passwordStrength } = require("check-password-strength");
|
||||||
const status = require("../util/statuses");
|
const status = require("../util/statuses");
|
||||||
const session = require("express-session");
|
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const joi = require("joi");
|
const joi = require("joi");
|
||||||
const salt = 12;
|
const salt = 12;
|
||||||
@@ -12,9 +12,17 @@ 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) => {
|
||||||
|
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');
|
return res.redirect('/login');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
|
|
||||||
@@ -46,10 +54,19 @@ 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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,6 +99,14 @@ module.exports = (users) => {
|
|||||||
return res.redirect("/signup");
|
return res.redirect("/signup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let strength = passwordStrength(req.body.password);
|
||||||
|
|
||||||
|
if (strength.id < 2) {
|
||||||
|
req.session.errMessage = `Password ${strength.value}`;
|
||||||
|
res.status(status.BadRequest);
|
||||||
|
return res.redirect("/signup");
|
||||||
|
}
|
||||||
|
|
||||||
let hashedPassword = await bcrypt.hashSync(req.body.password, salt);
|
let hashedPassword = await bcrypt.hashSync(req.body.password, salt);
|
||||||
|
|
||||||
users.insertOne({
|
users.insertOne({
|
||||||
@@ -97,11 +122,20 @@ module.exports = (users) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+55
-3
@@ -2,8 +2,11 @@ const express = require('express');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const joi = require('joi');
|
const joi = require('joi');
|
||||||
const nodeMail = require('nodemailer');
|
const nodeMail = require('nodemailer');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const PORT = process.env.PORT;
|
||||||
|
|
||||||
const transporter = nodeMail.createTransport({
|
const transporter = nodeMail.createTransport({
|
||||||
service: 'gmail',
|
service: 'gmail',
|
||||||
auth: {
|
auth: {
|
||||||
@@ -11,12 +14,11 @@ const transporter = nodeMail.createTransport({
|
|||||||
pass: process.env.EMAIL_PASS,
|
pass: process.env.EMAIL_PASS,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// users info
|
|
||||||
module.exports = (users) => {
|
module.exports = (users) => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/auth/resetPass', async (req, res) => {
|
router.post('/auth/resetPass', async (req, res) => {
|
||||||
console.log("we are inside of the post");
|
|
||||||
const resetSchema = joi.object({
|
const resetSchema = joi.object({
|
||||||
email: joi.string().email().required(),
|
email: joi.string().email().required(),
|
||||||
});
|
});
|
||||||
@@ -44,7 +46,7 @@ module.exports = (users) => {
|
|||||||
$set: { resetToken: token, resetTokenExpires: expiration }
|
$set: { resetToken: token, resetTokenExpires: expiration }
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetUrl = `http://localhost:3000/reset/${token}`;
|
const resetUrl = `http://localhost:${PORT}/reset/${token}`;
|
||||||
|
|
||||||
const mailSend = {
|
const mailSend = {
|
||||||
from: process.env.EMAIL_USER,
|
from: process.env.EMAIL_USER,
|
||||||
@@ -63,6 +65,56 @@ module.exports = (users) => {
|
|||||||
res.status(500).send('email failed to send try again');
|
res.status(500).send('email failed to send try again');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Reset link verifies information from user and checks token
|
||||||
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
+32
-5
@@ -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,52 @@ 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) {
|
||||||
|
return req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
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";
|
req.session.errMessage = "User not found";
|
||||||
|
res.status(status.NotFound);
|
||||||
res.redirect("/login");
|
res.redirect("/login");
|
||||||
return res.status(status.Unauthorized);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = "Failed to save session, 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;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Function takes in a number and formats it to currency
|
||||||
|
* @param {integer}
|
||||||
|
* @returns nuber formatted in currency
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const number = document.querySelectorAll(".planGoal");
|
||||||
|
number.forEach(value => {
|
||||||
|
num = parseFloat(value.textContent);
|
||||||
|
if (num >= 999999) {
|
||||||
|
value.textContent = "$" + (num /= 1000000) + "M";
|
||||||
|
} else if (num > 999) {
|
||||||
|
value.textContent = "$" + (num /= 1000) + "k";
|
||||||
|
} else {
|
||||||
|
return "$" + num;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const factButton = document.getElementById("factButton");
|
||||||
|
const factMenu = document.getElementById("factMenu");
|
||||||
|
const factInput = document.getElementById("factInput");
|
||||||
|
const factSubmitButton = document.getElementById("factSubmitButton");
|
||||||
|
|
||||||
|
var path = window.location.pathname;
|
||||||
|
var page = path.split("/").pop();
|
||||||
|
|
||||||
|
if(page !== "login" && page !== "signup" && page !== "forgotPassword" && page !== "") {
|
||||||
|
factButton.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
factButton.addEventListener("click", () => {
|
||||||
|
factMenu.classList.toggle("hidden");
|
||||||
|
});
|
||||||
|
factSubmitButton.addEventListener("click", () => {
|
||||||
|
fetch("/fact", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
fact: factInput.value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
Swal.fire('Here is a fact!', data.fact, 'success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching fact:", error);
|
||||||
|
Swal.fire('Error fetching fact', 'Please try again later', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const drawer = document.getElementById("drawer-navigation");
|
||||||
|
const toggle = document.getElementById("drawer-toggle");
|
||||||
|
|
||||||
|
function openDrawer() {
|
||||||
|
drawer.classList.remove("translate-x-full");
|
||||||
|
|
||||||
|
// Add backdrop
|
||||||
|
const backdrop = document.createElement("div");
|
||||||
|
backdrop.className = "fixed inset-0 bg-gray-900/50 z-30 drawer-backdrop";
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
// Optional: prevent scroll
|
||||||
|
document.body.classList.add("overflow-hidden");
|
||||||
|
|
||||||
|
backdrop.addEventListener("click", closeDrawer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
drawer.classList.add("translate-x-full");
|
||||||
|
|
||||||
|
const backdrop = document.querySelector(".drawer-backdrop");
|
||||||
|
if (backdrop) backdrop.remove();
|
||||||
|
|
||||||
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", openDrawer);
|
||||||
|
|
||||||
|
// Also close on X button if needed
|
||||||
|
const closeBtn = drawer.querySelector("[data-drawer-hide]");
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener("click", closeDrawer);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ function getLocation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update(data) {
|
function update(data) {
|
||||||
|
if (data.data.message != "error") {
|
||||||
document.getElementById("loading").style = "display: none";
|
document.getElementById("loading").style = "display: none";
|
||||||
document.getElementById("currencyExchange").style = "display: flex";
|
document.getElementById("currencyExchange").style = "display: flex";
|
||||||
|
|
||||||
@@ -37,6 +38,14 @@ function update(data) {
|
|||||||
|
|
||||||
dropdown.appendChild(listItem);
|
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) {
|
||||||
@@ -65,5 +74,11 @@ async function getLatestExchange(position) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function error(err) {
|
function error(err) {
|
||||||
console.error("Geolocation error: ", err);
|
const data = {
|
||||||
|
data: {
|
||||||
|
message: "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
function editPlan(planId) {
|
||||||
|
if (planId) {
|
||||||
|
window.location.href = `/plans/${planId}/edit`;
|
||||||
|
} else {
|
||||||
|
console.error('editPlan called without a planId');
|
||||||
|
alert('Cannot edit plan: Plan ID is missing.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePlan(planId) {
|
||||||
|
if (!planId) {
|
||||||
|
console.error('deletePlan called without a planId');
|
||||||
|
alert('Cannot delete plan: Plan ID is missing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm('Are you sure you want to delete this plan? This action cannot be undone.')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/plans/${planId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
alert('Plan deleted successfully.');
|
||||||
|
window.location.href = '/plans'; // Redirect to the plans list page
|
||||||
|
} else {
|
||||||
|
alert(`Failed to delete plan: ${result.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting plan:', error);
|
||||||
|
alert('An error occurred while trying to delete the plan. Please check the console for details and ensure the server is running.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ function lockAccount() {
|
|||||||
document.getElementById("password").disabled = true;
|
document.getElementById("password").disabled = true;
|
||||||
document.getElementById("repassword").disabled = true;
|
document.getElementById("repassword").disabled = true;
|
||||||
document.getElementById("save-account").classList.add("cursor-not-allowed");
|
document.getElementById("save-account").classList.add("cursor-not-allowed");
|
||||||
|
document.getElementById("save-account").classList.remove("cursor-pointer");
|
||||||
|
|
||||||
document.getElementById("edit-account").innerHTML = "Edit";
|
document.getElementById("edit-account").innerHTML = "Edit";
|
||||||
document.getElementById("edit-account").onclick = unlockAccount;
|
document.getElementById("edit-account").onclick = unlockAccount;
|
||||||
@@ -30,7 +31,12 @@ function lockPersonal() {
|
|||||||
document.getElementById("ms-married").disabled = true;
|
document.getElementById("ms-married").disabled = true;
|
||||||
document.getElementById("ms-divorced").disabled = true;
|
document.getElementById("ms-divorced").disabled = true;
|
||||||
document.getElementById("ms-widowed").disabled = true;
|
document.getElementById("ms-widowed").disabled = true;
|
||||||
|
document.getElementById("income").disabled = true;
|
||||||
|
document.getElementById("expenses").disabled = true;
|
||||||
|
document.getElementById("assets").disabled = true;
|
||||||
|
document.getElementById("liabilities").disabled = true;
|
||||||
document.getElementById("save-personal").classList.add("cursor-not-allowed");
|
document.getElementById("save-personal").classList.add("cursor-not-allowed");
|
||||||
|
document.getElementById("save-personal").classList.remove("cursor-pointer");
|
||||||
|
|
||||||
document.getElementById("edit-personal").innerHTML = "Edit";
|
document.getElementById("edit-personal").innerHTML = "Edit";
|
||||||
document.getElementById("edit-personal").onclick = unlockPersonal;
|
document.getElementById("edit-personal").onclick = unlockPersonal;
|
||||||
@@ -42,11 +48,12 @@ function lockPersonal() {
|
|||||||
*/
|
*/
|
||||||
function unlockAccount() {
|
function unlockAccount() {
|
||||||
document.getElementById("save-account").disabled = false;
|
document.getElementById("save-account").disabled = false;
|
||||||
// document.getElementById("email").disabled = false;
|
document.getElementById("email").disabled = false;
|
||||||
document.getElementById("name").disabled = false;
|
document.getElementById("name").disabled = false;
|
||||||
document.getElementById("password").disabled = false;
|
document.getElementById("password").disabled = false;
|
||||||
document.getElementById("repassword").disabled = false;
|
document.getElementById("repassword").disabled = false;
|
||||||
document.getElementById("save-account").classList.remove("cursor-not-allowed");
|
document.getElementById("save-account").classList.remove("cursor-not-allowed");
|
||||||
|
document.getElementById("save-account").classList.add("cursor-pointer");
|
||||||
|
|
||||||
document.getElementById("edit-account").innerHTML = "Cancel changes";
|
document.getElementById("edit-account").innerHTML = "Cancel changes";
|
||||||
document.getElementById("edit-account").onclick = lockAccount;
|
document.getElementById("edit-account").onclick = lockAccount;
|
||||||
@@ -64,7 +71,12 @@ function unlockPersonal() {
|
|||||||
document.getElementById("ms-married").disabled = false;
|
document.getElementById("ms-married").disabled = false;
|
||||||
document.getElementById("ms-divorced").disabled = false;
|
document.getElementById("ms-divorced").disabled = false;
|
||||||
document.getElementById("ms-widowed").disabled = false;
|
document.getElementById("ms-widowed").disabled = false;
|
||||||
|
document.getElementById("income").disabled = false;
|
||||||
|
document.getElementById("expenses").disabled = false;
|
||||||
|
document.getElementById("assets").disabled = false;
|
||||||
|
document.getElementById("liabilities").disabled = false;
|
||||||
document.getElementById("save-personal").classList.remove("cursor-not-allowed");
|
document.getElementById("save-personal").classList.remove("cursor-not-allowed");
|
||||||
|
document.getElementById("save-personal").classList.add("cursor-pointer");
|
||||||
|
|
||||||
document.getElementById("edit-personal").innerHTML = "Cancel changes";
|
document.getElementById("edit-personal").innerHTML = "Cancel changes";
|
||||||
document.getElementById("edit-personal").onclick = lockPersonal;
|
document.getElementById("edit-personal").onclick = lockPersonal;
|
||||||
@@ -72,4 +84,4 @@ function unlockPersonal() {
|
|||||||
|
|
||||||
// On page load, ensure forms are locked and reset
|
// On page load, ensure forms are locked and reset
|
||||||
lockAccount();
|
lockAccount();
|
||||||
// lockPersonal();
|
lockPersonal();
|
||||||
+293
-52
@@ -1,5 +1,6 @@
|
|||||||
const getRates = require("../util/exchangeRate");
|
const getRates = require("../util/exchangeRate");
|
||||||
const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations");
|
const { calculateProgress, updatePlanProgressInDB, updateProgress } = 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");
|
||||||
@@ -55,6 +56,12 @@ const getAssetSchema = (type) => {
|
|||||||
return assetSchema;
|
return assetSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function takes in a number and formats it to currency
|
||||||
|
* @param {integer}
|
||||||
|
* @returns nuber formatted in currency
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} middleware handler
|
* @param {function} middleware handler
|
||||||
* @param {MongoClient.collection} users db collection
|
* @param {MongoClient.collection} users db collection
|
||||||
@@ -71,6 +78,7 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
router.use(middleware);
|
router.use(middleware);
|
||||||
|
|
||||||
router.get('/home', async (req, res) => {
|
router.get('/home', async (req, res) => {
|
||||||
|
let user = req.session.user._id;
|
||||||
// if no session with geoData
|
// if no session with geoData
|
||||||
if (!req.session.geoData) {
|
if (!req.session.geoData) {
|
||||||
req.session.geoData = {
|
req.session.geoData = {
|
||||||
@@ -78,14 +86,26 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
toCurrencyRates: [],
|
toCurrencyRates: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray();
|
||||||
|
|
||||||
res.render('dashboard', { user: req.user, geoData: req.session.geoData });
|
for (const plan of userPlansFromDB) {
|
||||||
|
const progress = await calculateProgress(plan, assets, users, req.session.user._id);
|
||||||
|
await updatePlanProgressInDB(plan._id, progress.percentage, plans);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray();
|
||||||
|
|
||||||
|
res.render('dashboard', {
|
||||||
|
user: req.session.user,
|
||||||
|
geoData: req.session.geoData,
|
||||||
|
plans: updatedUserPlans,
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
||||||
@@ -98,20 +118,12 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
router.get('/plans', async (req, res) => {
|
router.get('/plans', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray();
|
|
||||||
|
|
||||||
// Use a for...of loop for proper async/await behavior in series for updates
|
const updatedUserPlans = await updateProgress(plans, assets, users, req.session.user._id);
|
||||||
for (const plan of userPlansFromDB) {
|
|
||||||
const percentage = await calculatePlanProgress(plan, assets, req.session.user._id);
|
|
||||||
await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-fetch plans to get updated progress for rendering
|
|
||||||
const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray();
|
|
||||||
|
|
||||||
res.render('plans', {
|
res.render('plans', {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
plans: updatedUserPlans, // Send the most up-to-date plans
|
plans: updatedUserPlans,
|
||||||
geoData: req.session.geoData
|
geoData: req.session.geoData
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -125,34 +137,28 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
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.userId), _id: new ObjectId(planId) });
|
||||||
const plan = await plans.findOne({ userId: new ObjectId(req.session.user._id), _id: new ObjectId(planId) });
|
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
console.log(`Plan not found with ID: ${planId} for user: ${req.session.user.email}`);
|
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
// The plan.progress should be up-to-date from the database as it was updated in the /plans route
|
const progress = await calculateProgress(plan, assets, users, req.session.userId);
|
||||||
// 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),
|
|
||||||
// you could do it here:
|
|
||||||
// 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
|
|
||||||
|
|
||||||
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,
|
||||||
geoData: req.session.geoData,
|
geoData: req.session.geoData,
|
||||||
assets: userAssets,
|
assets: userAssets,
|
||||||
|
progress: progress,
|
||||||
|
suggestions: await suggestions.generateSuggestions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -163,7 +169,7 @@ 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');
|
||||||
}
|
}
|
||||||
@@ -190,12 +196,13 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
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(', ');
|
const errorMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
|
||||||
res.status(status.BadRequest).redirect("/newPlan");
|
res.status(status.BadRequest).json({ success: false, message: errorMessage });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newPlan = {
|
|
||||||
userId: new ObjectId(req.session.user._id),
|
const planToInsert = {
|
||||||
|
userId: new ObjectId(req.session.userId),
|
||||||
name: value.name,
|
name: value.name,
|
||||||
retirementAge: value.retirementAge,
|
retirementAge: value.retirementAge,
|
||||||
retirementExpenses: value.retirementExpenses,
|
retirementExpenses: value.retirementExpenses,
|
||||||
@@ -205,17 +212,127 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await plans.insertOne({ userId: new ObjectId(req.session.user._id), ...newPlan });
|
await plans.insertOne(planToInsert);
|
||||||
req.session.errMessage = "";
|
res.status(status.Ok).json({ success: true, message: "Plan created successfully." });
|
||||||
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.";
|
res.status(status.InternalServerError).json({ success: false, message: "An error occurred while saving your plan. Please try again." });
|
||||||
res.status(status.InternalServerError).redirect("/newPlan");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.delete('/plans/:id', async (req, res) => {
|
||||||
|
const planId = req.params.id;
|
||||||
|
const plan = await plans.findOne({ _id: new ObjectId(planId) });
|
||||||
|
if(!plan) {
|
||||||
|
console.log(`Plan not found with ID: ${planId}`);
|
||||||
|
res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to delete it." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(plan.userId.toString() !== req.session.userId) {
|
||||||
|
console.log(`User ${req.session.userId} does not have permission to delete plan ${planId}`);
|
||||||
|
res.status(status.Forbidden).json({ success: false, message: "You do not have permission to delete this plan." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await plans.deleteOne({ _id: new ObjectId(planId) });
|
||||||
|
res.status(status.Ok).json({ success: true, message: "Plan deleted successfully." });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Error deleting plan:", err);
|
||||||
|
res.status(status.InternalServerError).json({ success: false, message: "An error occurred while deleting your plan. Please try again." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/plans/:id/edit', async (req, res) => {
|
||||||
|
const planId = req.params.id;
|
||||||
|
const plan = await plans.findOne({ _id: new ObjectId(planId) });
|
||||||
|
if(!plan) {
|
||||||
|
console.log(`Plan not found with ID: ${planId}`);
|
||||||
|
res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to edit it." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(plan.userId.toString() !== req.session.userId) {
|
||||||
|
console.log(`User ${req.session.userId} does not have permission to edit plan ${planId}`);
|
||||||
|
res.status(status.Forbidden).json({ success: false, message: "You do not have permission to edit this plan." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.render('editPlan', {
|
||||||
|
user: req.session.user,
|
||||||
|
plan: plan,
|
||||||
|
geoData: req.session.geoData
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/plans/:id/edit', async (req, res) => {
|
||||||
|
const planId = req.params.id;
|
||||||
|
const plan = await plans.findOne({ _id: new ObjectId(planId) });
|
||||||
|
if(!plan) {
|
||||||
|
console.log(`Plan not found with ID: ${planId}`);
|
||||||
|
res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to edit it." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(plan.userId.toString() !== req.session.userId) {
|
||||||
|
console.log(`User ${req.session.userId} does not have permission to edit plan ${planId}`);
|
||||||
|
res.status(status.Forbidden).json({ success: false, message: "You do not have permission to edit this plan." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const planSchema = joi.object({
|
||||||
|
name: joi.string().min(3).max(100).required(),
|
||||||
|
retirementAge: joi.number().min(18).max(120).required(),
|
||||||
|
retirementExpenses: joi.number().min(0).required(),
|
||||||
|
retirementAssets: joi.number().min(0).required(),
|
||||||
|
retirementLiabilities: joi.number().min(0).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object for Joi validation - only fields from req.body
|
||||||
|
const dataToValidate = {
|
||||||
|
name: req.body.name,
|
||||||
|
retirementAge: Number(req.body.retirementAge),
|
||||||
|
retirementExpenses: parseFloat(req.body.retirementExpenses),
|
||||||
|
retirementAssets: parseFloat(req.body.retirementAssets),
|
||||||
|
retirementLiabilities: parseFloat(req.body.retirementLiabilities),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error, value } = planSchema.validate(dataToValidate);
|
||||||
|
if (error) {
|
||||||
|
console.error("Plan validation error:", error.details);
|
||||||
|
const errorMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
|
||||||
|
res.status(status.BadRequest).json({ success: false, message: errorMessage });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressCalculated = await calculateProgress(value, assets, users, req.session.userId);
|
||||||
|
|
||||||
|
const planToSet = {
|
||||||
|
name: value.name,
|
||||||
|
retirementAge: value.retirementAge,
|
||||||
|
retirementExpenses: value.retirementExpenses,
|
||||||
|
retirementAssets: value.retirementAssets,
|
||||||
|
retirementLiabilities: value.retirementLiabilities,
|
||||||
|
progress: progressCalculated.percentage,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await plans.updateOne(
|
||||||
|
{ _id: new ObjectId(planId), userId: new ObjectId(req.session.userId) },
|
||||||
|
{ $set: planToSet }
|
||||||
|
);
|
||||||
|
await updateProgress(plans, assets, users, req.session.user._id);
|
||||||
|
res.status(status.Ok).json({ success: true, message: "Plan updated successfully." });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Error updating plan:", err);
|
||||||
|
res.status(status.InternalServerError).json({ success: false, message: "An error occurred while updating your plan. Please try again." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/fact', async (req, res) => {
|
||||||
|
const factInput = req.body.fact;
|
||||||
|
const fact = await suggestions.generateFact(factInput);
|
||||||
|
return res.status(status.Ok).json({ fact });
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/more', (req, res) => {
|
router.get('/more', (req, res) => {
|
||||||
res.render('more', {
|
res.render('more', {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
@@ -265,15 +382,17 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
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);
|
||||||
|
|
||||||
|
let referrer = req.get('Referrer') || "/home";
|
||||||
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");
|
let redirect = referrer.includes("?profile") ? "/questionnaire?profile" : "/questionnaire";
|
||||||
|
res.status(status.BadRequest).redirect(redirect);
|
||||||
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,
|
||||||
@@ -288,23 +407,43 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
}
|
}
|
||||||
).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.errMessage = "";
|
req.session.errMessage = "";
|
||||||
res.status(status.Ok).redirect("/home");
|
req.session.user = null; // set user to null so middleware updates user
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to save session: ", err);
|
||||||
|
|
||||||
|
return req.session.destroy((err) => {
|
||||||
|
req.session.errMessage = "Failed to save session, please login again.";
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to destroy session: ", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status.InternalServerError);
|
||||||
|
return res.redirect("/login");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let redirect = referrer.includes("?profile") ? "/profile" :
|
||||||
|
referrer != "/home" ? "/plans" : referrer;
|
||||||
|
return res.status(status.Ok).redirect(redirect);
|
||||||
|
});
|
||||||
|
|
||||||
}).catch(err => {
|
}).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");
|
let redirect = referrer.includes("?profile") ? "/questionnaire?profile" : "/questionnaire";
|
||||||
|
res.status(status.InternalServerError).redirect(redirect);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -325,6 +464,7 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let update = {
|
let update = {
|
||||||
|
email: req.body.email,
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,21 +478,40 @@ 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 = "";
|
||||||
|
req.session.user = null; // set user to null so middleware updates user
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to save session: ", err);
|
||||||
|
|
||||||
|
return req.session.destroy((err) => {
|
||||||
|
req.session.errMessage = "Failed to save session, please login again.";
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to destroy session: ", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status.InternalServerError);
|
||||||
|
return res.redirect("/login");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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.";
|
||||||
@@ -360,7 +519,81 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/createAsset", async (req, res) => {
|
router.post("/updatePersonal", (req, res) => {
|
||||||
|
const questionnaireSchema = joi.object({
|
||||||
|
dob: joi.date().required(),
|
||||||
|
education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(),
|
||||||
|
maritalStatus: joi.string().valid('single', 'married', 'divorced', 'widowed').required(),
|
||||||
|
income: joi.number().min(0).required(),
|
||||||
|
expenses: joi.number().min(0).required(),
|
||||||
|
assets: joi.number().min(0).required(),
|
||||||
|
liabilities: joi.number().min(0).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationOptions = { convert: true, abortEarly: false };
|
||||||
|
const { error, value } = questionnaireSchema.validate(req.body, validationOptions);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Personal info validation error:", error.details);
|
||||||
|
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
|
||||||
|
res.status(status.BadRequest).redirect("/profile");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
users.updateOne(
|
||||||
|
{ _id: new ObjectId(req.session.userId) },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
financialData: true,
|
||||||
|
dob: value.dob,
|
||||||
|
education: value.education,
|
||||||
|
maritalStatus: value.maritalStatus,
|
||||||
|
income: value.income,
|
||||||
|
expenses: value.expenses,
|
||||||
|
assets: value.assets,
|
||||||
|
liabilities: value.liabilities,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then((result) => {
|
||||||
|
if (result.matchedCount === 0) {
|
||||||
|
console.log(`User not found during personal info update: ${req.session.userId}`);
|
||||||
|
req.session.errMessage = "User session invalid. Please log in again.";
|
||||||
|
res.status(status.NotFound).redirect("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.modifiedCount === 0 && result.matchedCount === 1) {
|
||||||
|
console.log(`User personal info unchanged (already up-to-date): ${req.session.userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.errMessage = "";
|
||||||
|
req.session.user = null; // set user to null so middleware updates user
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to save session: ", err);
|
||||||
|
|
||||||
|
return req.session.destroy((err) => {
|
||||||
|
req.session.errMessage = "Failed to save session, please login again.";
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error("Failed to destroy session: ", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status.InternalServerError);
|
||||||
|
return res.redirect("/login");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(status.Ok).redirect("/profile");
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Error updating personal info in database:", err);
|
||||||
|
req.session.errMessage = "An error occurred while saving your information. Please try again.";
|
||||||
|
res.status(status.InternalServerError).redirect("/profile");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/createAsset", (req, res) => {
|
||||||
// Create asset, each asset has different data structure based on type
|
// Create asset, each asset has different data structure based on type
|
||||||
const type = req.body.type;
|
const type = req.body.type;
|
||||||
const assetSchema = getAssetSchema(type);
|
const assetSchema = getAssetSchema(type);
|
||||||
@@ -380,7 +613,7 @@ module.exports = (middleware, users, plans, 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(),
|
||||||
};
|
};
|
||||||
@@ -406,7 +639,7 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
return res.status(status.Ok).redirect("/assets");
|
return res.status(status.Ok).redirect("/assets");
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/updateAsset", async (req, res) => {
|
router.post("/updateAsset", (req, res) => {
|
||||||
const type = req.body.type;
|
const type = req.body.type;
|
||||||
const assetSchema = getAssetSchema(type);
|
const assetSchema = getAssetSchema(type);
|
||||||
|
|
||||||
@@ -424,7 +657,7 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
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");
|
||||||
@@ -494,20 +727,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}`);
|
||||||
@@ -538,6 +771,12 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
country = data.results[0].formatted_address;
|
country = data.results[0].formatted_address;
|
||||||
let results = await getRates(country);
|
let results = await getRates(country);
|
||||||
|
|
||||||
|
if (!results.exRates) {
|
||||||
|
req.session.geoData = {
|
||||||
|
message: "error"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
req.session.geoData = {
|
req.session.geoData = {
|
||||||
country: results.abbreviation,
|
country: results.abbreviation,
|
||||||
toCurrencyRates: results.exRates,
|
toCurrencyRates: results.exRates,
|
||||||
@@ -545,6 +784,8 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(status.Ok).send({ data: req.session.geoData });
|
return res.status(status.Ok).send({ data: req.session.geoData });
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,87 @@ async function updatePlanProgressInDB(planId, percentage, plans) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { calculatePlanProgress, updatePlanProgressInDB };
|
|
||||||
|
async function calculateProgress(plan, assets, users, userId) {
|
||||||
|
if (!plan || typeof plan !== 'object') {
|
||||||
|
console.error("Error with the plan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!assets || typeof assets.find !== 'function') {
|
||||||
|
console.error("Error with the assets collection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!users || typeof users.findOne !== 'function') {
|
||||||
|
console.error("Error with the users collection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!userId) {
|
||||||
|
console.error("No user ID provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userAssets = await assets.find({ userId: new ObjectId(userId) }).toArray();
|
||||||
|
const totalUserAssetValue = userAssets.reduce((total, asset) => total + asset.value, 0);
|
||||||
|
const totalUserPlanValue = plan.retirementAssets;
|
||||||
|
|
||||||
|
const userDoc = await users.findOne({ _id: new ObjectId(userId) });
|
||||||
|
|
||||||
|
if (!userDoc || !userDoc.dob) {
|
||||||
|
console.error("[calculateProgress] User document or DOB not found for userId:", userId);
|
||||||
|
// Ensure a structured return even on error to avoid undefined.progress issues
|
||||||
|
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
|
||||||
|
}
|
||||||
|
const userDob = new Date(userDoc.dob);
|
||||||
|
|
||||||
|
if (isNaN(userDob.getTime())) {
|
||||||
|
console.error("[calculateProgress] userDob is an invalid date. Aborting calculation.");
|
||||||
|
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const userUnalivedBy = new Date(userDob);
|
||||||
|
userUnalivedBy.setFullYear(userUnalivedBy.getFullYear() + 90);
|
||||||
|
const yearOfRetirement = userDob.getFullYear() + plan.retirementAge;
|
||||||
|
const monthsUntilRetirement = (yearOfRetirement - today.getFullYear()) * 12;
|
||||||
|
const yearsRetired = userUnalivedBy.getFullYear() - yearOfRetirement;
|
||||||
|
|
||||||
|
const totalCostOfRetirement = ((plan.retirementExpenses + plan.retirementLiabilities) * 12) * yearsRetired;
|
||||||
|
|
||||||
|
const monthlyInvestment = (totalUserPlanValue - totalUserAssetValue + totalCostOfRetirement) / monthsUntilRetirement;
|
||||||
|
|
||||||
|
const percentageCalculated = (totalUserAssetValue / (totalUserPlanValue + totalCostOfRetirement)) * 100;
|
||||||
|
|
||||||
|
const progress = {};
|
||||||
|
|
||||||
|
progress.monthlyInvestment = Math.round(monthlyInvestment);
|
||||||
|
progress.totalCostOfRetirement = totalCostOfRetirement;
|
||||||
|
progress.monthsUntilRetirement = monthsUntilRetirement;
|
||||||
|
progress.yearsRetired = yearsRetired;
|
||||||
|
progress.yearsUntilRetirement = (yearOfRetirement - today.getFullYear());
|
||||||
|
progress.percentage = percentageCalculated;
|
||||||
|
|
||||||
|
return progress;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in calculateProgress:", err);
|
||||||
|
// Ensure a structured return even on error to avoid undefined.progress issues
|
||||||
|
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProgress(plans, assets, users, userId) {
|
||||||
|
const userPlansFromDB = await plans.find({ userId: new ObjectId(userId) }).toArray();
|
||||||
|
|
||||||
|
for (const plan of userPlansFromDB) {
|
||||||
|
const progress = await calculateProgress(plan, assets, users, userId);
|
||||||
|
await updatePlanProgressInDB(plan._id, progress.percentage, plans);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUserPlans = await plans.find({ userId: new ObjectId(userId) }).toArray();
|
||||||
|
return updatedUserPlans;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { calculatePlanProgress, updatePlanProgressInDB, calculateProgress, updateProgress};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const { GoogleGenAI } = require("@google/genai");
|
||||||
|
|
||||||
|
const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });
|
||||||
|
|
||||||
|
async function generateFact(factInput) {
|
||||||
|
const response = await ai.models.generateContent({
|
||||||
|
model: "gemini-2.0-flash",
|
||||||
|
contents: `Generate a concise investment fact about "${factInput}". If "${factInput}" is not directly investment-related, provide a general, useful investment fact instead. Deliver only the fact itself, with no extra text or explanation.`,
|
||||||
|
});
|
||||||
|
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 = {
|
||||||
|
generateFact,
|
||||||
|
generateSuggestions
|
||||||
|
};
|
||||||
+22
-9
@@ -1,19 +1,24 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
<%- include("./partials/fileHeader") %>
|
||||||
<%- include("./partials/headerStart") %>
|
|
||||||
|
|
||||||
<main>
|
<% if (!geoData) { %>
|
||||||
<section class="bg-white dark:bg-gray-900 py-12 md:py-20">
|
<%- include("./partials/headerStart") %>
|
||||||
|
<% } else { %>
|
||||||
|
<%- include("./partials/header") %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<main class="pt-24 pb-16">
|
||||||
|
<section class="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 md:text-5xl lg:text-6xl text-white">About RCalculator</h1>
|
||||||
<p class="mb-12 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 dark:text-gray-400">Welcome to RCalculator, your partner in building a secure and fulfilling financial future.</p>
|
<p class="mb-12 text-lg font-normal lg:text-xl sm:px-16 text-gray-400">Welcome to RCalculator, your partner in building a secure and fulfilling financial future.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-screen-md mx-auto px-4">
|
<div class="max-w-screen-md mx-auto px-4">
|
||||||
<h2 class="mb-4 text-3xl font-bold tracking-tight text-gray-900 dark:text-white text-center">Our Mission</h2>
|
<h2 class="mb-4 text-3xl font-bold tracking-tight text-white text-center">Our Mission</h2>
|
||||||
<p class="mb-6 font-normal text-gray-600 dark:text-gray-400 text-lg text-center">
|
<p class="mb-6 font-normal text-gray-400 text-lg text-center">
|
||||||
Our mission is to empower you with the tools and insights needed to take control of your long-term financial goals.
|
Our mission is to empower you with the tools and insights needed to take control of your long-term financial goals.
|
||||||
</p>
|
</p>
|
||||||
<div class="prose lg:prose-lg dark:prose-invert mx-auto text-gray-600 dark:text-gray-400">
|
<div class="prose lg:prose-lg mx-auto text-gray-400">
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
We believe that planning for your desired future, especially retirement, shouldn't be daunting. It doesn't matter what your current age or career status is – starting now is what counts.
|
We believe that planning for your desired future, especially retirement, shouldn't be daunting. It doesn't matter what your current age or career status is – starting now is what counts.
|
||||||
</p>
|
</p>
|
||||||
@@ -25,15 +30,23 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if (!geoData) { %>
|
||||||
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
<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">
|
<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-900">
|
||||||
Get started
|
Get started
|
||||||
<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">
|
<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">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<% if (!geoData) { %>
|
||||||
|
<% } else { %>
|
||||||
|
<%- include("./partials/navBar") %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
+9
-329
@@ -1,157 +1,8 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
<%- include("./partials/fileHeader") %>
|
||||||
<%- include("./partials/header") %>
|
<%- include("./partials/header") %>
|
||||||
|
|
||||||
<main>
|
<main class="pt-26">
|
||||||
<!-- Create Asset popup Modal -->
|
<%- include("./partials/createAsset", { icons: icons }) %>
|
||||||
<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">
|
|
||||||
<h2 class="text-lg font-bold mb-2">Create New Asset</h2>
|
|
||||||
<form method="dialog">
|
|
||||||
<button
|
|
||||||
onclick="resetAll()"
|
|
||||||
type="submit"
|
|
||||||
class="text-center py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4">
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Asset Type</label>
|
|
||||||
<div class="mt-1 space-x-4">
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input checked onclick="selectAssetForm('create-other-asset-form')" id="asset-other" type="radio" name="type" value="other" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Other Assets</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input onclick="selectAssetForm('create-saving-asset-form')" id="asset-saving" type="radio" name="type" value="saving" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Savings</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input onclick="selectAssetForm('create-stock-asset-form')" id="asset-stock" type="radio" name="type" value="stock" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Stocks</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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="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>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<button id="dropdown-icon-button" value="" data-dropdown-toggle="dropdown-icons" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" type="button">
|
|
||||||
<img src="/static/svgs/icons/Other.svg" class="h-4 w-4 me-2" alt="Other"> Other
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div id="dropdown-icons" 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">
|
|
||||||
<ul id="dropdown" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-icon-button">
|
|
||||||
<% icons.forEach(icon => { %>
|
|
||||||
<li>
|
|
||||||
<button onclick="selectIcon(this)" type="button" value="<%= icon %>" class="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">
|
|
||||||
<span class="inline-flex items-center">
|
|
||||||
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<% }); %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Asset Name</label>
|
|
||||||
<input required id="name" type="text" name="name" minlength="3" maxlength="30" 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">
|
|
||||||
|
|
||||||
<label for="value" class="block text-sm font-medium text-gray-700 mt-2">Asset Value</label>
|
|
||||||
<input required placeholder="CAD" min="0" id="value" type="number" name="value" 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">
|
|
||||||
|
|
||||||
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Date of Purchase</label>
|
|
||||||
<input required type="date" id="purchaseDate" name="purchaseDate" 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">
|
|
||||||
|
|
||||||
<label for="description" class="block text-sm font-medium text-gray-700 mt-2">Description</label>
|
|
||||||
<textarea maxlength="240" id="description" name="description" rows="4" cols="50" 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"></textarea>
|
|
||||||
|
|
||||||
<div class="mt-6 flex flex-row justify-between">
|
|
||||||
<button
|
|
||||||
type="reset"
|
|
||||||
onclick="document.getElementById('create-other-asset-form').reset()"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Create savings asset form -->
|
|
||||||
<form style="display: none;" method="POST" action="/createAsset" id="create-saving-asset-form">
|
|
||||||
<input style="display: none;" value="saving" name="type" type="text">
|
|
||||||
|
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Savings Name</label>
|
|
||||||
<input required id="name" type="text" name="name" minlength="3" maxlength="30" 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">
|
|
||||||
|
|
||||||
<label for="value" class="block text-sm font-medium text-gray-700 mt-2">Savings Value</label>
|
|
||||||
<input required placeholder="CAD" min="0" id="value" type="number" name="value" 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">
|
|
||||||
|
|
||||||
<div class="mt-6 flex flex-row justify-between">
|
|
||||||
<button
|
|
||||||
type="reset"
|
|
||||||
onclick="document.getElementById('create-saving-asset-form').reset()"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Create stock asset form -->
|
|
||||||
<form style="display: none;" method="POST" action="/createAsset" id="create-stock-asset-form">
|
|
||||||
<input style="display: none;" value="stock" name="type" type="text">
|
|
||||||
|
|
||||||
<label for="ticker" class="block text-sm font-medium text-gray-700 mt-2">Stock Ticker</label>
|
|
||||||
<input required id="ticker" type="text" name="ticker" minlength="3" maxlength="5" 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">
|
|
||||||
|
|
||||||
<label for="price" class="block text-sm font-medium text-gray-700 mt-2">Price per Share</label>
|
|
||||||
<input required placeholder="CAD" min="0" id="price" type="number" name="price" 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">
|
|
||||||
|
|
||||||
<label for="quantity" class="block text-sm font-medium text-gray-700 mt-2">Quantity of Shares</label>
|
|
||||||
<input required type="number" min="1" id="quantity" name="quantity" 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">
|
|
||||||
|
|
||||||
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Date of Purchase</label>
|
|
||||||
<input required type="date" id="purchaseDate" name="purchaseDate" 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">
|
|
||||||
|
|
||||||
<div class="mt-6 flex flex-row justify-between">
|
|
||||||
<button
|
|
||||||
type="reset"
|
|
||||||
onclick="document.getElementById('create-stock-asset-form').reset()"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<% if (errMessage != "") { %>
|
<% if (errMessage != "") { %>
|
||||||
<div class="max-w-md mx-auto mb-10 rounded-lg shadow-md text-white font-bold bg-red-400 py-4 text-center"><%= errMessage %></div>
|
<div class="max-w-md mx-auto mb-10 rounded-lg shadow-md text-white font-bold bg-red-400 py-4 text-center"><%= errMessage %></div>
|
||||||
@@ -196,183 +47,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% assets.forEach((asset) => { %>
|
<% assets.forEach((asset) => { %>
|
||||||
<!-- Clickable asset list item -->
|
<%- include("./partials/assetPreview", { asset: asset }); %>
|
||||||
<div class="mt-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
|
|
||||||
<button onclick="document.getElementById('<%= asset._id %>-modal').showModal()" style="width: 100%; height: 100%;" class="px-4 py-4 cursor-pointer">
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<div class="mx-2">
|
|
||||||
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-6 w-6 me-2" alt="<%= asset.icon %>">
|
|
||||||
</div>
|
|
||||||
<div class="mx-2"><h3><%= asset.name %></h3></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3><strong>
|
|
||||||
$<%= asset.value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
|
|
||||||
</strong></h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Confirm delete asset modal -->
|
<%- include("./partials/assetDelete", { asset: asset }); %>
|
||||||
<dialog id="<%= asset._id %>-delete-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">
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div class="relative p-4 text-center bg-white rounded-lg shadow dark:bg-gray-800 sm:p-5">
|
|
||||||
<form method="dialog">
|
|
||||||
<button type="submit" class="text-gray-400 absolute top-2.5 right-2.5 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto 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 modal</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<svg class="text-gray-400 dark:text-gray-500 w-11 h-11 mb-3.5 mx-auto" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
|
||||||
<p class="mb-4 text-gray-500 dark:text-gray-300">Are you sure you want to delete this item?</p>
|
|
||||||
<div class="flex justify-center items-center space-x-4">
|
|
||||||
<div style="width: 100%;">
|
|
||||||
<form method="dialog" style="width: 100%;">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="py-2 px-3 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
|
||||||
>
|
|
||||||
No, cancel
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div style="width: 100%;">
|
|
||||||
<form method="POST" action="/deleteAsset">
|
|
||||||
<input id="id-delete-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="py-2 px-3 text-sm font-medium text-center text-white bg-red-600 rounded-lg hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-900"
|
|
||||||
>
|
|
||||||
Yes, I'm sure
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<!-- View/edit popup modal for asset -->
|
<%- include("./partials/assetModify", { asset: asset }); %>
|
||||||
<dialog id="<%= asset._id %>-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<div style="width: 45%;">
|
|
||||||
<form method="dialog" style="width: 100%;">
|
|
||||||
<button
|
|
||||||
style="width: 100%;"
|
|
||||||
onclick="lockAsset('<%= asset._id %>', '<%= asset.icon %>')"
|
|
||||||
type="submit"
|
|
||||||
class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div style="width: 45%;">
|
|
||||||
<button
|
|
||||||
style="width: 100%;"
|
|
||||||
type="submit"
|
|
||||||
class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 cursor-pointer"
|
|
||||||
onclick="document.getElementById('<%= asset._id %>-delete-modal').showModal()"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<% if (asset.type == "other") { %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<button id="dropdown-icon-button-<%= asset._id %>" value="" data-dropdown-toggle="dropdown-icons-<%= asset._id %>" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center 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" type="button">
|
|
||||||
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4 me-2" alt="<%= asset.icon %>"> <%= asset.icon %>
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div id="dropdown-icons-<%= asset._id %>" 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">
|
|
||||||
<ul id="dropdown-<%= asset._id %>" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-icon-button-<%= asset._id %>">
|
|
||||||
<% icons.forEach(icon => { %>
|
|
||||||
<li>
|
|
||||||
<button onclick="selectIcon(this, '<%= asset._id %>')" type="button" value="<%= icon %>" class="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">
|
|
||||||
<span class="inline-flex items-center">
|
|
||||||
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<% }); %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% } else { %>
|
|
||||||
<button disabled class="inline-flex items-center py-2.5 px-10 text-sm font-medium text-center 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" type="button">
|
|
||||||
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4" alt="<%= asset.icon %>">
|
|
||||||
</button>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-medium tracking-tight leading-none mt-3"><%= String(asset.type).charAt(0).toUpperCase() + String(asset.type).slice(1); %> Asset</h2>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="edit-<%= asset._id %>"
|
|
||||||
onclick="unlockAsset('<%= asset._id %>')"
|
|
||||||
class="py-3 px-8 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<form id="<%= asset._id %>-form" method="POST" action="/updateAsset">
|
|
||||||
<input id="type-<%= asset._id %>" name="type" type="text" hidden value="<%= asset.type %>">
|
|
||||||
<input id="id-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>">
|
|
||||||
<input id="userId-<%= asset.userId %>" name="userId" type="text" hidden value="<%= asset.userId %>">
|
|
||||||
<input id="icon-<%= asset._id %>" name="icon" type="text" hidden value="<%= asset.icon %>">
|
|
||||||
|
|
||||||
<% if (asset.type != "stock") { %>
|
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 mt-6">Name</label>
|
|
||||||
<input id="name-<%= asset._id %>" disabled type="text" name="name" value="<%= asset.name %>" class="mt-1 mb-2 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">
|
|
||||||
<% } else { %>
|
|
||||||
<label for="ticker" class="block text-sm font-medium text-gray-700 mt-2">Stock Ticker</label>
|
|
||||||
<input id="ticker-<%= asset._id %>" disabled type="text" name="ticker" value="<%= asset.ticker %>" class="mt-1 mb-2 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">
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<hr class="h-px mt-6 mb-4 bg-gray-500 border-0 mx-10">
|
|
||||||
|
|
||||||
<% if (asset.type != "stock") { %>
|
|
||||||
<label for="value" class="block text-sm font-medium text-gray-700">Value</label>
|
|
||||||
<input id="value-<%= asset._id %>" disabled type="number" name="value" value="<%= asset.value %>" 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">
|
|
||||||
<% } else { %>
|
|
||||||
<label for="price" class="block text-sm font-medium text-gray-700 mt-2">Price per Share</label>
|
|
||||||
<input id="price-<%= asset._id %>" disabled type="number" name="price" value="<%= asset.price %>" 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="quantity" class="block text-sm font-medium text-gray-700 mt-2">Quantity of Shares</label>
|
|
||||||
<input id="quantity-<%= asset._id %>" disabled type="number" name="quantity" value="<%= asset.quantity %>" 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">
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% if (asset.type == "other") { %>
|
|
||||||
<label for="description" class="block text-sm font-medium text-gray-700 mt-2">Description</label>
|
|
||||||
<textarea disabled maxlength="240" id="description-<%= asset._id %>" name="description" rows="4" cols="50" 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"><%= asset.description %></textarea>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% if (asset.type != "saving") { %>
|
|
||||||
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Purchased on</label>
|
|
||||||
<input id="purchaseDate-<%= asset._id %>" disabled type="date" name="purchaseDate" value="<%= asset.purchaseDate %>" 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">
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<small class="block text-sm font-medium text-gray-700">Last Modified: <%= asset.updatedAt %></small>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button id="save-<%= asset._id %>" disabled type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-not-allowed">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -382,9 +61,10 @@
|
|||||||
<script src="/static/scripts/assetManager.js"></script>
|
<script src="/static/scripts/assetManager.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Ensure all assets popup modals are locked and reset on page load
|
// Ensure all assets popup modals are locked and reset on page load
|
||||||
<% assets.forEach((asset) => { %>
|
let assets = <%- JSON.stringify(assets) %>;
|
||||||
lockAsset("<%= asset._id %>", "<%= asset.icon %>");
|
assets.forEach((asset) => {
|
||||||
<% }) %>
|
lockAsset(asset._id, asset.icon);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
|
|||||||
+8
-97
@@ -1,7 +1,7 @@
|
|||||||
<%- 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"
|
<a href="/assets?popup"
|
||||||
@@ -11,106 +11,17 @@
|
|||||||
class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800"
|
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>
|
type="button">Create new plan </a>
|
||||||
</div>
|
</div>
|
||||||
<!--This is the cards for plans and other things-->
|
<div class="w-full">
|
||||||
<div class="mt-12">
|
<div class="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
<!--box to put logo icon if we want on in a box-->
|
<% plans.forEach(plan=> { %>
|
||||||
<!-- <div -->
|
<%- include('./partials/dashboardBox.ejs', {thing: plan}) %>
|
||||||
<!-- 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> -->
|
|
||||||
<!--This is the cards for plans and other things-->
|
|
||||||
<div class="mt-12">
|
|
||||||
<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-->
|
|
||||||
<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-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>
|
||||||
<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> 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> 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> 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> than last
|
|
||||||
week
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-blue-gray-600">
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/static/scripts/dollarFormat.js"></script>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
<%- include("./partials/scriptLoader") %>
|
<%- include("./partials/scriptLoader") %>
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<%- include('./partials/fileHeader') %>
|
||||||
|
<%- include('./partials/header') %>
|
||||||
|
|
||||||
|
<main class="container mx-auto p-4 pt-28">
|
||||||
|
<div class="max-w-2xl mx-auto bg-gray-800 p-6 sm:p-8 rounded-xl shadow-xl">
|
||||||
|
<h2 class="text-2xl font-bold text-center text-white mb-8">Edit Plan: <%= plan.name %></h2>
|
||||||
|
|
||||||
|
<form id="editPlanForm" action="/plans/<%= plan._id %>/edit" method="POST" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-300">Plan Name</label>
|
||||||
|
<input type="text" name="name" id="name" value="<%= plan.name %>" required
|
||||||
|
class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementAge" class="block text-sm font-medium text-gray-300">Desired Retirement Age</label>
|
||||||
|
<input type="number" name="retirementAge" id="retirementAge" value="<%= plan.retirementAge %>" required min="18"
|
||||||
|
class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementExpenses" class="block text-sm font-medium text-gray-300">Estimated Monthly Expenses at Retirement (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementExpenses" id="retirementExpenses" value="<%= plan.retirementExpenses %>" required min="0" step="any"
|
||||||
|
class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementAssets" class="block text-sm font-medium text-gray-300">Target Retirement Assets (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementAssets" id="retirementAssets" value="<%= plan.retirementAssets %>" required min="0" step="any"
|
||||||
|
class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-300">Estimated Monthly Liabilities at Retirement (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementLiabilities" id="retirementLiabilities" value="<%= plan.retirementLiabilities %>" required min="0" step="any"
|
||||||
|
class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="formErrorMessages" class="text-red-400 text-sm my-2" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end space-x-4 pt-4">
|
||||||
|
<a href="/plans/<%= plan._id %>" class="inline-flex justify-center py-2 px-4 border border-gray-600 rounded-md shadow-sm bg-gray-600 text-sm font-medium text-white hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-indigo-500">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-indigo-500">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Basic client-side form submission handler to process JSON response
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('editPlanForm');
|
||||||
|
const errorMessagesDiv = document.getElementById('formErrorMessages');
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', async function(event) {
|
||||||
|
event.preventDefault(); // Prevent traditional form submission
|
||||||
|
errorMessagesDiv.textContent = '';
|
||||||
|
errorMessagesDiv.style.display = 'none';
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {};
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
data[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
// alert('Plan updated successfully!'); // Optional: show an alert
|
||||||
|
window.location.href = `/plans/<%= plan._id %>`; // Redirect to plan detail page
|
||||||
|
} else {
|
||||||
|
errorMessagesDiv.textContent = result.message || 'An error occurred while updating the plan.';
|
||||||
|
errorMessagesDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting form:', error);
|
||||||
|
errorMessagesDiv.textContent = 'A network error occurred. Please try again.';
|
||||||
|
errorMessagesDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%- include('./partials/navBar') %>
|
||||||
|
<%- include('./partials/scriptLoader') %>
|
||||||
|
<%- include('./partials/footer') %>
|
||||||
+22
-18
@@ -1,33 +1,37 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
<%- include("./partials/fileHeader") %>
|
||||||
|
<%- include("./partials/headerStart") %>
|
||||||
|
|
||||||
<main>
|
<main class="pt-12 bg-slate-100 min-h-screen">
|
||||||
<div class="flex justify-center items-center h-screen">
|
<div class="flex justify-center items-center py-10">
|
||||||
<form action="/auth/resetPass" method="POST">
|
<form action="/auth/resetPass" method="POST">
|
||||||
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
|
<div class="w-96 p-8 bg-white rounded-lg shadow-xl border border-slate-200">
|
||||||
<h1 class="text-3xl block text-center font-semibold">Reset Password</h1>
|
<h1 class="text-3xl block text-center font-semibold text-slate-700">Reset Password</h1>
|
||||||
<hr class="mt-3">
|
<hr class="mt-4 mb-6 border-slate-300">
|
||||||
|
|
||||||
<% if (!reset ) { %>
|
<% if (!reset ) { %>
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="email" class="block text-base mb-2">Email</label>
|
<label for="email" class="block text-base mb-2 text-slate-600">Email</label>
|
||||||
<input type="text" id="email" name="email"
|
<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 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Email" />
|
placeholder="Enter Email" />
|
||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="mt-3">
|
<div class="mt-4 text-center">
|
||||||
<h1 class="text-2xl block text-center font-bold text-red-600"> Reset link sent. </h1>
|
<h2 class="text-xl font-semibold text-green-600">Reset link sent.</h2>
|
||||||
|
<p class="text-sm text-slate-600 mt-2">Please check your email inbox (and spam folder).</p>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<!---->
|
|
||||||
|
|
||||||
<div class="text-red-800 font-bold" id="forgor">
|
<% if (typeof error !== 'undefined' && error && error.length > 0) { %>
|
||||||
<%= error%>
|
<div class="mt-3 text-red-600 font-semibold text-sm" id="forgor"><%= error %></div>
|
||||||
</div>
|
<% } else if (!reset) { %>
|
||||||
|
<div class="mt-3 h-5" id="forgor"></div> <% /* Placeholder for layout consistency */ %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<% if (!reset) { %>
|
<% if (!reset) { %>
|
||||||
<div class="mt-5">
|
<div class="mt-6">
|
||||||
<button type="submit"
|
<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
|
class="border-2 border-indigo-600 bg-indigo-600 text-white py-2 w-full rounded-md hover:bg-indigo-700 hover:border-indigo-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Send Reset Link</button>
|
||||||
reset Link</button>
|
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,4 +39,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-white md:text-5xl lg:text-6xl">Plan now, live better.</h1>
|
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-white md:text-5xl lg:text-6xl">Plan now, live better.</h1>
|
||||||
<p class="mb-8 text-lg font-normal text-gray-300 lg:text-xl sm:px-16 lg:px-48">Planning your retirement is a crucial step towards financial security and a happy future.</p>
|
<p class="mb-8 text-lg font-normal text-gray-300 lg:text-xl sm:px-16 lg:px-48">Planning your retirement is a crucial step towards financial security and a happy future.</p>
|
||||||
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
<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 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">
|
<a href="/signup" class="inline-flex justify-center items-center 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-900">
|
||||||
Get started
|
Get started
|
||||||
<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">
|
<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">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
||||||
|
|||||||
+27
-22
@@ -1,40 +1,45 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
<%- include("./partials/fileHeader") %>
|
||||||
|
<%- include("./partials/headerStart") %>
|
||||||
|
|
||||||
<main>
|
<main class="pt-12">
|
||||||
<div class="flex justify-center items-center h-screen">
|
<div class="flex justify-center items-center py-10">
|
||||||
<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-8 bg-white rounded-lg shadow-xl border border-slate-200">
|
||||||
<h1 class="text-3xl block text-center font-semibold">Login</h1>
|
<h1 class="text-3xl block text-center font-semibold text-slate-700">Login</h1>
|
||||||
<hr class="mt-3">
|
<hr class="mt-4 mb-6 border-slate-300">
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="email" class="block text-base mb-2">Email</label>
|
<label for="email" class="block text-base mb-2 text-slate-600">Email</label>
|
||||||
<input type="text" id="email" name="email"
|
<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 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Email" />
|
placeholder="Enter Email" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="password" class="block text-base mb-2">Password</label>
|
<label for="password" class="block text-base mb-2 text-slate-600">Password</label>
|
||||||
<input type="password" id="password" name="password"
|
<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 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Password" />
|
placeholder="Enter Password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-red-800 font-bold" id="forgor"> <%= errMessage %></div>
|
<% if (errMessage && errMessage.length > 0) { %>
|
||||||
<div class="mt-3 flex justify-between items-center">
|
<div class="mt-3 text-red-600 font-semibold text-sm" id="forgor"> <%= errMessage %></div>
|
||||||
<div>
|
<% } else { %>
|
||||||
<input type="checkbox">
|
<div class="mt-3 h-5" id="forgor"></div> <% /* Placeholder to maintain layout when no error */ %>
|
||||||
<label>Remember me</label>
|
<% } %>
|
||||||
|
<div class="mt-4 flex justify-between items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="rememberMe" class="h-4 w-4 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500">
|
||||||
|
<label for="rememberMe" class="ml-2 text-sm text-slate-600">Remember me</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="/forgotPassword" class="text-indigo-600 font-semibold">Forgot Password? </a>
|
<a href="/forgotPassword" class="text-sm text-indigo-600 hover:text-indigo-500 font-semibold transition-colors duration-200">Forgot Password? </a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-6">
|
||||||
<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">Login</button>
|
class="border-2 border-indigo-600 bg-indigo-600 text-white py-2 w-full rounded-md hover:bg-indigo-700 hover:border-indigo-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Login</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 block justify-between items-center">
|
<div class="mt-6 text-center">
|
||||||
<label>Don't have an account?</label>
|
<span class="text-sm text-slate-600">Don't have an account?</span>
|
||||||
<a href="/signup" class="text-indigo-600 font-semibold">Register</a>
|
<a href="/signup" class="text-sm text-indigo-600 hover:text-indigo-500 font-semibold ml-1 transition-colors duration-200">Register</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+46
-3
@@ -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,42 +0,0 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
|
||||||
<%- include("./partials/header") %>
|
|
||||||
|
|
||||||
<main class="container mx-auto p-4">
|
|
||||||
<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">
|
|
||||||
<h3 class="text-lg font-medium mb-6">Retirement Plan</h3>
|
|
||||||
<form action="/newPlan" method="post" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Plan Name</label>
|
|
||||||
<input type="text" name="name" placeholder="e.g., Retirement Plan" 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">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="retirementAge" class="block text-sm font-medium text-gray-700">Desired Retirement Age</label>
|
|
||||||
<input type="number" name="retirementAge" placeholder="e.g., 65" min="18" max="120" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementExpenses" class="block text-sm font-medium text-gray-700">Estimated Monthly Retirement Expenses</label>
|
|
||||||
<input type="number" name="retirementExpenses" min="0" placeholder="e.g., 3000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementAssets" class="block text-sm font-medium text-gray-700">Estimated Retirement Assets</label>
|
|
||||||
<input type="number" name="retirementAssets" min="0" placeholder="e.g., 500000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-700">Estimated Retirement Liabilities</label>
|
|
||||||
<input type="number" name="retirementLiabilities" min="0" placeholder="e.g., 50000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Save Plan</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
|
||||||
<%- include("./partials/scriptLoader") %>
|
|
||||||
<%- include("./partials/footer") %>
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-white md:text-5xl lg:text-6xl">404 - Not found</h1>
|
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-white md:text-5xl lg:text-6xl">404 - Not found</h1>
|
||||||
<p class="mb-8 text-lg font-normal text-gray-300 lg:text-xl sm:px-16 lg:px-48">It appears you stumbled accross a misleading page.</p>
|
<p class="mb-8 text-lg font-normal text-gray-300 lg:text-xl sm:px-16 lg:px-48">It appears you stumbled accross a misleading page.</p>
|
||||||
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
||||||
<a href="/" class="inline-flex justify-center items-center 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">
|
<a href="/" class="inline-flex justify-center items-center 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-900">
|
||||||
Go home
|
Go home
|
||||||
<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">
|
<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">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!-- Confirm delete asset modal -->
|
||||||
|
<dialog id="<%= asset._id %>-delete-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">
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 text-center rounded-lg shadow bg-gray-800 sm:p-5">
|
||||||
|
<form method="dialog">
|
||||||
|
<button type="submit" class="text-gray-500 absolute top-2.5 right-2.5 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:bg-gray-600 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 modal</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<svg class="text-gray-500 w-11 h-11 mb-3.5 mx-auto" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
||||||
|
<p class="mb-4 text-gray-300">Are you sure you want to delete this item?</p>
|
||||||
|
<div class="flex justify-center items-center space-x-4">
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<form method="dialog" style="width: 100%;">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="py-2 px-3 text-sm font-medium rounded-lg border focus:ring-4 focus:outline-none focus:ring-primary-300 focus:z-10 bg-gray-700 text-gray-300 border-gray-500 hover:text-white hover:bg-gray-600 focus:ring-gray-600"
|
||||||
|
>
|
||||||
|
No, cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<form method="POST" action="/deleteAsset">
|
||||||
|
<input id="id-delete-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="py-2 px-3 text-sm font-medium text-center text-white rounded-lg focus:ring-4 focus:outline-none bg-red-500 hover:bg-red-600 focus:ring-red-900"
|
||||||
|
>
|
||||||
|
Yes, I'm sure
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
|
||||||
|
<!-- View/edit popup modal for asset -->
|
||||||
|
<dialog id="<%= asset._id %>-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div style="width: 45%;">
|
||||||
|
<form method="dialog" style="width: 100%;">
|
||||||
|
<button
|
||||||
|
style="width: 100%;"
|
||||||
|
onclick="lockAsset('<%= asset._id %>', '<%= asset.icon %>')"
|
||||||
|
type="submit"
|
||||||
|
class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="width: 45%;">
|
||||||
|
<button
|
||||||
|
style="width: 100%;"
|
||||||
|
type="submit"
|
||||||
|
class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 cursor-pointer"
|
||||||
|
onclick="document.getElementById('<%= asset._id %>-delete-modal').showModal()"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<% if (asset.type == "other") { %>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button id="dropdown-icon-button-<%= asset._id %>" value="" data-dropdown-toggle="dropdown-icons-<%= asset._id %>" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center 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" type="button">
|
||||||
|
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4 me-2" alt="<%= asset.icon %>"> <%= asset.icon %>
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="dropdown-icons-<%= asset._id %>" class="overflow-y-auto h-40 z-10 hidden divide-y divide-gray-100 rounded-lg shadow-sm w-32 bg-gray-700">
|
||||||
|
<ul id="dropdown-<%= asset._id %>" class="py-2 text-sm text-gray-200" aria-labelledby="dropdown-icon-button-<%= asset._id %>">
|
||||||
|
<% icons.forEach(icon => { %>
|
||||||
|
<li>
|
||||||
|
<button onclick="selectIcon(this, '<%= asset._id %>')" type="button" value="<%= icon %>" class="inline-flex w-full px-4 py-2 text-sm text-gray-200 hover:bg-gray-600 hover:text-white" role="menuitem">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<button disabled class="inline-flex items-center py-2.5 px-10 text-sm font-medium text-center 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" type="button">
|
||||||
|
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4" alt="<%= asset.icon %>">
|
||||||
|
</button>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-medium tracking-tight leading-none mt-3"><%= String(asset.type).charAt(0).toUpperCase() + String(asset.type).slice(1); %> Asset</h2>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id="edit-<%= asset._id %>"
|
||||||
|
onclick="unlockAsset('<%= asset._id %>')"
|
||||||
|
class="py-3 px-8 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<form id="<%= asset._id %>-form" method="POST" action="/updateAsset">
|
||||||
|
<input id="type-<%= asset._id %>" name="type" type="text" hidden value="<%= asset.type %>">
|
||||||
|
<input id="id-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>">
|
||||||
|
<input id="userId-<%= asset.userId %>" name="userId" type="text" hidden value="<%= asset.userId %>">
|
||||||
|
<input id="icon-<%= asset._id %>" name="icon" type="text" hidden value="<%= asset.icon %>">
|
||||||
|
|
||||||
|
<% if (asset.type != "stock") { %>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mt-6">Name</label>
|
||||||
|
<input id="name-<%= asset._id %>" disabled type="text" name="name" value="<%= asset.name %>" class="mt-1 mb-2 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">
|
||||||
|
<% } else { %>
|
||||||
|
<label for="ticker" class="block text-sm font-medium text-gray-700 mt-2">Stock Ticker</label>
|
||||||
|
<input id="ticker-<%= asset._id %>" disabled type="text" name="ticker" value="<%= asset.ticker %>" class="mt-1 mb-2 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">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<hr class="h-px mt-6 mb-4 bg-gray-500 border-0 mx-10">
|
||||||
|
|
||||||
|
<% if (asset.type != "stock") { %>
|
||||||
|
<label for="value" class="block text-sm font-medium text-gray-700">Value</label>
|
||||||
|
<input id="value-<%= asset._id %>" disabled type="number" name="value" value="<%= asset.value %>" 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">
|
||||||
|
<% } else { %>
|
||||||
|
<label for="price" class="block text-sm font-medium text-gray-700 mt-2">Price per Share</label>
|
||||||
|
<input id="price-<%= asset._id %>" disabled type="number" name="price" value="<%= asset.price %>" 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="quantity" class="block text-sm font-medium text-gray-700 mt-2">Quantity of Shares</label>
|
||||||
|
<input id="quantity-<%= asset._id %>" disabled type="number" name="quantity" value="<%= asset.quantity %>" 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">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (asset.type == "other") { %>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700 mt-2">Description</label>
|
||||||
|
<textarea disabled maxlength="240" id="description-<%= asset._id %>" name="description" rows="4" cols="50" 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"><%= asset.description %></textarea>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (asset.type != "saving") { %>
|
||||||
|
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Purchased on</label>
|
||||||
|
<input id="purchaseDate-<%= asset._id %>" disabled type="date" name="purchaseDate" value="<%= asset.purchaseDate %>" 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">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<small class="block text-sm font-medium text-gray-700">Last Modified: <%= asset.updatedAt %></small>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="save-<%= asset._id %>" disabled type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-not-allowed">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- Clickable asset list item -->
|
||||||
|
<div class="mt-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
|
||||||
|
<button onclick="document.getElementById('<%= asset._id %>-modal').showModal()" style="width: 100%; height: 100%;" class="px-4 py-4 cursor-pointer">
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="mx-2">
|
||||||
|
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-6 w-6 me-2" alt="<%= asset.icon %>">
|
||||||
|
</div>
|
||||||
|
<div class="mx-2"><h3><%= asset.name %></h3></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3><strong>
|
||||||
|
$<%= asset.value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
|
||||||
|
</strong></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<!-- Create Asset popup Modal -->
|
||||||
|
<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">
|
||||||
|
<h2 class="text-lg font-bold mb-2">Create New Asset</h2>
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
onclick="resetAll()"
|
||||||
|
type="submit"
|
||||||
|
class="text-center py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Asset Type</label>
|
||||||
|
<div class="mt-1 space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input checked onclick="selectAssetForm('create-other-asset-form')" id="asset-other" type="radio" name="type" value="other" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Other Assets</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input onclick="selectAssetForm('create-saving-asset-form')" id="asset-saving" type="radio" name="type" value="saving" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Savings</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input onclick="selectAssetForm('create-stock-asset-form')" id="asset-stock" type="radio" name="type" value="stock" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Stocks</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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="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>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button id="dropdown-icon-button" value="" data-dropdown-toggle="dropdown-icons" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" type="button">
|
||||||
|
<img src="/static/svgs/icons/Other.svg" class="h-4 w-4 me-2" alt="Other"> Other
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="dropdown-icons" class="overflow-y-auto h-40 z-10 hidden divide-y divide-gray-100 rounded-lg shadow-sm w-32 bg-gray-700">
|
||||||
|
<ul id="dropdown" class="py-2 text-sm text-gray-200" aria-labelledby="dropdown-icon-button">
|
||||||
|
<% icons.forEach(icon => { %>
|
||||||
|
<li>
|
||||||
|
<button onclick="selectIcon(this)" type="button" value="<%= icon %>" class="inline-flex w-full px-4 py-2 text-sm text-gray-200 hover:bg-gray-600 hover:text-white" role="menuitem">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Asset Name</label>
|
||||||
|
<input required id="name" type="text" name="name" minlength="3" maxlength="30" 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">
|
||||||
|
|
||||||
|
<label for="value" class="block text-sm font-medium text-gray-700 mt-2">Asset Value</label>
|
||||||
|
<input required placeholder="CAD" min="0" id="value" type="number" name="value" 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">
|
||||||
|
|
||||||
|
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Date of Purchase</label>
|
||||||
|
<input required type="date" id="purchaseDate" name="purchaseDate" 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">
|
||||||
|
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700 mt-2">Description</label>
|
||||||
|
<textarea maxlength="240" id="description" name="description" rows="4" cols="50" 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"></textarea>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-row justify-between">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onclick="document.getElementById('create-other-asset-form').reset()"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Create savings asset form -->
|
||||||
|
<form style="display: none;" method="POST" action="/createAsset" id="create-saving-asset-form">
|
||||||
|
<input style="display: none;" value="saving" name="type" type="text">
|
||||||
|
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Savings Name</label>
|
||||||
|
<input required id="name" type="text" name="name" minlength="3" maxlength="30" 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">
|
||||||
|
|
||||||
|
<label for="value" class="block text-sm font-medium text-gray-700 mt-2">Savings Value</label>
|
||||||
|
<input required placeholder="CAD" min="0" id="value" type="number" name="value" 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">
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-row justify-between">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onclick="document.getElementById('create-saving-asset-form').reset()"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Create stock asset form -->
|
||||||
|
<form style="display: none;" method="POST" action="/createAsset" id="create-stock-asset-form">
|
||||||
|
<input style="display: none;" value="stock" name="type" type="text">
|
||||||
|
|
||||||
|
<label for="ticker" class="block text-sm font-medium text-gray-700 mt-2">Stock Ticker</label>
|
||||||
|
<input required id="ticker" type="text" name="ticker" minlength="3" maxlength="5" 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">
|
||||||
|
|
||||||
|
<label for="price" class="block text-sm font-medium text-gray-700 mt-2">Price per Share</label>
|
||||||
|
<input required placeholder="CAD" min="0" id="price" type="number" name="price" 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">
|
||||||
|
|
||||||
|
<label for="quantity" class="block text-sm font-medium text-gray-700 mt-2">Quantity of Shares</label>
|
||||||
|
<input required type="number" min="1" id="quantity" name="quantity" 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">
|
||||||
|
|
||||||
|
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mt-2">Date of Purchase</label>
|
||||||
|
<input required type="date" id="purchaseDate" name="purchaseDate" 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">
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-row justify-between">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onclick="document.getElementById('create-stock-asset-form').reset()"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-center w-sm py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="w-full h-full mt-3">
|
||||||
|
<a href="/plans/<%=thing._id%>"
|
||||||
|
class="block p-4 w-full h-full rounded-xl transition-shadow duration-300 ease-in-out hover:shadow-xl">
|
||||||
|
<div
|
||||||
|
class="relative flex flex-col bg-white text-gray-700 shadow-md rounded-xl w-full h-full overflow-hidden min-h-[175px]">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="p-4 flex items-center justify-between border-b ">
|
||||||
|
<p class="text-xl font-semibold text-blue-gray-700 truncate" title="<%= thing.name %>">
|
||||||
|
<%= thing.name %>
|
||||||
|
</p>
|
||||||
|
<h4 class="planGoal text-xl font-semibold whitespace-nowrap">
|
||||||
|
<%= thing.retirementAssets %>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar Section - Pushed to the bottom -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="p-4 mt-auto border-gray-900">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<p class="text-sm font-medium text-blue-gray-600">
|
||||||
|
Progress:
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-semibold justify-between text-green-500">
|
||||||
|
<%=Math.floor(thing.progress)> 100 ? 100 : Math.floor(thing.progress) %>%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-300 rounded-full h-2.5">
|
||||||
|
<div class="bg-green-500 h-2.5 rounded-full"
|
||||||
|
style="width: <%= thing.progress > 100 ? 100 : thing.progress %>%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -6,5 +6,16 @@
|
|||||||
<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">
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
|
<div class="fixed end-6 bottom-20 group">
|
||||||
|
<div id="factMenu" class="absolute right-0 bottom-full mb-2 hidden">
|
||||||
|
<div id="factContainer" class="p-4 rounded-lg shadow-md bg-gray-700 w-64">
|
||||||
|
<p class="mb-2 text-sm text-gray-300">Insert the kind of short fact you would like to know more about:</p>
|
||||||
|
<input type="text" id="factInput" placeholder="Enter your topic here" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 bg-gray-600 border-gray-500 text-white placeholder-gray-400">
|
||||||
|
<button type="button" id="factSubmitButton" class="mt-3 w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-700">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="factButton" class="flex items-center justify-center text-white rounded-full w-14 h-14 bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-800 hidden">
|
||||||
|
<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>
|
||||||
|
<span class="sr-only">Get a Fact</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16">
|
<footer class="mt-16">
|
||||||
<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="fixed bottom-0 left-0 z-30 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">
|
<div class="container mx-auto">
|
||||||
<p>© 2025 RCalculator. All rights reserved.</p>
|
<p>© 2025 RCalculator. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="/static/scripts/footer.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
<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>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,66 +1,68 @@
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<nav class="bg-gray-800 border-b border-gray-700">
|
<nav class="fixed bg-gray-800 border-b border-gray-700 z-30">
|
||||||
<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 text-white">RCalculator</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<% if (!geoData.country) { %>
|
<% if (!geoData.country) { %>
|
||||||
<div role="status" id="loading">
|
<div role="status" id="loading">
|
||||||
<svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="w-8 h-8 animate-spin text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||||
</svg>
|
</svg>
|
||||||
<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 border rounded-s-lg focus:ring-4 focus:outline-none bg-gray-700 focus:ring-gray-700 text-white 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-40 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 border rounded-e-lg focus:ring-4 focus:outline-none bg-gray-700 hover:bg-gray-600 focus:ring-gray-700 text-white border-gray-600" type="button">
|
||||||
<img src="" id="exchangeFlag" class="h-4 w-4 me-2" alt="">
|
<img src="" id="exchangeFlag" class="h-4 w-4 me-2" alt="">
|
||||||
<p id="exFlagTag"></p>
|
<p id="exFlagTag"></p>
|
||||||
<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"/>
|
||||||
</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 divide-y divide-gray-100 rounded-lg shadow-sm w-32 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-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 border rounded-s-lg focus:ring-4 focus:outline-none bg-gray-700 focus:ring-gray-700 text-white 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-40 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-s-0 border bg-gray-700 border-s-gray-700 border-gray-600 placeholder-gray-400 text-white 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 border rounded-e-lg focus:ring-4 focus:outline-none bg-gray-700 hover:bg-gray-600 focus:ring-gray-700 text-white 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"/>
|
||||||
</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 h-40 z-10 hidden divide-y divide-gray-100 rounded-lg shadow-sm w-32 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-200" aria-labelledby="dropdown-country-button">
|
||||||
<% let countries = Object.keys(geoData.toCurrencyRates);%>
|
<% let countries = Object.keys(geoData.toCurrencyRates);%>
|
||||||
<% countries.forEach(item => { %>
|
<% countries.forEach(item => { %>
|
||||||
<li>
|
<li>
|
||||||
<button onclick="switchButton(this)" type="button" value="<%= geoData.toCurrencyRates[item] %>" class="countryButton 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">
|
<button onclick="switchButton(this)" type="button" value="<%= geoData.toCurrencyRates[item] %>" class="countryButton inline-flex w-full px-4 py-2 text-sm text-gray-200 hover:bg-gray-600 hover:text-white" role="menuitem">
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
<img src="/static/svgs/flags/<%= item %>.svg" class="h-4 w-4 me-2" alt="<%= item %>"> (<%= item %>)
|
<img src="/static/svgs/flags/<%= item %>.svg" class="h-4 w-4 me-2" alt="<%= item %>"> (<%= item %>)
|
||||||
</span>
|
</span>
|
||||||
@@ -71,25 +73,68 @@
|
|||||||
</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 lg:space-x-8 rtl:space-x-reverse lg:flex-row lg:mt-0 lg:border-0 bg-gray-800 lg:bg-transparent 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 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg: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 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg: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 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg: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">
|
||||||
|
<button class="flex flex-row text-white hover:bg-blue-700 focus:ring-4 font-medium rounded-full text-sm focus:outline-none focus:ring-blue-800" type="button" id="drawer-toggle" data-drawer-target="drawer-navigation" data-drawer-placement="right" aria-controls="drawer-navigation">
|
||||||
|
<svg class="w-[30px] h-[30px] text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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>
|
||||||
|
<svg class="w-[30px] h-[30px] text-white" 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-width="2" d="M5 7h14M5 12h14M5 17h14"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- drawer component -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div id="drawer-navigation" class="lg:block fixed top-0 right-0 z-60 w-64 h-screen p-4 overflow-y-auto transition-transform translate-x-full bg-gray-800" tabindex="-1" aria-labelledby="drawer-navigation-label">
|
||||||
|
<h5 id="drawer-navigation-label" class="text-base font-semibold uppercase text-gray-400"><%= user.name %></h5>
|
||||||
|
<button type="button" data-drawer-hide="drawer-navigation" aria-controls="drawer-navigation" class="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 absolute top-2.5 end-2.5 inline-flex items-center hover:bg-gray-600 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 focus:outline-none focus:ring-4 font-medium rounded-full text-sm px-20 py-2.5 text-center me-2 mb-2 bg-blue-600 hover:bg-blue-700 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 rounded-lg text-white hover:bg-gray-700 group">
|
||||||
|
<svg class="w-5 h-5 text-gray-500 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>
|
||||||
<li>
|
<li>
|
||||||
<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>
|
<a href="aboutUs" class="flex items-center p-2 rounded-lg text-white hover:bg-gray-700 group">
|
||||||
</li>
|
<svg class="shrink-0 w-5 h-5 transition duration-75 text-gray-400 group-hover:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
|
||||||
<li>
|
<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"/>
|
||||||
<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>
|
</svg>
|
||||||
</li>
|
<span class="flex-1 ms-3 whitespace-nowrap">About Us</span>
|
||||||
<li>
|
</a>
|
||||||
<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>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<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 text-white">RCalculator</span>
|
||||||
</a>
|
</a>
|
||||||
<button data-collapse-toggle="navbar-solid-bg" type="button"
|
<button data-collapse-toggle="navbar-solid-bg" type="button"
|
||||||
class="inline-flex items-center justify-center text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
class="inline-flex items-center justify-center text-sm rounded-lg lg:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
|
||||||
aria-controls="navbar-solid-bg" aria-expanded="false">
|
aria-controls="navbar-solid-bg" aria-expanded="false">
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="none" viewBox="0 0 17 14">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="none" viewBox="0 0 17 14">
|
||||||
@@ -15,15 +15,15 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="hidden w-full lg:block lg:w-auto" id="navbar-solid-bg">
|
<div class="hidden w-full lg:block lg:w-auto" 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 lg:space-x-8 rtl:space-x-reverse lg:flex-row lg:mt-0 lg:border-0 bg-gray-800 lg:bg-transparent border-gray-700">
|
||||||
<li>
|
<li>
|
||||||
<a href="/" 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">Home</a>
|
<a href="/" class="block py-2 px-3 lg:p-0 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg:hover:bg-transparent">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/login" 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">Login</a>
|
<a href="/login" class="block py-2 px-3 lg:p-0 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg:hover:bg-transparent">Login</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="mailto:rcalculator@gmail.com" 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">Contact us</a>
|
<a href="mailto:rcalculator@gmail.com" class="block py-2 px-3 lg:p-0 rounded lg:border-0 text-white lg:hover:text-blue-500 hover:bg-gray-700 hover:text-white lg:hover:bg-transparent">Contact us</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<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-800 border-t border-gray-700 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-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-400 text-sm">Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/assets" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group">
|
<a href="/assets" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-800 group">
|
||||||
<img src="/static/svgs/assets.svg" alt="assets">
|
<img src="/static/svgs/assets.svg" alt="assets">
|
||||||
<span class="text-gray-500 dark:text-gray-400 text-sm">Assets</span>
|
<span class="text-gray-400 text-sm">Assets</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/plans" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group">
|
<a href="/plans" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-800 group">
|
||||||
<img src="/static/svgs/plans.svg" alt="plans">
|
<img src="/static/svgs/plans.svg" alt="plans">
|
||||||
<span class="text-gray-500 dark:text-gray-400 text-sm">Plans</span>
|
<span class="text-gray-400 text-sm">Plans</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/more" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group">
|
<a href="/more" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-800 group">
|
||||||
<img src="/static/svgs/more.svg" alt="more">
|
<img src="/static/svgs/more.svg" alt="more">
|
||||||
<span class="text-gray-500 dark:text-gray-400 text-sm">More</span>
|
<span class="text-gray-400 text-sm">More</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+57
-30
@@ -1,75 +1,102 @@
|
|||||||
<%- 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>
|
<div class="max-w-lg mx-auto p-6 sm:p-8 rounded-xl shadow-lg space-y-6 bg-gray-800">
|
||||||
<div class="max-w-lg mx-auto bg-white p-6 sm:p-8 rounded-xl shadow-lg space-y-6 dark:bg-gray-800">
|
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3 class="text-2xl font-bold text-gray-800 dark:text-white"><%= plan.name %></h3>
|
<h3 class="text-2xl font-bold text-white"><%= plan.name %></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between mb-1">
|
<div class="flex justify-between mb-1">
|
||||||
<span class="text-sm font-medium text-blue-700 dark:text-blue-400">Progress</span>
|
<span class="text-sm font-medium text-blue-400">Progress</span>
|
||||||
<span class="text-sm font-medium text-blue-700 dark:text-blue-400"><%= plan.progress %>%</span>
|
<span class="text-sm font-medium text-blue-400"><%-plan.progress > 100 ? 100 : plan.progress%>%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700">
|
<div class="w-full rounded-full h-3 bg-gray-700">
|
||||||
<div class="bg-blue-600 h-3 rounded-full dark:bg-blue-500" style="width: <%= plan.progress %>%;"></div>
|
<div class="h-3 rounded-full bg-blue-500" style="width: <%= plan.progress > 100 ? 100 : plan.progress %>%;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
|
<div>
|
||||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Assets:</h4>
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-sm font-medium text-blue-400">Calculated Monthly Investment</span>
|
||||||
|
<span class="text-sm font-medium text-blue-400"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(progress.monthlyInvestment > 0 ? progress.monthlyInvestment : 0) %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-700 pt-6 space-y-4">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-300 mb-3">Assets:</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Number of Assets:</label>
|
<label class="block text-sm font-medium text-gray-400">Number of Assets:</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= assets.length %></p>
|
<p class="mt-1 text-md text-white"><%= assets.length %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Total Value:</label>
|
<label class="block text-sm font-medium text-gray-400">Total Value:</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= assets.reduce((total, asset) => total + asset.value, 0) %></p>
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(assets.reduce((total, asset) => total + asset.value, 0)) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
|
<div class="border-t border-gray-700 pt-6 space-y-4">
|
||||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Plan Details:</h4>
|
<h4 class="text-lg font-semibold text-gray-300 mb-3">Plan Details:</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Retirement Age:</label>
|
<label class="block text-sm font-medium text-gray-400">Retirement Age:</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementAge %></p>
|
<p class="mt-1 text-md text-white"><%= plan.retirementAge %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Monthly Expenses:</label>
|
<label class="block text-sm font-medium0 text-gray-400">Target Monthly Expenses (Minus Liabilities):</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementExpenses %></p>
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(plan.retirementExpenses) %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Retirement Assets:</label>
|
<label class="block text-sm font-medium text-gray-400">Target Retirement Assets (Total):</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementAssets %></p>
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(plan.retirementAssets) %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Retirement Liabilities:</label>
|
<label class="block text-sm font-medium text-gray-400">Target Retirement Liabilities (Monthly):</label>
|
||||||
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementLiabilities %></p>
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(plan.retirementLiabilities) %></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Years Until Retirement:</label>
|
||||||
|
<p class="mt-1 text-md text-white"><%- progress.yearsUntilRetirement %></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Years Retired:</label>
|
||||||
|
<p class="mt-1 text-md text-white"><%- progress.yearsRetired %></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Total Cost of Retirement:</label>
|
||||||
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(progress.totalCostOfRetirement) %></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Total amount needed (Assets + Retirement Expenses): </label>
|
||||||
|
<p class="mt-1 text-md text-white"><%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(progress.totalCostOfRetirement + progress.monthlyInvestment * progress.monthsUntilRetirement) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
|
<div class="border-t border-gray-700 pt-6 space-y-4">
|
||||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Suggestions:</h4>
|
<h4 class="text-lg font-semibold text-gray-300 mb-3">Suggestions:</h4>
|
||||||
|
|
||||||
<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 border rounded-lg bg-gray-700 border-gray-600">
|
||||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
<p class="text-sm text-blue-300">
|
||||||
USE YOUR HEAD MOCK
|
<%= suggestions %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-center space-x-2">
|
||||||
|
<button type="button" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer" onclick="editPlan('<%= plan._id %>')">Edit Plan</button>
|
||||||
|
<button type="button" class="py-2 px-4 border border-red-600 rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 cursor-pointer" onclick="deletePlan('<%= plan._id %>')">Delete Plan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/static/scripts/planManager.js"></script>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
<%- include("./partials/scriptLoader") %>
|
<%- include("./partials/scriptLoader") %>
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
+130
-8
@@ -1,25 +1,147 @@
|
|||||||
<%- 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>
|
|
||||||
<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>
|
||||||
<a href="/newPlan" class="block max-w-sm p-2"><div class="text-center bg-blue-600 text-white py-1 w-full rounded-md hover:bg-blue-700 hover:text-white font-semibold">New Plan</div></a>
|
<!-- Changed link to a button to open modal -->
|
||||||
|
<button id="openNewPlanModalBtn" class="block w-full max-w-sm p-2"><div class="text-center bg-blue-600 text-white py-1 w-full rounded-md hover:bg-blue-700 hover:text-white font-semibold">New Plan</div></button>
|
||||||
<hr class="mt-3">
|
<hr class="mt-3">
|
||||||
<% 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 border rounded-lg shadow-sm bg-gray-800 border-gray-700 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-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 rounded-full h-2.5 mb-4 bg-black">
|
||||||
<div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %>%;"></div>
|
<div class=" h-2.5 rounded-full bg-green-500" style="width: <%= plan.progress > 100 ? 100 : 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-400"><%= plan.description %></p>
|
||||||
</a>
|
</a>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Plan Modal - Styled like createAsset.ejs -->
|
||||||
|
<dialog id="newPlanDialog" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
|
||||||
|
<!-- Header: Title and Cancel button -->
|
||||||
|
<div class="flex flex-row justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">Create New Retirement Plan</h3>
|
||||||
|
<form method="dialog" id="cancelNewPlanDialogForm">
|
||||||
|
<button type="submit" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Content -->
|
||||||
|
<div>
|
||||||
|
<form id="newPlanForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 text-left">Plan Name</label>
|
||||||
|
<input type="text" name="name" id="planName" placeholder="e.g., My Awesome Plan" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementAge" class="block text-sm font-medium text-gray-700 text-left">Desired Retirement Age</label>
|
||||||
|
<input type="number" name="retirementAge" id="retirementAge" placeholder="e.g., 65" min="18" max="120" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementExpenses" class="block text-sm font-medium text-gray-700 text-left">Estimated Monthly Retirement Expenses</label>
|
||||||
|
<input type="number" name="retirementExpenses" id="retirementExpenses" min="0" placeholder="e.g., 3000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementAssets" class="block text-sm font-medium text-gray-700 text-left">Estimated Retirement Assets</label>
|
||||||
|
<input type="number" name="retirementAssets" id="retirementAssets" min="0" placeholder="e.g., 500000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-700 text-left">Estimated Retirement Liabilities</label>
|
||||||
|
<input type="number" name="retirementLiabilities" id="retirementLiabilities" min="0" placeholder="e.g., 50000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div id="newPlanError" class="text-red-500 text-sm mt-2" style="display: none;"></div>
|
||||||
|
<!-- Form Buttons: Save Plan and Reset -->
|
||||||
|
<div class="mt-6 flex flex-row justify-between">
|
||||||
|
<button type="button" id="resetNewPlanFormBtn" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Reset</button>
|
||||||
|
<button id="submitNewPlanBtn" type="submit" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Save Plan</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
<%- include("./partials/scriptLoader") %>
|
<%- include("./partials/scriptLoader") %>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const openModalBtn = document.getElementById('openNewPlanModalBtn');
|
||||||
|
const newPlanDialog = document.getElementById('newPlanDialog');
|
||||||
|
const newPlanForm = document.getElementById('newPlanForm');
|
||||||
|
const submitNewPlanBtn = document.getElementById('submitNewPlanBtn');
|
||||||
|
const newPlanError = document.getElementById('newPlanError');
|
||||||
|
const resetNewPlanFormBtn = document.getElementById('resetNewPlanFormBtn');
|
||||||
|
|
||||||
|
openModalBtn.addEventListener('click', () => {
|
||||||
|
newPlanDialog.showModal();
|
||||||
|
// Error reset is now handled by 'close' event, but good to clear on open too
|
||||||
|
newPlanError.style.display = 'none';
|
||||||
|
newPlanError.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handles form reset and error clearing when dialog is closed by any means (Esc, Cancel button, backdrop click, successful submit)
|
||||||
|
newPlanDialog.addEventListener('close', () => {
|
||||||
|
newPlanForm.reset();
|
||||||
|
newPlanError.style.display = 'none';
|
||||||
|
newPlanError.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dialog if user clicks on the backdrop
|
||||||
|
newPlanDialog.addEventListener('click', (event) => {
|
||||||
|
if (event.target === newPlanDialog) {
|
||||||
|
newPlanDialog.close(); // This will trigger the 'close' event listener above
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetNewPlanFormBtn.addEventListener('click', () => {
|
||||||
|
newPlanForm.reset();
|
||||||
|
newPlanError.style.display = 'none';
|
||||||
|
newPlanError.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
newPlanForm.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
submitNewPlanBtn.disabled = true;
|
||||||
|
submitNewPlanBtn.textContent = 'Saving...';
|
||||||
|
newPlanError.style.display = 'none';
|
||||||
|
|
||||||
|
const formData = new FormData(newPlanForm);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/newPlan', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
newPlanDialog.close(); // This will trigger the 'close' event listener
|
||||||
|
window.location.reload(); // Reload to see the new plan
|
||||||
|
} else {
|
||||||
|
newPlanError.textContent = result.message || 'Failed to create plan. Please try again.';
|
||||||
|
newPlanError.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
newPlanError.textContent = errorData.message || `Error: ${response.status} - ${response.statusText}`;
|
||||||
|
newPlanError.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting new plan:', error);
|
||||||
|
newPlanError.textContent = 'An unexpected error occurred. Please try again.';
|
||||||
|
newPlanError.style.display = 'block';
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
submitNewPlanBtn.disabled = false;
|
||||||
|
submitNewPlanBtn.textContent = 'Save Plan';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
+98
-57
@@ -1,26 +1,26 @@
|
|||||||
<%- 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">
|
||||||
<!-- Modal content -->
|
<!-- Modal content -->
|
||||||
<div class="relative p-4 text-center bg-white rounded-lg shadow dark:bg-gray-800 sm:p-5">
|
<div class="relative p-4 text-center rounded-lg shadow bg-gray-800 sm:p-5">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button type="submit" class="text-gray-400 absolute top-2.5 right-2.5 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
<button type="submit" class="text-gray-400 absolute top-2.5 right-2.5 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:bg-gray-600 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>
|
<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 modal</span>
|
<span class="sr-only">Close modal</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<svg class="text-gray-400 dark:text-gray-500 w-11 h-11 mb-3.5 mx-auto" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
<svg class="text-gray-500 w-11 h-11 mb-3.5 mx-auto" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
||||||
<p class="mb-4 text-gray-500 dark:text-gray-300">Are you sure you want to delete you account? This process cannot be undone.</p>
|
<p class="mb-4 text-gray-300">Are you sure you want to delete you account? This process cannot be undone.</p>
|
||||||
<div class="flex justify-center items-center space-x-4">
|
<div class="flex justify-center items-center space-x-4">
|
||||||
<div style="width: 100%;">
|
<div style="width: 100%;">
|
||||||
<form method="dialog" style="width: 100%;">
|
<form method="dialog" style="width: 100%;">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="py-2 px-3 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
class="py-2 px-3 text-sm font-medium rounded-lg border focus:ring-4 focus:outline-none focus:z-10 bg-gray-700 text-gray-300 border-gray-500 hover:text-white hover:bg-gray-600 focus:ring-gray-600"
|
||||||
>
|
>
|
||||||
No, cancel
|
No, cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<input id="id" name="id" type="text" hidden value="<%= user._id %>">
|
<input id="id" name="id" type="text" hidden value="<%= user._id %>">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="py-2 px-3 text-sm font-medium text-center text-white bg-red-600 rounded-lg hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-900"
|
class="py-2 px-3 text-sm font-medium text-center rounded-lg focus:ring-4 focus:outline-none focus:ring-red-300 bg-red-500 hover:bg-red-600 focus:ring-red-900"
|
||||||
>
|
>
|
||||||
Yes, I'm sure
|
Yes, I'm sure
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<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">
|
||||||
|
|
||||||
<label for="password" class="block text-sm font-medium text-gray-700">New password</label>
|
<label for="password" class="block text-sm font-medium text-gray-700">New password</label>
|
||||||
<input id="password" disabled type="password" name="password" 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="password" disabled type="password" name="password" minlength="8" 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="repassword" class="block text-sm font-medium text-gray-700">Confirm new password</label>
|
<label for="repassword" class="block text-sm font-medium text-gray-700">Confirm new password</label>
|
||||||
<input id="repassword" disabled type="password" name="repassword" 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="repassword" disabled type="password" name="repassword" 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">
|
||||||
@@ -73,6 +73,96 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Questionnare details -->
|
||||||
|
<div class="mb-10 max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<h3 class="pt-2 pr-2 pb-2">Personal information</h3>
|
||||||
|
<% if (user.financialData) { %>
|
||||||
|
<button id="edit-personal" onclick="unlockPersonal()" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Edit</button>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (user.financialData) { %>
|
||||||
|
<form action="/updatePersonal" method="post" class="space-y-4" id="personal-form">
|
||||||
|
<div>
|
||||||
|
<label for="dob" class="block text-sm font-medium text-gray-700">Date of Birth</label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
id="dob"
|
||||||
|
type="date"
|
||||||
|
name="dob"
|
||||||
|
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"
|
||||||
|
<% let year = typeof(user.dob) == "object" ? user.dob.getFullYear() : user.dob.split('-')[0]; %>
|
||||||
|
<% let month = typeof(user.dob) == "object" ? user.dob.getMonth() : user.dob.split('-')[1] - 1; %>
|
||||||
|
<% let day = typeof(user.dob) == "object" ? user.dob.getDate() + 1 : user.dob.split('-')[2].split('T')[0];%>
|
||||||
|
<% let d = new Date(parseInt(year), parseInt(month), parseInt(day)); %>
|
||||||
|
value="<%= d.getFullYear() + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" + ("0" + d.getDate()).slice(-2); %>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="education" class="block text-sm font-medium text-gray-700">Education</label>
|
||||||
|
<select disabled id="education" name="education" id="education" class="mt-1 block w-full px-3 py-2 border border-gray-300 bg-white 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">
|
||||||
|
<option <%= user.education == "primary" ? 'selected' : '' %> value="primary">Primary (Elementary)</option>
|
||||||
|
<option <%= user.education == "secondary" ? 'selected' : '' %> value="secondary">Secondary (High School)</option>
|
||||||
|
<option <%= user.education == "tertiary" ? 'selected' : '' %> value="tertiary">Tertiary (College/University)</option>
|
||||||
|
<option <%= user.education == "postgraduate" ? 'selected' : '' %> value="postgraduate">Postgraduate (Master's/PhD)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Marital Status</label>
|
||||||
|
<div class="mt-1 space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input disabled <%= user.maritalStatus == "single" ? 'checked' : '' %> id="ms-single" type="radio" name="maritalStatus" value="single" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Single</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input disabled <%= user.maritalStatus == "married" ? 'checked' : '' %> id="ms-married" type="radio" name="maritalStatus" value="married" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Married</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input disabled <%= user.maritalStatus == "divorced" ? 'checked' : '' %> id="ms-divorced" type="radio" name="maritalStatus" value="divorced" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Divorced</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input disabled <%= user.maritalStatus == "widowed" ? 'checked' : '' %> id="ms-widowed" type="radio" name="maritalStatus" value="widowed" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Widowed</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="income" class="block text-sm font-medium text-gray-700">Annual Gross Income</label>
|
||||||
|
<input disabled value="<%= user.income %>" id="income" type="number" name="income" min="0" placeholder="e.g., 50000" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="expenses" class="block text-sm font-medium text-gray-700">Monthly Expenses</label>
|
||||||
|
<input disabled value="<%= user.expenses %>" id="expenses" type="number" name="expenses" min="0" placeholder="e.g., 2000" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="assets" class="block text-sm font-medium text-gray-700">Net Worth</label>
|
||||||
|
<input disabled value="<%= user.assets %>" id="assets" type="number" name="assets" min="0" placeholder="Estimated Net Worth" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="liabilities" class="block text-sm font-medium text-gray-700">Liabilities</label>
|
||||||
|
<input disabled value="<%= user.liabilities %>" id="liabilities" type="number" name="liabilities" min="0" placeholder="Estimated Liabilities" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="save-personal" disabled type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-not-allowed">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<div>
|
||||||
|
<a href="/questionnaire?profile" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer">Answer Questionnare</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete account -->
|
||||||
<div class="flex flex-row justify-center mx-auto p8">
|
<div class="flex flex-row justify-center mx-auto p8">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -83,55 +173,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="mb-10 max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<h3 class="pt-2 pr-2 pb-2">Personal information</h3>
|
|
||||||
<button onclick="unlockPersonal()" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Edit</button>
|
|
||||||
</div>
|
|
||||||
<form action="/updatePersonal" method="post" class="space-y-4" id="personal-form">
|
|
||||||
<div>
|
|
||||||
<label for="dob" class="block text-sm font-medium text-gray-700">Date of Birth</label>
|
|
||||||
<input id="dob" disabled type="date" name="dob" 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">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="education" class="block text-sm font-medium text-gray-700">Education</label>
|
|
||||||
<select disabled name="education" id="education" class="mt-1 block w-full px-3 py-2 border border-gray-300 bg-white 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">
|
|
||||||
<option value="primary">Primary (Elementary)</option>
|
|
||||||
<option value="secondary">Secondary (High School)</option>
|
|
||||||
<option value="tertiary">Tertiary (College/University)</option>
|
|
||||||
<option value="postgraduate">Postgraduate (Master's/PhD)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Marital Status</label>
|
|
||||||
<div class="mt-1 space-x-4">
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input disabled id="ms-single" type="radio" name="maritalStatus" value="single" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Single</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input disabled id="ms-married" type="radio" name="maritalStatus" value="married" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Married</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input disabled id="ms-divorced" type="radio" name="maritalStatus" value="divorced" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Divorced</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input disabled id="ms-widowed" type="radio" name="maritalStatus" value="widowed" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
|
|
||||||
<span class="ml-2 text-sm text-gray-700">Widowed</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button id="save-personal" disabled type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-not-allowed">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="mt-24"></div>
|
<div class="mt-24"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
<input type="hidden" name="token" value="<%=token %>">
|
<input type="hidden" name="token" value="<%=token %>">
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<label for="password" class="block text-base mb-2">New Password:</label>
|
<label for="password" class="block text-base mb-2">New Password:</label>
|
||||||
<input type="text" id="password" name="password"
|
<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 "
|
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="mt-3">
|
<div class="mt-3">
|
||||||
<label for="password" class="block text-base mb-2">Confirm Password:</label>
|
<label for="password" class="block text-base mb-2">Confirm Password:</label>
|
||||||
<input type="text" id="Repassword" name="confirmPassword"
|
<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 "
|
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" />
|
placeholder="ReType Password" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+28
-24
@@ -1,46 +1,50 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
<%- include("./partials/fileHeader") %>
|
||||||
<%- include("./partials/headerStart") %>
|
<%- include("./partials/headerStart") %>
|
||||||
|
|
||||||
<main>
|
<main class="pt-12">
|
||||||
<div class="flex justify-center items-center h-screen">
|
<div class="flex justify-center items-center py-10">
|
||||||
<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-8 bg-white rounded-lg shadow-xl border border-slate-200">
|
||||||
<h1 class="text-3xl block text-center font-semibold">Signup</h1>
|
<h1 class="text-3xl block text-center font-semibold text-slate-700">Signup</h1>
|
||||||
<hr class="mt-3">
|
<hr class="mt-4 mb-6 border-slate-300">
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="name" class="block text-base mb-2">Name</label>
|
<label for="name" class="block text-base mb-2 text-slate-600">Name</label>
|
||||||
<input type="text" id="name" name="name"
|
<input type="text" id="name" name="name"
|
||||||
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Name" />
|
placeholder="Enter Name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="email" class="block text-base mb-2">Email</label>
|
<label for="email" class="block text-base mb-2 text-slate-600">Email</label>
|
||||||
<input type="email" 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 border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Email" />
|
placeholder="Enter Email" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-4">
|
||||||
<label for="password" class="block text-base mb-2">Password</label>
|
<label for="password" class="block text-base mb-2 text-slate-600">Password</label>
|
||||||
<input type="password" id="password" name="password"
|
<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 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Password" />
|
placeholder="Enter Password" />
|
||||||
<label for="password" class="block text-base mb-2 mt-3">Re-type Password</label>
|
<label for="repassword" class="block text-base mb-2 mt-3 text-slate-600">Re-type Password</label>
|
||||||
<input type="password" id="repassword" name="repassword"
|
<input type="password" id="repassword" name="repassword"
|
||||||
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
|
class="border border-slate-300 focus:border-indigo-500 w-full rounded-md text-base px-3 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors duration-200"
|
||||||
placeholder="Enter Password" />
|
placeholder="Enter Password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-red-800 font-bold " id="forgor">
|
<% if (errMessage && errMessage.length > 0) { %>
|
||||||
<%= errMessage %>
|
<div class="mt-3 text-red-600 font-semibold text-sm" id="forgor"> <%= errMessage %></div>
|
||||||
</div>
|
<% } else { %>
|
||||||
<div class="mt-5">
|
<div class="mt-3 h-5" id="forgor"></div> <% /* Placeholder to maintain layout when no error */ %>
|
||||||
|
<% } %>
|
||||||
|
<div class="mt-6">
|
||||||
<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-indigo-600 bg-indigo-600 text-white py-2 w-full rounded-md hover:bg-indigo-700 hover:border-indigo-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Signup</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 block justify-between items-center">
|
<div class="mt-6 text-center">
|
||||||
<label>Already have an account?</label>
|
<span class="text-sm text-slate-600">Already have an account?</span>
|
||||||
<a href="/login" class="text-indigo-600 font-semibold">Login</a>
|
<a href="/login" class="text-sm text-indigo-600 hover:text-indigo-500 font-semibold ml-1 transition-colors duration-200">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
Reference in new issue
Block a user