├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── apps ├── bootstrap.js ├── journal_database.js ├── models │ ├── admin_model.js │ ├── graph_model.js │ ├── journal_model.js │ ├── linker.js │ └── topic_model.js ├── slug.js ├── topic_database.js └── user_database.js ├── bin └── www ├── config ├── config.json ├── owner.json └── vocab │ ├── biomed │ ├── defns.json │ └── labels.json │ ├── climate │ ├── defns.json │ └── labels.json │ └── vocab.md ├── data └── .keep ├── package.json ├── public ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── default.css │ ├── medium-editor.css │ └── style.css ├── images │ ├── TopicQuestsLogo_sm.png │ ├── github-1.jpg │ └── map.gif └── js │ ├── .DS_Store │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── bootstrap3-typeahead.js │ ├── jquery-3.2.1.min.js │ ├── medium-editor.min.js │ └── vis.js ├── routes ├── helper.js ├── index.js └── users.js └── views ├── editor.hbs ├── error.hbs ├── iframe.hbs ├── index.hbs ├── journal_edit_form.hbs ├── journalview.hbs ├── layout.hbs ├── login_form.hbs ├── partials ├── air_form.hbs ├── footer.hbs ├── triple_form.hbs └── webmenubar.hbs ├── signup_form.hbs ├── topic_index.hbs └── topicview.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lite-net-3 2 | Simple triple journal 3 | 4 | Just: 5 | * npm install 6 | * Edit the file /config/owner.json to suit your own credentials 7 | * Edit the file /config/config.json to suit whether private or public 8 | * Remove all database files in /data to start fresh 9 | * npm start 10 | * visit http://localhost:4000/ 11 | 12 | 13 | To clean the databases, just delete the two files in the /data folder. 14 | 15 | ## UX 16 | On the Journal landing page, there are three methods by which new journal entries are crafted: 17 | * Entering a URL for harvesting 18 | * Entering a text note 19 | * Creating a triple 20 | 21 | ### Entering a URL for harvesting 22 | Past a URL in the box and click Submit. 23 | A new page will be presented with the content of the selected URL visible, together with two modes of journal fabrication: a text note and a triple. 24 | 25 | This allows for harvesting information from the selected resource. 26 | ### Entering a text note 27 | A text note can be any text desired, including copying and pasting from other sources. This is not (yet) a _rich text_ feature. 28 | In any text entered, it is possible to create _Wikilinks_ by surrounding a word or phrase with "[[ ... ]]". 29 | ### Creating a statement as a triple structure 30 | A triple consists of three components to form a statement like _something causes something else_, which has the form {subject, predicate, object}. Type the subject word or phrase into its box, and do the same for the predicate and the object. In each case, _typeahead_ will help you find the term or predicate. There is an opportunity to add a URL and a comment. 31 | 32 | ### Viewing a Graph 33 | Each Topic, particularly those which are not Relations, may have a graph created by Statements. Viewing a Graph for a topic means clicking the Graph View Button on a Topic View. At this time, some small graphs tend to sit in the upper left corner, and do not center. 34 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const cookieParser = require('cookie-parser'); 5 | const logger = require('morgan'); 6 | const flash = require("connect-flash"); 7 | const session = require("express-session"); 8 | const uuid = require('uuid'); 9 | const indexRouter = require('./routes/index'); 10 | const usersRouter = require('./routes/users'); 11 | 12 | const app = express(); 13 | const hbs = require('hbs'); 14 | 15 | // view engine setup 16 | app.set('views', path.join(__dirname, 'views')); 17 | app.set('view engine', 'hbs'); 18 | hbs.registerPartials(`${__dirname}/views/partials`); 19 | 20 | app.use(logger('dev')); 21 | app.use(express.json({limit: '50mb'})); 22 | app.use(express.urlencoded({ extended: false, limit: '50mb' })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | 26 | 27 | app.use(flash()); 28 | app.use(session({ 29 | genid(req) { 30 | return uuid.v4(); // use UUIDs for session IDs 31 | }, 32 | secret: "collaborative really hot sauce", //TODO ChangeMe 33 | resave: true, 34 | saveUninitialized: true 35 | })); 36 | 37 | app.use('/', indexRouter); 38 | app.use('/users', usersRouter); 39 | 40 | // catch 404 and forward to error handler 41 | app.use((req, res, next) => { 42 | next(createError(404)); 43 | }); 44 | 45 | // error handler 46 | app.use((err, req, res, next) => { 47 | // set locals, only providing error in development 48 | res.locals.message = err.message; 49 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 50 | 51 | // render the error page 52 | res.status(err.status || 500); 53 | res.render('error'); 54 | }); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /apps/bootstrap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const owner = require('../config/owner'); 3 | const userDB = require('./user_database'); 4 | const topicDB = require('./topic_database'); 5 | const AdminModel = require('./models/admin_model'); 6 | 7 | class Bootstrap { 8 | constructor() { 9 | console.info("Bootstrap-1", AdminModel); 10 | } 11 | 12 | async migrateTransclusions() { 13 | console.info('Migrating Transclusions'); 14 | //Not using this 15 | //For every href entry in backlinks array 16 | // if it is an href. find the journal id and substitute that into 17 | // a new array journal/ 18 | 19 | /* topicDB.find({}, function(err, data) { 20 | console.info('Migrate-1', err, data); 21 | var backlinks; 22 | var newLinks = []; 23 | var found = false; 24 | var where; 25 | var theLink; 26 | data.forEach(function(topic) { 27 | console.info('Migrate-2', topic); 28 | backlinks = topic.backlinks; 29 | found = false; 30 | backlinks.forEach(function(link) { 31 | theLink = link; 32 | if (theLink.startsWith('"); 36 | theLink = theLink.substring(0, where); 37 | //theLink is now the journal Id 38 | console.info('Migrate-3', link, theLink); 39 | newLinks.push(theLink); 40 | found = true; 41 | } 42 | }); 43 | if (found) { 44 | console.log('Migrate-4', topic); 45 | topicDB.replaceBacklinks(topic._id, newLinks, function(err, data){ 46 | // 47 | }); 48 | } 49 | }); 50 | 51 | });*/ 52 | return true; 53 | }; 54 | 55 | /** 56 | * @param { done } 57 | */ 58 | async validateUserDB() { 59 | console.debug('validateUserDB'); 60 | const empty = await userDB.isEmpty(); 61 | console.info('BootstrapCheck', empty); 62 | if (!empty) { 63 | await AdminModel.signup( 64 | owner.email, 65 | owner.handle, 66 | owner.fullName, 67 | owner.password); 68 | } else { 69 | console.log('Not Bootstrapped'); 70 | } 71 | return true; 72 | }; 73 | 74 | async bootstrap() { 75 | console.debug('bootstrap'); 76 | const done = await this.validateUserDB(); 77 | return await this.migrateTransclusions(); 78 | }; 79 | 80 | }; 81 | 82 | const instance = new Bootstrap(); 83 | async function bootstrap() { 84 | await instance.bootstrap(); 85 | } 86 | module.exports = bootstrap; 87 | -------------------------------------------------------------------------------- /apps/journal_database.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Datastore = require('nedb-promises') 3 | 4 | class Database { 5 | constructor() { 6 | this.db = new Datastore({ filename: './data/journal' , autoload: true }); 7 | console.info(`Database ${this.db}`); 8 | } 9 | 10 | /** 11 | * Insert a journal entry 12 | * @param jsonDoc 13 | */ 14 | async put(jsonDoc) { 15 | return await this.db.insert(jsonDoc); 16 | } 17 | 18 | /** 19 | * Return a journal entry identified by id 20 | * @param id 21 | */ 22 | async get(id) { 23 | return await this.db.findOne({ id }); 24 | } 25 | 26 | /** 27 | * Return a list of journal entries sorted on date latest on top 28 | * @param limit 29 | * @param skip 30 | */ 31 | async list(limit, skip) { 32 | console.info('JnlList', limit, skip); 33 | const result = await Promise.all([this.db.find({}).sort({ date: -1 }).limit(limit).skip(skip), this.db.count({})]); 34 | //console.info('JnlList-1', result); 35 | return result; 36 | } 37 | 38 | /** 39 | * General find support 40 | * @param query json 41 | */ 42 | async find(query) { 43 | console.info('JnlFind', query); 44 | return await this.db.find(query); 45 | } 46 | 47 | async updateJournalText(id, newText, isTriple) { 48 | console.info('JnlUpdateText', id, newText, isTriple); 49 | if (!isTriple) { 50 | return await this.db.update( { id}, { $set: { text: newText} },{}); 51 | } else { 52 | return await this.db.update( { id}, { $set: { bodylist: newText} },{}); 53 | } 54 | } 55 | 56 | /** 57 | * Find topics by URL 58 | * @param url 59 | */ 60 | async findByURL(url) { 61 | return await this.find({ urllist: url }); 62 | } 63 | 64 | }; 65 | 66 | const instance = new Database(); 67 | module.exports = instance; 68 | -------------------------------------------------------------------------------- /apps/models/admin_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const userDB = require('../user_database'); 3 | const uuid = require('uuid'); 4 | const bcrypt = require('bcrypt-nodejs'); 5 | const util = require('util'); 6 | 7 | class AdminModel { 8 | 9 | constructor() { 10 | this.hash = util.promisify(bcrypt.hash); 11 | this.compare = util.promisify(bcrypt.compare); 12 | } 13 | 14 | /** 15 | * Register a new account 16 | * @param email 17 | * @param handle 18 | * @param fullName 19 | * @param password 20 | */ 21 | async signup(email, handle, fullName, password) { 22 | console.info("Signup", email, handle, fullName); 23 | const hash = await this.hash(password, null, null); 24 | console.info("Signup 2", hash); 25 | const account = await userDB.saveAccount({ 26 | id: uuid.v4(), 27 | pwd: hash, 28 | email, 29 | handle, 30 | fullName, 31 | }); 32 | console.info("Signup 3", account); 33 | return account 34 | }; 35 | 36 | /** 37 | * Authenticate a user 38 | * @param email 39 | * @param password 40 | */ 41 | async authenticate(email, password) { 42 | console.log("AdminModel.authenticate", email, password); 43 | const json = await userDB.fetchAccount(email); 44 | console.log("AdminModel.authenticate-1", email, json); 45 | if (!json) { 46 | throw new Error(`No account for ${email}`); 47 | } 48 | const success = await this.compare(password, json.pwd); 49 | return {success, handle: json.handle, userId: json.id}; 50 | } 51 | } 52 | 53 | const instance = new AdminModel(); 54 | module.exports = instance; 55 | -------------------------------------------------------------------------------- /apps/models/graph_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const topicDB = require('../topic_database'); 4 | 5 | class GraphModel { 6 | 7 | //////////////////// 8 | // NodeStruct and EdgeStruct construct JSON objects 9 | // for each topic and relation, to be used by vis.js 10 | //////////////////// 11 | 12 | nodeStruct(topicId, topiclabel) { 13 | var result = {}; 14 | result.id = topicId; 15 | result.label = topiclabel; 16 | result.shape = "oval"; 17 | result.mass = 2; 18 | //console.info('NODE', result); 19 | return result; 20 | } 21 | 22 | edgeStruct(fromId, toId) { 23 | var result = {}; 24 | result.from = fromId; 25 | result.to = toId; 26 | result.arrows = 'to'; 27 | //console.info('EDGE', result); 28 | return result; 29 | } 30 | 31 | /////////////////////// 32 | // We must construct sets, not bags of nodes and edges 33 | /////////////////////// 34 | nodeArrayContains(json, array) { 35 | var len = array.length, 36 | jo; 37 | for (var i = 0; i< len; i++) { 38 | jo = array[i]; 39 | if (jo.id === json.id) { 40 | return true; 41 | } 42 | } 43 | return false; 44 | }; 45 | 46 | edgeArrayContains(json, array) { 47 | var len = array.length, 48 | jo; 49 | for (var i = 0; i< len; i++) { 50 | jo = array[i]; 51 | if ((jo.from === json.from) && 52 | (jo.to === json.to) || 53 | (jo.to === json.from) && 54 | (jo.from === json.to)) { 55 | return true; 56 | } 57 | } 58 | return false; 59 | }; 60 | ///////////////////////// 61 | extractLabel(url) { 62 | var result = url; 63 | var where = result.indexOf('>'); // get first > from 64 | where = result.indexOf('>', where); // get second > 65 | result = result.substring(where+1).trim(); 66 | result = result.substring(0, (result.length - 4)); 67 | //console.info('EXTRACT', url, result); 68 | return result; 69 | } 70 | 71 | /** 72 | * Fetch a graph for a given topic 73 | * Called from TopicModel getTopic after it fetches the topic 74 | * @param topic 75 | * @return 76 | */ 77 | async fetchGraph(topic) { 78 | const topicId = topic.id; 79 | const topicLabel = topic.label; 80 | //Fetch this topics relations 81 | console.info('FetchGraph-1', topicId, topicLabel); 82 | const data = await topicDB.listRelations(topicId); 83 | console.info('FetchGraph-2', data); 84 | var result = {}; 85 | var nodeListSet = []; 86 | var edgeListSet = []; 87 | //If any data, construct a graph from that 88 | if (data && data.length > 0) { 89 | var reln; 90 | var sourceId; 91 | var sourceLabel; 92 | var sourceUrl; 93 | var targetUrl; 94 | var targetId; 95 | var targetLabel; 96 | var relnType; 97 | var relnId; 98 | var node; 99 | var edge; 100 | for (var r in data) { 101 | reln = data[r]; 102 | relnId = reln.id; 103 | relnType = reln.type; 104 | node = this.nodeStruct(relnId, relnType); 105 | if (!this.nodeArrayContains(node, nodeListSet)) { 106 | nodeListSet.push(node); 107 | } 108 | sourceId = reln.sourceId; 109 | targetId = reln.targetId; 110 | sourceUrl = reln.source; 111 | targetUrl = reln.target; 112 | sourceLabel = this.extractLabel(sourceUrl); 113 | targetLabel = this.extractLabel(targetUrl); 114 | node = this.nodeStruct(sourceId, sourceLabel); 115 | if (!this.nodeArrayContains(node, nodeListSet)) { 116 | nodeListSet.push(node); 117 | } 118 | node = this.nodeStruct(targetId, targetLabel); 119 | if (!this.nodeArrayContains(node, nodeListSet)) { 120 | nodeListSet.push(node); 121 | } 122 | edge = this.edgeStruct(sourceId, relnId); 123 | if (!this.edgeArrayContains(edge, edgeListSet)) { 124 | edgeListSet.push(edge); 125 | } 126 | edge = this.edgeStruct(relnId, targetId); 127 | if (!this.edgeArrayContains(edge, edgeListSet)) { 128 | edgeListSet.push(edge); 129 | } 130 | } 131 | } 132 | result.nodes = nodeListSet; 133 | result.edges = edgeListSet; 134 | return result; 135 | }; 136 | 137 | } 138 | 139 | const instance = new GraphModel(); 140 | module.exports = instance; -------------------------------------------------------------------------------- /apps/models/journal_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | {"id":"foo","label":"Foo","date":{"$$date":1582755161331},"backlinks":["Foo causes Bar","Foo isA Blah"],"_id":"cxo7mAtA67NaTa06","bodylist":["Foo is a crucial topic in the domain of nonsense."]} 4 | 5 | Topic Structure 6 | { 7 | id: the node's id - can be a slug or a uuid 8 | date: date value 9 | userHandle: 10 | userId: 11 | label: the topic's label 12 | backlinks: list of backlink hrefs ** Changing to list of Journal IDs 13 | bodylist: list of text objects, each of which becomes an AIR journal entry 14 | - has been processed for wikilinks and other items 15 | - in a topic, bodylist is an array 16 | urllist: list of URLs associated with this topic 17 | sourceId: relations only 18 | targetId: relations only 19 | source: href to source (subject) relations only 20 | target: href to target (object) relations only 21 | } 22 | example topic 23 | { 24 | "id": "TOP_backlinks", 25 | "userId": "7563c26a-319b-4652-b9ea-0b8cfee34b0b", 26 | "userHandle": "Joe", 27 | "nodeType": "topic", 28 | "label": "Backlinks", 29 | "date": { 30 | "$$date": 1584130903110 31 | }, 32 | "urllist": [], 33 | "backlinks": ["

