├── models ├── Share.js ├── Transaction.js └── User.js ├── public ├── javascript │ └── script.js ├── images │ ├── google.png │ ├── graph.jpeg │ ├── graph1.png │ ├── graph2.png │ ├── graph3.png │ ├── TradeByte.png │ ├── TradeByte-Small.png │ ├── desktop-mockup.png │ ├── TradeByte-Favicon.png │ ├── mobile-buy-mockup.png │ ├── mobile-view-mockup.png │ ├── mobile-market-mockup.png │ └── mobile-payment-mockup.png └── css │ ├── tailwind.css │ └── style.css ├── project-plan ├── TradeByte.jpg └── ui-designs │ ├── Market.png │ └── Portfolio-Dashboard.png ├── postcss.config.js ├── views ├── partials │ ├── _footer.ejs │ ├── _search.ejs │ ├── _messages.ejs │ └── _nav.ejs ├── layouts │ ├── login.ejs │ └── app.ejs ├── error │ ├── 404.ejs │ └── 500.ejs ├── history.ejs ├── edit.ejs ├── done.ejs ├── addBalance.ejs ├── cart.ejs ├── transaction.ejs ├── view.ejs ├── signup.ejs ├── portfolio.ejs └── market.ejs ├── .github ├── workflows │ └── greet-contributors.yml ├── ISSUE_TEMPLATE │ └── issue-template.md └── PULL_REQUEST_TEMPLATE.md ├── middleware └── auth.js ├── helpers ├── getPrice.js ├── getCompanyNameAndLogo.js ├── emailHelper.js └── getOverview.js ├── config ├── db.js ├── passportLocal.js ├── passport.js └── data-compact.json ├── routes └── api │ ├── done.js │ ├── search.js │ ├── index.js │ ├── auth.js │ ├── cart.js │ ├── market.js │ ├── edit.js │ ├── addBalance.js │ ├── view.js │ ├── user.js │ ├── portfolio.js │ └── transaction.js ├── tailwind.config.js ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── .gitignore ├── app.js ├── CODE_OF_CONDUCT.md └── README.md /models/Share.js: -------------------------------------------------------------------------------- 1 | // Sharing History 2 | -------------------------------------------------------------------------------- /public/javascript/script.js: -------------------------------------------------------------------------------- 1 | // Main Script 2 | 3 | console.log("Hello TradeByte!"); -------------------------------------------------------------------------------- /public/images/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/google.png -------------------------------------------------------------------------------- /public/images/graph.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/graph.jpeg -------------------------------------------------------------------------------- /public/images/graph1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/graph1.png -------------------------------------------------------------------------------- /public/images/graph2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/graph2.png -------------------------------------------------------------------------------- /public/images/graph3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/graph3.png -------------------------------------------------------------------------------- /project-plan/TradeByte.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/project-plan/TradeByte.jpg -------------------------------------------------------------------------------- /public/images/TradeByte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/TradeByte.png -------------------------------------------------------------------------------- /project-plan/ui-designs/Market.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/project-plan/ui-designs/Market.png -------------------------------------------------------------------------------- /public/images/TradeByte-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/TradeByte-Small.png -------------------------------------------------------------------------------- /public/images/desktop-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/desktop-mockup.png -------------------------------------------------------------------------------- /public/images/TradeByte-Favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/TradeByte-Favicon.png -------------------------------------------------------------------------------- /public/images/mobile-buy-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/mobile-buy-mockup.png -------------------------------------------------------------------------------- /public/images/mobile-view-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/mobile-view-mockup.png -------------------------------------------------------------------------------- /public/images/mobile-market-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/mobile-market-mockup.png -------------------------------------------------------------------------------- /public/images/mobile-payment-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/public/images/mobile-payment-mockup.png -------------------------------------------------------------------------------- /project-plan/ui-designs/Portfolio-Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iampavangandhi/TradeByte/HEAD/project-plan/ui-designs/Portfolio-Dashboard.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | require('cssnano')({ 6 | preset: 'default', 7 | }), 8 | ] 9 | } -------------------------------------------------------------------------------- /views/partials/_footer.ejs: -------------------------------------------------------------------------------- 1 |
4 |

Made with ❤ by TradeByte Team

5 |

© TradeByte Team, 2020

