├── .yarnrc.yml ├── public ├── favicon.ico ├── assets │ ├── img │ │ ├── logo.jpg │ │ ├── footer.jpg │ │ └── arrow-down.svg │ ├── js │ │ ├── script.js │ │ └── main.js │ ├── css │ │ ├── style.css │ │ └── dashboard.css │ └── vendor │ │ └── boxicons.min.css ├── vercel.svg └── main.css ├── pages ├── api │ ├── index.js │ ├── subs │ │ ├── count.js │ │ ├── [id].js │ │ └── index.js │ ├── systems │ │ ├── status.js │ │ ├── index.js │ │ └── [id].js │ ├── users │ │ ├── me.js │ │ ├── create.js │ │ ├── index.js │ │ └── [id].js │ ├── auth.js │ └── config │ │ └── index.js ├── _app.js ├── login.jsx ├── register.jsx ├── index.js └── admin │ ├── manage-admin.jsx │ └── index.jsx ├── screenshot ├── homepage.png └── dashboard.png ├── postcss.config.js ├── utils ├── admin.js ├── auth.js └── db.js ├── theme.js ├── next.config.js ├── styles ├── globals.css └── Home.module.css ├── services ├── userService.js ├── logService.js ├── httpService.js ├── movieService.js └── authService.js ├── tailwind.config.js ├── models ├── config.js ├── subscriber.js ├── system.js └── user.js ├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── .github └── workflows │ └── codeql-analysis.yml └── components └── Layout.jsx /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstats/UpStats/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /pages/api/index.js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | res.send("Home"); 3 | }; 4 | -------------------------------------------------------------------------------- /screenshot/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstats/UpStats/HEAD/screenshot/homepage.png -------------------------------------------------------------------------------- /public/assets/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstats/UpStats/HEAD/public/assets/img/logo.jpg -------------------------------------------------------------------------------- /screenshot/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstats/UpStats/HEAD/screenshot/dashboard.png -------------------------------------------------------------------------------- /public/assets/img/footer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstats/UpStats/HEAD/public/assets/img/footer.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /utils/admin.js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | if (!req.user.email === process.env.admin_email) 3 | return res.status(403).send("Access denied."); 4 | }; 5 | -------------------------------------------------------------------------------- /theme.js: -------------------------------------------------------------------------------- 1 | import { extendTheme } from "@chakra-ui/react"; 2 | 3 | const theme = extendTheme({ 4 | fonts: { 5 | heading: "Poppins", 6 | }, 7 | }); 8 | 9 | export default theme; 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true, 4 | }, 5 | env: { 6 | MONGODB_URI: "", 7 | jwtPrivateKey: "", 8 | admin_email: "", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /public/assets/img/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;600&display=swap"); 6 | 7 | .root { 8 | font-family: "Poppins", sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /services/userService.js: -------------------------------------------------------------------------------- 1 | import http from "./httpService"; 2 | 3 | const apiEndpoint = "/users"; 4 | 5 | export function register(user) { 6 | return http.post(apiEndpoint, { 7 | email: user.username, 8 | password: user.password, 9 | name: user.name, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /services/logService.js: -------------------------------------------------------------------------------- 1 | // For simplicity, I changed the implementation of this module 2 | // and removed Raven. We can always add that in the future 3 | // and this module is the only module we need to modify. 4 | 5 | function init() {} 6 | 7 | function log(error) { 8 | console.error(error); 9 | } 10 | 11 | export default { 12 | init, 13 | log 14 | }; 15 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { ChakraProvider } from "@chakra-ui/react"; 3 | import "@fontsource/poppins/300.css"; 4 | import theme from "../theme"; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /pages/api/subs/count.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import Email from "../../../models/subscriber"; 3 | export default async (req, res) => { 4 | await db(); 5 | if (req.method === "GET") { 6 | const emails = await Email.find().sort("email"); 7 | 8 | res.send(emails.length); 9 | } else { 10 | res.setHeader("Allow", ["GET"]); 11 | res.status(405).end(`Method Not Allowed`); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export default async (req, res, fn) => { 4 | const token = req.headers["x-auth-token"]; 5 | if (!token) return res.status(401).send("Access denied. No token provided."); 6 | 7 | try { 8 | const decoded = jwt.verify(token, process.env.jwtPrivateKey); 9 | req.user = decoded; 10 | } catch (ex) { 11 | return res.status(400).send("Invalid token."); 12 | } 13 | await fn(); 14 | }; 15 | -------------------------------------------------------------------------------- /models/config.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import mongoose from "mongoose"; 3 | 4 | const ConfigSchema = new mongoose.Schema({ 5 | mailing: { type: Boolean, required: true }, 6 | }); 7 | 8 | export function validateConfig(config) { 9 | const schema = Joi.object({ 10 | mailing: Joi.boolean().required(), 11 | }); 12 | 13 | return schema.validate(config); 14 | } 15 | 16 | export default mongoose.models.Config || mongoose.model("Config", ConfigSchema); 17 | -------------------------------------------------------------------------------- /public/assets/js/script.js: -------------------------------------------------------------------------------- 1 | mybutton = document.getElementById("scroll-to-top"); 2 | 3 | window.onscroll = function() { scrollFunction() }; 4 | 5 | function scrollFunction() { 6 | if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) { 7 | mybutton.style.display = "block"; 8 | } else { 9 | mybutton.style.display = "none"; 10 | } 11 | } 12 | 13 | function topFunction() { 14 | document.body.scrollTop = 0; 15 | document.documentElement.scrollTop = 0; 16 | } -------------------------------------------------------------------------------- /pages/api/subs/[id].js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import Email from "../../../models/subscriber"; 3 | export default async (req, res) => { 4 | await db(); 5 | if (req.method === "DELETE") { 6 | const email = await Email.findByIdAndRemove(req.query.id); 7 | 8 | if (!email) 9 | return res.status(404).send("The email with the given ID was not found."); 10 | 11 | res.send(email); 12 | } else { 13 | res.setHeader("Allow", ["DELETE"]); 14 | res.status(405).end(`Method Not Allowed`); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /pages/api/systems/status.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import System from "../../../models/system"; 3 | export default async (req, res) => { 4 | await db(); 5 | 6 | let isOperational = false; 7 | let outageCount = 0; 8 | const systems = await System.find(); 9 | for (let system of systems) { 10 | isOperational = system.status === "up" ? true : false; 11 | if (!isOperational) outageCount += 1; 12 | } 13 | 14 | res.status(200).send({ 15 | operational: isOperational, 16 | ...(!isOperational && { outageCount }), 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /models/subscriber.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import mongoose from "mongoose"; 3 | 4 | const emailSchema = new mongoose.Schema({ 5 | email: { 6 | type: String, 7 | required: true, 8 | minlength: 5, 9 | maxlength: 255, 10 | unique: true, 11 | }, 12 | }); 13 | 14 | export function validateEmail(email) { 15 | const schema = Joi.object({ 16 | email: Joi.string().min(5).max(255).required().email(), 17 | }); 18 | 19 | return schema.validate(email); 20 | } 21 | 22 | export default mongoose.models.Email || mongoose.model("Email", emailSchema); 23 | -------------------------------------------------------------------------------- /pages/api/users/me.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import User from "../../../models/user"; 3 | import withAuth from "../../../utils/auth"; 4 | export default async (req, res) => { 5 | await db(); 6 | switch (req.method) { 7 | case "GET": 8 | withAuth(req, res, async () => { 9 | const user = await User.findById(req.user._id).select("-password"); 10 | 11 | res.send(user); 12 | }); 13 | break; 14 | default: 15 | res.setHeader("Allow", ["POST"]); 16 | res.status(405).end(`Method Not Allowed`); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | yarn.lock 37 | 38 | .yarn/ 39 | .pnp.js 40 | .pnp.cjs 41 | -------------------------------------------------------------------------------- /services/httpService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | axios.defaults.baseURL = "/api"; 4 | 5 | axios.interceptors.response.use(null, (error) => { 6 | const expectedError = 7 | error.response && 8 | error.response.status >= 400 && 9 | error.response.status < 500; 10 | 11 | if (!expectedError) { 12 | alert("An unexpected error occurrred."); 13 | } 14 | 15 | return Promise.reject(error); 16 | }); 17 | 18 | function setJwt(jwt) { 19 | axios.defaults.headers.common["x-auth-token"] = jwt; 20 | } 21 | 22 | export default { 23 | get: axios.get, 24 | post: axios.post, 25 | put: axios.put, 26 | delete: axios.delete, 27 | setJwt, 28 | }; 29 | -------------------------------------------------------------------------------- /models/system.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import mongoose from "mongoose"; 3 | 4 | const SystemSchema = new mongoose.Schema({ 5 | name: { type: String, required: true }, 6 | url: { type: String, required: true }, 7 | type: { type: String, required: true }, 8 | status: String, 9 | logs: [{ status: String, time: Date }], 10 | }); 11 | 12 | export function validateSystem(system) { 13 | const schema = Joi.object({ 14 | name: Joi.string().max(50).required(), 15 | url: Joi.required(), 16 | type: Joi.string().required(), 17 | }); 18 | 19 | return schema.validate(system); 20 | } 21 | 22 | export default mongoose.models.System || mongoose.model("System", SystemSchema); 23 | -------------------------------------------------------------------------------- /services/movieService.js: -------------------------------------------------------------------------------- 1 | import http from "./httpService"; 2 | 3 | const apiEndpoint = "/movies"; 4 | 5 | function movieUrl(id) { 6 | return `${apiEndpoint}/${id}`; 7 | } 8 | 9 | export function getMovies() { 10 | return http.get(apiEndpoint); 11 | } 12 | 13 | export function getMovie(movieId) { 14 | return http.get(movieUrl(movieId)); 15 | } 16 | 17 | export function saveMovie(movie) { 18 | if (movie._id) { 19 | const body = { ...movie }; 20 | delete body._id; 21 | return http.put(movieUrl(movie._id), body); 22 | } 23 | 24 | return http.post(apiEndpoint, movie); 25 | } 26 | 27 | export function deleteMovie(movieId) { 28 | return http.delete(movieUrl(movieId)); 29 | } 30 | -------------------------------------------------------------------------------- /pages/api/subs/index.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import Email, { validateEmail } from "../../../models/subscriber"; 3 | import withAuth from "../../../utils/auth"; 4 | export default async (req, res) => { 5 | await db(); 6 | if (req.method === "GET") { 7 | withAuth(req, res, async () => { 8 | const emails = await Email.find().sort("email"); 9 | res.send(emails); 10 | }); 11 | } else if (req.method === "POST") { 12 | const { error } = validateEmail(req.body); 13 | if (error) return res.status(400).send(error.details[0].message); 14 | 15 | const email = new Email({ 16 | email: req.body.email, 17 | }); 18 | await email.save(); 19 | 20 | res.send(email); 21 | } else { 22 | res.setHeader("Allow", ["GET", "POST"]); 23 | res.status(405).end(`Method Not Allowed`); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstats", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/icons": "^1.0.12", 12 | "@chakra-ui/react": "^1.6.0", 13 | "@emotion/react": "^11.1.5", 14 | "@emotion/styled": "^11.3.0", 15 | "@fontsource/poppins": "^4.2.2", 16 | "apisauce": "^2.1.1", 17 | "axios": "^0.21.2", 18 | "bcrypt": "^5.0.1", 19 | "formik": "^2.2.6", 20 | "framer-motion": "^4.1.11", 21 | "joi": "^17.4.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "jwt-decode": "^3.1.2", 24 | "lodash": "^4.17.21", 25 | "mongoose": "^5.13.20", 26 | "next": "^12.1.0", 27 | "react": "17.0.2", 28 | "react-dom": "17.0.2", 29 | "yup": "^0.32.9" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^10.2.5", 33 | "postcss": "^8.2.13", 34 | "tailwindcss": "^2.1.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services/authService.js: -------------------------------------------------------------------------------- 1 | import jwtDecode from "jwt-decode"; 2 | import http from "./httpService"; 3 | 4 | const apiEndpoint = "/auth"; 5 | const tokenKey = "token"; 6 | 7 | http.setJwt(getJwt()); 8 | 9 | export async function login(email, password) { 10 | const { data: jwt } = await http.post(apiEndpoint, { email, password }); 11 | 12 | window.localStorage.setItem(tokenKey, jwt); 13 | } 14 | 15 | export function loginWithJwt(jwt) { 16 | window.localStorage.setItem(tokenKey, jwt); 17 | } 18 | 19 | export function logout() { 20 | window.localStorage.removeItem(tokenKey); 21 | } 22 | 23 | export function getCurrentUser() { 24 | try { 25 | const jwt = window.localStorage.getItem(tokenKey); 26 | return jwtDecode(jwt); 27 | } catch (ex) { 28 | return null; 29 | } 30 | } 31 | 32 | export function getJwt() { 33 | return window.localStorage.getItem(tokenKey); 34 | } 35 | 36 | export default { 37 | login, 38 | loginWithJwt, 39 | logout, 40 | getCurrentUser, 41 | getJwt, 42 | }; 43 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import Joi from "joi"; 3 | import mongoose from "mongoose"; 4 | 5 | const userSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | minlength: 5, 9 | maxlength: 50, 10 | }, 11 | email: { 12 | type: String, 13 | minlength: 5, 14 | maxlength: 255, 15 | unique: true, 16 | }, 17 | password: { 18 | type: String, 19 | minlength: 5, 20 | maxlength: 1024, 21 | }, 22 | }); 23 | 24 | userSchema.methods.generateAuthToken = function () { 25 | const token = jwt.sign( 26 | { _id: this._id, isAdmin: this.email === process.env.admin_email }, 27 | process.env.jwtPrivateKey 28 | ); 29 | return token; 30 | }; 31 | 32 | export function validateUser(user) { 33 | const schema = Joi.object({ 34 | name: Joi.string().min(5).max(50), 35 | email: Joi.string().min(5).max(255).email(), 36 | password: Joi.string().min(5).max(255), 37 | }); 38 | 39 | return schema.validate(user); 40 | } 41 | 42 | export default mongoose.models.User || mongoose.model("User", userSchema); 43 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/api/systems/index.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import db from "../../../utils/db"; 3 | import System, { validateSystem } from "../../../models/system"; 4 | import withAuth from "../../../utils/auth"; 5 | 6 | export default async (req, res) => { 7 | await db(); 8 | switch (req.method) { 9 | case "GET": 10 | const systems = await System.find().sort("name"); 11 | res.status(200).send(systems); 12 | break; 13 | case "POST": 14 | await withAuth(req, res, async () => { 15 | const { error } = validateSystem(req.body); 16 | if (error) return res.status(400).send(error.details[0].message); 17 | 18 | const system = new System({ 19 | name: req.body.name, 20 | url: req.body.url, 21 | type: req.body.type, 22 | status: "up", 23 | }); 24 | await system.save(); 25 | res.send(system); 26 | }); 27 | break; 28 | default: 29 | res.setHeader("Allow", ["GET", "POST"]); 30 | res.status(405).end(`Method Not Allowed`); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 UpStats 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 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | 5 | if (!MONGODB_URI) { 6 | throw new Error( 7 | "Please define the MONGODB_URI environment variable inside .env.local" 8 | ); 9 | } 10 | 11 | /** 12 | * Global is used here to maintain a cached connection across hot reloads 13 | * in development. This prevents connections growing exponentially 14 | * during API Route usage. 15 | */ 16 | let cached = global.mongoose; 17 | 18 | if (!cached) { 19 | cached = global.mongoose = { conn: null, promise: null }; 20 | } 21 | 22 | async function dbConnect() { 23 | if (cached.conn) { 24 | return cached.conn; 25 | } 26 | 27 | if (!cached.promise) { 28 | const opts = { 29 | useNewUrlParser: true, 30 | useUnifiedTopology: true, 31 | bufferCommands: false, 32 | bufferMaxEntries: 0, 33 | useFindAndModify: false, 34 | useCreateIndex: true, 35 | }; 36 | 37 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 38 | return mongoose; 39 | }); 40 | } 41 | cached.conn = await cached.promise; 42 | return cached.conn; 43 | } 44 | 45 | export default dbConnect; 46 | -------------------------------------------------------------------------------- /pages/api/auth.js: -------------------------------------------------------------------------------- 1 | import db from "../../utils/db"; 2 | import Joi from "joi"; 3 | import bcrypt from "bcrypt"; 4 | import _ from "lodash"; 5 | import User from "../../models/user"; 6 | 7 | export default async (req, res) => { 8 | await db(); 9 | if (req.method === "POST") { 10 | const { error } = validate(req.body); 11 | if (error) return res.status(400).send(error.details[0].message); 12 | 13 | let user = await User.findOne({ email: req.body.email }); 14 | if (!user) return res.status(400).send("Invalid email or password."); 15 | 16 | const validPassword = await bcrypt.compare( 17 | req.body.password, 18 | user.password 19 | ); 20 | if (!validPassword) 21 | return res.status(400).send("Invalid email or password."); 22 | 23 | const token = user.generateAuthToken(); 24 | 25 | res.status(200).send(token); 26 | } else { 27 | res.setHeader("Allow", ["POST"]); 28 | res.status(405).end(`Method Not Allowed`); 29 | } 30 | }; 31 | 32 | function validate(req) { 33 | const schema = Joi.object({ 34 | email: Joi.string().min(5).max(255).required().email(), 35 | password: Joi.string().min(5).max(255).required(), 36 | }); 37 | 38 | return schema.validate(req); 39 | } 40 | -------------------------------------------------------------------------------- /pages/api/config/index.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import withAuth from "../../../utils/auth"; 3 | import Config, { validateConfig } from "../../../models/config"; 4 | export default async (req, res) => { 5 | await db(); 6 | if (req.method === "GET") { 7 | const configs = await Config.find(); 8 | console.log("😀", configs); 9 | res.send(configs[0]); 10 | } else if (req.method === "PUT") { 11 | withAuth(req, res, async () => { 12 | const { error } = validateConfig(req.body); 13 | if (error) return res.status(400).send(error.details[0].message); 14 | 15 | let config = await Config.find(); 16 | if (!config.length) { 17 | let config = new Config({ 18 | mailing: req.body.mailing, 19 | }); 20 | await config.save(); 21 | return res.send(config); 22 | } 23 | config[0].mailing = req.body.mailing; 24 | config[0].save(); 25 | 26 | if (!config) 27 | return res 28 | .status(404) 29 | .send("The config with the given ID was not found."); 30 | 31 | res.send(config); 32 | }); 33 | } else { 34 | res.setHeader("Allow", ["GET", "PUT"]); 35 | res.status(405).end(`Method Not Allowed`); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /pages/api/users/create.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import User, { validateUser } from "../../../models/user"; 3 | const bcrypt = require("bcrypt"); 4 | const _ = require("lodash"); 5 | 6 | export default async (req, res) => { 7 | await db(); 8 | switch (req.method) { 9 | case "POST": 10 | const { error } = validateUser(req.body); 11 | if (error) return res.status(400).send(error.details[0].message); 12 | 13 | let user = await User.findOne({ email: req.body.email.toLowerCase() }); 14 | if (user && user.password) 15 | return res.status(400).send("User already registered."); 16 | else user = new User(_.pick(req.body, ["name", "email", "password"])); 17 | user.password = req.body.password; 18 | user.name = req.body.name; 19 | 20 | const salt = await bcrypt.genSalt(10); 21 | user.password = await bcrypt.hash(user.password, salt); 22 | await user.save(); 23 | const token = user.generateAuthToken(); 24 | 25 | res 26 | .setHeader("x-auth-token", token) 27 | .send(_.pick(user, ["_id", "name", "email"])); 28 | break; 29 | default: 30 | res.setHeader("Allow", ["POST"]); 31 | res.status(405).end(`Method Not Allowed`); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /pages/api/users/index.js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import withAuth from "../../../utils/auth"; 3 | import admin from "../../../utils/admin"; 4 | import User, { validateUser } from "../../../models/user"; 5 | const _ = require("lodash"); 6 | 7 | export default async (req, res) => { 8 | await db(); 9 | switch (req.method) { 10 | case "GET": 11 | withAuth(req, res, async () => { 12 | admin(req, res); 13 | const users = await User.find().sort("name"); 14 | res.send(users); 15 | }); 16 | break; 17 | case "POST": 18 | withAuth(req, res, async () => { 19 | admin(req, res); 20 | const { error } = validateUser(req.body); 21 | if (error) return res.status(400).send(error.details[0].message); 22 | 23 | let user = await User.findOne({ email: req.body.email.toLowerCase() }); 24 | if (user) return res.status(400).send("User already registered."); 25 | 26 | user = new User({ email: req.body.email }); 27 | await user.save(); 28 | 29 | res.send(_.pick(user, ["_id", "email"])); 30 | }); 31 | break; 32 | default: 33 | res.setHeader("Allow", ["GET", "POST"]); 34 | res.status(405).end(`Method Not Allowed`); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /public/assets/js/main.js: -------------------------------------------------------------------------------- 1 | const showMenu = (headerToggle, navbarId) => { 2 | const toggleBtn = document.getElementById(headerToggle), 3 | nav = document.getElementById(navbarId); 4 | 5 | if (headerToggle && navbarId) { 6 | if (toggleBtn) { 7 | toggleBtn.addEventListener("click", () => { 8 | nav.classList.toggle("show-menu"); 9 | toggleBtn.classList.toggle("bx-x"); 10 | }); 11 | } 12 | } 13 | }; 14 | showMenu("header-toggle", "navbar"); 15 | 16 | const linkColor = document.querySelectorAll(".nav_link"); 17 | 18 | function colorLink() { 19 | linkColor.forEach((l) => l.classList.remove("active")); 20 | this.classList.add("active"); 21 | } 22 | 23 | linkColor.forEach((l) => l.addEventListener("click", colorLink)); 24 | 25 | const selected = document.querySelector(".selected"); 26 | const optionsContainer = document.querySelector(".options-container"); 27 | 28 | const optionsList = document.querySelectorAll(".option"); 29 | if (selected) { 30 | selected.addEventListener("click", () => { 31 | optionsContainer.classList.toggle("active"); 32 | }); 33 | } 34 | 35 | optionsList.forEach((o) => { 36 | if (o) { 37 | o.addEventListener("click", () => { 38 | selected.innerHTML = o.querySelector("label").innerHTML; 39 | optionsContainer.classList.remove("active"); 40 | }); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /pages/api/users/[id].js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import withAuth from "../../../utils/auth"; 3 | import admin from "../../../utils/admin"; 4 | import User, { validateUser } from "../../../models/user"; 5 | 6 | export default async (req, res) => { 7 | await db(); 8 | switch (req.method) { 9 | case "PUT": 10 | withAuth(req, res, async () => { 11 | admin(req, res); 12 | const { error } = validateUser(req.body); 13 | if (error) return res.status(400).send(error.details[0].message); 14 | 15 | const user = await User.findByIdAndUpdate(req.query.id, { 16 | email: req.body.email.toLowerCase(), 17 | }); 18 | 19 | if (!user) 20 | return res 21 | .status(404) 22 | .send("The user with the given ID was not found."); 23 | 24 | res.send(user); 25 | }); 26 | break; 27 | case "DELETE": 28 | withAuth(req, res, async () => { 29 | admin(req, res); 30 | const user = await User.findByIdAndRemove(req.query.id); 31 | 32 | if (!user) 33 | return res 34 | .status(404) 35 | .send("The user with the given ID was not found."); 36 | 37 | res.send(user); 38 | }); 39 | break; 40 | default: 41 | res.setHeader("Allow", ["DELETE", "PUT"]); 42 | res.status(405).end(`Method Not Allowed`); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /pages/api/systems/[id].js: -------------------------------------------------------------------------------- 1 | import db from "../../../utils/db"; 2 | import System, { validateSystem } from "../../../models/system"; 3 | import withAuth from "../../../utils/auth"; 4 | 5 | export default async (req, res) => { 6 | await db(); 7 | switch (req.method) { 8 | case "GET": 9 | try { 10 | const system = await System.findById(req.query.id); 11 | if (!system) 12 | return res 13 | .status(404) 14 | .send("The system with the given ID was not found."); 15 | 16 | res.status(200).send(system); 17 | } catch { 18 | res.status(400).send("Invalid Object Id"); 19 | } 20 | break; 21 | case "PUT": 22 | await withAuth(req, res, async () => { 23 | const { error } = validateSystem(req.body); 24 | if (error) return res.status(400).send(error.details[0].message); 25 | 26 | const system_updated = await System.findByIdAndUpdate( 27 | req.query.id, 28 | { 29 | name: req.body.name, 30 | url: req.body.url, 31 | type: req.body.type, 32 | }, 33 | { new: true } 34 | ); 35 | 36 | if (!system_updated) 37 | return res 38 | .status(404) 39 | .send("The system with the given ID was not found."); 40 | 41 | res.send(system_updated); 42 | }); 43 | break; 44 | case "DELETE": 45 | await withAuth(req, res, async () => { 46 | const system_deleted = await System.findByIdAndRemove(req.query.id); 47 | 48 | if (!system_deleted) 49 | return res 50 | .status(404) 51 | .send("The system with the given ID was not found."); 52 | 53 | res.send(system_deleted); 54 | }); 55 | break; 56 | default: 57 | res.setHeader("Allow", ["GET", "DELETE", "PUT"]); 58 | res.status(405).end(`Method Not Allowed`); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About UpStats 2 | 3 | There are many great Opensource, Hosted Solutions for Status Page, however, I didn't find one that really suit my needs so we created this enhanced one. I want to create a Status Page so amazing that it'll be the last one you ever need. I think this is the One. 4 | 5 | Here's why: 6 | * Managing your site's right from your Mobile is now easier with Mobile First Dashboard. 7 | * You don't need to do the task, you can appoint others to do your Tasks. 8 | * Let your user to know about your System Status directly via Email. 9 | * Don't let your Telegram Bots simply Down 10 | 11 | Since your needs maybe different.So I'll be adding more in the near future. You may also suggest changes by forking this repo and creating a pull request or opening an issue. Thanks to all the people have have contributed to expanding UpStats! 12 | 13 | ## Built with 14 | 15 | * [Tailwind](https://tailwindcss.com) 16 | * [NextJS](https://nextjs.org/) 17 | * [Typescript](https://www.typescriptlang.org) 18 | * [GramJS](https://github.com/gram-js/gramjs) 19 | 20 | ## Getting Started 21 | 22 | 1. Clone the Repo 23 | ``` 24 | git clone https://github.com/UpStats/UpStats.git 25 | ``` 26 | 2. Open next.config.js file and replace with your Details 27 | ``` 28 | MONGODB_URI - Get from MongoDB.com 29 | jwtPrivateKey - can be Random word like Password, make sure it's not Public 30 | admin_email - Your Email, Required to access Dashboard 31 | ``` 32 | 3. Open public/assets/ and replace Favicon.ico 33 | 4. Open public/assets/img/ and replace with your images. 34 | 5. Deploy [UpStats Worker](https://github.com/upstats/worker#getting-started) 35 | 36 | You can deploy UpStats on Vercel 37 | 38 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/Upstats/UpStats) 39 | 40 | ## Demo 41 | ![Dashboard](https://raw.githubusercontent.com/Upstats/UpStats/master/screenshot/homepage.png) 42 |
UpStats Homepage
43 | 44 | ![Dashboard](https://raw.githubusercontent.com/Upstats/UpStats/master/screenshot/dashboard.png) 45 |
UpStats Admin Panel
46 | 47 | ## Contributing 48 | 49 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 50 | 51 | 1. Fork the Project 52 | 2. Create your Feature Branch 53 | 3. Commit your Changes 54 | 4. Push to the Branch 55 | 5. Open a Pull Request 56 | 6. That's it 57 | 58 | ## License 59 | 60 | Distributed under the GNU GPLV3 License. See `LICENSE` for more information. 61 | 62 | ## About US 63 | 64 | UpStats is developed by [ToolsHD](https://gitHub.com/ToolsHD) and [Gowtham](https://github.com/Gowtham2003) 65 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '16 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | main.root { 2 | scroll-behavior: smooth; 3 | font-family: "Open Sans", sans-serif; 4 | } 5 | 6 | /*nav {*/ 7 | /*font-family: "Open Sans", sans-serif;*/ 8 | /*}*/ 9 | 10 | .nav-brand { 11 | display: block; 12 | width: 50px; 13 | height: auto; 14 | border-radius: 50%; 15 | margin: auto; 16 | } 17 | 18 | /*span {*/ 19 | /*width: 100%;*/ 20 | /*}*/ 21 | 22 | #global-status { 23 | width: 90%; 24 | height: 3em; 25 | background: #3747d4; 26 | place-items: center; 27 | color: #ffffff; 28 | font-size: 20px; 29 | font-family: "Open Sans", sans-serif; 30 | letter-spacing: 3px; 31 | margin-top: 0px; 32 | margin-left: 10px; 33 | border-radius: 5px; 34 | border: none; 35 | } 36 | 37 | #status { 38 | width: 90%; 39 | height: 5em; 40 | background-color: white; 41 | color: black; 42 | margin: 30px 0px 10px 11px; 43 | border-radius: 5px; 44 | left: 0; 45 | right: 0; 46 | letter-spacing: 3px; 47 | place-items: center; 48 | } 49 | 50 | div.monitor { 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); 52 | } 53 | 54 | .site-status { 55 | padding-left: 50%; 56 | } 57 | 58 | #footer-img { 59 | width: 50px; 60 | border-radius: 50%; 61 | } 62 | 63 | #scroll-to-top { 64 | display: none; 65 | position: fixed; 66 | bottom: 20px; 67 | right: 30px; 68 | z-index: 99; 69 | border: none; 70 | outline: none; 71 | background-color: #3747d4; 72 | color: white; 73 | cursor: pointer; 74 | padding: 15px; 75 | border-radius: 10px; 76 | } 77 | 78 | #scroll-to-top:hover { 79 | background-color: #000000; 80 | } 81 | 82 | @media (min-width: 768px) and (max-width: 1024px) { 83 | .site-status { 84 | padding: 0 0 0 70%; 85 | } 86 | 87 | #global-status, 88 | #status { 89 | margin: 30px 0 0 51px; 90 | } 91 | } 92 | 93 | .email-subscription { 94 | max-width: 400px; 95 | margin: auto; 96 | background: #fff; 97 | text-align: center; 98 | } 99 | 100 | .email-subscription .newsletter { 101 | margin: 20px 27px; 102 | font-family: "Open Sans", sans-serif; 103 | } 104 | 105 | .email-subscription .field { 106 | width: 100%; 107 | height: 45px; 108 | display: flex; 109 | position: relative; 110 | } 111 | 112 | .newsletter .field #email { 113 | width: 100%; 114 | height: 100%; 115 | border: 1px solid lightgrey; 116 | border-radius: 5px; 117 | padding-left: 15px; 118 | font-size: 18px; 119 | outline: none; 120 | } 121 | 122 | #submit { 123 | margin-top: 10px; 124 | width: 100%; 125 | height: 45px; 126 | border: none; 127 | outline: none; 128 | border-radius: 5px; 129 | background-color: #3747d4; 130 | color: #ffffff; 131 | font-size: 18px; 132 | font-weight: 500; 133 | letter-spacing: 1px; 134 | cursor: pointer; 135 | } 136 | -------------------------------------------------------------------------------- /public/assets/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | scroll-behavior: smooth; 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | nav { 7 | font-family: 'Open Sans', sans-serif; 8 | } 9 | 10 | .nav-brand { 11 | display: block; 12 | width: 50px; 13 | height: auto; 14 | border-radius: 50%; 15 | margin: auto; 16 | } 17 | 18 | span { 19 | width: 100%; 20 | } 21 | 22 | #global-status { 23 | width: 90%; 24 | height: 3em; 25 | background: #3747D4; 26 | place-items: center; 27 | color: #ffffff; 28 | font-size: 20px; 29 | font-family: 'Open Sans', sans-serif; 30 | letter-spacing: 3px; 31 | margin-top: 0px; 32 | margin-left: 10px; 33 | border-radius: 5px; 34 | border: none; 35 | } 36 | 37 | #status { 38 | width: 90%; 39 | height: 5em; 40 | background-color: white; 41 | color: black; 42 | margin: 30px 0px 10px 11px; 43 | border-radius: 5px; 44 | left: 0; 45 | right: 0; 46 | letter-spacing: 3px; 47 | place-items: center; 48 | } 49 | 50 | div.monitor { 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); 52 | } 53 | 54 | .site-status { 55 | padding-left: 50%; 56 | } 57 | 58 | #footer-img { 59 | width: 50px; 60 | border-radius: 50%; 61 | } 62 | 63 | #scroll-to-top { 64 | display: none; 65 | position: fixed; 66 | bottom: 20px; 67 | right: 30px; 68 | z-index: 99; 69 | border: none; 70 | outline: none; 71 | background-color: #3747D4; 72 | color: white; 73 | cursor: pointer; 74 | padding: 15px; 75 | border-radius: 10px; 76 | } 77 | 78 | #scroll-to-top:hover { 79 | background-color: #000000; 80 | } 81 | 82 | @media (min-width: 768px) and (max-width: 1024px) { 83 | .site-status { 84 | padding: 0 0 0 70%; 85 | } 86 | 87 | #global-status, 88 | #status { 89 | margin: 30px 0 0 51px; 90 | } 91 | } 92 | 93 | .email-subscription { 94 | max-width: 400px; 95 | margin: auto; 96 | background: #fff; 97 | text-align: center; 98 | font-family: 'Open Sans', sans-serif; 99 | } 100 | 101 | .email-subscription .newsletter { 102 | margin: 20px 27px; 103 | font-family: 'Open Sans', sans-serif; 104 | } 105 | 106 | .email-subscription .field { 107 | width: 100%; 108 | height: 45px; 109 | display: flex; 110 | position: relative; 111 | } 112 | 113 | .newsletter .field #email { 114 | width: 100%; 115 | height: 100%; 116 | border: 1px solid lightgrey; 117 | border-radius: 5px; 118 | padding-left: 15px; 119 | font-size: 18px; 120 | outline: none; 121 | } 122 | 123 | #submit { 124 | margin-top: 10px; 125 | width: 100%; 126 | height: 45px; 127 | border: none; 128 | outline: none; 129 | border-radius: 5px; 130 | background-color: #3747D4; 131 | color: #ffffff; 132 | font-size: 18px; 133 | font-weight: 500; 134 | letter-spacing: 1px; 135 | cursor: pointer; 136 | 137 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | main.root { 2 | scroll-behavior: smooth; 3 | font-family: "Open Sans", sans-serif; 4 | } 5 | 6 | /*nav {*/ 7 | /*font-family: "Open Sans", sans-serif;*/ 8 | /*}*/ 9 | 10 | .nav-brand { 11 | display: block; 12 | width: 50px; 13 | height: auto; 14 | border-radius: 50%; 15 | margin: auto; 16 | } 17 | 18 | /*span {*/ 19 | /*width: 100%;*/ 20 | /*}*/ 21 | 22 | #global-status { 23 | width: 90%; 24 | height: 3em; 25 | background: #3747d4; 26 | place-items: center; 27 | color: #ffffff; 28 | font-size: 20px; 29 | font-family: "Open Sans", sans-serif; 30 | letter-spacing: 3px; 31 | margin-top: 0px; 32 | margin-left: 10px; 33 | border-radius: 5px; 34 | border: none; 35 | } 36 | 37 | #status { 38 | width: 90%; 39 | height: 5em; 40 | background-color: white; 41 | color: black; 42 | margin: 30px 0px 10px 11px; 43 | border-radius: 5px; 44 | left: 0; 45 | right: 0; 46 | letter-spacing: 3px; 47 | place-items: center; 48 | } 49 | 50 | div.monitor { 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); 52 | } 53 | 54 | .site-status { 55 | padding-left: 50%; 56 | } 57 | 58 | #footer-img { 59 | width: 50px; 60 | border-radius: 50%; 61 | } 62 | 63 | #scroll-to-top { 64 | display: none; 65 | position: fixed; 66 | bottom: 20px; 67 | right: 30px; 68 | z-index: 99; 69 | border: none; 70 | outline: none; 71 | background-color: #3747d4; 72 | color: white; 73 | cursor: pointer; 74 | padding: 15px; 75 | border-radius: 10px; 76 | } 77 | 78 | #scroll-to-top:hover { 79 | background-color: #000000; 80 | } 81 | 82 | @media (min-width: 768px) and (max-width: 1024px) { 83 | .site-status { 84 | padding: 0 0 0 70%; 85 | } 86 | 87 | #global-status, 88 | #status { 89 | margin: 30px 0 0 51px; 90 | } 91 | } 92 | 93 | .email-subscription { 94 | max-width: 400px; 95 | margin: auto; 96 | background: #fff; 97 | text-align: center; 98 | } 99 | 100 | .email-subscription .newsletter { 101 | margin: 20px 27px; 102 | font-family: "Open Sans", sans-serif; 103 | } 104 | 105 | .email-subscription .field { 106 | width: 100%; 107 | height: 45px; 108 | display: flex; 109 | position: relative; 110 | } 111 | 112 | .newsletter .field #email { 113 | width: 100%; 114 | height: 100%; 115 | border: 1px solid lightgrey; 116 | border-radius: 5px; 117 | padding-left: 15px; 118 | font-size: 18px; 119 | outline: none; 120 | } 121 | 122 | #submit { 123 | margin-top: 10px; 124 | width: 100%; 125 | height: 45px; 126 | border: none; 127 | outline: none; 128 | border-radius: 5px; 129 | background-color: #3747d4; 130 | color: #ffffff; 131 | font-size: 18px; 132 | font-weight: 500; 133 | letter-spacing: 1px; 134 | cursor: pointer; 135 | } 136 | -------------------------------------------------------------------------------- /components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | const Layout = (props) => { 5 | const logout = () => { 6 | window.localStorage.removeItem("token"); 7 | window.location = "/login"; 8 | }; 9 | return ( 10 | <> 11 | 12 | 13 | 14 | Dashboard | Site Name 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | <> 27 |
28 |
29 | {/* Website Logo mentioned in Envs */} 30 | 31 | 32 | UP STATS 33 | 34 |
35 | 36 |
37 |
38 |
39 | {/* Nav */} 40 | 69 |
{props.children}
70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default Layout; 77 | -------------------------------------------------------------------------------- /pages/login.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import http from "../services/httpService"; 3 | const tokenKey = "token"; 4 | import { useEffect } from "react"; 5 | import { 6 | Input, 7 | useToast, 8 | Text, 9 | Stack, 10 | Alert, 11 | AlertIcon, 12 | } from "@chakra-ui/react"; 13 | import { useFormik } from "formik"; 14 | import * as Yup from "yup"; 15 | 16 | const Login = (props) => { 17 | const toast = useToast(); 18 | const login = async (email, password) => { 19 | try { 20 | const { data: jwt } = await http.post("/auth", { email, password }); 21 | window.localStorage.setItem(tokenKey, jwt); 22 | window.location = "/admin"; 23 | toast({ 24 | title: "Success", 25 | description: "Redirecting...", 26 | status: "success", 27 | duration: 9000, 28 | isClosable: true, 29 | }); 30 | } catch (ex) { 31 | toast({ 32 | title: "Error", 33 | description: "Cannot Login to Account", 34 | status: "error", 35 | duration: 9000, 36 | isClosable: true, 37 | }); 38 | } 39 | }; 40 | useEffect(() => { 41 | const token = window.localStorage.getItem("token"); 42 | 43 | if (token) { 44 | window.location = "/admin"; 45 | } 46 | }, []); 47 | 48 | const formik = useFormik({ 49 | initialValues: { 50 | email: "", 51 | password: "", 52 | }, 53 | validationSchema: Yup.object({ 54 | email: Yup.string().email().label("Email").required(), 55 | password: Yup.string().label("Password").required(), 56 | }), 57 | onSubmit: (values) => { 58 | login(values.email, values.password); 59 | 60 | //alert(JSON.stringify(values, null, 2)); 61 | }, 62 | }); 63 | return ( 64 |
65 |
66 |

Welcome Back

67 |

Login to continue

68 |
69 | 70 | Email 71 | 83 | {formik.touched.email && formik.errors.email ? ( 84 | 85 | 86 | {formik.errors.email} 87 | 88 | ) : null} 89 | 90 | 91 | Password 92 | 104 | {formik.touched.password && formik.errors.password ? ( 105 | 106 | 107 | {formik.errors.password} 108 | 109 | ) : null} 110 | 111 | {/* Login */} 112 |
113 | 120 |
121 |
122 |
123 |
124 | ); 125 | }; 126 | 127 | export default Login; 128 | -------------------------------------------------------------------------------- /pages/register.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import http from "../services/httpService"; 3 | const tokenKey = "token"; 4 | import { useEffect } from "react"; 5 | import { 6 | Input, 7 | useToast, 8 | Text, 9 | Stack, 10 | Alert, 11 | AlertIcon, 12 | } from "@chakra-ui/react"; 13 | import { useFormik } from "formik"; 14 | import * as Yup from "yup"; 15 | 16 | const Register = (props) => { 17 | const toast = useToast(); 18 | const register = async (creds) => { 19 | try { 20 | const { data: jwt } = await http.post("/users/create", { ...creds }); 21 | window.localStorage.setItem(tokenKey, jwt); 22 | window.location = "/admin"; 23 | toast({ 24 | title: "Success", 25 | description: "Redirecting...", 26 | status: "success", 27 | duration: 9000, 28 | isClosable: true, 29 | }); 30 | } catch (ex) { 31 | toast({ 32 | title: "Error", 33 | description: "Cannot Login to Account", 34 | status: "error", 35 | duration: 9000, 36 | isClosable: true, 37 | }); 38 | } 39 | }; 40 | useEffect(() => { 41 | const token = window.localStorage.getItem("token"); 42 | 43 | if (token) { 44 | window.location = "/admin"; 45 | } 46 | }, []); 47 | 48 | const formik = useFormik({ 49 | initialValues: { 50 | name: "", 51 | email: "", 52 | password: "", 53 | }, 54 | validationSchema: Yup.object({ 55 | name: Yup.string().label("Name").required(), 56 | email: Yup.string().email().label("Email").required(), 57 | password: Yup.string().label("Password").required(), 58 | }), 59 | onSubmit: (values) => { 60 | register(values); 61 | 62 | //alert(JSON.stringify(values, null, 2)); 63 | }, 64 | }); 65 | return ( 66 |
67 |
68 |

Welcome Back

69 |

Login to continue

70 |
71 | 72 | Name 73 | 85 | {formik.touched.name && formik.errors.name ? ( 86 | 87 | 88 | {formik.errors.name} 89 | 90 | ) : null} 91 | 92 | 93 | Email 94 | 106 | {formik.touched.email && formik.errors.email ? ( 107 | 108 | 109 | {formik.errors.email} 110 | 111 | ) : null} 112 | 113 | 114 | Password 115 | 127 | {formik.touched.password && formik.errors.password ? ( 128 | 129 | 130 | {formik.errors.password} 131 | 132 | ) : null} 133 | 134 | {/* Register */} 135 |
136 | 143 |
144 |
145 |
146 |
147 | ); 148 | }; 149 | 150 | export default Register; 151 | -------------------------------------------------------------------------------- /public/assets/css/dashboard.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); 2 | 3 | *, 4 | ::before, 5 | ::after { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | margin: 3.5rem 0 0 0; 11 | padding: 1rem 1rem 0; 12 | font-family: 'Poppins', sans-serif; 13 | font-size: .938rem; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | } 19 | 20 | .header { 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | background-color: #FFFFFF; 26 | box-shadow: 0 1px 0 rgba(22, 8, 43, 0.1); 27 | padding: 0 1rem; 28 | z-index: 100; 29 | } 30 | 31 | .header_container { 32 | display: flex; 33 | align-items: center; 34 | height: 3.5rem; 35 | justify-content: space-between; 36 | } 37 | 38 | .header_img { 39 | width: 35px; 40 | height: 35px; 41 | border-radius: 50%; 42 | } 43 | 44 | .header_logo { 45 | color: #19181B; 46 | font-weight: 500; 47 | display: none; 48 | } 49 | 50 | .header_icon, 51 | .header_toggle { 52 | font-size: 1.2rem; 53 | } 54 | 55 | .header_toggle { 56 | color: #19181B; 57 | cursor: pointer; 58 | } 59 | 60 | 61 | .nav { 62 | position: fixed; 63 | top: 0; 64 | left: -100%; 65 | height: 100vh; 66 | padding: 1rem 1rem 0; 67 | background-color: #FFFFFF; 68 | box-shadow: 1px 0 0 rgba(22, 8, 43, 0.1); 69 | z-index: 100; 70 | transition: .4s; 71 | } 72 | 73 | .nav_container { 74 | height: 100%; 75 | display: flex; 76 | flex-direction: column; 77 | justify-content: space-between; 78 | padding-bottom: 3rem; 79 | overflow: auto; 80 | scrollbar-width: none; 81 | 82 | } 83 | 84 | .nav_container::-webkit-scrollbar { 85 | display: none; 86 | } 87 | 88 | .nav_logo { 89 | font-weight: var(--font-semi-bold); 90 | margin-bottom: 2.5rem; 91 | } 92 | 93 | .nav_list, 94 | .nav_items { 95 | display: grid; 96 | } 97 | 98 | .nav_list { 99 | row-gap: 2.5rem; 100 | } 101 | 102 | .nav_items { 103 | row-gap: 1.5rem; 104 | } 105 | 106 | .nav_mainmenu { 107 | font-size: .938rem; 108 | text-transform: uppercase; 109 | letter-spacing: .1rem; 110 | color: #A5A1AA; 111 | font-weight: 900; 112 | } 113 | 114 | .nav_link { 115 | display: flex; 116 | align-items: center; 117 | color: #3747D4; 118 | } 119 | 120 | .nav_link:hover { 121 | color: #000000; 122 | } 123 | 124 | .nav_icon { 125 | font-size: 1.2rem; 126 | margin-right: .5rem; 127 | } 128 | 129 | .nav_name { 130 | font-size: .75rem; 131 | font-weight: 500; 132 | white-space: nowrap; 133 | } 134 | 135 | .nav_logout { 136 | margin-top: 5rem; 137 | } 138 | 139 | .show-menu { 140 | left: 0; 141 | } 142 | 143 | .active { 144 | color: #000000; 145 | } 146 | 147 | @media screen and (min-width: 768px) { 148 | body { 149 | padding: 1rem 3rem 0 6rem; 150 | } 151 | 152 | .header { 153 | padding: 0 3rem 0 6rem; 154 | } 155 | 156 | .header_container { 157 | height: calc(3.5rem + .5rem); 158 | } 159 | 160 | .header_toggle { 161 | display: none; 162 | } 163 | 164 | .header_logo { 165 | display: block; 166 | } 167 | 168 | .header_img { 169 | width: 40px; 170 | height: 40px; 171 | order: 1; 172 | } 173 | 174 | .nav { 175 | left: 0; 176 | padding: 1.2rem 1.5rem 0; 177 | width: 68px; 178 | } 179 | 180 | .nav_items { 181 | row-gap: 1.7rem; 182 | } 183 | 184 | .nav_icon { 185 | font-size: 1.3rem; 186 | } 187 | 188 | .nav_logo-name, 189 | .nav_name, 190 | .nav_mainmenu { 191 | opacity: 0; 192 | transition: .3s; 193 | } 194 | 195 | .nav:hover { 196 | width: 219px; 197 | } 198 | 199 | .nav:hover .nav_logo-name { 200 | opacity: 1; 201 | } 202 | 203 | .nav:hover .nav_mainmenu { 204 | opacity: 1; 205 | } 206 | 207 | .nav:hover .nav_name { 208 | opacity: 1; 209 | } 210 | } 211 | 212 | .users-status { 213 | color: #081DFF; 214 | } 215 | 216 | .status-items-manage { 217 | background: #fff; 218 | margin-top: 20px; 219 | margin-bottom: 3px; 220 | 221 | } 222 | 223 | .status-items-manage .items { 224 | color: black; 225 | display: flex; 226 | padding: 12px 17px; 227 | background: white; 228 | border-radius: 3px; 229 | align-items: center; 230 | border: solid 1px; 231 | justify-content: space-between; 232 | } 233 | 234 | .status-items-manage .items .site-title { 235 | font-size: 18px; 236 | font-weight: 400; 237 | } 238 | 239 | .status-items-manage .items .i { 240 | font-size: 16px; 241 | } 242 | 243 | .bx-trash-alt { 244 | color: red; 245 | } 246 | 247 | .select-box { 248 | display: flex; 249 | width: 250px; 250 | flex-direction: column; 251 | } 252 | 253 | .select-box .options-container { 254 | background: #2f3640; 255 | color: #f5f6fa; 256 | max-height: 0; 257 | width: 100%; 258 | opacity: 0; 259 | transition: all 0.4s; 260 | border-radius: 8px; 261 | overflow: hidden; 262 | order: 1; 263 | } 264 | 265 | .selected { 266 | margin-top: 10px; 267 | background: #2f3640; 268 | border-radius: 8px; 269 | margin-bottom: 8px; 270 | color: #f5f6fa; 271 | position: relative; 272 | order: 0; 273 | } 274 | 275 | .selected::after { 276 | content: ""; 277 | background: url("/assets/img/arrow-down.svg"); 278 | background-size: contain; 279 | background-repeat: no-repeat; 280 | position: absolute; 281 | height: 100%; 282 | width: 32px; 283 | right: 10px; 284 | top: 5px; 285 | transition: all 0.4s; 286 | } 287 | 288 | .select-box .options-container.active { 289 | max-height: 240px; 290 | opacity: 1; 291 | overflow-y: scroll; 292 | } 293 | 294 | .select-box .options-container.active+.selected::after { 295 | transform: rotateX(180deg); 296 | top: -6px; 297 | } 298 | 299 | .select-box .options-container::-webkit-scrollbar { 300 | width: 8px; 301 | background: #0d141f; 302 | border-radius: 0 8px 8px 0; 303 | } 304 | 305 | .select-box .options-container::-webkit-scrollbar-thumb { 306 | background: #525861; 307 | border-radius: 0 8px 8px 0; 308 | } 309 | 310 | .select-box .option, 311 | .selected { 312 | padding: 12px 24px; 313 | cursor: pointer; 314 | } 315 | 316 | .select-box .option:hover { 317 | background: #414b57; 318 | } 319 | 320 | .select-box label { 321 | cursor: pointer; 322 | } 323 | 324 | .select-box .option .radio { 325 | display: none; 326 | } 327 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { 3 | Box, 4 | Center, 5 | VStack, 6 | Text, 7 | Flex, 8 | Spacer, 9 | Input, 10 | FormControl, 11 | FormLabel, 12 | useToast, 13 | } from "@chakra-ui/react"; 14 | import http from "../services/httpService"; 15 | 16 | import { CheckCircleIcon, WarningIcon, EmailIcon } from "@chakra-ui/icons"; 17 | import { useState } from "react"; 18 | import db from "../utils/db"; 19 | import System from "../models/system"; 20 | import Config from "../models/config"; 21 | 22 | export default function Home({ status, systems, config }) { 23 | console.log(status); 24 | console.log(systems); 25 | const [email, setEmail] = useState(""); 26 | const toast = useToast(); 27 | const handleSubmit = async (email) => { 28 | try { 29 | await http.post("/subs", { email: email }); 30 | toast({ 31 | title: "Success", 32 | description: "Successfully Subscribed ", 33 | status: "success", 34 | duration: 9000, 35 | isClosable: true, 36 | }); 37 | } catch (ex) { 38 | toast({ 39 | title: "Error", 40 | description: "Submit Unsuccessful", 41 | status: "error", 42 | duration: 9000, 43 | isClosable: true, 44 | }); 45 | } 46 | }; 47 | return ( 48 | <> 49 | 50 | 51 | 55 | 56 | 57 | UP Stats 58 | 59 | 60 |
61 |
62 | 68 |
69 |
70 |
71 | 72 | {status.operational 73 | ? "All Systems Operational" 74 | : `${status.outageCount} Systems Outage`} 75 | 76 |
77 |
78 | 79 | {systems.map((system) => ( 80 | 89 | {system.name} 90 | 91 | {system.status === "up" && ( 92 | 93 | )} 94 | 95 | {system.status === "down" && ( 96 | 97 | )} 98 | 99 | ))} 100 | 101 |
102 | {config.mailing ? ( 103 | 104 |

Want to see Back in action?

105 |

106 | Subscribe via Email and
107 | Get notified about the System Status 108 |

109 |
110 | 111 | Email address 112 | setEmail(e.target.value)} 116 | /> 117 | handleSubmit(email)} 127 | > 128 | 129 | Subscribe 130 | 131 | 132 |
133 |
134 | ) : ( 135 | "" 136 | )} 137 | 148 |
149 | 165 |
166 |
167 | 168 | ); 169 | } 170 | export async function getStaticProps(context) { 171 | const getSystems = async () => { 172 | const result = await System.find().sort("name"); 173 | return result.map((doc) => { 174 | const system = doc.toObject(); 175 | 176 | system._id = system._id.toString(); 177 | return system; 178 | }); 179 | }; 180 | 181 | const getStatus = async () => { 182 | let isOperational = false; 183 | let outageCount = 0; 184 | const systems = await System.find().lean(); 185 | for (let system of systems) { 186 | isOperational = system.status === "up" ? true : false; 187 | if (!isOperational) outageCount += 1; 188 | } 189 | return { 190 | operational: isOperational, 191 | ...(!isOperational && { outageCount }), 192 | }; 193 | }; 194 | await db(); 195 | const systems = await getSystems(); 196 | const status = await getStatus(); 197 | const config = await Config.findOne().lean(); 198 | config._id = config._id.toString(); 199 | 200 | if (!status && !systems) { 201 | return { 202 | notFound: true, 203 | }; 204 | } 205 | let data = JSON.stringify({ 206 | status: status, 207 | systems: systems, 208 | config: config, 209 | }); 210 | return { 211 | props: JSON.parse(data), 212 | revalidate: 1, 213 | }; 214 | } 215 | -------------------------------------------------------------------------------- /pages/admin/manage-admin.jsx: -------------------------------------------------------------------------------- 1 | import Layout from "../../components/Layout"; 2 | import { 3 | Input, 4 | Alert, 5 | AlertIcon, 6 | Text, 7 | Stack, 8 | useToast, 9 | } from "@chakra-ui/react"; 10 | 11 | import { useEffect, useState } from "react"; 12 | 13 | import http from "../../services/httpService"; 14 | import { EditIcon, DeleteIcon } from "@chakra-ui/icons"; 15 | import { useRouter } from "next/router"; 16 | import * as Yup from "yup"; 17 | import { useFormik, FormikProvider } from "formik"; 18 | import { create } from "apisauce"; 19 | 20 | const Test = (props) => { 21 | const api = create({ 22 | baseURL: `/api`, 23 | }); 24 | const toast = useToast(); 25 | 26 | const router = useRouter(); 27 | const [users, setUsers] = useState([]); 28 | const [isLoggedIn, setIsLoggedIn] = useState(false); 29 | 30 | const [currentEditUser, setCurrentEditUser] = useState({ 31 | email: "", 32 | }); 33 | const loadUsers = async () => { 34 | try { 35 | const { data } = await http.get("/users"); 36 | setUsers(data); 37 | } catch (e) { 38 | toast({ 39 | title: "Error", 40 | description: "Error Loading Users", 41 | status: "error", 42 | duration: 9000, 43 | isClosable: true, 44 | }); 45 | } 46 | }; 47 | useEffect(() => { 48 | const token = window.localStorage.getItem("token"); 49 | http.setJwt(token); 50 | 51 | if (!token) { 52 | setIsLoggedIn(false); 53 | toast({ 54 | title: "Error", 55 | description: "Redirecting to Login Page", 56 | status: "warning", 57 | duration: 9000, 58 | isClosable: true, 59 | }); 60 | router.push("/login"); 61 | } else setIsLoggedIn(true); 62 | }, []); 63 | 64 | useEffect(() => { 65 | loadUsers(); 66 | }, []); 67 | 68 | const handleDelete = async (user) => { 69 | const originalUsers = users; 70 | const newUsers = originalUsers.filter((s) => s._id !== user._id); 71 | 72 | setUsers(newUsers); 73 | try { 74 | await http.delete(`/users/${user._id}`); 75 | } catch (ex) { 76 | if (ex.response && ex.response.status === 404) 77 | toast({ 78 | title: "Error", 79 | description: "User May be Already Deleted", 80 | status: "error", 81 | duration: 9000, 82 | isClosable: true, 83 | }); 84 | setUsers(originalUsers); 85 | } 86 | }; 87 | const handleAdd = async (user) => { 88 | try { 89 | const { data } = await api.post("/users", user, { 90 | headers: localStorage.getItem("token"), 91 | }); 92 | setUsers([...users, data]); 93 | } catch (ex) { 94 | toast({ 95 | title: "Error", 96 | description: "Submit Unsuccessful", 97 | status: "error", 98 | duration: 9000, 99 | isClosable: true, 100 | }); 101 | } 102 | }; 103 | 104 | const handleEdit = async () => { 105 | const originalUsers = users; 106 | let newUsers = [...users]; 107 | const idx = newUsers.findIndex((sys) => sys._id === currentEditUser._id); 108 | newUsers[idx] = { ...currentEditUser }; 109 | 110 | setUsers(newUsers); 111 | try { 112 | await http.put(`/users/${currentEditUser._id}`, { 113 | email: currentEditUser.email, 114 | }); 115 | setCurrentEditUser({ email: "" }); 116 | } catch (ex) { 117 | toast({ 118 | title: "Error", 119 | description: "Error Updating The User", 120 | status: "error", 121 | duration: 9000, 122 | isClosable: true, 123 | }); 124 | setUsers(originalUsers); 125 | setCurrentEditUser({ email: "" }); 126 | } 127 | }; 128 | const formik = useFormik({ 129 | initialValues: { 130 | email: "", 131 | }, 132 | validationSchema: Yup.object({ 133 | email: Yup.string().label("Email").email().required("Required"), 134 | }), 135 | onSubmit: (values) => { 136 | handleAdd(values); 137 | }, 138 | }); 139 | return ( 140 | 141 | 142 | <> 143 | {isLoggedIn ? ( 144 | <> 145 | {/* CRUD Status List */} 146 |
147 |

New Admin

148 |
149 | 150 | Email 151 | 165 | {formik.touched.email && formik.errors.email ? ( 166 | 167 | 168 | {formik.errors.email} 169 | 170 | ) : null} 171 | 172 | {/* Add */} 173 |
174 | 181 |
182 |
183 | {users.map((user) => ( 184 |
185 |
186 | {user.name} 187 |
188 | { 191 | setCurrentEditUser(user); 192 | }} 193 | /> 194 | { 198 | handleDelete(user); 199 | }} 200 | /> 201 |
202 |
203 |
204 | ))} 205 | {/* End */} 206 | {currentEditUser.email ? ( 207 |
208 | 209 |

210 | Edit User 211 |

212 | 213 | Email 214 | 221 | setCurrentEditUser({ 222 | ...currentEditUser, 223 | email: e.target.value, 224 | }) 225 | } 226 | /> 227 | 228 |
229 | {/* Add */} 230 |
231 | 238 |
239 |
240 | ) : ( 241 | "" 242 | )} 243 |
244 | 245 | ) : ( 246 | "" 247 | )} 248 | 249 |
250 |
251 | ); 252 | }; 253 | 254 | export default Test; 255 | -------------------------------------------------------------------------------- /pages/admin/index.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | Alert, 4 | AlertIcon, 5 | Text, 6 | Stack, 7 | useToast, 8 | Radio, 9 | RadioGroup, 10 | Switch, 11 | } from "@chakra-ui/react"; 12 | import { useEffect, useState } from "react"; 13 | import http from "../../services/httpService"; 14 | import { EditIcon, DeleteIcon } from "@chakra-ui/icons"; 15 | import { useRouter } from "next/router"; 16 | import * as Yup from "yup"; 17 | import { useFormik, Field, FormikProvider } from "formik"; 18 | import React from "react"; 19 | import { create } from "apisauce"; 20 | import Layout from "../../components/Layout"; 21 | 22 | export default function Dashboard() { 23 | const api = create({ 24 | baseURL: "/api", 25 | }); 26 | 27 | const toast = useToast(); 28 | 29 | const router = useRouter(); 30 | const [systems, setSystems] = useState([]); 31 | const [isLoggedIn, setIsLoggedIn] = useState(false); 32 | const [mailing, setMailing] = useState(false); 33 | 34 | const [currentEditSystem, setCurrentEditSystem] = useState({ 35 | name: "", 36 | url: "", 37 | }); 38 | const [subsCount, setSubsCount] = useState(0); 39 | const loadSystems = async () => { 40 | try { 41 | const { data } = await http.get("/systems"); 42 | setSystems(data); 43 | } catch (e) { 44 | toast({ 45 | title: "Error", 46 | description: "Error Loading Systems", 47 | status: "error", 48 | duration: 9000, 49 | isClosable: true, 50 | }); 51 | } 52 | }; 53 | const loadConfig = async () => { 54 | try { 55 | const { data } = await http.get("/config"); 56 | setMailing(data.mailing); 57 | } catch (e) { 58 | console.log("Error Loading Config"); 59 | } 60 | }; 61 | const loadCount = async () => { 62 | try { 63 | const { data } = await http.get("/subs"); 64 | 65 | setSubsCount(data.length); 66 | } catch (e) { 67 | toast({ 68 | title: "Error", 69 | description: "Error Loading Subs Count", 70 | status: "error", 71 | duration: 9000, 72 | isClosable: true, 73 | }); 74 | } 75 | }; 76 | useEffect(() => { 77 | const token = window.localStorage.getItem("token"); 78 | http.setJwt(token); 79 | 80 | if (!token) { 81 | setIsLoggedIn(false); 82 | toast({ 83 | title: "Error", 84 | description: "Redirecting to Login Page", 85 | status: "warning", 86 | duration: 9000, 87 | isClosable: true, 88 | }); 89 | router.push("/login"); 90 | } else setIsLoggedIn(true); 91 | }, []); 92 | useEffect(() => { 93 | loadSystems(); 94 | }, []); 95 | 96 | useEffect(() => { 97 | loadCount(); 98 | }, []); 99 | useEffect(() => { 100 | loadConfig(); 101 | }, []); 102 | const handleDelete = async (system) => { 103 | const originalSystems = systems; 104 | const newSystems = originalSystems.filter((s) => s._id !== system._id); 105 | 106 | setSystems(newSystems); 107 | try { 108 | await http.delete(`/systems/${system._id}`); 109 | } catch (ex) { 110 | if (ex.response && ex.response.status === 404) 111 | toast({ 112 | title: "Error", 113 | description: "System May be Already Deleted", 114 | status: "error", 115 | duration: 9000, 116 | isClosable: true, 117 | }); 118 | setSystems(originalSystems); 119 | } 120 | }; 121 | const handleAdd = async (system) => { 122 | try { 123 | const { data } = await api.post("/systems", system, { 124 | headers: localStorage.getItem("token"), 125 | }); 126 | setSystems([...systems, data]); 127 | } catch (ex) { 128 | toast({ 129 | title: "Error", 130 | description: "Submit Unsuccessful", 131 | status: "error", 132 | duration: 9000, 133 | isClosable: true, 134 | }); 135 | } 136 | }; 137 | 138 | const formik = useFormik({ 139 | initialValues: { 140 | name: "", 141 | url: "", 142 | type: "web", 143 | }, 144 | validationSchema: Yup.object({ 145 | name: Yup.string() 146 | .max(15, "Must be 15 characters or less") 147 | .required("Required"), 148 | url: Yup.string().required("Required"), 149 | type: Yup.string(), 150 | }), 151 | onSubmit: (values) => { 152 | handleAdd(values); 153 | //alert(JSON.stringify(values, null, 2)); 154 | }, 155 | }); 156 | const handleEdit = async () => { 157 | const originalSystems = systems; 158 | let newSystems = [...systems]; 159 | const idx = newSystems.findIndex( 160 | (sys) => sys._id === currentEditSystem._id 161 | ); 162 | newSystems[idx] = { ...currentEditSystem }; 163 | 164 | setSystems(newSystems); 165 | try { 166 | await http.put(`/systems/${currentEditSystem._id}`, { 167 | name: currentEditSystem.name, 168 | url: currentEditSystem.url, 169 | type: currentEditSystem.type, 170 | }); 171 | setCurrentEditSystem({ name: "", url: "" }); 172 | } catch (ex) { 173 | toast({ 174 | title: "Error", 175 | description: "Error Updating The System", 176 | status: "error", 177 | duration: 9000, 178 | isClosable: true, 179 | }); 180 | setSystems(originalSystems); 181 | setCurrentEditSystem({ name: "", url: "" }); 182 | } 183 | }; 184 | const handleChangeConfig = async () => { 185 | try { 186 | await http.put(`/config`, { 187 | mailing: mailing, 188 | }); 189 | } catch (ex) { 190 | toast({ 191 | title: "Error", 192 | description: "Error Updating The Config", 193 | status: "error", 194 | duration: 9000, 195 | isClosable: true, 196 | }); 197 | } 198 | }; 199 | return ( 200 | 201 | <> 202 | 203 | {isLoggedIn ? ( 204 | <> 205 |
206 |
207 |
208 |
209 | 218 | 219 | 220 | 221 | 222 |

223 | {subsCount} 224 |

225 |

Users Subscribed

226 |
227 |
228 |
229 |
230 | {/* CRUD Status List */} 231 |
232 |

233 | Add New System 234 |

235 |
236 | 237 | System Title 238 | 250 | {formik.touched.name && formik.errors.name ? ( 251 | 252 | 253 | {formik.errors.name} 254 | 255 | ) : null} 256 | 257 | 258 | System URL 259 | 271 | {formik.touched.url && formik.errors.url ? ( 272 | 273 | 274 | {formik.errors.url} 275 | 276 | ) : null} 277 | 278 | {/* Select System Type */} 279 | 280 | 281 | 282 | Web 283 | 284 | 290 | Telegram Bot 291 | 292 | 293 | 294 | {/* Add */} 295 |
296 | 303 |
304 |
305 | {/* Status Page List */} 306 | {/* Show Sites here */} 307 | {systems.map((system) => ( 308 |
309 |
310 | {system?.name} 311 |
312 | { 315 | setCurrentEditSystem(system); 316 | }} 317 | /> 318 | { 322 | handleDelete(system); 323 | }} 324 | /> 325 |
326 |
327 |
328 | ))} 329 | {/* End */} 330 | {currentEditSystem.name ? ( 331 |
332 | 333 |

334 | Edit System 335 |

336 | 337 | System Title 338 | { 344 | setCurrentEditSystem({ 345 | ...currentEditSystem, 346 | name: e.target.value, 347 | }); 348 | }} 349 | placeholder="Enter here" 350 | /> 351 | 352 | 353 | System URL 354 | 361 | setCurrentEditSystem({ 362 | ...currentEditSystem, 363 | url: e.target.value, 364 | }) 365 | } 366 | /> 367 | 368 |
369 | {/* Add */} 370 |
371 | 378 |
379 |
380 | ) : ( 381 | "" 382 | )} 383 | 384 |

Configs

385 |

Mailing

386 | setMailing(e.target.checked)} 390 | /> 391 |
392 | 399 |
400 |
401 |
402 | 403 | ) : ( 404 | "" 405 | )} 406 | 407 | {/* Total No. of Users Subscribed */} 408 |
409 | 410 |
411 | ); 412 | } 413 | -------------------------------------------------------------------------------- /public/assets/vendor/boxicons.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'boxicons';font-weight:normal;font-style:normal;src:url('../fonts/boxicons.eot');src:url('../fonts/boxicons.eot') format('embedded-opentype'),url('../fonts/boxicons.woff2') format('woff2'),url('../fonts/boxicons.woff') format('woff'),url('../fonts/boxicons.ttf') format('truetype'),url('../fonts/boxicons.svg?#boxicons') format('svg')}.bx{font-family:'boxicons'!important;font-weight:normal;font-style:normal;font-variant:normal;line-height:1;display:inline-block;text-transform:none;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bx-ul{margin-left:2em;padding-left:0;list-style:none}.bx-ul>li{position:relative}.bx-ul .bx{font-size:inherit;line-height:inherit;position:absolute;left:-2em;width:2em;text-align:center}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-webkit-keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@-webkit-keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@-webkit-keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@-webkit-keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@-webkit-keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@-webkit-keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@-webkit-keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.bx-spin{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-spin-hover:hover{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-tada{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-tada-hover:hover{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-flashing{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-flashing-hover:hover{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-burst{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-burst-hover:hover{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-fade-up{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-up-hover:hover{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-down{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-down-hover:hover{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-left{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-left-hover:hover{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-right{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-fade-right-hover:hover{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-xs{font-size:1rem!important}.bx-sm{font-size:1.55rem!important}.bx-md{font-size:2.25rem!important}.bx-fw{font-size:1.2857142857em;line-height:.8em;width:1.2857142857em;height:.8em;margin-top:-.2em!important;vertical-align:middle}.bx-lg{font-size:3.0!important}.bx-pull-left{float:left;margin-right:.3em!important}.bx-pull-right{float:right;margin-left:.3em!important}.bx-rotate-90{transform:rotate(90deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)'}.bx-rotate-180{transform:rotate(180deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)'}.bx-rotate-270{transform:rotate(270deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)'}.bx-flip-horizontal{transform:scaleX(-1);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1)'}.bx-flip-vertical{transform:scaleY(-1);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1)'}.bx-border{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:.25em}.bx-border-circle{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:50%}.bx-abacus:before{content:"\e900"}.bx-accessibility:before{content:"\e901"}.bx-add-to-queue:before{content:"\e902"}.bx-adjust:before{content:"\e903"}.bx-alarm:before{content:"\e904"}.bx-alarm-add:before{content:"\e905"}.bx-alarm-exclamation:before{content:"\e906"}.bx-alarm-off:before{content:"\e907"}.bx-alarm-snooze:before{content:"\e908"}.bx-album:before{content:"\e909"}.bx-align-justify:before{content:"\e90a"}.bx-align-left:before{content:"\e90b"}.bx-align-middle:before{content:"\e90c"}.bx-align-right:before{content:"\e90d"}.bx-analyse:before{content:"\e90e"}.bx-anchor:before{content:"\e90f"}.bx-angry:before{content:"\e910"}.bx-aperture:before{content:"\e911"}.bx-arch:before{content:"\e912"}.bx-archive:before{content:"\e913"}.bx-archive-in:before{content:"\e914"}.bx-archive-out:before{content:"\e915"}.bx-area:before{content:"\e916"}.bx-arrow-back:before{content:"\e917"}.bx-arrow-from-bottom:before{content:"\e918"}.bx-arrow-from-left:before{content:"\e919"}.bx-arrow-from-right:before{content:"\e91a"}.bx-arrow-from-top:before{content:"\e91b"}.bx-arrow-to-bottom:before{content:"\e91c"}.bx-arrow-to-left:before{content:"\e91d"}.bx-arrow-to-right:before{content:"\e91e"}.bx-arrow-to-top:before{content:"\e91f"}.bx-at:before{content:"\e920"}.bx-atom:before{content:"\e921"}.bx-award:before{content:"\e922"}.bx-badge:before{content:"\e923"}.bx-badge-check:before{content:"\e924"}.bx-ball:before{content:"\e925"}.bx-band-aid:before{content:"\e926"}.bx-bar-chart:before{content:"\e927"}.bx-bar-chart-alt:before{content:"\e928"}.bx-bar-chart-alt-2:before{content:"\e929"}.bx-bar-chart-square:before{content:"\e92a"}.bx-barcode:before{content:"\e92b"}.bx-barcode-reader:before{content:"\e92c"}.bx-baseball:before{content:"\e92d"}.bx-basket:before{content:"\e92e"}.bx-basketball:before{content:"\e92f"}.bx-bath:before{content:"\e930"}.bx-battery:before{content:"\e931"}.bx-bed:before{content:"\e932"}.bx-been-here:before{content:"\e933"}.bx-beer:before{content:"\e934"}.bx-bell:before{content:"\e935"}.bx-bell-minus:before{content:"\e936"}.bx-bell-off:before{content:"\e937"}.bx-bell-plus:before{content:"\e938"}.bx-bible:before{content:"\e939"}.bx-bitcoin:before{content:"\e93a"}.bx-blanket:before{content:"\e93b"}.bx-block:before{content:"\e93c"}.bx-bluetooth:before{content:"\e93d"}.bx-body:before{content:"\e93e"}.bx-bold:before{content:"\e93f"}.bx-bolt-circle:before{content:"\e940"}.bx-bomb:before{content:"\e941"}.bx-bone:before{content:"\e942"}.bx-bong:before{content:"\e943"}.bx-book:before{content:"\e944"}.bx-book-add:before{content:"\e945"}.bx-book-alt:before{content:"\e946"}.bx-book-bookmark:before{content:"\e947"}.bx-book-content:before{content:"\e948"}.bx-book-heart:before{content:"\e949"}.bx-bookmark:before{content:"\e94a"}.bx-bookmark-alt:before{content:"\e94b"}.bx-bookmark-alt-minus:before{content:"\e94c"}.bx-bookmark-alt-plus:before{content:"\e94d"}.bx-bookmark-heart:before{content:"\e94e"}.bx-bookmark-minus:before{content:"\e94f"}.bx-bookmark-plus:before{content:"\e950"}.bx-bookmarks:before{content:"\e951"}.bx-book-open:before{content:"\e952"}.bx-book-reader:before{content:"\e953"}.bx-border-all:before{content:"\e954"}.bx-border-bottom:before{content:"\e955"}.bx-border-inner:before{content:"\e956"}.bx-border-left:before{content:"\e957"}.bx-border-none:before{content:"\e958"}.bx-border-outer:before{content:"\e959"}.bx-border-radius:before{content:"\e95a"}.bx-border-right:before{content:"\e95b"}.bx-border-top:before{content:"\e95c"}.bx-bot:before{content:"\e95d"}.bx-bowling-ball:before{content:"\e95e"}.bx-box:before{content:"\e95f"}.bx-bracket:before{content:"\e960"}.bx-braille:before{content:"\e961"}.bx-brain:before{content:"\e962"}.bx-briefcase:before{content:"\e963"}.bx-briefcase-alt:before{content:"\e964"}.bx-briefcase-alt-2:before{content:"\e965"}.bx-brightness:before{content:"\e966"}.bx-brightness-half:before{content:"\e967"}.bx-broadcast:before{content:"\e968"}.bx-brush:before{content:"\e969"}.bx-brush-alt:before{content:"\e96a"}.bx-bug:before{content:"\e96b"}.bx-bug-alt:before{content:"\e96c"}.bx-building:before{content:"\e96d"}.bx-building-house:before{content:"\e96e"}.bx-buildings:before{content:"\e96f"}.bx-bulb:before{content:"\e970"}.bx-bullseye:before{content:"\e971"}.bx-buoy:before{content:"\e972"}.bx-bus:before{content:"\e973"}.bx-bus-school:before{content:"\e974"}.bx-cabinet:before{content:"\e975"}.bx-cake:before{content:"\e976"}.bx-calculator:before{content:"\e977"}.bx-calendar:before{content:"\e978"}.bx-calendar-alt:before{content:"\e979"}.bx-calendar-check:before{content:"\e97a"}.bx-calendar-edit:before{content:"\e97b"}.bx-calendar-event:before{content:"\e97c"}.bx-calendar-exclamation:before{content:"\e97d"}.bx-calendar-heart:before{content:"\e97e"}.bx-calendar-minus:before{content:"\e97f"}.bx-calendar-plus:before{content:"\e980"}.bx-calendar-star:before{content:"\e981"}.bx-calendar-week:before{content:"\e982"}.bx-calendar-x:before{content:"\e983"}.bx-camera:before{content:"\e984"}.bx-camera-home:before{content:"\e985"}.bx-camera-movie:before{content:"\e986"}.bx-camera-off:before{content:"\e987"}.bx-capsule:before{content:"\e988"}.bx-captions:before{content:"\e989"}.bx-car:before{content:"\e98a"}.bx-card:before{content:"\e98b"}.bx-caret-down:before{content:"\e98c"}.bx-caret-down-circle:before{content:"\e98d"}.bx-caret-down-square:before{content:"\e98e"}.bx-caret-left:before{content:"\e98f"}.bx-caret-left-circle:before{content:"\e990"}.bx-caret-left-square:before{content:"\e991"}.bx-caret-right:before{content:"\e992"}.bx-caret-right-circle:before{content:"\e993"}.bx-caret-right-square:before{content:"\e994"}.bx-caret-up:before{content:"\e995"}.bx-caret-up-circle:before{content:"\e996"}.bx-caret-up-square:before{content:"\e997"}.bx-carousel:before{content:"\e998"}.bx-cart:before{content:"\e999"}.bx-cart-alt:before{content:"\e99a"}.bx-cast:before{content:"\e99b"}.bx-category:before{content:"\e99c"}.bx-category-alt:before{content:"\e99d"}.bx-cctv:before{content:"\e99e"}.bx-certification:before{content:"\e99f"}.bx-chair:before{content:"\e9a0"}.bx-chalkboard:before{content:"\e9a1"}.bx-chart:before{content:"\e9a2"}.bx-chat:before{content:"\e9a3"}.bx-check:before{content:"\e9a4"}.bx-checkbox:before{content:"\e9a5"}.bx-checkbox-checked:before{content:"\e9a6"}.bx-checkbox-square:before{content:"\e9a7"}.bx-check-circle:before{content:"\e9a8"}.bx-check-double:before{content:"\e9a9"}.bx-check-shield:before{content:"\e9aa"}.bx-check-square:before{content:"\e9ab"}.bx-chevron-down:before{content:"\e9ac"}.bx-chevron-down-circle:before{content:"\e9ad"}.bx-chevron-down-square:before{content:"\e9ae"}.bx-chevron-left:before{content:"\e9af"}.bx-chevron-left-circle:before{content:"\e9b0"}.bx-chevron-left-square:before{content:"\e9b1"}.bx-chevron-right:before{content:"\e9b2"}.bx-chevron-right-circle:before{content:"\e9b3"}.bx-chevron-right-square:before{content:"\e9b4"}.bx-chevrons-down:before{content:"\e9b5"}.bx-chevrons-left:before{content:"\e9b6"}.bx-chevrons-right:before{content:"\e9b7"}.bx-chevrons-up:before{content:"\e9b8"}.bx-chevron-up:before{content:"\e9b9"}.bx-chevron-up-circle:before{content:"\e9ba"}.bx-chevron-up-square:before{content:"\e9bb"}.bx-chip:before{content:"\e9bc"}.bx-church:before{content:"\e9bd"}.bx-circle:before{content:"\e9be"}.bx-clinic:before{content:"\e9bf"}.bx-clipboard:before{content:"\e9c0"}.bx-closet:before{content:"\e9c1"}.bx-cloud:before{content:"\e9c2"}.bx-cloud-download:before{content:"\e9c3"}.bx-cloud-drizzle:before{content:"\e9c4"}.bx-cloud-lightning:before{content:"\e9c5"}.bx-cloud-light-rain:before{content:"\e9c6"}.bx-cloud-rain:before{content:"\e9c7"}.bx-cloud-snow:before{content:"\e9c8"}.bx-cloud-upload:before{content:"\e9c9"}.bx-code:before{content:"\e9ca"}.bx-code-alt:before{content:"\e9cb"}.bx-code-block:before{content:"\e9cc"}.bx-code-curly:before{content:"\e9cd"}.bx-coffee:before{content:"\e9ce"}.bx-coffee-togo:before{content:"\e9cf"}.bx-cog:before{content:"\e9d0"}.bx-coin:before{content:"\e9d1"}.bx-coin-stack:before{content:"\e9d2"}.bx-collapse:before{content:"\e9d3"}.bx-collection:before{content:"\e9d4"}.bx-color-fill:before{content:"\e9d5"}.bx-columns:before{content:"\e9d6"}.bx-command:before{content:"\e9d7"}.bx-comment:before{content:"\e9d8"}.bx-comment-add:before{content:"\e9d9"}.bx-comment-check:before{content:"\e9da"}.bx-comment-detail:before{content:"\e9db"}.bx-comment-dots:before{content:"\e9dc"}.bx-comment-edit:before{content:"\e9dd"}.bx-comment-error:before{content:"\e9de"}.bx-comment-minus:before{content:"\e9df"}.bx-comment-x:before{content:"\e9e0"}.bx-compass:before{content:"\e9e1"}.bx-confused:before{content:"\e9e2"}.bx-conversation:before{content:"\e9e3"}.bx-cookie:before{content:"\e9e4"}.bx-cool:before{content:"\e9e5"}.bx-copy:before{content:"\e9e6"}.bx-copy-alt:before{content:"\e9e7"}.bx-copyright:before{content:"\e9e8"}.bx-credit-card:before{content:"\e9e9"}.bx-credit-card-alt:before{content:"\e9ea"}.bx-credit-card-front:before{content:"\e9eb"}.bx-crop:before{content:"\e9ec"}.bx-crosshair:before{content:"\e9ed"}.bx-crown:before{content:"\e9ee"}.bx-cube:before{content:"\e9ef"}.bx-cube-alt:before{content:"\e9f0"}.bx-cuboid:before{content:"\e9f1"}.bx-current-location:before{content:"\e9f2"}.bx-customize:before{content:"\e9f3"}.bx-cut:before{content:"\e9f4"}.bx-cycling:before{content:"\e9f5"}.bx-cylinder:before{content:"\e9f6"}.bx-data:before{content:"\e9f7"}.bx-desktop:before{content:"\e9f8"}.bx-detail:before{content:"\e9f9"}.bx-devices:before{content:"\e9fa"}.bx-dialpad:before{content:"\e9fb"}.bx-dialpad-alt:before{content:"\e9fc"}.bx-diamond:before{content:"\e9fd"}.bx-dice-1:before{content:"\e9fe"}.bx-dice-2:before{content:"\e9ff"}.bx-dice-3:before{content:"\ea00"}.bx-dice-4:before{content:"\ea01"}.bx-dice-5:before{content:"\ea02"}.bx-dice-6:before{content:"\ea03"}.bx-directions:before{content:"\ea04"}.bx-disc:before{content:"\ea05"}.bx-dish:before{content:"\ea06"}.bx-dislike:before{content:"\ea07"}.bx-dizzy:before{content:"\ea08"}.bx-dna:before{content:"\ea09"}.bx-dock-bottom:before{content:"\ea0a"}.bx-dock-left:before{content:"\ea0b"}.bx-dock-right:before{content:"\ea0c"}.bx-dock-top:before{content:"\ea0d"}.bx-dollar:before{content:"\ea0e"}.bx-dollar-circle:before{content:"\ea0f"}.bx-donate-blood:before{content:"\ea10"}.bx-donate-heart:before{content:"\ea11"}.bx-door-open:before{content:"\ea12"}.bx-dots-horizontal:before{content:"\ea13"}.bx-dots-horizontal-rounded:before{content:"\ea14"}.bx-dots-vertical:before{content:"\ea15"}.bx-dots-vertical-rounded:before{content:"\ea16"}.bx-doughnut-chart:before{content:"\ea17"}.bx-down-arrow:before{content:"\ea18"}.bx-down-arrow-alt:before{content:"\ea19"}.bx-down-arrow-circle:before{content:"\ea1a"}.bx-download:before{content:"\ea1b"}.bx-downvote:before{content:"\ea1c"}.bx-drink:before{content:"\ea1d"}.bx-droplet:before{content:"\ea1e"}.bx-dumbbell:before{content:"\ea1f"}.bx-duplicate:before{content:"\ea20"}.bx-edit:before{content:"\ea21"}.bx-edit-alt:before{content:"\ea22"}.bx-envelope:before{content:"\ea23"}.bx-envelope-open:before{content:"\ea24"}.bx-equalizer:before{content:"\ea25"}.bx-eraser:before{content:"\ea26"}.bx-error:before{content:"\ea27"}.bx-error-alt:before{content:"\ea28"}.bx-error-circle:before{content:"\ea29"}.bx-euro:before{content:"\ea2a"}.bx-exclude:before{content:"\ea2b"}.bx-exit:before{content:"\ea2c"}.bx-exit-fullscreen:before{content:"\ea2d"}.bx-expand:before{content:"\ea2e"}.bx-expand-alt:before{content:"\ea2f"}.bx-export:before{content:"\ea30"}.bx-extension:before{content:"\ea31"}.bx-face:before{content:"\ea32"}.bx-fast-forward:before{content:"\ea33"}.bx-fast-forward-circle:before{content:"\ea34"}.bx-female:before{content:"\ea35"}.bx-female-sign:before{content:"\ea36"}.bx-file:before{content:"\ea37"}.bx-file-blank:before{content:"\ea38"}.bx-file-find:before{content:"\ea39"}.bx-film:before{content:"\ea3a"}.bx-filter:before{content:"\ea3b"}.bx-filter-alt:before{content:"\ea3c"}.bx-fingerprint:before{content:"\ea3d"}.bx-first-aid:before{content:"\ea3e"}.bx-first-page:before{content:"\ea3f"}.bx-flag:before{content:"\ea40"}.bx-folder:before{content:"\ea41"}.bx-folder-minus:before{content:"\ea42"}.bx-folder-open:before{content:"\ea43"}.bx-folder-plus:before{content:"\ea44"}.bx-font:before{content:"\ea45"}.bx-font-color:before{content:"\ea46"}.bx-font-family:before{content:"\ea47"}.bx-font-size:before{content:"\ea48"}.bx-food-menu:before{content:"\ea49"}.bx-food-tag:before{content:"\ea4a"}.bx-football:before{content:"\ea4b"}.bx-fridge:before{content:"\ea4c"}.bx-fullscreen:before{content:"\ea4d"}.bx-game:before{content:"\ea4e"}.bx-gas-pump:before{content:"\ea4f"}.bx-ghost:before{content:"\ea50"}.bx-gift:before{content:"\ea51"}.bx-git-branch:before{content:"\ea52"}.bx-git-commit:before{content:"\ea53"}.bx-git-compare:before{content:"\ea54"}.bx-git-merge:before{content:"\ea55"}.bx-git-pull-request:before{content:"\ea56"}.bx-git-repo-forked:before{content:"\ea57"}.bx-glasses:before{content:"\ea58"}.bx-glasses-alt:before{content:"\ea59"}.bx-globe:before{content:"\ea5a"}.bx-globe-alt:before{content:"\ea5b"}.bx-grid:before{content:"\ea5c"}.bx-grid-alt:before{content:"\ea5d"}.bx-grid-horizontal:before{content:"\ea5e"}.bx-grid-small:before{content:"\ea5f"}.bx-grid-vertical:before{content:"\ea60"}.bx-group:before{content:"\ea61"}.bx-handicap:before{content:"\ea62"}.bx-happy:before{content:"\ea63"}.bx-happy-alt:before{content:"\ea64"}.bx-happy-beaming:before{content:"\ea65"}.bx-happy-heart-eyes:before{content:"\ea66"}.bx-hash:before{content:"\ea67"}.bx-hdd:before{content:"\ea68"}.bx-heading:before{content:"\ea69"}.bx-headphone:before{content:"\ea6a"}.bx-health:before{content:"\ea6b"}.bx-heart:before{content:"\ea6c"}.bx-heart-circle:before{content:"\ea6d"}.bx-heart-square:before{content:"\ea6e"}.bx-help-circle:before{content:"\ea6f"}.bx-hide:before{content:"\ea70"}.bx-highlight:before{content:"\ea71"}.bx-history:before{content:"\ea72"}.bx-hive:before{content:"\ea73"}.bx-home:before{content:"\ea74"}.bx-home-alt:before{content:"\ea75"}.bx-home-circle:before{content:"\ea76"}.bx-home-heart:before{content:"\ea77"}.bx-home-smile:before{content:"\ea78"}.bx-horizontal-center:before{content:"\ea79"}.bx-hotel:before{content:"\ea7a"}.bx-hourglass:before{content:"\ea7b"}.bx-id-card:before{content:"\ea7c"}.bx-image:before{content:"\ea7d"}.bx-image-add:before{content:"\ea7e"}.bx-image-alt:before{content:"\ea7f"}.bx-images:before{content:"\ea80"}.bx-import:before{content:"\ea81"}.bx-infinite:before{content:"\ea82"}.bx-info-circle:before{content:"\ea83"}.bx-info-square:before{content:"\ea84"}.bx-intersect:before{content:"\ea85"}.bx-italic:before{content:"\ea86"}.bx-joystick:before{content:"\ea87"}.bx-joystick-alt:before{content:"\ea88"}.bx-joystick-button:before{content:"\ea89"}.bx-key:before{content:"\ea8a"}.bx-label:before{content:"\ea8b"}.bx-landscape:before{content:"\ea8c"}.bx-laptop:before{content:"\ea8d"}.bx-last-page:before{content:"\ea8e"}.bx-laugh:before{content:"\ea8f"}.bx-layer:before{content:"\ea90"}.bx-layer-minus:before{content:"\ea91"}.bx-layer-plus:before{content:"\ea92"}.bx-layout:before{content:"\ea93"}.bx-left-arrow:before{content:"\ea94"}.bx-left-arrow-alt:before{content:"\ea95"}.bx-left-arrow-circle:before{content:"\ea96"}.bx-left-down-arrow-circle:before{content:"\ea97"}.bx-left-indent:before{content:"\ea98"}.bx-left-top-arrow-circle:before{content:"\ea99"}.bx-library:before{content:"\ea9a"}.bx-like:before{content:"\ea9b"}.bx-line-chart:before{content:"\ea9c"}.bx-line-chart-down:before{content:"\ea9d"}.bx-link:before{content:"\ea9e"}.bx-link-alt:before{content:"\ea9f"}.bx-link-external:before{content:"\eaa0"}.bx-lira:before{content:"\eaa1"}.bx-list-check:before{content:"\eaa2"}.bx-list-minus:before{content:"\eaa3"}.bx-list-ol:before{content:"\eaa4"}.bx-list-plus:before{content:"\eaa5"}.bx-list-ul:before{content:"\eaa6"}.bx-loader:before{content:"\eaa7"}.bx-loader-alt:before{content:"\eaa8"}.bx-loader-circle:before{content:"\eaa9"}.bx-location-plus:before{content:"\eaaa"}.bx-lock:before{content:"\eaab"}.bx-lock-alt:before{content:"\eaac"}.bx-lock-open:before{content:"\eaad"}.bx-lock-open-alt:before{content:"\eaae"}.bx-log-in:before{content:"\eaaf"}.bx-log-in-circle:before{content:"\eab0"}.bx-log-out:before{content:"\eab1"}.bx-log-out-circle:before{content:"\eab2"}.bx-low-vision:before{content:"\eab3"}.bx-magnet:before{content:"\eab4"}.bx-mail-send:before{content:"\eab5"}.bx-male:before{content:"\eab6"}.bx-male-sign:before{content:"\eab7"}.bx-map:before{content:"\eab8"}.bx-map-alt:before{content:"\eab9"}.bx-map-pin:before{content:"\eaba"}.bx-mask:before{content:"\eabb"}.bx-medal:before{content:"\eabc"}.bx-meh:before{content:"\eabd"}.bx-meh-alt:before{content:"\eabe"}.bx-meh-blank:before{content:"\eabf"}.bx-memory-card:before{content:"\eac0"}.bx-menu:before{content:"\eac1"}.bx-menu-alt-left:before{content:"\eac2"}.bx-menu-alt-right:before{content:"\eac3"}.bx-merge:before{content:"\eac4"}.bx-message:before{content:"\eac5"}.bx-message-add:before{content:"\eac6"}.bx-message-alt:before{content:"\eac7"}.bx-message-alt-add:before{content:"\eac8"}.bx-message-alt-check:before{content:"\eac9"}.bx-message-alt-detail:before{content:"\eaca"}.bx-message-alt-dots:before{content:"\eacb"}.bx-message-alt-edit:before{content:"\eacc"}.bx-message-alt-error:before{content:"\eacd"}.bx-message-alt-minus:before{content:"\eace"}.bx-message-alt-x:before{content:"\eacf"}.bx-message-check:before{content:"\ead0"}.bx-message-detail:before{content:"\ead1"}.bx-message-dots:before{content:"\ead2"}.bx-message-edit:before{content:"\ead3"}.bx-message-error:before{content:"\ead4"}.bx-message-minus:before{content:"\ead5"}.bx-message-rounded:before{content:"\ead6"}.bx-message-rounded-add:before{content:"\ead7"}.bx-message-rounded-check:before{content:"\ead8"}.bx-message-rounded-detail:before{content:"\ead9"}.bx-message-rounded-dots:before{content:"\eada"}.bx-message-rounded-edit:before{content:"\eadb"}.bx-message-rounded-error:before{content:"\eadc"}.bx-message-rounded-minus:before{content:"\eadd"}.bx-message-rounded-x:before{content:"\eade"}.bx-message-square:before{content:"\eadf"}.bx-message-square-add:before{content:"\eae0"}.bx-message-square-check:before{content:"\eae1"}.bx-message-square-detail:before{content:"\eae2"}.bx-message-square-dots:before{content:"\eae3"}.bx-message-square-edit:before{content:"\eae4"}.bx-message-square-error:before{content:"\eae5"}.bx-message-square-minus:before{content:"\eae6"}.bx-message-square-x:before{content:"\eae7"}.bx-message-x:before{content:"\eae8"}.bx-meteor:before{content:"\eae9"}.bx-microchip:before{content:"\eaea"}.bx-microphone:before{content:"\eaeb"}.bx-microphone-off:before{content:"\eaec"}.bx-minus:before{content:"\eaed"}.bx-minus-back:before{content:"\eaee"}.bx-minus-circle:before{content:"\eaef"}.bx-minus-front:before{content:"\eaf0"}.bx-mobile:before{content:"\eaf1"}.bx-mobile-alt:before{content:"\eaf2"}.bx-mobile-landscape:before{content:"\eaf3"}.bx-mobile-vibration:before{content:"\eaf4"}.bx-money:before{content:"\eaf5"}.bx-moon:before{content:"\eaf6"}.bx-mouse:before{content:"\eaf7"}.bx-mouse-alt:before{content:"\eaf8"}.bx-move:before{content:"\eaf9"}.bx-move-horizontal:before{content:"\eafa"}.bx-move-vertical:before{content:"\eafb"}.bx-movie:before{content:"\eafc"}.bx-movie-play:before{content:"\eafd"}.bx-music:before{content:"\eafe"}.bx-navigation:before{content:"\eaff"}.bx-network-chart:before{content:"\eb00"}.bx-news:before{content:"\eb01"}.bx-no-entry:before{content:"\eb02"}.bx-note:before{content:"\eb03"}.bx-notepad:before{content:"\eb04"}.bx-notification:before{content:"\eb05"}.bx-notification-off:before{content:"\eb06"}.bx-outline:before{content:"\eb07"}.bx-package:before{content:"\eb08"}.bx-paint:before{content:"\eb09"}.bx-paint-roll:before{content:"\eb0a"}.bx-palette:before{content:"\eb0b"}.bx-paperclip:before{content:"\eb0c"}.bx-paper-plane:before{content:"\eb0d"}.bx-paragraph:before{content:"\eb0e"}.bx-paste:before{content:"\eb0f"}.bx-pause:before{content:"\eb10"}.bx-pause-circle:before{content:"\eb11"}.bx-pen:before{content:"\eb12"}.bx-pencil:before{content:"\eb13"}.bx-phone:before{content:"\eb14"}.bx-phone-call:before{content:"\eb15"}.bx-phone-incoming:before{content:"\eb16"}.bx-phone-outgoing:before{content:"\eb17"}.bx-photo-album:before{content:"\eb18"}.bx-pie-chart:before{content:"\eb19"}.bx-pie-chart-alt:before{content:"\eb1a"}.bx-pie-chart-alt-2:before{content:"\eb1b"}.bx-pin:before{content:"\eb1c"}.bx-planet:before{content:"\eb1d"}.bx-play:before{content:"\eb1e"}.bx-play-circle:before{content:"\eb1f"}.bx-plug:before{content:"\eb20"}.bx-plus:before{content:"\eb21"}.bx-plus-circle:before{content:"\eb22"}.bx-plus-medical:before{content:"\eb23"}.bx-pointer:before{content:"\eb24"}.bx-poll:before{content:"\eb25"}.bx-polygon:before{content:"\eb26"}.bx-pound:before{content:"\eb27"}.bx-power-off:before{content:"\eb28"}.bx-printer:before{content:"\eb29"}.bx-pulse:before{content:"\eb2a"}.bx-purchase-tag:before{content:"\eb2b"}.bx-purchase-tag-alt:before{content:"\eb2c"}.bx-pyramid:before{content:"\eb2d"}.bx-question-mark:before{content:"\eb2e"}.bx-radar:before{content:"\eb2f"}.bx-radio:before{content:"\eb30"}.bx-radio-circle:before{content:"\eb31"}.bx-radio-circle-marked:before{content:"\eb32"}.bx-receipt:before{content:"\eb33"}.bx-rectangle:before{content:"\eb34"}.bx-recycle:before{content:"\eb35"}.bx-redo:before{content:"\eb36"}.bx-refresh:before{content:"\eb37"}.bx-rename:before{content:"\eb38"}.bx-repeat:before{content:"\eb39"}.bx-reply:before{content:"\eb3a"}.bx-reply-all:before{content:"\eb3b"}.bx-repost:before{content:"\eb3c"}.bx-reset:before{content:"\eb3d"}.bx-restaurant:before{content:"\eb3e"}.bx-revision:before{content:"\eb3f"}.bx-rewind:before{content:"\eb40"}.bx-rewind-circle:before{content:"\eb41"}.bx-right-arrow:before{content:"\eb42"}.bx-right-arrow-alt:before{content:"\eb43"}.bx-right-arrow-circle:before{content:"\eb44"}.bx-right-down-arrow-circle:before{content:"\eb45"}.bx-right-indent:before{content:"\eb46"}.bx-right-top-arrow-circle:before{content:"\eb47"}.bx-rocket:before{content:"\eb48"}.bx-rotate-left:before{content:"\eb49"}.bx-rotate-right:before{content:"\eb4a"}.bx-rss:before{content:"\eb4b"}.bx-ruble:before{content:"\eb4c"}.bx-ruler:before{content:"\eb4d"}.bx-run:before{content:"\eb4e"}.bx-rupee:before{content:"\eb4f"}.bx-sad:before{content:"\eb50"}.bx-save:before{content:"\eb51"}.bx-scan:before{content:"\eb52"}.bx-screenshot:before{content:"\eb53"}.bx-search:before{content:"\eb54"}.bx-search-alt:before{content:"\eb55"}.bx-search-alt-2:before{content:"\eb56"}.bx-selection:before{content:"\eb57"}.bx-select-multiple:before{content:"\eb58"}.bx-send:before{content:"\eb59"}.bx-server:before{content:"\eb5a"}.bx-shape-circle:before{content:"\eb5b"}.bx-shape-polygon:before{content:"\eb5c"}.bx-shape-square:before{content:"\eb5d"}.bx-shape-triangle:before{content:"\eb5e"}.bx-share:before{content:"\eb5f"}.bx-share-alt:before{content:"\eb60"}.bx-shekel:before{content:"\eb61"}.bx-shield:before{content:"\eb62"}.bx-shield-alt:before{content:"\eb63"}.bx-shield-alt-2:before{content:"\eb64"}.bx-shield-quarter:before{content:"\eb65"}.bx-shield-x:before{content:"\eb66"}.bx-shocked:before{content:"\eb67"}.bx-shopping-bag:before{content:"\eb68"}.bx-show:before{content:"\eb69"}.bx-show-alt:before{content:"\eb6a"}.bx-shuffle:before{content:"\eb6b"}.bx-sidebar:before{content:"\eb6c"}.bx-sitemap:before{content:"\eb6d"}.bx-skip-next:before{content:"\eb6e"}.bx-skip-next-circle:before{content:"\eb6f"}.bx-skip-previous:before{content:"\eb70"}.bx-skip-previous-circle:before{content:"\eb71"}.bx-sleepy:before{content:"\eb72"}.bx-slider:before{content:"\eb73"}.bx-slider-alt:before{content:"\eb74"}.bx-slideshow:before{content:"\eb75"}.bx-smile:before{content:"\eb76"}.bx-sort:before{content:"\eb77"}.bx-sort-alt-2:before{content:"\eb78"}.bx-sort-a-z:before{content:"\eb79"}.bx-sort-down:before{content:"\eb7a"}.bx-sort-up:before{content:"\eb7b"}.bx-sort-z-a:before{content:"\eb7c"}.bx-spa:before{content:"\eb7d"}.bx-space-bar:before{content:"\eb7e"}.bx-spray-can:before{content:"\eb7f"}.bx-spreadsheet:before{content:"\eb80"}.bx-square:before{content:"\eb81"}.bx-square-rounded:before{content:"\eb82"}.bx-star:before{content:"\eb83"}.bx-station:before{content:"\eb84"}.bx-stats:before{content:"\eb85"}.bx-sticker:before{content:"\eb86"}.bx-stop:before{content:"\eb87"}.bx-stop-circle:before{content:"\eb88"}.bx-stopwatch:before{content:"\eb89"}.bx-store:before{content:"\eb8a"}.bx-store-alt:before{content:"\eb8b"}.bx-street-view:before{content:"\eb8c"}.bx-strikethrough:before{content:"\eb8d"}.bx-subdirectory-left:before{content:"\eb8e"}.bx-subdirectory-right:before{content:"\eb8f"}.bx-sun:before{content:"\eb90"}.bx-support:before{content:"\eb91"}.bx-swim:before{content:"\eb92"}.bx-sync:before{content:"\eb93"}.bx-tab:before{content:"\eb94"}.bx-table:before{content:"\eb95"}.bx-tachometer:before{content:"\eb96"}.bx-tag:before{content:"\eb97"}.bx-tag-alt:before{content:"\eb98"}.bx-target-lock:before{content:"\eb99"}.bx-task:before{content:"\eb9a"}.bx-task-x:before{content:"\eb9b"}.bx-taxi:before{content:"\eb9c"}.bx-tennis-ball:before{content:"\eb9d"}.bx-terminal:before{content:"\eb9e"}.bx-test-tube:before{content:"\eb9f"}.bx-text:before{content:"\eba0"}.bx-time:before{content:"\eba1"}.bx-time-five:before{content:"\eba2"}.bx-timer:before{content:"\eba3"}.bx-tired:before{content:"\eba4"}.bx-toggle-left:before{content:"\eba5"}.bx-toggle-right:before{content:"\eba6"}.bx-tone:before{content:"\eba7"}.bx-traffic-cone:before{content:"\eba8"}.bx-train:before{content:"\eba9"}.bx-transfer:before{content:"\ebaa"}.bx-transfer-alt:before{content:"\ebab"}.bx-trash:before{content:"\ebac"}.bx-trash-alt:before{content:"\ebad"}.bx-trending-down:before{content:"\ebae"}.bx-trending-up:before{content:"\ebaf"}.bx-trim:before{content:"\ebb0"}.bx-trip:before{content:"\ebb1"}.bx-trophy:before{content:"\ebb2"}.bx-tv:before{content:"\ebb3"}.bx-underline:before{content:"\ebb4"}.bx-undo:before{content:"\ebb5"}.bx-unite:before{content:"\ebb6"}.bx-unlink:before{content:"\ebb7"}.bx-up-arrow:before{content:"\ebb8"}.bx-up-arrow-alt:before{content:"\ebb9"}.bx-up-arrow-circle:before{content:"\ebba"}.bx-upload:before{content:"\ebbb"}.bx-upside-down:before{content:"\ebbc"}.bx-upvote:before{content:"\ebbd"}.bx-usb:before{content:"\ebbe"}.bx-user:before{content:"\ebbf"}.bx-user-check:before{content:"\ebc0"}.bx-user-circle:before{content:"\ebc1"}.bx-user-minus:before{content:"\ebc2"}.bx-user-pin:before{content:"\ebc3"}.bx-user-plus:before{content:"\ebc4"}.bx-user-voice:before{content:"\ebc5"}.bx-user-x:before{content:"\ebc6"}.bx-vector:before{content:"\ebc7"}.bx-vertical-center:before{content:"\ebc8"}.bx-vial:before{content:"\ebc9"}.bx-video:before{content:"\ebca"}.bx-video-off:before{content:"\ebcb"}.bx-video-plus:before{content:"\ebcc"}.bx-video-recording:before{content:"\ebcd"}.bx-voicemail:before{content:"\ebce"}.bx-volume:before{content:"\ebcf"}.bx-volume-full:before{content:"\ebd0"}.bx-volume-low:before{content:"\ebd1"}.bx-volume-mute:before{content:"\ebd2"}.bx-walk:before{content:"\ebd3"}.bx-wallet:before{content:"\ebd4"}.bx-wallet-alt:before{content:"\ebd5"}.bx-water:before{content:"\ebd6"}.bx-webcam:before{content:"\ebd7"}.bx-wifi:before{content:"\ebd8"}.bx-wifi-0:before{content:"\ebd9"}.bx-wifi-1:before{content:"\ebda"}.bx-wifi-2:before{content:"\ebdb"}.bx-wifi-off:before{content:"\ebdc"}.bx-wind:before{content:"\ebdd"}.bx-window:before{content:"\ebde"}.bx-window-alt:before{content:"\ebdf"}.bx-window-close:before{content:"\ebe0"}.bx-window-open:before{content:"\ebe1"}.bx-windows:before{content:"\ebe2"}.bx-wine:before{content:"\ebe3"}.bx-wink-smile:before{content:"\ebe4"}.bx-wink-tongue:before{content:"\ebe5"}.bx-won:before{content:"\ebe6"}.bx-world:before{content:"\ebe7"}.bx-wrench:before{content:"\ebe8"}.bx-x:before{content:"\ebe9"}.bx-x-circle:before{content:"\ebea"}.bx-yen:before{content:"\ebeb"}.bx-zoom-in:before{content:"\ebec"}.bx-zoom-out:before{content:"\ebed"}.bxs-add-to-queue:before{content:"\ebee"}.bxs-adjust:before{content:"\ebef"}.bxs-adjust-alt:before{content:"\ebf0"}.bxs-alarm:before{content:"\ebf1"}.bxs-alarm-add:before{content:"\ebf2"}.bxs-alarm-exclamation:before{content:"\ebf3"}.bxs-alarm-off:before{content:"\ebf4"}.bxs-alarm-snooze:before{content:"\ebf5"}.bxs-album:before{content:"\ebf6"}.bxs-ambulance:before{content:"\ebf7"}.bxs-analyse:before{content:"\ebf8"}.bxs-angry:before{content:"\ebf9"}.bxs-arch:before{content:"\ebfa"}.bxs-archive:before{content:"\ebfb"}.bxs-archive-in:before{content:"\ebfc"}.bxs-archive-out:before{content:"\ebfd"}.bxs-area:before{content:"\ebfe"}.bxs-arrow-from-bottom:before{content:"\ebff"}.bxs-arrow-from-left:before{content:"\ec00"}.bxs-arrow-from-right:before{content:"\ec01"}.bxs-arrow-from-top:before{content:"\ec02"}.bxs-arrow-to-bottom:before{content:"\ec03"}.bxs-arrow-to-left:before{content:"\ec04"}.bxs-arrow-to-right:before{content:"\ec05"}.bxs-arrow-to-top:before{content:"\ec06"}.bxs-award:before{content:"\ec07"}.bxs-baby-carriage:before{content:"\ec08"}.bxs-backpack:before{content:"\ec09"}.bxs-badge:before{content:"\ec0a"}.bxs-badge-check:before{content:"\ec0b"}.bxs-badge-dollar:before{content:"\ec0c"}.bxs-ball:before{content:"\ec0d"}.bxs-band-aid:before{content:"\ec0e"}.bxs-bank:before{content:"\ec0f"}.bxs-bar-chart-alt-2:before{content:"\ec10"}.bxs-bar-chart-square:before{content:"\ec11"}.bxs-barcode:before{content:"\ec12"}.bxs-baseball:before{content:"\ec13"}.bxs-basket:before{content:"\ec14"}.bxs-basketball:before{content:"\ec15"}.bxs-bath:before{content:"\ec16"}.bxs-battery:before{content:"\ec17"}.bxs-battery-charging:before{content:"\ec18"}.bxs-battery-full:before{content:"\ec19"}.bxs-battery-low:before{content:"\ec1a"}.bxs-bed:before{content:"\ec1b"}.bxs-been-here:before{content:"\ec1c"}.bxs-beer:before{content:"\ec1d"}.bxs-bell:before{content:"\ec1e"}.bxs-bell-minus:before{content:"\ec1f"}.bxs-bell-off:before{content:"\ec20"}.bxs-bell-plus:before{content:"\ec21"}.bxs-bell-ring:before{content:"\ec22"}.bxs-bible:before{content:"\ec23"}.bxs-binoculars:before{content:"\ec24"}.bxs-blanket:before{content:"\ec25"}.bxs-bolt:before{content:"\ec26"}.bxs-bolt-circle:before{content:"\ec27"}.bxs-bomb:before{content:"\ec28"}.bxs-bone:before{content:"\ec29"}.bxs-bong:before{content:"\ec2a"}.bxs-book:before{content:"\ec2b"}.bxs-book-add:before{content:"\ec2c"}.bxs-book-alt:before{content:"\ec2d"}.bxs-book-bookmark:before{content:"\ec2e"}.bxs-book-content:before{content:"\ec2f"}.bxs-book-heart:before{content:"\ec30"}.bxs-bookmark:before{content:"\ec31"}.bxs-bookmark-alt:before{content:"\ec32"}.bxs-bookmark-alt-minus:before{content:"\ec33"}.bxs-bookmark-alt-plus:before{content:"\ec34"}.bxs-bookmark-heart:before{content:"\ec35"}.bxs-bookmark-minus:before{content:"\ec36"}.bxs-bookmark-plus:before{content:"\ec37"}.bxs-bookmarks:before{content:"\ec38"}.bxs-bookmark-star:before{content:"\ec39"}.bxs-book-open:before{content:"\ec3a"}.bxs-book-reader:before{content:"\ec3b"}.bxs-bot:before{content:"\ec3c"}.bxs-bowling-ball:before{content:"\ec3d"}.bxs-box:before{content:"\ec3e"}.bxs-brain:before{content:"\ec3f"}.bxs-briefcase:before{content:"\ec40"}.bxs-briefcase-alt:before{content:"\ec41"}.bxs-briefcase-alt-2:before{content:"\ec42"}.bxs-brightness:before{content:"\ec43"}.bxs-brightness-half:before{content:"\ec44"}.bxs-brush:before{content:"\ec45"}.bxs-brush-alt:before{content:"\ec46"}.bxs-bug:before{content:"\ec47"}.bxs-bug-alt:before{content:"\ec48"}.bxs-building:before{content:"\ec49"}.bxs-building-house:before{content:"\ec4a"}.bxs-buildings:before{content:"\ec4b"}.bxs-bulb:before{content:"\ec4c"}.bxs-bullseye:before{content:"\ec4d"}.bxs-buoy:before{content:"\ec4e"}.bxs-bus:before{content:"\ec4f"}.bxs-business:before{content:"\ec50"}.bxs-bus-school:before{content:"\ec51"}.bxs-cabinet:before{content:"\ec52"}.bxs-cake:before{content:"\ec53"}.bxs-calculator:before{content:"\ec54"}.bxs-calendar:before{content:"\ec55"}.bxs-calendar-alt:before{content:"\ec56"}.bxs-calendar-check:before{content:"\ec57"}.bxs-calendar-edit:before{content:"\ec58"}.bxs-calendar-event:before{content:"\ec59"}.bxs-calendar-exclamation:before{content:"\ec5a"}.bxs-calendar-heart:before{content:"\ec5b"}.bxs-calendar-minus:before{content:"\ec5c"}.bxs-calendar-plus:before{content:"\ec5d"}.bxs-calendar-star:before{content:"\ec5e"}.bxs-calendar-week:before{content:"\ec5f"}.bxs-calendar-x:before{content:"\ec60"}.bxs-camera:before{content:"\ec61"}.bxs-camera-home:before{content:"\ec62"}.bxs-camera-movie:before{content:"\ec63"}.bxs-camera-off:before{content:"\ec64"}.bxs-camera-plus:before{content:"\ec65"}.bxs-capsule:before{content:"\ec66"}.bxs-captions:before{content:"\ec67"}.bxs-car:before{content:"\ec68"}.bxs-car-battery:before{content:"\ec69"}.bxs-car-crash:before{content:"\ec6a"}.bxs-card:before{content:"\ec6b"}.bxs-caret-down-circle:before{content:"\ec6c"}.bxs-caret-down-square:before{content:"\ec6d"}.bxs-caret-left-circle:before{content:"\ec6e"}.bxs-caret-left-square:before{content:"\ec6f"}.bxs-caret-right-circle:before{content:"\ec70"}.bxs-caret-right-square:before{content:"\ec71"}.bxs-caret-up-circle:before{content:"\ec72"}.bxs-caret-up-square:before{content:"\ec73"}.bxs-car-garage:before{content:"\ec74"}.bxs-car-mechanic:before{content:"\ec75"}.bxs-carousel:before{content:"\ec76"}.bxs-cart:before{content:"\ec77"}.bxs-cart-add:before{content:"\ec78"}.bxs-cart-alt:before{content:"\ec79"}.bxs-cart-download:before{content:"\ec7a"}.bxs-car-wash:before{content:"\ec7b"}.bxs-category:before{content:"\ec7c"}.bxs-category-alt:before{content:"\ec7d"}.bxs-cctv:before{content:"\ec7e"}.bxs-certification:before{content:"\ec7f"}.bxs-chalkboard:before{content:"\ec80"}.bxs-chart:before{content:"\ec81"}.bxs-chat:before{content:"\ec82"}.bxs-checkbox:before{content:"\ec83"}.bxs-checkbox-checked:before{content:"\ec84"}.bxs-check-circle:before{content:"\ec85"}.bxs-check-shield:before{content:"\ec86"}.bxs-check-square:before{content:"\ec87"}.bxs-chess:before{content:"\ec88"}.bxs-chevron-down:before{content:"\ec89"}.bxs-chevron-down-circle:before{content:"\ec8a"}.bxs-chevron-down-square:before{content:"\ec8b"}.bxs-chevron-left:before{content:"\ec8c"}.bxs-chevron-left-circle:before{content:"\ec8d"}.bxs-chevron-left-square:before{content:"\ec8e"}.bxs-chevron-right:before{content:"\ec8f"}.bxs-chevron-right-circle:before{content:"\ec90"}.bxs-chevron-right-square:before{content:"\ec91"}.bxs-chevrons-down:before{content:"\ec92"}.bxs-chevrons-left:before{content:"\ec93"}.bxs-chevrons-right:before{content:"\ec94"}.bxs-chevrons-up:before{content:"\ec95"}.bxs-chevron-up:before{content:"\ec96"}.bxs-chevron-up-circle:before{content:"\ec97"}.bxs-chevron-up-square:before{content:"\ec98"}.bxs-chip:before{content:"\ec99"}.bxs-church:before{content:"\ec9a"}.bxs-circle:before{content:"\ec9b"}.bxs-city:before{content:"\ec9c"}.bxs-clinic:before{content:"\ec9d"}.bxs-cloud:before{content:"\ec9e"}.bxs-cloud-download:before{content:"\ec9f"}.bxs-cloud-lightning:before{content:"\eca0"}.bxs-cloud-rain:before{content:"\eca1"}.bxs-cloud-upload:before{content:"\eca2"}.bxs-coffee:before{content:"\eca3"}.bxs-coffee-alt:before{content:"\eca4"}.bxs-coffee-togo:before{content:"\eca5"}.bxs-cog:before{content:"\eca6"}.bxs-coin:before{content:"\eca7"}.bxs-coin-stack:before{content:"\eca8"}.bxs-collection:before{content:"\eca9"}.bxs-color-fill:before{content:"\ecaa"}.bxs-comment:before{content:"\ecab"}.bxs-comment-add:before{content:"\ecac"}.bxs-comment-check:before{content:"\ecad"}.bxs-comment-detail:before{content:"\ecae"}.bxs-comment-dots:before{content:"\ecaf"}.bxs-comment-edit:before{content:"\ecb0"}.bxs-comment-error:before{content:"\ecb1"}.bxs-comment-minus:before{content:"\ecb2"}.bxs-comment-x:before{content:"\ecb3"}.bxs-compass:before{content:"\ecb4"}.bxs-component:before{content:"\ecb5"}.bxs-confused:before{content:"\ecb6"}.bxs-contact:before{content:"\ecb7"}.bxs-conversation:before{content:"\ecb8"}.bxs-cookie:before{content:"\ecb9"}.bxs-cool:before{content:"\ecba"}.bxs-copy:before{content:"\ecbb"}.bxs-copy-alt:before{content:"\ecbc"}.bxs-copyright:before{content:"\ecbd"}.bxs-coupon:before{content:"\ecbe"}.bxs-credit-card:before{content:"\ecbf"}.bxs-credit-card-alt:before{content:"\ecc0"}.bxs-credit-card-front:before{content:"\ecc1"}.bxs-crop:before{content:"\ecc2"}.bxs-crown:before{content:"\ecc3"}.bxs-cube:before{content:"\ecc4"}.bxs-cube-alt:before{content:"\ecc5"}.bxs-cuboid:before{content:"\ecc6"}.bxs-customize:before{content:"\ecc7"}.bxs-cylinder:before{content:"\ecc8"}.bxs-dashboard:before{content:"\ecc9"}.bxs-data:before{content:"\ecca"}.bxs-detail:before{content:"\eccb"}.bxs-devices:before{content:"\eccc"}.bxs-diamond:before{content:"\eccd"}.bxs-dice-1:before{content:"\ecce"}.bxs-dice-2:before{content:"\eccf"}.bxs-dice-3:before{content:"\ecd0"}.bxs-dice-4:before{content:"\ecd1"}.bxs-dice-5:before{content:"\ecd2"}.bxs-dice-6:before{content:"\ecd3"}.bxs-direction-left:before{content:"\ecd4"}.bxs-direction-right:before{content:"\ecd5"}.bxs-directions:before{content:"\ecd6"}.bxs-disc:before{content:"\ecd7"}.bxs-discount:before{content:"\ecd8"}.bxs-dish:before{content:"\ecd9"}.bxs-dislike:before{content:"\ecda"}.bxs-dizzy:before{content:"\ecdb"}.bxs-dock-bottom:before{content:"\ecdc"}.bxs-dock-left:before{content:"\ecdd"}.bxs-dock-right:before{content:"\ecde"}.bxs-dock-top:before{content:"\ecdf"}.bxs-dollar-circle:before{content:"\ece0"}.bxs-donate-blood:before{content:"\ece1"}.bxs-donate-heart:before{content:"\ece2"}.bxs-door-open:before{content:"\ece3"}.bxs-doughnut-chart:before{content:"\ece4"}.bxs-down-arrow:before{content:"\ece5"}.bxs-down-arrow-alt:before{content:"\ece6"}.bxs-down-arrow-circle:before{content:"\ece7"}.bxs-down-arrow-square:before{content:"\ece8"}.bxs-download:before{content:"\ece9"}.bxs-downvote:before{content:"\ecea"}.bxs-drink:before{content:"\eceb"}.bxs-droplet:before{content:"\ecec"}.bxs-droplet-half:before{content:"\eced"}.bxs-dryer:before{content:"\ecee"}.bxs-duplicate:before{content:"\ecef"}.bxs-edit:before{content:"\ecf0"}.bxs-edit-alt:before{content:"\ecf1"}.bxs-edit-location:before{content:"\ecf2"}.bxs-eject:before{content:"\ecf3"}.bxs-envelope:before{content:"\ecf4"}.bxs-envelope-open:before{content:"\ecf5"}.bxs-eraser:before{content:"\ecf6"}.bxs-error:before{content:"\ecf7"}.bxs-error-alt:before{content:"\ecf8"}.bxs-error-circle:before{content:"\ecf9"}.bxs-ev-station:before{content:"\ecfa"}.bxs-exit:before{content:"\ecfb"}.bxs-extension:before{content:"\ecfc"}.bxs-eyedropper:before{content:"\ecfd"}.bxs-face:before{content:"\ecfe"}.bxs-face-mask:before{content:"\ecff"}.bxs-factory:before{content:"\ed00"}.bxs-fast-forward-circle:before{content:"\ed01"}.bxs-file:before{content:"\ed02"}.bxs-file-archive:before{content:"\ed03"}.bxs-file-blank:before{content:"\ed04"}.bxs-file-css:before{content:"\ed05"}.bxs-file-doc:before{content:"\ed06"}.bxs-file-export:before{content:"\ed07"}.bxs-file-find:before{content:"\ed08"}.bxs-file-gif:before{content:"\ed09"}.bxs-file-html:before{content:"\ed0a"}.bxs-file-image:before{content:"\ed0b"}.bxs-file-import:before{content:"\ed0c"}.bxs-file-jpg:before{content:"\ed0d"}.bxs-file-js:before{content:"\ed0e"}.bxs-file-json:before{content:"\ed0f"}.bxs-file-md:before{content:"\ed10"}.bxs-file-pdf:before{content:"\ed11"}.bxs-file-plus:before{content:"\ed12"}.bxs-file-png:before{content:"\ed13"}.bxs-file-txt:before{content:"\ed14"}.bxs-film:before{content:"\ed15"}.bxs-filter-alt:before{content:"\ed16"}.bxs-first-aid:before{content:"\ed17"}.bxs-flag:before{content:"\ed18"}.bxs-flag-alt:before{content:"\ed19"}.bxs-flag-checkered:before{content:"\ed1a"}.bxs-flame:before{content:"\ed1b"}.bxs-flask:before{content:"\ed1c"}.bxs-florist:before{content:"\ed1d"}.bxs-folder:before{content:"\ed1e"}.bxs-folder-minus:before{content:"\ed1f"}.bxs-folder-open:before{content:"\ed20"}.bxs-folder-plus:before{content:"\ed21"}.bxs-food-menu:before{content:"\ed22"}.bxs-fridge:before{content:"\ed23"}.bxs-game:before{content:"\ed24"}.bxs-gas-pump:before{content:"\ed25"}.bxs-ghost:before{content:"\ed26"}.bxs-gift:before{content:"\ed27"}.bxs-graduation:before{content:"\ed28"}.bxs-grid:before{content:"\ed29"}.bxs-grid-alt:before{content:"\ed2a"}.bxs-group:before{content:"\ed2b"}.bxs-guitar-amp:before{content:"\ed2c"}.bxs-hand-down:before{content:"\ed2d"}.bxs-hand-left:before{content:"\ed2e"}.bxs-hand-right:before{content:"\ed2f"}.bxs-hand-up:before{content:"\ed30"}.bxs-happy:before{content:"\ed31"}.bxs-happy-alt:before{content:"\ed32"}.bxs-happy-beaming:before{content:"\ed33"}.bxs-happy-heart-eyes:before{content:"\ed34"}.bxs-hdd:before{content:"\ed35"}.bxs-heart:before{content:"\ed36"}.bxs-heart-circle:before{content:"\ed37"}.bxs-heart-square:before{content:"\ed38"}.bxs-help-circle:before{content:"\ed39"}.bxs-hide:before{content:"\ed3a"}.bxs-home:before{content:"\ed3b"}.bxs-home-circle:before{content:"\ed3c"}.bxs-home-heart:before{content:"\ed3d"}.bxs-home-smile:before{content:"\ed3e"}.bxs-hot:before{content:"\ed3f"}.bxs-hotel:before{content:"\ed40"}.bxs-hourglass:before{content:"\ed41"}.bxs-hourglass-bottom:before{content:"\ed42"}.bxs-hourglass-top:before{content:"\ed43"}.bxs-id-card:before{content:"\ed44"}.bxs-image:before{content:"\ed45"}.bxs-image-add:before{content:"\ed46"}.bxs-image-alt:before{content:"\ed47"}.bxs-inbox:before{content:"\ed48"}.bxs-info-circle:before{content:"\ed49"}.bxs-info-square:before{content:"\ed4a"}.bxs-institution:before{content:"\ed4b"}.bxs-joystick:before{content:"\ed4c"}.bxs-joystick-alt:before{content:"\ed4d"}.bxs-joystick-button:before{content:"\ed4e"}.bxs-key:before{content:"\ed4f"}.bxs-keyboard:before{content:"\ed50"}.bxs-label:before{content:"\ed51"}.bxs-landmark:before{content:"\ed52"}.bxs-landscape:before{content:"\ed53"}.bxs-laugh:before{content:"\ed54"}.bxs-layer:before{content:"\ed55"}.bxs-layer-minus:before{content:"\ed56"}.bxs-layer-plus:before{content:"\ed57"}.bxs-layout:before{content:"\ed58"}.bxs-left-arrow:before{content:"\ed59"}.bxs-left-arrow-alt:before{content:"\ed5a"}.bxs-left-arrow-circle:before{content:"\ed5b"}.bxs-left-arrow-square:before{content:"\ed5c"}.bxs-left-down-arrow-circle:before{content:"\ed5d"}.bxs-left-top-arrow-circle:before{content:"\ed5e"}.bxs-like:before{content:"\ed5f"}.bxs-location-plus:before{content:"\ed60"}.bxs-lock:before{content:"\ed61"}.bxs-lock-alt:before{content:"\ed62"}.bxs-lock-open:before{content:"\ed63"}.bxs-lock-open-alt:before{content:"\ed64"}.bxs-log-in:before{content:"\ed65"}.bxs-log-in-circle:before{content:"\ed66"}.bxs-log-out:before{content:"\ed67"}.bxs-log-out-circle:before{content:"\ed68"}.bxs-low-vision:before{content:"\ed69"}.bxs-magic-wand:before{content:"\ed6a"}.bxs-magnet:before{content:"\ed6b"}.bxs-map:before{content:"\ed6c"}.bxs-map-alt:before{content:"\ed6d"}.bxs-map-pin:before{content:"\ed6e"}.bxs-mask:before{content:"\ed6f"}.bxs-medal:before{content:"\ed70"}.bxs-megaphone:before{content:"\ed71"}.bxs-meh:before{content:"\ed72"}.bxs-meh-alt:before{content:"\ed73"}.bxs-meh-blank:before{content:"\ed74"}.bxs-memory-card:before{content:"\ed75"}.bxs-message:before{content:"\ed76"}.bxs-message-add:before{content:"\ed77"}.bxs-message-alt:before{content:"\ed78"}.bxs-message-alt-add:before{content:"\ed79"}.bxs-message-alt-check:before{content:"\ed7a"}.bxs-message-alt-detail:before{content:"\ed7b"}.bxs-message-alt-dots:before{content:"\ed7c"}.bxs-message-alt-edit:before{content:"\ed7d"}.bxs-message-alt-error:before{content:"\ed7e"}.bxs-message-alt-minus:before{content:"\ed7f"}.bxs-message-alt-x:before{content:"\ed80"}.bxs-message-check:before{content:"\ed81"}.bxs-message-detail:before{content:"\ed82"}.bxs-message-dots:before{content:"\ed83"}.bxs-message-edit:before{content:"\ed84"}.bxs-message-error:before{content:"\ed85"}.bxs-message-minus:before{content:"\ed86"}.bxs-message-rounded:before{content:"\ed87"}.bxs-message-rounded-add:before{content:"\ed88"}.bxs-message-rounded-check:before{content:"\ed89"}.bxs-message-rounded-detail:before{content:"\ed8a"}.bxs-message-rounded-dots:before{content:"\ed8b"}.bxs-message-rounded-edit:before{content:"\ed8c"}.bxs-message-rounded-error:before{content:"\ed8d"}.bxs-message-rounded-minus:before{content:"\ed8e"}.bxs-message-rounded-x:before{content:"\ed8f"}.bxs-message-square:before{content:"\ed90"}.bxs-message-square-add:before{content:"\ed91"}.bxs-message-square-check:before{content:"\ed92"}.bxs-message-square-detail:before{content:"\ed93"}.bxs-message-square-dots:before{content:"\ed94"}.bxs-message-square-edit:before{content:"\ed95"}.bxs-message-square-error:before{content:"\ed96"}.bxs-message-square-minus:before{content:"\ed97"}.bxs-message-square-x:before{content:"\ed98"}.bxs-message-x:before{content:"\ed99"}.bxs-meteor:before{content:"\ed9a"}.bxs-microchip:before{content:"\ed9b"}.bxs-microphone:before{content:"\ed9c"}.bxs-microphone-alt:before{content:"\ed9d"}.bxs-microphone-off:before{content:"\ed9e"}.bxs-minus-circle:before{content:"\ed9f"}.bxs-minus-square:before{content:"\eda0"}.bxs-mobile:before{content:"\eda1"}.bxs-mobile-vibration:before{content:"\eda2"}.bxs-moon:before{content:"\eda3"}.bxs-mouse:before{content:"\eda4"}.bxs-mouse-alt:before{content:"\eda5"}.bxs-movie:before{content:"\eda6"}.bxs-movie-play:before{content:"\eda7"}.bxs-music:before{content:"\eda8"}.bxs-navigation:before{content:"\eda9"}.bxs-network-chart:before{content:"\edaa"}.bxs-news:before{content:"\edab"}.bxs-no-entry:before{content:"\edac"}.bxs-note:before{content:"\edad"}.bxs-notepad:before{content:"\edae"}.bxs-notification:before{content:"\edaf"}.bxs-notification-off:before{content:"\edb0"}.bxs-offer:before{content:"\edb1"}.bxs-package:before{content:"\edb2"}.bxs-paint:before{content:"\edb3"}.bxs-paint-roll:before{content:"\edb4"}.bxs-palette:before{content:"\edb5"}.bxs-paper-plane:before{content:"\edb6"}.bxs-parking:before{content:"\edb7"}.bxs-paste:before{content:"\edb8"}.bxs-pen:before{content:"\edb9"}.bxs-pencil:before{content:"\edba"}.bxs-phone:before{content:"\edbb"}.bxs-phone-call:before{content:"\edbc"}.bxs-phone-incoming:before{content:"\edbd"}.bxs-phone-outgoing:before{content:"\edbe"}.bxs-photo-album:before{content:"\edbf"}.bxs-piano:before{content:"\edc0"}.bxs-pie-chart:before{content:"\edc1"}.bxs-pie-chart-alt:before{content:"\edc2"}.bxs-pie-chart-alt-2:before{content:"\edc3"}.bxs-pin:before{content:"\edc4"}.bxs-pizza:before{content:"\edc5"}.bxs-plane:before{content:"\edc6"}.bxs-plane-alt:before{content:"\edc7"}.bxs-plane-land:before{content:"\edc8"}.bxs-planet:before{content:"\edc9"}.bxs-plane-take-off:before{content:"\edca"}.bxs-playlist:before{content:"\edcb"}.bxs-plug:before{content:"\edcc"}.bxs-plus-circle:before{content:"\edcd"}.bxs-plus-square:before{content:"\edce"}.bxs-pointer:before{content:"\edcf"}.bxs-polygon:before{content:"\edd0"}.bxs-printer:before{content:"\edd1"}.bxs-purchase-tag:before{content:"\edd2"}.bxs-purchase-tag-alt:before{content:"\edd3"}.bxs-pyramid:before{content:"\edd4"}.bxs-quote-alt-left:before{content:"\edd5"}.bxs-quote-alt-right:before{content:"\edd6"}.bxs-quote-left:before{content:"\edd7"}.bxs-quote-right:before{content:"\edd8"}.bxs-quote-single-left:before{content:"\edd9"}.bxs-quote-single-right:before{content:"\edda"}.bxs-radiation:before{content:"\eddb"}.bxs-radio:before{content:"\eddc"}.bxs-receipt:before{content:"\eddd"}.bxs-rectangle:before{content:"\edde"}.bxs-rename:before{content:"\eddf"}.bxs-report:before{content:"\ede0"}.bxs-rewind-circle:before{content:"\ede1"}.bxs-right-arrow:before{content:"\ede2"}.bxs-right-arrow-alt:before{content:"\ede3"}.bxs-right-arrow-circle:before{content:"\ede4"}.bxs-right-arrow-square:before{content:"\ede5"}.bxs-right-down-arrow-circle:before{content:"\ede6"}.bxs-right-top-arrow-circle:before{content:"\ede7"}.bxs-rocket:before{content:"\ede8"}.bxs-ruler:before{content:"\ede9"}.bxs-sad:before{content:"\edea"}.bxs-save:before{content:"\edeb"}.bxs-school:before{content:"\edec"}.bxs-search:before{content:"\eded"}.bxs-search-alt-2:before{content:"\edee"}.bxs-select-multiple:before{content:"\edef"}.bxs-send:before{content:"\edf0"}.bxs-server:before{content:"\edf1"}.bxs-shapes:before{content:"\edf2"}.bxs-share:before{content:"\edf3"}.bxs-share-alt:before{content:"\edf4"}.bxs-shield:before{content:"\edf5"}.bxs-shield-alt-2:before{content:"\edf6"}.bxs-shield-x:before{content:"\edf7"}.bxs-ship:before{content:"\edf8"}.bxs-shocked:before{content:"\edf9"}.bxs-shopping-bag:before{content:"\edfa"}.bxs-shopping-bag-alt:before{content:"\edfb"}.bxs-shopping-bags:before{content:"\edfc"}.bxs-show:before{content:"\edfd"}.bxs-skip-next-circle:before{content:"\edfe"}.bxs-skip-previous-circle:before{content:"\edff"}.bxs-skull:before{content:"\ee00"}.bxs-sleepy:before{content:"\ee01"}.bxs-slideshow:before{content:"\ee02"}.bxs-smile:before{content:"\ee03"}.bxs-sort-alt:before{content:"\ee04"}.bxs-spa:before{content:"\ee05"}.bxs-spray-can:before{content:"\ee06"}.bxs-spreadsheet:before{content:"\ee07"}.bxs-square:before{content:"\ee08"}.bxs-square-rounded:before{content:"\ee09"}.bxs-star:before{content:"\ee0a"}.bxs-star-half:before{content:"\ee0b"}.bxs-sticker:before{content:"\ee0c"}.bxs-stopwatch:before{content:"\ee0d"}.bxs-store:before{content:"\ee0e"}.bxs-store-alt:before{content:"\ee0f"}.bxs-sun:before{content:"\ee10"}.bxs-tachometer:before{content:"\ee11"}.bxs-tag:before{content:"\ee12"}.bxs-tag-alt:before{content:"\ee13"}.bxs-tag-x:before{content:"\ee14"}.bxs-taxi:before{content:"\ee15"}.bxs-tennis-ball:before{content:"\ee16"}.bxs-terminal:before{content:"\ee17"}.bxs-thermometer:before{content:"\ee18"}.bxs-time:before{content:"\ee19"}.bxs-time-five:before{content:"\ee1a"}.bxs-timer:before{content:"\ee1b"}.bxs-tired:before{content:"\ee1c"}.bxs-toggle-left:before{content:"\ee1d"}.bxs-toggle-right:before{content:"\ee1e"}.bxs-tone:before{content:"\ee1f"}.bxs-torch:before{content:"\ee20"}.bxs-to-top:before{content:"\ee21"}.bxs-traffic:before{content:"\ee22"}.bxs-traffic-barrier:before{content:"\ee23"}.bxs-traffic-cone:before{content:"\ee24"}.bxs-train:before{content:"\ee25"}.bxs-trash:before{content:"\ee26"}.bxs-trash-alt:before{content:"\ee27"}.bxs-tree:before{content:"\ee28"}.bxs-trophy:before{content:"\ee29"}.bxs-truck:before{content:"\ee2a"}.bxs-t-shirt:before{content:"\ee2b"}.bxs-tv:before{content:"\ee2c"}.bxs-up-arrow:before{content:"\ee2d"}.bxs-up-arrow-alt:before{content:"\ee2e"}.bxs-up-arrow-circle:before{content:"\ee2f"}.bxs-up-arrow-square:before{content:"\ee30"}.bxs-upside-down:before{content:"\ee31"}.bxs-upvote:before{content:"\ee32"}.bxs-user:before{content:"\ee33"}.bxs-user-account:before{content:"\ee34"}.bxs-user-badge:before{content:"\ee35"}.bxs-user-check:before{content:"\ee36"}.bxs-user-circle:before{content:"\ee37"}.bxs-user-detail:before{content:"\ee38"}.bxs-user-minus:before{content:"\ee39"}.bxs-user-pin:before{content:"\ee3a"}.bxs-user-plus:before{content:"\ee3b"}.bxs-user-rectangle:before{content:"\ee3c"}.bxs-user-voice:before{content:"\ee3d"}.bxs-user-x:before{content:"\ee3e"}.bxs-vector:before{content:"\ee3f"}.bxs-vial:before{content:"\ee40"}.bxs-video:before{content:"\ee41"}.bxs-video-off:before{content:"\ee42"}.bxs-video-plus:before{content:"\ee43"}.bxs-video-recording:before{content:"\ee44"}.bxs-videos:before{content:"\ee45"}.bxs-virus:before{content:"\ee46"}.bxs-virus-block:before{content:"\ee47"}.bxs-volume:before{content:"\ee48"}.bxs-volume-full:before{content:"\ee49"}.bxs-volume-low:before{content:"\ee4a"}.bxs-volume-mute:before{content:"\ee4b"}.bxs-wallet:before{content:"\ee4c"}.bxs-wallet-alt:before{content:"\ee4d"}.bxs-washer:before{content:"\ee4e"}.bxs-watch:before{content:"\ee4f"}.bxs-watch-alt:before{content:"\ee50"}.bxs-webcam:before{content:"\ee51"}.bxs-widget:before{content:"\ee52"}.bxs-window-alt:before{content:"\ee53"}.bxs-wine:before{content:"\ee54"}.bxs-wink-smile:before{content:"\ee55"}.bxs-wink-tongue:before{content:"\ee56"}.bxs-wrench:before{content:"\ee57"}.bxs-x-circle:before{content:"\ee58"}.bxs-x-square:before{content:"\ee59"}.bxs-yin-yang:before{content:"\ee5a"}.bxs-zap:before{content:"\ee5b"}.bxs-zoom-in:before{content:"\ee5c"}.bxs-zoom-out:before{content:"\ee5d"}.bxl-500px:before{content:"\ee5e"}.bxl-adobe:before{content:"\ee5f"}.bxl-airbnb:before{content:"\ee60"}.bxl-algolia:before{content:"\ee61"}.bxl-amazon:before{content:"\ee62"}.bxl-android:before{content:"\ee63"}.bxl-angular:before{content:"\ee64"}.bxl-apple:before{content:"\ee65"}.bxl-audible:before{content:"\ee66"}.bxl-baidu:before{content:"\ee67"}.bxl-behance:before{content:"\ee68"}.bxl-bing:before{content:"\ee69"}.bxl-bitcoin:before{content:"\ee6a"}.bxl-blender:before{content:"\ee6b"}.bxl-blogger:before{content:"\ee6c"}.bxl-bootstrap:before{content:"\ee6d"}.bxl-chrome:before{content:"\ee6e"}.bxl-codepen:before{content:"\ee6f"}.bxl-c-plus-plus:before{content:"\ee70"}.bxl-creative-commons:before{content:"\ee71"}.bxl-css3:before{content:"\ee72"}.bxl-dailymotion:before{content:"\ee73"}.bxl-deviantart:before{content:"\ee74"}.bxl-dev-to:before{content:"\ee75"}.bxl-digg:before{content:"\ee76"}.bxl-digitalocean:before{content:"\ee77"}.bxl-discord:before{content:"\ee78"}.bxl-discourse:before{content:"\ee79"}.bxl-django:before{content:"\ee7a"}.bxl-dribbble:before{content:"\ee7b"}.bxl-dropbox:before{content:"\ee7c"}.bxl-drupal:before{content:"\ee7d"}.bxl-ebay:before{content:"\ee7e"}.bxl-edge:before{content:"\ee7f"}.bxl-etsy:before{content:"\ee80"}.bxl-facebook:before{content:"\ee81"}.bxl-facebook-circle:before{content:"\ee82"}.bxl-facebook-square:before{content:"\ee83"}.bxl-figma:before{content:"\ee84"}.bxl-firebase:before{content:"\ee85"}.bxl-firefox:before{content:"\ee86"}.bxl-flickr:before{content:"\ee87"}.bxl-flickr-square:before{content:"\ee88"}.bxl-foursquare:before{content:"\ee89"}.bxl-git:before{content:"\ee8a"}.bxl-github:before{content:"\ee8b"}.bxl-gitlab:before{content:"\ee8c"}.bxl-google:before{content:"\ee8d"}.bxl-google-cloud:before{content:"\ee8e"}.bxl-google-plus:before{content:"\ee8f"}.bxl-google-plus-circle:before{content:"\ee90"}.bxl-html5:before{content:"\ee91"}.bxl-imdb:before{content:"\ee92"}.bxl-instagram:before{content:"\ee93"}.bxl-instagram-alt:before{content:"\ee94"}.bxl-internet-explorer:before{content:"\ee95"}.bxl-invision:before{content:"\ee96"}.bxl-javascript:before{content:"\ee97"}.bxl-joomla:before{content:"\ee98"}.bxl-jquery:before{content:"\ee99"}.bxl-jsfiddle:before{content:"\ee9a"}.bxl-kickstarter:before{content:"\ee9b"}.bxl-kubernetes:before{content:"\ee9c"}.bxl-less:before{content:"\ee9d"}.bxl-linkedin:before{content:"\ee9e"}.bxl-linkedin-square:before{content:"\ee9f"}.bxl-magento:before{content:"\eea0"}.bxl-mailchimp:before{content:"\eea1"}.bxl-markdown:before{content:"\eea2"}.bxl-mastercard:before{content:"\eea3"}.bxl-medium:before{content:"\eea4"}.bxl-medium-old:before{content:"\eea5"}.bxl-medium-square:before{content:"\eea6"}.bxl-messenger:before{content:"\eea7"}.bxl-microsoft:before{content:"\eea8"}.bxl-microsoft-teams:before{content:"\eea9"}.bxl-nodejs:before{content:"\eeaa"}.bxl-ok-ru:before{content:"\eeab"}.bxl-opera:before{content:"\eeac"}.bxl-patreon:before{content:"\eead"}.bxl-paypal:before{content:"\eeae"}.bxl-periscope:before{content:"\eeaf"}.bxl-pinterest:before{content:"\eeb0"}.bxl-pinterest-alt:before{content:"\eeb1"}.bxl-play-store:before{content:"\eeb2"}.bxl-pocket:before{content:"\eeb3"}.bxl-product-hunt:before{content:"\eeb4"}.bxl-python:before{content:"\eeb5"}.bxl-quora:before{content:"\eeb6"}.bxl-react:before{content:"\eeb7"}.bxl-redbubble:before{content:"\eeb8"}.bxl-reddit:before{content:"\eeb9"}.bxl-redux:before{content:"\eeba"}.bxl-sass:before{content:"\eebb"}.bxl-shopify:before{content:"\eebc"}.bxl-skype:before{content:"\eebd"}.bxl-slack:before{content:"\eebe"}.bxl-slack-old:before{content:"\eebf"}.bxl-snapchat:before{content:"\eec0"}.bxl-soundcloud:before{content:"\eec1"}.bxl-spotify:before{content:"\eec2"}.bxl-spring-boot:before{content:"\eec3"}.bxl-squarespace:before{content:"\eec4"}.bxl-stack-overflow:before{content:"\eec5"}.bxl-stripe:before{content:"\eec6"}.bxl-telegram:before{content:"\eec7"}.bxl-trello:before{content:"\eec8"}.bxl-tumblr:before{content:"\eec9"}.bxl-tux:before{content:"\eeca"}.bxl-twitch:before{content:"\eecb"}.bxl-twitter:before{content:"\eecc"}.bxl-unsplash:before{content:"\eecd"}.bxl-vimeo:before{content:"\eece"}.bxl-visa:before{content:"\eecf"}.bxl-vk:before{content:"\eed0"}.bxl-vuejs:before{content:"\eed1"}.bxl-whatsapp:before{content:"\eed2"}.bxl-whatsapp-square:before{content:"\eed3"}.bxl-wikipedia:before{content:"\eed4"}.bxl-windows:before{content:"\eed5"}.bxl-wix:before{content:"\eed6"}.bxl-wordpress:before{content:"\eed7"}.bxl-yahoo:before{content:"\eed8"}.bxl-yelp:before{content:"\eed9"}.bxl-youtube:before{content:"\eeda"}.bxl-zoom:before{content:"\eedb"} 2 | --------------------------------------------------------------------------------