From 5abec70c613d12783b4ab02735c5a48d196dd47e Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 6 May 2025 00:33:36 -0700 Subject: [PATCH 01/10] login and signup touch ups index page template added --- app.js | 14 +-- src/views/index.ejs | 285 +++++++++++++++++++++++++++++++++++++++++-- src/views/login.ejs | 4 +- src/views/signup.ejs | 12 +- 4 files changed, 295 insertions(+), 20 deletions(-) diff --git a/app.js b/app.js index 5324680..e96e45a 100644 --- a/app.js +++ b/app.js @@ -10,7 +10,7 @@ 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"; +const secret = process.env.secret || "123-secret-xyz"; /*** Sessions ***/ app.use(session({ @@ -22,7 +22,7 @@ app.use(session({ })); 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("/static", express.static("./src/public")); app.use("/images", express.static("./src/public/images")); @@ -33,7 +33,7 @@ const { connectMongo, getCollection } = require("./src/database/connection"); let users; async function initDatabase() { const db = await connectMongo(mongoURI, database); - + // For any collection, init here users = await getCollection(db, "users"); } @@ -42,7 +42,7 @@ async function initDatabase() { app.get('/', (req, res) => { if (!req.session.errMessage) req.session.errMessage = ""; - res.render('landing'); + res.render('index'); return res.status(status.Ok); }); @@ -54,7 +54,7 @@ app.get('/signup', (req, res) => { app.get('/login', (req, res) => { if (req.session.authenticated) { res.redirect("/home"); - return res.status(status.Ok); + return res.status(status.Ok); } res.render('login', { errMessage: req.session.errMessage }); return res.status(status.Ok); @@ -81,10 +81,10 @@ initDatabase().then(() => { app.get('/*splat', (req, res) => { res.send('404 Not Found'); return res.status(status.NotFound); - }); + }); // Start app app.listen(port, () => { console.log(`Server listening on port ${port}`); - }); + }); }); diff --git a/src/views/index.ejs b/src/views/index.ejs index 975b566..fe570e0 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -9,15 +9,284 @@ - The Index Page - +
+
+ + + + + <%# include("./partials/navBar") %> + <%# include("./partials/footer") %> diff --git a/src/views/login.ejs b/src/views/login.ejs index 981b77c..cf46b63 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -21,13 +21,13 @@
<%= errMessage %>
diff --git a/src/views/signup.ejs b/src/views/signup.ejs index e19fc96..2345bbf 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -18,20 +18,26 @@

Signup