6 |
7 | -------------------------------------------------------------------------------- /.github/workflows/greet-contributors.yml: -------------------------------------------------------------------------------- 1 | name: "GreetContributor" 2 | on: 3 | pull_request: 4 | types: [opened,synchronize] 5 | 6 | jobs: 7 | GreetCommitter: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: "Greet contributor" 11 | uses: ibakshay/greet-contributors-action@v3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: Describe this issue here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description** 11 | A clear and concise description of what the problem is. 12 | 13 | **Suggestions and Tips** 14 | Suggestions and Tips for the Issue. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | // Auth Middlewares 2 | 3 | module.exports = { 4 | ensureAuth: function (req, res, next) { 5 | if (req.isAuthenticated()) { 6 | return next(); 7 | } else { 8 | res.redirect("/"); 9 | } 10 | }, 11 | ensureGuest: function (req, res, next) { 12 | if (!req.isAuthenticated()) { 13 | return next(); 14 | } else { 15 | res.redirect("/portfolio"); 16 | } 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Fixes #issuenumber 2 | - 3 | 4 | ## What type of PR is this? 5 | 6 | - [ ] 🚀 Added Feature 7 | - [ ] 🐛 Fixed Bug 8 | - [ ] 📝 Docs Update 9 | - [ ] 🚩 Other 10 | 11 | ## Check everything 12 | 13 | - [ ] I have added the issue number for which this pull request is created. 14 | - [ ] I have selected the TradeByte Project in Projects 15 | 16 | ## Description 17 | 18 | A clear and concise description 19 | 20 | -------------------------------------------------------------------------------- /helpers/getPrice.js: -------------------------------------------------------------------------------- 1 | // Get Stock Price Function 2 | 3 | const alpha = require("alphavantage")({ 4 | key: process.env.ALPHA_VANTAGE_KEY, 5 | }); 6 | 7 | module.exports = async function getPrice(symbol) { 8 | let stockPrice = alpha.data.quote(symbol).then((res) => { 9 | let latestPrice = res["Global Quote"]["05. price"]; 10 | let low = res["Global Quote"]["04. low"]; 11 | let high = res["Global Quote"]["03. high"]; 12 | return { latestPrice, high, low }; 13 | }); 14 | return stockPrice; 15 | }; 16 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | // MongoDB Connection 2 | 3 | const mongoose = require("mongoose"); 4 | 5 | const connectDB = async () => { 6 | try { 7 | const conn = await mongoose.connect(process.env.MONGO_URI, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useUnifiedTopology: true, 11 | useFindAndModify: false, 12 | }); 13 | 14 | console.log(`MongoDB Connected: ${conn.connection.host}`); 15 | } catch (err) { 16 | console.error(err); 17 | process.exit(1); 18 | } 19 | }; 20 | 21 | module.exports = connectDB; 22 | -------------------------------------------------------------------------------- /routes/api/done.js: -------------------------------------------------------------------------------- 1 | // Done Route 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureAuth } = require("../../middleware/auth"); 8 | 9 | // @desc Done page 10 | // @route GET /done 11 | // @access Private 12 | router.get("/", ensureAuth, (req, res) => { 13 | const userId = req.user.id; 14 | const host = req.headers.host; 15 | res.status(200).render("done", { userId, host }); // pass userId for the share link generation 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /routes/api/search.js: -------------------------------------------------------------------------------- 1 | // Search Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | 8 | const { ensureAuth } = require("../../middleware/auth"); 9 | 10 | // Importing JSON Data 11 | const totalData = require("../../config/data-total.json"); 12 | 13 | // @desc Search Bar 14 | // @route GET /search 15 | // @access Private 16 | router.get("/", ensureAuth, (req, res) => { 17 | let avatar = req.user.image; 18 | 19 | res.status(200).render("search", { 20 | layout: "layouts/app", 21 | avatar, 22 | totalData, 23 | href: "/search", 24 | }); 25 | }); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /models/Transaction.js: -------------------------------------------------------------------------------- 1 | // Adding / Spending Balance History 2 | 3 | const mongoose = require("mongoose"); 4 | var Float = require("mongoose-float").loadType(mongoose); 5 | 6 | const TransactionSchema = new mongoose.Schema({ 7 | details: { 8 | type: String, 9 | required: true, 10 | }, 11 | amount: { 12 | type: Float, 13 | required: true, 14 | }, 15 | operation: { 16 | type: String, 17 | required: true, 18 | }, 19 | user: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: "User", 22 | }, 23 | createdAt: { 24 | type: Date, 25 | default: Date.now, 26 | }, 27 | }); 28 | 29 | module.exports = mongoose.model("Transaction", TransactionSchema); 30 | -------------------------------------------------------------------------------- /routes/api/index.js: -------------------------------------------------------------------------------- 1 | // Index Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureGuest } = require("../../middleware/auth"); 8 | 9 | // @desc Login/Landing page 10 | // @route GET / 11 | // @access Public 12 | router.get("/", ensureGuest, (req, res) => { 13 | res.status(200).render("login", { layout: "layouts/login" }); 14 | }); 15 | 16 | // @desc Login from Referral link 17 | // @route GET /share/:id (will be redirected to login page) 18 | // @access Public 19 | router.get("/share/:id", ensureGuest, (req, res) => { 20 | let user1 = req.params.id; 21 | res.cookie("prevUser", user1, { expire: 36000 + Date.now() }).redirect("/"); 22 | }); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /helpers/getCompanyNameAndLogo.js: -------------------------------------------------------------------------------- 1 | // Get Stock Company symbol Function 2 | 3 | const axios = require("axios"); 4 | 5 | module.exports = async function getCompanyNameAndLogo(symbol) { 6 | let data = await axios 7 | .get( 8 | `https://www.alphavantage.co/query?function=OVERVIEW&symbol=${symbol}&apikey=${process.env.ALPHA_VANTAGE_KEY}` 9 | ) 10 | .then(async (resp) => { 11 | let companyName = resp.data.Name; 12 | let logoSrc = await axios 13 | .get( 14 | `https://autocomplete.clearbit.com/v1/companies/suggest?query=:${ 15 | companyName.split(" ")[0] 16 | }` 17 | ) 18 | .then((resp) => resp.data[0].logo) 19 | .catch((err) => console.log(err)); 20 | return { companyName, logoSrc }; 21 | }) 22 | .catch((err) => { 23 | console.log(err); 24 | }); 25 | 26 | return data; 27 | }; 28 | -------------------------------------------------------------------------------- /routes/api/auth.js: -------------------------------------------------------------------------------- 1 | // Auth Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const passport = require("passport"); 7 | const router = express.Router(); 8 | const { ensureGuest } = require("../../middleware/auth"); 9 | 10 | // @desc Auth with Google 11 | // @route GET /auth/google 12 | router.get( 13 | "/google", 14 | passport.authenticate("google", { scope: ["profile email"] }) 15 | ); 16 | 17 | // @desc Google auth callback 18 | // @route GET /auth/google/callback 19 | router.get( 20 | "/google/callback", 21 | passport.authenticate("google", { failureRedirect: "/" }), 22 | (req, res) => { 23 | res.status(200).redirect("/portfolio"); 24 | } 25 | ); 26 | 27 | // @desc Logout user 28 | // @route /auth/logout 29 | router.get("/logout", (req, res) => { 30 | req.logout(); 31 | res.status(200).redirect("/"); 32 | }); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /routes/api/cart.js: -------------------------------------------------------------------------------- 1 | // Cart Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureAuth } = require("../../middleware/auth"); 8 | 9 | const User = require("../../models/User"); 10 | const getPrice = require("../../helpers/getPrice"); 11 | const getCompanyNameAndLogo = require("../../helpers/getCompanyNameAndLogo"); 12 | 13 | // @desc Cart Page 14 | // @route GET /cart/:symbol 15 | // @access Private 16 | router.get("/:symbol", ensureAuth, async (req, res) => { 17 | const symbol = req.params.symbol; 18 | const { latestPrice } = await getPrice(symbol); 19 | const { companyName, logoSrc } = await getCompanyNameAndLogo(symbol); 20 | res.status(200).render("cart", { 21 | layout: "layouts/app", 22 | symbol, 23 | latestPrice, 24 | logoSrc, 25 | companyName, 26 | href: "/market", 27 | avatar: req.user.image, 28 | }); 29 | }); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /helpers/emailHelper.js: -------------------------------------------------------------------------------- 1 | // Email Helper Function 2 | 3 | require("dotenv").config(); 4 | const nodemailer = require("nodemailer"); 5 | 6 | const transporter = nodemailer.createTransport({ 7 | host: "smtp.gmail.com", 8 | port: 465, 9 | secure: true, 10 | auth: { 11 | type: "OAuth2", 12 | user: process.env.GMAIL_ADDRESS, 13 | clientId: process.env.GMAIL_OAUTH_CLIENT_ID, 14 | clientSecret: process.env.GMAIL_OAUTH_CLIENT_SECRET, 15 | refreshToken: process.env.GMAIL_OAUTH_REFRESH_TOKEN, 16 | accessToken: process.env.GMAIL_OAUTH_ACCESS_TOKEN, 17 | expires: Number.parseInt(process.env.GMAIL_OAUTH_TOKEN_EXPIRE, 10), 18 | }, 19 | }); 20 | 21 | exports.sendEmail = (mailOptions) => 22 | new Promise((resolve, reject) => { 23 | transporter.sendMail(mailOptions, (error) => { 24 | if (error) { 25 | console.error(error.stack || error); 26 | return reject(error); 27 | } 28 | resolve(); 29 | }); 30 | }).catch((error) => { 31 | console.log(error); 32 | }); 33 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { colors } = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | purge: { 5 | enabled: true, 6 | content: ["./views/**/*.ejs"], 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | blue: { 12 | ...colors.blue, 13 | 900: "#1955e4", 14 | }, 15 | primary: "#FF6363", 16 | secondary: { 17 | 100: "#E2E2D5", 18 | 200: "#888883", 19 | }, 20 | // colors according to design 400->blue 200->red 500->green 21 | tempc: { 22 | 100: "#365088", 23 | 200: "#E93434", 24 | 300: "#AAB8D0", 25 | 400: "#266FEA", 26 | 500: "#41D7AA", 27 | 600: "#236AE1", 28 | 700: "#879CC0", 29 | 800: "#CADAEC", 30 | 900: "#3DB197", 31 | 1000: "#D9FFF7", 32 | 1100: "#E5EDFB", 33 | 1200: "#F8FAFF", 34 | }, 35 | }, 36 | fontFamily: { 37 | body: ["Nunito"], 38 | }, 39 | }, 40 | }, 41 | variants: {}, 42 | plugins: [], 43 | }; 44 | -------------------------------------------------------------------------------- /views/layouts/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 17 | 18 | 19 | 23 | 24 | TradeByte 25 | 26 | 27 | 28 | <%- body %> 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pavan Gandhi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /views/layouts/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 21 | TradeByte 22 | 23 | 24 | 25 |
26 | <%- include('../partials/_nav.ejs') %> <%- body %> 27 |
28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /config/passportLocal.js: -------------------------------------------------------------------------------- 1 | // Passport Local Strategy 2 | 3 | const LocalStrategy = require("passport-local").Strategy; 4 | const bcrypt = require("bcryptjs"); 5 | 6 | // Load User model 7 | const User = require("../models/User"); 8 | 9 | module.exports = function (passport) { 10 | passport.use( 11 | new LocalStrategy({ usernameField: "email" }, (email, password, done) => { 12 | // Match User 13 | User.findOne({ 14 | email: email, 15 | }).then((user) => { 16 | if (!user) { 17 | return done(null, false, { message: "That email is not registered" }); 18 | } 19 | 20 | // Match user Password 21 | bcrypt.compare(password, user.password, (err, isMatch) => { 22 | if (err) throw err; 23 | if (isMatch) { 24 | done(null, user); 25 | } else { 26 | return done(null, false, { message: "That password is incorrect" }); 27 | } 28 | }); 29 | }); 30 | }) 31 | ); 32 | 33 | passport.serializeUser((user, done) => { 34 | done(null, user.id); 35 | }); 36 | 37 | passport.deserializeUser((id, done) => { 38 | User.findById(id, (err, user) => { 39 | done(err, user); 40 | }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /views/partials/_search.ejs: -------------------------------------------------------------------------------- 1 |
4 | 11 | 31 |
32 | -------------------------------------------------------------------------------- /routes/api/market.js: -------------------------------------------------------------------------------- 1 | // Market Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureAuth, ensureGuest } = require("../../middleware/auth"); 8 | 9 | const compactData = require("../../config/data-compact.json"); 10 | const totalData = require("../../config/data-total.json"); 11 | 12 | // @desc Market page 13 | // @route GET /Market 14 | // @access Private 15 | router.get("/", ensureAuth, async (req, res) => { 16 | let min = 0; 17 | let max = 50; 18 | 19 | res.status(200).render("market", { 20 | layout: "layouts/app", 21 | compactData, 22 | totalData, 23 | min, 24 | max, 25 | href: "/market", 26 | avatar: req.user.image, 27 | }); 28 | }); 29 | 30 | // @desc Market page 31 | // @route GET /Market/:page 32 | // @access Private 33 | router.get("/:page", ensureAuth, async (req, res) => { 34 | let page = req.params.page - 1; 35 | let min = page * 50; 36 | let max = page * 50 + 50; 37 | 38 | if (page <= 0 || page >= 10) { 39 | res.status(200).redirect("/market"); 40 | } else { 41 | res.status(200).render("market", { 42 | layout: "layouts/app", 43 | compactData, 44 | totalData, 45 | min, 46 | max, 47 | href: "/market", 48 | avatar: req.user.image, 49 | }); 50 | } 51 | }); 52 | 53 | module.exports = router; 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | This is a group project started under The Uplift Project (Nodejs Stack) by GS Foundation. But It's open for contribution from everyone. 4 | 5 | Kindly write a proper description while making issues or pull requests. 6 | 7 | ## Ways of Contributing 8 | 9 | ### You can contribute in many ways: 10 | 11 | 1. By adding features 12 | 2. By reporting bugs 13 | 3. By solving issues 14 | 4. By improving the code quality 15 | 5. By improving the readme and docs 16 | 17 | ## Contributing Guidelines 18 | 19 | ### The basic guidelines for contributing are as follows: 20 | 21 | - Fork the repo 22 | - Clone the repo 23 | - Create a branch using 24 | - `git checkout -b fix/#issuenumber` 25 | or 26 | - `git checkout -b bug/feature` 27 | - Fix Issue / Add Feature / Solve Bug / Do Coding 28 | - Test the changes in your system 29 | - Create a pull request using below commands 30 | - `git add --all` 31 | - `git commit -m "your commit message"` 32 | - `git push origin branch-name` 33 | - Go to the [Repository](https://github.com/iampavangandhi/TradeByte) 34 | - Create Pull Request against `master` branch 35 | - Add a suitable title and description to the Pull Request, `Add TradeByte Project in Projects`, and `Tag the Issue number in Pull Request description` if the pull request is related to some issue logged here: [Issues](https://github.com/iampavangandhi/TheNodeCourse/issues) 36 | - You're done. Wait for your code to get reviewed and merged 37 | 38 | ### Give it a Star 🌟 if you like our Work 😊 39 | -------------------------------------------------------------------------------- /helpers/getOverview.js: -------------------------------------------------------------------------------- 1 | // Stock Overview Helper Function 2 | 3 | const axios = require("axios"); 4 | 5 | module.exports = async function getOverview(symbol) { 6 | let data = await axios 7 | .get( 8 | `https://www.alphavantage.co/query?function=OVERVIEW&symbol=${symbol}&apikey=${process.env.ALPHA_VANTAGE_KEY}` 9 | ) 10 | .then((resp) => ({ 11 | Symbol: resp.data.Symbol, 12 | AssetType: resp.data.AssetType, 13 | Name: resp.data.Name, 14 | Exchange: resp.data.Exchange, 15 | Currency: resp.data.Currency, 16 | Country: resp.data.Country, 17 | weeksHigh: resp.data["52WeekHigh"], 18 | weeksLow: resp.data["52WeekLow"], 19 | Desc: resp.data.Description, 20 | Sector: resp.data.Sector, 21 | MarketCap: new Intl.NumberFormat("en-US", { 22 | style: "currency", 23 | currency: "USD", 24 | }).format(resp.data.MarketCapitalization), 25 | EBITDA: new Intl.NumberFormat("en-US", { 26 | style: "currency", 27 | currency: "USD", 28 | }).format(resp.data.EBITDA), 29 | PERatio: resp.data.PERatio, 30 | PriceToBookRatio: resp.data.PriceToBookRatio, 31 | EPS: resp.data.EPS, 32 | DividendYield: resp.data.DividendYield, 33 | BookValue: resp.data.BookValue, 34 | ProfitMargin: resp.data.ProfitMargin, 35 | RevenueTTM: resp.data.RevenueTTM, 36 | })) 37 | .catch((err) => { 38 | console.log(err); 39 | res.status(500).render("error/500"); 40 | }); 41 | 42 | return data; 43 | }; 44 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | // User Model (Google OAuth) 2 | 3 | const mongoose = require("mongoose"); 4 | var Float = require("mongoose-float").loadType(mongoose); 5 | 6 | const UserSchema = new mongoose.Schema({ 7 | googleId: { 8 | type: String, 9 | required: true, 10 | }, 11 | displayName: { 12 | type: String, 13 | required: true, 14 | }, 15 | firstName: { 16 | type: String, 17 | required: true, 18 | }, 19 | lastName: { 20 | type: String, 21 | required: true, 22 | }, 23 | email: { 24 | type: String, 25 | required: true, 26 | }, 27 | password: { 28 | type: String, 29 | required: true, 30 | }, 31 | image: { 32 | type: String, 33 | }, 34 | balance: { 35 | type: Float, 36 | required: true, 37 | }, 38 | stock: [ 39 | { 40 | companySymbol: { 41 | type: String, 42 | required: true, 43 | }, 44 | stockPrice: { 45 | type: Float, 46 | required: true, 47 | }, 48 | noOfStock: { 49 | type: Number, 50 | required: true, 51 | }, 52 | totalAmount: { 53 | type: Float, 54 | required: true, 55 | }, 56 | createdAt: { 57 | type: Date, 58 | default: Date.now, 59 | }, 60 | }, 61 | ], 62 | favStock: [ 63 | { 64 | companySymbol: { 65 | type: String, 66 | required: true, 67 | }, 68 | createdAt: { 69 | type: Date, 70 | default: Date.now, 71 | }, 72 | }, 73 | ], 74 | createdAt: { 75 | type: Date, 76 | default: Date.now, 77 | }, 78 | }); 79 | 80 | module.exports = mongoose.model("User", UserSchema); 81 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | // Passport Google OAuth2 Strategy 2 | 3 | const GoogleStrategy = require("passport-google-oauth20").Strategy; 4 | const mongoose = require("mongoose"); 5 | const User = require("../models/User"); 6 | 7 | module.exports = function (passport) { 8 | passport.use( 9 | new GoogleStrategy( 10 | { 11 | clientID: process.env.GOOGLE_CLIENT_ID, 12 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 13 | callbackURL: "/auth/google/callback", 14 | }, 15 | async (accessToken, refreshToken, profile, done) => { 16 | console.log(profile.emails[0].value); 17 | const newUser = { 18 | googleId: profile.id, 19 | displayName: profile.displayName, 20 | firstName: profile.name.givenName, 21 | lastName: profile.name.familyName, 22 | email: profile.emails[0].value, 23 | password: 24 | process.env.OAUTH_PASS || "google-oauth-MOGicvVFYPzk9O7Y1vAo", 25 | image: profile.photos[0].value, 26 | balance: 10000, 27 | }; 28 | 29 | try { 30 | let user = await User.findOne({ googleId: profile.id }); 31 | 32 | if (user) { 33 | done(null, user); 34 | } else { 35 | user = await User.create(newUser); 36 | done(null, user); 37 | } 38 | } catch (err) { 39 | console.error(err); 40 | } 41 | } 42 | ) 43 | ); 44 | 45 | passport.serializeUser((user, done) => { 46 | done(null, user.id); 47 | }); 48 | 49 | passport.deserializeUser((id, done) => { 50 | User.findById(id, (err, user) => done(err, user)); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /config/data-compact.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Company Name": "Apple Inc.", 4 | "Symbol": "AAPL" 5 | }, 6 | { 7 | "Company Name": "Adobe Systems, Inc.", 8 | "Symbol": "ADBE" 9 | }, 10 | { 11 | "Company Name": "Advance Micro Devices", 12 | "Symbol": "AMD" 13 | }, 14 | { 15 | "Company Name": "Amazon.com, Inc.", 16 | "Symbol": "AMZN" 17 | }, 18 | { 19 | "Company Name": "Alibaba Group, Inc.", 20 | "Symbol": "BABA" 21 | }, 22 | { 23 | "Company Name": "Cisco Systems, Inc.", 24 | "Symbol": "CSCO" 25 | }, 26 | { 27 | "Company Name": "Facebook, Inc.", 28 | "Symbol": "FB" 29 | }, 30 | { 31 | "Company Name": "Google, Inc.", 32 | "Symbol": "GOOGL" 33 | }, 34 | { 35 | "Company Name": "Intel Corporation", 36 | "Symbol": "INTC" 37 | }, 38 | { 39 | "Company Name": "Intuit, Inc.", 40 | "Symbol": "INTU" 41 | }, 42 | { 43 | "Company Name": "iRobot Corporation", 44 | "Symbol": "IRBT" 45 | }, 46 | { 47 | "Company Name": "Mastercard, Inc.", 48 | "Symbol": "MA" 49 | }, 50 | { 51 | "Company Name": "Microsoft Corporation", 52 | "Symbol": "MSFT" 53 | }, 54 | { 55 | "Company Name": "Netflix, Inc.", 56 | "Symbol": "NFLX" 57 | }, 58 | { 59 | "Company Name": "NVIDIA Corporation", 60 | "Symbol": "NVDA" 61 | }, 62 | { 63 | "Company Name": "Qualcomm, Inc.", 64 | "Symbol": "QCOM" 65 | }, 66 | { 67 | "Company Name": "Tesla Motors, Inc.", 68 | "Symbol": "TSLA" 69 | }, 70 | { 71 | "Company Name": "Visa, Inc.", 72 | "Symbol": "V" 73 | }, 74 | { 75 | "Company Name": "Walmart, Inc.", 76 | "Symbol": "WMT" 77 | }, 78 | { 79 | "Company Name": "Zoom Video, Inc.", 80 | "Symbol": "ZM" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /routes/api/edit.js: -------------------------------------------------------------------------------- 1 | // Edit Profile Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | 8 | const { ensureAuth } = require("../../middleware/auth"); 9 | 10 | const User = require("../../models/User.js"); 11 | 12 | // @desc Show edit page 13 | // @route GET /edit 14 | // @access Private 15 | router.get("/", ensureAuth, (req, res) => { 16 | let avatar = req.user.image; 17 | 18 | let displayName = req.user.displayName; 19 | let firstName = req.user.firstName; 20 | let lastName = req.user.lastName; 21 | let email = req.user.email; 22 | 23 | res.status(200).render("edit", { 24 | layout: "layouts/app", 25 | avatar, 26 | displayName, 27 | firstName, 28 | lastName, 29 | email, 30 | href: "/edit", 31 | }); 32 | }); 33 | 34 | // @desc Edit page 35 | // @route PUT /edit 36 | // @access Private 37 | router.put("/", ensureAuth, async (req, res) => { 38 | try { 39 | let curruser1 = await User.findById(req.user._id).lean(); 40 | 41 | curruser1.displayName = req.body.displayName; 42 | curruser1.firstName = req.body.firstName; 43 | curruser1.lastName = req.body.lastName; 44 | curruser1.email = req.body.email; 45 | 46 | curruser1 = await User.findOneAndUpdate( 47 | { _id: curruser1._id }, 48 | { 49 | firstName: curruser1.firstName, 50 | lastName: curruser1.lastName, 51 | email: curruser1.email, 52 | displayName: curruser1.displayName, 53 | }, 54 | { 55 | new: true, // it will create a new one, if it doesn't exist 56 | runValidators: true, // it check weather the fields are valid or not 57 | } 58 | ); 59 | 60 | res.redirect("/portfolio"); 61 | } catch (err) { 62 | console.error(err); 63 | return res.render("error/500"); 64 | } 65 | }); 66 | 67 | module.exports = router; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradebyte", 3 | "version": "1.0.0", 4 | "description": "Stocks Trading Simulation WebApp", 5 | "main": "app.js", 6 | "scripts": { 7 | "build:css": "postcss public/css/tailwind.css -o public/css/style.css", 8 | "start": "concurrently \"cross-env NODE_ENV=production node app\" \"npm:build:css\"", 9 | "dev": "concurrently \"cross-env NODE_ENV=development nodemon app\" \"npm:build:css\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/iampavangandhi/TradeByte.git" 14 | }, 15 | "keywords": [ 16 | "nodejs", 17 | "express", 18 | "mongodb", 19 | "stocks", 20 | "trading" 21 | ], 22 | "author": "Pavan Gandhi", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/iampavangandhi/TradeByte/issues" 26 | }, 27 | "homepage": "https://github.com/iampavangandhi/TradeByte#readme", 28 | "dependencies": { 29 | "alphavantage": "^2.1.0", 30 | "autoprefixer": "^9.8.6", 31 | "axios": "^0.21.2", 32 | "bcryptjs": "^2.4.3", 33 | "concurrently": "^5.3.0", 34 | "connect-flash": "^0.1.1", 35 | "connect-mongo": "^3.2.0", 36 | "cookie-parser": "^1.4.5", 37 | "cross-env": "^7.0.2", 38 | "date-fns": "^2.15.0", 39 | "dotenv": "^8.2.0", 40 | "ejs": "^3.1.7", 41 | "express": "^4.17.1", 42 | "express-ejs-layouts": "^2.5.0", 43 | "express-session": "^1.17.1", 44 | "express-validator": "^6.6.0", 45 | "googleapis": "^39.2.0", 46 | "method-override": "^3.0.0", 47 | "mongoose": "^5.9.25", 48 | "mongoose-float": "^1.0.4", 49 | "morgan": "^1.10.0", 50 | "nodemailer": "^6.4.16", 51 | "npm": "^6.14.7", 52 | "passport": "^0.4.1", 53 | "passport-google-oauth20": "^2.0.0", 54 | "passport-local": "^1.0.0", 55 | "postcss-cli": "^7.1.1", 56 | "stripe": "^8.89.0", 57 | "tailwindcss": "^1.6.2", 58 | "uuid": "^8.3.0" 59 | }, 60 | "devDependencies": { 61 | "cssnano": "^4.1.10", 62 | "nodemon": "^2.0.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /views/error/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TradeByte 7 | 13 | 17 | 18 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |

