├── README.md ├── ntask-api ├── .babelrc ├── .gitignore ├── README.md ├── auth.js ├── clusters.js ├── db.js ├── index.js ├── libs │ ├── boot.js │ ├── config.development.js │ ├── config.js │ ├── config.test.js │ ├── logger.js │ └── middlewares.js ├── models │ ├── tasks.js │ └── users.js ├── ntask.cert ├── ntask.key ├── package.json ├── routes │ ├── index.js │ ├── tasks.js │ ├── token.js │ └── users.js └── test │ ├── helpers.js │ ├── mocha.opts │ └── routes │ ├── index.js │ ├── tasks.js │ ├── token.js │ └── users.js └── ntask-web ├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── public ├── css │ ├── ionic.min.css │ └── ionicons.min.css ├── fonts │ ├── ionicons.eot │ ├── ionicons.svg │ ├── ionicons.ttf │ └── ionicons.woff ├── index.html └── js │ └── app.js └── src ├── app.js ├── components ├── menu.js ├── signin.js ├── signup.js ├── taskForm.js ├── tasks.js └── user.js ├── index.js ├── ntask.js └── templates ├── footer.js ├── signin.js ├── signup.js ├── taskForm.js ├── tasks.js └── user.js /README.md: -------------------------------------------------------------------------------- 1 | # Building APIs with Node.js 2 | 3 | ## About 4 | 5 | This repository have all pilot projects which are used in the ebook: [Building APIs with Node.js](http://www.apress.com/br/book/9781484224410). 6 | 7 | 8 | ## Author 9 | 10 | Caio Ribeiro Pereira - [https://crpwebdev.github.io](https://crpwebdev.github.io) 11 | -------------------------------------------------------------------------------- /ntask-api/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /ntask-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | ntask.sqlite 4 | ntask_test.sqlite 5 | .DS_Store 6 | public/apidoc 7 | logs/*.log 8 | -------------------------------------------------------------------------------- /ntask-api/README.md: -------------------------------------------------------------------------------- 1 | # NTask API 2 | 3 | This is a API project used for study purposes and it is one of the pilot project of the ebook: [Building APIs with Node.js](https://leanpub.com/building-apis-with-nodejs). 4 | 5 | ## Setup 6 | 7 | * Clone this repo: `git clone git@github.com:caio-ribeiro-pereira/building-apis-with-nodejs.git` 8 | * Access the folder: `cd ntask-api` 9 | * Install dependencies: `npm install` 10 | * Boot the server: `npm start` 11 | * Running tests: `npm test` 12 | * Accessing API: `https://localhost:3000` (Yes, HTTPS!) 13 | ## About 14 | 15 | Caio Ribeiro Pereira - [crpwebdev.com](http://crpwebdev.com) 16 | -------------------------------------------------------------------------------- /ntask-api/auth.js: -------------------------------------------------------------------------------- 1 | import passport from "passport"; 2 | import {Strategy, ExtractJwt} from "passport-jwt"; 3 | 4 | module.exports = app => { 5 | const Users = app.db.models.Users; 6 | const cfg = app.libs.config; 7 | const params = { 8 | secretOrKey: cfg.jwtSecret, 9 | jwtFromRequest: ExtractJwt.fromAuthHeader() 10 | }; 11 | const strategy = new Strategy(params, (payload, done) => { 12 | Users.findById(payload.id) 13 | .then(user => { 14 | if (user) { 15 | return done(null, { 16 | id: user.id, 17 | email: user.email 18 | }); 19 | } 20 | return done(null, false); 21 | }) 22 | .catch(error => done(error, null)); 23 | }); 24 | passport.use(strategy); 25 | return { 26 | initialize: () => { 27 | return passport.initialize(); 28 | }, 29 | authenticate: () => { 30 | return passport.authenticate("jwt", cfg.jwtSession); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /ntask-api/clusters.js: -------------------------------------------------------------------------------- 1 | import cluster from "cluster"; 2 | import os from "os"; 3 | 4 | const CPUS = os.cpus(); 5 | if (cluster.isMaster) { 6 | CPUS.forEach(() => cluster.fork()); 7 | cluster.on("listening", worker => { 8 | console.log("Cluster %d connected", worker.process.pid); 9 | }); 10 | cluster.on("disconnect", worker => { 11 | console.log("Cluster %d disconnected", worker.process.pid); 12 | }); 13 | cluster.on("exit", worker => { 14 | console.log("Cluster %d is dead", worker.process.pid); 15 | cluster.fork(); 16 | // Ensure to starts a new cluster if an old one dies 17 | }); 18 | } else { 19 | require("./index.js"); 20 | } 21 | -------------------------------------------------------------------------------- /ntask-api/db.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import Sequelize from "sequelize"; 4 | 5 | let db = null; 6 | 7 | module.exports = app => { 8 | if (!db) { 9 | const config = app.libs.config; 10 | const sequelize = new Sequelize( 11 | config.database, 12 | config.username, 13 | config.password, 14 | config.params 15 | ); 16 | db = { 17 | sequelize, 18 | Sequelize, 19 | models: {} 20 | }; 21 | const dir = path.join(__dirname, "models"); 22 | fs.readdirSync(dir).forEach(file => { 23 | const modelDir = path.join(dir, file); 24 | const model = sequelize.import(modelDir); 25 | db.models[model.name] = model; 26 | }); 27 | Object.keys(db.models).forEach(key => { 28 | db.models[key].associate(db.models); 29 | }); 30 | } 31 | return db; 32 | }; 33 | -------------------------------------------------------------------------------- /ntask-api/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import consign from "consign"; 3 | 4 | const app = express(); 5 | 6 | consign({verbose: false}) 7 | .include("libs/config.js") 8 | .then("db.js") 9 | .then("auth.js") 10 | .then("libs/middlewares.js") 11 | .then("routes") 12 | .then("libs/boot.js") 13 | .into(app); 14 | 15 | module.exports = app; 16 | -------------------------------------------------------------------------------- /ntask-api/libs/boot.js: -------------------------------------------------------------------------------- 1 | import https from "https"; 2 | import fs from "fs"; 3 | 4 | module.exports = app => { 5 | if (process.env.NODE_ENV !== "test") { 6 | const credentials = { 7 | key: fs.readFileSync("ntask.key", "utf8"), 8 | cert: fs.readFileSync("ntask.cert", "utf8") 9 | } 10 | app.db.sequelize.sync().done(() => { 11 | https.createServer(credentials, app) 12 | .listen(app.get("port"), () => { 13 | console.log(`NTask API - Port ${app.get("port")}`); 14 | }); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /ntask-api/libs/config.development.js: -------------------------------------------------------------------------------- 1 | import logger from "./logger.js"; 2 | 3 | module.exports = { 4 | database: "ntask", 5 | username: "", 6 | password: "", 7 | params: { 8 | dialect: "sqlite", 9 | storage: "ntask.sqlite", 10 | logging: (sql) => { 11 | logger.info(`[${new Date()}] ${sql}`); 12 | }, 13 | define: { 14 | underscored: true 15 | } 16 | }, 17 | jwtSecret: "Nta$K-AP1", 18 | jwtSession: {session: false} 19 | }; 20 | -------------------------------------------------------------------------------- /ntask-api/libs/config.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const env = process.env.NODE_ENV; 3 | if (env) { 4 | return require(`./config.${env}.js`); 5 | } 6 | return require("./config.development.js"); 7 | }; 8 | -------------------------------------------------------------------------------- /ntask-api/libs/config.test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | database: "ntask_test", 3 | username: "", 4 | password: "", 5 | params: { 6 | dialect: "sqlite", 7 | storage: "ntask.sqlite", 8 | logging: false, 9 | define: { 10 | underscored: true 11 | } 12 | }, 13 | jwtSecret: "NTASK_TEST", 14 | jwtSession: {session: false} 15 | }; 16 | -------------------------------------------------------------------------------- /ntask-api/libs/logger.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import winston from "winston"; 3 | 4 | if (!fs.existsSync("logs")) { 5 | fs.mkdirSync("logs"); 6 | } 7 | 8 | module.exports = new winston.Logger({ 9 | transports: [ 10 | new winston.transports.File({ 11 | level: "info", 12 | filename: "logs/app.log", 13 | maxsize: 1048576, 14 | maxFiles: 10, 15 | colorize: false 16 | }) 17 | ] 18 | }); 19 | -------------------------------------------------------------------------------- /ntask-api/libs/middlewares.js: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import express from "express"; 3 | import morgan from "morgan"; 4 | import cors from "cors"; 5 | import compression from "compression"; 6 | import helmet from "helmet"; 7 | import logger from "./logger.js"; 8 | 9 | module.exports = app => { 10 | app.set("port", 3000); 11 | app.set("json spaces", 4); 12 | app.use(morgan("common", { 13 | stream: { 14 | write: (message) => { 15 | logger.info(message); 16 | } 17 | } 18 | })); 19 | app.use(helmet()); 20 | app.use(cors({ 21 | origin: ["http://localhost:3001"], 22 | methods: ["GET", "POST", "PUT", "DELETE"], 23 | allowedHeaders: ["Content-Type", "Authorization"] 24 | })); 25 | app.use(compression()); 26 | app.use(bodyParser.json()); 27 | app.use(app.auth.initialize()); 28 | app.use((req, res, next) => { 29 | delete req.body.id; 30 | next(); 31 | }); 32 | app.use(express.static("public")); 33 | }; 34 | -------------------------------------------------------------------------------- /ntask-api/models/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataType) => { 2 | const Tasks = sequelize.define("Tasks", { 3 | id: { 4 | type: DataType.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true 7 | }, 8 | title: { 9 | type: DataType.STRING, 10 | allowNull: false, 11 | validate: { 12 | notEmpty: true 13 | } 14 | }, 15 | done: { 16 | type: DataType.BOOLEAN, 17 | allowNull: false, 18 | defaultValue: false 19 | } 20 | }, { 21 | classMethods: { 22 | associate: (models) => { 23 | Tasks.belongsTo(models.Users); 24 | } 25 | } 26 | }); 27 | return Tasks; 28 | }; 29 | -------------------------------------------------------------------------------- /ntask-api/models/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | 3 | module.exports = (sequelize, DataType) => { 4 | const Users = sequelize.define("Users", { 5 | id: { 6 | type: DataType.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | name: { 11 | type: DataType.STRING, 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true 15 | } 16 | }, 17 | password: { 18 | type: DataType.STRING, 19 | allowNull: false, 20 | validate: { 21 | notEmpty: true 22 | } 23 | }, 24 | email: { 25 | type: DataType.STRING, 26 | unique: true, 27 | allowNull: false, 28 | validate: { 29 | notEmpty: true 30 | } 31 | } 32 | }, { 33 | hooks: { 34 | beforeCreate: user => { 35 | const salt = bcrypt.genSaltSync(); 36 | user.password = bcrypt.hashSync(user.password, salt); 37 | } 38 | }, 39 | classMethods: { 40 | associate: models => { 41 | Users.hasMany(models.Tasks); 42 | }, 43 | isPassword: (encodedPassword, password) => { 44 | return bcrypt.compareSync(password, encodedPassword); 45 | } 46 | } 47 | }); 48 | return Users; 49 | }; 50 | -------------------------------------------------------------------------------- /ntask-api/ntask.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8zCCAdugAwIBAgIJAMltM6AKqPQxMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV 3 | BAMMBW50YXNrMB4XDTE1MTAwNDE2MzIzNVoXDTI1MTAwMTE2MzIzNVowEDEOMAwG 4 | A1UEAwwFbnRhc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ6P38 5 | AGoIpHpgOGdEYfh0/Tf5OL0XPFh2PK+wWsCCZzgmL5RUnfV+Gw3wR/rwfHeOJSXy 6 | cSwq7UJe0Rf0EnMULBpp+oeb2iGqbcAke7DTOzaihcp27l+zzNK/BvXq/BtI7Plm 7 | 4mziIuCf3QOeQemkPEiztGZKEJeEE/AFSZmx4J1ugJ0G+fqjkMWTXcYj0RWlqG2y 8 | LBmDFErQANaUeYOrjdSIp96MBFLC8afYnNp/XrItm1XqQ29mUpbwRw2qnR+WdM6S 9 | YZiNcEo/eXLDBnLYiJjSqoiY5hd6CiCO3yMv4IT/LdR1V+FTDg5KecQ7WD5/IKIA 10 | 44844xZzVGkgt3xPAgMBAAGjUDBOMB0GA1UdDgQWBBTAx3g/qsJRyaYpDS6AGTaT 11 | f43bLTAfBgNVHSMEGDAWgBTAx3g/qsJRyaYpDS6AGTaTf43bLTAMBgNVHRMEBTAD 12 | AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC2Pwv5W/DQ2OFmHsGPVRwGBY0UP0PuTxpX 13 | 4rOe9ZtJDi2QRybA59iEdTT3pe7FX+LPd0DUiJswwYF8+BYDVjJRHiIdcFDC7Jqb 14 | nk3g1JdxzzMdhPpexHkkYzU8Zaxl1wkp3KDlrfqRD2dYSzaToiR8UjkDAwjXcfmA 15 | 489HPKBtZLWydbV/3J9EW/I6zmKgampdIn0qOp6FlCN2dgfarpD8bUxruLgN2wFu 16 | Bb8g3hzQo5ZC7Ws98TIh0B8MFidzMuLjqr1azm147wUr/NfnXS1a4w7/r302sGwa 17 | OuNmGHImnUpAnVpjV482M/DdUy5VYP0M45ImzX+rbJOUffFobx6E 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /ntask-api/ntask.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyej9/ABqCKR6YDhnRGH4dP03+Ti9FzxYdjyvsFrAgmc4Ji+U 3 | VJ31fhsN8Ef68Hx3jiUl8nEsKu1CXtEX9BJzFCwaafqHm9ohqm3AJHuw0zs2ooXK 4 | du5fs8zSvwb16vwbSOz5ZuJs4iLgn90DnkHppDxIs7RmShCXhBPwBUmZseCdboCd 5 | Bvn6o5DFk13GI9EVpahtsiwZgxRK0ADWlHmDq43UiKfejARSwvGn2Jzaf16yLZtV 6 | 6kNvZlKW8EcNqp0flnTOkmGYjXBKP3lywwZy2IiY0qqImOYXegogjt8jL+CE/y3U 7 | dVfhUw4OSnnEO1g+fyCiAOOPOOMWc1RpILd8TwIDAQABAoIBAHKTuv10Jre8zo0n 8 | tMJDbkDFKSxOHE/BONnv2isTdMcLV/ujaGMUOClVpPVDg41QtG9/eSc5Pb0mYlF4 9 | CkXA6nj6Bgs51haFFDGoki6h2lgj8/8KOTiAUOKxSq6IfqjYY4tgnq7Zsrwo2psd 10 | Sl5WPQWsB/2iU6GYBMM4pS369DLRp5RIu7fvGv4gT9hWxQldNCg0oDadsm185FBL 11 | 0TlKDd2fFnt0Be96yDpwli/Kd2xh3x58/J3AZNrC3dQl8LLPgoo+YvPSzz3Y/Ilj 12 | HImngbQY/c2k6F8wun8H1x0MhBFrN52Q8F0YSLMd3EDR9aaZzGUJfsbSjMsklSIz 13 | Q74mHCECgYEA622fv37f3+V2Uz/H8Tb/r/ym5kHtVxzxszQ84gpzNQjD8IZcfIbu 14 | AldJocHpKuWeN4W5Ttvi5WyH4o77FRz2OBHvmdTmWkrbISH0x7JKq4442PAYamD1 15 | iFCJrLBYVK+6mycBILRqm32eJARrTuBN5DHgvEiXg0DkX9GAxZrhSJcCgYEA242X 16 | FlSNV8OLzjCubJU+Vv0MijX7tMnzmziWCAabxOJUyWEsk4QjoQTL4gQjqNXIDfKt 17 | 162n7mKk7/GJZOG9gC7lZw7AIGwTQ9WyjfvgQIbFo2V9zDF2KGDX5oeW8qPISi52 18 | l0TocSKxy5T+7QQJMH+8Be7OSVvIlEAMu0LqaQkCgYAdPq/ibNNIj8uECd8/cpKO 19 | fPcKkVP3R0wq86lAdwXap60XWslwWp6EQe2On3TkdEOUKBNd3WixEStMFHDSLZfU 20 | XT4DQPQgcT4JPpuWluo5p2AeaqzNwh+eAEsp3XoLgwzOKykzs9WuXQtg8/+Ue76R 21 | QzTkjqvrjQsRcAfsBBJKHwKBgHKhDVZKVPWSkibYQelNTpwKSIbMwptUqYzMUYDl 22 | OmTkKpJt2uE2J4gFQhHCSX/4BhhKMTufXkNXW3gvaqWyOsd3NKzHBcanxrMvGqeI 23 | 7z+hXgT+k1yOInvYfEDPYB9VJdidQ6uc/aM8EwoQw7yp08ZvmpKaaTfh5OqKOlt3 24 | B35JAoGBALZ78bt2PJKdQyxO8WtSBUsD8lrB91qyNJRZhM2fMnAVe9eKAeOXtyD5 25 | DRsSp65CUMzBXN7f0JYutumBh5LSUX1RYzNtwcL4/Rj8mXMssYBE3TQ3Ajc8nAES 26 | W3uk7jU0ZNjRsAbpgaiGEH1uGXH7euC+XMpfDILAptxsam8r+ODA 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /ntask-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ntask-api", 3 | "version": "1.0.0", 4 | "description": "Task list API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run apidoc && npm run clusters", 8 | "clusters": "babel-node clusters.js", 9 | "test": "NODE_ENV=test mocha test/**/*.js", 10 | "apidoc": "apidoc -i routes/ -o public/apidoc" 11 | }, 12 | "apidoc": { 13 | "name": "Node Task API - Documentation", 14 | "template": { 15 | "forceLanguage": "en" 16 | } 17 | }, 18 | "author": "Caio Ribeiro Pereira", 19 | "dependencies": { 20 | "babel-cli": "^6.5.1", 21 | "babel-preset-es2015": "^6.5.0", 22 | "bcrypt": "^0.8.5", 23 | "body-parser": "^1.15.0", 24 | "compression": "^1.6.1", 25 | "consign": "^0.1.2", 26 | "cors": "^2.7.1", 27 | "express": "^4.13.4", 28 | "helmet": "^1.1.0", 29 | "jwt-simple": "^0.4.1", 30 | "morgan": "^1.6.1", 31 | "passport": "^0.3.2", 32 | "passport-jwt": "^2.0.0", 33 | "sequelize": "^3.19.2", 34 | "sqlite3": "^3.1.1", 35 | "winston": "^2.1.1" 36 | }, 37 | "devDependencies": { 38 | "apidoc": "^0.15.1", 39 | "babel-register": "^6.5.2", 40 | "chai": "^3.5.0", 41 | "mocha": "^2.4.5", 42 | "supertest": "^1.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ntask-api/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | /** 3 | * @api {get} / API Status 4 | * @apiGroup Status 5 | * @apiSuccess {String} status API Status' message 6 | * @apiSuccessExample {json} Success 7 | * HTTP/1.1 200 OK 8 | * {"status": "NTask API"} 9 | */ 10 | app.get("/", (req, res) => { 11 | res.json({status: "NTask API"}); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /ntask-api/routes/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const Tasks = app.db.models.Tasks; 3 | 4 | app.route("/tasks") 5 | .all(app.auth.authenticate()) 6 | /** 7 | * @api {get} /tasks List the user's tasks 8 | * @apiGroup Tasks 9 | * @apiHeader {String} Authorization Token of authenticated user 10 | * @apiHeaderExample {json} Header 11 | * {"Authorization": "JWT xyz.abc.123.hgf"} 12 | * @apiSuccess {Object[]} tasks Task's list 13 | * @apiSuccess {Number} tasks.id Task id 14 | * @apiSuccess {String} tasks.title Task title 15 | * @apiSuccess {Boolean} tasks.done Task is done? 16 | * @apiSuccess {Date} tasks.updated_at Update's date 17 | * @apiSuccess {Date} tasks.created_at Register's date 18 | * @apiSuccess {Number} tasks.user_id User id 19 | * @apiSuccessExample {json} Success 20 | * HTTP/1.1 200 OK 21 | * [{ 22 | * "id": 1, 23 | * "title": "Study", 24 | * "done": false, 25 | * "updated_at": "2016-02-10T15:46:51.778Z", 26 | * "created_at": "2016-02-10T15:46:51.778Z", 27 | * "user_id": 1 28 | * }] 29 | * @apiErrorExample {json} List error 30 | * HTTP/1.1 412 Precondition Failed 31 | */ 32 | .get((req, res) => { 33 | Tasks.findAll({ 34 | where: { user_id: req.user.id } 35 | }) 36 | .then(result => res.json(result)) 37 | .catch(error => { 38 | res.status(412).json({msg: error.message}); 39 | }); 40 | }) 41 | /** 42 | * @api {post} /tasks Register a new task 43 | * @apiGroup Tasks 44 | * @apiHeader {String} Authorization Token of authenticated user 45 | * @apiHeaderExample {json} Header 46 | * {"Authorization": "JWT xyz.abc.123.hgf"} 47 | * @apiParam {String} title Task title 48 | * @apiParamExample {json} Input 49 | * {"title": "Study"} 50 | * @apiSuccess {Number} id Task id 51 | * @apiSuccess {String} title Task title 52 | * @apiSuccess {Boolean} done false Task is done? 53 | * @apiSuccess {Date} updated_at Update's date 54 | * @apiSuccess {Date} created_at Register's date 55 | * @apiSuccess {Number} user_id User id 56 | * @apiSuccessExample {json} Success 57 | * HTTP/1.1 200 OK 58 | * { 59 | * "id": 1, 60 | * "title": "Study", 61 | * "done": false, 62 | * "updated_at": "2016-02-10T15:46:51.778Z", 63 | * "created_at": "2016-02-10T15:46:51.778Z", 64 | * "user_id": 1 65 | * } 66 | * @apiErrorExample {json} Register error 67 | * HTTP/1.1 412 Precondition Failed 68 | */ 69 | .post((req, res) => { 70 | req.body.user_id = req.user.id; 71 | Tasks.create(req.body) 72 | .then(result => res.json(result)) 73 | .catch(error => { 74 | res.status(412).json({msg: error.message}); 75 | }); 76 | }); 77 | 78 | app.route("/tasks/:id") 79 | .all(app.auth.authenticate()) 80 | /** 81 | * @api {get} /tasks/:id Get a task 82 | * @apiGroup Tasks 83 | * @apiHeader {String} Authorization Token of authenticated user 84 | * @apiHeaderExample {json} Header 85 | * {"Authorization": "JWT xyz.abc.123.hgf"} 86 | * @apiParam {id} id Task id 87 | * @apiSuccess {Number} id Task id 88 | * @apiSuccess {String} title Task title 89 | * @apiSuccess {Boolean} done Task is done? 90 | * @apiSuccess {Date} updated_at Update's date 91 | * @apiSuccess {Date} created_at Register's date 92 | * @apiSuccess {Number} user_id User id 93 | * @apiSuccessExample {json} Success 94 | * HTTP/1.1 200 OK 95 | * { 96 | * "id": 1, 97 | * "title": "Study", 98 | * "done": false 99 | * "updated_at": "2016-02-10T15:46:51.778Z", 100 | * "created_at": "2016-02-10T15:46:51.778Z", 101 | * "user_id": 1 102 | * } 103 | * @apiErrorExample {json} Task not found error 104 | * HTTP/1.1 404 Not Found 105 | * @apiErrorExample {json} Find error 106 | * HTTP/1.1 412 Precondition Failed 107 | */ 108 | .get((req, res) => { 109 | Tasks.findOne({ where: { 110 | id: req.params.id, 111 | user_id: req.user.id 112 | }}) 113 | .then(result => { 114 | if (result) { 115 | res.json(result); 116 | } else { 117 | res.sendStatus(404); 118 | } 119 | }) 120 | .catch(error => { 121 | res.status(412).json({msg: error.message}); 122 | }); 123 | }) 124 | /** 125 | * @api {put} /tasks/:id Update a task 126 | * @apiGroup Tasks 127 | * @apiHeader {String} Authorization Token of authenticated user 128 | * @apiHeaderExample {json} Header 129 | * {"Authorization": "JWT xyz.abc.123.hgf"} 130 | * @apiParam {id} id Task id 131 | * @apiParam {String} title Task title 132 | * @apiParam {Boolean} done Task is done? 133 | * @apiParamExample {json} Input 134 | * { 135 | * "title": "Work", 136 | * "done": true 137 | * } 138 | * @apiSuccessExample {json} Success 139 | * HTTP/1.1 204 No Content 140 | * @apiErrorExample {json} Update error 141 | * HTTP/1.1 412 Precondition Failed 142 | */ 143 | .put((req, res) => { 144 | Tasks.update(req.body, { where: { 145 | id: req.params.id, 146 | user_id: req.user.id 147 | }}) 148 | .then(result => res.sendStatus(204)) 149 | .catch(error => { 150 | res.status(412).json({msg: error.message}); 151 | }); 152 | }) 153 | /** 154 | * @api {delete} /tasks/:id Remove a task 155 | * @apiGroup Tasks 156 | * @apiHeader {String} Authorization Token of authenticated user 157 | * @apiHeaderExample {json} Header 158 | * {"Authorization": "JWT xyz.abc.123.hgf"} 159 | * @apiParam {id} id Task id 160 | * @apiSuccessExample {json} Success 161 | * HTTP/1.1 204 No Content 162 | * @apiErrorExample {json} Delete error 163 | * HTTP/1.1 412 Precondition Failed 164 | */ 165 | .delete((req, res) => { 166 | Tasks.destroy({ where: { 167 | id: req.params.id, 168 | user_id: req.user.id 169 | }}) 170 | .then(result => res.sendStatus(204)) 171 | .catch(error => { 172 | res.status(412).json({msg: error.message}); 173 | }); 174 | }); 175 | }; 176 | -------------------------------------------------------------------------------- /ntask-api/routes/token.js: -------------------------------------------------------------------------------- 1 | import jwt from "jwt-simple"; 2 | 3 | module.exports = app => { 4 | const cfg = app.libs.config; 5 | const Users = app.db.models.Users; 6 | 7 | /** 8 | * @api {post} /token Authentication Token 9 | * @apiGroup Credentials 10 | * @apiParam {String} email User email 11 | * @apiParam {String} password User password 12 | * @apiParamExample {json} Input 13 | * { 14 | * "email": "john@connor.net", 15 | * "password": "123456" 16 | * } 17 | * @apiSuccess {String} token Token of authenticated user 18 | * @apiSuccessExample {json} Success 19 | * HTTP/1.1 200 OK 20 | * {"token": "xyz.abc.123.hgf"} 21 | * @apiErrorExample {json} Authentication error 22 | * HTTP/1.1 401 Unauthorized 23 | */ 24 | app.post("/token", (req, res) => { 25 | if (req.body.email && req.body.password) { 26 | const email = req.body.email; 27 | const password = req.body.password; 28 | Users.findOne({where: {email: email}}) 29 | .then(user => { 30 | if (Users.isPassword(user.password, password)) { 31 | const payload = {id: user.id}; 32 | res.json({ 33 | token: jwt.encode(payload, cfg.jwtSecret) 34 | }); 35 | } else { 36 | res.sendStatus(401); 37 | } 38 | }) 39 | .catch(error => res.sendStatus(401)); 40 | } else { 41 | res.sendStatus(401); 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /ntask-api/routes/users.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const Users = app.db.models.Users; 3 | 4 | app.route("/user") 5 | .all(app.auth.authenticate()) 6 | /** 7 | * @api {get} /user Return the authenticated user's data 8 | * @apiGroup User 9 | * @apiHeader {String} Authorization Token of authenticated user 10 | * @apiHeaderExample {json} Header 11 | * {"Authorization": "JWT xyz.abc.123.hgf"} 12 | * @apiSuccess {Number} id User id 13 | * @apiSuccess {String} name User name 14 | * @apiSuccess {String} email User email 15 | * @apiSuccessExample {json} Success 16 | * HTTP/1.1 200 OK 17 | * { 18 | * "id": 1, 19 | * "name": "John Connor", 20 | * "email": "john@connor.net" 21 | * } 22 | * @apiErrorExample {json} Find error 23 | * HTTP/1.1 412 Precondition Failed 24 | */ 25 | .get((req, res) => { 26 | Users.findById(req.user.id, { 27 | attributes: ["id", "name", "email"] 28 | }) 29 | .then(result => res.json(result)) 30 | .catch(error => { 31 | res.status(412).json({msg: error.message}); 32 | }); 33 | }) 34 | /** 35 | * @api {delete} /user Deletes an authenticated user 36 | * @apiGroup User 37 | * @apiHeader {String} Authorization Token of authenticated user 38 | * @apiHeaderExample {json} Header 39 | * {"Authorization": "JWT xyz.abc.123.hgf"} 40 | * @apiSuccessExample {json} Success 41 | * HTTP/1.1 204 No Content 42 | * @apiErrorExample {json} Delete error 43 | * HTTP/1.1 412 Precondition Failed 44 | */ 45 | .delete((req, res) => { 46 | Users.destroy({where: {id: req.user.id} }) 47 | .then(result => res.sendStatus(204)) 48 | .catch(error => { 49 | res.status(412).json({msg: error.message}); 50 | }); 51 | }); 52 | 53 | /** 54 | * @api {post} /users Register a new user 55 | * @apiGroup User 56 | * @apiParam {String} name User name 57 | * @apiParam {String} email User email 58 | * @apiParam {String} password User password 59 | * @apiParamExample {json} Input 60 | * { 61 | * "name": "John Connor", 62 | * "email": "john@connor.net", 63 | * "password": "123456" 64 | * } 65 | * @apiSuccess {Number} id User id 66 | * @apiSuccess {String} name User name 67 | * @apiSuccess {String} email User email 68 | * @apiSuccess {String} password User encrypted password 69 | * @apiSuccess {Date} updated_at Update's date 70 | * @apiSuccess {Date} created_at Register's date 71 | * @apiSuccessExample {json} Success 72 | * HTTP/1.1 200 OK 73 | * { 74 | * "id": 1, 75 | * "name": "John Connor", 76 | * "email": "john@connor.net", 77 | * "password": "$2a$10$SK1B1", 78 | * "updated_at": "2016-02-10T15:20:11.700Z", 79 | * "created_at": "2016-02-10T15:29:11.700Z" 80 | * } 81 | * @apiErrorExample {json} Register error 82 | * HTTP/1.1 412 Precondition Failed 83 | */ 84 | app.post("/users", (req, res) => { 85 | Users.create(req.body) 86 | .then(result => res.json(result)) 87 | .catch(error => { 88 | res.status(412).json({msg: error.message}); 89 | }); 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /ntask-api/test/helpers.js: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import chai from "chai"; 3 | import app from "../index.js"; 4 | 5 | global.app = app; 6 | global.request = supertest(app); 7 | global.expect = chai.expect; 8 | -------------------------------------------------------------------------------- /ntask-api/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/helpers 2 | --reporter spec 3 | --compilers js:babel-register 4 | --slow 5000 5 | -------------------------------------------------------------------------------- /ntask-api/test/routes/index.js: -------------------------------------------------------------------------------- 1 | describe("Routes: Index", () => { 2 | describe("GET /", () => { 3 | it("returns the API status", done => { 4 | request.get("/") 5 | .expect(200) 6 | .end((err, res) => { 7 | const expected = {status: "NTask API"}; 8 | expect(res.body).to.eql(expected); 9 | done(err); 10 | }); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /ntask-api/test/routes/tasks.js: -------------------------------------------------------------------------------- 1 | import jwt from "jwt-simple"; 2 | 3 | describe("Routes: Tasks", () => { 4 | const Users = app.db.models.Users; 5 | const Tasks = app.db.models.Tasks; 6 | const jwtSecret = app.libs.config.jwtSecret; 7 | let token; 8 | let fakeTask; 9 | beforeEach(done => { 10 | Users 11 | .destroy({where: {}}) 12 | .then(() => Users.create({ 13 | name: "John", 14 | email: "john@mail.net", 15 | password: "12345" 16 | })) 17 | .then(user => { 18 | Tasks 19 | .destroy({where: {}}) 20 | .then(() => Tasks.bulkCreate([{ 21 | id: 1, 22 | title: "Work", 23 | user_id: user.id 24 | }, { 25 | id: 2, 26 | title: "Study", 27 | user_id: user.id 28 | }])) 29 | .then(tasks => { 30 | fakeTask = tasks[0]; 31 | token = jwt.encode({id: user.id}, jwtSecret); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | describe("GET /tasks", () => { 37 | describe("status 200", () => { 38 | it("returns a list of tasks", done => { 39 | request.get("/tasks") 40 | .set("Authorization", `JWT ${token}`) 41 | .expect(200) 42 | .end((err, res) => { 43 | expect(res.body).to.have.length(2); 44 | expect(res.body[0].title).to.eql("Work"); 45 | expect(res.body[1].title).to.eql("Study"); 46 | done(err); 47 | }); 48 | }); 49 | }); 50 | }); 51 | describe("POST /tasks", () => { 52 | describe("status 200", () => { 53 | it("creates a new task", done => { 54 | request.post("/tasks") 55 | .set("Authorization", `JWT ${token}`) 56 | .send({title: "Run"}) 57 | .expect(200) 58 | .end((err, res) => { 59 | expect(res.body.title).to.eql("Run"); 60 | expect(res.body.done).to.be.false; 61 | done(err); 62 | }); 63 | }); 64 | }); 65 | }); 66 | describe("GET /tasks/:id", () => { 67 | describe("status 200", () => { 68 | it("returns one task", done => { 69 | request.get(`/tasks/${fakeTask.id}`) 70 | .set("Authorization", `JWT ${token}`) 71 | .expect(200) 72 | .end((err, res) => { 73 | expect(res.body.title).to.eql("Work"); 74 | done(err); 75 | }); 76 | }); 77 | }); 78 | describe("status 404", () => { 79 | it("throws error when task not exist", done => { 80 | request.get("/tasks/0") 81 | .set("Authorization", `JWT ${token}`) 82 | .expect(404) 83 | .end((err, res) => done(err)); 84 | }); 85 | }); 86 | }); 87 | describe("PUT /tasks/:id", () => { 88 | describe("status 204", () => { 89 | it("updates a task", done => { 90 | request.put(`/tasks/${fakeTask.id}`) 91 | .set("Authorization", `JWT ${token}`) 92 | .send({ 93 | title: "Travel", 94 | done: true 95 | }) 96 | .expect(204) 97 | .end((err, res) => done(err)); 98 | }); 99 | }); 100 | }); 101 | describe("DELETE /tasks/:id", () => { 102 | describe("status 204", () => { 103 | it("removes a task", done => { 104 | request.delete(`/tasks/${fakeTask.id}`) 105 | .set("Authorization", `JWT ${token}`) 106 | .expect(204) 107 | .end((err, res) => done(err)); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /ntask-api/test/routes/token.js: -------------------------------------------------------------------------------- 1 | describe("Routes: Token", () => { 2 | const Users = app.db.models.Users; 3 | describe("POST /token", () => { 4 | beforeEach(done => { 5 | Users 6 | .destroy({where: {}}) 7 | .then(() => Users.create({ 8 | name: "John", 9 | email: "john@mail.net", 10 | password: "12345" 11 | })) 12 | .then(() => done()); 13 | }); 14 | describe("status 200", () => { 15 | it("returns authenticated user token", done => { 16 | request.post("/token") 17 | .send({ 18 | email: "john@mail.net", 19 | password: "12345" 20 | }) 21 | .expect(200) 22 | .end((err, res) => { 23 | expect(res.body).to.include.keys("token"); 24 | done(err); 25 | }); 26 | }); 27 | }); 28 | describe("status 401", () => { 29 | it("throws error when password is incorrect", done => { 30 | request.post("/token") 31 | .send({ 32 | email: "john@mail.net", 33 | password: "WRONG_PASSWORD" 34 | }) 35 | .expect(401) 36 | .end((err, res) => { 37 | done(err); 38 | }); 39 | }); 40 | it("throws error when email not exist", done => { 41 | request.post("/token") 42 | .send({ 43 | email: "wrong@email.com", 44 | password: "12345" 45 | }) 46 | .expect(401) 47 | .end((err, res) => { 48 | done(err); 49 | }); 50 | }); 51 | it("throws error when email and password are blank", done => { 52 | request.post("/token") 53 | .expect(401) 54 | .end((err, res) => { 55 | done(err); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /ntask-api/test/routes/users.js: -------------------------------------------------------------------------------- 1 | import jwt from "jwt-simple"; 2 | 3 | describe("Routes: Users", () => { 4 | const Users = app.db.models.Users; 5 | const jwtSecret = app.libs.config.jwtSecret; 6 | let token; 7 | beforeEach(done => { 8 | Users 9 | .destroy({where: {}}) 10 | .then(() => Users.create({ 11 | name: "John", 12 | email: "john@mail.net", 13 | password: "12345" 14 | })) 15 | .then(user => { 16 | token = jwt.encode({id: user.id}, jwtSecret); 17 | done(); 18 | }); 19 | }); 20 | describe("GET /user", () => { 21 | describe("status 200", () => { 22 | it("returns an authenticated user", done => { 23 | request.get("/user") 24 | .set("Authorization", `JWT ${token}`) 25 | .expect(200) 26 | .end((err, res) => { 27 | expect(res.body.name).to.eql("John"); 28 | expect(res.body.email).to.eql("john@mail.net"); 29 | done(err); 30 | }); 31 | }); 32 | }); 33 | }); 34 | describe("DELETE /user", () => { 35 | describe("status 204", () => { 36 | it("deletes an authenticated user", done => { 37 | request.delete("/user") 38 | .set("Authorization", `JWT ${token}`) 39 | .expect(204) 40 | .end((err, res) => done(err)); 41 | }); 42 | }); 43 | }); 44 | describe("POST /users", () => { 45 | describe("status 200", () => { 46 | it("creates a new user", done => { 47 | request.post("/users") 48 | .send({ 49 | name: "Mary", 50 | email: "mary@mail.net", 51 | password: "12345" 52 | }) 53 | .expect(200) 54 | .end((err, res) => { 55 | expect(res.body.name).to.eql("Mary"); 56 | expect(res.body.email).to.eql("mary@mail.net"); 57 | done(err); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /ntask-web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /ntask-web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /ntask-web/README.md: -------------------------------------------------------------------------------- 1 | # NTask Web 2 | 3 | Web client app for [NTask API](https://github.com/caio-ribeiro-pereira/building-apis-with-nodejs/tree/master/ntask-api). 4 | 5 | This project is used for study purposes and it is one the pilot project of the ebook: [Building APIs with Node.js](https://leanpub.com/building-apis-with-nodejs). 6 | 7 | ## Setup 8 | 9 | * Clone this repo: `git clone git@github.com:caio-ribeiro-pereira/building-apis-with-nodejs.git` 10 | * Access the folder: `cd ntask-web` 11 | * Install dependencies: `npm install` 12 | * Boot the server: `npm start` 13 | 14 | ## About 15 | 16 | Caio Ribeiro Pereira - [crpwebdev.com](http://crpwebdev.com) 17 | -------------------------------------------------------------------------------- /ntask-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ntask-web", 3 | "version": "1.0.0", 4 | "description": "NTask web client application", 5 | "scripts": { 6 | "start": "npm run build && npm run server", 7 | "server": "http-server public -p 3001", 8 | "build": "browserify src -t babelify -o public/js/app.js" 9 | }, 10 | "author": "Caio Ribeiro Pereira", 11 | "dependencies": { 12 | "babel-preset-es2015": "^6.5.0", 13 | "babelify": "^7.2.0", 14 | "browser-request": "^0.3.3", 15 | "browserify": "^13.0.0", 16 | "http-server": "^0.9.0", 17 | "tiny-emitter": "^1.0.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ntask-web/public/css/ionicons.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! 2 | Ionicons, v2.0.0 3 | Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ 4 | https://twitter.com/benjsperry https://twitter.com/ionicframework 5 | MIT License: https://github.com/driftyco/ionicons 6 | 7 | Android-style icons originally built by Google’s 8 | Material Design Icons: https://github.com/google/material-design-icons 9 | used under CC BY http://creativecommons.org/licenses/by/4.0/ 10 | Modified icons to fit ionicon’s grid from original. 11 | */@font-face{font-family:"Ionicons";src:url("../fonts/ionicons.eot?v=2.0.0");src:url("../fonts/ionicons.eot?v=2.0.0#iefix") format("embedded-opentype"),url("../fonts/ionicons.ttf?v=2.0.0") format("truetype"),url("../fonts/ionicons.woff?v=2.0.0") format("woff"),url("../fonts/ionicons.svg?v=2.0.0#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-alert:before,.ion-alert-circled:before,.ion-android-add:before,.ion-android-add-circle:before,.ion-android-alarm-clock:before,.ion-android-alert:before,.ion-android-apps:before,.ion-android-archive:before,.ion-android-arrow-back:before,.ion-android-arrow-down:before,.ion-android-arrow-dropdown:before,.ion-android-arrow-dropdown-circle:before,.ion-android-arrow-dropleft:before,.ion-android-arrow-dropleft-circle:before,.ion-android-arrow-dropright:before,.ion-android-arrow-dropright-circle:before,.ion-android-arrow-dropup:before,.ion-android-arrow-dropup-circle:before,.ion-android-arrow-forward:before,.ion-android-arrow-up:before,.ion-android-attach:before,.ion-android-bar:before,.ion-android-bicycle:before,.ion-android-boat:before,.ion-android-bookmark:before,.ion-android-bulb:before,.ion-android-bus:before,.ion-android-calendar:before,.ion-android-call:before,.ion-android-camera:before,.ion-android-cancel:before,.ion-android-car:before,.ion-android-cart:before,.ion-android-chat:before,.ion-android-checkbox:before,.ion-android-checkbox-blank:before,.ion-android-checkbox-outline:before,.ion-android-checkbox-outline-blank:before,.ion-android-checkmark-circle:before,.ion-android-clipboard:before,.ion-android-close:before,.ion-android-cloud:before,.ion-android-cloud-circle:before,.ion-android-cloud-done:before,.ion-android-cloud-outline:before,.ion-android-color-palette:before,.ion-android-compass:before,.ion-android-contact:before,.ion-android-contacts:before,.ion-android-contract:before,.ion-android-create:before,.ion-android-delete:before,.ion-android-desktop:before,.ion-android-document:before,.ion-android-done:before,.ion-android-done-all:before,.ion-android-download:before,.ion-android-drafts:before,.ion-android-exit:before,.ion-android-expand:before,.ion-android-favorite:before,.ion-android-favorite-outline:before,.ion-android-film:before,.ion-android-folder:before,.ion-android-folder-open:before,.ion-android-funnel:before,.ion-android-globe:before,.ion-android-hand:before,.ion-android-hangout:before,.ion-android-happy:before,.ion-android-home:before,.ion-android-image:before,.ion-android-laptop:before,.ion-android-list:before,.ion-android-locate:before,.ion-android-lock:before,.ion-android-mail:before,.ion-android-map:before,.ion-android-menu:before,.ion-android-microphone:before,.ion-android-microphone-off:before,.ion-android-more-horizontal:before,.ion-android-more-vertical:before,.ion-android-navigate:before,.ion-android-notifications:before,.ion-android-notifications-none:before,.ion-android-notifications-off:before,.ion-android-open:before,.ion-android-options:before,.ion-android-people:before,.ion-android-person:before,.ion-android-person-add:before,.ion-android-phone-landscape:before,.ion-android-phone-portrait:before,.ion-android-pin:before,.ion-android-plane:before,.ion-android-playstore:before,.ion-android-print:before,.ion-android-radio-button-off:before,.ion-android-radio-button-on:before,.ion-android-refresh:before,.ion-android-remove:before,.ion-android-remove-circle:before,.ion-android-restaurant:before,.ion-android-sad:before,.ion-android-search:before,.ion-android-send:before,.ion-android-settings:before,.ion-android-share:before,.ion-android-share-alt:before,.ion-android-star:before,.ion-android-star-half:before,.ion-android-star-outline:before,.ion-android-stopwatch:before,.ion-android-subway:before,.ion-android-sunny:before,.ion-android-sync:before,.ion-android-textsms:before,.ion-android-time:before,.ion-android-train:before,.ion-android-unlock:before,.ion-android-upload:before,.ion-android-volume-down:before,.ion-android-volume-mute:before,.ion-android-volume-off:before,.ion-android-volume-up:before,.ion-android-walk:before,.ion-android-warning:before,.ion-android-watch:before,.ion-android-wifi:before,.ion-aperture:before,.ion-archive:before,.ion-arrow-down-a:before,.ion-arrow-down-b:before,.ion-arrow-down-c:before,.ion-arrow-expand:before,.ion-arrow-graph-down-left:before,.ion-arrow-graph-down-right:before,.ion-arrow-graph-up-left:before,.ion-arrow-graph-up-right:before,.ion-arrow-left-a:before,.ion-arrow-left-b:before,.ion-arrow-left-c:before,.ion-arrow-move:before,.ion-arrow-resize:before,.ion-arrow-return-left:before,.ion-arrow-return-right:before,.ion-arrow-right-a:before,.ion-arrow-right-b:before,.ion-arrow-right-c:before,.ion-arrow-shrink:before,.ion-arrow-swap:before,.ion-arrow-up-a:before,.ion-arrow-up-b:before,.ion-arrow-up-c:before,.ion-asterisk:before,.ion-at:before,.ion-backspace:before,.ion-backspace-outline:before,.ion-bag:before,.ion-battery-charging:before,.ion-battery-empty:before,.ion-battery-full:before,.ion-battery-half:before,.ion-battery-low:before,.ion-beaker:before,.ion-beer:before,.ion-bluetooth:before,.ion-bonfire:before,.ion-bookmark:before,.ion-bowtie:before,.ion-briefcase:before,.ion-bug:before,.ion-calculator:before,.ion-calendar:before,.ion-camera:before,.ion-card:before,.ion-cash:before,.ion-chatbox:before,.ion-chatbox-working:before,.ion-chatboxes:before,.ion-chatbubble:before,.ion-chatbubble-working:before,.ion-chatbubbles:before,.ion-checkmark:before,.ion-checkmark-circled:before,.ion-checkmark-round:before,.ion-chevron-down:before,.ion-chevron-left:before,.ion-chevron-right:before,.ion-chevron-up:before,.ion-clipboard:before,.ion-clock:before,.ion-close:before,.ion-close-circled:before,.ion-close-round:before,.ion-closed-captioning:before,.ion-cloud:before,.ion-code:before,.ion-code-download:before,.ion-code-working:before,.ion-coffee:before,.ion-compass:before,.ion-compose:before,.ion-connection-bars:before,.ion-contrast:before,.ion-crop:before,.ion-cube:before,.ion-disc:before,.ion-document:before,.ion-document-text:before,.ion-drag:before,.ion-earth:before,.ion-easel:before,.ion-edit:before,.ion-egg:before,.ion-eject:before,.ion-email:before,.ion-email-unread:before,.ion-erlenmeyer-flask:before,.ion-erlenmeyer-flask-bubbles:before,.ion-eye:before,.ion-eye-disabled:before,.ion-female:before,.ion-filing:before,.ion-film-marker:before,.ion-fireball:before,.ion-flag:before,.ion-flame:before,.ion-flash:before,.ion-flash-off:before,.ion-folder:before,.ion-fork:before,.ion-fork-repo:before,.ion-forward:before,.ion-funnel:before,.ion-gear-a:before,.ion-gear-b:before,.ion-grid:before,.ion-hammer:before,.ion-happy:before,.ion-happy-outline:before,.ion-headphone:before,.ion-heart:before,.ion-heart-broken:before,.ion-help:before,.ion-help-buoy:before,.ion-help-circled:before,.ion-home:before,.ion-icecream:before,.ion-image:before,.ion-images:before,.ion-information:before,.ion-information-circled:before,.ion-ionic:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-americanfootball:before,.ion-ios-americanfootball-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-down:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-left:before,.ion-ios-arrow-right:before,.ion-ios-arrow-thin-down:before,.ion-ios-arrow-thin-left:before,.ion-ios-arrow-thin-right:before,.ion-ios-arrow-thin-up:before,.ion-ios-arrow-up:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-bell:before,.ion-ios-bell-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bolt:before,.ion-ios-bolt-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-box:before,.ion-ios-box-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubble:before,.ion-ios-chatbubble-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-empty:before,.ion-ios-checkmark-outline:before,.ion-ios-circle-filled:before,.ion-ios-circle-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-empty:before,.ion-ios-close-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compose:before,.ion-ios-compose-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-crop:before,.ion-ios-crop-strong:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-drag:before,.ion-ios-email:before,.ion-ios-email-outline:before,.ion-ios-eye:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-gear:before,.ion-ios-gear-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-grid-view:before,.ion-ios-grid-view-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-empty:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-empty:before,.ion-ios-information-outline:before,.ion-ios-ionic-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-lightbulb:before,.ion-ios-lightbulb-outline:before,.ion-ios-list:before,.ion-ios-list-outline:before,.ion-ios-location:before,.ion-ios-location-outline:before,.ion-ios-locked:before,.ion-ios-locked-outline:before,.ion-ios-loop:before,.ion-ios-loop-strong:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-outline:before,.ion-ios-minus:before,.ion-ios-minus-empty:before,.ion-ios-minus-outline:before,.ion-ios-monitor:before,.ion-ios-monitor-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-notes:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paperplane:before,.ion-ios-paperplane-outline:before,.ion-ios-partlysunny:before,.ion-ios-partlysunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-outline:before,.ion-ios-personadd:before,.ion-ios-personadd-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-plus:before,.ion-ios-plus-empty:before,.ion-ios-plus-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-printer:before,.ion-ios-printer-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-strong:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-empty:before,.ion-ios-refresh-outline:before,.ion-ios-reload:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-search:before,.ion-ios-search-strong:before,.ion-ios-settings:before,.ion-ios-settings-strong:before,.ion-ios-shuffle:before,.ion-ios-shuffle-strong:before,.ion-ios-skipbackward:before,.ion-ios-skipbackward-outline:before,.ion-ios-skipforward:before,.ion-ios-skipforward-outline:before,.ion-ios-snowy:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-telephone:before,.ion-ios-telephone-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-toggle:before,.ion-ios-toggle-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlocked:before,.ion-ios-unlocked-outline:before,.ion-ios-upload:before,.ion-ios-upload-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-high:before,.ion-ios-volume-low:before,.ion-ios-wineglass:before,.ion-ios-wineglass-outline:before,.ion-ios-world:before,.ion-ios-world-outline:before,.ion-ipad:before,.ion-iphone:before,.ion-ipod:before,.ion-jet:before,.ion-key:before,.ion-knife:before,.ion-laptop:before,.ion-leaf:before,.ion-levels:before,.ion-lightbulb:before,.ion-link:before,.ion-load-a:before,.ion-load-b:before,.ion-load-c:before,.ion-load-d:before,.ion-location:before,.ion-lock-combination:before,.ion-locked:before,.ion-log-in:before,.ion-log-out:before,.ion-loop:before,.ion-magnet:before,.ion-male:before,.ion-man:before,.ion-map:before,.ion-medkit:before,.ion-merge:before,.ion-mic-a:before,.ion-mic-b:before,.ion-mic-c:before,.ion-minus:before,.ion-minus-circled:before,.ion-minus-round:before,.ion-model-s:before,.ion-monitor:before,.ion-more:before,.ion-mouse:before,.ion-music-note:before,.ion-navicon:before,.ion-navicon-round:before,.ion-navigate:before,.ion-network:before,.ion-no-smoking:before,.ion-nuclear:before,.ion-outlet:before,.ion-paintbrush:before,.ion-paintbucket:before,.ion-paper-airplane:before,.ion-paperclip:before,.ion-pause:before,.ion-person:before,.ion-person-add:before,.ion-person-stalker:before,.ion-pie-graph:before,.ion-pin:before,.ion-pinpoint:before,.ion-pizza:before,.ion-plane:before,.ion-planet:before,.ion-play:before,.ion-playstation:before,.ion-plus:before,.ion-plus-circled:before,.ion-plus-round:before,.ion-podium:before,.ion-pound:before,.ion-power:before,.ion-pricetag:before,.ion-pricetags:before,.ion-printer:before,.ion-pull-request:before,.ion-qr-scanner:before,.ion-quote:before,.ion-radio-waves:before,.ion-record:before,.ion-refresh:before,.ion-reply:before,.ion-reply-all:before,.ion-ribbon-a:before,.ion-ribbon-b:before,.ion-sad:before,.ion-sad-outline:before,.ion-scissors:before,.ion-search:before,.ion-settings:before,.ion-share:before,.ion-shuffle:before,.ion-skip-backward:before,.ion-skip-forward:before,.ion-social-android:before,.ion-social-android-outline:before,.ion-social-angular:before,.ion-social-angular-outline:before,.ion-social-apple:before,.ion-social-apple-outline:before,.ion-social-bitcoin:before,.ion-social-bitcoin-outline:before,.ion-social-buffer:before,.ion-social-buffer-outline:before,.ion-social-chrome:before,.ion-social-chrome-outline:before,.ion-social-codepen:before,.ion-social-codepen-outline:before,.ion-social-css3:before,.ion-social-css3-outline:before,.ion-social-designernews:before,.ion-social-designernews-outline:before,.ion-social-dribbble:before,.ion-social-dribbble-outline:before,.ion-social-dropbox:before,.ion-social-dropbox-outline:before,.ion-social-euro:before,.ion-social-euro-outline:before,.ion-social-facebook:before,.ion-social-facebook-outline:before,.ion-social-foursquare:before,.ion-social-foursquare-outline:before,.ion-social-freebsd-devil:before,.ion-social-github:before,.ion-social-github-outline:before,.ion-social-google:before,.ion-social-google-outline:before,.ion-social-googleplus:before,.ion-social-googleplus-outline:before,.ion-social-hackernews:before,.ion-social-hackernews-outline:before,.ion-social-html5:before,.ion-social-html5-outline:before,.ion-social-instagram:before,.ion-social-instagram-outline:before,.ion-social-javascript:before,.ion-social-javascript-outline:before,.ion-social-linkedin:before,.ion-social-linkedin-outline:before,.ion-social-markdown:before,.ion-social-nodejs:before,.ion-social-octocat:before,.ion-social-pinterest:before,.ion-social-pinterest-outline:before,.ion-social-python:before,.ion-social-reddit:before,.ion-social-reddit-outline:before,.ion-social-rss:before,.ion-social-rss-outline:before,.ion-social-sass:before,.ion-social-skype:before,.ion-social-skype-outline:before,.ion-social-snapchat:before,.ion-social-snapchat-outline:before,.ion-social-tumblr:before,.ion-social-tumblr-outline:before,.ion-social-tux:before,.ion-social-twitch:before,.ion-social-twitch-outline:before,.ion-social-twitter:before,.ion-social-twitter-outline:before,.ion-social-usd:before,.ion-social-usd-outline:before,.ion-social-vimeo:before,.ion-social-vimeo-outline:before,.ion-social-whatsapp:before,.ion-social-whatsapp-outline:before,.ion-social-windows:before,.ion-social-windows-outline:before,.ion-social-wordpress:before,.ion-social-wordpress-outline:before,.ion-social-yahoo:before,.ion-social-yahoo-outline:before,.ion-social-yen:before,.ion-social-yen-outline:before,.ion-social-youtube:before,.ion-social-youtube-outline:before,.ion-soup-can:before,.ion-soup-can-outline:before,.ion-speakerphone:before,.ion-speedometer:before,.ion-spoon:before,.ion-star:before,.ion-stats-bars:before,.ion-steam:before,.ion-stop:before,.ion-thermometer:before,.ion-thumbsdown:before,.ion-thumbsup:before,.ion-toggle:before,.ion-toggle-filled:before,.ion-transgender:before,.ion-trash-a:before,.ion-trash-b:before,.ion-trophy:before,.ion-tshirt:before,.ion-tshirt-outline:before,.ion-umbrella:before,.ion-university:before,.ion-unlocked:before,.ion-upload:before,.ion-usb:before,.ion-videocamera:before,.ion-volume-high:before,.ion-volume-low:before,.ion-volume-medium:before,.ion-volume-mute:before,.ion-wand:before,.ion-waterdrop:before,.ion-wifi:before,.ion-wineglass:before,.ion-woman:before,.ion-wrench:before,.ion-xbox:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-alert:before{content:"\f101"}.ion-alert-circled:before{content:"\f100"}.ion-android-add:before{content:"\f2c7"}.ion-android-add-circle:before{content:"\f359"}.ion-android-alarm-clock:before{content:"\f35a"}.ion-android-alert:before{content:"\f35b"}.ion-android-apps:before{content:"\f35c"}.ion-android-archive:before{content:"\f2c9"}.ion-android-arrow-back:before{content:"\f2ca"}.ion-android-arrow-down:before{content:"\f35d"}.ion-android-arrow-dropdown:before{content:"\f35f"}.ion-android-arrow-dropdown-circle:before{content:"\f35e"}.ion-android-arrow-dropleft:before{content:"\f361"}.ion-android-arrow-dropleft-circle:before{content:"\f360"}.ion-android-arrow-dropright:before{content:"\f363"}.ion-android-arrow-dropright-circle:before{content:"\f362"}.ion-android-arrow-dropup:before{content:"\f365"}.ion-android-arrow-dropup-circle:before{content:"\f364"}.ion-android-arrow-forward:before{content:"\f30f"}.ion-android-arrow-up:before{content:"\f366"}.ion-android-attach:before{content:"\f367"}.ion-android-bar:before{content:"\f368"}.ion-android-bicycle:before{content:"\f369"}.ion-android-boat:before{content:"\f36a"}.ion-android-bookmark:before{content:"\f36b"}.ion-android-bulb:before{content:"\f36c"}.ion-android-bus:before{content:"\f36d"}.ion-android-calendar:before{content:"\f2d1"}.ion-android-call:before{content:"\f2d2"}.ion-android-camera:before{content:"\f2d3"}.ion-android-cancel:before{content:"\f36e"}.ion-android-car:before{content:"\f36f"}.ion-android-cart:before{content:"\f370"}.ion-android-chat:before{content:"\f2d4"}.ion-android-checkbox:before{content:"\f374"}.ion-android-checkbox-blank:before{content:"\f371"}.ion-android-checkbox-outline:before{content:"\f373"}.ion-android-checkbox-outline-blank:before{content:"\f372"}.ion-android-checkmark-circle:before{content:"\f375"}.ion-android-clipboard:before{content:"\f376"}.ion-android-close:before{content:"\f2d7"}.ion-android-cloud:before{content:"\f37a"}.ion-android-cloud-circle:before{content:"\f377"}.ion-android-cloud-done:before{content:"\f378"}.ion-android-cloud-outline:before{content:"\f379"}.ion-android-color-palette:before{content:"\f37b"}.ion-android-compass:before{content:"\f37c"}.ion-android-contact:before{content:"\f2d8"}.ion-android-contacts:before{content:"\f2d9"}.ion-android-contract:before{content:"\f37d"}.ion-android-create:before{content:"\f37e"}.ion-android-delete:before{content:"\f37f"}.ion-android-desktop:before{content:"\f380"}.ion-android-document:before{content:"\f381"}.ion-android-done:before{content:"\f383"}.ion-android-done-all:before{content:"\f382"}.ion-android-download:before{content:"\f2dd"}.ion-android-drafts:before{content:"\f384"}.ion-android-exit:before{content:"\f385"}.ion-android-expand:before{content:"\f386"}.ion-android-favorite:before{content:"\f388"}.ion-android-favorite-outline:before{content:"\f387"}.ion-android-film:before{content:"\f389"}.ion-android-folder:before{content:"\f2e0"}.ion-android-folder-open:before{content:"\f38a"}.ion-android-funnel:before{content:"\f38b"}.ion-android-globe:before{content:"\f38c"}.ion-android-hand:before{content:"\f2e3"}.ion-android-hangout:before{content:"\f38d"}.ion-android-happy:before{content:"\f38e"}.ion-android-home:before{content:"\f38f"}.ion-android-image:before{content:"\f2e4"}.ion-android-laptop:before{content:"\f390"}.ion-android-list:before{content:"\f391"}.ion-android-locate:before{content:"\f2e9"}.ion-android-lock:before{content:"\f392"}.ion-android-mail:before{content:"\f2eb"}.ion-android-map:before{content:"\f393"}.ion-android-menu:before{content:"\f394"}.ion-android-microphone:before{content:"\f2ec"}.ion-android-microphone-off:before{content:"\f395"}.ion-android-more-horizontal:before{content:"\f396"}.ion-android-more-vertical:before{content:"\f397"}.ion-android-navigate:before{content:"\f398"}.ion-android-notifications:before{content:"\f39b"}.ion-android-notifications-none:before{content:"\f399"}.ion-android-notifications-off:before{content:"\f39a"}.ion-android-open:before{content:"\f39c"}.ion-android-options:before{content:"\f39d"}.ion-android-people:before{content:"\f39e"}.ion-android-person:before{content:"\f3a0"}.ion-android-person-add:before{content:"\f39f"}.ion-android-phone-landscape:before{content:"\f3a1"}.ion-android-phone-portrait:before{content:"\f3a2"}.ion-android-pin:before{content:"\f3a3"}.ion-android-plane:before{content:"\f3a4"}.ion-android-playstore:before{content:"\f2f0"}.ion-android-print:before{content:"\f3a5"}.ion-android-radio-button-off:before{content:"\f3a6"}.ion-android-radio-button-on:before{content:"\f3a7"}.ion-android-refresh:before{content:"\f3a8"}.ion-android-remove:before{content:"\f2f4"}.ion-android-remove-circle:before{content:"\f3a9"}.ion-android-restaurant:before{content:"\f3aa"}.ion-android-sad:before{content:"\f3ab"}.ion-android-search:before{content:"\f2f5"}.ion-android-send:before{content:"\f2f6"}.ion-android-settings:before{content:"\f2f7"}.ion-android-share:before{content:"\f2f8"}.ion-android-share-alt:before{content:"\f3ac"}.ion-android-star:before{content:"\f2fc"}.ion-android-star-half:before{content:"\f3ad"}.ion-android-star-outline:before{content:"\f3ae"}.ion-android-stopwatch:before{content:"\f2fd"}.ion-android-subway:before{content:"\f3af"}.ion-android-sunny:before{content:"\f3b0"}.ion-android-sync:before{content:"\f3b1"}.ion-android-textsms:before{content:"\f3b2"}.ion-android-time:before{content:"\f3b3"}.ion-android-train:before{content:"\f3b4"}.ion-android-unlock:before{content:"\f3b5"}.ion-android-upload:before{content:"\f3b6"}.ion-android-volume-down:before{content:"\f3b7"}.ion-android-volume-mute:before{content:"\f3b8"}.ion-android-volume-off:before{content:"\f3b9"}.ion-android-volume-up:before{content:"\f3ba"}.ion-android-walk:before{content:"\f3bb"}.ion-android-warning:before{content:"\f3bc"}.ion-android-watch:before{content:"\f3bd"}.ion-android-wifi:before{content:"\f305"}.ion-aperture:before{content:"\f313"}.ion-archive:before{content:"\f102"}.ion-arrow-down-a:before{content:"\f103"}.ion-arrow-down-b:before{content:"\f104"}.ion-arrow-down-c:before{content:"\f105"}.ion-arrow-expand:before{content:"\f25e"}.ion-arrow-graph-down-left:before{content:"\f25f"}.ion-arrow-graph-down-right:before{content:"\f260"}.ion-arrow-graph-up-left:before{content:"\f261"}.ion-arrow-graph-up-right:before{content:"\f262"}.ion-arrow-left-a:before{content:"\f106"}.ion-arrow-left-b:before{content:"\f107"}.ion-arrow-left-c:before{content:"\f108"}.ion-arrow-move:before{content:"\f263"}.ion-arrow-resize:before{content:"\f264"}.ion-arrow-return-left:before{content:"\f265"}.ion-arrow-return-right:before{content:"\f266"}.ion-arrow-right-a:before{content:"\f109"}.ion-arrow-right-b:before{content:"\f10a"}.ion-arrow-right-c:before{content:"\f10b"}.ion-arrow-shrink:before{content:"\f267"}.ion-arrow-swap:before{content:"\f268"}.ion-arrow-up-a:before{content:"\f10c"}.ion-arrow-up-b:before{content:"\f10d"}.ion-arrow-up-c:before{content:"\f10e"}.ion-asterisk:before{content:"\f314"}.ion-at:before{content:"\f10f"}.ion-backspace:before{content:"\f3bf"}.ion-backspace-outline:before{content:"\f3be"}.ion-bag:before{content:"\f110"}.ion-battery-charging:before{content:"\f111"}.ion-battery-empty:before{content:"\f112"}.ion-battery-full:before{content:"\f113"}.ion-battery-half:before{content:"\f114"}.ion-battery-low:before{content:"\f115"}.ion-beaker:before{content:"\f269"}.ion-beer:before{content:"\f26a"}.ion-bluetooth:before{content:"\f116"}.ion-bonfire:before{content:"\f315"}.ion-bookmark:before{content:"\f26b"}.ion-bowtie:before{content:"\f3c0"}.ion-briefcase:before{content:"\f26c"}.ion-bug:before{content:"\f2be"}.ion-calculator:before{content:"\f26d"}.ion-calendar:before{content:"\f117"}.ion-camera:before{content:"\f118"}.ion-card:before{content:"\f119"}.ion-cash:before{content:"\f316"}.ion-chatbox:before{content:"\f11b"}.ion-chatbox-working:before{content:"\f11a"}.ion-chatboxes:before{content:"\f11c"}.ion-chatbubble:before{content:"\f11e"}.ion-chatbubble-working:before{content:"\f11d"}.ion-chatbubbles:before{content:"\f11f"}.ion-checkmark:before{content:"\f122"}.ion-checkmark-circled:before{content:"\f120"}.ion-checkmark-round:before{content:"\f121"}.ion-chevron-down:before{content:"\f123"}.ion-chevron-left:before{content:"\f124"}.ion-chevron-right:before{content:"\f125"}.ion-chevron-up:before{content:"\f126"}.ion-clipboard:before{content:"\f127"}.ion-clock:before{content:"\f26e"}.ion-close:before{content:"\f12a"}.ion-close-circled:before{content:"\f128"}.ion-close-round:before{content:"\f129"}.ion-closed-captioning:before{content:"\f317"}.ion-cloud:before{content:"\f12b"}.ion-code:before{content:"\f271"}.ion-code-download:before{content:"\f26f"}.ion-code-working:before{content:"\f270"}.ion-coffee:before{content:"\f272"}.ion-compass:before{content:"\f273"}.ion-compose:before{content:"\f12c"}.ion-connection-bars:before{content:"\f274"}.ion-contrast:before{content:"\f275"}.ion-crop:before{content:"\f3c1"}.ion-cube:before{content:"\f318"}.ion-disc:before{content:"\f12d"}.ion-document:before{content:"\f12f"}.ion-document-text:before{content:"\f12e"}.ion-drag:before{content:"\f130"}.ion-earth:before{content:"\f276"}.ion-easel:before{content:"\f3c2"}.ion-edit:before{content:"\f2bf"}.ion-egg:before{content:"\f277"}.ion-eject:before{content:"\f131"}.ion-email:before{content:"\f132"}.ion-email-unread:before{content:"\f3c3"}.ion-erlenmeyer-flask:before{content:"\f3c5"}.ion-erlenmeyer-flask-bubbles:before{content:"\f3c4"}.ion-eye:before{content:"\f133"}.ion-eye-disabled:before{content:"\f306"}.ion-female:before{content:"\f278"}.ion-filing:before{content:"\f134"}.ion-film-marker:before{content:"\f135"}.ion-fireball:before{content:"\f319"}.ion-flag:before{content:"\f279"}.ion-flame:before{content:"\f31a"}.ion-flash:before{content:"\f137"}.ion-flash-off:before{content:"\f136"}.ion-folder:before{content:"\f139"}.ion-fork:before{content:"\f27a"}.ion-fork-repo:before{content:"\f2c0"}.ion-forward:before{content:"\f13a"}.ion-funnel:before{content:"\f31b"}.ion-gear-a:before{content:"\f13d"}.ion-gear-b:before{content:"\f13e"}.ion-grid:before{content:"\f13f"}.ion-hammer:before{content:"\f27b"}.ion-happy:before{content:"\f31c"}.ion-happy-outline:before{content:"\f3c6"}.ion-headphone:before{content:"\f140"}.ion-heart:before{content:"\f141"}.ion-heart-broken:before{content:"\f31d"}.ion-help:before{content:"\f143"}.ion-help-buoy:before{content:"\f27c"}.ion-help-circled:before{content:"\f142"}.ion-home:before{content:"\f144"}.ion-icecream:before{content:"\f27d"}.ion-image:before{content:"\f147"}.ion-images:before{content:"\f148"}.ion-information:before{content:"\f14a"}.ion-information-circled:before{content:"\f149"}.ion-ionic:before{content:"\f14b"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-americanfootball:before{content:"\f3cc"}.ion-ios-americanfootball-outline:before{content:"\f3cb"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-left:before{content:"\f3d2"}.ion-ios-arrow-right:before{content:"\f3d3"}.ion-ios-arrow-thin-down:before{content:"\f3d4"}.ion-ios-arrow-thin-left:before{content:"\f3d5"}.ion-ios-arrow-thin-right:before{content:"\f3d6"}.ion-ios-arrow-thin-up:before{content:"\f3d7"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-bell:before{content:"\f3e2"}.ion-ios-bell-outline:before{content:"\f3e1"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bolt:before{content:"\f3e6"}.ion-ios-bolt-outline:before{content:"\f3e5"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-box:before{content:"\f3ec"}.ion-ios-box-outline:before{content:"\f3eb"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubble:before{content:"\f3fc"}.ion-ios-chatbubble-outline:before{content:"\f3fb"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-empty:before{content:"\f3fd"}.ion-ios-checkmark-outline:before{content:"\f3fe"}.ion-ios-circle-filled:before{content:"\f400"}.ion-ios-circle-outline:before{content:"\f401"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-empty:before{content:"\f404"}.ion-ios-close-outline:before{content:"\f405"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compose:before{content:"\f418"}.ion-ios-compose-outline:before{content:"\f417"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-strong:before{content:"\f41d"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-drag:before{content:"\f421"}.ion-ios-email:before{content:"\f423"}.ion-ios-email-outline:before{content:"\f422"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-gear:before{content:"\f43d"}.ion-ios-gear-outline:before{content:"\f43c"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-grid-view:before{content:"\f441"}.ion-ios-grid-view-outline:before{content:"\f440"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-empty:before{content:"\f444"}.ion-ios-help-outline:before{content:"\f445"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-empty:before{content:"\f44b"}.ion-ios-information-outline:before{content:"\f44c"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-lightbulb:before{content:"\f452"}.ion-ios-lightbulb-outline:before{content:"\f451"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-outline:before{content:"\f453"}.ion-ios-location:before{content:"\f456"}.ion-ios-location-outline:before{content:"\f455"}.ion-ios-locked:before{content:"\f458"}.ion-ios-locked-outline:before{content:"\f457"}.ion-ios-loop:before{content:"\f45a"}.ion-ios-loop-strong:before{content:"\f459"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-minus:before{content:"\f464"}.ion-ios-minus-empty:before{content:"\f462"}.ion-ios-minus-outline:before{content:"\f463"}.ion-ios-monitor:before{content:"\f466"}.ion-ios-monitor-outline:before{content:"\f465"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f46a"}.ion-ios-more-outline:before{content:"\f469"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paperplane:before{content:"\f474"}.ion-ios-paperplane-outline:before{content:"\f473"}.ion-ios-partlysunny:before{content:"\f476"}.ion-ios-partlysunny-outline:before{content:"\f475"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-personadd:before{content:"\f480"}.ion-ios-personadd-outline:before{content:"\f47f"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-plus:before{content:"\f48b"}.ion-ios-plus-empty:before{content:"\f489"}.ion-ios-plus-outline:before{content:"\f48a"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-printer:before{content:"\f491"}.ion-ios-printer-outline:before{content:"\f490"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-strong:before{content:"\f492"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-empty:before{content:"\f49a"}.ion-ios-refresh-outline:before{content:"\f49b"}.ion-ios-reload:before{content:"\f49d"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-strong:before{content:"\f4a4"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-strong:before{content:"\f4a6"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-strong:before{content:"\f4a8"}.ion-ios-skipbackward:before{content:"\f4ab"}.ion-ios-skipbackward-outline:before{content:"\f4aa"}.ion-ios-skipforward:before{content:"\f4ad"}.ion-ios-skipforward-outline:before{content:"\f4ac"}.ion-ios-snowy:before{content:"\f4ae"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-telephone:before{content:"\f4b9"}.ion-ios-telephone-outline:before{content:"\f4b8"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-toggle:before{content:"\f4c3"}.ion-ios-toggle-outline:before{content:"\f4c2"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlocked:before{content:"\f4c9"}.ion-ios-unlocked-outline:before{content:"\f4c8"}.ion-ios-upload:before{content:"\f4cb"}.ion-ios-upload-outline:before{content:"\f4ca"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-high:before{content:"\f4ce"}.ion-ios-volume-low:before{content:"\f4cf"}.ion-ios-wineglass:before{content:"\f4d1"}.ion-ios-wineglass-outline:before{content:"\f4d0"}.ion-ios-world:before{content:"\f4d3"}.ion-ios-world-outline:before{content:"\f4d2"}.ion-ipad:before{content:"\f1f9"}.ion-iphone:before{content:"\f1fa"}.ion-ipod:before{content:"\f1fb"}.ion-jet:before{content:"\f295"}.ion-key:before{content:"\f296"}.ion-knife:before{content:"\f297"}.ion-laptop:before{content:"\f1fc"}.ion-leaf:before{content:"\f1fd"}.ion-levels:before{content:"\f298"}.ion-lightbulb:before{content:"\f299"}.ion-link:before{content:"\f1fe"}.ion-load-a:before{content:"\f29a"}.ion-load-b:before{content:"\f29b"}.ion-load-c:before{content:"\f29c"}.ion-load-d:before{content:"\f29d"}.ion-location:before{content:"\f1ff"}.ion-lock-combination:before{content:"\f4d4"}.ion-locked:before{content:"\f200"}.ion-log-in:before{content:"\f29e"}.ion-log-out:before{content:"\f29f"}.ion-loop:before{content:"\f201"}.ion-magnet:before{content:"\f2a0"}.ion-male:before{content:"\f2a1"}.ion-man:before{content:"\f202"}.ion-map:before{content:"\f203"}.ion-medkit:before{content:"\f2a2"}.ion-merge:before{content:"\f33f"}.ion-mic-a:before{content:"\f204"}.ion-mic-b:before{content:"\f205"}.ion-mic-c:before{content:"\f206"}.ion-minus:before{content:"\f209"}.ion-minus-circled:before{content:"\f207"}.ion-minus-round:before{content:"\f208"}.ion-model-s:before{content:"\f2c1"}.ion-monitor:before{content:"\f20a"}.ion-more:before{content:"\f20b"}.ion-mouse:before{content:"\f340"}.ion-music-note:before{content:"\f20c"}.ion-navicon:before{content:"\f20e"}.ion-navicon-round:before{content:"\f20d"}.ion-navigate:before{content:"\f2a3"}.ion-network:before{content:"\f341"}.ion-no-smoking:before{content:"\f2c2"}.ion-nuclear:before{content:"\f2a4"}.ion-outlet:before{content:"\f342"}.ion-paintbrush:before{content:"\f4d5"}.ion-paintbucket:before{content:"\f4d6"}.ion-paper-airplane:before{content:"\f2c3"}.ion-paperclip:before{content:"\f20f"}.ion-pause:before{content:"\f210"}.ion-person:before{content:"\f213"}.ion-person-add:before{content:"\f211"}.ion-person-stalker:before{content:"\f212"}.ion-pie-graph:before{content:"\f2a5"}.ion-pin:before{content:"\f2a6"}.ion-pinpoint:before{content:"\f2a7"}.ion-pizza:before{content:"\f2a8"}.ion-plane:before{content:"\f214"}.ion-planet:before{content:"\f343"}.ion-play:before{content:"\f215"}.ion-playstation:before{content:"\f30a"}.ion-plus:before{content:"\f218"}.ion-plus-circled:before{content:"\f216"}.ion-plus-round:before{content:"\f217"}.ion-podium:before{content:"\f344"}.ion-pound:before{content:"\f219"}.ion-power:before{content:"\f2a9"}.ion-pricetag:before{content:"\f2aa"}.ion-pricetags:before{content:"\f2ab"}.ion-printer:before{content:"\f21a"}.ion-pull-request:before{content:"\f345"}.ion-qr-scanner:before{content:"\f346"}.ion-quote:before{content:"\f347"}.ion-radio-waves:before{content:"\f2ac"}.ion-record:before{content:"\f21b"}.ion-refresh:before{content:"\f21c"}.ion-reply:before{content:"\f21e"}.ion-reply-all:before{content:"\f21d"}.ion-ribbon-a:before{content:"\f348"}.ion-ribbon-b:before{content:"\f349"}.ion-sad:before{content:"\f34a"}.ion-sad-outline:before{content:"\f4d7"}.ion-scissors:before{content:"\f34b"}.ion-search:before{content:"\f21f"}.ion-settings:before{content:"\f2ad"}.ion-share:before{content:"\f220"}.ion-shuffle:before{content:"\f221"}.ion-skip-backward:before{content:"\f222"}.ion-skip-forward:before{content:"\f223"}.ion-social-android:before{content:"\f225"}.ion-social-android-outline:before{content:"\f224"}.ion-social-angular:before{content:"\f4d9"}.ion-social-angular-outline:before{content:"\f4d8"}.ion-social-apple:before{content:"\f227"}.ion-social-apple-outline:before{content:"\f226"}.ion-social-bitcoin:before{content:"\f2af"}.ion-social-bitcoin-outline:before{content:"\f2ae"}.ion-social-buffer:before{content:"\f229"}.ion-social-buffer-outline:before{content:"\f228"}.ion-social-chrome:before{content:"\f4db"}.ion-social-chrome-outline:before{content:"\f4da"}.ion-social-codepen:before{content:"\f4dd"}.ion-social-codepen-outline:before{content:"\f4dc"}.ion-social-css3:before{content:"\f4df"}.ion-social-css3-outline:before{content:"\f4de"}.ion-social-designernews:before{content:"\f22b"}.ion-social-designernews-outline:before{content:"\f22a"}.ion-social-dribbble:before{content:"\f22d"}.ion-social-dribbble-outline:before{content:"\f22c"}.ion-social-dropbox:before{content:"\f22f"}.ion-social-dropbox-outline:before{content:"\f22e"}.ion-social-euro:before{content:"\f4e1"}.ion-social-euro-outline:before{content:"\f4e0"}.ion-social-facebook:before{content:"\f231"}.ion-social-facebook-outline:before{content:"\f230"}.ion-social-foursquare:before{content:"\f34d"}.ion-social-foursquare-outline:before{content:"\f34c"}.ion-social-freebsd-devil:before{content:"\f2c4"}.ion-social-github:before{content:"\f233"}.ion-social-github-outline:before{content:"\f232"}.ion-social-google:before{content:"\f34f"}.ion-social-google-outline:before{content:"\f34e"}.ion-social-googleplus:before{content:"\f235"}.ion-social-googleplus-outline:before{content:"\f234"}.ion-social-hackernews:before{content:"\f237"}.ion-social-hackernews-outline:before{content:"\f236"}.ion-social-html5:before{content:"\f4e3"}.ion-social-html5-outline:before{content:"\f4e2"}.ion-social-instagram:before{content:"\f351"}.ion-social-instagram-outline:before{content:"\f350"}.ion-social-javascript:before{content:"\f4e5"}.ion-social-javascript-outline:before{content:"\f4e4"}.ion-social-linkedin:before{content:"\f239"}.ion-social-linkedin-outline:before{content:"\f238"}.ion-social-markdown:before{content:"\f4e6"}.ion-social-nodejs:before{content:"\f4e7"}.ion-social-octocat:before{content:"\f4e8"}.ion-social-pinterest:before{content:"\f2b1"}.ion-social-pinterest-outline:before{content:"\f2b0"}.ion-social-python:before{content:"\f4e9"}.ion-social-reddit:before{content:"\f23b"}.ion-social-reddit-outline:before{content:"\f23a"}.ion-social-rss:before{content:"\f23d"}.ion-social-rss-outline:before{content:"\f23c"}.ion-social-sass:before{content:"\f4ea"}.ion-social-skype:before{content:"\f23f"}.ion-social-skype-outline:before{content:"\f23e"}.ion-social-snapchat:before{content:"\f4ec"}.ion-social-snapchat-outline:before{content:"\f4eb"}.ion-social-tumblr:before{content:"\f241"}.ion-social-tumblr-outline:before{content:"\f240"}.ion-social-tux:before{content:"\f2c5"}.ion-social-twitch:before{content:"\f4ee"}.ion-social-twitch-outline:before{content:"\f4ed"}.ion-social-twitter:before{content:"\f243"}.ion-social-twitter-outline:before{content:"\f242"}.ion-social-usd:before{content:"\f353"}.ion-social-usd-outline:before{content:"\f352"}.ion-social-vimeo:before{content:"\f245"}.ion-social-vimeo-outline:before{content:"\f244"}.ion-social-whatsapp:before{content:"\f4f0"}.ion-social-whatsapp-outline:before{content:"\f4ef"}.ion-social-windows:before{content:"\f247"}.ion-social-windows-outline:before{content:"\f246"}.ion-social-wordpress:before{content:"\f249"}.ion-social-wordpress-outline:before{content:"\f248"}.ion-social-yahoo:before{content:"\f24b"}.ion-social-yahoo-outline:before{content:"\f24a"}.ion-social-yen:before{content:"\f4f2"}.ion-social-yen-outline:before{content:"\f4f1"}.ion-social-youtube:before{content:"\f24d"}.ion-social-youtube-outline:before{content:"\f24c"}.ion-soup-can:before{content:"\f4f4"}.ion-soup-can-outline:before{content:"\f4f3"}.ion-speakerphone:before{content:"\f2b2"}.ion-speedometer:before{content:"\f2b3"}.ion-spoon:before{content:"\f2b4"}.ion-star:before{content:"\f24e"}.ion-stats-bars:before{content:"\f2b5"}.ion-steam:before{content:"\f30b"}.ion-stop:before{content:"\f24f"}.ion-thermometer:before{content:"\f2b6"}.ion-thumbsdown:before{content:"\f250"}.ion-thumbsup:before{content:"\f251"}.ion-toggle:before{content:"\f355"}.ion-toggle-filled:before{content:"\f354"}.ion-transgender:before{content:"\f4f5"}.ion-trash-a:before{content:"\f252"}.ion-trash-b:before{content:"\f253"}.ion-trophy:before{content:"\f356"}.ion-tshirt:before{content:"\f4f7"}.ion-tshirt-outline:before{content:"\f4f6"}.ion-umbrella:before{content:"\f2b7"}.ion-university:before{content:"\f357"}.ion-unlocked:before{content:"\f254"}.ion-upload:before{content:"\f255"}.ion-usb:before{content:"\f2b8"}.ion-videocamera:before{content:"\f256"}.ion-volume-high:before{content:"\f257"}.ion-volume-low:before{content:"\f258"}.ion-volume-medium:before{content:"\f259"}.ion-volume-mute:before{content:"\f25a"}.ion-wand:before{content:"\f358"}.ion-waterdrop:before{content:"\f25b"}.ion-wifi:before{content:"\f25c"}.ion-wineglass:before{content:"\f2b9"}.ion-woman:before{content:"\f25d"}.ion-wrench:before{content:"\f2ba"}.ion-xbox:before{content:"\f30c"} 12 | -------------------------------------------------------------------------------- /ntask-web/public/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caio-ribeiro-pereira/building-apis-with-nodejs/947038430b687f6d640f46ca06293c552b6dbdf2/ntask-web/public/fonts/ionicons.eot -------------------------------------------------------------------------------- /ntask-web/public/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caio-ribeiro-pereira/building-apis-with-nodejs/947038430b687f6d640f46ca06293c552b6dbdf2/ntask-web/public/fonts/ionicons.ttf -------------------------------------------------------------------------------- /ntask-web/public/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caio-ribeiro-pereira/building-apis-with-nodejs/947038430b687f6d640f46ca06293c552b6dbdf2/ntask-web/public/fonts/ionicons.woff -------------------------------------------------------------------------------- /ntask-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |