├── 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 |
12 |
26 |
29 |
30 |
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 |
42 |
43 |
47 | Go to Homepage
48 |
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 |
73 | Go to Homepage
74 |
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 |
28 | Date
29 |
30 |
33 | Operation
34 |
35 |
38 | Amount
39 |
40 |
43 | Details
44 |
45 |
46 |
47 | <% transactions.forEach( (stock) => { %>
48 |
49 |
52 | <%= stock.createdAt %>
53 |
54 |
55 | <% if(stock.operation == "Debited") {%>
56 |
59 | <%= stock.operation %>
60 |
61 | <%} else {%>
62 |
65 | <%= stock.operation %>
66 |
67 | <%}%>
68 |
69 |
72 | <%= stock.amount %>
73 |
74 |
75 | <%= stock.details %>
76 |
77 |
78 | <% }) %>
79 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
46 | Back
47 |
48 |
49 |
53 | Share
54 |
63 |
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 |
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 |
117 |
127 |
--------------------------------------------------------------------------------
/views/transaction.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
29 | Add Balance
30 |
31 |
32 |
33 | <% } else { %>
34 |
35 | <%= message %>
36 |
37 |
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 |
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 |
122 | Buy
123 |
124 |
125 |
126 |
127 |
148 |
149 |
--------------------------------------------------------------------------------
/views/signup.ejs:
--------------------------------------------------------------------------------
1 |
125 |
126 |
127 |
128 |
169 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # ⚠️ NOT ACTIVELY MAINTAINED
4 |
5 | ---
6 |
7 | #
**TradeByte**
8 |
9 | ## Stocks Trading Simulation WebApp
10 |
11 |     
12 |
13 |      
14 |
15 | ## Website : https://tradebyte.up.railway.app
16 |
17 | [](https://github.com/iampavangandhi/TradeByte/projects/1) [](https://github.com/iampavangandhi/TradeByte/wiki) [](https://docs.google.com/spreadsheets/d/1_vg-F2dk6jWy9jcYlGuKqAnXUX6NwbbnLg9Bmn_Ulrk/edit?usp=sharing) [](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 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
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 | 
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 | [](https://github.com/iampavangandhi/TradeByte/graphs/contributors)
132 |
133 | ## Maintainers
134 |
135 |
136 |
137 | #### This repo is maintained by [Pavan Gandhi](https://github.con/iampavangandhi).
138 |
139 | ## API Credits
140 |
141 |
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 |
47 |
48 |
49 |
50 | $ <%= totalPortfolio.toFixed(2)%>
51 |
52 |
Total Portfolio
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 |
67 |
68 |
69 |
70 | 0
71 |
72 |
Profit / Loss
73 |
74 |
75 |
76 |
77 |
78 |
81 |
82 |
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 | Symbol
111 | Current
112 | Up/Down
113 | Stocks
114 | Sell
115 |
116 |
117 |
118 | <% user.stock.slice().reverse().forEach( (stock) => { %>
119 |
120 |
121 | <%= stock.companySymbol %>
124 |
125 |
126 | <% currentPrice = Math.floor(stock.stockPrice*(Math.random()+0.7))
127 | %> $<%= currentPrice %>
128 |
129 |
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 |
146 |
147 | <%= stock.noOfStock %>
148 |
149 |
172 |
173 |
174 | <% }) %>
175 |
176 |
177 |
<%= profitLoss %>
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 |
118 |
130 |
134 |
135 | Previous page
136 |
137 |
141 | Next page
142 |
154 |
160 |
161 |
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 |
28 |
32 |
33 |
39 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
83 |
84 |
88 |
103 | Trading Active
104 |
105 |
106 |
194 |
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))}}
--------------------------------------------------------------------------------