30 | 4 4 31 |

32 |

33 | PAGE NOT FOUND 34 |

35 |

36 | The page/stock you are looking for was moved or doesn't exist 37 |

38 |
39 |
40 |

Let's get back

41 |
42 |
43 | 49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac File 2 | .DS_Store 3 | */.DS_Store 4 | ._.DS_Store 5 | **/.DS_Store 6 | **/._.DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | .vscode/ 51 | 52 | # Config file 53 | config/config.env 54 | 55 | # TypeScript v1 declaration files 56 | typings/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variables file 83 | .env 84 | .env.test 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | 89 | # Next.js build output 90 | .next 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # Serverless directories 106 | .serverless/ 107 | 108 | # FuseBox cache 109 | .fusebox/ 110 | 111 | # DynamoDB Local files 112 | .dynamodb/ 113 | 114 | # TernJS port file 115 | .tern-port 116 | -------------------------------------------------------------------------------- /routes/api/addBalance.js: -------------------------------------------------------------------------------- 1 | // Add Balance Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const stripe = require("stripe")(process.env.SK_TEST); 8 | 9 | const { ensureAuth, ensureGuest } = require("../../middleware/auth"); 10 | 11 | const User = require("../../models/User"); 12 | const Transaction = require("../../models/Transaction"); 13 | 14 | // @desc Add Balance 15 | // @route GET /addBalance 16 | // @access Private 17 | router.get("/", ensureAuth, (req, res) => { 18 | let user = req.user; 19 | let avatar = req.user.image; 20 | res.status(200).render("addBalance", { 21 | layout: "layouts/app", 22 | avatar, 23 | user, 24 | publishableKey: process.env.PK_TEST, 25 | href: "/addBalance", 26 | }); 27 | }); 28 | 29 | // @desc Add Balance 30 | // @route POST /addBalance 31 | // @access Private 32 | router.post("/", ensureAuth, async (req, res) => { 33 | let amount = Number(req.body.addAmount); // type cast amount to number as body parser take it as string 34 | 35 | let finalAmount = amount + req.user.balance; 36 | 37 | const { stripeToken } = req.body; 38 | 39 | try { 40 | // Stripe Payment 41 | await stripe.charges.create( 42 | { 43 | amount: req.user.balance * 100, 44 | currency: "usd", 45 | source: stripeToken, 46 | description: req.user.email, 47 | }, 48 | (err) => { 49 | if (err && err.type === "StripeCardError") { 50 | return res.render("error/500"); 51 | } else { 52 | console.log("Payment Success"); 53 | } 54 | } 55 | ); 56 | 57 | // Updating balance to user's schema. 58 | req.body.user = req.user.id; 59 | await User.findOneAndUpdate( 60 | { _id: req.user.id }, 61 | { balance: finalAmount }, 62 | { 63 | new: true, // it will create a new one, if it doesn't exist 64 | runValidators: true, // it check weather the fields are valid or not 65 | } 66 | ); 67 | 68 | // Adding new transaction details on Transaction Schema. 69 | const transactionDetails = "Balance Added to Wallet"; 70 | const transactionOperation = "Deposit"; 71 | const transactionUser = req.user.id; 72 | await Transaction.create({ 73 | details: transactionDetails, 74 | amount: amount, 75 | operation: transactionOperation, 76 | user: transactionUser, 77 | }); 78 | 79 | res.redirect("/done"); 80 | } catch (err) { 81 | console.error(err); 82 | res.render("error/500"); 83 | } 84 | }); 85 | 86 | module.exports = router; 87 | -------------------------------------------------------------------------------- /views/error/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TradeByte 7 | 13 | 17 | 18 | 23 | 24 | 49 | 50 | 51 | 52 |
53 |
54 |
57 |

ERROR 500

58 |