Testing [[Backlinks]]

"], 34 | "_id": "BY8d6yU8fAkajCzH" 35 | } 36 | 37 | Journal Entry Structure 38 | { 39 | id: 40 | date: 41 | userHandle: 42 | userId: 43 | raw: raw text without hrefs 44 | text: in an AIR entry, this is processed for wikilinks 45 | subj: triple journal entry only 46 | pred: triple journal entry only 47 | obj: triple journal entry only 48 | urllist: urls if any 49 | //notes: triple journal entry only 50 | bodylist: a single text object in a journal entry 51 | } 52 | */ 53 | const journalDB = require('../journal_database'); 54 | const TopicModel = require('./topic_model'); 55 | 56 | const topicDB = require('../topic_database'); 57 | const uuid = require('uuid'); 58 | const toSlug = require('../slug'); 59 | const linker = require('./linker'); 60 | /** 61 | * JournalModel provides a kind of DSL for the platform 62 | */ 63 | class JournalModel { 64 | /** 65 | * For a given {@code topic}, populate its backlinks 66 | * @param topic 67 | */ 68 | async populateBacklinks(topic) { 69 | console.info('Populating', topic); 70 | const backlinks = topic.backlinks; 71 | let theLink; 72 | //var jnlId; 73 | 74 | console.info("StartBacks", backlinks.length); 75 | const promises = backlinks.map((jnlId)=> 76 | new Promise(async (resolve, reject) => { 77 | try { 78 | const data = await journalDB.find({ id: jnlId}); 79 | const dx = data[0]; 80 | console.log('Populating-2', data, dx.raw); 81 | 82 | //data is the entire journal entry 83 | //We want raw; for now, href the whole thing 84 | //TODO add an image for the href 85 | resolve(`${dx.raw}`); 86 | } catch (err) { 87 | console.error("error", err); 88 | reject(err); 89 | } 90 | })); 91 | console.log('PX', promises); 92 | const newLinks = await Promise.allSettled(promises); 93 | const links = newLinks.filter(x=>x.status == 'fulfilled').map(x=>x.value); 94 | console.log("promise.all", links); 95 | topic.backlinks = links; 96 | return topic; 97 | }; 98 | 99 | /** 100 | * Form a journal entry and construct the backlinks for 101 | * the subject, predicate, and object 102 | * @param subject 103 | * @param predicate 104 | * @param object 105 | * @param url 106 | * @param notes 107 | * @param userId 108 | * @param userHandle 109 | */ 110 | async processTriple(subject, predicate, object, url, notes, 111 | userId, userHandle) { 112 | const uid = `JNL_${uuid.v4()}`; 113 | const json = {}; 114 | const subjectSlug = `TOP_${toSlug(subject)}`; 115 | const objectSlug = `TOP_${toSlug(object)}`; 116 | const predicateSlug = `TOP_${subjectSlug}${toSlug(predicate)}${objectSlug}`; 117 | const triple = `${subject} ${predicate} ${object}`; 118 | json.raw = `${subject} ${predicate} ${object}`; 119 | json.text = linker.setHrefs(subject, subjectSlug, object, objectSlug, predicate, predicateSlug); 120 | json.subj = subject; 121 | json.pred = predicate; 122 | json.obj = object; 123 | json.date = new Date(); 124 | json.userId = userId; 125 | json.userHandle = userHandle; 126 | if (url) { 127 | const ul = []; 128 | ul.push(url); 129 | json.urllist = ul; 130 | } 131 | //json.bodylist = []; 132 | if (notes) { 133 | const {body, topiclist} = linker.resolveWikiLinks(notes); 134 | console.info('ProcessTriple-1', body, topiclist); 135 | json.bodylist = body; //push(body); 136 | json.id = uid; 137 | //process the topics 138 | TopicModel.processTopic(subject, subjectSlug, url, null, uid, userId, userHandle); 139 | TopicModel.processTopic(object, objectSlug, url, null, uid, userId, userHandle); 140 | const predlabel = `${subject} ${predicate} ${object}`; 141 | await TopicModel.processPredicate(predlabel, predicateSlug, predicate, 142 | subject, subjectSlug, 143 | object, objectSlug, 144 | url, triple, uid, userId, userHandle); 145 | // persist the journal entry 146 | const dat = await journalDB.put(json); 147 | console.info("ProcessTriple", dat); 148 | const len = topiclist.length; 149 | console.info("PT-1", dat, len, topiclist); 150 | if (len > 0) { 151 | await this.processTopics(topiclist, url, null, uid, userId, userHandle); 152 | } 153 | return dat; 154 | } else { 155 | json.id = uid; 156 | //process the topics 157 | await TopicModel.processTopic(subject, subjectSlug, url, null, uid, userId, userHandle); 158 | await TopicModel.processTopic(object, objectSlug, url, null, uid, userId, userHandle); 159 | const predlabel = `${subject} ${predicate} ${object}`; 160 | await TopicModel.processPredicate(predlabel, predicateSlug, predicate, 161 | subject, subjectSlug, 162 | object, objectSlug, 163 | url, triple, uid, userId, userHandle); 164 | // persist the journal entry 165 | const dat = await journalDB.put(json); 166 | console.info("ProcessTriple", dat); 167 | return dat; 168 | } 169 | } 170 | 171 | async listByURL(url) { 172 | return await journalDB.findByURL(url); 173 | }; 174 | 175 | /** 176 | * List journal entries 177 | * @param limit 178 | * @param skip 179 | */ 180 | async list(limit, skip) { 181 | return await journalDB.list(limit, skip); 182 | }; 183 | 184 | /** 185 | * List topics 186 | * @param limit 187 | * @param skip 188 | */ 189 | async listTopics(limit, skip) { 190 | return await TopicModel.listTopics(limit, skip); 191 | }; 192 | 193 | /** 194 | * Return a topic identified by id 195 | * @param id 196 | */ 197 | async getTopic(id) { 198 | const data = await TopicModel.getTopic(id); 199 | console.info('JnlModel.getTopic', data); 200 | return await this.populateBacklinks(data); 201 | }; 202 | 203 | async ajaxFindLabel(q) { 204 | console.log('JournalAjax', q); 205 | return await TopicModel.ajaxFindLabel(q); 206 | }; 207 | /** 208 | * Return a specific journal entry identified by id 209 | * @param id 210 | */ 211 | async getJournalEntry(id) { 212 | console.info("NM-GJ", id); 213 | return await journalDB.get(id); 214 | }; 215 | 216 | /** 217 | * Add another AIR (addressable information resource) or URL 218 | * to a topic identified by id 219 | * @param id 220 | * @param body the AIR 221 | * @param url optional 222 | */ 223 | async updateTopic(id, url, body) { 224 | return await topicDB.updateTopic(id, url, body); 225 | }; 226 | 227 | async updateJournalEntry(id, content, userId, userHandle, isTriple) { 228 | console.info("JournalModel.updateJournalEntry", isTriple); 229 | const {body, topiclist} = linker.resolveWikiLinks(content); 230 | const jnl = await journalDB.get(id); 231 | var notes = jnl.notes; 232 | if (notes) { 233 | 234 | } 235 | await journalDB.updateJournalText(id, body, isTriple); 236 | if (topiclist.length > 0) { 237 | console.info("updateJournal-1"); 238 | await this.processTopics(topiclist, null, null, id, userId, userHandle); 239 | return; 240 | } else { 241 | return; 242 | } 243 | }; 244 | 245 | async processTopics(topiclist, url, text, id, userId, userHandle) { 246 | console.info('ProcessTopics', topiclist, id, text); 247 | let json; 248 | let i; 249 | const promises = topiclist.map( 250 | (json)=> TopicModel.processTopic( 251 | json.label, json.slug, url, text, 252 | id, userId, userHandle) 253 | ) 254 | await Promise.allSettled(promises); 255 | }; 256 | 257 | /** 258 | * Create a new AIR - text topic 259 | * @param content which may have wikilinks 260 | * @param url optional 261 | * @param userId 262 | * @param userHandle 263 | */ 264 | async newAIR(content, url, userId, userHandle) { 265 | const json = {}; 266 | json.raw = content; 267 | console.info('NewAirJnl-1', content, url); 268 | const {body, topiclist} = linker.resolveWikiLinks(content); 269 | console.info('NewAirJnl-2', body, topiclist); 270 | const uid = `JNL_${uuid.v4()}`; 271 | json.id = uid; 272 | json.userId = userId; 273 | json.userHandle = userHandle; 274 | json.text = body; 275 | json.date = new Date(); 276 | if (url) { 277 | const ul = []; 278 | ul.push(url); 279 | json.urllist = ul; 280 | } 281 | // we now have an AIR ready to persist 282 | // and possible a list of topics to process 283 | const dat = await journalDB.put(json); 284 | const len = topiclist.length; 285 | console.info("newAIR", dat, len, topiclist); 286 | if (len > 0) { 287 | console.info("newAir-1"); 288 | await this.processTopics(topiclist, url, null, uid, userId, userHandle); 289 | return dat; 290 | } else { 291 | console.info('newAir-2'); 292 | return dat; 293 | } 294 | } 295 | } 296 | 297 | const instance = new JournalModel(); 298 | module.exports = instance; 299 | -------------------------------------------------------------------------------- /apps/models/linker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const toSlug = require('../slug'); 3 | 4 | class Linker { 5 | 6 | /** 7 | * A term might be surrounded with html, e.g. , 8 | * @param term 9 | * @returns cleaned up term 10 | */ 11 | cleanTerm(term) { 12 | if (term.startsWith('<')) { 13 | let result = term; 14 | let where = result.indexOf('>'); 15 | if (where > -1) { 16 | result = result.substring((where+1)); 17 | where = result.indexOf('<'); 18 | if (where > -1) { 19 | result = result.substring(0, where); 20 | } 21 | return result; 22 | } 23 | } else { 24 | return term; 25 | } 26 | } 27 | 28 | /** 29 | * Given a term --> topic, 30 | * create an href for it. 31 | * @param term 32 | * @return href 33 | */ 34 | getHref(term, slug) { 35 | 36 | const result = `${term}`; 37 | return result; 38 | }; 39 | ////////////////////////////////// 40 | // This is complex: 41 | // We are walking through a block of text to be added to a 42 | // topic node. If we see a Wikilink, we must do the following: 43 | // a) convert that term to an href 44 | // b) fire up the backlink to this block of text 45 | // which technically means that the block of text must have 46 | // its own ID, 47 | // OR,it means that this entire topic represents the backlink 48 | // 49 | ////////////////////////////////// 50 | /** 51 | * Given some text, look for Wikilinks. 52 | * Where found, convert those to hrefs and reconstruct the 53 | * text including the hrefs. 54 | * Returns the revised text (with hrefs, if any) and a list 55 | * of topics and their slugs found, if any 56 | * @param text 57 | */ 58 | resolveWikiLinks(text) { 59 | console.info('LINKER', text); 60 | const topiclist = []; // topic is a json object with label and slug 61 | let result = ""; 62 | let begin = text.indexOf("[["); 63 | if (begin > -1) { 64 | result = `${text.substring(0, begin)} `; 65 | } 66 | let end = 0; 67 | let term; 68 | let slug; 69 | let jsonT; 70 | //loop if there is any [[ found 71 | while (begin > -1) { 72 | begin += 2; 73 | //find the end 74 | end = text.indexOf("]]", begin); 75 | if (end > -1) { // end found 76 | term = text.substring(begin, end).trim(); 77 | console.info('LINKER-1', begin, end, term, text); 78 | term = this.cleanTerm(term); 79 | //ALL wikilinks are to Topics 80 | slug = `TOP_${toSlug(term)}`; 81 | // add href to result 82 | result += `${this.getHref(term, slug)} `; 83 | jsonT = {}; 84 | jsonT.label = term; 85 | jsonT.slug = slug; 86 | topiclist.push(jsonT); 87 | end += 2; 88 | begin = text.indexOf("[[", end); 89 | console.info('LINKER-2', begin, end, text.length, term, text); 90 | if (begin === -1 && (end) < text.length) { 91 | //add remainder, if any 92 | result += text.substring(end); 93 | } else if (begin > -1 && end < text.length) { 94 | //add gap from last end+2 95 | result += `${text.substring((end), begin)} `; 96 | } 97 | } else { // proper end not found - error condition 98 | throw new Error("Open Wikilink with improper or no Closing Wikilink-missing ]]"); 99 | } 100 | } 101 | if (result === "") { // if nothing found, just return the text 102 | result = text; 103 | } 104 | return {body: result.trim(), topiclist}; 105 | }; 106 | 107 | /** 108 | * Craft a triple for viewing as 3 hrefs 109 | * @param subject 110 | * @param sSlug 111 | * @param object 112 | * @param oSlug 113 | * @param predicate 114 | * @param pSlug 115 | * @return 116 | */ 117 | setHrefs(subject, sSlug, object, oSlug, predicate, pSlug) { 118 | let result = ""; 119 | const sHref = `${subject}`; 120 | const oHref = `${object}`; 121 | const pHref = `${predicate}`; 122 | result += `${sHref} `; 123 | result += `${pHref} `; 124 | result += ` ${oHref}`; 125 | return result; 126 | }; 127 | 128 | }; 129 | 130 | const instance = new Linker(); 131 | module.exports = instance; 132 | -------------------------------------------------------------------------------- /apps/models/topic_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const topicDB = require('../topic_database'); 3 | const graphModel = require('./graph_model'); 4 | 5 | class TopicModel { 6 | 7 | /** 8 | * Add another AIR (addressable information resource) or URL 9 | * to a topic identified by id 10 | * @param id 11 | * @param body the AIR 12 | * @param url optional 13 | */ 14 | async updateTopic(id, url, body) { 15 | console.info('UpdateTopic', id, url, body); 16 | //fetch the topic 17 | const data = await topicDB.get(id); 18 | console.info('UpdateTopic-1', data); 19 | //update the topic using treating arrays as sets (no duplicates) 20 | let somelist; 21 | let madeChanges = false; 22 | if (url) { 23 | somelist = data.urllist; 24 | if (!somelist) { 25 | somelist = []; 26 | somelist.push(url); 27 | madeChanges = true; 28 | } else if (!somelist.includes(url)) { 29 | madeChanges = true; 30 | somelist.push(url); 31 | } 32 | data.urllist = somelist; 33 | } 34 | if (body) { 35 | somelist = data.bodylist; 36 | if (!somelist) { 37 | somelist = []; 38 | somelist.push(body); 39 | madeChanges = true; 40 | } 41 | else if (!somelist.includes(body)) { 42 | somelist.push(body); 43 | madeChanges = true; 44 | } 45 | data.bodylist = somelist; 46 | } 47 | if (madeChanges) { 48 | console.info('UpdateTopic-3', data); 49 | //delete the old one 50 | const numRemoved = await topicDB.delete(data._id); 51 | console.info('RemTopic', id, numRemoved); 52 | // insert the new one 53 | const dat = await topicDB.put(data); 54 | console.info('UpdateTopic-4', dat); 55 | //await topicDB.compact(); 56 | //NO: compacting messes up backlinks - not sure why 57 | return dat; 58 | } 59 | } 60 | 61 | /** 62 | * Process a term which is a topic 63 | * either make a new node from that term if not exists 64 | * else add backlink to it with the content and its id 65 | * @param term 66 | * @param slug 67 | * @param url 68 | * @param content of the journal entry 69 | * @param id of the journal entry 70 | * @param userId 71 | * @param userHandle 72 | */ 73 | async processTopic(term, slug, url, content, id, userId, userHandle) { 74 | console.info("ProcessTopic", term, slug, '|', content, '|', url, id); 75 | const data = await topicDB.get(slug); 76 | console.info('ProcessTopic-1', data); 77 | if (data) { 78 | console.info('ProcessTopic-1a', content); 79 | if (content) { 80 | await this.updateTopic(slug, url, content); 81 | await topicDB.addBacklink(slug, id); 82 | } else { 83 | await topicDB.addBacklink(slug, id); 84 | } 85 | } else { 86 | const json = {}; 87 | if (slug.startsWith('TOP')) { 88 | json.id = slug; 89 | } else { 90 | json.id = `TOP_${slug}`; 91 | } 92 | json.userId = userId; 93 | json.userHandle = userHandle; 94 | json.nodeType = 'topic'; 95 | json.label = term; 96 | json.date = new Date(); 97 | json.urllist = []; 98 | if (url) { 99 | json.urllist.push(url); 100 | } 101 | json.backlinks = []; 102 | json.backlinks.push(id); 103 | const dat = await topicDB.put(json); 104 | console.info('ProceessTopic-2', dat); 105 | } 106 | }; 107 | 108 | /** 109 | * Process a term which is a predicate - also a topic 110 | * @param predicate 111 | * @param predicateSlug 112 | * @param type the predicate type - not its slug 113 | * @param subject 114 | * @param subjectSlug 115 | * @param object 116 | * @param objectSlug 117 | * @param url 118 | * @param content the journal entry itself 119 | * @param id the journal entry id 120 | * @param userId 121 | * @param userHandle 122 | */ 123 | async processPredicate(predicate, predicateSlug, type, 124 | subject, subjectSlug, 125 | object, objectSlug, 126 | url, 127 | content, id, 128 | userId, userHandle) { 129 | console.info("ProcessPredicate", predicate, predicateSlug, url, id); 130 | const data = await topicDB.get(predicateSlug); 131 | console.info('ProcessPredicate-1', data); 132 | if (data) { 133 | await this.updateTopic(predicateSlug, url, content); 134 | await topicDB.addBacklink(predicateSlug, id); 135 | } else { 136 | const json = {}; 137 | if (predicateSlug.startsWith('TOP')) { 138 | json.id = predicateSlug; 139 | } else { 140 | json.id = `TOP_${predicateSlug}`; 141 | } 142 | json.userId = userId; 143 | json.userHandle = userHandle; 144 | json.nodeType = 'relation'; 145 | json.type = type; 146 | json.label = predicate; 147 | json.date = new Date(); 148 | json.source = `${subject}` 149 | json.target = `${object}` 150 | json.sourceId = subjectSlug; 151 | json.targetId = objectSlug; 152 | json.backlinks = []; 153 | json.urllist = []; 154 | if (url) { 155 | json.urllist.push(url); 156 | } 157 | 158 | json.backlinks.push(id); 159 | const dat = await topicDB.put(json); 160 | console.info('ProcessPredicate-2', dat); 161 | } 162 | }; 163 | 164 | async ajaxFindLabel(q) { 165 | 166 | const rx = new RegExp(q, 'i'); 167 | console.info("TMajax", rx); 168 | const docs = await topicDB.find({ label: { $regex: rx } }); 169 | console.info('AjaxFind', docs); 170 | return { 171 | options: docs.map((doc)=>doc.label) 172 | } 173 | }; 174 | 175 | /** 176 | * List topics 177 | * @param limit 178 | * @param skip 179 | */ 180 | async listTopics(limit, skip) { 181 | const docs = await topicDB.list(limit, skip); 182 | console.info("TopicModel.listTopics", docs); 183 | return docs; 184 | }; 185 | 186 | async getTopic(topicId) { 187 | const data = await topicDB.get(topicId); 188 | const graph = await graphModel.fetchGraph(data); 189 | data.graph = JSON.stringify(graph); 190 | return data; 191 | } 192 | 193 | 194 | } 195 | 196 | const instance = new TopicModel(); 197 | module.exports = instance; 198 | -------------------------------------------------------------------------------- /apps/slug.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /**A utility file to convert a term to a slug */ 3 | const slugify = require('slugify'); 4 | 5 | function toSlug(term) { 6 | // by making it lower case, we trap the same term 7 | // no matter whether caps are involved. 8 | var re = new RegExp("'", "g"); // remove apostrophies from text 9 | const tx = term.trim() 10 | .toLowerCase() 11 | .replace(re, "_"); 12 | return slugify(tx, '_'); 13 | }; 14 | 15 | module.exports = toSlug; 16 | -------------------------------------------------------------------------------- /apps/topic_database.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Datastore = require('nedb-promises'); 3 | 4 | class Database { 5 | constructor() { 6 | this.db = new Datastore({ filename: './data/topics' , autoload: true }); 7 | console.info(`Database ${this.db}`); 8 | } 9 | 10 | /** 11 | * Insert a topic 12 | * @param jsonDoc the topic 13 | */ 14 | async put(jsonDoc) { 15 | return await this.db.insert(jsonDoc); 16 | }; 17 | 18 | /** 19 | * @param id 20 | * @parm backlink 21 | */ 22 | async addBacklink(id, backlink) { 23 | return await this.db.update({ id }, { $push: { backlinks: backlink } }); 24 | }; 25 | 26 | /** 27 | * For compacting as needed 28 | */ 29 | async compact() { 30 | return await this.db.persistence.compactDatafile(); 31 | }; 32 | 33 | /** 34 | * Remove a topic identified by _id 35 | * @param _id 36 | */ 37 | async delete(_id) { 38 | return await this.db.remove({ _id }); 39 | }; 40 | 41 | /** 42 | * @param id 43 | */ 44 | async get(id) { 45 | return await this.db.findOne({ id }); 46 | }; 47 | 48 | /** 49 | * General find support 50 | * @param query json 51 | */ 52 | async find(query) { 53 | console.info('TDB', query); 54 | return await this.db.find(query); 55 | }; 56 | 57 | /** 58 | * Find topics by URL 59 | * @param url 60 | */ 61 | async findByURL(url) { 62 | return await this.find({ url }); 63 | }; 64 | 65 | /** 66 | * Replace a topic 67 | * @param newTopic 68 | */ 69 | async replaceBacklinks(_id, backlinks) { 70 | numRep = await this.db.update({ _id }, {$set:{ backlinks }}); 71 | console.info('ReplaceBacklinks', _id, backlinks, numRep); 72 | await this.db.persistence.compactDatafile(); 73 | return numRep; 74 | }; 75 | 76 | /** 77 | * Add in either {@code url}, or {@code body} or both 78 | * @param url can be {@code null} 79 | * @param body can be {@code null} 80 | */ 81 | async updateTopic(id, url, body) { 82 | //TODO rewrite this to avoid duplicates 83 | if (url) { 84 | await this.db.update({ id }, { $push: { urllist: url } }, {}); 85 | await this.db.update({ id }, { $push: { bodylist: body } }, {}); 86 | } else if (body) { 87 | await this.db.update({ id }, { $push: { bodylist: body } }, {}); 88 | } else { 89 | throw new Error('TopicDatabas.updateTopic got nothing ', id); 90 | } 91 | }; 92 | 93 | /** 94 | * Return a list of topics sorted on label 95 | * @param limit 96 | * @param skip 97 | */ 98 | async list(limit, skip) { 99 | console.info('TopList', limit, skip); 100 | 101 | const result = await Promise.all([this.db.find({}).sort({ label: 1 }).limit(limit).skip(skip), this.db.count({})]); 102 | 103 | return result; 104 | }; 105 | 106 | /** 107 | * Return a list of relations for a given topic 108 | * @param topicId 109 | */ 110 | async listRelations(topicId) { 111 | return await this.db.find({ nodeType: 'relation', $or: [{ sourceId: topicId }, { targetId: topicId }]}); 112 | }; 113 | 114 | }; 115 | 116 | const instance = new Database(); 117 | module.exports = instance; 118 | -------------------------------------------------------------------------------- /apps/user_database.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Datastore = require('nedb-promises'); 3 | 4 | class UserDatabase { 5 | constructor() { 6 | this.db = new Datastore({ filename: './data/users' , autoload: true }); 7 | console.info(`Database ${this.db}`); 8 | } 9 | 10 | /** 11 | * Persist an account 12 | * @param struct 13 | */ 14 | async saveAccount(struct) { 15 | return await this.db.insert(struct); 16 | }; 17 | 18 | /** 19 | * Report {@code true} if database is empty 20 | */ 21 | async isEmpty() { 22 | const doc = await this.db.find({}); 23 | console.info("UserEmpty", doc); 24 | return (doc && doc.length > 0); 25 | }; 26 | 27 | /** 28 | * Return an account or {@code null} 29 | * @param email 30 | */ 31 | async fetchAccount(email) { 32 | return await this.db.findOne({ email }); 33 | } 34 | } 35 | 36 | const instance = new UserDatabase(); 37 | module.exports = instance; 38 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict" 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | const app = require('../app'); 9 | const bootstrap = require('../apps/bootstrap'); 10 | const debug = require('debug')('lite-net:server'); 11 | const http = require('http'); 12 | 13 | /** 14 | * Get port from environment and store in Express. 15 | */ 16 | 17 | const port = normalizePort(process.env.PORT || '4000'); 18 | app.set('port', port); 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | let server; 24 | bootstrap().then(()=>{ 25 | server = http.createServer(app); 26 | 27 | /** 28 | * Listen on provided port, on all network interfaces. 29 | */ 30 | 31 | server.listen(port); 32 | server.on('error', onError); 33 | server.on('listening', onListening); 34 | }) 35 | 36 | /** 37 | * Normalize a port into a number, string, or false. 38 | */ 39 | 40 | function normalizePort(val) { 41 | const port = parseInt(val, 10); 42 | 43 | if (isNaN(port)) { 44 | // named pipe 45 | return val; 46 | } 47 | 48 | if (port >= 0) { 49 | // port number 50 | return port; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Event listener for HTTP server "error" event. 58 | */ 59 | 60 | function onError(error) { 61 | if (error.syscall !== 'listen') { 62 | throw error; 63 | } 64 | 65 | const bind = typeof port === 'string' 66 | ? `Pipe ${port}` 67 | : `Port ${port}`; 68 | 69 | // handle specific listen errors with friendly messages 70 | switch (error.code) { 71 | case 'EACCES': 72 | console.error(`${bind} requires elevated privileges`); 73 | process.exit(1); 74 | break; 75 | case 'EADDRINUSE': 76 | console.error(`${bind} is already in use`); 77 | process.exit(1); 78 | break; 79 | default: 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Event listener for HTTP server "listening" event. 86 | */ 87 | 88 | function onListening() { 89 | const addr = server.address(); 90 | const bind = typeof addr === 'string' 91 | ? `pipe ${addr}` 92 | : `port ${addr.port}`; 93 | debug(`Listening on ${bind}`); 94 | } 95 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "banner": "LiteNet-3", 3 | "vocabulary": "biomed", 4 | "canSignup": false, 5 | "isPrivatePortal": false 6 | } -------------------------------------------------------------------------------- /config/owner.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "joe@sixpack.com", 3 | "fullName": "Joe Sixpack", 4 | "handle": "Joe", 5 | "password": "joe" 6 | } -------------------------------------------------------------------------------- /config/vocab/biomed/defns.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/vocab/biomed/labels.json: -------------------------------------------------------------------------------- 1 | { "terms": [ 2 | "absorb", 3 | "acetylate", 4 | "act as", 5 | "activate", 6 | "affect", 7 | "affect positive", 8 | "affect negative", 9 | "alkalize", 10 | "alter", 11 | "anthropomorphize", 12 | "associated with", 13 | "bind to", 14 | "block", 15 | "can elevate", 16 | "catalyze", 17 | "not catalyze", 18 | "cause", 19 | "cleave", 20 | "conjugate", 21 | "conjugated by", 22 | "contain", 23 | "control", 24 | "deactivate", 25 | "decrease", 26 | "defined as", 27 | "destroy", 28 | "disrupt", 29 | "differentiate", 30 | "down regulate", 31 | "essential for", 32 | "express", 33 | "expressed in", 34 | "has acronym", 35 | "has part", 36 | "has synonym", 37 | "hydrolise", 38 | "improve", 39 | "inactivate", 40 | "incorporate", 41 | "increase", 42 | "induce", 43 | "inhibit", 44 | "is a", 45 | "linked to", 46 | "located at", 47 | "located in", 48 | "mediate", 49 | "move", 50 | "neutralize", 51 | "not absorb", 52 | "not affect", 53 | "not associated with", 54 | "not bind to", 55 | "not block", 56 | "not conjugated by", 57 | "not contain", 58 | "not decrease", 59 | "not hydrolise", 60 | "not increase", 61 | "not induce", 62 | "not inhibit", 63 | "not linked to", 64 | "not oxygenate", 65 | "not polymerize", 66 | "not prevent", 67 | "not promote", 68 | "not reduce", 69 | "not regulate", 70 | "not require", 71 | "not same as", 72 | "not suppress", 73 | "oxydize", 74 | "oxygenate", 75 | "phosphorylate", 76 | "polymerize", 77 | "prevent", 78 | "produce", 79 | "promote", 80 | "reduce", 81 | "regulate", 82 | "require", 83 | "same as", 84 | "suppress", 85 | "synthesize", 86 | "transport", 87 | "trigger", 88 | "up regulate", 89 | "use", 90 | "weaken", 91 | "what", 92 | "who", 93 | "where", 94 | "when", 95 | "inform", 96 | "determine", 97 | "address", 98 | "lead", 99 | "lag", 100 | "suggest", 101 | "contradict", 102 | "agree with", 103 | "question", 104 | "influence", 105 | "threaten", 106 | "predict", 107 | "mediate", 108 | "complement", 109 | "own", 110 | "use", 111 | "risk", 112 | "confound", 113 | "measure", 114 | "relevant", 115 | "equivalent", 116 | "consistent with", 117 | "inconsistent with", 118 | "support", 119 | "object", 120 | "challenge" 121 | 122 | ] 123 | } 124 | -------------------------------------------------------------------------------- /config/vocab/climate/defns.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/config/vocab/climate/defns.json -------------------------------------------------------------------------------- /config/vocab/climate/labels.json: -------------------------------------------------------------------------------- 1 | { 2 | "terms": [ 3 | "causes", 4 | "is a", 5 | "emits", 6 | "releases", 7 | "absorbs", 8 | "who", 9 | "where", 10 | "when", 11 | "inform", 12 | "determine", 13 | "address", 14 | "lead", 15 | "lag", 16 | "suggest", 17 | "contradict", 18 | "agree with", 19 | "question", 20 | "influence", 21 | "threaten", 22 | "predict", 23 | "mediate", 24 | "complement", 25 | "own", 26 | "use", 27 | "risk", 28 | "confound", 29 | "measure", 30 | "relevant", 31 | "equivalent", 32 | "consistent with", 33 | "inconsistent with", 34 | "support", 35 | "object", 36 | "challenge" 37 | ] 38 | } -------------------------------------------------------------------------------- /config/vocab/vocab.md: -------------------------------------------------------------------------------- 1 | # Predicate Vocabulary Plugins 2 | This directory defines a space in which new predicate vocabularies can be defined. The _climate_ vocabulary is a shell for now. 3 | 4 | The _biomed_ vocabulary is currently being developed. 5 | 6 | The particular vocabulary to be used is defined in the /config/config.json file. 7 | 8 | Each vocabular directory contains two files:
9 | * defns.json in which _topics_ are defined as JSON objects to be imported into the topic database. 10 | * labels.json in which the labels for each predicate are made available to the _triple forms_ for selecting predicates. 11 | -------------------------------------------------------------------------------- /data/.keep: -------------------------------------------------------------------------------- 1 | keep round if empty -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lite-net", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "bcrypt-nodejs": "0.0.3", 10 | "connect-flash": "^0.1.1", 11 | "cookie-parser": "~1.4.4", 12 | "debug": "~2.6.9", 13 | "express": "~4.16.1", 14 | "express-session": "^1.17.0", 15 | "hbs": "^4.1.0", 16 | "http-errors": "~1.6.3", 17 | "jquery-typeahead": "^2.11.0", 18 | "morgan": "~1.9.1", 19 | "nedb-promises": "^4.0.1", 20 | "slugify": "^1.3.6", 21 | "uuid": "^7.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /public/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /public/css/default.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | border-color: #242424 transparent transparent transparent; 3 | top: 50px; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | border-color: transparent transparent #242424 transparent; 7 | top: -8px; } 8 | 9 | .medium-editor-toolbar { 10 | background-color: #242424; 11 | background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.75)); 12 | background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.75)); 13 | border: 1px solid #000; 14 | border-radius: 5px; 15 | box-shadow: 0 0 3px #000; } 16 | .medium-editor-toolbar li button { 17 | background-color: #242424; 18 | background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.89)); 19 | background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.89)); 20 | border: 0; 21 | border-right: 1px solid #000; 22 | border-left: 1px solid #333; 23 | border-left: 1px solid rgba(255, 255, 255, 0.1); 24 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3); 25 | color: #fff; 26 | height: 50px; 27 | min-width: 50px; 28 | -webkit-transition: background-color .2s ease-in; 29 | transition: background-color .2s ease-in; } 30 | .medium-editor-toolbar li button:hover { 31 | background-color: #000; 32 | color: yellow; } 33 | .medium-editor-toolbar li .medium-editor-button-first { 34 | border-bottom-left-radius: 5px; 35 | border-top-left-radius: 5px; } 36 | .medium-editor-toolbar li .medium-editor-button-last { 37 | border-bottom-right-radius: 5px; 38 | border-top-right-radius: 5px; } 39 | .medium-editor-toolbar li .medium-editor-button-active { 40 | background-color: #000; 41 | background: -webkit-linear-gradient(top, #242424, rgba(0, 0, 0, 0.89)); 42 | background: linear-gradient(to bottom, #242424, rgba(0, 0, 0, 0.89)); 43 | color: #fff; } 44 | 45 | .medium-editor-toolbar-form { 46 | background: #242424; 47 | border-radius: 5px; 48 | color: #999; } 49 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 50 | background: #242424; 51 | box-sizing: border-box; 52 | color: #ccc; 53 | height: 50px; } 54 | .medium-editor-toolbar-form a { 55 | color: #fff; } 56 | 57 | .medium-editor-toolbar-anchor-preview { 58 | background: #242424; 59 | border-radius: 5px; 60 | color: #fff; } 61 | 62 | .medium-editor-placeholder:after { 63 | color: #b3b3b1; } 64 | -------------------------------------------------------------------------------- /public/css/medium-editor.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes medium-editor-image-loading { 2 | 0% { 3 | -webkit-transform: scale(0); 4 | transform: scale(0); } 5 | 100% { 6 | -webkit-transform: scale(1); 7 | transform: scale(1); } } 8 | 9 | @keyframes medium-editor-image-loading { 10 | 0% { 11 | -webkit-transform: scale(0); 12 | transform: scale(0); } 13 | 100% { 14 | -webkit-transform: scale(1); 15 | transform: scale(1); } } 16 | 17 | @-webkit-keyframes medium-editor-pop-upwards { 18 | 0% { 19 | opacity: 0; 20 | -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12); 21 | transform: matrix(0.97, 0, 0, 1, 0, 12); } 22 | 20% { 23 | opacity: .7; 24 | -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2); 25 | transform: matrix(0.99, 0, 0, 1, 0, 2); } 26 | 40% { 27 | opacity: 1; 28 | -webkit-transform: matrix(1, 0, 0, 1, 0, -1); 29 | transform: matrix(1, 0, 0, 1, 0, -1); } 30 | 100% { 31 | -webkit-transform: matrix(1, 0, 0, 1, 0, 0); 32 | transform: matrix(1, 0, 0, 1, 0, 0); } } 33 | 34 | @keyframes medium-editor-pop-upwards { 35 | 0% { 36 | opacity: 0; 37 | -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12); 38 | transform: matrix(0.97, 0, 0, 1, 0, 12); } 39 | 20% { 40 | opacity: .7; 41 | -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2); 42 | transform: matrix(0.99, 0, 0, 1, 0, 2); } 43 | 40% { 44 | opacity: 1; 45 | -webkit-transform: matrix(1, 0, 0, 1, 0, -1); 46 | transform: matrix(1, 0, 0, 1, 0, -1); } 47 | 100% { 48 | -webkit-transform: matrix(1, 0, 0, 1, 0, 0); 49 | transform: matrix(1, 0, 0, 1, 0, 0); } } 50 | 51 | .medium-editor-anchor-preview { 52 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 53 | font-size: 16px; 54 | left: 0; 55 | line-height: 1.4; 56 | max-width: 280px; 57 | position: absolute; 58 | text-align: center; 59 | top: 0; 60 | word-break: break-all; 61 | word-wrap: break-word; 62 | visibility: hidden; 63 | z-index: 2000; } 64 | .medium-editor-anchor-preview a { 65 | color: #fff; 66 | display: inline-block; 67 | margin: 5px 5px 10px; } 68 | 69 | .medium-editor-anchor-preview-active { 70 | visibility: visible; } 71 | 72 | .medium-editor-dragover { 73 | background: #ddd; } 74 | 75 | .medium-editor-image-loading { 76 | -webkit-animation: medium-editor-image-loading 1s infinite ease-in-out; 77 | animation: medium-editor-image-loading 1s infinite ease-in-out; 78 | background-color: #333; 79 | border-radius: 100%; 80 | display: inline-block; 81 | height: 40px; 82 | width: 40px; } 83 | 84 | .medium-editor-placeholder { 85 | position: relative; } 86 | .medium-editor-placeholder:after { 87 | content: attr(data-placeholder) !important; 88 | font-style: italic; 89 | position: absolute; 90 | left: 0; 91 | top: 0; 92 | white-space: pre; 93 | padding: inherit; 94 | margin: inherit; } 95 | 96 | .medium-editor-placeholder-relative { 97 | position: relative; } 98 | .medium-editor-placeholder-relative:after { 99 | content: attr(data-placeholder) !important; 100 | font-style: italic; 101 | position: relative; 102 | white-space: pre; 103 | padding: inherit; 104 | margin: inherit; } 105 | 106 | .medium-toolbar-arrow-under:after, .medium-toolbar-arrow-over:before { 107 | border-style: solid; 108 | content: ''; 109 | display: block; 110 | height: 0; 111 | left: 50%; 112 | margin-left: -8px; 113 | position: absolute; 114 | width: 0; } 115 | 116 | .medium-toolbar-arrow-under:after { 117 | border-width: 8px 8px 0 8px; } 118 | 119 | .medium-toolbar-arrow-over:before { 120 | border-width: 0 8px 8px 8px; 121 | top: -8px; } 122 | 123 | .medium-editor-toolbar { 124 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 125 | font-size: 16px; 126 | left: 0; 127 | position: absolute; 128 | top: 0; 129 | visibility: hidden; 130 | z-index: 2000; } 131 | .medium-editor-toolbar ul { 132 | margin: 0; 133 | padding: 0; } 134 | .medium-editor-toolbar li { 135 | float: left; 136 | list-style: none; 137 | margin: 0; 138 | padding: 0; } 139 | .medium-editor-toolbar li button { 140 | box-sizing: border-box; 141 | cursor: pointer; 142 | display: block; 143 | font-size: 14px; 144 | line-height: 1.33; 145 | margin: 0; 146 | padding: 15px; 147 | text-decoration: none; } 148 | .medium-editor-toolbar li button:focus { 149 | outline: none; } 150 | .medium-editor-toolbar li .medium-editor-action-underline { 151 | text-decoration: underline; } 152 | .medium-editor-toolbar li .medium-editor-action-pre { 153 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 154 | font-size: 12px; 155 | font-weight: 100; 156 | padding: 15px 0; } 157 | 158 | .medium-editor-toolbar-active { 159 | visibility: visible; } 160 | 161 | .medium-editor-sticky-toolbar { 162 | position: fixed; 163 | top: 1px; } 164 | 165 | .medium-editor-relative-toolbar { 166 | position: relative; } 167 | 168 | .medium-editor-toolbar-active.medium-editor-stalker-toolbar { 169 | -webkit-animation: medium-editor-pop-upwards 160ms forwards linear; 170 | animation: medium-editor-pop-upwards 160ms forwards linear; } 171 | 172 | .medium-editor-action-bold { 173 | font-weight: bolder; } 174 | 175 | .medium-editor-action-italic { 176 | font-style: italic; } 177 | 178 | .medium-editor-toolbar-form { 179 | display: none; } 180 | .medium-editor-toolbar-form input, 181 | .medium-editor-toolbar-form a { 182 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } 183 | .medium-editor-toolbar-form .medium-editor-toolbar-form-row { 184 | line-height: 14px; 185 | margin-left: 5px; 186 | padding-bottom: 5px; } 187 | .medium-editor-toolbar-form .medium-editor-toolbar-input, 188 | .medium-editor-toolbar-form label { 189 | border: none; 190 | box-sizing: border-box; 191 | font-size: 14px; 192 | margin: 0; 193 | padding: 6px; 194 | width: 316px; 195 | display: inline-block; } 196 | .medium-editor-toolbar-form .medium-editor-toolbar-input:focus, 197 | .medium-editor-toolbar-form label:focus { 198 | -webkit-appearance: none; 199 | -moz-appearance: none; 200 | appearance: none; 201 | border: none; 202 | box-shadow: none; 203 | outline: 0; } 204 | .medium-editor-toolbar-form a { 205 | display: inline-block; 206 | font-size: 24px; 207 | font-weight: bolder; 208 | margin: 0 10px; 209 | text-decoration: none; } 210 | 211 | .medium-editor-toolbar-form-active { 212 | display: block; } 213 | 214 | .medium-editor-toolbar-actions:after { 215 | clear: both; 216 | content: ""; 217 | display: table; } 218 | 219 | .medium-editor-element { 220 | word-wrap: break-word; 221 | min-height: 30px; } 222 | .medium-editor-element img { 223 | max-width: 100%; } 224 | .medium-editor-element sub { 225 | vertical-align: sub; } 226 | .medium-editor-element sup { 227 | vertical-align: super; } 228 | 229 | .medium-editor-hidden { 230 | display: none; } 231 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | .editor { 11 | 12 | } 13 | 14 | .bullet { 15 | width: 8px; 16 | height: 8px; 17 | background-color: #ff0000; 18 | } 19 | -------------------------------------------------------------------------------- /public/images/TopicQuestsLogo_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/public/images/TopicQuestsLogo_sm.png -------------------------------------------------------------------------------- /public/images/github-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/public/images/github-1.jpg -------------------------------------------------------------------------------- /public/images/map.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/public/images/map.gif -------------------------------------------------------------------------------- /public/js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/public/js/.DS_Store -------------------------------------------------------------------------------- /public/js/bootstrap3-typeahead.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap3-typeahead.js v4.0.2 3 | * https://github.com/bassjobsen/Bootstrap-3-Typeahead 4 | * ============================================================= 5 | * Original written by @mdo and @fat 6 | * ============================================================= 7 | * Copyright 2014 Bass Jobsen @bassjobsen 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the 'License'); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an 'AS IS' BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * ============================================================ */ 21 | 22 | 23 | (function (root, factory) { 24 | 25 | 'use strict'; 26 | 27 | // CommonJS module is defined 28 | if (typeof module !== 'undefined' && module.exports) { 29 | module.exports = factory(require('jquery')); 30 | } 31 | // AMD module is defined 32 | else if (typeof define === 'function' && define.amd) { 33 | define(['jquery'], function ($) { 34 | return factory($); 35 | }); 36 | } else { 37 | factory(root.jQuery); 38 | } 39 | 40 | }(this, function ($) { 41 | 42 | 'use strict'; 43 | // jshint laxcomma: true 44 | 45 | 46 | /* TYPEAHEAD PUBLIC CLASS DEFINITION 47 | * ================================= */ 48 | 49 | var Typeahead = function (element, options) { 50 | this.$element = $(element); 51 | this.options = $.extend({}, Typeahead.defaults, options); 52 | this.matcher = this.options.matcher || this.matcher; 53 | this.sorter = this.options.sorter || this.sorter; 54 | this.select = this.options.select || this.select; 55 | this.autoSelect = typeof this.options.autoSelect == 'boolean' ? this.options.autoSelect : true; 56 | this.highlighter = this.options.highlighter || this.highlighter; 57 | this.render = this.options.render || this.render; 58 | this.updater = this.options.updater || this.updater; 59 | this.displayText = this.options.displayText || this.displayText; 60 | this.itemLink = this.options.itemLink || this.itemLink; 61 | this.itemTitle = this.options.itemTitle || this.itemTitle; 62 | this.followLinkOnSelect = this.options.followLinkOnSelect || this.followLinkOnSelect; 63 | this.source = this.options.source; 64 | this.delay = this.options.delay; 65 | this.theme = this.options.theme && this.options.themes && this.options.themes[this.options.theme] || Typeahead.defaults.themes[Typeahead.defaults.theme]; 66 | this.$menu = $(this.options.menu || this.theme.menu); 67 | this.$appendTo = this.options.appendTo ? $(this.options.appendTo) : null; 68 | this.fitToElement = typeof this.options.fitToElement == 'boolean' ? this.options.fitToElement : false; 69 | this.shown = false; 70 | this.listen(); 71 | this.showHintOnFocus = typeof this.options.showHintOnFocus == 'boolean' || this.options.showHintOnFocus === 'all' ? this.options.showHintOnFocus : false; 72 | this.afterSelect = this.options.afterSelect; 73 | this.afterEmptySelect = this.options.afterEmptySelect; 74 | this.addItem = false; 75 | this.value = this.$element.val() || this.$element.text(); 76 | this.keyPressed = false; 77 | this.focused = this.$element.is(':focus'); 78 | this.changeInputOnSelect = this.options.changeInputOnSelect || this.changeInputOnSelect; 79 | this.changeInputOnMove = this.options.changeInputOnMove || this.changeInputOnMove; 80 | this.openLinkInNewTab = this.options.openLinkInNewTab || this.openLinkInNewTab; 81 | this.selectOnBlur = this.options.selectOnBlur || this.selectOnBlur; 82 | this.showCategoryHeader = this.options.showCategoryHeader || this.showCategoryHeader; 83 | }; 84 | 85 | Typeahead.prototype = { 86 | 87 | constructor: Typeahead, 88 | 89 | 90 | setDefault: function (val) { 91 | // var val = this.$menu.find('.active').data('value'); 92 | this.$element.data('active', val); 93 | if (this.autoSelect || val) { 94 | var newVal = this.updater(val); 95 | // Updater can be set to any random functions via "options" parameter in constructor above. 96 | // Add null check for cases when updater returns void or undefined. 97 | if (!newVal) { 98 | newVal = ''; 99 | } 100 | this.$element 101 | .val(this.displayText(newVal) || newVal) 102 | .text(this.displayText(newVal) || newVal) 103 | .change(); 104 | this.afterSelect(newVal); 105 | } 106 | return this.hide(); 107 | }, 108 | 109 | select: function () { 110 | var val = this.$menu.find('.active').data('value'); 111 | 112 | this.$element.data('active', val); 113 | if (this.autoSelect || val) { 114 | var newVal = this.updater(val); 115 | // Updater can be set to any random functions via "options" parameter in constructor above. 116 | // Add null check for cases when updater returns void or undefined. 117 | if (!newVal) { 118 | newVal = ''; 119 | } 120 | 121 | if (this.changeInputOnSelect) { 122 | this.$element 123 | .val(this.displayText(newVal) || newVal) 124 | .text(this.displayText(newVal) || newVal) 125 | .change(); 126 | } 127 | 128 | if (this.followLinkOnSelect && this.itemLink(val)) { 129 | if (this.openLinkInNewTab) { 130 | window.open(this.itemLink(val), '_blank'); 131 | } else { 132 | document.location = this.itemLink(val); 133 | } 134 | this.afterSelect(newVal); 135 | } else if (this.followLinkOnSelect && !this.itemLink(val)) { 136 | this.afterEmptySelect(newVal); 137 | } else { 138 | this.afterSelect(newVal); 139 | } 140 | } else { 141 | this.afterEmptySelect(); 142 | } 143 | 144 | return this.hide(); 145 | }, 146 | 147 | updater: function (item) { 148 | return item; 149 | }, 150 | 151 | setSource: function (source) { 152 | this.source = source; 153 | }, 154 | 155 | show: function () { 156 | var pos = $.extend({}, this.$element.position(), { 157 | height: this.$element[0].offsetHeight 158 | }); 159 | 160 | var scrollHeight = typeof this.options.scrollHeight == 'function' ? 161 | this.options.scrollHeight.call() : 162 | this.options.scrollHeight; 163 | 164 | var element; 165 | if (this.shown) { 166 | element = this.$menu; 167 | } else if (this.$appendTo) { 168 | element = this.$menu.appendTo(this.$appendTo); 169 | this.hasSameParent = this.$appendTo.is(this.$element.parent()); 170 | } else { 171 | element = this.$menu.insertAfter(this.$element); 172 | this.hasSameParent = true; 173 | } 174 | 175 | if (!this.hasSameParent) { 176 | // We cannot rely on the element position, need to position relative to the window 177 | element.css('position', 'fixed'); 178 | var offset = this.$element.offset(); 179 | pos.top = offset.top; 180 | pos.left = offset.left; 181 | } 182 | // The rules for bootstrap are: 'dropup' in the parent and 'dropdown-menu-right' in the element. 183 | // Note that to get right alignment, you'll need to specify `menu` in the options to be: 184 | // '' 185 | var dropup = $(element).parent().hasClass('dropup'); 186 | var newTop = dropup ? 'auto' : (pos.top + pos.height + scrollHeight); 187 | var right = $(element).hasClass('dropdown-menu-right'); 188 | var newLeft = right ? 'auto' : pos.left; 189 | // it seems like setting the css is a bad idea (just let Bootstrap do it), but I'll keep the old 190 | // logic in place except for the dropup/right-align cases. 191 | element.css({ top: newTop, left: newLeft }).show(); 192 | 193 | if (this.options.fitToElement === true) { 194 | element.css('width', this.$element.outerWidth() + 'px'); 195 | } 196 | 197 | this.shown = true; 198 | return this; 199 | }, 200 | 201 | hide: function () { 202 | this.$menu.hide(); 203 | this.shown = false; 204 | return this; 205 | }, 206 | 207 | lookup: function (query) { 208 | if (typeof(query) != 'undefined' && query !== null) { 209 | this.query = query; 210 | } else { 211 | this.query = this.$element.val(); 212 | } 213 | 214 | if (this.query.length < this.options.minLength && !this.options.showHintOnFocus) { 215 | return this.shown ? this.hide() : this; 216 | } 217 | 218 | var worker = $.proxy(function () { 219 | 220 | // Bloodhound (since 0.11) needs three arguments. 221 | // Two of them are callback functions (sync and async) for local and remote data processing 222 | // see https://github.com/twitter/typeahead.js/blob/master/src/bloodhound/bloodhound.js#L132 223 | if ($.isFunction(this.source) && this.source.length === 3) { 224 | this.source(this.query, $.proxy(this.process, this), $.proxy(this.process, this)); 225 | } else if ($.isFunction(this.source)) { 226 | this.source(this.query, $.proxy(this.process, this)); 227 | } else if (this.source) { 228 | this.process(this.source); 229 | } 230 | }, this); 231 | 232 | clearTimeout(this.lookupWorker); 233 | this.lookupWorker = setTimeout(worker, this.delay); 234 | }, 235 | 236 | process: function (items) { 237 | var that = this; 238 | 239 | items = $.grep(items, function (item) { 240 | return that.matcher(item); 241 | }); 242 | 243 | items = this.sorter(items); 244 | 245 | if (!items.length && !this.options.addItem) { 246 | return this.shown ? this.hide() : this; 247 | } 248 | 249 | if (items.length > 0) { 250 | this.$element.data('active', items[0]); 251 | } else { 252 | this.$element.data('active', null); 253 | } 254 | 255 | if (this.options.items != 'all') { 256 | items = items.slice(0, this.options.items); 257 | } 258 | 259 | // Add item 260 | if (this.options.addItem) { 261 | items.push(this.options.addItem); 262 | } 263 | 264 | return this.render(items).show(); 265 | }, 266 | 267 | matcher: function (item) { 268 | var it = this.displayText(item); 269 | return ~it.toLowerCase().indexOf(this.query.toLowerCase()); 270 | }, 271 | 272 | sorter: function (items) { 273 | var beginswith = []; 274 | var caseSensitive = []; 275 | var caseInsensitive = []; 276 | var item; 277 | 278 | while ((item = items.shift())) { 279 | var it = this.displayText(item); 280 | if (!it.toLowerCase().indexOf(this.query.toLowerCase())) { 281 | beginswith.push(item); 282 | } else if (~it.indexOf(this.query)) { 283 | caseSensitive.push(item); 284 | } else { 285 | caseInsensitive.push(item); 286 | } 287 | } 288 | 289 | return beginswith.concat(caseSensitive, caseInsensitive); 290 | }, 291 | 292 | highlighter: function (item) { 293 | var text = this.query; 294 | if (text === '') { 295 | return item; 296 | } 297 | var matches = item.match(/(>)([^<]*)(<)/g); 298 | var first = []; 299 | var second = []; 300 | var i; 301 | if (matches && matches.length) { 302 | // html 303 | for (i = 0; i < matches.length; ++i) { 304 | if (matches[i].length > 2) {// escape '><' 305 | first.push(matches[i]); 306 | } 307 | } 308 | } else { 309 | // text 310 | first = []; 311 | first.push(item); 312 | } 313 | text = text.replace((/[\(\)\/\.\*\+\?\[\]]/g), function (mat) { 314 | return '\\' + mat; 315 | }); 316 | var reg = new RegExp(text, 'g'); 317 | var m; 318 | for (i = 0; i < first.length; ++i) { 319 | m = first[i].match(reg); 320 | if (m && m.length > 0) {// find all text nodes matches 321 | second.push(first[i]); 322 | } 323 | } 324 | for (i = 0; i < second.length; ++i) { 325 | item = item.replace(second[i], second[i].replace(reg, '$&')); 326 | } 327 | return item; 328 | }, 329 | 330 | render: function (items) { 331 | var that = this; 332 | var self = this; 333 | var activeFound = false; 334 | var data = []; 335 | var _category = that.options.separator; 336 | 337 | $.each(items, function (key, value) { 338 | // inject separator 339 | if (key > 0 && value[_category] !== items[key - 1][_category]) { 340 | data.push({ 341 | __type: 'divider' 342 | }); 343 | } 344 | 345 | if (this.showCategoryHeader) { 346 | // inject category header 347 | if (value[_category] && (key === 0 || value[_category] !== items[key - 1][_category])) { 348 | data.push({ 349 | __type: 'category', 350 | name: value[_category] 351 | }); 352 | } 353 | } 354 | 355 | data.push(value); 356 | }); 357 | 358 | items = $(data).map(function (i, item) { 359 | if ((item.__type || false) == 'category'){ 360 | return $(that.options.headerHtml || that.theme.headerHtml).text(item.name)[0]; 361 | } 362 | 363 | if ((item.__type || false) == 'divider'){ 364 | return $(that.options.headerDivider || that.theme.headerDivider)[0]; 365 | } 366 | 367 | var text = self.displayText(item); 368 | i = $(that.options.item || that.theme.item).data('value', item); 369 | i.find(that.options.itemContentSelector || that.theme.itemContentSelector) 370 | .addBack(that.options.itemContentSelector || that.theme.itemContentSelector) 371 | .html(that.highlighter(text, item)); 372 | if(that.options.followLinkOnSelect) { 373 | i.find('a').attr('href', self.itemLink(item)); 374 | } 375 | i.find('a').attr('title', self.itemTitle(item)); 376 | if (text == self.$element.val()) { 377 | i.addClass('active'); 378 | self.$element.data('active', item); 379 | activeFound = true; 380 | } 381 | return i[0]; 382 | }); 383 | 384 | if (this.autoSelect && !activeFound) { 385 | items.filter(':not(.dropdown-header)').first().addClass('active'); 386 | this.$element.data('active', items.first().data('value')); 387 | } 388 | this.$menu.html(items); 389 | return this; 390 | }, 391 | 392 | displayText: function (item) { 393 | return typeof item !== 'undefined' && typeof item.name != 'undefined' ? item.name : item; 394 | }, 395 | 396 | itemLink: function (item) { 397 | return null; 398 | }, 399 | 400 | itemTitle: function (item) { 401 | return null; 402 | }, 403 | 404 | next: function (event) { 405 | var active = this.$menu.find('.active').removeClass('active'); 406 | var next = active.next(); 407 | 408 | if (!next.length) { 409 | next = $(this.$menu.find($(this.options.item || this.theme.item).prop('tagName'))[0]); 410 | } 411 | 412 | while (next.hasClass('divider') || next.hasClass('dropdown-header')) { 413 | next = next.next(); 414 | } 415 | 416 | next.addClass('active'); 417 | // added for screen reader 418 | var newVal = this.updater(next.data('value')); 419 | if (this.changeInputOnMove) { 420 | this.$element.val(this.displayText(newVal) || newVal); 421 | } 422 | }, 423 | 424 | prev: function (event) { 425 | var active = this.$menu.find('.active').removeClass('active'); 426 | var prev = active.prev(); 427 | 428 | if (!prev.length) { 429 | prev = this.$menu.find($(this.options.item || this.theme.item).prop('tagName')).last(); 430 | } 431 | 432 | while (prev.hasClass('divider') || prev.hasClass('dropdown-header')) { 433 | prev = prev.prev(); 434 | } 435 | 436 | prev.addClass('active'); 437 | // added for screen reader 438 | var newVal = this.updater(prev.data('value')); 439 | if (this.changeInputOnMove) { 440 | this.$element.val(this.displayText(newVal) || newVal); 441 | } 442 | }, 443 | 444 | listen: function () { 445 | this.$element 446 | .on('focus.bootstrap3Typeahead', $.proxy(this.focus, this)) 447 | .on('blur.bootstrap3Typeahead', $.proxy(this.blur, this)) 448 | .on('keypress.bootstrap3Typeahead', $.proxy(this.keypress, this)) 449 | .on('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead', $.proxy(this.input, this)) 450 | .on('keyup.bootstrap3Typeahead', $.proxy(this.keyup, this)); 451 | 452 | if (this.eventSupported('keydown')) { 453 | this.$element.on('keydown.bootstrap3Typeahead', $.proxy(this.keydown, this)); 454 | } 455 | 456 | var itemTagName = $(this.options.item || this.theme.item).prop('tagName'); 457 | if ('ontouchstart' in document.documentElement && 'onmousemove' in document.documentElement) { 458 | this.$menu 459 | .on('touchstart', itemTagName, $.proxy(this.touchstart, this)) 460 | .on('touchend', itemTagName, $.proxy(this.click, this)) 461 | .on('click', $.proxy(this.click, this)) 462 | .on('mouseenter', itemTagName, $.proxy(this.mouseenter, this)) 463 | .on('mouseleave', itemTagName, $.proxy(this.mouseleave, this)) 464 | .on('mousedown', $.proxy(this.mousedown,this)); 465 | } else if ('ontouchstart' in document.documentElement) { 466 | this.$menu 467 | .on('touchstart', itemTagName, $.proxy(this.touchstart, this)) 468 | .on('touchend', itemTagName, $.proxy(this.click, this)); 469 | } else { 470 | this.$menu 471 | .on('click', $.proxy(this.click, this)) 472 | .on('mouseenter', itemTagName, $.proxy(this.mouseenter, this)) 473 | .on('mouseleave', itemTagName, $.proxy(this.mouseleave, this)) 474 | .on('mousedown', $.proxy(this.mousedown, this)); 475 | } 476 | }, 477 | 478 | destroy: function () { 479 | this.$element.data('typeahead', null); 480 | this.$element.data('active', null); 481 | this.$element 482 | .unbind('focus.bootstrap3Typeahead') 483 | .unbind('blur.bootstrap3Typeahead') 484 | .unbind('keypress.bootstrap3Typeahead') 485 | .unbind('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead') 486 | .unbind('keyup.bootstrap3Typeahead'); 487 | 488 | if (this.eventSupported('keydown')) { 489 | this.$element.unbind('keydown.bootstrap3-typeahead'); 490 | } 491 | 492 | this.$menu.remove(); 493 | this.destroyed = true; 494 | }, 495 | 496 | eventSupported: function (eventName) { 497 | var isSupported = eventName in this.$element; 498 | if (!isSupported) { 499 | this.$element.setAttribute(eventName, 'return;'); 500 | isSupported = typeof this.$element[eventName] === 'function'; 501 | } 502 | return isSupported; 503 | }, 504 | 505 | move: function (e) { 506 | if (!this.shown) { 507 | return; 508 | } 509 | 510 | switch (e.keyCode) { 511 | case 9: // tab 512 | case 13: // enter 513 | case 27: // escape 514 | e.preventDefault(); 515 | break; 516 | 517 | case 38: // up arrow 518 | // with the shiftKey (this is actually the left parenthesis) 519 | if (e.shiftKey) { 520 | return; 521 | } 522 | e.preventDefault(); 523 | this.prev(); 524 | break; 525 | 526 | case 40: // down arrow 527 | // with the shiftKey (this is actually the right parenthesis) 528 | if (e.shiftKey) { 529 | return; 530 | } 531 | e.preventDefault(); 532 | this.next(); 533 | break; 534 | } 535 | }, 536 | 537 | keydown: function (e) { 538 | /** 539 | * Prevent to make an ajax call while copying and pasting. 540 | * 541 | * @author Simone Sacchi 542 | * @version 2018/01/18 543 | */ 544 | if (e.keyCode === 17) { // ctrl 545 | return; 546 | } 547 | this.keyPressed = true; 548 | this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40, 38, 9, 13, 27]); 549 | if (!this.shown && e.keyCode == 40) { 550 | this.lookup(); 551 | } else { 552 | this.move(e); 553 | } 554 | }, 555 | 556 | keypress: function (e) { 557 | if (this.suppressKeyPressRepeat) { 558 | return; 559 | } 560 | this.move(e); 561 | }, 562 | 563 | input: function (e) { 564 | // This is a fixed for IE10/11 that fires the input event when a placehoder is changed 565 | // (https://connect.microsoft.com/IE/feedback/details/810538/ie-11-fires-input-event-on-focus) 566 | var currentValue = this.$element.val() || this.$element.text(); 567 | if (this.value !== currentValue) { 568 | this.value = currentValue; 569 | this.lookup(); 570 | } 571 | }, 572 | 573 | keyup: function (e) { 574 | if (this.destroyed) { 575 | return; 576 | } 577 | switch (e.keyCode) { 578 | case 40: // down arrow 579 | case 38: // up arrow 580 | case 16: // shift 581 | case 17: // ctrl 582 | case 18: // alt 583 | break; 584 | 585 | case 9: // tab 586 | if (!this.shown || (this.showHintOnFocus && !this.keyPressed)) { 587 | return; 588 | } 589 | this.select(); 590 | break; 591 | case 13: // enter 592 | if (!this.shown) { 593 | return; 594 | } 595 | this.select(); 596 | break; 597 | 598 | case 27: // escape 599 | if (!this.shown) { 600 | return; 601 | } 602 | this.hide(); 603 | break; 604 | } 605 | 606 | }, 607 | 608 | focus: function (e) { 609 | if (!this.focused) { 610 | this.focused = true; 611 | this.keyPressed = false; 612 | if (this.options.showHintOnFocus && this.skipShowHintOnFocus !== true) { 613 | if (this.options.showHintOnFocus === 'all') { 614 | this.lookup(''); 615 | } else { 616 | this.lookup(); 617 | } 618 | } 619 | } 620 | if (this.skipShowHintOnFocus) { 621 | this.skipShowHintOnFocus = false; 622 | } 623 | }, 624 | 625 | blur: function (e) { 626 | if (!this.mousedover && !this.mouseddown && this.shown) { 627 | if (this.selectOnBlur) { 628 | this.select(); 629 | } 630 | this.hide(); 631 | this.focused = false; 632 | this.keyPressed = false; 633 | } else if (this.mouseddown) { 634 | // This is for IE that blurs the input when user clicks on scroll. 635 | // We set the focus back on the input and prevent the lookup to occur again 636 | this.skipShowHintOnFocus = true; 637 | this.$element.focus(); 638 | this.mouseddown = false; 639 | } 640 | }, 641 | 642 | click: function (e) { 643 | e.preventDefault(); 644 | this.skipShowHintOnFocus = true; 645 | this.select(); 646 | this.$element.focus(); 647 | this.hide(); 648 | }, 649 | 650 | mouseenter: function (e) { 651 | this.mousedover = true; 652 | this.$menu.find('.active').removeClass('active'); 653 | $(e.currentTarget).addClass('active'); 654 | }, 655 | 656 | mouseleave: function (e) { 657 | this.mousedover = false; 658 | if (!this.focused && this.shown) { 659 | this.hide(); 660 | } 661 | }, 662 | 663 | /** 664 | * We track the mousedown for IE. When clicking on the menu scrollbar, IE makes the input blur thus hiding the menu. 665 | */ 666 | mousedown: function (e) { 667 | this.mouseddown = true; 668 | this.$menu.one('mouseup', function (e) { 669 | // IE won't fire this, but FF and Chrome will so we reset our flag for them here 670 | this.mouseddown = false; 671 | }.bind(this)); 672 | }, 673 | 674 | touchstart: function (e) { 675 | e.preventDefault(); 676 | this.$menu.find('.active').removeClass('active'); 677 | $(e.currentTarget).addClass('active'); 678 | }, 679 | 680 | touchend: function (e) { 681 | e.preventDefault(); 682 | this.select(); 683 | this.$element.focus(); 684 | } 685 | 686 | }; 687 | 688 | 689 | /* TYPEAHEAD PLUGIN DEFINITION 690 | * =========================== */ 691 | 692 | var old = $.fn.typeahead; 693 | 694 | $.fn.typeahead = function (option) { 695 | var arg = arguments; 696 | if (typeof option == 'string' && option == 'getActive') { 697 | return this.data('active'); 698 | } 699 | return this.each(function () { 700 | var $this = $(this); 701 | var data = $this.data('typeahead'); 702 | var options = typeof option == 'object' && option; 703 | if (!data) { 704 | $this.data('typeahead', (data = new Typeahead(this, options))); 705 | } 706 | if (typeof option == 'string' && data[option]) { 707 | if (arg.length > 1) { 708 | data[option].apply(data, Array.prototype.slice.call(arg, 1)); 709 | } else { 710 | data[option](); 711 | } 712 | } 713 | }); 714 | }; 715 | 716 | Typeahead.defaults = { 717 | source: [], 718 | items: 8, 719 | minLength: 1, 720 | scrollHeight: 0, 721 | autoSelect: true, 722 | afterSelect: $.noop, 723 | afterEmptySelect: $.noop, 724 | addItem: false, 725 | followLinkOnSelect: false, 726 | delay: 0, 727 | separator: 'category', 728 | changeInputOnSelect: true, 729 | changeInputOnMove: true, 730 | openLinkInNewTab: false, 731 | selectOnBlur: true, 732 | showCategoryHeader: true, 733 | theme: "bootstrap3", 734 | themes: { 735 | bootstrap3: { 736 | menu: '', 737 | item: '
  • ', 738 | itemContentSelector: "a", 739 | headerHtml: '', 740 | headerDivider: '' 741 | }, 742 | bootstrap4: { 743 | menu: '', 744 | item: '', 745 | itemContentSelector: '.dropdown-item', 746 | headerHtml: '', 747 | headerDivider: '' 748 | } 749 | } 750 | }; 751 | 752 | $.fn.typeahead.Constructor = Typeahead; 753 | 754 | /* TYPEAHEAD NO CONFLICT 755 | * =================== */ 756 | 757 | $.fn.typeahead.noConflict = function () { 758 | $.fn.typeahead = old; 759 | return this; 760 | }; 761 | 762 | 763 | /* TYPEAHEAD DATA-API 764 | * ================== */ 765 | 766 | $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 767 | var $this = $(this); 768 | if ($this.data('typeahead')) { 769 | return; 770 | } 771 | $this.typeahead($this.data()); 772 | }); 773 | 774 | })); 775 | -------------------------------------------------------------------------------- /routes/helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* @author park */ 3 | const config = require('../config/config'); 4 | 5 | class Helper { 6 | //console.log("HELPER"); 7 | 8 | /** 9 | * Returns the ip value 10 | * @param {*} req 11 | * @param {*} which 12 | * @return 13 | */ 14 | checkIP(req, which) { 15 | const ip = 16 | req.connection.remoteAddress || 17 | req.socket.remoteAddress || 18 | req.connection.socket.remoteAddress; 19 | console.log("Helper.checkIP",ip,which); 20 | return ip; 21 | }; 22 | 23 | isPrivate(req, res, next) { 24 | if (config.isPrivatePortal) { 25 | if (self.isAuthenticated(req)) { 26 | return next(); 27 | } 28 | return res.redirect("/login"); 29 | } else { 30 | return next(); 31 | } 32 | }; 33 | 34 | /** 35 | * Prefill a JSONObject with data for rendering 36 | * @return 37 | * / 38 | self.startData = function(req) { 39 | // console.log("Helper.startData",req.session); 40 | var title = config.brand; 41 | var result = { title: title }; 42 | if (req.flash) { 43 | result.flashMsg = req.flash("error") || req.flash("success"); 44 | } 45 | if (self.isAuthenticated(req)) { 46 | result.isAuthenticated = true; 47 | result.userId = req.session.theUserId; 48 | } 49 | result.invitationOnly = config.invitationOnly; 50 | self.isAdmin(req, function(truth) { 51 | result.isAdmin = truth; 52 | }); 53 | //current conversation 54 | result.curCon = req.session.curCon; 55 | //remembered 56 | result.isRemembered = req.session.transclude; 57 | // console.log("XYZ-2",req.session.curCon,req.session.theUser); 58 | return result; 59 | }; 60 | 61 | /** 62 | * This will be extended to ask if user is an admin 63 | * @param {*} userId 64 | * @param {*} node 65 | */ 66 | canEdit(userId, node) { 67 | return (userId === node.userId); 68 | }; 69 | 70 | canDelete(userId, node) { 71 | const hasIBISkids = CommonModel.hasIBISChildren(node); 72 | return (self.canEdit(userId, node) && !hasIBISkids); 73 | }; 74 | 75 | isAuthenticated(req) { 76 | // console.log("Helper.isAuthenticated",req.session); 77 | if (req.session.theUser) { 78 | return true; 79 | } 80 | return false; 81 | }; 82 | 83 | /** 84 | * @param req 85 | * @param {*} callback truth 86 | */ 87 | async isAdmin(req) { 88 | const email = req.session.theUserEmail; 89 | return await AdminModel.checkIsAdmin(email); 90 | }; 91 | 92 | logout(req) { 93 | //DO NOTHING FOR NOW 94 | req.session.theUser = null; 95 | }; 96 | 97 | } 98 | 99 | 100 | const instance = new Helper(); 101 | module.exports = instance; 102 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); 3 | const helper = require('./helper'); 4 | const router = express.Router(); 5 | 6 | const JournalModel = require('../apps/models/journal_model'); 7 | const AdminModel = require('../apps/models/admin_model'); 8 | 9 | const config = require('../config/config'); 10 | let predicates; 11 | const hbs = require('hbs'); 12 | 13 | hbs.registerHelper('toJSON', obj => { 14 | return JSON.stringify(obj, null); 15 | }); 16 | 17 | function baseData(req) { 18 | const data = {}; 19 | data.title = config.banner; 20 | data.canSignup = config.canSignup; 21 | data.isAuthenticated = helper.isAuthenticated(req); 22 | 23 | return data; 24 | } 25 | 26 | function validatePredicates() { 27 | if (!predicates) { 28 | let whichvocab = config.vocabulary; 29 | whichvocab = `../config/vocab/${whichvocab}/labels`; 30 | predicates = require(whichvocab); 31 | console.info('IndexPreds', predicates); 32 | console.info('IP2', predicates.terms[0]); 33 | } 34 | } 35 | 36 | ///////////////////// 37 | // User Accounts 38 | ///////////////////// 39 | 40 | router.get('/signup', (req, res, next) => { 41 | const data = baseData(req); 42 | return res.render('signup_form', data); 43 | }); 44 | 45 | router.get('/login', (req, res, next) => { 46 | console.info("login"); 47 | const data = baseData(req); 48 | return res.render('login_form', data); 49 | }); 50 | 51 | router.get('/logout', (req, res, next) => { 52 | const struct = {}; 53 | req.session.theUser = null; 54 | req.session.theUserId = null; 55 | helper.logout(req); 56 | return res.redirect('/'); 57 | }); 58 | 59 | router.post('/signup', async (req, res, next) => { 60 | const email = req.body.email; 61 | const handle = req.body.handle; 62 | const fullName = req.body.fullname; 63 | const pwd = req.body.password; 64 | try { 65 | await AdminModel.signup(email, handle, fullName, pwd); 66 | console.log("Index.post",email,err); 67 | req.flash("success", "Signup succeeded"); 68 | return res.redirect('/'); 69 | } catch (err) { 70 | console.log("Index.post-2"); 71 | req.flash("error", `Signup Problem: ${err}`); 72 | return res.redirect('/'); 73 | } 74 | }); 75 | 76 | router.post('/login', async (req, res, next) => { 77 | const email = req.body.email; 78 | const password = req.body.password; 79 | //ip = helper.checkIP(req, "login", "signup"); 80 | try { 81 | const {success, handle, userId} = await AdminModel.authenticate(email, password); 82 | console.info("Authenticate", success, handle, userId); 83 | req.session.theUser = handle; 84 | req.session.theUserId = userId; 85 | req.session.theUserEmail = email; 86 | console.info("Authentication passed"); 87 | req.flash("success", "Login succeeded"); 88 | return res.redirect('/'); 89 | } catch (err) { 90 | console.error(err); 91 | console.info("Authentication failed"); 92 | req.flash("error", "Login failed"); 93 | return res.redirect('/'); 94 | } 95 | }); 96 | ////////////////////////// 97 | 98 | /** 99 | * Display Topics Index 100 | */ 101 | router.get('/topics', async (req, res, next) => { 102 | return res.redirect('/topics/1'); 103 | }); 104 | 105 | router.get('/topics/:page', async (req, res, next) => { 106 | const ql = 50; 107 | const page = req.params.page || 1 108 | const skp = (ql * page) - ql; 109 | console.info('GETT', page, ql, skp); 110 | 111 | const data = baseData(req); 112 | try { 113 | const [ topics, itemCount ] = await JournalModel.listTopics(ql, skp); 114 | data.topicList = topics; 115 | data.itemCount = itemCount; 116 | switch (page) { 117 | case '2': 118 | data.a2 = true; 119 | break; 120 | case '3': 121 | data.a3 = true; 122 | break; 123 | case '4': 124 | data.a4 = true; 125 | break; 126 | case '5': 127 | data.a5 = true; 128 | break; 129 | default: 130 | data.a1 = true; 131 | } 132 | return res.render('topic_index', data); 133 | } catch (err) { 134 | console.error(err); 135 | return res.redirect('/'); 136 | } 137 | 138 | }); 139 | /** 140 | * Ajax for typeahead 141 | */ 142 | router.get('/ajax/label', async (req, res, next) => { 143 | const q = req.query.query; 144 | console.info('Ajax', q); 145 | try { 146 | const data = await JournalModel.ajaxFindLabel(q); 147 | return res.json(data); 148 | } catch (err) { 149 | console.error(err); 150 | return res.redirect('/'); 151 | } 152 | }); 153 | 154 | router.get('/', async (req, res, next) => { 155 | return res.redirect('/1'); 156 | }); 157 | 158 | /* GET home page. */ 159 | router.get('/:page', helper.isPrivate, async (req, res, next) => { 160 | validatePredicates(); 161 | const ql = 50; 162 | const page = req.params.page || 1 163 | const skp = (ql * page) - ql; 164 | console.info('GETX', page, ql, skp); 165 | try { 166 | const [ noteList, itemCount ] = await JournalModel.list(ql, skp); 167 | const pageCount = Math.ceil(itemCount / ql); 168 | console.info('GETY', noteList.length, itemCount, pageCount); 169 | 170 | const data = baseData(req); 171 | 172 | data.predicates = predicates; 173 | console.info('IP3', predicates.terms[0]); 174 | if (req.flash) { 175 | data.flashMsg = req.flash("error") || req.flash("success"); 176 | } 177 | data.noteList = noteList; 178 | data.itemCount = itemCount; 179 | switch (page) { 180 | case '2': 181 | data.a2 = true; 182 | break; 183 | case '3': 184 | data.a3 = true; 185 | break; 186 | case '4': 187 | data.a4 = true; 188 | break; 189 | case '5': 190 | data.a5 = true; 191 | break; 192 | default: 193 | data.a1 = true; 194 | } 195 | // console.info("GETTING", page); 196 | return res.render('index', data); 197 | } catch (err) { 198 | console.error(err); 199 | return res.redirect('/'); 200 | } 201 | }); 202 | 203 | router.get('/iframe', async (req, res, next) => { 204 | validatePredicates(); 205 | const url = req.query.fName; 206 | console.info('IFRAME', url); 207 | try { 208 | const hits = await JournalModel.listByURL(url); 209 | const data = baseData(req); 210 | data.predicates = predicates; 211 | data.url = url; 212 | data.hits = hits; 213 | return res.render('iframe', data); 214 | } catch (err) { 215 | console.error(err); 216 | return res.redirect('/'); 217 | } 218 | }); 219 | 220 | router.get('/new_note_route', (req, res, next) => { 221 | console.info('NEW'); 222 | const noteList = []; 223 | const x = {}; 224 | x.details = '[[Foo]] causes [[Bar]]'; 225 | noteList.push(x); 226 | const data = baseData(req); 227 | data.title = config.banner; 228 | data.noteList = noteList; 229 | data.isNew = true; 230 | return res.render('index', data); 231 | }); 232 | 233 | 234 | router.post('/postAtriple', async (req, res, next) => { 235 | const subject = req.body.subject; 236 | const predicate = req.body.predicate; 237 | const object = req.body.object; 238 | const url = req.body.url; 239 | const notes = req.body.notes; 240 | const usr = req.session.theUser; 241 | const usrId = req.session.theUserId; 242 | console.info('PostTriple', subject, predicate, object, url, notes); 243 | try { 244 | const dat = await JournalModel.processTriple(subject, predicate, object, url, notes, 245 | usrId, usr); 246 | console.log('BigTriple', dat); 247 | return res.redirect(`/journal/${dat.id}`); 248 | } catch (err) { 249 | console.error(err); 250 | req.flash("error", err); 251 | return res.redirect('/'); 252 | } 253 | }); 254 | 255 | /** 256 | * A post response which serves many purposes: 257 | * a: Update a topic with an added text object. That object 258 | * may have Wikilinks embedded in it. 259 | * b: A topic is a fresh text AIR journal entry 260 | * c: (future) A topic is a fresh text AIR but is a child 261 | * node to another topic - a conversation tree node 262 | */ 263 | router.post('/posttopic', async (req, res, next) => { 264 | const body = req.body.body; 265 | const id = req.body.topicid; 266 | const parentId = req.body.parentid; 267 | const url = req.body.url; 268 | const usr = req.session.theUser; 269 | const usrId = req.session.theUserId; 270 | console.info("PostTopic", id, parentId, url, body); 271 | if (!id && !parentId && body) { 272 | try { 273 | const data = await JournalModel.newAIR(body, url, usrId, usr); 274 | return res.redirect(`/journal/${data.id}`); 275 | } catch (err) { 276 | console.error(err); 277 | req.flash("error", err); 278 | return res.redirect('/'); 279 | } 280 | } else if (body || url) { // NOTE ignoring parentId for now 281 | try { 282 | await JournalModel.updateTopic(id, url, body); 283 | return res.redirect(`/topic/${id}`); 284 | } catch (err) { 285 | console.error(err); 286 | return res.redirect(`/topic/${id}`); 287 | } 288 | } else { 289 | //bad post - for now 290 | return res.redirect('/'); 291 | } 292 | }); 293 | 294 | router.post('/postjournaledit', async (req, res, next) => { 295 | const body = req.body.body; 296 | const id = req.body.id; 297 | const isTriple = req.body.istriple; 298 | const usr = req.session.theUser; 299 | const usrId = req.session.theUserId; 300 | //var url = req.body.url; 301 | console.info('JournalEdit', id, body, isTriple); 302 | try { 303 | await JournalModel.updateJournalEntry(id, body, usrId, usr, isTriple); 304 | return res.redirect(`/journal/${id}`); 305 | } catch (err) { 306 | console.error(err); 307 | req.flash(`Error saving edited Journal: ${id}`); 308 | return res.redirect('/'); 309 | } 310 | }); 311 | 312 | router.get('/topic/:id', helper.isPrivate, async (req, res, next) => { 313 | const id = req.params.id; 314 | console.info("GetTopic", id); 315 | try { 316 | const data = await JournalModel.getTopic(id); 317 | console.info("GetTopic-2", data); 318 | const json = data; 319 | json.title = config.banner; 320 | json.canSignup = config.canSignup; 321 | json.isAuthenticated = helper.isAuthenticated(req); 322 | 323 | json.jsonSource = JSON.stringify(data); 324 | //check for graph 325 | var g = json.graph; 326 | if (g) { 327 | g = JSON.parse(g); 328 | } 329 | if (g && g.nodes && g.nodes.length > 0) { 330 | json.hasGraph = true; 331 | } 332 | console.info('TOPV', g.nodes, json.hasGraph); 333 | return res.render('topicview', json); 334 | } catch (err) { 335 | console.error(err); 336 | req.flash("error", `Cannot find Topic: ${id}`); 337 | return res.redirect('/'); 338 | } 339 | }); 340 | 341 | router.get('/journal/:id', helper.isPrivate, async (req, res, next) => { 342 | const id = req.params.id; 343 | const userId = req.session.theUserId; 344 | console.info("GetJournal", id); 345 | try { 346 | const data = await JournalModel.getJournalEntry(id); 347 | data.title = config.banner; 348 | data.canEdit = userId === data.userId; 349 | data.canSignup = config.canSignup; 350 | //migration from array to single string 351 | var bl = data.bodylist; 352 | if (bl && Array.isArray(bl)) { 353 | //heritage installations have just one entry 354 | // convert it back to a single string 355 | data.bodylist = bl[0]; 356 | } 357 | console.info("GetJournal-1", data); 358 | return res.render('journalview', data); 359 | } catch (err) { 360 | console.error(err); 361 | req.flash("error", `Cannot find Journal: ${id}`); 362 | return res.redirect('/'); 363 | } 364 | }); 365 | 366 | router.get('/journaledit/:id', async (req, res, next) => { 367 | const id = req.params.id; 368 | 369 | try { 370 | const data = await JournalModel.getJournalEntry(id); 371 | const subj = data.subj; 372 | let isTriple = true; 373 | if (!subj) { 374 | isTriple = false; 375 | data.texttoedit = data.raw; 376 | } else { 377 | var bl = data.bodylist; 378 | if (bl && Array.isArray(bl)) { 379 | bl = bl[0]; 380 | } 381 | data.texttoedit = bl; 382 | } 383 | data.isTriple = isTriple; 384 | data.title = config.banner; 385 | data.canSignup = config.canSignup; 386 | return res.render('journal_edit_form', data); 387 | } catch (err) { 388 | console.error(err); 389 | req.flash("error", `Cannot find Journal to edit: ${id}`); 390 | return res.redirect('/'); 391 | } 392 | }); 393 | 394 | module.exports = router; 395 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); 3 | const router = express.Router(); 4 | 5 | /* GET users listing. */ 6 | router.get('/', (req, res, next) => { 7 | res.send('respond with a resource'); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /views/editor.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgeGarden/lite-net-3/92d9e8053fb95e06e2a325eb25a775435dc86c00/views/editor.hbs -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

    {{message}}

    2 |

    {{error.status}}

    3 |
    {{error.stack}}
    4 | -------------------------------------------------------------------------------- /views/iframe.hbs: -------------------------------------------------------------------------------- 1 | 2 | 54 | 89 |
    90 |
    91 | × 92 | 93 | 94 |
    95 | {{> air_form}} 96 |
    97 | 100 |
    101 |
    102 |

    History

    103 |
    104 | {{#each hits}} 105 |
    106 |

    {{{raw}}}


    107 |
    108 | {{/each}} 109 |
    110 |
    111 |
    112 |
    113 | Page to Harvest 114 | Open Editors 115 |

    116 | 117 |
    118 | 119 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 68 | 76 |

    Journal

    77 |

    Topics Index

    78 | {{#if isAuthenticated}} 79 | 80 | 81 | 82 |
    83 | {{> air_form}} 84 |
    85 | 88 | 95 |
    96 | {{/if}} 97 |

    Journal Entries

    98 | Number of Entries: {{itemCount}}
    99 |
      100 | {{#if a1}} 101 |
    • 1
    • 102 |
    • 2
    • 103 | {{else if a2}} 104 |
    • 1
    • 105 |
    • 2
    • 106 |
    • 3
    • 107 | {{else if a3}} 108 |
    • 1
    • 109 |
    • 2
    • 110 |
    • 3
    • 111 |
    • 4
    • 112 | {{else if a4}} 113 |
    • 1
    • 114 |
    • 2
    • 115 |
    • 3
    • 116 |
    • 4
    • 117 |
    • 5
    • 118 | {{else}} 119 |
    • 1
    • 120 |
    • 2
    • 121 |
    • 3
    • 122 |
    • 4
    • 123 |
    • 5
    • 124 | {{/if }} 125 | 126 | 127 |
    128 |
    129 | {{#each noteList}} 130 |
    131 | {{date}}
    132 | {{{text}}} 133 |

    134 | {{/each}} 135 |
    136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /views/journal_edit_form.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 |

    Journal Editor

    9 |
    10 | 11 | 12 |
    13 | 14 | 15 | 16 |
    17 |
    -------------------------------------------------------------------------------- /views/journalview.hbs: -------------------------------------------------------------------------------- 1 |

    Journal Entry

    2 |

    3 | {{#if canEdit}} 4 | Edit 5 | {{/if}} 6 | {{date}}
    7 | {{{text}}}
    8 | {{#if urllist}} 9 |


    10 | {{#each urllist}} 11 | {{this}}
    12 | {{/each}} 13 | {{/if}} 14 | {{#if bodylist}} 15 |
    16 | {{{bodylist}}}
    17 | {{/if}} 18 |

    -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | 9 | 10 | 19 | 74 | 75 | 76 |
    77 | {{> webmenubar}} 78 | 79 | {{#if flashMsg}} 80 |

    {{{flashMsg}}}

    81 | {{/if}} 82 | {{{body}}} 83 | {{> footer}} 84 |
    85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /views/login_form.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 | Email
    6 | 7 | 8 |

    9 |

    10 | Password
    11 | 12 | 13 |

    14 |

    15 | 16 |  Cancel 17 |

    18 | 21 |
    22 |
    23 |
    -------------------------------------------------------------------------------- /views/partials/air_form.hbs: -------------------------------------------------------------------------------- 1 | 2 | 12 |
    13 | 14 | 15 |
    16 |
    17 | 18 | 19 |
    -------------------------------------------------------------------------------- /views/partials/footer.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 |
    -------------------------------------------------------------------------------- /views/partials/triple_form.hbs: -------------------------------------------------------------------------------- 1 | 81 | 82 |
    83 | 84 | 85 |

    86 |

    87 |
    88 | 89 |
    -------------------------------------------------------------------------------- /views/partials/webmenubar.hbs: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /views/signup_form.hbs: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 |

    Signup

    5 | {{#if invitationOnly}} 6 |
    7 |

    Signup is by invitation only: requires your email to be on the invitation list.

    8 |
    9 | {{/if}} 10 | 19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 | 26 | 27 |
    28 |
    29 | 30 | 31 |
    32 | 40 |
    41 | 42 | 43 |
    44 | 54 | 57 | 58 | 59 |
    60 |
    61 | 62 |

    Already have an account? Login

    63 |

    Or go home.

    64 |
    65 | 66 | -------------------------------------------------------------------------------- /views/topic_index.hbs: -------------------------------------------------------------------------------- 1 | 14 | 15 |

    Topics

    16 | Number of Topics: {{itemCount}}
    17 |
      18 | {{#if a1}} 19 |
    • 1
    • 20 |
    • 2
    • 21 | {{else if a2}} 22 |
    • 1
    • 23 |
    • 2
    • 24 |
    • 3
    • 25 | {{else if a3}} 26 |
    • 1
    • 27 |
    • 2
    • 28 |
    • 3
    • 29 |
    • 4
    • 30 | {{else if a4}} 31 |
    • 1
    • 32 |
    • 2
    • 33 |
    • 3
    • 34 |
    • 4
    • 35 |
    • 5
    • 36 | {{else}} 37 |
    • 1
    • 38 |
    • 2
    • 39 |
    • 3
    • 40 |
    • 4
    • 41 |
    • 5
    • 42 | {{/if }} 43 |
    44 |
    45 | Quick Autocomplete Scan: 46 | 47 |
    48 |
    49 | {{#each topicList}} 50 |
    51 | {{label}}
    52 |

    53 | {{/each}} 54 |
    -------------------------------------------------------------------------------- /views/topicview.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39 | 54 | 55 | {{#if source}} 56 |

    Relation: {{label}}

    57 | {{else}} 58 |

    Topic: {{label}}

    59 | {{/if}} 60 | 61 | {{#if hasGraph}} 62 | 63 | {{/if}} 64 |
    65 | {{#if isAuthenticated}} 66 | {{> air_form}} 67 |
    68 | {{/if}} 69 | {{#if source}} 70 |

    Source Topic: {{{source}}}

    71 |

    Target Topic: {{{target}}}

    72 | {{/if}} 73 | {{#if bodylist}} 74 |
    75 |

    Body

    76 |
    77 | {{#each bodylist}} 78 |
    79 | {{{this}}} 80 |

    81 | {{/each}} 82 |
    83 | {{/if}} 84 | {{#if urllist}} 85 |

    Resources

    86 | {{#each urllist}} 87 | {{this}}
    88 | {{/each}} 89 | {{/if}} 90 |

    Backlinks

    91 |
    92 | {{#each backlinks}} 93 |
    94 | {{{this}}} 95 |

    96 | {{/each}} 97 |
    98 |
    {{jsonSource}}
    99 |
    100 |
    101 | 102 |
    Network Graph
    103 |
    --------------------------------------------------------------------------------