+
+ + +
<%= errMessage %>
From b558a75f20424be778d0a2f7d3b5a737f0f4d6e2 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 6 May 2025 09:46:14 -0700 Subject: [PATCH 02/10] uncomment partials --- src/views/index.ejs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/index.ejs b/src/views/index.ejs index fe570e0..512ff5b 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -9,8 +9,8 @@ - <%# include("./partials/fileHeader") %> - <%# include("./partials/header") %> + <%- include("./partials/fileHeader") %> + <%- include("./partials/header") %>
@@ -285,8 +285,8 @@
- <%# include("./partials/navBar") %> - <%# include("./partials/footer") %> + <%- include("./partials/navBar") %> + <%- include("./partials/footer") %> From 4bb9a015243ec0d07d24de2020245a51af550570 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 6 May 2025 11:05:37 -0700 Subject: [PATCH 03/10] deleting projects table --- src/views/index.ejs | 320 +++++++++++++++++++------------------------- 1 file changed, 134 insertions(+), 186 deletions(-) diff --git a/src/views/index.ejs b/src/views/index.ejs index 3c612b2..3e79630 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -1,64 +1,102 @@ - <%- include("./partials/fileHeader") %> - <%- include("./partials/header") %> - +<%- include("./partials/fileHeader") %> + <%- include("./partials/header") %> +
+
+
+ -<%- include("./partials/navBar") %> -<%- include("./partials/footer") %> \ No newline at end of file + <%- include("./partials/navBar") %> + <%- include("./partials/footer") %> From 83be006a83c4d9c1f41d341b33e17190f2a62629 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Tue, 6 May 2025 13:38:00 -0700 Subject: [PATCH 04/10] feature/geolocation implemented --- app.js | 10 ++++++++++ src/public/scripts/geolocation.js | 24 ++++++++++++++++++++++++ src/views/landing.ejs | 5 +++++ 3 files changed, 39 insertions(+) create mode 100644 src/public/scripts/geolocation.js diff --git a/app.js b/app.js index 6321d6e..9e5e519 100644 --- a/app.js +++ b/app.js @@ -28,6 +28,7 @@ app.set('views',path.join(__dirname, 'src/views')); 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"); @@ -69,6 +70,15 @@ app.get('/aboutUs', (req, res) => { return res.status(status.Ok); }); +app.post('/api/location', async (req,res) => { + const { latitude, longitude } = req.body; + + const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&result_type=country&key=${process.env.geolocation_api}`); + + const data = await response.json(); + + res.json(data); +}); // Initialize database and start app initDatabase().then(() => { diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js new file mode 100644 index 0000000..ebe001b --- /dev/null +++ b/src/public/scripts/geolocation.js @@ -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); +} diff --git a/src/views/landing.ejs b/src/views/landing.ejs index d7a24d1..9b2c7c7 100644 --- a/src/views/landing.ejs +++ b/src/views/landing.ejs @@ -18,7 +18,12 @@
+ + + + + <%- include("./partials/footer") %> From b944c37c02b42fe5de10d7c4f7c39353f2e62c58 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 6 May 2025 14:27:27 -0700 Subject: [PATCH 05/10] Plans page created, listing the plans from the database, new plan page created and saving to database --- app.js | 5 ++- src/auth/authentication.js | 2 + src/router/user.js | 77 +++++++++++++++++++++++++++++++++++--- src/views/newPlan.ejs | 40 ++++++++++++++++++++ src/views/plans.ejs | 19 +++++++++- 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 src/views/newPlan.ejs diff --git a/app.js b/app.js index 8e9dd77..9bd6875 100644 --- a/app.js +++ b/app.js @@ -38,6 +38,7 @@ async function initDatabase() { // For any collection, init here users = await getCollection(db, "users"); + plans = await getCollection(db, "plans"); } /*** ROUTINGS ***/ @@ -76,8 +77,8 @@ initDatabase().then(() => { 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)); + const middleware = require("./src/auth/middleware")(users, plans); + app.use(require('./src/router/user')(middleware, users, plans)); // 404 handler app.get('/*splat', (req, res) => { diff --git a/src/auth/authentication.js b/src/auth/authentication.js index fe7c97d..db53450 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -42,6 +42,7 @@ module.exports = (users) => { console.log("User logged in successfully"); console.log("User email: " + req.body.email); req.session.authenticated = true; + req.session.userId = user._id; req.session.email = req.body.email; req.session.errMessage = ""; res.redirect("/home"); @@ -86,6 +87,7 @@ module.exports = (users) => { req.session.authenticated = true; req.session.email = req.body.email; + req.session.userId = results.insertedId; req.session.errMessage = ""; res.status(status.Ok); diff --git a/src/router/user.js b/src/router/user.js index dd8eaf6..dfd022d 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,7 +1,7 @@ const status = require("../util/statuses"); const joi = require("joi"); -module.exports = (middleware, users) => { +module.exports = (middleware, users, plans) => { const router = require("express").Router(); router.use(middleware); @@ -16,11 +16,78 @@ module.exports = (middleware, users) => { return res.status(status.Ok); }); - router.get('/plans', (req, res) => { - res.render('plans', { user: req.user }); - return res.status(status.Ok); + router.get('/plans', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + try { + + // console.log(req.user.email); + const userPlans = await plans.find({userEmail: req.user.email }).toArray(); + // console.log(userPlans); + res.render('plans', { + user: req.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('/newPlan', (req, res) => { + const errMessage = req.session.errMessage; + req.session.errMessage = ""; + res.render('newPlan', { user: req.user, errMessage: errMessage }); + }); + + router.post('/newPlan', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + 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 = { + userEmail: req.user.email, + name: value.name, + retirementAge: value.retirementAge, + retirementExpenses: value.retirementExpenses, + retirementAssets: value.retirementAssets, + retirementLiabilities: value.retirementLiabilities, + progress: "0%" + }; + + try{ + await plans.insertOne(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.user }); return res.status(status.Ok); @@ -43,7 +110,7 @@ module.exports = (middleware, users) => { }); router.post('/questionnaire', (req, res) => { - console.log("Questionnaire POST body:", req.body); + // console.log("Questionnaire POST body:", req.body); const questionnaireSchema = joi.object({ dob: joi.date().required(), diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs new file mode 100644 index 0000000..40b409b --- /dev/null +++ b/src/views/newPlan.ejs @@ -0,0 +1,40 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.email %>

+
+

Financial Questionnaire

+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/plans.ejs b/src/views/plans.ejs index 7f6e699..10a6ca1 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -1,8 +1,23 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
- The Plans Page +
+

Welcome: <%= user.email %>

+
+

My Retirement Plans

+
New Plan
+
+ <% plans.forEach(plan => { %> + +
<%= plan.name %>
+
+
+
+

<%= plan.description %>

+
+ <% }) %> +
+
<%- include("./partials/navBar") %> From 9b998078b7120612bb73e39a0e2663364a1c64c8 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 6 May 2025 14:30:43 -0700 Subject: [PATCH 06/10] Questionnaire page cleaned up and updated --- src/router/user.js | 8 -------- src/views/questionnaire.ejs | 20 -------------------- 2 files changed, 28 deletions(-) diff --git a/src/router/user.js b/src/router/user.js index dfd022d..d592f37 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -120,10 +120,6 @@ module.exports = (middleware, users, plans) => { expenses: joi.number().min(0).required(), assets: joi.number().min(0).required(), liabilities: joi.number().min(0).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 }; @@ -148,10 +144,6 @@ module.exports = (middleware, users, plans) => { expenses: value.expenses, assets: value.assets, liabilities: value.liabilities, - retirementAge: value.retirementAge, - retirementExpenses: value.retirementExpenses, - retirementAssets: value.retirementAssets, - retirementLiabilities: value.retirementLiabilities, } } ).then((result) => { diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 10a1b29..c023058 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -60,26 +60,6 @@ - -
- - -
- -
- - -
- -
- - -
- -
- - -
From e1626f6ae95181142a548a9b725a6fff99752013 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Tue, 6 May 2025 14:46:01 -0700 Subject: [PATCH 07/10] feature/completed getting geolocation data --- src/views/landing.ejs | 5 ----- src/views/partials/footer.ejs | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/views/landing.ejs b/src/views/landing.ejs index 9b2c7c7..d7a24d1 100644 --- a/src/views/landing.ejs +++ b/src/views/landing.ejs @@ -18,12 +18,7 @@
- - -
- - <%- include("./partials/footer") %> diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 8f457f3..0c2379c 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -6,5 +6,6 @@ + \ No newline at end of file From 7b9b18f030dba35b85e19cc2d69800badb759290 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 6 May 2025 15:08:04 -0700 Subject: [PATCH 08/10] dashboard page with cards to fill in and add asset and new plan button --- app.js | 4 +- src/views/dashboard.ejs | 99 ++++++++++++++++++ src/views/home.ejs | 1 - src/views/index.ejs | 226 ---------------------------------------- 4 files changed, 101 insertions(+), 229 deletions(-) create mode 100644 src/views/dashboard.ejs delete mode 100644 src/views/index.ejs diff --git a/app.js b/app.js index 54804bd..9f7f654 100644 --- a/app.js +++ b/app.js @@ -3,7 +3,7 @@ const MongoStore = require("connect-mongo"); const session = require("express-session"); const express = require('express'); const path = require('path'); -const joi = require('joi'); +const joi = require('joi'); require('dotenv').config(); @@ -44,7 +44,7 @@ async function initDatabase() { app.get('/', (req, res) => { if (!req.session.errMessage) req.session.errMessage = ""; - res.render('index'); + res.render('dashboard'); return res.status(status.Ok); }); diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs new file mode 100644 index 0000000..3de6635 --- /dev/null +++ b/src/views/dashboard.ejs @@ -0,0 +1,99 @@ +<%- include("./partials/fileHeader") %> + <%- include("./partials/header") %> + +
+ +
+ + +
+ +
+
+ +
+ + + +
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or graph or something  than last + week +

+
+
+
+
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or graph or something  than last + week +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or graph or something  than last + week +

+
+
+
+
+
+
+
+
+ + <%- include("./partials/navBar") %> + <%- include("./partials/footer") %> diff --git a/src/views/home.ejs b/src/views/home.ejs index 3637163..405e232 100644 --- a/src/views/home.ejs +++ b/src/views/home.ejs @@ -2,7 +2,6 @@ <%- include("./partials/header") %>
- <%- include("./partials/header") %> Welcome: <%= user.email %> The Dashboard Page <% if(user.authenticated) { %> diff --git a/src/views/index.ejs b/src/views/index.ejs deleted file mode 100644 index 3e79630..0000000 --- a/src/views/index.ejs +++ /dev/null @@ -1,226 +0,0 @@ -<%- include("./partials/fileHeader") %> - <%- include("./partials/header") %> - -
-
- -
-
-
-
- -
-
-

Today's - Money

-

- $53k

-
-
-

- +55% than last week -

-
-
-
-
- -
-
-

Today's - Users

-

- 2,300

-
-
-

- +3% than last month -

-
-
-
-
- -
-
-

New Clients -

-

- 3,462

-
-
-

- -2% than yesterday -

-
-
-
-
- -
-
-

Sales

-

- $103,430

-
-
-

- +5% from yesterday -

-
-
-
- - -
-
-
-
-
-
-
- - <%- include("./partials/navBar") %> - <%- include("./partials/footer") %> From d67718733d28d4dda6ef5ddc854826885e106ff4 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 6 May 2025 15:46:57 -0700 Subject: [PATCH 09/10] fix/ Route fixed for index/dashboard --- app.js | 2 +- src/router/user.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index fa59c80..2d7af71 100644 --- a/app.js +++ b/app.js @@ -46,7 +46,7 @@ async function initDatabase() { app.get('/', (req, res) => { if (!req.session.errMessage) req.session.errMessage = ""; - res.render('dashboard'); + res.render('landing'); return res.status(status.Ok); }); diff --git a/src/router/user.js b/src/router/user.js index 9daa7cb..6061bdd 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -9,7 +9,7 @@ module.exports = (middleware, users, plans) => { router.use(middleware); router.get('/home', async (req, res) => { - res.render('home', { user: req.user }); + res.render('dashboard', { user: req.user }); return res.status(status.Ok); }); From 173c0b63c426585a21875035d66295962ee35bf5 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 6 May 2025 18:36:25 -0700 Subject: [PATCH 10/10] Plan detail page finished with mock data --- src/auth/authentication.js | 1 + src/router/user.js | 42 ++++++++++++++++++++ src/views/dashboard.ejs | 5 ++- src/views/newPlan.ejs | 4 +- src/views/partials/footer.ejs | 2 +- src/views/partials/navBar.ejs | 2 +- src/views/planDetail.ejs | 74 +++++++++++++++++++++++++++++++++++ src/views/plans.ejs | 6 +-- src/views/questionnaire.ejs | 2 +- src/views/signup.ejs | 6 --- 10 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 src/views/planDetail.ejs diff --git a/src/auth/authentication.js b/src/auth/authentication.js index 261b5a4..7702450 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -76,6 +76,7 @@ module.exports = (users) => { email: req.body.email, name: req.body.name, password: hashedPassword, + financialData: false, }).then((results, err) => { if (err) { console.error(err); diff --git a/src/router/user.js b/src/router/user.js index 6061bdd..4bc3207 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,6 +1,7 @@ const status = require("../util/statuses"); const bcrypt = require('bcrypt'); const joi = require("joi"); +const { ObjectId } = require('mongodb'); const salt = 12; module.exports = (middleware, users, plans) => { @@ -40,7 +41,48 @@ module.exports = (middleware, users, plans) => { } }); + router.get('/plans/:id', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + 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({ userEmail: req.user.email, _id: new ObjectId(planId) }); + + if (!plan) { + console.log(`Plan not found with ID: ${planId} for user: ${req.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.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.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + if(!req.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.user, errMessage: errMessage }); diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs index 3de6635..e4c9b36 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -2,14 +2,15 @@ <%- include("./partials/header") %>
+

Welcome: <%= user.name %>

diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs index 40b409b..a05dd1a 100644 --- a/src/views/newPlan.ejs +++ b/src/views/newPlan.ejs @@ -2,9 +2,9 @@ <%- include("./partials/header") %>
-

Welcome: <%= user.email %>

+

Welcome: <%= user.name %>

+ +<%- include("./partials/navBar") %> +<%- include("./partials/footer") %> diff --git a/src/views/plans.ejs b/src/views/plans.ejs index 10a6ca1..ce7ca80 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -2,13 +2,13 @@ <%- include("./partials/header") %>
-

Welcome: <%= user.email %>

+

Welcome: <%= user.name %>