59 | Unexpected error :( 60 |

61 | 62 |

63 | There seems to be a problem 64 |

65 |

Let's get to back

66 |
67 |
68 |
69 | 75 |
76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | 89 | -------------------------------------------------------------------------------- /public/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | /* Custom CSS */ 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | min-height: 100vh; 10 | min-width: 100vw; 11 | overflow-x: hidden; 12 | box-sizing: border-box; 13 | background: linear-gradient(122.51deg, #e6ecfc 30%, #7fb4f1 100%) !important; 14 | scroll-behavior: smooth; 15 | } 16 | 17 | @tailwind components; 18 | 19 | .chartjs-container { 20 | position: relative; 21 | margin: auto; 22 | height: 75vh; 23 | width: 75vw; 24 | } 25 | 26 | /* Remove number input arrow buttons on transaction page */ 27 | input::-webkit-outer-spin-button.without-arrow, 28 | input::-webkit-inner-spin-button.without-arrow { 29 | -webkit-appearance: none; 30 | margin: 0; 31 | } 32 | 33 | /* Firefox */ 34 | input[type="number"].without-arrow { 35 | -moz-appearance: textfield; 36 | } 37 | 38 | .symbolicon { 39 | font-size: 72px; 40 | background: -webkit-linear-gradient(#eee, #333); 41 | background-clip: text; 42 | -webkit-background-clip: text; 43 | -webkit-text-fill-color: transparent; 44 | } 45 | 46 | /* Edit icon */ 47 | .icon .badge { 48 | position: absolute; 49 | top: -5px; 50 | right: -5px; 51 | padding: 5px 2px; 52 | border-radius: 50%; 53 | background-color: #0072ff; 54 | color: white; 55 | } 56 | 57 | /* Search bar */ 58 | #searchbar:focus .search-list { 59 | display: block; 60 | } 61 | 62 | /* Login Page animations */ 63 | animated { 64 | -webkit-animation-duration: 1s; 65 | animation-duration: 1s; 66 | -webkit-animation-fill-mode: both; 67 | animation-fill-mode: both; 68 | } 69 | 70 | .animated.faster { 71 | -webkit-animation-duration: 500ms; 72 | animation-duration: 500ms; 73 | } 74 | 75 | .fadeIn { 76 | -webkit-animation-name: fadeIn; 77 | animation-name: fadeIn; 78 | } 79 | 80 | .fadeOut { 81 | -webkit-animation-name: fadeOut; 82 | animation-name: fadeOut; 83 | } 84 | 85 | @keyframes fadeIn { 86 | from { 87 | opacity: 0; 88 | } 89 | 90 | to { 91 | opacity: 1; 92 | } 93 | } 94 | 95 | @keyframes fadeOut { 96 | from { 97 | opacity: 1; 98 | } 99 | 100 | to { 101 | opacity: 0; 102 | } 103 | } 104 | 105 | .serif { 106 | font-family: "Zilla Slab", serif; 107 | } 108 | 109 | [x-cloak] { 110 | display: none; 111 | } 112 | 113 | .grid-indigo { 114 | background-image: radial-gradient(#5a67d8 2px, transparent 2px); 115 | background-size: 16px 16px; 116 | } 117 | 118 | /* Done page */ 119 | .share > .container { 120 | opacity: 0; 121 | visibility: hidden; 122 | transition: 0.4s all ease-in-out; 123 | transform: translateY(50%); 124 | } 125 | 126 | /* Share button */ 127 | .share.showAddThisButtons > .container { 128 | opacity: 1; 129 | visibility: visible; 130 | transform: translateY(40%); 131 | margin-top: 0.5rem; 132 | } 133 | 134 | @tailwind utilities; 135 | -------------------------------------------------------------------------------- /routes/api/view.js: -------------------------------------------------------------------------------- 1 | // View Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | 8 | const alpha = require("alphavantage")({ 9 | key: process.env.ALPHA_VANTAGE_KEY, 10 | }); 11 | 12 | const getPrice = require("../../helpers/getPrice"); 13 | const getOverview = require("../../helpers/getOverview"); 14 | const { ensureAuth } = require("../../middleware/auth"); 15 | 16 | // @desc View Page 17 | // @route GET /view/:symbol 18 | // @access Private 19 | router.get("/:symbol", ensureAuth, async (req, res) => { 20 | const symbol = req.params.symbol; 21 | let data = await getOverview(symbol); 22 | let { latestPrice, low, high } = await getPrice(symbol); 23 | let AssetType = data["AssetType"]; 24 | let assetName = data["Name"]; 25 | let assetExchange = data["Exchange"]; 26 | let Currency = data["Currency"]; 27 | let Country = data["Country"]; 28 | let Sector = data["Sector"]; 29 | let MarketCap = data["MarketCap"]; 30 | let Ebitda = data["EBITDA"]; 31 | let PERatio = data["PERatio"]; 32 | let PriceToBookRatio = data["PriceToBookRatio"]; 33 | let EPS = data["EPS"]; 34 | let DividendYield = data["DividendYield"]; 35 | let BookValue = data["BookValue"]; 36 | let ProfitMargin = data["ProfitMargin"]; 37 | let RevenueTTM = data["RevenueTTM"]; 38 | let Desc = data["Desc"]; 39 | let weeksLow = data["weeksLow"]; 40 | let weeksHigh = data["weeksHigh"]; 41 | 42 | alpha.data 43 | .intraday(symbol, "compact", "json", "60min") 44 | .then((data) => { 45 | const intraDay = data["Time Series (60min)"]; 46 | 47 | let dates = []; 48 | let opening = []; 49 | let closing = []; 50 | let highs = []; 51 | let lows = []; 52 | let volumes = []; 53 | const keys = Object.getOwnPropertyNames(intraDay); 54 | 55 | for (let i = 0; i < 40; i++) { 56 | dates.push(keys[i]); 57 | opening.push(intraDay[keys[i]]["1. open"]); 58 | highs.push(intraDay[keys[i]]["2. high"]); 59 | lows.push(intraDay[keys[i]]["3. low"]); 60 | closing.push(intraDay[keys[i]]["4. close"]); 61 | volumes.push(intraDay[keys[i]]["5. volume"]); 62 | } 63 | 64 | // reverse so dates appear from left to right 65 | dates.reverse(); 66 | closing.reverse(); 67 | 68 | res.status(200).render("view", { 69 | layout: "layouts/app", 70 | href: "/market", 71 | avatar: req.user.image, 72 | symbol, 73 | data, 74 | dates, 75 | opening, 76 | closing, 77 | highs, 78 | lows, 79 | volumes, 80 | AssetType, 81 | assetName, 82 | assetExchange, 83 | Currency, 84 | Country, 85 | Sector, 86 | MarketCap, 87 | Ebitda, 88 | PERatio, 89 | PriceToBookRatio, 90 | EPS, 91 | DividendYield, 92 | BookValue, 93 | ProfitMargin, 94 | RevenueTTM, 95 | Desc, 96 | latestPrice, 97 | high, 98 | low, 99 | weeksLow, 100 | weeksHigh, 101 | }); 102 | }) 103 | .catch((err) => { 104 | console.error(err); 105 | res.render("error/404"); 106 | }); 107 | }); 108 | 109 | module.exports = router; 110 | -------------------------------------------------------------------------------- /routes/api/user.js: -------------------------------------------------------------------------------- 1 | // User Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const passport = require("passport"); 8 | const bcrypt = require("bcryptjs"); 9 | const { v4: uuidv4 } = require("uuid"); 10 | const { ensureGuest } = require("../../middleware/auth"); 11 | 12 | // Load User Model 13 | const User = require("../../models/User"); 14 | 15 | // @desc Sign Up Page 16 | // @route GET /user/signup 17 | // @access Public 18 | router.get("/signup", ensureGuest, (req, res) => { 19 | res.status(200).render("signup", { layout: "layouts/login" }); 20 | }); 21 | 22 | // @desc Submit Sign Up Form 23 | // @route GET /user/signup 24 | // @access Public 25 | router.post("/signup", ensureGuest, (req, res) => { 26 | const { firstName, lastName, password1, password2, email } = req.body; 27 | let errors = []; 28 | 29 | if (!firstName || !lastName || !password1 || !password2 || !email) { 30 | errors.push({ msg: "Please enter all fields" }); 31 | } 32 | if (password1 !== password2) { 33 | errors.push({ msg: "Passwords do not match" }); 34 | } 35 | if (password1.length < 6) { 36 | errors.push({ msg: "Password must be longer than 6 characters" }); 37 | } 38 | 39 | if (errors.length > 0) { 40 | res.render("signup", { 41 | layout: "layouts/login", 42 | errors, 43 | firstName, 44 | email, 45 | lastName, 46 | password1, 47 | password2, 48 | }); 49 | } else { 50 | User.findOne({ email: email }).then((user) => { 51 | if (user) { 52 | errors.push({ msg: "Email already exists" }); 53 | res.render("signup", { 54 | layout: "layouts/login", 55 | errors, 56 | firstName, 57 | lastName, 58 | password1, 59 | password2, 60 | }); 61 | } else { 62 | const newUser = new User({ 63 | googleId: uuidv4(), 64 | displayName: `${firstName} ${lastName}`, 65 | firstName, 66 | lastName, 67 | email, 68 | image: 69 | "https://t3.ftcdn.net/jpg/00/64/67/52/240_F_64675209_7ve2XQANuzuHjMZXP3aIYIpsDKEbF5dD.jpg", 70 | password: password1, 71 | balance: 10000, 72 | }); 73 | 74 | bcrypt.genSalt(10, (err, salt) => { 75 | bcrypt.hash(newUser.password, salt, (err, hash) => { 76 | if (err) throw err; 77 | newUser.password = hash; 78 | newUser 79 | .save() 80 | .then((user) => { 81 | req.flash( 82 | "success_msg", 83 | "You are now registered and can log in" 84 | ); 85 | res.status(200).redirect("/"); 86 | }) 87 | .catch((err) => console.log(err)); 88 | }); 89 | }); 90 | } 91 | }); 92 | } 93 | }); 94 | 95 | // @desc Submit Sign In Form 96 | // @route GET /user/signin 97 | // @access Public 98 | router.post("/signin", ensureGuest, (req, res, next) => { 99 | passport.authenticate("local", { 100 | successRedirect: "/portfolio", 101 | failureRedirect: "/", 102 | failureFlash: true, 103 | })(req, res, next); 104 | }); 105 | 106 | module.exports = router; 107 | -------------------------------------------------------------------------------- /views/history.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Transaction History 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
19 | <% if(message === '') { %> 20 | 23 | 24 | 25 | 30 | 35 | 40 | 45 | 46 | 47 | <% transactions.forEach( (stock) => { %> 48 | 49 | 54 | 69 | 74 | 77 | 78 | <% }) %> 79 |
28 | Date 29 | 33 | Operation 34 | 38 | Amount 39 | 43 | Details 44 |
52 | <%= stock.createdAt %> 53 | 55 | <% if(stock.operation == "Debited") {%> 56 |

59 | <%= stock.operation %> 60 |

61 | <%} else {%> 62 |

65 | <%= stock.operation %> 66 |

67 | <%}%> 68 |
72 | <%= stock.amount %> 73 | 75 | <%= stock.details %> 76 |
80 | <% } else { %> 81 |

<%= message %>

82 | <% } %> 83 |
84 |
85 |
86 |
87 |
88 | 89 | <%- include('./partials/_footer.ejs') %> 90 |
91 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Welcome to TradeByte 2 | 3 | //jshint esversion: 8 4 | 5 | // Includes 6 | const path = require("path"); 7 | const express = require("express"); 8 | const dotenv = require("dotenv"); 9 | const morgan = require("morgan"); 10 | const methodOverride = require("method-override"); 11 | const expressLayouts = require("express-ejs-layouts"); 12 | 13 | // Auth and DB Includes 14 | const passport = require("passport"); 15 | const session = require("express-session"); 16 | const mongoose = require("mongoose"); 17 | const MongoStore = require("connect-mongo")(session); 18 | const connectDB = require("./config/db"); 19 | const flash = require("connect-flash"); 20 | var cookieParser = require("cookie-parser"); 21 | 22 | // Load config 23 | dotenv.config({ path: "./config/config.env" }); 24 | 25 | // Passport config 26 | require("./config/passport")(passport); 27 | require("./config/passportLocal")(passport); 28 | 29 | // DB Connected 30 | connectDB(); 31 | 32 | // App init 33 | const app = express(); 34 | 35 | // Cache Fix 36 | app.disable("etag"); 37 | 38 | // Body parser 39 | app.use(express.urlencoded({ extended: false })); 40 | app.use(express.json()); 41 | 42 | // Method override 43 | app.use( 44 | methodOverride(function (req, res) { 45 | if (req.body && typeof req.body === "object" && "_method" in req.body) { 46 | let method = req.body._method; 47 | delete req.body._method; 48 | return method; 49 | } 50 | }) 51 | ); 52 | 53 | // Logging 54 | if (process.env.NODE_ENV === "development") { 55 | app.use(morgan("dev")); 56 | } 57 | 58 | // EJS 59 | app.set("view engine", "ejs"); 60 | app.set("views", __dirname + "/views"); 61 | 62 | // Express layouts 63 | app.set("layout", "layouts/login", "layouts/app"); 64 | app.use(expressLayouts); 65 | 66 | // Sessions 67 | app.use( 68 | session({ 69 | secret: "keyboard cat", 70 | resave: false, 71 | saveUninitialized: false, 72 | store: new MongoStore({ mongooseConnection: mongoose.connection }), 73 | }) 74 | ); 75 | 76 | // cookie middleware 77 | app.use(cookieParser()); 78 | 79 | // Passport middleware 80 | app.use(passport.initialize()); 81 | app.use(passport.session()); 82 | 83 | // Connect Flash 84 | app.use(flash()); 85 | 86 | // Set Global variables 87 | app.use(function (req, res, next) { 88 | res.locals.user = req.user || null; 89 | res.locals.success_msg = req.flash("success_msg"); 90 | res.locals.error_msg = req.flash("error_msg"); 91 | res.locals.error = req.flash("error"); 92 | next(); 93 | }); 94 | 95 | // Static folder 96 | app.use(express.static(path.join(__dirname, "/public"))); 97 | 98 | // Routes 99 | app.use("/", require("./routes/api/index")); 100 | app.use("/auth", require("./routes/api/auth")); 101 | app.use("/user", require("./routes/api/user")); 102 | app.use("/portfolio", require("./routes/api/portfolio")); 103 | app.use("/market", require("./routes/api/market")); 104 | app.use("/view", require("./routes/api/view")); 105 | app.use("/cart", require("./routes/api/cart")); 106 | app.use("/done", require("./routes/api/done")); 107 | app.use("/addBalance", require("./routes/api/addBalance")); 108 | app.use("/transaction", require("./routes/api/transaction")); 109 | app.use("/search", require("./routes/api/search")); 110 | app.use("/edit", require("./routes/api/edit")); 111 | 112 | // Port: Love You 3000 113 | const PORT = process.env.PORT || 3000; 114 | 115 | // Server Listening 116 | app.listen( 117 | PORT, 118 | console.log( 119 | `TradeByte is running in ${process.env.NODE_ENV} mode on port ${PORT}` 120 | ) 121 | ); 122 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at pavangandhi100@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /routes/api/portfolio.js: -------------------------------------------------------------------------------- 1 | // Portfolio Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureAuth, ensureGuest } = require("../../middleware/auth"); 8 | 9 | const totalData = require("../../config/data-total.json"); 10 | 11 | // import User and Transaction models 12 | const User = require("../../models/User"); 13 | const Transaction = require("../../models/Transaction"); 14 | 15 | // @desc Portfolio page 16 | // @route GET /portfolio 17 | // @access Private 18 | router.get("/", ensureAuth, async (req, res) => { 19 | let avatar = req.user.image; 20 | const user = req.user; 21 | // See if prevUser cookie exists and the new user have balance of 10000 (amount for the newly created account) 22 | if (req.cookies.prevUser !== "" && req.user.balance === 10000) { 23 | try { 24 | // Get the balance of the user who shared link 25 | const { balance: prevUserBalance } = await User.findOne({ 26 | _id: req.cookies.prevUser, 27 | }); 28 | 29 | // Update the balance of user who shared the share link 30 | await User.findOneAndUpdate( 31 | { _id: req.cookies.prevUser }, 32 | { balance: prevUserBalance + 100 }, // updating existing balance 33 | { 34 | new: true, // it will create a new one, if it doesn't exist 35 | runValidators: true, // it check weather the fields are valid or not 36 | } 37 | ); 38 | 39 | // Update the balance of user who used the share link 40 | await User.findOneAndUpdate( 41 | { _id: req.user.id }, 42 | { balance: req.user.balance + 50 }, 43 | { 44 | new: true, // it will create a new one, if it doesn't exist 45 | runValidators: true, // it check weather the fields are valid or not 46 | } 47 | ); 48 | 49 | // Adding new transaction details on Transaction Schema for user who signed up using the share link 50 | const transactionDetails = 51 | "50$ Balance Added to Wallet from the share link"; 52 | const transactionOperation = "Debited"; 53 | const transactionUser = req.user.id; 54 | await Transaction.create({ 55 | details: transactionDetails, 56 | amount: 50, 57 | operation: transactionOperation, 58 | user: transactionUser, 59 | }); 60 | 61 | // Adding new transaction details on Transaction Schema for the user who shared the link 62 | await Transaction.create({ 63 | details: "100$ Balance Added to Wallet from the shared link", 64 | amount: 100, 65 | operation: transactionOperation, 66 | user: req.cookies.prevUser, 67 | }); 68 | 69 | // set prevUser cookie to empty string after one user signed up using it 70 | res.cookie("prevUser", ""); 71 | } catch (err) { 72 | console.error(err); 73 | } 74 | } 75 | 76 | // If a cookie exists but the user has balance more than 10000 which means he already have an account, set the cookie value to empty 77 | if (req.cookies.prevUser !== "" && req.user.balance !== 10000) { 78 | res.cookie("prevUser", ""); 79 | } 80 | 81 | // Stock Message 82 | var StockMessage = ""; 83 | stocks = user.stock; 84 | 85 | if (Object.keys(stocks).length == 0) { 86 | StockMessage = "No Stock"; 87 | } else { 88 | StockMessage = ""; 89 | } 90 | 91 | // Total Portfolio 92 | totalPortfolio = 0; 93 | 94 | stocks.forEach((stock) => { 95 | totalPortfolio = totalPortfolio + stock.totalAmount; 96 | }); 97 | 98 | // Render 99 | res.render("portfolio", { 100 | StockMessage, 101 | totalPortfolio, 102 | user, 103 | avatar, 104 | totalData, 105 | layout: "layouts/app", 106 | href: "/portfolio", 107 | }); 108 | }); 109 | 110 | module.exports = router; 111 | -------------------------------------------------------------------------------- /views/edit.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | Edit Profile 10 |
11 | 12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 | 26 |
27 |
28 | 36 |
37 |
38 |
39 |
40 | 46 |
47 |
48 | 55 |
56 |
57 |
58 |
59 | 65 |
66 |
67 | 75 |
76 |
77 |
78 |
79 | 85 |
86 |
87 | 95 |
96 |
97 | 98 |
99 | 105 | 106 | 107 | 114 | 115 |
116 |
117 |
118 |
119 | -------------------------------------------------------------------------------- /views/done.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tradebyte 8 | 14 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
27 |
28 |

31 | Done 32 |

33 |
34 | 35 |
36 |
37 |

38 | Thank you for using TradeByte 39 |

40 |
41 |
42 | 48 | 49 | 64 |
65 |
66 |
67 |
68 | 69 | 70 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /views/addBalance.ejs: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |

9 | Add Balance 10 |

11 |
12 | 13 |
14 | 15 |
16 |

Your Balance: <%= user.balance%>

17 |
22 |
23 | 26 | 38 | 39 | 40 | 49 |
50 |
51 | 52 |
53 |
54 |
55 |
VISA
56 |

59 | 4242 4242 4242 4242 60 |

61 |
62 |
63 | MM/YY : 01/30 67 | CVC : 123 71 |
72 |
73 |
74 |
75 |
VISA PREPAID
76 |

79 | 5555 5555 5555 4444 80 |

81 |
82 |
83 | MM/YY : 12/40 87 | CVC : 555 91 |
92 |
93 |
94 |
95 |
MASTERCARD
96 |

99 | 5105 1051 0510 5100 100 |

101 |
102 |
103 | MM/YY : 11/31 107 | CVC : 005 111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 | 119 | 131 | -------------------------------------------------------------------------------- /views/cart.ejs: -------------------------------------------------------------------------------- 1 |
2 |
5 |
6 |

7 | Buy Stock : 15 |

16 |
17 |
20 |
23 | Company Name 24 |
25 |
28 |
29 | 34 |

37 | <%= companyName %> 38 |

39 |
40 |
41 | 46 | 57 |
60 | Qty 61 |
62 | 67 |
70 |
71 |
72 | 81 |
82 |
83 |
84 |
87 | Total Price 88 |
89 |
92 |

96 | <%= latestPrice %> 97 | $ 98 |

99 |
100 |
101 |
102 | Back to Market 107 | 113 |
114 |
115 |
116 |
117 | 127 | -------------------------------------------------------------------------------- /views/transaction.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | Transaction 10 |
11 | 12 |
13 |
14 | 15 |
16 |
19 |
20 | <% if(message === "Insufficent Balance") { %> 21 |
22 |

23 | Please Add $<%= ExtraBalance %> or 24 | more to complete the Transaction. 25 |

26 | 31 |
32 |
33 | <% } else { %> 34 |

35 | <%= message %> 36 |

37 |
38 | 39 | 40 | 41 | 42 | 45 | 54 | 55 | 56 | 57 | 67 | 68 | 69 | 72 | 80 | 81 | 82 | 83 | 93 | 94 | 95 |
43 | Company Symbol: 44 | 46 | 53 |
Stock Price: 58 | 66 |
70 | Number of Stock: 71 | 73 | <%= "without-arrow" %> <% } %>" <% if(method == 77 | "confirm") { %> <%= "readonly" %> <% } %> type="number" 78 | name="noOfStock" value="<%= data.noOfStock%>" /> 79 |
Total Amount: 84 | 92 |
96 |
97 | 102 | 110 |
111 |
112 | <% } %> 113 |
114 |
115 | 116 | 117 | <%- include('./partials/_footer.ejs') %> 118 |
119 | 145 | -------------------------------------------------------------------------------- /views/partials/_messages.ejs: -------------------------------------------------------------------------------- 1 | <% if(typeof errors != "undefined") { %> <% errors.forEach(function(error) { %> 2 |
5 |
6 |
7 | 19 | 22 | 23 | 24 | 25 |
26 |

<%= error.msg %>

27 |
28 |
29 | 41 | 42 | 43 | 44 |
45 |
46 | <% }); %> <% } %> <% if(success_msg != "") { %> 47 |
50 |
51 |
52 | 64 | 67 | 68 | 69 | 70 |
71 |

<%= success_msg %>

72 |
73 |
74 | 86 | 87 | 88 | 89 |
90 |
91 | <% } %> <% if(error_msg != "") { %> 92 |
95 |
96 |
97 | 109 | 112 | 113 | 114 | 115 |
116 |

<%= error_msg %>

117 |
118 |
119 | 131 | 132 | 133 | 134 |
135 |
136 | <% } %> <% if(error != "") { %> 137 |
140 |
141 |
142 | 154 | 157 | 158 | 159 | 160 |
161 |

<%= error %>

162 |
163 |
164 | 176 | 177 | 178 | 179 |
180 |
181 | <% } %> 182 | -------------------------------------------------------------------------------- /views/view.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

<%= assetName %>

7 |

8 | Sector: 9 | <%= Sector %> 12 |

13 |
14 |
15 |

<%= latestPrice%>

16 |
17 |
18 |

Price Summary

19 |
20 |
21 |
24 |
25 |

26 | Today's High 27 |

28 |

<%= high %>

29 |
30 |
31 |

32 | Today's Low 33 |

34 |

<%= low %>

35 |
36 |
37 |

38 | 52 Week High 39 |

40 |

<%= weeksHigh %>

41 |
42 |
43 |

44 | 52 Week Low 45 |

46 |

<%= weeksLow %>

47 |
48 |
49 |
50 |
51 | 52 |

Overview

53 |
57 | 58 |
59 | 60 |

Company Essentials

61 |
62 |
63 |
66 |
67 |

68 | Market Cap 69 |

70 |

<%= MarketCap %>

71 |
72 |
73 |

74 | EBITDA 75 |

76 |

<%= Ebitda %>

77 |
78 |
79 |

P/E

80 |

<%= PERatio %>

81 |
82 |
83 |

P/B

84 |

85 | <%= PriceToBookRatio %> 86 |

87 |
88 |
89 |

90 | EPS(TTM) 91 |

92 |

<%= EPS %>

93 |
94 |
95 |

96 | Div. Yield 97 |

98 |

99 | <%= DividendYield %> 100 |

101 |
102 |
103 |

104 | Book Value 105 |

106 |

<%= BookValue %>

107 |
108 |
109 |

110 | Revenue(TTM) 111 |

112 |

<%= RevenueTTM %>

113 |
114 |
115 |
116 |
117 |
118 | 119 | 124 | 125 |
126 | 127 | 148 |
149 | -------------------------------------------------------------------------------- /views/signup.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | TradeByte 5 |

6 |
7 |
10 |

Sign Up for  TradeByte

11 | <%- include('./partials/_messages') %> 12 |
13 |
14 |
15 |
16 |
17 | 22 | 31 |
32 |
33 | 38 | 47 |
48 |
49 |
50 | 55 | 64 |
65 |
66 | 71 | 81 |

82 | Password must be 6 to 13 characters long which should contain one 83 | lower case letter, one upper case letter, one digit, and no 84 | spaces. 85 |

86 |
87 |
88 | 93 | 102 | 107 |
108 |
109 | 116 |

117 | Go back to Login 118 |

119 |
120 |
121 |
122 |
123 |
124 |
125 | 126 | 127 | 128 | 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # ⚠️ NOT ACTIVELY MAINTAINED 4 | 5 | --- 6 | 7 | # logo **TradeByte** 8 | 9 | ## Stocks Trading Simulation WebApp 10 | 11 | ![Stars](https://img.shields.io/github/stars/iampavangandhi/TradeByte?color=2948ff&label=Stars&style=flat-square) ![Forks](https://img.shields.io/github/forks/iampavangandhi/TradeByte?color=2948ff&label=Forks&style=flat-square) ![Contributors](https://img.shields.io/github/contributors/iampavangandhi/TradeByte?color=2948ff&label=Contributors&style=flat-square) ![Issues](https://img.shields.io/github/issues/iampavangandhi/TradeByte?color=2948ff&label=Issues&style=flat-square) ![Pull Requests](https://img.shields.io/github/issues-pr/iampavangandhi/TradeByte?color=2948ff&label=Pull%20Requests&style=flat-square) 12 | 13 | ![License](https://img.shields.io/github/license/iampavangandhi/TradeByte?color=2948ff&label=License&style=flat-square) ![Language](https://img.shields.io/github/languages/top/iampavangandhi/TradeByte?color=2948ff&label=Language&style=flat-square) ![Lines of Code](https://img.shields.io/tokei/lines/github/iampavangandhi/TradeByte?color=2948ff&label=Lines%20of%20Code&style=flat-square) ![Version](https://img.shields.io/github/package-json/v/iampavangandhi/TradeByte?color=2948ff&label=Version&style=flat-square) ![Deployment](https://img.shields.io/github/deployments/iampavangandhi/TradeByte/tradebyte?color=2948ff&label=Deployment&style=flat-square) ![Repo Size](https://img.shields.io/github/repo-size/iampavangandhi/TradeByte?color=2948ff&label=Repo%20Size&style=flat-square) 14 | 15 | ## Website : https://tradebyte.up.railway.app 16 | 17 | [![Project Board](https://img.shields.io/badge/-Project%20Board-0e76a8?style=for-the-badge&logo=Github&logoColor=white)](https://github.com/iampavangandhi/TradeByte/projects/1) [![Project Wiki](https://img.shields.io/badge/-Project%20Wiki-283c86?style=for-the-badge&logo=wikipedia&logoColor=white)](https://github.com/iampavangandhi/TradeByte/wiki) [![Project Report](https://img.shields.io/badge/-Project%20Report-3b5998?style=for-the-badge&logo=google-sheets&logoColor=white)](https://docs.google.com/spreadsheets/d/1_vg-F2dk6jWy9jcYlGuKqAnXUX6NwbbnLg9Bmn_Ulrk/edit?usp=sharing) [![TheNodeCourse](https://img.shields.io/badge/-TheNodeCourse-11998e?style=for-the-badge&logo=node.js&logoColor=white)](https://github.com/iampavangandhi/TheNodeCourse) 18 | 19 |
20 | 21 | --- 22 | 23 | ## Table of Contents 24 | 25 | - [**TradeByte**](#img-srcpublicimagestradebyte-faviconpng-width30-tradebyte) 26 | - [Tech Stack](#tech-stack) 27 | - [Config Setup](#config-setup) 28 | - [Build Setup](#build-setup) 29 | - [Folder Structure](#folder-structure) 30 | - [Contributing](#contributing) 31 | - [Contributors](#contributors) 32 | - [Maintainers](#maintainers) 33 | - [API Credits](#api-credits) 34 | - [License](#license) 35 | 36 | ## Tech Stack 37 | 38 | javascript 39 | nodejs 40 | npm 41 | express 42 | mongodb 43 | ejs 44 | tailwind-css 45 | google 46 | gmail 47 | passport 48 | stripe 49 | heroku 50 | 51 | ## Config Setup 52 | 53 | #### Add following keys into the `config.env` file. These all are free to use keys. 54 | 55 | - [MongoDB URI](https://www.mongodb.com/cloud/atlas) 56 | - [Alpha Vantage Keys](https://www.alphavantage.co/) 57 | - [Google OAuth Credentials](console.developers.google.com/) 58 | - [GMAIL API Credentials](https://www.woolha.com/tutorials/node-js-send-email-using-gmail-with-nodemailer-oauth-2) 59 | - [Stripe Payment Gateway Keys](https://stripe.com/) 60 | 61 | #### Make sure to use the same `config.env` format given below. 62 | 63 | ```bash 64 | PORT = XXXX 65 | MONGO_URI = XXXX 66 | ALPHA_VANTAGE_KEY = XXXX 67 | 68 | GOOGLE_CLIENT_ID = XXXX 69 | GOOGLE_CLIENT_SECRET = XXXX 70 | 71 | GMAIL_ADDRESS = XXXX 72 | GMAIL_OAUTH_CLIENT_ID = XXXX 73 | GMAIL_OAUTH_CLIENT_SECRET = XXXX 74 | GMAIL_OAUTH_REFRESH_TOKEN = XXXX 75 | GMAIL_OAUTH_ACCESS_TOKEN = XXXX 76 | GMAIL_OAUTH_TOKEN_EXPIRE = XXXX 77 | 78 | PK_TEST = XXXX 79 | SK_TEST = XXXX 80 | 81 | ``` 82 | 83 | ## Build Setup 84 | 85 | ```bash 86 | # install dependencies 87 | npm install 88 | 89 | # run in production 90 | npm start 91 | 92 | # run in development 93 | npm run dev 94 | ``` 95 | 96 | ## Folder Structure 97 | 98 | . 99 | ├── 📁 .github # Github files (don't change) 100 | ├── 📁 config # Configuration files 101 | ├── 📁 helpers # Helper or Controller files 102 | ├── 📁 middleware # Express middleware 103 | ├── 📁 models # Mongoose models 104 | ├── 📁 project-plan # Project planning files 105 | │ └── 📁 ui-designs 106 | ├── 📁 public # Public folder 107 | │ ├── 📁 css 108 | │ ├── 📁 images 109 | │ └── 📁 javascript 110 | ├── 📁 routes # Express routes 111 | │ └── 📁 api 112 | └── 📁 views # EJS views 113 | ├── 📁 error 114 | ├── 📁 layouts 115 | └── 📁 partials 116 | 117 | ## Project Plan 118 | 119 | ![Project Plan](project-plan/TradeByte.jpg) 120 | 121 | ## Contributing 122 | 123 | Feel free to dive in! [Open an issue](https://github.com/iampavangandhi/TradeByte/issues/new) or submit PRs. 124 | 125 | See the [CONTRIBUTING.md](CONTRIBUTING.md) for details. Kindly follow the [Code Of Conduct](CODE_OF_CONDUCT.md) 126 | 127 | ## Contributors 128 | 129 | This project exists thanks to all the people who contribute. 130 | 131 | [![Contributors](https://readme-contributors.now.sh/iampavangandhi/tradebyte?extension=jpg&width=300&avatarSize=25)](https://github.com/iampavangandhi/TradeByte/graphs/contributors) 132 | 133 | ## Maintainers 134 | 135 | Pavan Gandhi 136 | 137 | #### This repo is maintained by [Pavan Gandhi](https://github.con/iampavangandhi). 138 | 139 | ## API Credits 140 | 141 | alpha-vantage 142 | 143 | ### [Alpha Vantage](https://www.alphavantage.co/) 144 | 145 | ## License 146 | 147 | [MIT](LICENSE) © Pavan Gandhi 148 | 149 | --- 150 | 151 |
152 | 153 | ### Show some ❤️ by starring the repository! 154 | 155 |
156 | -------------------------------------------------------------------------------- /views/portfolio.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 |
8 | Hello, 12 |

15 | <%= user.displayName%>. 16 |

17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | <%- include('./partials/_search.ejs') %> 25 | 26 | 32 | 33 |
34 | 35 | 36 | 37 | 38 |
41 |
42 | total portfolio graph illustration 47 |
48 |
49 |

50 | $ <%= totalPortfolio.toFixed(2)%> 51 |

52 | Total Portfolio 53 |
54 |
55 | 56 | 57 | 58 |
61 |
62 | profit and loss graph illustration 67 |
68 |
69 |

70 | 0 71 |

72 | Profit / Loss 73 |
74 |
75 | 76 | 77 | 78 |
81 |
82 | balance graph illustration 87 |
88 |
89 |

90 | $ <%= user.balance.toFixed(2)%> 91 |

92 | Balance 93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 | 102 |

Your Stock Status

103 |
104 | <% if( StockMessage === '') { %> 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | <% user.stock.slice().reverse().forEach( (stock) => { %> 119 | 120 | 125 | 129 | 146 | 147 | 148 | 173 | 174 | <% }) %> 175 | 176 |
SymbolCurrentUp/DownStocksSell
121 | <%= stock.companySymbol %> 124 | 126 | <% currentPrice = Math.floor(stock.stockPrice*(Math.random()+0.7)) 127 | %> $<%= currentPrice %> 128 | 130 | <% let price = currentPrice - stock.stockPrice %> <% profitLoss = 131 | + currentPrice - stock.stockPrice %> <% if(price < 0) { %> <% 132 | color="red" %> 133 | $<%= price.toFixed(2) %> 138 | <% } else { %> <% color="green" %> 139 | $<%= price.toFixed(2) %> 144 | <% } %> 145 | <%= stock.noOfStock %> 149 |
150 | 156 | 171 |
172 |
177 | 178 | 179 | <% } else { %> 180 |

<%= StockMessage %>

181 | <% } %> 182 |
183 |
184 | 185 | 186 | <%- include('partials/_footer.ejs') %> 187 |
188 | 189 | 190 | 191 | 253 | -------------------------------------------------------------------------------- /routes/api/transaction.js: -------------------------------------------------------------------------------- 1 | // Cart Routes 2 | 3 | //jshint esversion:8 4 | 5 | const express = require("express"); 6 | const router = express.Router(); 7 | const { ensureAuth } = require("../../middleware/auth"); 8 | const User = require("../../models/User"); 9 | const Transaction = require("../../models/Transaction"); 10 | const getPrice = require("../../helpers/getPrice"); 11 | 12 | // Import emailHelper helper function 13 | const emailHelper = require("../../helpers/emailHelper"); 14 | 15 | // @desc View Transaction History 16 | // @route GET /transaction 17 | // @access Private 18 | router.get("/", ensureAuth, async (req, res) => { 19 | const user = req.user; 20 | try { 21 | const transactions = await Transaction.find({ 22 | user: req.user.id, 23 | }) 24 | .populate("user") 25 | .sort({ 26 | createdAt: -1, 27 | }) 28 | .lean(); 29 | 30 | if (Object.keys(transactions).length == 0) { 31 | var message = "No Transaction"; 32 | res.render("history", { 33 | message, 34 | transactions, 35 | user, 36 | layout: "layouts/app", 37 | href: "/transaction", 38 | }); 39 | } else { 40 | var message = ""; 41 | res.render("history", { 42 | message, 43 | transactions, 44 | user, 45 | layout: "layouts/app", 46 | href: "/transaction", 47 | }); 48 | } 49 | } catch (err) { 50 | console.error(err); 51 | res.render("error/500"); 52 | } 53 | }); 54 | 55 | // @desc To Buy Stocks 56 | // @route POST /transaction/buy 57 | // @access Private 58 | router.post("/buy", ensureAuth, async (req, res) => { 59 | const user = req.user; 60 | console.log(req.body); 61 | const symbol = req.body.companySymbol; 62 | const noOfStock = req.body.noOfStock; 63 | const stockPrice = req.body.stockPrice; 64 | // const { latestPrice } = await getPrice(symbol); 65 | const totalAmount = await parseFloat(stockPrice * noOfStock).toFixed(2); 66 | 67 | const data = { 68 | companySymbol: symbol, 69 | stockPrice: stockPrice, 70 | noOfStock: noOfStock, 71 | totalAmount: totalAmount, 72 | }; 73 | 74 | let method = "confirm"; 75 | 76 | try { 77 | if (totalAmount > req.user.balance) { 78 | let ExtraBalance = totalAmount - req.user.balance; 79 | ExtraBalance = ExtraBalance.toFixed(2); 80 | res.render("transaction", { 81 | layout: "layouts/app", 82 | href: "/buy", 83 | method, 84 | ExtraBalance, 85 | message: "Insufficient Balance", 86 | }); 87 | } else { 88 | res.render("transaction", { 89 | data, 90 | user, 91 | method, 92 | totalAmount, 93 | stockPrice, 94 | message: "Transaction Review", 95 | layout: "layouts/app", 96 | href: "/buy", 97 | }); 98 | } 99 | } catch (err) { 100 | console.error(err); 101 | res.render("error/500"); 102 | } 103 | }); 104 | 105 | // @desc Confirm Transaction (BUY) 106 | // @route PUT transaction/confirm 107 | // @access Private 108 | router.put("/confirm", ensureAuth, async (req, res) => { 109 | try { 110 | const companySymbol = req.body.companySymbol; 111 | const stockCount = Number(req.body.noOfStock); 112 | const totalPrice = Number(req.body.totalAmount); 113 | let balance = Number(req.user.balance) - totalPrice; 114 | req.body.user = req.user.id; 115 | const { email, displayName } = req.user; 116 | 117 | // Creating Body 118 | let body = { 119 | companySymbol: req.body.companySymbol, 120 | stockPrice: Number(req.body.stockPrice), 121 | noOfStock: Number(req.body.noOfStock), 122 | totalAmount: Number(req.body.totalAmount), 123 | }; 124 | 125 | // Finding if the stock already Exist 126 | let stockArr = req.user.stock.find( 127 | (obj) => obj.companySymbol === companySymbol 128 | ); 129 | 130 | // If stock exits and update body 131 | if (stockArr != null) { 132 | body = { 133 | companySymbol: req.body.companySymbol, 134 | stockPrice: Number(req.body.stockPrice), 135 | noOfStock: stockArr.noOfStock + stockCount, 136 | totalAmount: stockArr.totalAmount + totalPrice, 137 | }; 138 | } 139 | 140 | // Pull the stock if already exist 141 | await User.findOneAndUpdate( 142 | { 143 | _id: req.user.id, 144 | }, 145 | { 146 | $pull: { 147 | stock: { companySymbol: companySymbol }, 148 | }, 149 | }, 150 | { safe: true } 151 | ); 152 | 153 | // Push Stock with the body or update body in case of pull 154 | await User.findOneAndUpdate( 155 | { _id: req.user.id }, 156 | { 157 | balance: balance, 158 | $push: { 159 | stock: body, 160 | }, 161 | }, 162 | { 163 | new: true, // it will create a new one, if it doesn't exist 164 | runValidators: true, // it check weather the fields are valid or not 165 | } 166 | ); 167 | 168 | // Email Message 169 | let msg = ""; 170 | 171 | if (stockCount == 1) { 172 | msg = "stock"; 173 | } else { 174 | msg = "stocks"; 175 | } 176 | 177 | // Email Object 178 | const options = { 179 | to: email, // list of receivers 180 | subject: "Hello from TradeByte ✔", // Subject line 181 | html: ` 182 | Hello ${displayName}, 183 |

You bought ${stockCount} ${msg} of ${companySymbol} from TradeByte of amount ${totalPrice.toFixed( 184 | 2 185 | )}, your remaining TradeByte balance is ${balance.toFixed(2)}

186 |

This is a Demo Project made by TradeByte team for educational purpose only.

187 |

You can check the Github Repo for details.

188 |

Have a great Day!

189 | `, // html body 190 | }; 191 | emailHelper.sendEmail(options); 192 | 193 | // Adding new transaction details on Transaction Schema. 194 | const transactionDetails = `Added ${req.body.noOfStock} ${req.body.companySymbol} ${msg}`; 195 | const transactionOperation = "Debited"; 196 | const transactionUser = req.user.id; 197 | const transactionAmount = Number(req.body.totalAmount); 198 | 199 | // Create Transaction 200 | await Transaction.create({ 201 | details: transactionDetails, 202 | amount: transactionAmount, 203 | operation: transactionOperation, 204 | user: transactionUser, 205 | }); 206 | 207 | // Done 208 | res.redirect("/done"); 209 | } catch (err) { 210 | console.error(err); 211 | res.render("error/500"); 212 | } 213 | }); 214 | 215 | // @desc Sell Stocks 216 | // @route POST /transaction/sell 217 | // @access Private 218 | router.post("/sell", ensureAuth, async (req, res) => { 219 | let companySymbol = req.body.companySymbol; 220 | let method = "confirmSell"; 221 | let data = req.user.stock.find((obj) => obj.companySymbol == companySymbol); 222 | 223 | try { 224 | res.render("transaction", { 225 | layout: "layouts/app", 226 | method, 227 | data, 228 | message: "Transaction Review", 229 | href: "/sell", 230 | }); 231 | } catch (err) { 232 | console.error(err); 233 | res.render("error/500"); 234 | } 235 | }); 236 | 237 | // @desc Confirm Transaction (SELL) 238 | // @route PUT transaction/confirmSell 239 | // @access Private 240 | router.put("/confirmSell", ensureAuth, async (req, res) => { 241 | try { 242 | const companySymbol = req.body.companySymbol; 243 | const noOfStock = Number(req.body.noOfStock); 244 | const { high } = await getPrice(companySymbol); 245 | const totalAmount = high * noOfStock; 246 | const newBalance = req.user.balance + totalAmount; 247 | 248 | const { email, displayName } = req.user; 249 | 250 | let data = req.user.stock.find((obj) => obj.companySymbol == companySymbol); 251 | 252 | await User.findOneAndUpdate( 253 | { 254 | _id: req.user.id, 255 | }, 256 | { 257 | balance: newBalance, 258 | $pull: { 259 | stock: { 260 | companySymbol: companySymbol, 261 | }, 262 | }, 263 | }, 264 | { safe: true } 265 | ); 266 | 267 | if (noOfStock < data.noOfStock) { 268 | let body = { 269 | companySymbol: companySymbol, 270 | stockPrice: high, 271 | noOfStock: data.noOfStock - noOfStock, 272 | totalAmount: (data.noOfStock - noOfStock) * high, 273 | }; 274 | 275 | // Push Stock with the body or update body in case of pull 276 | await User.findOneAndUpdate( 277 | { _id: req.user.id }, 278 | { 279 | balance: newBalance, 280 | $push: { 281 | stock: body, 282 | }, 283 | }, 284 | { 285 | new: true, // it will create a new one, if it doesn't exist 286 | runValidators: true, // it check weather the fields are valid or not 287 | } 288 | ); 289 | } 290 | 291 | // Email Message 292 | let msg = ""; 293 | 294 | if (noOfStock == 1) { 295 | msg = "stock"; 296 | } else { 297 | msg = "stocks"; 298 | } 299 | 300 | // Email Object 301 | const options = { 302 | to: email, // list of receivers 303 | subject: "Hello from TradeByte ✔", // Subject line 304 | html: ` 305 | Hello ${displayName}, 306 |

You sold ${noOfStock} ${msg} of ${companySymbol} from TradeByte of amount ${totalAmount.toFixed( 307 | 2 308 | )}, your updated TradeByte balance is ${req.user.balance.toFixed(2)}

309 |

This is a Demo Project made by TradeByte team for educational purpose only.

310 |

You can check the Github Repo for details.

311 |

Have a great Day!

312 | `, // html body 313 | }; 314 | emailHelper.sendEmail(options); 315 | 316 | // Adding new transaction details on Transaction Schema. 317 | const transactionDetails = `Sold ${noOfStock} ${msg} of ${companySymbol}`; 318 | const transactionOperation = "Credit"; 319 | const transactionUser = req.user.id; 320 | 321 | // Added to Transaction 322 | await Transaction.create({ 323 | details: transactionDetails, 324 | amount: totalAmount, 325 | operation: transactionOperation, 326 | user: transactionUser, 327 | }); 328 | 329 | // Done 330 | res.redirect("/done"); 331 | } catch (err) { 332 | console.error(err); 333 | res.render("error/500"); 334 | } 335 | }); 336 | 337 | module.exports = router; 338 | -------------------------------------------------------------------------------- /views/market.ejs: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |
7 |

10 | Popular Stocks 11 |

12 |
13 | 14 | 15 |
16 |
17 | <%- include('./partials/_search.ejs') %> 18 | 19 | 25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | <% for(var i=0; i < compactData.length; i++) { %> 34 |
35 |
36 |
37 | <%- compactData[i]["Symbol"] %> 38 |
39 |

40 | <%- compactData[i]["Company Name"] %> 41 |

42 | 58 |
59 |
60 | <% } %> 61 |
62 |
63 |
64 | 65 | 66 | 67 |
68 |
69 |

72 | Trending Stocks 73 |

74 |
75 | 76 |
77 | <% for(var i=min; i < max; i++) { %> 78 |
79 |
82 |
83 |
84 | <%- totalData[i]["Symbol"] %> 85 |
86 |

87 | <%- totalData[i]["Company Name"] %> 88 |

89 | 105 |
106 |
107 |
108 | <% } %> 109 |
110 | 111 | 112 |
113 |
114 | 137 | 162 |
163 |
164 |
165 |
166 | 167 |
168 | 169 | 201 | 202 | 203 | 204 | 257 | -------------------------------------------------------------------------------- /views/partials/_nav.ejs: -------------------------------------------------------------------------------- 1 | 5 | 6 |
11 |
14 |
15 | 19 |
20 |

21 | TradeByte 27 |

28 | 47 |
48 | 49 | 195 |
196 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#a0aec0}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:#a0aec0}input::placeholder,textarea::placeholder{color:#a0aec0}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}body,html{margin:0;padding:0;min-height:100vh;min-width:100vw;overflow-x:hidden;box-sizing:border-box;background:linear-gradient(122.51deg,#e6ecfc 30%,#7fb4f1)!important;scroll-behavior:smooth}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.chartjs-container{position:relative;margin:auto;height:75vh;width:75vw}input::-webkit-inner-spin-button.without-arrow,input::-webkit-outer-spin-button.without-arrow{-webkit-appearance:none;margin:0}input[type=number].without-arrow{-moz-appearance:textfield}.symbolicon{font-size:72px;background:-webkit-linear-gradient(#eee,#333);background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent}.icon .badge{position:absolute;top:-5px;right:-5px;padding:5px 2px;border-radius:50%;background-color:#0072ff;color:#fff}#searchbar:focus .search-list{display:block}animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.faster{-webkit-animation-duration:.5s;animation-duration:.5s}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.serif{font-family:Zilla Slab,serif}[x-cloak]{display:none}.grid-indigo{background-image:radial-gradient(#5a67d8 2px,transparent 0);background-size:16px 16px}.share>.container{opacity:0;visibility:hidden;transition:all .4s ease-in-out;transform:translateY(50%)}.share.showAddThisButtons>.container{opacity:1;visibility:visible;transform:translateY(40%);margin-top:.5rem}.space-y-5>:not(template)~:not(template){--space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--space-y-reverse)));margin-bottom:calc(1.25rem*var(--space-y-reverse))}.divide-y>:not(template)~:not(template){--divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--divide-y-reverse)));border-bottom-width:calc(1px*var(--divide-y-reverse))}.divide-gray-200>:not(template)~:not(template){--divide-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--divide-opacity))}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.bg-transparent{background-color:transparent}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.bg-gray-200{--bg-opacity:1;background-color:#edf2f7;background-color:rgba(237,242,247,var(--bg-opacity))}.bg-gray-300{--bg-opacity:1;background-color:#e2e8f0;background-color:rgba(226,232,240,var(--bg-opacity))}.bg-gray-700{--bg-opacity:1;background-color:#4a5568;background-color:rgba(74,85,104,var(--bg-opacity))}.bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.bg-red-300{--bg-opacity:1;background-color:#feb2b2;background-color:rgba(254,178,178,var(--bg-opacity))}.bg-red-600{--bg-opacity:1;background-color:#e53e3e;background-color:rgba(229,62,62,var(--bg-opacity))}.bg-red-700{--bg-opacity:1;background-color:#c53030;background-color:rgba(197,48,48,var(--bg-opacity))}.bg-red-900{--bg-opacity:1;background-color:#742a2a;background-color:rgba(116,42,42,var(--bg-opacity))}.bg-yellow-300{--bg-opacity:1;background-color:#faf089;background-color:rgba(250,240,137,var(--bg-opacity))}.bg-yellow-500{--bg-opacity:1;background-color:#ecc94b;background-color:rgba(236,201,75,var(--bg-opacity))}.bg-green-200{--bg-opacity:1;background-color:#c6f6d5;background-color:rgba(198,246,213,var(--bg-opacity))}.bg-green-300{--bg-opacity:1;background-color:#9ae6b4;background-color:rgba(154,230,180,var(--bg-opacity))}.bg-green-400{--bg-opacity:1;background-color:#68d391;background-color:rgba(104,211,145,var(--bg-opacity))}.bg-green-600{--bg-opacity:1;background-color:#38a169;background-color:rgba(56,161,105,var(--bg-opacity))}.bg-teal-500{--bg-opacity:1;background-color:#38b2ac;background-color:rgba(56,178,172,var(--bg-opacity))}.bg-blue-300{--bg-opacity:1;background-color:#90cdf4;background-color:rgba(144,205,244,var(--bg-opacity))}.bg-blue-400{--bg-opacity:1;background-color:#63b3ed;background-color:rgba(99,179,237,var(--bg-opacity))}.bg-blue-600{--bg-opacity:1;background-color:#3182ce;background-color:rgba(49,130,206,var(--bg-opacity))}.bg-blue-700{--bg-opacity:1;background-color:#2b6cb0;background-color:rgba(43,108,176,var(--bg-opacity))}.bg-blue-800{--bg-opacity:1;background-color:#2c5282;background-color:rgba(44,82,130,var(--bg-opacity))}.bg-blue-900{--bg-opacity:1;background-color:#1955e4;background-color:rgba(25,85,228,var(--bg-opacity))}.bg-tempc-400{--bg-opacity:1;background-color:#266fea;background-color:rgba(38,111,234,var(--bg-opacity))}.bg-tempc-500{--bg-opacity:1;background-color:#41d7aa;background-color:rgba(65,215,170,var(--bg-opacity))}.hover\:bg-white:hover{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.hover\:bg-gray-200:hover{--bg-opacity:1;background-color:#edf2f7;background-color:rgba(237,242,247,var(--bg-opacity))}.hover\:bg-red-400:hover{--bg-opacity:1;background-color:#fc8181;background-color:rgba(252,129,129,var(--bg-opacity))}.hover\:bg-red-500:hover{--bg-opacity:1;background-color:#f56565;background-color:rgba(245,101,101,var(--bg-opacity))}.hover\:bg-yellow-600:hover{--bg-opacity:1;background-color:#d69e2e;background-color:rgba(214,158,46,var(--bg-opacity))}.hover\:bg-green-400:hover{--bg-opacity:1;background-color:#68d391;background-color:rgba(104,211,145,var(--bg-opacity))}.hover\:bg-green-500:hover{--bg-opacity:1;background-color:#48bb78;background-color:rgba(72,187,120,var(--bg-opacity))}.hover\:bg-teal-500:hover{--bg-opacity:1;background-color:#38b2ac;background-color:rgba(56,178,172,var(--bg-opacity))}.hover\:bg-blue-100:hover{--bg-opacity:1;background-color:#ebf8ff;background-color:rgba(235,248,255,var(--bg-opacity))}.hover\:bg-blue-400:hover{--bg-opacity:1;background-color:#63b3ed;background-color:rgba(99,179,237,var(--bg-opacity))}.hover\:bg-blue-500:hover{--bg-opacity:1;background-color:#4299e1;background-color:rgba(66,153,225,var(--bg-opacity))}.hover\:bg-blue-700:hover{--bg-opacity:1;background-color:#2b6cb0;background-color:rgba(43,108,176,var(--bg-opacity))}.hover\:bg-blue-800:hover{--bg-opacity:1;background-color:#2c5282;background-color:rgba(44,82,130,var(--bg-opacity))}.hover\:bg-indigo-700:hover{--bg-opacity:1;background-color:#4c51bf;background-color:rgba(76,81,191,var(--bg-opacity))}.focus\:bg-white:focus{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.focus\:bg-gray-200:focus{--bg-opacity:1;background-color:#edf2f7;background-color:rgba(237,242,247,var(--bg-opacity))}.focus\:bg-red-600:focus{--bg-opacity:1;background-color:#e53e3e;background-color:rgba(229,62,62,var(--bg-opacity))}.focus\:bg-blue-200:focus{--bg-opacity:1;background-color:#bee3f8;background-color:rgba(190,227,248,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-gray-300{--border-opacity:1;border-color:#e2e8f0;border-color:rgba(226,232,240,var(--border-opacity))}.border-gray-400{--border-opacity:1;border-color:#cbd5e0;border-color:rgba(203,213,224,var(--border-opacity))}.border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.border-green-400{--border-opacity:1;border-color:#68d391;border-color:rgba(104,211,145,var(--border-opacity))}.border-teal-500{--border-opacity:1;border-color:#38b2ac;border-color:rgba(56,178,172,var(--border-opacity))}.border-blue-500{--border-opacity:1;border-color:#4299e1;border-color:rgba(66,153,225,var(--border-opacity))}.border-blue-700{--border-opacity:1;border-color:#2b6cb0;border-color:rgba(43,108,176,var(--border-opacity))}.border-blue-900{--border-opacity:1;border-color:#1955e4;border-color:rgba(25,85,228,var(--border-opacity))}.hover\:border-green-400:hover{--border-opacity:1;border-color:#68d391;border-color:rgba(104,211,145,var(--border-opacity))}.hover\:border-blue-500:hover{--border-opacity:1;border-color:#4299e1;border-color:rgba(66,153,225,var(--border-opacity))}.hover\:border-blue-600:hover{--border-opacity:1;border-color:#3182ce;border-color:rgba(49,130,206,var(--border-opacity))}.focus\:border-purple-500:focus{--border-opacity:1;border-color:#9f7aea;border-color:rgba(159,122,234,var(--border-opacity))}.rounded-sm{border-radius:.125rem}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-r-full{border-top-right-radius:9999px;border-bottom-right-radius:9999px}.rounded-l-full{border-top-left-radius:9999px;border-bottom-left-radius:9999px}.border-none{border-style:none}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-r-2{border-right-width:2px}.border-b-2{border-bottom-width:2px}.border-b-4{border-bottom-width:4px}.border-l-8{border-left-width:8px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-pointer{cursor:pointer}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.flex-auto{flex:1 1 auto}.flex-grow{flex-grow:1}.flex-shrink-0{flex-shrink:0}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-black{font-weight:900}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-56{height:14rem}.h-64{height:16rem}.h-px{height:1px}.h-full{height:100%}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem}.text-3xl{font-size:1.875rem}.text-4xl{font-size:2.25rem}.text-5xl{font-size:3rem}.text-6xl{font-size:4rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.m-2{margin:.5rem}.m-8{margin:2rem}.m-auto{margin:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.mx-auto{margin-left:auto;margin-right:auto}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mb-0{margin-bottom:0}.ml-0{margin-left:0}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-6{margin-top:1.5rem}.mb-6{margin-bottom:1.5rem}.ml-6{margin-left:1.5rem}.mt-8{margin-top:2rem}.mb-8{margin-bottom:2rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mb-12{margin-bottom:3rem}.-mt-2{margin-top:-.5rem}.-mr-8{margin-right:-2rem}.-ml-8{margin-left:-2rem}.-ml-12{margin-left:-3rem}.max-w-xs{max-width:20rem}.max-w-sm{max-width:24rem}.max-w-md{max-width:28rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.object-cover{-o-object-fit:cover;object-fit:cover}.opacity-25{opacity:.25}.opacity-100{opacity:1}.focus\:outline-none:focus,.outline-none{outline:0}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-8{padding:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-8{padding-left:2rem;padding-right:2rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.px-12{padding-left:3rem;padding-right:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.pt-0{padding-top:0}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.pb-3{padding-bottom:.75rem}.pt-4{padding-top:1rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pt-6{padding-top:1.5rem}.pb-6{padding-bottom:1.5rem}.pr-8{padding-right:2rem}.pb-8{padding-bottom:2rem}.pt-10{padding-top:2.5rem}.pb-10{padding-bottom:2.5rem}.pl-10{padding-left:2.5rem}.pt-20{padding-top:5rem}.pt-64{padding-top:16rem}.placeholder-gray-400::-moz-placeholder{--placeholder-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--placeholder-opacity))}.placeholder-gray-400:-ms-input-placeholder{--placeholder-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--placeholder-opacity))}.placeholder-gray-400::-ms-input-placeholder{--placeholder-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--placeholder-opacity))}.placeholder-gray-400::placeholder{--placeholder-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--placeholder-opacity))}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.shadow-xs{box-shadow:0 0 0 1px rgba(0,0,0,.05)}.shadow-sm{box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.shadow-md{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)}.shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.shadow-xl{box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04)}.shadow-2xl{box-shadow:0 25px 50px -12px rgba(0,0,0,.25)}.hover\:shadow-md:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)}.hover\:shadow-lg:hover{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgba(66,153,225,.5)}.fill-current{fill:currentColor}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-black{--text-opacity:1;color:#000;color:rgba(0,0,0,var(--text-opacity))}.text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.text-gray-100{--text-opacity:1;color:#f7fafc;color:rgba(247,250,252,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-800{--text-opacity:1;color:#2d3748;color:rgba(45,55,72,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.text-red-500{--text-opacity:1;color:#f56565;color:rgba(245,101,101,var(--text-opacity))}.text-red-600{--text-opacity:1;color:#e53e3e;color:rgba(229,62,62,var(--text-opacity))}.text-red-700{--text-opacity:1;color:#c53030;color:rgba(197,48,48,var(--text-opacity))}.text-yellow-700{--text-opacity:1;color:#b7791f;color:rgba(183,121,31,var(--text-opacity))}.text-green-400{--text-opacity:1;color:#68d391;color:rgba(104,211,145,var(--text-opacity))}.text-green-600{--text-opacity:1;color:#38a169;color:rgba(56,161,105,var(--text-opacity))}.text-green-700{--text-opacity:1;color:#2f855a;color:rgba(47,133,90,var(--text-opacity))}.text-teal-500{--text-opacity:1;color:#38b2ac;color:rgba(56,178,172,var(--text-opacity))}.text-blue-200{--text-opacity:1;color:#bee3f8;color:rgba(190,227,248,var(--text-opacity))}.text-blue-600{--text-opacity:1;color:#3182ce;color:rgba(49,130,206,var(--text-opacity))}.text-blue-700{--text-opacity:1;color:#2b6cb0;color:rgba(43,108,176,var(--text-opacity))}.text-blue-900{--text-opacity:1;color:#1955e4;color:rgba(25,85,228,var(--text-opacity))}.hover\:text-black:hover{--text-opacity:1;color:#000;color:rgba(0,0,0,var(--text-opacity))}.hover\:text-white:hover{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.hover\:text-gray-900:hover{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.hover\:text-red-500:hover{--text-opacity:1;color:#f56565;color:rgba(245,101,101,var(--text-opacity))}.hover\:text-yellow-500:hover{--text-opacity:1;color:#ecc94b;color:rgba(236,201,75,var(--text-opacity))}.hover\:text-green-500:hover{--text-opacity:1;color:#48bb78;color:rgba(72,187,120,var(--text-opacity))}.hover\:text-blue-800:hover{--text-opacity:1;color:#2c5282;color:rgba(44,82,130,var(--text-opacity))}.hover\:text-blue-900:hover{--text-opacity:1;color:#1955e4;color:rgba(25,85,228,var(--text-opacity))}.focus\:text-black:focus{--text-opacity:1;color:#000;color:rgba(0,0,0,var(--text-opacity))}.focus\:text-gray-900:focus{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.focus\:text-blue-900:focus{--text-opacity:1;color:#1955e4;color:rgba(25,85,228,var(--text-opacity))}.italic{font-style:italic}.uppercase{text-transform:uppercase}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.align-middle{vertical-align:middle}.whitespace-no-wrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-48{width:12rem}.w-auto{width:auto}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-2\/4{width:50%}.w-3\/4{width:75%}.w-2\/5{width:40%}.w-11\/12{width:91.666667%}.w-full{width:100%}.w-screen{width:100vw}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.gap-4{grid-gap:1rem;gap:1rem}.gap-8{grid-gap:2rem;gap:2rem}.gap-10{grid-gap:2.5rem;gap:2.5rem}.grid-flow-col{grid-auto-flow:column}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.transform{--transform-translate-x:0;--transform-translate-y:0;--transform-rotate:0;--transform-skew-x:0;--transform-skew-y:0;--transform-scale-x:1;--transform-scale-y:1;transform:translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y))}.skew-x-6{--transform-skew-x:6deg}.-skew-x-6{--transform-skew-x:-6deg}.skew-y-6{--transform-skew-y:6deg}.-skew-y-6{--transform-skew-y:-6deg}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}@-webkit-keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@-webkit-keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@media (min-width:640px){.sm\:container{width:100%;max-width:640px}@media (min-width:768px){.sm\:container{max-width:768px}}@media (min-width:1024px){.sm\:container{max-width:1024px}}@media (min-width:1280px){.sm\:container{max-width:1280px}}.sm\:rounded-lg{border-radius:.5rem}.sm\:h-20{height:5rem}.sm\:text-xl{font-size:1.25rem}.sm\:text-5xl{font-size:3rem}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:ml-0{margin-left:0}.sm\:max-w-lg{max-width:32rem}.sm\:p-4{padding:1rem}.sm\:px-2{padding-left:.5rem;padding-right:.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-center{text-align:center}.sm\:w-56{width:14rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:768px){.md\:container{width:100%}@media (min-width:640px){.md\:container{max-width:640px}}@media (min-width:768px){.md\:container{max-width:768px}}@media (min-width:1024px){.md\:container{max-width:1024px}}@media (min-width:1280px){.md\:container{max-width:1280px}}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:justify-between{justify-content:space-between}.md\:h-16{height:4rem}.md\:h-40{height:10rem}.md\:h-screen{height:100vh}.md\:text-sm{font-size:.875rem}.md\:text-base{font-size:1rem}.md\:text-lg{font-size:1.125rem}.md\:text-xl{font-size:1.25rem}.md\:text-2xl{font-size:1.5rem}.md\:text-3xl{font-size:1.875rem}.md\:text-5xl{font-size:3rem}.md\:m-0{margin:0}.md\:my-0{margin-top:0;margin-bottom:0}.md\:my-16{margin-top:4rem;margin-bottom:4rem}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mt-0{margin-top:0}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-6{margin-left:1.5rem}.md\:ml-8{margin-left:2rem}.md\:mt-10{margin-top:2.5rem}.md\:mb-16{margin-bottom:4rem}.md\:ml-20{margin-left:5rem}.md\:mt-24{margin-top:6rem}.md\:ml-56{margin-left:14rem}.md\:ml-auto{margin-left:auto}.md\:max-w-md{max-width:28rem}.md\:max-w-5xl{max-width:64rem}.md\:p-8{padding:2rem}.md\:p-12{padding:3rem}.md\:px-1{padding-left:.25rem;padding-right:.25rem}.md\:px-2{padding-left:.5rem;padding-right:.5rem}.md\:px-4{padding-left:1rem;padding-right:1rem}.md\:px-5{padding-left:1.25rem;padding-right:1.25rem}.md\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:py-12{padding-top:3rem;padding-bottom:3rem}.md\:py-16{padding-top:4rem;padding-bottom:4rem}.md\:py-24{padding-top:6rem;padding-bottom:6rem}.md\:pb-0{padding-bottom:0}.md\:fixed{position:fixed}.md\:top-0{top:0}.md\:left-0{left:0}.md\:text-left{text-align:left}.md\:text-right{text-align:right}.md\:break-all{word-break:break-all}.md\:w-16{width:4rem}.md\:w-40{width:10rem}.md\:w-56{width:14rem}.md\:w-64{width:16rem}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.333333%}.md\:w-2\/3{width:66.666667%}.md\:w-4\/5{width:80%}.md\:z-50{z-index:50}.md\:gap-4{grid-gap:1rem;gap:1rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:col-span-4{grid-column:span 4/span 4}.md\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.md\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.lg\:container{width:100%}@media (min-width:640px){.lg\:container{max-width:640px}}@media (min-width:768px){.lg\:container{max-width:768px}}@media (min-width:1024px){.lg\:container{max-width:1024px}}@media (min-width:1280px){.lg\:container{max-width:1280px}}.lg\:block{display:block}.lg\:items-start{align-items:flex-start}.lg\:items-center{align-items:center}.lg\:justify-center{justify-content:center}.lg\:text-sm{font-size:.875rem}.lg\:text-base{font-size:1rem}.lg\:text-lg{font-size:1.125rem}.lg\:text-xl{font-size:1.25rem}.lg\:text-2xl{font-size:1.5rem}.lg\:text-3xl{font-size:1.875rem}.lg\:text-4xl{font-size:2.25rem}.lg\:text-5xl{font-size:3rem}.lg\:text-6xl{font-size:4rem}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:ml-6{margin-left:1.5rem}.lg\:ml-24{margin-left:6rem}.lg\:ml-64{margin-left:16rem}.lg\:-mr-16{margin-right:-4rem}.lg\:-ml-16{margin-left:-4rem}.lg\:max-w-5xl{max-width:64rem}.lg\:p-6{padding:1.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}.lg\:px-16{padding-left:4rem;padding-right:4rem}.lg\:text-center{text-align:center}.lg\:w-64{width:16rem}.lg\:w-1\/3{width:33.333333%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.lg\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.lg\:row-span-2{grid-row:span 2/span 2}}@media (min-width:1280px){.xl\:container{width:100%}@media (min-width:640px){.xl\:container{max-width:640px}}@media (min-width:768px){.xl\:container{max-width:768px}}@media (min-width:1024px){.xl\:container{max-width:1024px}}@media (min-width:1280px){.xl\:container{max-width:1280px}}.xl\:text-2xl{font-size:1.5rem}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} --------------------------------------------------------------------------------