├── .env.example ├── .gitignore ├── LICENSE ├── Node.js ├── Procfile ├── README.md ├── app.js ├── app.json ├── bot-processor.js ├── config └── passport.js ├── controllers ├── bot.js ├── form.js ├── home.js └── user.js ├── models ├── Form.js ├── Responder.js └── User.js ├── package.json ├── public ├── bckg.png ├── bckg.svg ├── css │ ├── lib │ │ ├── bootstrap-social.scss │ │ ├── bootstrap │ │ │ ├── _alerts.scss │ │ │ ├── _badges.scss │ │ │ ├── _breadcrumbs.scss │ │ │ ├── _button-groups.scss │ │ │ ├── _buttons.scss │ │ │ ├── _carousel.scss │ │ │ ├── _close.scss │ │ │ ├── _code.scss │ │ │ ├── _component-animations.scss │ │ │ ├── _dropdowns.scss │ │ │ ├── _forms.scss │ │ │ ├── _glyphicons.scss │ │ │ ├── _grid.scss │ │ │ ├── _input-groups.scss │ │ │ ├── _jumbotron.scss │ │ │ ├── _labels.scss │ │ │ ├── _list-group.scss │ │ │ ├── _media.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modals.scss │ │ │ ├── _navbar.scss │ │ │ ├── _navs.scss │ │ │ ├── _normalize.scss │ │ │ ├── _pager.scss │ │ │ ├── _pagination.scss │ │ │ ├── _panels.scss │ │ │ ├── _popovers.scss │ │ │ ├── _print.scss │ │ │ ├── _progress-bars.scss │ │ │ ├── _responsive-embed.scss │ │ │ ├── _responsive-utilities.scss │ │ │ ├── _scaffolding.scss │ │ │ ├── _tables.scss │ │ │ ├── _theme.scss │ │ │ ├── _thumbnails.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _type.scss │ │ │ ├── _utilities.scss │ │ │ ├── _variables.scss │ │ │ ├── _wells.scss │ │ │ ├── bootstrap.scss │ │ │ └── mixins │ │ │ │ ├── _alerts.scss │ │ │ │ ├── _background-variant.scss │ │ │ │ ├── _border-radius.scss │ │ │ │ ├── _buttons.scss │ │ │ │ ├── _center-block.scss │ │ │ │ ├── _clearfix.scss │ │ │ │ ├── _forms.scss │ │ │ │ ├── _gradients.scss │ │ │ │ ├── _grid-framework.scss │ │ │ │ ├── _grid.scss │ │ │ │ ├── _hide-text.scss │ │ │ │ ├── _image.scss │ │ │ │ ├── _labels.scss │ │ │ │ ├── _list-group.scss │ │ │ │ ├── _nav-divider.scss │ │ │ │ ├── _nav-vertical-align.scss │ │ │ │ ├── _opacity.scss │ │ │ │ ├── _pagination.scss │ │ │ │ ├── _panels.scss │ │ │ │ ├── _progress-bar.scss │ │ │ │ ├── _reset-filter.scss │ │ │ │ ├── _reset-text.scss │ │ │ │ ├── _resize.scss │ │ │ │ ├── _responsive-visibility.scss │ │ │ │ ├── _size.scss │ │ │ │ ├── _tab-focus.scss │ │ │ │ ├── _table-row.scss │ │ │ │ ├── _text-emphasis.scss │ │ │ │ ├── _text-overflow.scss │ │ │ │ └── _vendor-prefixes.scss │ │ └── font-awesome │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ ├── lity.min.css │ ├── main.scss │ ├── style.css │ ├── style2.css │ └── themes │ │ ├── default │ │ ├── _default.scss │ │ └── _variables.scss │ │ ├── flatly │ │ ├── _flatly.scss │ │ └── _variables.scss │ │ ├── gsdk │ │ ├── gsdk.scss │ │ └── gsdk │ │ │ ├── _alerts.scss │ │ │ ├── _buttons.scss │ │ │ ├── _carousel.scss │ │ │ ├── _collapse.scss │ │ │ ├── _dropdown.scss │ │ │ ├── _inputs.scss │ │ │ ├── _labels-and-progress-bars.scss │ │ │ ├── _misc.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modal.scss │ │ │ ├── _navbars.scss │ │ │ ├── _responsive.scss │ │ │ ├── _sliders.scss │ │ │ ├── _social-buttons.scss │ │ │ ├── _tabs-navs-pagination.scss │ │ │ ├── _tooltips.scss │ │ │ ├── _typography.scss │ │ │ ├── _variables.scss │ │ │ └── mixins │ │ │ ├── _buttons.scss │ │ │ ├── _inputs.scss │ │ │ ├── _labels.scss │ │ │ ├── _navbars.scss │ │ │ ├── _tabs.scss │ │ │ ├── _transparency.scss │ │ │ └── _vendor-prefixes.scss │ │ └── modern │ │ ├── _modern.scss │ │ └── _variables.scss ├── favicon.ico ├── favicon.png ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── icon.svg ├── icon_boost.svg ├── icon_channels.svg ├── icon_focus.svg ├── icon_open_source.svg ├── icon_worflow.svg ├── js │ ├── lib │ │ ├── bootstrap.min.js │ │ └── jquery-2.2.4.min.js │ ├── lity.js │ └── main.js ├── logo-icon.png ├── logo.png ├── logo.svg ├── logo_full.png ├── logo_purple.png └── smooch_logo.svg ├── test ├── app.js └── models.js └── views ├── account ├── forgot.pug ├── login.pug ├── profile.pug ├── reset.pug └── signup.pug ├── edit_form.pug ├── forms.pug ├── home.pug ├── layout.pug ├── partials ├── flash.pug ├── footer.pug └── header.pug └── publish_form.pug /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost:27017/test 2 | MONGOLAB_URI=mongodb://localhost:27017/test 3 | 4 | SESSION_SECRET=OMG SO SECRET SESSION 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | #Build 17 | public/css/main.css 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | 26 | # Editors 27 | .idea 28 | *.iml 29 | 30 | # OS metadata 31 | .DS_Store 32 | Thumbs.db 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Chatform: 4 | Copyright (c) 2017 Mike Gozzo 5 | 6 | Chatform design and UI: 7 | Copyright (c) 2017 Lucie Le Touze 8 | 9 | Chatform logo: 10 | Copyright (c) 2017 Kevin Gauthier 11 | 12 | Boilerplate express app (Hackathon Starter): 13 | Copyright (c) 2014-2016 Sahat Yalkabov 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /Node.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gozman/chatform/3c87c44f8546f498ba89cb7ea8e7cb0e8fe5db26/Node.js -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | worker: node bot-processor.js 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chatform.ai 2 | ======================= 3 | 4 | **Live Demo**: http://chatform.ai 5 | 6 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 7 | 8 | 9 | Table of Contents 10 | ----------------- 11 | 12 | - [Features](#features) 13 | - [Prerequisites](#prerequisites) 14 | - [Getting Started](#getting-started) 15 | - [FAQ](#faq) 16 | - [Contributing](#contributing) 17 | - [License](#license) 18 | 19 | Features 20 | -------- 21 | 22 | - Create surveys or forms and gather responses using: Facebook Messenger, WeChat, Twilio SMS, LINE Messenger, Telegram, Viber, Twitter DM, Web Chat, e-mail, and in-app chat on iOS and Android. 23 | - Download responses to your forms and surveys as a CSV file: easily import these responses into Microsoft Excel, Google Sheets, Numbers and loads of other software. 24 | - Automatically identify the respondent (First & Last Name) when they respond using a supported messaging app. 25 | - Supervise all responses to your survey using Slack, HipChat, Front or your favourite business system. Jump into any conversation live! 26 | - Open source (MIT license) so you can modify and improve chatform and make it even more awesome. 27 | 28 | Prerequisites 29 | ------------- 30 | 31 | - [Smooch.io](https://smooch.io) 32 | - [MongoDB](https://www.mongodb.org/downloads) 33 | - [Node.js 6.0+](http://nodejs.org) 34 | - [Redis](http://redis.org) 35 | 36 | Getting Started 37 | --------------- 38 | 39 | The easiest way to get started is to simply deploy this code to Heroku with the following button: 40 | 41 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 42 | 43 | Once you fill out the button, you only have one enviroment variable to specify: CHATFORM_BASE_URL. This variable should be set to the web address for your new app. For example, if you are calling your app "foo-form", then CHATFORM_BASE_URL should be set to https://foo-form.herokuapp.com 44 | 45 | FAQ 46 | --- 47 | 48 | ### Why did you build this? 49 | 50 | I'm the co-founder of Smooch.io and spend a lot of my time thinking about how messaging is going to re-shape the way we interact with technology. A few months ago, we transformed our product's on-boarding: from a set of static web forms to a completely [conversational flow](http://blog.smooch.io/our-newest-hire-is-a-bot/). This simple change drove 12% more people to complete the new on-boarding. As a result, some of us at Smooch began to wonder how engaging a traditional web form could become if it were made conversational and delivered over the user's favourite messaging app. 51 | 52 | Fast forward to early 2017, back from Christmas vacation and with an urge to code. Lucie and I decided to join forces and build chatform to demonstrate how easily one could build a conversational form builder using the Smooch APIs. We built this project really quickly, there are probably lots of bugs, but we just had to get it out to the world and really hope you enjoy it. 53 | 54 | ### Why is this free? 55 | 56 | We want to inspire more people to think about how messaging interactions can change the way their users interact with their software. Chatform is one example of this, but what if Typeform, Google Forms or Wufoo went conversational? Will we begin to see more and more people conversing with their forms and surveys, instead of mechanically filling them out? 57 | 58 | ### What messaging channels does this support? 59 | 60 | Chatform uses [smooch.io](https://smooch.io) to send and receive messages through one API to a wide variety of [supported messaging channels](https://app.smooch.io/integrations/categories/customer-channels). You can publish surveys built with chatform across any of the messaging channels that Smooch supports. 61 | 62 | ### Can you use carousels? Lists? Images? 63 | 64 | Yes you can! But... 65 | 66 | We only had 48 hours to complete this project and as such, we couldn't build in support for all of the [rich messaging features](https://smooch.io/rich-messaging/) that Smooch.io supports. If there's something you'd like to see, why not submit a pull request? The [Smooch API Reference](http://docs.smooch.io/rest) contains everything you need to add support for more messaging features to Chatform. Best of all, if you implement any of those features, they'll become available across all channels that Smooch supports. 67 | 68 | Contributing 69 | ------------ 70 | 71 | If something is unclear, confusing, or needs to be refactored, please let me know. 72 | Pull requests are always welcome, but due to the opinionated nature of this 73 | project, I cannot accept every pull request. Please open an issue before 74 | submitting a pull request. 75 | 76 | License 77 | ------- 78 | 79 | The MIT License (MIT) 80 | 81 | Chatform logic and implementation: 82 | Copyright (c) 2017 Mike Gozzo 83 | 84 | Chatform design and UI: 85 | Copyright (c) 2017 Lucie Le Touze 86 | 87 | Chatform logo: 88 | Copyright (c) 2017 Kevin Gauthier 89 | 90 | Boilerplate express app (Hackathon Starter): 91 | Copyright (c) 2014-2016 Sahat Yalkabov 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 94 | 95 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 98 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | const express = require('express'); 5 | const compression = require('compression'); 6 | const session = require('express-session'); 7 | const bodyParser = require('body-parser'); 8 | const logger = require('morgan'); 9 | const chalk = require('chalk'); 10 | const errorHandler = require('errorhandler'); 11 | const lusca = require('lusca'); 12 | const dotenv = require('dotenv'); 13 | const MongoStore = require('connect-mongo')(session); 14 | const flash = require('express-flash'); 15 | const path = require('path'); 16 | const mongoose = require('mongoose'); 17 | const passport = require('passport'); 18 | const expressValidator = require('express-validator'); 19 | const expressStatusMonitor = require('express-status-monitor'); 20 | const sass = require('node-sass-middleware'); 21 | const multer = require('multer'); 22 | 23 | const upload = multer({ dest: path.join(__dirname, 'uploads') }); 24 | 25 | /** 26 | * Load environment variables from .env file, where API keys and passwords are configured. 27 | */ 28 | dotenv.load({ path: '.env.example' }); 29 | 30 | /** 31 | * Controllers (route handlers). 32 | */ 33 | const homeController = require('./controllers/home'); 34 | const userController = require('./controllers/user'); 35 | const formController = require('./controllers/form'); 36 | const botController = require('./controllers/bot'); 37 | 38 | /** 39 | * API keys and Passport configuration. 40 | */ 41 | const passportConfig = require('./config/passport'); 42 | 43 | /** 44 | * Create Express server. 45 | */ 46 | const app = express(); 47 | 48 | /** 49 | * Connect to MongoDB. 50 | */ 51 | mongoose.Promise = global.Promise; 52 | mongoose.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI); 53 | mongoose.connection.on('error', () => { 54 | console.log('%s MongoDB connection error. Please make sure MongoDB is running.', chalk.red('✗')); 55 | process.exit(); 56 | }); 57 | 58 | /** 59 | * Express configuration. 60 | */ 61 | app.set('port', process.env.PORT || 3000); 62 | app.set('views', path.join(__dirname, 'views')); 63 | app.set('view engine', 'pug'); 64 | app.use(expressStatusMonitor()); 65 | app.use(compression()); 66 | app.use(sass({ 67 | src: path.join(__dirname, 'public'), 68 | dest: path.join(__dirname, 'public') 69 | })); 70 | app.use(logger('dev')); 71 | app.use(bodyParser.json()); 72 | app.use(bodyParser.urlencoded({ extended: true })); 73 | app.use(expressValidator()); 74 | app.use(session({ 75 | resave: true, 76 | saveUninitialized: true, 77 | secret: process.env.SESSION_SECRET, 78 | store: new MongoStore({ 79 | url: process.env.MONGODB_URI || process.env.MONGOLAB_URI, 80 | autoReconnect: true 81 | }) 82 | })); 83 | app.use(passport.initialize()); 84 | app.use(passport.session()); 85 | app.use(flash()); 86 | app.use((req, res, next) => { 87 | if (req.path === '/api/upload' || req.path.indexOf('/bot') != -1) { 88 | next(); 89 | } else { 90 | lusca.csrf()(req, res, next); 91 | } 92 | }); 93 | app.use(lusca.xframe('SAMEORIGIN')); 94 | app.use(lusca.xssProtection(true)); 95 | app.use((req, res, next) => { 96 | res.locals.user = req.user; 97 | next(); 98 | }); 99 | app.use((req, res, next) => { 100 | // After successful login, redirect back to the intended page 101 | if (!req.user && 102 | req.path !== '/login' && 103 | req.path !== '/signup' && 104 | !req.path.match(/^\/auth/) && 105 | !req.path.match(/\./)) { 106 | req.session.returnTo = req.path; 107 | } else if (req.user && 108 | req.path == '/account') { 109 | req.session.returnTo = req.path; 110 | } 111 | next(); 112 | }); 113 | app.use(express.static(path.join(__dirname, 'public'), { maxAge: 31557600000 })); 114 | 115 | /** 116 | * Primary app routes. 117 | */ 118 | app.get('/', homeController.index); 119 | app.get('/login', userController.getLogin); 120 | app.post('/login', userController.postLogin); 121 | app.get('/logout', userController.logout); 122 | app.get('/forgot', userController.getForgot); 123 | app.post('/forgot', userController.postForgot); 124 | app.get('/reset/:token', userController.getReset); 125 | app.post('/reset/:token', userController.postReset); 126 | app.get('/signup', userController.getSignup); 127 | app.post('/signup', userController.postSignup); 128 | app.get('/account', passportConfig.isAuthenticated, userController.getAccount); 129 | app.post('/account/profile', passportConfig.isAuthenticated, userController.postUpdateProfile); 130 | app.post('/account/password', passportConfig.isAuthenticated, userController.postUpdatePassword); 131 | app.post('/account/delete', passportConfig.isAuthenticated, userController.postDeleteAccount); 132 | app.get('/account/unlink/:provider', passportConfig.isAuthenticated, userController.getOauthUnlink); 133 | 134 | /** 135 | * Form controller routes 136 | */ 137 | app.get('/forms', formController.getForms); 138 | app.get('/forms/new', formController.newForm); 139 | app.get('/forms/:formId', formController.getForm); 140 | app.get('/forms/:formId/responses', formController.getResponses); 141 | app.get('/forms/:formId/delete', formController.deleteForm); 142 | app.post('/forms/:formId', formController.postForm); 143 | app.post('/forms/new', formController.postForm); 144 | app.get('/connect-to-smooch', formController.oauthCallabck); 145 | app.get('/forms/:formId/publish', formController.getPublishForm); 146 | app.post('/forms/:formId/publish', formController.postPublishForm); 147 | /** 148 | * Bot controller routes 149 | */ 150 | app.post('/bot/:formId', botController.postMessage); 151 | 152 | /** 153 | * Error Handler. 154 | */ 155 | app.use(errorHandler()); 156 | 157 | /** 158 | * Start Express server. 159 | */ 160 | app.listen(app.get('port'), () => { 161 | console.log('%s App is running at http://localhost:%d in %s mode', chalk.green('✓'), app.get('port'), app.get('env'));
 162 | console.log(' Press CTRL-C to stop\n'); 163 | }); 164 | 165 | module.exports = app; 166 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatform", 3 | "description": "Create forms and surveys that are answered through your favourite messaging app, over Smooch", 4 | "website": "http://chatform.ai", 5 | "repository": "https://github.com/gozman/chatform", 6 | "keywords": [ 7 | "Smooch", 8 | "messaging", 9 | "bots", 10 | "surveys", 11 | "forms" 12 | ], 13 | "addons": [ 14 | "mongolab:sandbox", 15 | "heroku-redis:hobby-dev" 16 | ], 17 | "env": { 18 | "CHATFORM_BASE_URL": { 19 | "description": "The URL to your deployed instance of chatform. For example: https://app-name.herokuapp.com" 20 | } 21 | }, 22 | "generator": "https://www.expeditedssl.com/heroku-button-maker" 23 | } 24 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const passport = require('passport'); 3 | const request = require('request'); 4 | const InstagramStrategy = require('passport-instagram').Strategy; 5 | const LocalStrategy = require('passport-local').Strategy; 6 | const FacebookStrategy = require('passport-facebook').Strategy; 7 | const TwitterStrategy = require('passport-twitter').Strategy; 8 | const GitHubStrategy = require('passport-github').Strategy; 9 | const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 10 | const LinkedInStrategy = require('passport-linkedin-oauth2').Strategy; 11 | const OpenIDStrategy = require('passport-openid').Strategy; 12 | const OAuthStrategy = require('passport-oauth').OAuthStrategy; 13 | const OAuth2Strategy = require('passport-oauth').OAuth2Strategy; 14 | 15 | const User = require('../models/User'); 16 | 17 | passport.serializeUser((user, done) => { 18 | done(null, user.id); 19 | }); 20 | 21 | passport.deserializeUser((id, done) => { 22 | User.findById(id, (err, user) => { 23 | done(err, user); 24 | }); 25 | }); 26 | 27 | /** 28 | * Sign in using Email and Password. 29 | */ 30 | passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { 31 | User.findOne({ email: email.toLowerCase() }, (err, user) => { 32 | if (err) { return done(err); } 33 | if (!user) { 34 | return done(null, false, { msg: `Email ${email} not found.` }); 35 | } 36 | user.comparePassword(password, (err, isMatch) => { 37 | if (err) { return done(err); } 38 | if (isMatch) { 39 | return done(null, user); 40 | } 41 | return done(null, false, { msg: 'Invalid email or password.' }); 42 | }); 43 | }); 44 | })); 45 | 46 | /** 47 | * Login Required middleware. 48 | */ 49 | exports.isAuthenticated = (req, res, next) => { 50 | if (req.isAuthenticated()) { 51 | return next(); 52 | } 53 | res.redirect('/login'); 54 | }; 55 | 56 | /** 57 | * Authorization Required middleware. 58 | */ 59 | exports.isAuthorized = (req, res, next) => { 60 | const provider = req.path.split('/').slice(-1)[0]; 61 | 62 | if (_.find(req.user.tokens, { kind: provider })) { 63 | next(); 64 | } else { 65 | res.redirect(`/auth/${provider}`); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /controllers/bot.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const Form = require('../models/Form'); 3 | const Responder = require('../models/Responder'); 4 | const request = require('request'); 5 | const Smooch = require('smooch-core'); 6 | 7 | var kue = require('kue'); 8 | var queue = kue.createQueue({redis: process.env.REDIS_URL}); 9 | 10 | /** 11 | * POST /bot/:formId 12 | * 13 | */ 14 | exports.postMessage = (req, res, next) => { 15 | //Get form 16 | Form.findById(req.params.formId, (err, form) => { 17 | if(err) { 18 | console.log(err); 19 | return res.sendStatus(500); 20 | } 21 | 22 | queue.create('bot_dispatch', { 23 | appUser: req.body.appUser, 24 | messageText: req.body.messages[0].text, 25 | headers: req.headers, 26 | formId: req.params.formId 27 | }).save(function(err) { 28 | if(err){console.log(err)}; 29 | }); 30 | 31 | res.sendStatus(200); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET / 3 | * Home page. 4 | */ 5 | exports.index = (req, res) => { 6 | res.render('home', { 7 | title: 'Home' 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /models/Form.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const formSchema = new mongoose.Schema({ 5 | ownerId: {type: ObjectId, index:true}, 6 | name: String, 7 | smoochToken: String, 8 | fields: [{}], 9 | startTrigger: String, 10 | startMessage: String, 11 | endTrigger: String, 12 | endMessage: String, 13 | responseCount: Number, 14 | smoochWebHookId: String 15 | }, { timestamps: true }); 16 | 17 | const Form = mongoose.model('Form', formSchema); 18 | module.exports = Form; 19 | -------------------------------------------------------------------------------- /models/Responder.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const responderSchema = new mongoose.Schema({ 5 | formId: {type: ObjectId, index:true}, 6 | appUserId: {type: String, index:true}, 7 | appUser: {}, 8 | response: {} 9 | }, { timestamps: true }); 10 | 11 | const Responder = mongoose.model('Responder', responderSchema); 12 | module.exports = Responder; 13 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt-nodejs'); 2 | const crypto = require('crypto'); 3 | const mongoose = require('mongoose'); 4 | 5 | const userSchema = new mongoose.Schema({ 6 | email: { type: String, unique: true }, 7 | password: String, 8 | passwordResetToken: String, 9 | passwordResetExpires: Date, 10 | 11 | facebook: String, 12 | twitter: String, 13 | google: String, 14 | github: String, 15 | instagram: String, 16 | linkedin: String, 17 | steam: String, 18 | tokens: Array, 19 | 20 | profile: { 21 | name: String, 22 | gender: String, 23 | location: String, 24 | website: String, 25 | picture: String 26 | } 27 | }, { timestamps: true }); 28 | 29 | /** 30 | * Password hash middleware. 31 | */ 32 | userSchema.pre('save', function save(next) { 33 | const user = this; 34 | if (!user.isModified('password')) { return next(); } 35 | bcrypt.genSalt(10, (err, salt) => { 36 | if (err) { return next(err); } 37 | bcrypt.hash(user.password, salt, null, (err, hash) => { 38 | if (err) { return next(err); } 39 | user.password = hash; 40 | next(); 41 | }); 42 | }); 43 | }); 44 | 45 | /** 46 | * Helper method for validating user's password. 47 | */ 48 | userSchema.methods.comparePassword = function comparePassword(candidatePassword, cb) { 49 | bcrypt.compare(candidatePassword, this.password, (err, isMatch) => { 50 | cb(err, isMatch); 51 | }); 52 | }; 53 | 54 | /** 55 | * Helper method for getting user's gravatar. 56 | */ 57 | userSchema.methods.gravatar = function gravatar(size) { 58 | if (!size) { 59 | size = 200; 60 | } 61 | if (!this.email) { 62 | return `https://gravatar.com/avatar/?s=${size}&d=retro`; 63 | } 64 | const md5 = crypto.createHash('md5').update(this.email).digest('hex'); 65 | return `https://gravatar.com/avatar/${md5}?s=${size}&d=retro`; 66 | }; 67 | 68 | const User = mongoose.model('User', userSchema); 69 | 70 | module.exports = User; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackathon-starter", 3 | "version": "4.3.0", 4 | "description": "A boilerplate for Node.js web applications", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sahat/hackathon-starter.git" 8 | }, 9 | "author": "Sahat Yalkabov", 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "node app.js", 13 | "test": "mocha --reporter spec" 14 | }, 15 | "dependencies": { 16 | "async": "^2.1.2", 17 | "bcrypt-nodejs": "^0.0.3", 18 | "body-parser": "^1.15.2", 19 | "chalk": "^1.1.3", 20 | "cheerio": "^0.22.0", 21 | "clockwork": "^0.1.4", 22 | "compression": "^1.6.2", 23 | "connect-mongo": "^1.3.2", 24 | "dotenv": "^2.0.0", 25 | "errorhandler": "^1.4.3", 26 | "express": "^4.14.0", 27 | "express-flash": "^0.0.2", 28 | "express-session": "^1.14.2", 29 | "express-status-monitor": "^0.1.5", 30 | "express-validator": "^2.21.0", 31 | "fbgraph": "^1.3.0", 32 | "github": "^6.0.2", 33 | "instagram-node": "^0.5.8", 34 | "json-2-csv": "^2.1.0", 35 | "jsonwebtoken": "^7.2.1", 36 | "kue": "^0.11.5", 37 | "lastfm": "^0.9.2", 38 | "lob": "^3.9.0", 39 | "lodash": "^4.16.6", 40 | "lusca": "^1.4.1", 41 | "mongoose": "^4.6.6", 42 | "morgan": "^1.7.0", 43 | "multer": "^1.2.0", 44 | "node-foursquare": "^0.3.0", 45 | "node-linkedin": "^0.5.4", 46 | "node-sass-middleware": "^0.9.8", 47 | "nodemailer": "^2.6.4", 48 | "passport": "0.3.2", 49 | "passport-facebook": "^2.1.1", 50 | "passport-github": "^1.1.0", 51 | "passport-google-oauth": "^1.0.0", 52 | "passport-instagram": "^1.0.0", 53 | "passport-linkedin-oauth2": "^1.4.1", 54 | "passport-local": "^1.0.0", 55 | "passport-oauth": "^1.0.0", 56 | "passport-openid": "^0.4.0", 57 | "passport-twitter": "^1.0.4", 58 | "paypal-rest-sdk": "^1.7.0", 59 | "pug": "^2.0.0-beta6", 60 | "request": "^2.78.0", 61 | "smooch-core": "^3.1.1", 62 | "stripe": "^4.12.0", 63 | "tumblr.js": "^1.1.1", 64 | "twilio": "^3.3.1-edge", 65 | "twit": "^2.2.5", 66 | "validator": "^6.1.0" 67 | }, 68 | "devDependencies": { 69 | "chai": "^3.5.0", 70 | "eslint": "^3.9.1", 71 | "eslint-config-airbnb-base": "^10.0.0", 72 | "eslint-plugin-import": "^2.1.0", 73 | "mocha": "^3.1.2", 74 | "sinon": "^1.17.6", 75 | "sinon-mongoose": "^1.3.0", 76 | "supertest": "^2.0.1" 77 | }, 78 | "eslintConfig": { 79 | "extends": "airbnb-base", 80 | "rules": { 81 | "comma-dangle": 0, 82 | "consistent-return": 0, 83 | "no-param-reassign": 0, 84 | "no-underscore-dangle": 0, 85 | "no-shadow": 0, 86 | "no-console": 0, 87 | "no-plusplus": 0 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /public/bckg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gozman/chatform/3c87c44f8546f498ba89cb7ea8e7cb0e8fe5db26/public/bckg.png -------------------------------------------------------------------------------- /public/css/lib/bootstrap-social.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Social Buttons for Bootstrap 3 | * 4 | * Copyright 2013-2015 Panayiotis Lipiridis 5 | * Licensed under the MIT License 6 | * 7 | * https://github.com/lipis/bootstrap-social 8 | */ 9 | 10 | $bs-height-base: ($line-height-computed + $padding-base-vertical * 2); 11 | $bs-height-lg: (floor($font-size-large * $line-height-base) + $padding-large-vertical * 2); 12 | $bs-height-sm: (floor($font-size-small * 1.5) + $padding-small-vertical * 2); 13 | $bs-height-xs: (floor($font-size-small * 1.2) + $padding-small-vertical + 1); 14 | 15 | .btn-social { 16 | position: relative; 17 | padding-left: ($bs-height-base + $padding-base-horizontal); 18 | text-align: left; 19 | white-space: nowrap; 20 | overflow: hidden; 21 | text-overflow: ellipsis; 22 | > :first-child { 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | bottom: 0; 27 | width: $bs-height-base; 28 | line-height: ($bs-height-base + 2); 29 | font-size: 1.6em; 30 | text-align: center; 31 | border-right: 1px solid rgba(0, 0, 0, 0.2); 32 | } 33 | &.btn-lg { 34 | padding-left: ($bs-height-lg + $padding-large-horizontal); 35 | > :first-child { 36 | line-height: $bs-height-lg; 37 | width: $bs-height-lg; 38 | font-size: 1.8em; 39 | } 40 | } 41 | &.btn-sm { 42 | padding-left: ($bs-height-sm + $padding-small-horizontal); 43 | > :first-child { 44 | line-height: $bs-height-sm; 45 | width: $bs-height-sm; 46 | font-size: 1.4em; 47 | } 48 | } 49 | &.btn-xs { 50 | padding-left: ($bs-height-xs + $padding-small-horizontal); 51 | > :first-child { 52 | line-height: $bs-height-xs; 53 | width: $bs-height-xs; 54 | font-size: 1.2em; 55 | } 56 | } 57 | } 58 | 59 | .btn-social-icon { 60 | @extend .btn-social; 61 | height: ($bs-height-base + 2); 62 | width: ($bs-height-base + 2); 63 | padding: 0; 64 | > :first-child { 65 | border: none; 66 | text-align: center; 67 | width: 100%!important; 68 | } 69 | &.btn-lg { 70 | height: $bs-height-lg; 71 | width: $bs-height-lg; 72 | padding-left: 0; 73 | padding-right: 0; 74 | } 75 | &.btn-sm { 76 | height: ($bs-height-sm + 2); 77 | width: ($bs-height-sm + 2); 78 | padding-left: 0; 79 | padding-right: 0; 80 | } 81 | &.btn-xs { 82 | height: ($bs-height-xs + 2); 83 | width: ($bs-height-xs + 2); 84 | padding-left: 0; 85 | padding-right: 0; 86 | } 87 | } 88 | 89 | @mixin btn-social($color-bg, $color: #fff) { 90 | background-color: $color-bg; 91 | @include button-variant($color, $color-bg, rgba(0,0,0,.2)); 92 | } 93 | 94 | 95 | .btn-adn { @include btn-social(#d87a68); } 96 | .btn-bitbucket { @include btn-social(#205081); } 97 | .btn-dropbox { @include btn-social(#1087dd); } 98 | .btn-facebook { @include btn-social(#3b5998); } 99 | .btn-flickr { @include btn-social(#ff0084); } 100 | .btn-foursquare { @include btn-social(#f94877); } 101 | .btn-github { @include btn-social(#444444); } 102 | .btn-google { @include btn-social(#dd4b39); } 103 | .btn-instagram { @include btn-social(#3f729b); } 104 | .btn-linkedin { @include btn-social(#007bb6); } 105 | .btn-microsoft { @include btn-social(#2672ec); } 106 | .btn-odnoklassniki { @include btn-social(#f4731c); } 107 | .btn-openid { @include btn-social(#f7931e); } 108 | .btn-pinterest { @include btn-social(#cb2027); } 109 | .btn-reddit { @include btn-social(#eff7ff, #000); } 110 | .btn-soundcloud { @include btn-social(#ff5500); } 111 | .btn-tumblr { @include btn-social(#2c4762); } 112 | .btn-twitter { @include btn-social(#55acee); } 113 | .btn-vimeo { @include btn-social(#1ab7ea); } 114 | .btn-vk { @include btn-social(#587ea3); } 115 | .btn-yahoo { @include btn-social(#720e9e); } 116 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headings-color 19 | color: inherit; 20 | } 21 | 22 | // Provide class for links that match alerts 23 | .alert-link { 24 | font-weight: $alert-link-font-weight; 25 | } 26 | 27 | // Improve alignment and spacing of inner content 28 | > p, 29 | > ul { 30 | margin-bottom: 0; 31 | } 32 | 33 | > p + p { 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | // Dismissible alerts 39 | // 40 | // Expand the right padding and account for the close button's positioning. 41 | 42 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 43 | .alert-dismissible { 44 | padding-right: ($alert-padding + 20); 45 | 46 | // Adjust close link position 47 | .close { 48 | position: relative; 49 | top: -2px; 50 | right: -21px; 51 | color: inherit; 52 | } 53 | } 54 | 55 | // Alternate styles 56 | // 57 | // Generate contextual modifier classes for colorizing the alert. 58 | 59 | .alert-success { 60 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 61 | } 62 | 63 | .alert-info { 64 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 65 | } 66 | 67 | .alert-warning { 68 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 69 | } 70 | 71 | .alert-danger { 72 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 73 | } 74 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: middle; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | 32 | .btn-xs &, 33 | .btn-group-xs > .btn & { 34 | top: 0; 35 | padding: 1px 5px; 36 | } 37 | 38 | // [converter] extracted a& to a.badge 39 | 40 | // Account for badges in navs 41 | .list-group-item.active > &, 42 | .nav-pills > .active > a > & { 43 | color: $badge-active-color; 44 | background-color: $badge-active-bg; 45 | } 46 | 47 | .list-group-item > & { 48 | float: right; 49 | } 50 | 51 | .list-group-item > & + & { 52 | margin-right: 5px; 53 | } 54 | 55 | .nav-pills > li > a > & { 56 | margin-left: 3px; 57 | } 58 | } 59 | 60 | // Hover state, but only for links 61 | a.badge { 62 | &:hover, 63 | &:focus { 64 | color: $badge-link-hover-color; 65 | text-decoration: none; 66 | cursor: pointer; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | // [converter] Workaround for https://github.com/sass/libsass/issues/1115 18 | $nbsp: "\00a0"; 19 | content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space 20 | padding: 0 5px; 21 | color: $breadcrumb-color; 22 | } 23 | } 24 | 25 | > .active { 26 | color: $breadcrumb-active-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // -------------------------------------------------- 8 | 9 | .btn { 10 | display: inline-block; 11 | margin-bottom: 0; // For input.btn 12 | font-weight: $btn-font-weight; 13 | text-align: center; 14 | vertical-align: middle; 15 | touch-action: manipulation; 16 | cursor: pointer; 17 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 18 | border: 1px solid transparent; 19 | white-space: nowrap; 20 | @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base); 21 | @include user-select(none); 22 | 23 | &, 24 | &:active, 25 | &.active { 26 | &:focus, 27 | &.focus { 28 | @include tab-focus; 29 | } 30 | } 31 | 32 | &:hover, 33 | &:focus, 34 | &.focus { 35 | color: $btn-default-color; 36 | text-decoration: none; 37 | } 38 | 39 | &:active, 40 | &.active { 41 | outline: 0; 42 | background-image: none; 43 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 44 | } 45 | 46 | &.disabled, 47 | &[disabled], 48 | fieldset[disabled] & { 49 | cursor: $cursor-disabled; 50 | @include opacity(.65); 51 | @include box-shadow(none); 52 | } 53 | 54 | // [converter] extracted a& to a.btn 55 | } 56 | 57 | a.btn { 58 | &.disabled, 59 | fieldset[disabled] & { 60 | pointer-events: none; // Future-proof disabling of clicks on `` elements 61 | } 62 | } 63 | 64 | 65 | // Alternate buttons 66 | // -------------------------------------------------- 67 | 68 | .btn-default { 69 | @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); 70 | } 71 | .btn-primary { 72 | @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); 73 | } 74 | // Success appears as green 75 | .btn-success { 76 | @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); 77 | } 78 | // Info appears as blue-green 79 | .btn-info { 80 | @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); 81 | } 82 | // Warning appears as orange 83 | .btn-warning { 84 | @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); 85 | } 86 | // Danger and error appear as red 87 | .btn-danger { 88 | @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); 89 | } 90 | 91 | 92 | // Link buttons 93 | // ------------------------- 94 | 95 | // Make a button look and behave like a link 96 | .btn-link { 97 | color: $link-color; 98 | font-weight: normal; 99 | border-radius: 0; 100 | 101 | &, 102 | &:active, 103 | &.active, 104 | &[disabled], 105 | fieldset[disabled] & { 106 | background-color: transparent; 107 | @include box-shadow(none); 108 | } 109 | &, 110 | &:hover, 111 | &:focus, 112 | &:active { 113 | border-color: transparent; 114 | } 115 | &:hover, 116 | &:focus { 117 | color: $link-hover-color; 118 | text-decoration: $link-hover-decoration; 119 | background-color: transparent; 120 | } 121 | &[disabled], 122 | fieldset[disabled] & { 123 | &:hover, 124 | &:focus { 125 | color: $btn-link-disabled-color; 126 | text-decoration: none; 127 | } 128 | } 129 | } 130 | 131 | 132 | // Button Sizes 133 | // -------------------------------------------------- 134 | 135 | .btn-lg { 136 | // line-height: ensure even-numbered height of button next to large input 137 | @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large); 138 | } 139 | .btn-sm { 140 | // line-height: ensure proper height of button next to small input 141 | @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); 142 | } 143 | .btn-xs { 144 | @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); 145 | } 146 | 147 | 148 | // Block button 149 | // -------------------------------------------------- 150 | 151 | .btn-block { 152 | display: block; 153 | width: 100%; 154 | } 155 | 156 | // Vertically space out multiple block buttons 157 | .btn-block + .btn-block { 158 | margin-top: 5px; 159 | } 160 | 161 | // Specificity overrides 162 | input[type="submit"], 163 | input[type="reset"], 164 | input[type="button"] { 165 | &.btn-block { 166 | width: 100%; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | } 37 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: $font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: $code-color; 19 | background-color: $code-bg; 20 | border-radius: $border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: $kbd-color; 28 | background-color: $kbd-bg; 29 | border-radius: $border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | font-weight: bold; 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | // Blocks of code 41 | pre { 42 | display: block; 43 | padding: (($line-height-computed - 1) / 2); 44 | margin: 0 0 ($line-height-computed / 2); 45 | font-size: ($font-size-base - 1); // 14px to 13px 46 | line-height: $line-height-base; 47 | word-break: break-all; 48 | word-wrap: break-word; 49 | color: $pre-color; 50 | background-color: $pre-bg; 51 | border: 1px solid $pre-border-color; 52 | border-radius: $border-radius-base; 53 | 54 | // Account for some code outputs that place code tags in pre tags 55 | code { 56 | padding: 0; 57 | font-size: inherit; 58 | color: inherit; 59 | white-space: pre-wrap; 60 | background-color: transparent; 61 | border-radius: 0; 62 | } 63 | } 64 | 65 | // Enable scrollable blocks of code 66 | .pre-scrollable { 67 | max-height: $pre-scrollable-max-height; 68 | overflow-y: scroll; 69 | } 70 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | // [converter] extracted tr&.in to tr.collapse.in 23 | // [converter] extracted tbody&.in to tbody.collapse.in 24 | } 25 | 26 | tr.collapse.in { display: table-row; } 27 | 28 | tbody.collapse.in { display: table-row-group; } 29 | 30 | .collapsing { 31 | position: relative; 32 | height: 0; 33 | overflow: hidden; 34 | @include transition-property(height, visibility); 35 | @include transition-duration(.35s); 36 | @include transition-timing-function(ease); 37 | } 38 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | @include container-fixed; 12 | 13 | @media (min-width: $screen-sm-min) { 14 | width: $container-sm; 15 | } 16 | @media (min-width: $screen-md-min) { 17 | width: $container-md; 18 | } 19 | @media (min-width: $screen-lg-min) { 20 | width: $container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | @include container-fixed; 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | @include make-row; 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | @include make-grid-columns; 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | @include make-grid(xs); 57 | 58 | 59 | // Small grid 60 | // 61 | // Columns, offsets, pushes, and pulls for the small device range, from phones 62 | // to tablets. 63 | 64 | @media (min-width: $screen-sm-min) { 65 | @include make-grid(sm); 66 | } 67 | 68 | 69 | // Medium grid 70 | // 71 | // Columns, offsets, pushes, and pulls for the desktop device range. 72 | 73 | @media (min-width: $screen-md-min) { 74 | @include make-grid(md); 75 | } 76 | 77 | 78 | // Large grid 79 | // 80 | // Columns, offsets, pushes, and pulls for the large desktop device range. 81 | 82 | @media (min-width: $screen-lg-min) { 83 | @include make-grid(lg); 84 | } 85 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_input-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Input groups 3 | // -------------------------------------------------- 4 | 5 | // Base styles 6 | // ------------------------- 7 | .input-group { 8 | position: relative; // For dropdowns 9 | display: table; 10 | border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table 11 | 12 | // Undo padding and float of grid classes 13 | &[class*="col-"] { 14 | float: none; 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .form-control { 20 | // Ensure that the input is always above the *appended* addon button for 21 | // proper border colors. 22 | position: relative; 23 | z-index: 2; 24 | 25 | // IE9 fubars the placeholder attribute in text inputs and the arrows on 26 | // select elements in input groups. To fix it, we float the input. Details: 27 | // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 28 | float: left; 29 | 30 | width: 100%; 31 | margin-bottom: 0; 32 | 33 | &:focus { 34 | z-index: 3; 35 | } 36 | } 37 | } 38 | 39 | // Sizing options 40 | // 41 | // Remix the default form control sizing classes into new ones for easier 42 | // manipulation. 43 | 44 | .input-group-lg > .form-control, 45 | .input-group-lg > .input-group-addon, 46 | .input-group-lg > .input-group-btn > .btn { 47 | @extend .input-lg; 48 | } 49 | .input-group-sm > .form-control, 50 | .input-group-sm > .input-group-addon, 51 | .input-group-sm > .input-group-btn > .btn { 52 | @extend .input-sm; 53 | } 54 | 55 | 56 | // Display as table-cell 57 | // ------------------------- 58 | .input-group-addon, 59 | .input-group-btn, 60 | .input-group .form-control { 61 | display: table-cell; 62 | 63 | &:not(:first-child):not(:last-child) { 64 | border-radius: 0; 65 | } 66 | } 67 | // Addon and addon wrapper for buttons 68 | .input-group-addon, 69 | .input-group-btn { 70 | width: 1%; 71 | white-space: nowrap; 72 | vertical-align: middle; // Match the inputs 73 | } 74 | 75 | // Text input groups 76 | // ------------------------- 77 | .input-group-addon { 78 | padding: $padding-base-vertical $padding-base-horizontal; 79 | font-size: $font-size-base; 80 | font-weight: normal; 81 | line-height: 1; 82 | color: $input-color; 83 | text-align: center; 84 | background-color: $input-group-addon-bg; 85 | border: 1px solid $input-group-addon-border-color; 86 | border-radius: $input-border-radius; 87 | 88 | // Sizing 89 | &.input-sm { 90 | padding: $padding-small-vertical $padding-small-horizontal; 91 | font-size: $font-size-small; 92 | border-radius: $input-border-radius-small; 93 | } 94 | &.input-lg { 95 | padding: $padding-large-vertical $padding-large-horizontal; 96 | font-size: $font-size-large; 97 | border-radius: $input-border-radius-large; 98 | } 99 | 100 | // Nuke default margins from checkboxes and radios to vertically center within. 101 | input[type="radio"], 102 | input[type="checkbox"] { 103 | margin-top: 0; 104 | } 105 | } 106 | 107 | // Reset rounded corners 108 | .input-group .form-control:first-child, 109 | .input-group-addon:first-child, 110 | .input-group-btn:first-child > .btn, 111 | .input-group-btn:first-child > .btn-group > .btn, 112 | .input-group-btn:first-child > .dropdown-toggle, 113 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), 114 | .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { 115 | @include border-right-radius(0); 116 | } 117 | .input-group-addon:first-child { 118 | border-right: 0; 119 | } 120 | .input-group .form-control:last-child, 121 | .input-group-addon:last-child, 122 | .input-group-btn:last-child > .btn, 123 | .input-group-btn:last-child > .btn-group > .btn, 124 | .input-group-btn:last-child > .dropdown-toggle, 125 | .input-group-btn:first-child > .btn:not(:first-child), 126 | .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { 127 | @include border-left-radius(0); 128 | } 129 | .input-group-addon:last-child { 130 | border-left: 0; 131 | } 132 | 133 | // Button input groups 134 | // ------------------------- 135 | .input-group-btn { 136 | position: relative; 137 | // Jankily prevent input button groups from wrapping with `white-space` and 138 | // `font-size` in combination with `inline-block` on buttons. 139 | font-size: 0; 140 | white-space: nowrap; 141 | 142 | // Negative margin for spacing, position for bringing hovered/focused/actived 143 | // element above the siblings. 144 | > .btn { 145 | position: relative; 146 | + .btn { 147 | margin-left: -1px; 148 | } 149 | // Bring the "active" button to the front 150 | &:hover, 151 | &:focus, 152 | &:active { 153 | z-index: 2; 154 | } 155 | } 156 | 157 | // Negative margin to only have a 1px border between the two 158 | &:first-child { 159 | > .btn, 160 | > .btn-group { 161 | margin-right: -1px; 162 | } 163 | } 164 | &:last-child { 165 | > .btn, 166 | > .btn-group { 167 | z-index: 2; 168 | margin-left: -1px; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding-top: $jumbotron-padding; 8 | padding-bottom: $jumbotron-padding; 9 | margin-bottom: $jumbotron-padding; 10 | color: $jumbotron-color; 11 | background-color: $jumbotron-bg; 12 | 13 | h1, 14 | .h1 { 15 | color: $jumbotron-heading-color; 16 | } 17 | 18 | p { 19 | margin-bottom: ($jumbotron-padding / 2); 20 | font-size: $jumbotron-font-size; 21 | font-weight: 200; 22 | } 23 | 24 | > hr { 25 | border-top-color: darken($jumbotron-bg, 10%); 26 | } 27 | 28 | .container &, 29 | .container-fluid & { 30 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 31 | padding-left: ($grid-gutter-width / 2); 32 | padding-right: ($grid-gutter-width / 2); 33 | } 34 | 35 | .container { 36 | max-width: 100%; 37 | } 38 | 39 | @media screen and (min-width: $screen-sm-min) { 40 | padding-top: ($jumbotron-padding * 1.6); 41 | padding-bottom: ($jumbotron-padding * 1.6); 42 | 43 | .container &, 44 | .container-fluid & { 45 | padding-left: ($jumbotron-padding * 2); 46 | padding-right: ($jumbotron-padding * 2); 47 | } 48 | 49 | h1, 50 | .h1 { 51 | font-size: $jumbotron-heading-font-size; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // [converter] extracted a& to a.label 18 | 19 | // Empty labels collapse automatically (not available in IE8) 20 | &:empty { 21 | display: none; 22 | } 23 | 24 | // Quick fix for labels in buttons 25 | .btn & { 26 | position: relative; 27 | top: -1px; 28 | } 29 | } 30 | 31 | // Add hover effects, but only for links 32 | a.label { 33 | &:hover, 34 | &:focus { 35 | color: $label-link-hover-color; 36 | text-decoration: none; 37 | cursor: pointer; 38 | } 39 | } 40 | 41 | // Colors 42 | // Contextual variations (linked labels get darker on :hover) 43 | 44 | .label-default { 45 | @include label-variant($label-default-bg); 46 | } 47 | 48 | .label-primary { 49 | @include label-variant($label-primary-bg); 50 | } 51 | 52 | .label-success { 53 | @include label-variant($label-success-bg); 54 | } 55 | 56 | .label-info { 57 | @include label-variant($label-info-bg); 58 | } 59 | 60 | .label-warning { 61 | @include label-variant($label-warning-bg); 62 | } 63 | 64 | .label-danger { 65 | @include label-variant($label-danger-bg); 66 | } 67 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_list-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | // 8 | // Easily usable on