Compare commits

...
Author SHA1 Message Date
SowinskiBraeden 6910de3789 refactor/move asset creation to partial 2025-05-07 15:12:50 -07:00
SowinskiBraeden 04a7fa428c Merge pull request #8 from JoaquinPar/feature/404
fix/middleware preventing 404 redirect
2025-05-07 13:04:54 -07:00
SowinskiBraeden cadbc2187d fix/middleware preventing 404 redirect 2025-05-07 13:01:24 -07:00
nicoagostini f417ac00fb fix/ Plans creation/display bug fixed 2025-05-07 11:37:33 -07:00
SowinskiBraeden b2e795427d fix/remove duplicated code 2025-05-07 10:53:28 -07:00
SowinskiBraeden 7ff74b8bbd fix/remove deprecated file 2025-05-07 10:51:54 -07:00
SowinskiBraeden 700ded35e4 Merge dev@latest into dev 2025-05-07 10:49:37 -07:00
SowinskiBraeden dbffe122a2 fix/reset forms on modal close 2025-05-07 10:43:05 -07:00
SowinskiBraeden d85a4120e2 fix/no assets message 2025-05-07 10:36:46 -07:00
SowinskiBraeden 3ad4e20588 update/render error messages 2025-05-07 10:35:09 -07:00
SowinskiBraeden aedfc17c36 update/render error messages 2025-05-07 10:34:06 -07:00
SowinskiBraeden 4c6945ecbc update/modify + delete assets 2025-05-07 10:33:02 -07:00
Nícolas Agostini 0658a8a85c Merge pull request #6
Plan detail page finished with mock data
2025-05-06 18:42:34 -07:00
nicoagostini 173c0b63c4 Plan detail page finished with mock data 2025-05-06 18:36:25 -07:00
SowinskiBraeden 78946bb6ca update/save asset to database 2025-05-06 16:59:49 -07:00
SowinskiBraeden 1f8579a052 update/save user from middleware to session 2025-05-06 16:59:33 -07:00
SowinskiBraeden 47fc1983f8 update/doc comments + create assets collection 2025-05-06 15:52:53 -07:00
nicoagostini d67718733d fix/ Route fixed for index/dashboard 2025-05-06 15:46:57 -07:00
SowinskiBraeden 803330b174 update/assets page and creation forms 2025-05-06 15:46:09 -07:00
knighthawk4227 e65af5336f Merge branch 'feature/dashboard' into dev 2025-05-06 15:09:16 -07:00
knighthawk4227 7b9b18f030 dashboard page with cards to fill in and add asset and new plan button 2025-05-06 15:08:04 -07:00
Joaquin f3a1a2191a merge feature/geolocation into dev 2025-05-06 14:48:52 -07:00
Joaquin e1626f6ae9 feature/completed getting geolocation data 2025-05-06 14:46:01 -07:00
Nícolas Agostini 05402432a6 Merge pull request #4 from JoaquinPar/feature/calculations
Feature/calculations
2025-05-06 14:32:54 -07:00
Nícolas Agostini 03c3f9a610 Merge branch 'dev' into feature/calculations 2025-05-06 14:32:43 -07:00
nicoagostini 9b998078b7 Questionnaire page cleaned up and updated 2025-05-06 14:30:43 -07:00
nicoagostini b944c37c02 Plans page created, listing the plans from the database, new plan page created and saving to database 2025-05-06 14:27:27 -07:00
Joaquin 83be006a83 feature/geolocation implemented 2025-05-06 13:38:00 -07:00
SowinskiBraeden 6d91934229 fix/ignore login err message on signup 2025-05-06 12:49:20 -07:00
SowinskiBraeden 97a1473e21 fix/require name on register 2025-05-06 12:46:21 -07:00
SowinskiBraeden 3bb9ede515 update/cancel changes 2025-05-06 11:52:53 -07:00
SowinskiBraeden b350cdad9f update/keep email input disabled 2025-05-06 11:46:14 -07:00
SowinskiBraeden f0d324151d update/error message style 2025-05-06 11:44:26 -07:00
SowinskiBraeden b88333e3b4 refactor/temp remove personal section 2025-05-06 11:41:14 -07:00
SowinskiBraeden 75bc3bee8f update/save modified account settings 2025-05-06 11:39:49 -07:00
SowinskiBraeden a2b4e8d36d refactor/remove debug statements 2025-05-06 11:19:10 -07:00
SowinskiBraeden 033401dbf7 update/profile page with user data 2025-05-06 11:15:18 -07:00
knighthawk4227 4bb9a01524 deleting projects table 2025-05-06 11:05:37 -07:00
knighthawk4227 47ab6c9eed merging dev with feature dashboard 2025-05-06 10:56:19 -07:00
knighthawk4227 b558a75f20 uncomment partials 2025-05-06 09:46:14 -07:00
SowinskiBraeden 1d8ec431b2 fix/tidy auth+user routers 2025-05-06 09:32:39 -07:00
Joaquin 4050e95cc3 fixed conflict 2025-05-06 09:30:30 -07:00
Joaquin ae3ddbeaeb Merge branch 'dev' of https://github.com/JoaquinPar/2800-202510-BBY14 into dev 2025-05-06 09:30:22 -07:00
Nícolas Agostini 84cd8b0bae Merge pull request #3 from JoaquinPar/feature/questions
feature/questionnaire page create + saving to MongoDb
2025-05-06 02:10:14 -07:00
Nícolas Agostini 5684e96a79 Merge branch 'dev' into feature/questions 2025-05-06 02:10:05 -07:00
nicoagostini 1e32ccfa17 feature/questionnaire page create + saving to MongoDb 2025-05-06 02:07:58 -07:00
knighthawk4227 5abec70c61 login and signup touch ups index page template added 2025-05-06 00:33:36 -07:00
Joaquin 54566f05b7 feature/fully implemented ejs inclusion to main files 2025-05-05 21:56:47 -07:00
Joaquin decdb299b7 Merge branch 'dev' of https://github.com/JoaquinPar/2800-202510-BBY14 into feature/layoutPage 2025-05-05 21:17:50 -07:00
Joaquin c8bf2e49ab feature/completed most of ejs implementation into other pages 2025-05-05 21:12:52 -07:00
nicoagostini f39b8555fa fix/session check fixed 2025-05-05 18:59:59 -07:00
nicoagostini fef7184404 fix/home.ejs include fix 2025-05-05 18:45:59 -07:00
SowinskiBraeden 6a8595d24b fix/impliment middleware 2025-05-05 18:39:43 -07:00
SowinskiBraeden 7c7098636a fix/middleware authentication + bad merge 2025-05-05 18:34:19 -07:00
Nícolas Agostini a446943d4a Merge pull request #2 from JoaquinPar/feature/questionnaire
Just a bug fix
2025-05-05 18:18:55 -07:00
Nícolas Agostini 612bb262d1 Merge branch 'dev' into feature/questionnaire 2025-05-05 18:18:45 -07:00
nicoagostini 6819b82723 Just a bug fix 2025-05-05 18:17:35 -07:00
SowinskiBraeden 5b1b36a553 fix/compare password on sign in 2025-05-05 18:12:19 -07:00
SowinskiBraeden 1114789e57 Merge latest dev into dev 2025-05-05 17:34:24 -07:00
SowinskiBraeden bb6e2eba2c fix/user routing 2025-05-05 17:33:27 -07:00
Joaquin f6640b23cf feature/some progress made on layouts 2025-05-05 17:07:51 -07:00
nicoagostini d0aa7a0fbd Landing moved to be the index page, routes added and landing page image route fixed 2025-05-05 16:46:16 -07:00
Joaquin 4be7ca3a8e Merge branch 'dev' into feature/layoutPage 2025-05-05 16:30:51 -07:00
Joaquin 10541d2428 merge feature/navBar with dev 2025-05-05 16:26:01 -07:00
Joaquin 7fa75457c9 feature/completed navbar/header functionality 2025-05-05 16:24:25 -07:00
Joaquin 8b6ff1354b resolved merge conflict 2025-05-05 14:27:24 -07:00
Joaquin 4bcf8dd812 feature/started work on layout.ejs 2025-05-05 14:21:30 -07:00
SowinskiBraeden fe7152b03b update/create middleware 2025-05-05 14:16:50 -07:00
SowinskiBraeden 87cad63425 update/return status codes 2025-05-05 13:17:30 -07:00
SowinskiBraeden d08fd30888 login/signup function with no sessions 2025-05-02 13:19:14 -07:00
SowinskiBraeden 7ef6282ac0 update login/signup.ejs pages 2025-05-02 13:18:51 -07:00
Joaquin Paredes 02a8374a58 feature/most nav bar functionality done 2025-05-02 13:15:38 -07:00
SowinskiBraeden 0e4d4d7daf fix project structure & static file 2025-05-02 11:41:13 -07:00
knighthawk4227 50dab82945 making login ejs and setting index blank for now 2025-05-02 02:07:55 -07:00
nicoagostini 8832768838 Merge branch 'feature/landingPage' into dev 2025-05-02 02:01:06 -07:00
knighthawk4227 44cee349e9 ejs login and signup with tailwind 2025-05-02 01:53:47 -07:00
35 changed files with 2831 additions and 262 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
mongoURI='mongodb://localhost:27017/' mongoURI='mongodb://localhost:27017/'
database='nameOfDatabase' database='nameOfDatabase'
PORT=8000 PORT=8000
secret='123456789'
+6 -3
View File
@@ -54,9 +54,12 @@ Example:
retirementCalculator/ retirementCalculator/
├── src/ ├── src/
│ ├── views/ │ ├── views/
| ── css/ │ │ ── partials/
| ├── images/ ├── css/
| ── scripts/ ── images/
│ ├── scripts/
│ └── utils/
├── app.js ├── app.js
├── .env.example ├── .env.example
├── .gitignore ├── .gitignore
+79 -59
View File
@@ -1,94 +1,114 @@
const status = require("./src/util/statuses");
const MongoStore = require("connect-mongo");
const session = require("express-session");
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const dotenv = require('dotenv'); const joi = require('joi');
require('dotenv').config();
dotenv.config();
const app = express(); const app = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const mongoURI = process.env.mongoURI || "mongodb://localhost:27017/";
const database = process.env.database || "knoldus"; // Database name
const secret = process.env.secret || "123-secret-xyz";
/*** Sessions ***/
app.use(session({
secret: secret,
store: MongoStore.create({ mongoUrl: `${mongoURI}${database}`, crypto: { secret: secret } }),
resave: true,
saveUninitialized: false,
cookie: { maxAge: 3600000 },
}));
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('views',path.join(__dirname, 'src/views')); app.set('views', path.join(__dirname, 'src/views'));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use("/static", express.static("./src/public"));
app.use("/images", express.static("./src/public/images"));
app.use(express.json());
/*** Database ***/
const { connectMongo, getCollection } = require("./src/database/connection");
/* let users;
STATIC ROUTINGS let assets;
*/ let plans;
async function initDatabase() {
const db = await connectMongo(mongoURI, database);
app.use("/scripts", express.static("./src/scripts")); // For any collection, init here
app.use("/css", express.static("./src/css")); users = await getCollection(db, "users");
app.use("/images", express.static("./src/images")); assets = await getCollection(db, "assets");
app.use("/views", express.static("./src/views")); plans = await getCollection(db, "plans");
}
/*** ROUTINGS ***/
/*
ROUTINGS
*/
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.render('index'); if (!req.session.errMessage) req.session.errMessage = "";
});
app.get('/landing', (req, res) => {
res.render('landing'); res.render('landing');
return res.status(status.Ok);
}); });
app.get('/signup', (req, res) => { app.get('/signup', (req, res) => {
res.render('signup'); const ignore = ["User not found", "Incorrect password"];
if (ignore.includes(req.session.errMessage)) req.session.errMessage = "";
res.render('signup', { errMessage: req.session.errMessage });
return res.status(status.Ok);
}); });
app.get('/login', (req, res) => { app.get('/login', (req, res) => {
res.render('login'); if (req.session.authenticated) {
}); res.redirect("/home");
return res.status(status.Ok);
app.get('/home', (req, res) => { }
res.render('home'); res.render('login', { errMessage: req.session.errMessage });
}); return res.status(status.Ok);
app.get('/assets', (req, res) => {
res.render('assets');
});
app.get('/plans', (req, res) => {
res.render('plans');
});
app.get('/more', (req, res) => {
res.render('more');
});
app.get('/profile', (req, res) => {
res.render('profiles');
});
app.get('/settings', (req, res) => {
res.render('settings');
}); });
app.get('/aboutUs', (req, res) => { app.get('/aboutUs', (req, res) => {
res.render('aboutUs'); res.render('aboutUs');
return res.status(status.Ok);
}); });
app.post('/api/location', async (req,res) => {
const { latitude, longitude } = req.body;
app.post('/signup', (req, res) => { const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&result_type=country&key=${process.env.geolocation_api}`);
res.status(200);
res.send('200 status code'); const data = await response.json();
res.json(data);
}); });
app.post('/login', (req, res) => { // 404 handler - keep the actual notFound route please
res.status(200); app.get('/notFound', (req, res) => {
res.send('200 status code'); res.render('notFound');
return res.status(status.NotFound);
}); });
app.get('/*splat', (req, res) => { // Initialize database and start app
res.status(404); initDatabase().then(() => {
res.send('404 Not Found'); console.log("Successfully connected to MongoDB");
});
app.listen(port, () => { // Import authentication handler
console.log(`Server listening on port ${port}`); app.use(require("./src/auth/authentication")(users));
// Import middleware & apply to user routes
const middleware = require("./src/auth/middleware")(users);
app.use(require('./src/router/user')(middleware, users, plans, assets));
// 404 handler
app.get('/*splat', (req, res) => {
res.render('notFound');
return res.status(status.NotFound);
});
// Start app
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
}); });
Generated Executable → Regular
+876 -35
View File
File diff suppressed because it is too large. Load diff
+9 -4
View File
@@ -2,10 +2,10 @@
"name": "retirementcalculator", "name": "retirementcalculator",
"version": "0.1.0", "version": "0.1.0",
"description": "Retirement Calculator", "description": "Retirement Calculator",
"main": "./src/app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "node ./src/app.js", "start": "node app.js",
"dev": "nodemon ./src/app.js" "dev": "nodemon app.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,9 +18,14 @@
}, },
"homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0" "express": "^5.1.0",
"express-session": "^1.18.1",
"joi": "^17.13.3",
"mongodb": "^6.16.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
+101
View File
@@ -0,0 +1,101 @@
const status = require("../util/statuses");
const bcrypt = require('bcrypt');
const joi = require("joi");
const salt = 12;
/**
* @param {MongoClient.collection} users db collection
* @returns {express.Router} authentication router
*/
module.exports = (users) => {
const router = require("express").Router();
router.get("/logout", (req, res) => {
req.session.destroy();
return res.redirect('/login');
});
router.post("/login", async (req, res) => {
const credentialSchema = joi.object({
email: joi.string().email().required(),
password: joi.string().max(20).required(),
});
const valid = credentialSchema.validate(req.body);
if (valid.err) {
req.session.errMessage = "Invalid input";
res.status(status.BadRequest);
return res.redirect("/login");
}
users.findOne({ "email": req.body.email }).then((user) => {
if (!user) {
req.session.errMessage = "User not found";
res.status(status.NotFound);
return res.redirect("/login");
}
if (!bcrypt.compareSync(req.body.password, user.password)) {
req.session.errMessage = "Incorrect password";
res.status(status.Unauthorized);
return res.redirect("/login");
}
req.session.authenticated = true;
req.session.userId = user._id;
req.session.email = req.body.email;
req.session.errMessage = "";
res.redirect("/home");
return res.status(status.Ok);
});
});
router.post("/signup", async (req, res) => {
const userSchema = joi.object({
email: joi.string().email().required(),
name: joi.string().alphanum().max(20).required(),
password: joi.string().max(20).min(8).required(),
repassword: joi.string().max(20).min(8).required(),
});
const valid = userSchema.validate(req.body);
if (valid.err) {
req.session.errMessage = "Invalid input",
res.status(status.BadRequest);
return res.redirect("/signup");
}
if (req.body.password != req.body.repassword) {
req.session.errMessage = "Passwords must match";
res.status(status.BadRequest);
return res.redirect("/signup");
}
let hashedPassword = await bcrypt.hashSync(req.body.password, salt);
users.insertOne({
email: req.body.email,
name: req.body.name,
password: hashedPassword,
financialData: false,
}).then((results, err) => {
if (err) {
console.error(err);
res.session.errMessage = "Internal server error";
return res.status(status.InternalServerError).redirect("/signup");
}
req.session.authenticated = true;
req.session.email = req.body.email;
req.session.userId = results.insertedId;
req.session.errMessage = "";
return res.status(status.Ok).redirect("/home");
});
});
return router;
}
+42
View File
@@ -0,0 +1,42 @@
const status = require("../util/statuses");
const session = require("express-session");
// Get all user routes names
let userRouter = require("../router/user")((req, res, next) => next(), null, null, null);
userRouter.stack.shift();
const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1]);
/**
* @param {MongoClient.collection} users db collection
* @returns {async function} middleware handler function
*/
const createMiddleware = (users) => {
return async (req, res, next) => {
// Redirect not found pages to 404 page
if (!userRoutes.includes(req.url.substring(1))) {
return res.status(status.NotFound).redirect("/notFound");
}
if (!req.session.authenticated || !req.session.email) {
req.session.errMessage = "Please login to view that resource";
res.redirect("/login");
return res.status(status.Unauthorized);
}
if (!req.session.user) {
let user = await users.findOne({ "email": req.session.email }).then((user) => user);
if (!user) {
req.session.errMessage = "User not found";
res.redirect("/login");
return res.status(status.Unauthorized);
}
req.session.user = user;
}
next();
};
}
module.exports = createMiddleware;
+28
View File
@@ -0,0 +1,28 @@
const MongoClient = require("mongodb").MongoClient;
/**
* connectMongo returns a database connection to MongoDB
* @param {string} mongoURI
* @param {string} databaseName
* @return {MongoClient}
*/
const connectMongo = async (mongoURI, databaseName) => {
const database = await MongoClient.connect(mongoURI, { connectTimeoutMS: 1000 });
const dbo = database.db(databaseName);
return dbo;
}
/**
* getCollection object to interact with MongoDB
* @param {MongoClient} dbo
* @param {string} collection
* @return {MongoClient.collection}
*/
const getCollection = async (dbo, collection) => {
return await dbo.collection(collection);
}
module.exports = {
connectMongo: connectMongo,
getCollection: getCollection,
}
File renamed without changes.
+99
View File
@@ -0,0 +1,99 @@
const assetForms = [
"create-other-asset-form",
"create-saving-asset-form",
"create-stock-asset-form"
];
/**
* resetAll asset creation forms
*/
function resetAll() {
document.getElementById("create-other-asset-form").reset();
document.getElementById("create-saving-asset-form").reset();
document.getElementById("create-stock-asset-form").reset();
}
/**
* selectAssetForm changes which form is displayed
* to create specified assets.
* @param {string} assetFormId
*/
function selectAssetForm(assetFormId) {
for (let i = 0; i < assetForms.length; i++) {
if (assetForms[i] == assetFormId) {
document.getElementById(assetFormId).style.display = "block";
} else {
document.getElementById(assetForms[i]).style.display = "none";
}
}
}
/**
* resetRadio to default to other asset type.
*/
function resetRadio() {
document.getElementById("asset-other").checked = true;
document.getElementById("asset-saving").checked = false;
document.getElementById("asset-stock").checked = false;
}
const assetKeys = {
other: [
"name",
"value",
"description",
"purchaseDate",
],
saving: [
"name",
"value",
],
stock: [
"ticker",
"price",
"quantity",
"purchaseDate",
],
}
/**
* lockAsset prevents edits to asset view modal
* @param {string} assetId
*/
function lockAsset(assetId) {
document.getElementById(`${assetId}-form`).reset();
const type = document.getElementById(`type-${assetId}`).value;
assetKeys[type].forEach((key) => {
document.getElementById(`${key}-${assetId}`).disabled = true;
});
document.getElementById(`save-${assetId}`).disabled = true;
document.getElementById(`save-${assetId}`).classList.remove("cursor-pointer");
document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed");
document.getElementById(`edit-${assetId}`).innerHTML = "Edit";
document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId) };
}
/**
* unlockAsset allows edits to asset view modal
* @param {string} assetId
*/
function unlockAsset(assetId) {
const type = document.getElementById(`type-${assetId}`).value;
assetKeys[type].forEach((key) => {
document.getElementById(`${key}-${assetId}`).disabled = false;
});
document.getElementById(`save-${assetId}`).disabled = false;
document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed");
document.getElementById(`save-${assetId}`).classList.add("cursor-pointer");
document.getElementById(`edit-${assetId}`).innerHTML = "Cancel changes";
document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId) };
}
resetRadio();
+24
View File
@@ -0,0 +1,24 @@
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(success, error);
}
}
function success(position) {
const { latitude, longitude } = position.coords;
fetch("api/location" , {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ latitude, longitude })
}).then(res => res.json())
.then(data => {
console.log(data.results[0].formatted_address);
})
}
function error(err) {
console.error("Geolocation error: ", err);
}
+75
View File
@@ -0,0 +1,75 @@
/**
* lockAccount resets inputs and disabled inputs
*/
function lockAccount() {
// Clear unsaved inputs on page load (refresh doesnt clear them)
document.getElementById("account-form").reset();
document.getElementById("save-account").disabled = true;
document.getElementById("email").disabled = true;
document.getElementById("name").disabled = true;
document.getElementById("password").disabled = true;
document.getElementById("repassword").disabled = true;
document.getElementById("save-account").classList.add("cursor-not-allowed");
document.getElementById("edit-account").innerHTML = "Edit";
document.getElementById("edit-account").onclick = unlockAccount;
}
/**
* lockPersonal resets inputs and disabled inputs
*/
function lockPersonal() {
// Clear unsaved inputs on page load (refresh doesnt clear them)
document.getElementById("personal-form").reset();
document.getElementById("save-personal").disabled = true;
document.getElementById("dob").disabled = true;
document.getElementById("education").disabled = true;
document.getElementById("ms-single").disabled = true;
document.getElementById("ms-married").disabled = true;
document.getElementById("ms-divorced").disabled = true;
document.getElementById("ms-widowed").disabled = true;
document.getElementById("save-personal").classList.add("cursor-not-allowed");
document.getElementById("edit-personal").innerHTML = "Edit";
document.getElementById("edit-personal").onclick = unlockPersonal;
}
/**
* unlockAccount removes disabled from inputs and
* allows users to edit their profile.
*/
function unlockAccount() {
document.getElementById("save-account").disabled = false;
// document.getElementById("email").disabled = false;
document.getElementById("name").disabled = false;
document.getElementById("password").disabled = false;
document.getElementById("repassword").disabled = false;
document.getElementById("save-account").classList.remove("cursor-not-allowed");
document.getElementById("edit-account").innerHTML = "Cancel changes";
document.getElementById("edit-account").onclick = lockAccount;
}
/**
* unlocPersonal removes disabled from inputs and
* allows users to edit their personal information.
*/
function unlockPersonal() {
document.getElementById("save-personal").disabled = false;
document.getElementById("dob").disabled = false;
document.getElementById("education").disabled = false;
document.getElementById("ms-single").disabled = false;
document.getElementById("ms-married").disabled = false;
document.getElementById("ms-divorced").disabled = false;
document.getElementById("ms-widowed").disabled = false;
document.getElementById("save-personal").classList.remove("cursor-not-allowed");
document.getElementById("edit-personal").innerHTML = "Cancel changes";
document.getElementById("edit-personal").onclick = lockPersonal;
}
// On page load, ensure forms are locked and reset
lockAccount();
// lockPersonal();
+436
View File
@@ -0,0 +1,436 @@
const status = require("../util/statuses");
const ObjectId = require('mongodb').ObjectId;
const bcrypt = require("bcrypt");
const joi = require("joi");
const salt = 12;
/**
* getAssetSchema returns correct joi object
* to validate req.body depending on asset type
* @param {string} type of asset
* @returns {joi.object} schema
*/
const getAssetSchema = (type) => {
let assetSchema;
switch (type) {
case "other":
assetSchema = joi.object({
ownerId: joi.string().alphanum().required(),
type: joi.string().valid("other", "stock", "saving").required(),
name: joi.string().alphanum().min(3).max(30).required(),
value: joi.number().min(0).required(),
purchaseDate: joi.date().required(),
description: joi.string().alphanum().max(240),
id: joi.string().alphanum(), // May be passed when updating existing asset
});
break;
case "saving":
assetSchema = joi.object({
ownerId: joi.string().alphanum().required(),
type: joi.string().valid("other", "stock", "saving").required(),
name: joi.string().alphanum().min(3).max(30).required(),
value: joi.number().min(0).required(),
id: joi.string().alphanum(), // May be passed when updating existing asset
});
break;
case "stock":
assetSchema = joi.object({
ownerId: joi.string().alphanum().required(),
type: joi.string().valid("other", "stock", "saving").required(),
ticker: joi.string().alphanum().min(3).max(5).required(),
price: joi.number().min(0).required(),
quantity: joi.number().min(1).required(),
purchaseDate: joi.date().required(),
id: joi.string().alphanum(), // May be passed when updating existing asset
});
break;
default:
return null;
};
return assetSchema;
}
/**
* @param {function} middleware handler
* @param {MongoClient.collection} users db collection
* @param {MongoClient.collection} assets db collection
* @returns {express.Router} user protected routes router
*/
module.exports = (middleware, users, plans, assets) => {
const router = require("express").Router();
router.use(middleware);
router.get('/home', async (req, res) => {
res.render('dashboard', { user: req.session.user });
return res.status(status.Ok);
});
router.get('/assets', async (req, res) => {
let userAssets = await assets.find({ "ownerId": req.session.user._id }).toArray();
res.render('assets', { user: req.session.user, errMessage: req.session.errMessage, assets: userAssets });
return res.status(status.Ok);
});
router.get('/plans', async (req, res) => {
try {
// console.log(new ObjectId(req.session.user._id));
const userPlans = await plans.find({userId: new ObjectId(req.session.user._id) }).toArray();
// console.log(userPlans);
res.render('plans', {
user: req.session.user,
plans: userPlans
});
} catch (err) {
console.error("Error fetching plans:", err);
req.session.errMessage = "Could not load your plans. Please try again.";
res.status(status.InternalServerError).redirect('/home');
}
});
router.get('/plans/:id', async (req, res) => {
try {
const planId = req.params.id;
if (!ObjectId.isValid(planId)) {
req.session.errMessage = "Invalid plan ID format.";
return res.status(status.BadRequest).redirect('/plans');
}
const plan = await plans.findOne({ userId: new ObjectId(req.session.user._id), _id: new ObjectId(planId) });
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.";
return res.status(status.NotFound).redirect('/plans');
}
// console.log("Found plan:", plan);
res.render('planDetail', {
user: req.session.user,
plan: plan
});
} catch (err) {
console.error("Error fetching plan:", err);
req.session.errMessage = "Could not load your plan. Please try again.";
res.status(status.InternalServerError).redirect('/home');
}
});
router.get('/newPlan', (req, res) => {
if(!req.session.user.financialData){
req.session.errMessage = "Please complete your financial data before creating a plan.";
return res.status(status.Unauthorized).redirect('/questionnaire');
}
const errMessage = req.session.errMessage;
req.session.errMessage = "";
res.render('newPlan', { user: req.session.user, errMessage: errMessage });
});
router.post('/newPlan', async (req, res) => {
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(),
});
const validationOptions = { convert: true, abortEarly: false };
const { error, value } = planSchema.validate(req.body, validationOptions);
if (error) {
console.error("Plan validation error:", error.details);
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
res.status(status.BadRequest).redirect("/newPlan");
return;
}
const newPlan = {
userId: new ObjectId(req.session.user._id),
name: value.name,
retirementAge: value.retirementAge,
retirementExpenses: value.retirementExpenses,
retirementAssets: value.retirementAssets,
retirementLiabilities: value.retirementLiabilities,
progress: "0%"
};
try{
await plans.insertOne({userId: new ObjectId(req.session.user._id), ...newPlan});
req.session.errMessage = "";
res.redirect('/plans');
}
catch(err){
console.error("Error saving plan:", err);
req.session.errMessage = "An error occurred while saving your plan. Please try again.";
res.status(status.InternalServerError).redirect("/newPlan");
}
});
router.get('/more', (req, res) => {
res.render('more', { user: req.session.user });
return res.status(status.Ok);
});
router.get('/profile', (req, res) => {
res.render('profile', { user: req.session.user, errMessage: req.session.errMessage });
return res.status(status.Ok);
});
router.get('/settings', (req, res) => {
res.render('settings', { user: req.session.user });
return res.status(status.Ok);
});
router.get('/questionnaire', (req, res) => {
const errMessage = req.session.errMessage;
req.session.errMessage = "";
res.render('questionnaire', { user: req.session.user, errMessage: errMessage });
});
router.post('/questionnaire', (req, res) => {
// console.log("Questionnaire POST body:", req.body);
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("Questionnaire validation error:", error.details);
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
res.status(status.BadRequest).redirect("/questionnaire");
return;
}
users.updateOne(
{ _id: new ObjectId(req.session.user._id) },
{
$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 questionnaire update: ${req.session.user.email}`);
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 questionnaire data unchanged (already up-to-date): ${req.session.user.email}`);
}
req.session.user.financialData = true;
req.session.errMessage = "";
res.status(status.Ok).redirect("/home");
}).catch(err => {
console.error("Error updating questionnaire in database:", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
res.status(status.InternalServerError).redirect("/questionnaire");
});
});
router.post("/updateAccount", async (req, res) => {
const accountSchema = joi.object({
email: joi.string().email(),
name: joi.string().alphanum().max(20),
password: joi.string().max(20).min(8),
repassword: joi.string().max(20).min(8),
});
const valid = accountSchema.validate(req.body);
if (valid.err) {
req.session.errMessage = "Invalid input",
res.status(status.BadRequest);
return res.redirect("/profile");
}
let update = {
name: req.body.name,
};
if ((req.body.password != "") && (req.body.password != req.body.repassword)) {
req.session.errMessage = "New passwords must match";
res.status(status.BadRequest);
return res.redirect("/profile");
} else if (req.body.password != "") {
let hashedPassword = await bcrypt.hashSync(req.body.password, salt);
update.password = hashedPassword;
}
users.updateOne(
{ email: req.session.email },
{ $set: update }
).then((result) => {
if (result.matchedCount === 0) {
console.log(`User not found during account update: ${req.session.email}`);
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 account data unchanged (already up-to-date): ${req.session.email}`);
}
req.session.errMessage = "";
return res.status(status.Ok).redirect("/profile");
}).catch(err => {
console.error("Error updating account in database:", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/profile");
});
});
router.post("/createAsset", async (req, res) => {
// Create asset, each asset has different data structure based on type
const type = req.body.type;
const assetSchema = getAssetSchema(type);
if (!assetSchema) {
console.error("Modified asset type, rejected");
req.session.errMessage = "Invalid input";
return res.status(status.BadRequest).redirect("/assets");
}
const valid = assetSchema.validate(req.body);
if (valid.err) {
req.session.errMessage = "Invalid input",
res.status(status.BadRequest);
return res.redirect("/assets");
}
let newAsset = {
...req.body,
updatedAt: new Date(),
};
if (type == "stock") {
newAsset.quantity = parseInt(newAsset.quantity);
newAsset.price = parseFloat(newAsset.price)
newAsset.value = newAsset.quantity * newAsset.price;
newAsset.name = `${newAsset.ticker} Stock`;
}
newAsset.value = parseFloat(newAsset.value);
assets.insertOne(newAsset, (err, _) => {
if (err) {
console.error("Error creating asset: ", err);
req.session.errMessage = "Internal server error";
return res.status(status.InternalServerError).redirect("/assets");
}
});
req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets");
});
router.post("/updateAsset", async (req, res) => {
const type = req.body.type;
const assetSchema = getAssetSchema(type);
if (!assetSchema) {
console.error("Modified asset type, rejected");
req.session.errMessage = "Invalid input";
return res.status(status.BadRequest).redirect("/assets");
}
const valid = assetSchema.validate(req.body);
if (valid.err) {
req.session.errMessage = "Invalid input",
res.status(status.BadRequest);
return res.redirect("/assets");
}
if (req.body.ownerId != req.session.user._id) {
req.session.errMessage = "Cannot change asset owner",
res.status(status.BadRequest);
return res.redirect("/assets");
}
let update = { ...req.body, updatedAt: new Date() };
if (type == "stock") {
update.quantity = parseInt(update.quantity);
update.price = parseFloat(update.price)
update.value = update.quantity * update.price;
update.name = `${update.ticker} Stock`;
}
update.value = parseFloat(update.value);
delete update.id
assets.updateOne(
{ "_id": new ObjectId(req.body.id) },
{ $set: update },
).then((result) => {
if (result.matchedCount === 0) {
console.log(`Asset not found: ${req.body.id}`);
req.session.errMessage = "Unable to update asset";
return res.status(status.NotFound).redirect("/assets");
}
if (result.modifiedCount === 0 && result.matchedCount === 1) {
console.log(`Asset data unchanged (already up-to-date): ${req.body.id}`);
}
req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets");
}).catch((err) => {
console.error("Error updating asset: ", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/assets");
});
});
router.post("/deleteAsset", (req, res) => {
const id = new ObjectId(req.body.id);
assets.deleteOne(
{ "_id": id }
).then((result) => {
if (result.deletedCount === 0) {
console.log(`Asset not found: ${req.body.id}`);
req.session.errMessage = "Unable to delete asset";
return res.status(status.NotFound).redirect("/assets");
}
if (!result.acknowledged) {
console.error("Error updating asset: ", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/assets");
}
req.session.errMessage = "";
return res.status(status.Ok).redirect("/assets");
}).catch((err) => {
console.error("Error updating asset: ", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
return res.status(status.InternalServerError).redirect("/assets");
});
});
return router;
};
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
Ok: 200,
BadRequest: 400,
Unauthorized: 401,
NotFound: 404,
InternalServerError: 500,
};
+36 -77
View File
@@ -1,80 +1,39 @@
<html> <%- include("./partials/fileHeader") %>
<head> <%- include("./partials/headerStart") %>
<title>RCalculator</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-white dark:bg-gray-900">
<header>
<nav class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<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" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span>
</a>
<button data-collapse-toggle="navbar-solid-bg" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md: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" aria-controls="navbar-solid-bg" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-solid-bg">
<ul class="flex flex-col font-medium mt-4 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-transparent dark:bg-gray-800 md:dark:bg-transparent dark:border-gray-700">
<li>
<a href="/landing" class="block py-2 px-3 md:p-0 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Home</a>
</li>
<li>
<a href="/aboutUs" class="block py-2 px-3 md:p-0 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent" aria-current="page">About Us</a>
</li>
<li>
<a href="mailto:rcalculator@gmail.com" class="block py-2 px-3 md:p-0 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Contact us</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main>
<section class="bg-white dark:bg-gray-900 py-12 md:py-20"> <main>
<div class="max-w-screen-lg mx-auto px-4 text-center"> <section class="bg-white dark:bg-gray-900 py-12 md:py-20">
<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> <div class="max-w-screen-lg mx-auto px-4 text-center">
<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> <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>
</div> <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>
</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-gray-900 dark: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-600 dark: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 dark:prose-invert mx-auto text-gray-600 dark: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>
<p class="mb-4"> <p class="mb-4">
At RCalculator, we help you trace a personalized plan that suits your unique aspirations. We provide clear, actionable advice and projections to guide you on how to get there, whether you're just starting your career, mid-way through, or nearing retirement. At RCalculator, we help you trace a personalized plan that suits your unique aspirations. We provide clear, actionable advice and projections to guide you on how to get there, whether you're just starting your career, mid-way through, or nearing retirement.
</p> </p>
<p> <p>
Let us help you navigate the path to financial independence and achieve the retirement lifestyle you envision. Let us help you navigate the path to financial independence and achieve the retirement lifestyle you envision.
</p> </p>
</div>
</div>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/login" 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">
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">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
</svg>
</a>
</div>
</section>
</main>
<footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 p-6 text-center text-gray-500 dark:text-gray-400 text-sm">
<div class="container mx-auto">
<p>&copy; 2025 RCalculator. All rights reserved.</p>
</div> </div>
</footer> </div>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script> <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">
</body> Get started
</html> <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"/>
</svg>
</a>
</div>
</section>
</main>
<%- include("./partials/footer") %>
+218
View File
@@ -0,0 +1,218 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main>
<%- include("./partials/createAsset") %>
<% 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>
<% } %>
<!-- Main display -->
<section>
<div class="mt-8 max-w-md mx-auto bg-white px-8 py-6 rounded-lg shadow-md flex flex-col justify-center">
<div class="flex flex-row justify-between text-right">
<h1 class="text-2xl font-bold tracking-tight leading-none">Assets:</h1>
<h1 class="text-xl font-medium tracking-tight leading-none"><%= assets.length %></h1>
</div>
<div class="flex flex-row justify-between text-right mt-6">
<h1 class="text-2xl font-bold tracking-tight leading-none">Total Value:</h1>
<h1 class="text-xl font-medium tracking-tight leading-none">
<!-- sum of assets array values and formated as proper dollar amount -->
$<%= assets.reduce((total, e) => total + e.value, 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
</h1>
</div>
</div>
<div class="w-md my-6 max-w-md mx-auto px-4 flex flex-row justify-between">
<a href="/home" 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">
Back to Dashboard
</a>
<button
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"
onclick="document.getElementById('create-asset-modal').showModal()"
>
Add New Asset
</button>
</div>
</section>
<hr class="h-px my-8 bg-gray-500 border-0 mx-10">
<!-- list each asset and create hidden popup modal for view/edit -->
<section class="mt-6">
<% if (assets.length == 0) { %>
<div class="max-w-md mx-auto mb-10 rounded-lg shadow-md text-white font-bold bg-red-400 py-4 text-center">
You have no assets, create a new asset.
</div>
<% } %>
<% assets.forEach((asset) => { %>
<!-- 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">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 -->
<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 -->
<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 %>')"
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">
<div>
ICON
</div>
<div>
<button
id="edit-<%= asset._id %>"
onclick="unlockAsset('<%= asset._id %>')"
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"
>
Edit
</button>
</div>
</div>
<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="ownerId-<%= asset.ownerId %>" name="ownerId" type="text" hidden value="<%= asset.ownerId %>">
<% 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>
</main>
<div class="mt-20"></div>
<script src="/static/scripts/assetManager.js"></script>
<script>
// Ensure all assets popup modals are locked and reset on page load
let assetIds = [];
<% assets.forEach((asset) => { %>
assetIds.push("<%= asset._id %>");
<% }) %>
// EJS or JS may say this is an error above.
// I assure you its not, just leave it be.
assetIds.forEach((id) => lockAsset(id));
</script>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+100
View File
@@ -0,0 +1,100 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main>
<h2 class="text-xl text-white font-semibold mb-4">Welcome: <%= user.name %></h2>
<!-- Buttons -->
<div class="flex justify-end gap-4 px-5 py-6">
<button class="cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">
Add Asset
</button>
<a href="/newPlan" class="cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">
Create new plan
</a>
</div>
<!--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 class="border-t border-blue-gray-50 p-10">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last
week
</p>
</div>
</div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last
week
</p>
</div>
</div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<!-- <div -->
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-green-600 to-green-400 text-white shadow-green-500/40 shadow-lg absolute -mt-4 grid h-16 w-16 place-items-center"> -->
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" -->
<!-- class="w-6 h-6 text-white"> -->
<!-- <path -->
<!-- d="M6.25 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM3.25 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM19.75 7.5a.75.75 0 00-1.5 0v2.25H16a.75.75 0 000 1.5h2.25v2.25a.75.75 0 001.5 0v-2.25H22a.75.75 0 000-1.5h-2.25V7.5z"> -->
<!-- </path> -->
<!-- </svg> -->
<!-- </div> -->
<!-- we can use this later if needed not sure what it shoudl be for right now. -->
<!-- <div class="p-4 text-right"> -->
<!-- <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">New Clients -->
<!-- </p> -->
<!-- <h4 -->
<!-- class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> -->
<!-- 3,462</h4> -->
<!-- </div> -->
<!-- <div class="border-t border-blue-gray-50 p-4"> -->
<!-- <p class="block antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> -->
<!-- <strong class="text-red-500">-2%</strong>&nbsp;than yesterday -->
<!-- </p> -->
<!-- </div> -->
<!-- </div> -->
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last
week
</p>
</div>
</div>
</div>
</div>
</div>
<div class="text-blue-gray-600">
</div>
</main>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<h1>Welcome!</h1>
<form method="get" action="/signup">
<button type="submit">signup</button>
</form>
</body>
</html>
+23 -63
View File
@@ -1,64 +1,24 @@
<html> <%- include("./partials/fileHeader") %>
<head> <%- include("./partials/headerStart") %>
<title>RCalculator</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png"> <main>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.2/dist/tailwind.min.css" rel="stylesheet"> <section class="bg-center bg-no-repeat bg-cover bg-[url('/images/bg1.jpg')] bg-gray-400 bg-blend-multiply">
</head> <div class="px-4 mx-auto max-w-screen-xl text-center py-24 lg:py-56">
<body class="bg-white dark:bg-gray-900"> <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>
<header> <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>
<nav class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> <div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"> <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="#" class="flex items-center space-x-3 rtl:space-x-reverse"> Get started
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/rekor.png" class="h-8" alt="Flowbite Logo" /> <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">
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
</a> </svg>
<button data-collapse-toggle="navbar-solid-bg" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md: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" aria-controls="navbar-solid-bg" aria-expanded="false"> </a>
<span class="sr-only">Open main menu</span> <a href="/aboutUs" class="inline-flex justify-center hover:text-gray-900 items-center py-3 px-5 sm:ms-4 text-base font-medium text-center text-white rounded-lg border border-white hover:bg-gray-100 focus:ring-4 focus:ring-gray-400">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14"> Learn more
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/> </a>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-solid-bg">
<ul class="flex flex-col font-medium mt-4 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-transparent dark:bg-gray-800 md:dark:bg-transparent dark:border-gray-700">
<li>
<a href="/landing" class="block py-2 px-3 md:p-0 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent" aria-current="page">Home</a>
</li>
<li>
<a href="/aboutUs" class="block py-2 px-3 md:p-0 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">About Us</a>
</li>
<li>
<a href="mailto:rcalculator@gmail.com" class="block py-2 px-3 md:p-0 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Contact us</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main>
<section class="bg-center bg-no-repeat bg-cover bg-[url('/images/bg1.jpg')] bg-gray-400 bg-blend-multiply">
<div class="px-4 mx-auto max-w-screen-xl text-center py-24 lg:py-56">
<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>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/login" 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">
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">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
</svg>
</a>
<a href="/aboutUs" class="inline-flex justify-center hover:text-gray-900 items-center py-3 px-5 sm:ms-4 text-base font-medium text-center text-white rounded-lg border border-white hover:bg-gray-100 focus:ring-4 focus:ring-gray-400">
Learn more
</a>
</div>
</div>
</section>
</main>
<footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 p-6 text-center text-gray-500 dark:text-gray-400 text-sm">
<div class="container mx-auto">
<p>&copy; 2025 RCalculator. All rights reserved.</p>
</div> </div>
</footer> </div>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> </section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script> </main>
</body>
</html> <%- include("./partials/footer") %>
+44
View File
@@ -0,0 +1,44 @@
<%- include("./partials/fileHeader") %>
<main>
<div class="flex justify-center items-center h-screen">
<form action="/login" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Login</h1>
<hr class="mt-3">
<div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" />
</div>
<div class="mt-3">
<label for="password" class="block text-base mb-2">Password</label>
<input type="password" id="password" name="password"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" />
</div>
<div class="text-red-800 font-bold" id="forgor"> <%= errMessage %></div>
<div class="mt-3 flex justify-between items-center">
<div>
<input type="checkbox">
<label>Remember me</label>
</div>
<div>
<a href="#" class="text-indigo-600 font-semibold">Forgot Password? </a>
</div>
</div>
<div class="mt-5">
<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>
</div>
<div class="mt-5 block justify-between items-center">
<label>Don't have an account?</label>
<a href="/signup" class="text-indigo-600 font-semibold">Register</a>
</div>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %>
+10
View File
@@ -0,0 +1,10 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main>
The More Page
</main>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+40
View File
@@ -0,0 +1,40 @@
<%- 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/footer") %>
+24
View File
@@ -0,0 +1,24 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/headerStart") %>
<main>
<section class="bg-center bg-no-repeat bg-cover bg-[url('/images/bg1.jpg')] bg-gray-400 bg-blend-multiply">
<div class="px-4 mx-auto max-w-screen-xl text-center py-24 lg:py-56">
<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>
<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">
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">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
</svg>
</a>
<a href="/aboutUs" class="inline-flex justify-center hover:text-gray-900 items-center py-3 px-5 sm:ms-4 text-base font-medium text-center text-white rounded-lg border border-white hover:bg-gray-100 focus:ring-4 focus:ring-gray-400">
Learn more
</a>
</div>
</div>
</section>
</main>
<%- include("./partials/footer") %>
+129
View File
@@ -0,0 +1,129 @@
<!-- 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;" value="<%= user._id %>" name="ownerId" type="text">
<input style="display: none;" value="other" name="type" type="text">
<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="<%= user._id %>" name="ownerId" type="text">
<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="<%= user._id %>" name="ownerId" type="text">
<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>
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>RCalculator</title>
</head>
<body class="bg-white dark:bg-gray-900">
+11
View File
@@ -0,0 +1,11 @@
<footer class="mt-16">
<div class="fixed bottom-0 left-0 z-50 hidden lg:block w-full h-16 bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 p-6 text-center text-gray-500 dark:text-gray-400 text-sm">
<div class="container mx-auto">
<p>&copy; 2025 RCalculator. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/static/scripts/geolocation.js"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<header>
<nav class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="/home" 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" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span>
</a>
<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">
<li>
<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>
</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>
</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>
</li>
<li>
<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>
</li>
<li>
<a href="/logout" class="block py-2 px-3 lg:p-0 text-gray-900 rounded hover:bg-gray-100 lg:hover:bg-transparent lg:border-0 lg:hover:text-blue-700 dark:text-white lg:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
+32
View File
@@ -0,0 +1,32 @@
<header>
<nav class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<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" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">RCalculator</span>
</a>
<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"
aria-controls="navbar-solid-bg" aria-expanded="false">
<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">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
<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">
<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>
</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>
</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>
</li>
</ul>
</div>
</div>
</nav>
</header>
+29
View File
@@ -0,0 +1,29 @@
<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="grid h-16 max-w-lg 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">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M11.293 3.293a1 1 0 0 1 1.414 0l6 6 2 2a1 1 0 0 1-1.414 1.414L19 12.414V19a2 2 0 0 1-2 2h-3a1 1 0 0 1-1-1v-3h-2v3a1 1 0 0 1-1 1H7a2 2 0 0 1-2-2v-6.586l-.293.293a1 1 0 0 1-1.414-1.414l2-2 6-6Z" clip-rule="evenodd"/>
</svg>
<span class="text-gray-500 dark:text-gray-400 text-sm">Dashboard</span>
</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">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M10.464 8.746c.227-.18.497-.311.786-.394v2.795a2.252 2.252 0 0 1-.786-.393c-.394-.313-.546-.681-.546-1.004 0-.323.152-.691.546-1.004ZM12.75 15.662v-2.824c.347.085.664.228.921.421.427.32.579.686.579.991 0 .305-.152.671-.579.991a2.534 2.534 0 0 1-.921.42Z" />
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25ZM12.75 6a.75.75 0 0 0-1.5 0v.816a3.836 3.836 0 0 0-1.72.756c-.712.566-1.112 1.35-1.112 2.178 0 .829.4 1.612 1.113 2.178.502.4 1.102.647 1.719.756v2.978a2.536 2.536 0 0 1-.921-.421l-.879-.66a.75.75 0 0 0-.9 1.2l.879.66c.533.4 1.169.645 1.821.75V18a.75.75 0 0 0 1.5 0v-.81a4.124 4.124 0 0 0 1.821-.749c.745-.559 1.179-1.344 1.179-2.191 0-.847-.434-1.632-1.179-2.191a4.122 4.122 0 0 0-1.821-.75V8.354c.29.082.559.213.786.393l.415.33a.75.75 0 0 0 .933-1.175l-.415-.33a3.836 3.836 0 0 0-1.719-.755V6Z" clip-rule="evenodd" />
</svg>
<span class="text-gray-500 dark:text-gray-400 text-sm">Assets</span>
</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">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 24 24">
<path d="M13.09 3.294c1.924.95 3.422 1.69 5.472.692a1 1 0 0 1 1.438.9v9.54a1 1 0 0 1-.562.9c-2.981 1.45-5.382.24-7.25-.701a38.739 38.739 0 0 0-.622-.31c-1.033-.497-1.887-.812-2.756-.77-.76.036-1.672.357-2.81 1.396V21a1 1 0 1 1-2 0V4.971a1 1 0 0 1 .297-.71c1.522-1.506 2.967-2.185 4.417-2.255 1.407-.068 2.653.453 3.72.967.225.108.443.216.655.32Z"/>
</svg>
<span class="text-gray-500 dark:text-gray-400 text-sm">Plans</span>
</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">
<svg height="30" width="30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"></path>
</svg>
<span class="text-gray-500 dark:text-gray-400 text-sm">More</span>
</a>
</div>
</div>
+74
View File
@@ -0,0 +1,74 @@
<%- 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-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">
<h3 class="text-2xl font-bold text-gray-800 dark:text-white"><%= plan.name %></h3>
</div>
<div>
<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-700 dark:text-blue-400"><%= plan.progress %></span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700">
<div class="bg-blue-600 h-3 rounded-full dark:bg-blue-500" style="width: <%= plan.progress %>;"></div>
</div>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Assets:</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Number of Assets:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white">16 MOCK</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Total Value:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white">$165,000 MOCK</p>
</div>
</div>
</div>
<div class="border-t border-gray-200 dark: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>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Retirement Age:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementAge %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Monthly Expenses:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementExpenses %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Retirement Assets:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementAssets %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400">Target Retirement Liabilities:</label>
<p class="mt-1 text-md text-gray-900 dark:text-white"><%= plan.retirementLiabilities %></p>
</div>
</div>
</div>
<div class="border-t border-gray-200 dark: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>
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-700 dark:text-blue-300">
USE YOUR HEAD MOCK
</p>
</div>
</div>
</div>
</main>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+24
View File
@@ -0,0 +1,24 @@
<%- 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 space-y-4">
<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>
<hr class="mt-3">
<% 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">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"><%= plan.name %></h5>
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4 dark:bg-gray-700">
<div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %> "></div>
</div>
<p class="font-normal text-gray-700 dark:text-gray-400"><%= plan.description %></p>
</a>
<% }) %>
</div>
</main>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+90
View File
@@ -0,0 +1,90 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main>
<h2 class="ml-11 mt-5 mb-6 text-xl font-semibold mb-4 text-white">Welcome <%= user.name %></h2>
<% 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 bg-white p-8 mb-10 rounded-lg shadow-md">
<div class="flex flex-row justify-between">
<h3 class="pt-2 pr-2 pb-2">Account settings</h3>
<button id="edit-account" onclick="unlockAccount()" 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="/updateAccount" method="post" class="space-y-4 mt-4" id="account-form">
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input id="email" disabled type="text" name="email" value="<%= user.email %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input id="name" disabled type="text" name="name" value="<%= user.name %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
<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">
<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">
<div>
<button id="save-account" 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="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>
</main>
<script src="/static/scripts/profile.js"></script>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+71
View File
@@ -0,0 +1,71 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main class="container mx-auto p-4">
<h2 class="text-xl 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">Financial Questionnaire</h3>
<form action="/questionnaire" method="post" class="space-y-4">
<div>
<label for="dob" class="block text-sm font-medium text-gray-700">Date of Birth</label>
<input 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">
</div>
<div>
<label for="education" class="block text-sm font-medium text-gray-700">Education</label>
<select 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">
<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 type="radio" name="maritalStatus" value="single" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2 text-sm text-gray-700">Single</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="maritalStatus" value="married" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2 text-sm text-gray-700">Married</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="maritalStatus" value="divorced" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2 text-sm text-gray-700">Divorced</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="maritalStatus" value="widowed" class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<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 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">
</div>
<div>
<label for="expenses" class="block text-sm font-medium text-gray-700">Monthly Expenses</label>
<input 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">
</div>
<div>
<label for="assets" class="block text-sm font-medium text-gray-700">Net Worth</label>
<input 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">
</div>
<div>
<label for="liabilities" class="block text-sm font-medium text-gray-700">Liabilities</label>
<input 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">
</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">Submit Questionnaire</button>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %>
+45 -8
View File
@@ -1,9 +1,46 @@
<!DOCTYPE html> <%- include("./partials/fileHeader") %>
<html> <%- include("./partials/headerStart") %>
<head>
</head> <main>
<body> <div class="flex justify-center items-center h-screen">
<h1>SIGNUP</h1> <form action="/signup" method="POST">
</body> <div class="w-96 p-6 shadow-1g bg-white rounded-md">
</html> <h1 class="text-3xl block text-center font-semibold">Signup</h1>
<hr class="mt-3">
<div class="mt-3">
<label for="name" class="block text-base mb-2">Name</label>
<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 "
placeholder="Enter Name" />
</div>
<div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" />
</div>
<div class="mt-3">
<label for="password" class="block text-base mb-2">Password</label>
<input type="password" id="password" name="password"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" />
<label for="password" class="block text-base mb-2 mt-3">Re-type Password</label>
<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 "
placeholder="Enter Password" />
</div>
<div class="text-red-800 font-bold " id="forgor"> <%= errMessage %></div>
<div class="mt-5">
<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>
</div>
<div class="mt-5 block justify-between items-center">
<label>Already have an account?</label>
<a href="/login" class="text-indigo-600 font-semibold">Login</a>
</div>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %>
+9
View File
@@ -0,0 +1,9 @@
<%- include("./partials/fileHeader") %>
<%- include("./partials/header") %>
<main>
<!-- Your content here -->
</main>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>