├── Procfile ├── README.md ├── Vagrantfile ├── config.json ├── docker ├── Dockerfile ├── mongodb.conf └── start.sh ├── drawings ├── favicon.ico ├── favicon.png ├── home-page.bmml ├── home-page.png ├── logo.svg └── ocb-logo.png ├── examples ├── auth.html ├── demo_client.html ├── jsonp.html ├── jsonp.js └── sockjs.html ├── lib ├── helpers.js ├── models │ ├── admin.js │ ├── auth.js │ ├── client.js │ ├── config.js │ └── index.js ├── passport │ ├── facebook.js │ ├── gmail.js │ ├── index.js │ ├── local.js │ ├── persona.js │ └── twitter.js ├── routes │ ├── admin.js │ ├── comments.js │ ├── index.js │ └── public.js ├── socketManager.js └── sockets.js ├── package.json ├── plugins ├── wordpress.php └── wordpress.zip ├── public ├── css │ ├── admin.css │ └── public.css ├── favicon.ico ├── images │ ├── docker-logo.png │ ├── ghost-logo.png │ ├── heroku-logo.png │ ├── modulus-logo.jpg │ ├── nodejitsu-logo.png │ ├── user.png │ └── wordpress-logo.png ├── js │ ├── admin.js │ └── settings.js └── lib │ ├── toast.js │ └── vendor │ ├── bootbox.js │ ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ └── jquery.bootstrap-growl.min.js ├── server.js ├── static ├── client.js ├── comments.html ├── scripts │ ├── modules │ │ ├── Channel.js │ │ ├── Comment.js │ │ ├── Interface.js │ │ └── User.js │ ├── moment.min.js │ └── scripts.js └── styles │ ├── loader.gif │ ├── modules │ ├── _comment.less │ ├── _loading.less │ ├── _message.less │ └── _normalize.less │ ├── styles.css │ └── styles.less ├── test ├── configModel.js ├── helpers.js ├── mocha.opts └── socketManager.js └── views ├── admin ├── createadmin.ejs ├── index.ejs ├── login.ejs ├── moderate.ejs ├── navigation.ejs └── settings.ejs ├── browser_send.ejs └── index.ejs /Procfile: -------------------------------------------------------------------------------- 1 | web: HEROKU=true node server.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Comment Box 2 | 3 | Visit http://meteorhacks.2013.nodeknockout.com/ for more information 4 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | BOX_NAME = ENV['BOX_NAME'] || "ubuntu" 5 | BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64.box" 6 | VF_BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64_vmware_fusion.box" 7 | AWS_REGION = ENV['AWS_REGION'] || "us-east-1" 8 | AWS_AMI = ENV['AWS_AMI'] || "ami-d0f89fb9" 9 | FORWARD_DOCKER_PORTS = ENV['FORWARD_DOCKER_PORTS'] 10 | 11 | Vagrant::Config.run do |config| 12 | # Setup virtual machine box. This VM configuration code is always executed. 13 | config.vm.box = BOX_NAME 14 | config.vm.box_url = BOX_URI 15 | 16 | # Provision docker and new kernel if deployment was not done. 17 | # It is assumed Vagrant can successfully launch the provider instance. 18 | if Dir.glob("#{File.dirname(__FILE__)}/.vagrant/machines/default/*/id").empty? 19 | # Add lxc-docker package 20 | pkg_cmd = "wget -q -O - https://get.docker.io/gpg | apt-key add -;" \ 21 | "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list;" \ 22 | "apt-get update -qq; apt-get install -q -y --force-yes lxc-docker; " 23 | # Add Ubuntu raring backported kernel 24 | pkg_cmd << "apt-get update -qq; apt-get install -q -y linux-image-generic-lts-raring; " 25 | # Add guest additions if local vbox VM. As virtualbox is the default provider, 26 | # it is assumed it won't be explicitly stated. 27 | if ENV["VAGRANT_DEFAULT_PROVIDER"].nil? && ARGV.none? { |arg| arg.downcase.start_with?("--provider") } 28 | pkg_cmd << "apt-get install -q -y linux-headers-generic-lts-raring dkms; " \ 29 | "echo 'Downloading VBox Guest Additions...'; " \ 30 | "wget -q http://dlc.sun.com.edgesuite.net/virtualbox/4.2.12/VBoxGuestAdditions_4.2.12.iso; " 31 | # Prepare the VM to add guest additions after reboot 32 | pkg_cmd << "echo -e 'mount -o loop,ro /home/vagrant/VBoxGuestAdditions_4.2.12.iso /mnt\n" \ 33 | "echo yes | /mnt/VBoxLinuxAdditions.run\numount /mnt\n" \ 34 | "rm /root/guest_additions.sh; ' > /root/guest_additions.sh; " \ 35 | "chmod 700 /root/guest_additions.sh; " \ 36 | "sed -i -E 's#^exit 0#[ -x /root/guest_additions.sh ] \\&\\& /root/guest_additions.sh#' /etc/rc.local; " \ 37 | "echo 'Installation of VBox Guest Additions is proceeding in the background.'; " \ 38 | "echo '\"vagrant reload\" can be used in about 2 minutes to activate the new guest additions.'; " 39 | end 40 | # Activate new kernel 41 | pkg_cmd << "shutdown -r +1; " 42 | config.vm.provision :shell, :inline => pkg_cmd 43 | end 44 | end 45 | 46 | 47 | # Providers were added on Vagrant >= 1.1.0 48 | Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config| 49 | config.vm.provider :aws do |aws, override| 50 | aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"] 51 | aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"] 52 | aws.keypair_name = ENV["AWS_KEYPAIR_NAME"] 53 | override.ssh.private_key_path = ENV["AWS_SSH_PRIVKEY"] 54 | override.ssh.username = "ubuntu" 55 | aws.region = AWS_REGION 56 | aws.ami = AWS_AMI 57 | aws.instance_type = "t1.micro" 58 | end 59 | 60 | config.vm.provider :rackspace do |rs| 61 | config.ssh.private_key_path = ENV["RS_PRIVATE_KEY"] 62 | rs.username = ENV["RS_USERNAME"] 63 | rs.api_key = ENV["RS_API_KEY"] 64 | rs.public_key_path = ENV["RS_PUBLIC_KEY"] 65 | rs.flavor = /512MB/ 66 | rs.image = /Ubuntu/ 67 | end 68 | 69 | config.vm.provider :vmware_fusion do |f, override| 70 | override.vm.box = BOX_NAME 71 | override.vm.box_url = VF_BOX_URI 72 | override.vm.synced_folder ".", "/vagrant", disabled: true 73 | f.vmx["displayName"] = "docker" 74 | end 75 | 76 | config.vm.provider :virtualbox do |vb| 77 | config.vm.box = BOX_NAME 78 | config.vm.box_url = BOX_URI 79 | end 80 | end 81 | 82 | if !FORWARD_DOCKER_PORTS.nil? 83 | Vagrant::VERSION < "1.1.0" and Vagrant::Config.run do |config| 84 | (49000..49900).each do |port| 85 | config.vm.forward_port port, port 86 | end 87 | end 88 | 89 | Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config| 90 | (49000..49900).each do |port| 91 | config.vm.network :forwarded_port, :host => port, :guest => port 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "passport":{ 4 | "facebook":{ 5 | "clientID": "xxxx", 6 | "clientSecret": "xxx" 7 | }, 8 | "google":{ 9 | "clientID": "xxx", 10 | "clientSecret": "xxx" 11 | }, 12 | "twitter":{ 13 | "consumerKey": "xxxx", 14 | "consumerSecret": "xxxx" 15 | } 16 | }, 17 | 18 | "url": "http://localhost:8000" 19 | } -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | #Docker Image for Open Comment Box 2 | 3 | FROM ubuntu 4 | MAINTAINER Arunoda Susiripala, arunoda.susiripala@gmail.com 5 | 6 | RUN echo deb http://archive.ubuntu.com/ubuntu precise universe >> /etc/apt/sources.list 7 | RUN apt-get update -y 8 | 9 | #install dependencies 10 | RUN apt-get -y install build-essential libssl-dev wget 11 | 12 | #configuration for node 13 | ENV NODE_VERSION 0.10.21 14 | ENV NODE_ARCH x64 15 | 16 | #installation node 17 | WORKDIR /tmp 18 | RUN wget http://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$NODE_ARCH.tar.gz 19 | RUN tar xvzf node-v$NODE_VERSION-linux-$NODE_ARCH.tar.gz 20 | RUN rm -rf /opt/nodejs 21 | RUN mv node-v$NODE_VERSION-linux-$NODE_ARCH /opt/nodejs 22 | 23 | RUN ln -sf /opt/nodejs/bin/node /usr/bin/node 24 | RUN ln -sf /opt/nodejs/bin/npm /usr/bin/npm 25 | 26 | #install mongodb 27 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 28 | RUN echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | tee /etc/apt/sources.list.d/10gen.list 29 | RUN apt-get update -y 30 | RUN apt-get install mongodb-10gen=2.4.5 31 | 32 | #initialize system 33 | RUN mkdir -p /opt/ocb 34 | WORKDIR /opt/ocb 35 | 36 | RUN npm install -g node-gyp 37 | 38 | #downloading app 39 | RUN wget --no-check-certificate https://github.com/arunoda/open-comment-box/archive/v0.2.0.tar.gz 40 | RUN tar xvzf v0.2.0.tar.gz 41 | RUN mv open-comment-box-0.2.0 app 42 | RUN cd app && npm install 43 | 44 | #db setup 45 | RUN mkdir -p /data/db 46 | 47 | #copy running scripts 48 | ADD start.sh /opt/ocb/start.sh 49 | ENTRYPOINT ["bash", "start.sh"] 50 | -------------------------------------------------------------------------------- /docker/mongodb.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/docker/mongodb.conf -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$MONGO_URL" != "" ]]; then 4 | echo "Using MONGO_URL"; 5 | 6 | cd app 7 | NODE_ENV=production node server.js 8 | else 9 | echo "Using local mongodb server(not recommended)" 10 | 11 | mongod & 12 | sleep 5 13 | cd app 14 | NODE_ENV=production node server.js 15 | fi -------------------------------------------------------------------------------- /drawings/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/drawings/favicon.ico -------------------------------------------------------------------------------- /drawings/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/drawings/favicon.png -------------------------------------------------------------------------------- /drawings/home-page.bmml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A%20Web%20Page%0Ahttp%3A// 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 60 13 | Open%20Comment%20Box 14 | 15 | 16 | 17 | 18 | 32 19 | Open%20Source%20Realtime%20Comments%20Platform 20 | 21 | 22 | 23 | 24 | 20 25 | Easy%20way%20to%20power%20your%20website%20with%20comments 26 | 27 | 28 | 29 | 30 | 31 | 32 | Add%20Comments 33 | 34 | 35 | 36 | 37 | 24 38 | Moderate%20Comments%20Now 39 | 40 | 41 | 42 | 43 | 44 | 45 | 20 46 | Easy%20Install%20on%20your%20Server 47 | 48 | 49 | 50 | 51 | 20 52 | Docker%20based%20Installation 53 | 54 | 55 | 56 | 57 | See%20More 58 | 59 | 60 | 61 | 62 | See%20More 63 | 64 | 65 | 66 | 67 | 68 | See%20More 69 | 70 | 71 | 72 | 73 | 20 74 | Install%20to%20a%20PAAS 75 | 76 | 77 | 78 | 79 | Help 80 | 81 | 82 | 83 | 84 | 24 85 | Vote%20Us 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /drawings/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/drawings/home-page.png -------------------------------------------------------------------------------- /drawings/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 28 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 68 | 78 | 86 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /drawings/ocb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/drawings/ocb-logo.png -------------------------------------------------------------------------------- /examples/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/demo_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Comments 6 | 10 | 11 | 12 | 13 |
14 |

Example Header

15 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Minima, incidunt, quos, quisquam, non excepturi beatae animi quia cum quod molestiae maxime sit molestias. Non quos ad corrupti id officiis soluta. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis, blanditiis, pariatur, voluptate nesciunt cumque saepe eveniet eum officiis reiciendis quibusdam iure ad beatae modi omnis in a nam esse cupiditate.

16 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Minima, incidunt, quos, quisquam, non excepturi beatae animi quia cum quod molestiae maxime sit molestias. Non quos ad corrupti id officiis soluta. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis, blanditiis, pariatur, voluptate nesciunt cumque saepe eveniet eum officiis reiciendis quibusdam iure ad beatae modi omnis in a nam esse cupiditate. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Obcaecati, dignissimos molestias veritatis pariatur in culpa assumenda aperiam laboriosam delectus fugit. Dolorum, quisquam, a nam veniam aut labore est delectus reprehenderit!

17 |

Example Sub Header

18 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Minima, incidunt, quos, quisquam, non excepturi beatae animi quia cum quod molestiae maxime sit molestias. Non quos ad corrupti id officiis soluta. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis, blanditiis, pariatur, voluptate nesciunt cumque saepe eveniet eum officiis reiciendis quibusdam iure ad beatae modi omnis in a nam esse cupiditate.

19 | 20 | 21 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/jsonp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | -------------------------------------------------------------------------------- /examples/jsonp.js: -------------------------------------------------------------------------------- 1 | function jsonp(url) { 2 | var script = document.createElement('script'); 3 | script.src = url; 4 | document.getElementsByTagName('head')[0].appendChild(script); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /examples/sockjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var helpers = module.exports = {}; 4 | 5 | helpers.urlToDomain = function(url) { 6 | var parsedUrl = require('url').parse(url); 7 | return parsedUrl.host; 8 | }; 9 | 10 | helpers.urlToHash = function(url) { 11 | return helpers.md5(url); 12 | }; 13 | 14 | helpers.md5 = function(data) { 15 | return crypto.createHash('md5').update(data).digest("hex"); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/models/admin.js: -------------------------------------------------------------------------------- 1 | var helpers = require('../helpers'); 2 | var ObjectID = require('mongodb').ObjectID; 3 | var moment = require('moment'); 4 | function AdminModel(db) { 5 | var commentsCollection = db.collection('comments'); 6 | var usersCollection = db.collection('users'); 7 | var domainsCollection = db.collection('domains'); 8 | 9 | 10 | this.deleteComment = function (commentId, callback){ 11 | commentsCollection.findOne({"_id":ObjectID.createFromHexString(commentId)},function(err,result){ 12 | if(result && (result.domain != 'meteorhacks.2013.nodeknockout.com')){ 13 | commentsCollection.remove({"_id": ObjectID.createFromHexString(commentId)}, callback); 14 | } 15 | }); 16 | 17 | }; 18 | 19 | this.approveComment = function (commentId , callback){ 20 | commentsCollection.findOne({"_id":ObjectID.createFromHexString(commentId)},function(err,result){ 21 | if(result && (result.domain != 'meteorhacks.2013.nodeknockout.com')){ 22 | commentsCollection.update({"_id": ObjectID.createFromHexString(commentId)}, {$set:{approved:true}}, callback); 23 | } 24 | }); 25 | }; 26 | this.rejectComment = function (commentId , callback){ 27 | commentsCollection.findOne({"_id":ObjectID.createFromHexString(commentId)},function(err,result){ 28 | if(result && (result.domain != 'meteorhacks.2013.nodeknockout.com')){ 29 | commentsCollection.update({"_id": ObjectID.createFromHexString(commentId)}, {$set:{approved:false}}, callback); 30 | } 31 | }); 32 | }; 33 | this.getComments = function(domain, skip, limit, callback) { 34 | var comments; 35 | if(domain){ 36 | commentsCollection.find({domain:domain}).sort({createdAt:-1}).skip(skip).limit(limit).toArray(function(err, _comments) { 37 | if(err) { 38 | callback(err); 39 | } else { 40 | comments = _comments; 41 | var userIdMap = {}; 42 | comments.forEach(function(comment) { 43 | comment.createdAt = moment(comment.createdAt).format("MMMMM Do, YYYY h:mm A"); 44 | if(comment.userId && !(userIdMap[comment.userId])) { 45 | userIdMap[comment.userId] = true; 46 | } 47 | }); 48 | //create _id list with converting into ObjectId 49 | var userIdsList = Object.keys(userIdMap).map(function(id) { 50 | return ObjectID.createFromHexString(id); 51 | }); 52 | 53 | var query = {_id: {$in: userIdsList}}; 54 | usersCollection.find(query, {_id: 1, name: 1, avatar: 1}).toArray(afterUsersFound); 55 | } 56 | }); 57 | }else{ 58 | commentsCollection.find({}).sort({createdAt:-1}).skip(skip).limit(limit).toArray(function(err, _comments) { 59 | if(err) { 60 | callback(err); 61 | } else { 62 | comments = _comments; 63 | var userIdMap = {}; 64 | 65 | comments.forEach(function(comment) { 66 | comment.createdAt = moment(comment.createdAt).format("MMMMM Do, YYYY h:mm A"); 67 | if(comment.userId && !(userIdMap[comment.userId])) { 68 | userIdMap[comment.userId] = true; 69 | } 70 | }); 71 | //create _id list with converting into ObjectId 72 | var userIdsList = Object.keys(userIdMap).map(function(id) { 73 | return ObjectID.createFromHexString(id); 74 | }); 75 | 76 | var query = {_id: {$in: userIdsList}}; 77 | usersCollection.find(query, {_id: 1, name: 1, avatar: 1}).toArray(afterUsersFound); 78 | } 79 | }); 80 | } 81 | 82 | 83 | function afterUsersFound(err, users) { 84 | if(err) { 85 | callback(err); 86 | } else { 87 | var userMap = {}; 88 | users.forEach(function(user) { 89 | userMap[user._id.toString()] = user; 90 | }); 91 | callback(null, {comments: comments, users: userMap}); 92 | } 93 | } 94 | }; 95 | 96 | this.getDomains = function (callback) { 97 | 98 | domainsCollection.find().toArray(function(err, results) { 99 | if(err){ 100 | callback(err) 101 | }else{ 102 | callback(null,results) 103 | } 104 | }); 105 | }; 106 | 107 | } 108 | module.exports = function(db) { 109 | return new AdminModel(db); 110 | }; 111 | module.exports.model = AdminModel; -------------------------------------------------------------------------------- /lib/models/auth.js: -------------------------------------------------------------------------------- 1 | var helpers = require('../helpers'); 2 | var uuid = require('uuid'); 3 | 4 | function ClientModel(db) { 5 | var usersCollection = db.collection('users'); 6 | 7 | this.getUser = function(type, typeId, name, avatar, profileLink, callback) { 8 | var query = {type: type, typeId: typeId}; 9 | var userData = {name: name, avatar: avatar, link: profileLink}; 10 | usersCollection.update(query, {$set: userData}, {upsert: true}, function(err) { 11 | if(err) { 12 | callback(err); 13 | } else { 14 | usersCollection.findOne(query, {fields: {_id: 1, name: 1, avatar: 1, link: 1}}, callback); 15 | } 16 | }); 17 | }; 18 | 19 | this.getToken = function(userId, callback) { 20 | var token; 21 | var tokenExpiryTime = 1000 * 3600 * 24 * 10; //10 days 22 | usersCollection.findOne({_id: userId}, function(err, user) { 23 | if(err) { 24 | callback(err); 25 | } else if(user) { 26 | if(user.token && user.tokenExpireAt.getTime() > Date.now()) { 27 | callback(null, user.token); 28 | } else { 29 | user.token = uuid.v4(); 30 | user.tokenExpireAt = new Date(Date.now() + tokenExpiryTime); 31 | usersCollection.save(user, afterUserSaved); 32 | token = user.token; 33 | } 34 | } else { 35 | console.warn('user cannot be null', {_id: userId}); 36 | } 37 | 38 | function afterUserSaved(err) { 39 | if(err) { 40 | callback(err); 41 | } else { 42 | callback(null, token); 43 | } 44 | } 45 | }); 46 | }; 47 | } 48 | 49 | module.exports = function(db) { 50 | return new ClientModel(db); 51 | }; 52 | 53 | module.exports.model = ClientModel; -------------------------------------------------------------------------------- /lib/models/client.js: -------------------------------------------------------------------------------- 1 | var helpers = require('../helpers'); 2 | var ObjectID = require('mongodb').ObjectID; 3 | 4 | function ClientModel(db) { 5 | var commentsCollection = db.collection('comments'); 6 | var usersCollection = db.collection('users'); 7 | var domainsCollection = db.collection('domains'); 8 | 9 | this.getInitData = function(url, callback) { 10 | var comments; 11 | commentsCollection.find({url: url}).toArray(function(err, _comments) { 12 | if(err) { 13 | callback(err); 14 | } else { 15 | comments = _comments; 16 | var userIdMap = {}; 17 | comments.forEach(function(comment) { 18 | if(comment.userId && !(userIdMap[comment.userId])) { 19 | userIdMap[comment.userId] = true; 20 | } 21 | }); 22 | 23 | //create _id list with converting into ObjectId 24 | var userIdsList = Object.keys(userIdMap).map(function(id) { 25 | return ObjectID.createFromHexString(id); 26 | }); 27 | 28 | var query = {_id: {$in: userIdsList}}; 29 | usersCollection.find(query, {_id: 1, name: 1, avatar: 1, link: 1}).toArray(afterUsersFound); 30 | } 31 | }); 32 | 33 | function afterUsersFound(err, users) { 34 | if(err) { 35 | callback(err); 36 | } else { 37 | var userMap = {}; 38 | users.forEach(function(user) { 39 | userMap[user._id.toString()] = user; 40 | }); 41 | 42 | callback(null, {comments: comments, users: userMap}); 43 | } 44 | } 45 | }; 46 | 47 | this.addComment = function(url, userId, parentId, message, htmlMessage, callback) { 48 | var domain = helpers.urlToDomain(url); 49 | commentsCollection.insert({ 50 | appId: null, 51 | domain: domain, 52 | url: url, 53 | urlHash: helpers.urlToHash(url), 54 | userId: (userId)? userId.toString(): null, //not in objectId mode 55 | message: htmlMessage, 56 | markdown: message, 57 | createdAt: new Date(), 58 | updatedAt: new Date(), 59 | votes: 0, 60 | parentId: parentId, 61 | approved: true 62 | }, afterAddComment); 63 | 64 | function afterAddComment(err, insertResult){ 65 | console.log() 66 | if(err){ 67 | callback(err); 68 | } else { 69 | domainsCollection.update( {name:domain}, 70 | {$set:{name:domain}},{upsert:true}, function(err){ 71 | if(err){ 72 | callback(err); 73 | }else{ 74 | callback(null, insertResult); 75 | } 76 | }); 77 | } 78 | } 79 | 80 | }; 81 | 82 | this.deleteComment = function(id, callback) { 83 | var query = { 84 | _id: ObjectID.createFromHexString(id) 85 | }; 86 | 87 | commentsCollection.remove(query, callback); 88 | }; 89 | 90 | this.getComment = function(id, callback) { 91 | var comment; 92 | commentsCollection.findOne({_id: id}, function(err, _comment) { 93 | if(err) { 94 | callback(err); 95 | } else { 96 | comment = _comment; 97 | if(comment.userId) { 98 | //get the url 99 | var query = {_id: ObjectID.createFromHexString(comment.userId)}; 100 | usersCollection.findOne(query, afterUsersFound); 101 | } else { 102 | callback(null, comment); 103 | } 104 | } 105 | }); 106 | 107 | function afterUsersFound(err, user) { 108 | if(err) { 109 | callback(err); 110 | } else { 111 | comment.user = user; 112 | callback(null, comment); 113 | } 114 | } 115 | }; 116 | } 117 | module.exports = function(db) { 118 | return new ClientModel(db); 119 | }; 120 | module.exports.model = ClientModel; -------------------------------------------------------------------------------- /lib/models/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var deepExtend = require('deep-extend'); 5 | 6 | function ConfigModel(db) { 7 | 8 | var configCollection = db.collection('config'); 9 | var self = this; 10 | var Config= {}; 11 | 12 | this.init= function(defaultConfigFilename){ 13 | self.getConfigDB(function(err,res){ 14 | if (res==null) { 15 | self.getConfigFile('config.json',function(res){ 16 | Config = res; 17 | self.emit('change', res); 18 | }); 19 | }else{ 20 | details = res.details; 21 | Config = details; 22 | self.emit('change', details); 23 | } 24 | }) 25 | } 26 | 27 | this.getConfigFile = function(defaultConfigFilename,callback){ 28 | fs.exists(defaultConfigFilename, function(exists) { 29 | if (exists) { 30 | fs.readFile(defaultConfigFilename, function (err, data) { 31 | details=JSON.parse(data.toString()); 32 | callback(details); 33 | }); 34 | } else { 35 | console.warn('Config not found'); 36 | } 37 | }); 38 | } 39 | 40 | this.getConfigDB = function(callback){ 41 | configCollection.findOne({_id: "config"}, function(err,res){ 42 | callback(err,res); 43 | }); 44 | } 45 | 46 | // @use - 47 | // var mock = {passport:{ facebook:{clientID:"",clientSecret:"",callbackURL:""}}}; 48 | // self.savePartial(Config,mock ); 49 | 50 | this.savePartial = function (a,b){ 51 | deepExtend(a,b); 52 | self.save(a); 53 | return a; 54 | } 55 | 56 | this.save = function (details){ 57 | configCollection.save({ _id: "config",details:details }, function(err,res){ 58 | self.emit('change', details); 59 | Config = details; 60 | }); 61 | } 62 | 63 | this.get = function(){ 64 | return Config; 65 | } 66 | } 67 | 68 | util.inherits(ConfigModel, EventEmitter); 69 | 70 | module.exports = function(db) { 71 | return new ConfigModel(db); 72 | }; 73 | 74 | module.exports.model = ConfigModel; -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(db) { 2 | return { 3 | client: require('./client')(db), 4 | auth: require('./auth')(db), 5 | admin: require('./admin')(db), 6 | config: require('./config')(db) 7 | } 8 | }; -------------------------------------------------------------------------------- /lib/passport/facebook.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | var passport = require('passport') 3 | var FacebookStrategy = require('passport-facebook').Strategy; 4 | 5 | var authModel = env.models.auth; 6 | var config = env.models.config; 7 | 8 | config.on('change', function(data) { 9 | passport.unuse('facebook'); 10 | passport.use(new FacebookStrategy({ 11 | clientID: data.passport.facebook.clientID, 12 | clientSecret: data.passport.facebook.clientSecret, 13 | callbackURL: config.get().url+"/auth/facebook/callback" 14 | }, 15 | onStrategy 16 | )); 17 | }); 18 | 19 | function onStrategy(accessToken, refreshToken, profile, done) { 20 | var user; 21 | var avatarUrl = "http://graph.facebook.com/" + profile.id + "/picture"; 22 | var profileUrl = profile.profileUrl; 23 | 24 | authModel.getUser('facebook', profile.id, profile.displayName, avatarUrl, profileUrl, function(err, _user) { 25 | if(err) { 26 | done(err); 27 | } else { 28 | user = _user; 29 | authModel.getToken(user._id, withToken); 30 | } 31 | }); 32 | 33 | function withToken(err, token) { 34 | if(err) { 35 | done(err); 36 | } else { 37 | user.userToken = token; 38 | done(null, user); 39 | } 40 | } 41 | } 42 | 43 | passport.serializeUser(function(user, done) { 44 | done(null, user); 45 | }); 46 | 47 | passport.deserializeUser(function(user, done) { 48 | done(null, user); 49 | }); 50 | 51 | app.get('/auth/facebook', passport.authenticate('facebook')); 52 | 53 | app.get('/auth/facebook/callback', 54 | passport.authenticate('facebook', { 55 | failureRedirect: '/login', 56 | session:false 57 | }), 58 | function(req, res) { 59 | res.render('browser_send.ejs', {user: req.user}); 60 | }); 61 | } -------------------------------------------------------------------------------- /lib/passport/gmail.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db,env) { 2 | var passport = require('passport') 3 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 4 | 5 | var authModel = env.models.auth; 6 | var config = env.models.config; 7 | 8 | config.on('change', function(data) { 9 | passport.unuse('google'); 10 | passport.use(new GoogleStrategy({ 11 | clientID: data.passport.google.clientID, 12 | clientSecret: data.passport.google.clientSecret, 13 | callbackURL: config.get().url +"/auth/google/callback" 14 | }, 15 | onStrategy 16 | )); 17 | }); 18 | 19 | function onStrategy(accessToken, refreshToken, profile, done) { 20 | var avatarUrl = "http://profiles.google.com/s2/photos/profile/" + profile.id + "?sz=50"; 21 | var profileUrl = profile._json.link; 22 | 23 | authModel.getUser('google', profile.id, profile.displayName, avatarUrl, profileUrl, function(err, _user) { 24 | if(err) { 25 | done(err); 26 | } else { 27 | user = _user; 28 | authModel.getToken(user._id, withToken); 29 | } 30 | }); 31 | 32 | function withToken(err, token) { 33 | if(err) { 34 | done(err); 35 | } else { 36 | user.userToken = token; 37 | done(null, user); 38 | } 39 | } 40 | } 41 | 42 | passport.serializeUser(function(user, done) { 43 | done(null, user); 44 | }); 45 | 46 | passport.deserializeUser(function(user, done) { 47 | done(null, user); 48 | }); 49 | 50 | app.get('/auth/google', passport.authenticate('google', { scope: 'https://www.googleapis.com/auth/userinfo.profile' })); 51 | 52 | app.get('/auth/google/callback', 53 | passport.authenticate('google', { failureRedirect: '/login' }), 54 | function(req, res) { 55 | res.render('browser_send.ejs', {user: req.user}); 56 | }); 57 | 58 | } -------------------------------------------------------------------------------- /lib/passport/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | require('./facebook')(app, db, env); 3 | require('./twitter')(app, db, env); 4 | require('./gmail')(app, db, env); 5 | require('./persona')(app, db, env); 6 | require('./local')(app, db, env); 7 | }; -------------------------------------------------------------------------------- /lib/passport/local.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | var passport = require('passport'); 3 | // var bcrypt = require('bcrypt'); 4 | var LocalStrategy = require('passport-local').Strategy; 5 | var configCollection = db.collection('config'); 6 | 7 | passport.use(new LocalStrategy( 8 | function(username, password, done) { 9 | configCollection.findOne({_id: "admin"}, function(err,configs){ 10 | if (configs.admin && configs.admin.password && configs.admin.username){ 11 | done(null,{username:username}) 12 | // bcrypt.compare(password, configs.admin.password, function(err, res) { 13 | // if(err){ 14 | // done(err, false) 15 | // } 16 | // if (configs.admin && (username == configs.admin.username)) { 17 | // done(null,{username:username}) 18 | // }else{ 19 | // done(null, false); 20 | // } 21 | // }); 22 | } 23 | }); 24 | } 25 | )); 26 | 27 | app.get('/admin/login/submit', 28 | passport.authenticate('local', { 29 | failureRedirect: '/admin/login' 30 | }), 31 | function(req, res) { 32 | res.redirect('/admin/moderate'); 33 | }); 34 | 35 | 36 | } -------------------------------------------------------------------------------- /lib/passport/persona.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | var passport = require('passport') 3 | var PersonaStrategy = require('passport-persona').Strategy; 4 | 5 | var authModel = env.models.auth; 6 | 7 | passport.use(new PersonaStrategy({ 8 | audience: 'http://localhost:8000' 9 | }, 10 | function(email, done) { 11 | console.log("asdasdasd",email, done); 12 | } 13 | 14 | /*function(email, done) { 15 | var user; 16 | var token = ''; 17 | console.log(profile); 18 | var avatarUrl = profile.id ; 19 | authModel.getUser('persona', profile.id, profile.displayName, avatarUrl, function(err, _user) { 20 | if(err) { 21 | done(err); 22 | } else { 23 | user = _user; 24 | authModel.getToken(user._id, withToken); 25 | } 26 | }); 27 | 28 | function withToken(err, token) { 29 | if(err) { 30 | done(err); 31 | } else { 32 | user.userToken = token; 33 | done(null, user); 34 | } 35 | } 36 | }*/ 37 | )); 38 | 39 | passport.serializeUser(function(user, done) { 40 | done(null, user); 41 | }); 42 | 43 | passport.deserializeUser(function(user, done) { 44 | done(null, user); 45 | }); 46 | 47 | app.get('/auth/persona', passport.authenticate('persona')); 48 | 49 | app.get('/auth/persona/callback', 50 | passport.authenticate('persona', { 51 | failureRedirect: '/login', 52 | session:false 53 | }), 54 | function(req, res) { 55 | res.render('browser_send.ejs', {user: req.user}); 56 | }); 57 | } -------------------------------------------------------------------------------- /lib/passport/twitter.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | var passport = require('passport') 3 | var TwitterStrategy = require('passport-twitter').Strategy; 4 | 5 | var authModel = env.models.auth; 6 | 7 | var config = env.models.config; 8 | 9 | config.on('change', function(data) { 10 | passport.unuse('twitter'); 11 | passport.use(new TwitterStrategy({ 12 | consumerKey: data.passport.twitter.consumerKey, 13 | consumerSecret: data.passport.twitter.consumerSecret, 14 | callbackURL: config.get().url +"/auth/twitter/callback" 15 | }, 16 | onStrategy 17 | )); 18 | }); 19 | 20 | function onStrategy(accessToken, refreshToken, profile, done) { 21 | var user; 22 | var avatarUrl =profile.photos[0].value; 23 | var profileUrl = "https://twitter.com/" + profile.username; 24 | 25 | authModel.getUser('twitter', profile.id, profile.displayName, avatarUrl, profileUrl, function(err, _user) { 26 | if(err) { 27 | done(err); 28 | } else { 29 | user = _user; 30 | authModel.getToken(user._id, withToken); 31 | } 32 | }); 33 | 34 | function withToken(err, token) { 35 | if(err) { 36 | done(err); 37 | } else { 38 | user.userToken = token; 39 | done(null, user); 40 | } 41 | } 42 | } 43 | 44 | passport.serializeUser(function(user, done) { 45 | done(null, user); 46 | }); 47 | 48 | passport.deserializeUser(function(user, done) { 49 | done(null, user); 50 | }); 51 | 52 | app.get('/auth/twitter', passport.authenticate('twitter')); 53 | 54 | app.get('/auth/twitter/callback', 55 | passport.authenticate('twitter', { 56 | failureRedirect: '/login', 57 | session:false 58 | }), 59 | function(req, res) { 60 | res.render('browser_send.ejs', {user: req.user}); 61 | }); 62 | } -------------------------------------------------------------------------------- /lib/routes/admin.js: -------------------------------------------------------------------------------- 1 | var urlModule = require('url'); 2 | module.exports = function(app, db, env) { 3 | var adminModel = env.models.admin; 4 | var configModal = env.models.config; 5 | 6 | var configCollection = db.collection('config'); 7 | 8 | app.get('/admin', function(req, res) { 9 | res.redirect('/admin/moderate') 10 | }); 11 | 12 | app.get('/admin/login', function(req, res) { 13 | 14 | configCollection.findOne({_id: "admin"}, function(err,configs){ 15 | if (!(configs && configs.admin && configs.admin.password && configs.admin.username)){ 16 | res.redirect('/admin/createadmin'); 17 | } 18 | if(req.user){ 19 | res.redirect('/admin/moderate') 20 | }else{ 21 | res.render('admin/login',{}); 22 | } 23 | }); 24 | }); 25 | app.get('/admin/createadmin', function(req, res) { 26 | configCollection.findOne({_id: "admin"}, function(err,configs){ 27 | if ((configs && configs.admin && configs.admin.password && configs.admin.username)){ 28 | res.redirect('/admin/login'); 29 | }else{ 30 | res.render('admin/createadmin',{}); 31 | } 32 | }); 33 | }); 34 | 35 | app.post('/admin/createadmin', function(req, res) { 36 | var username = req.body['username']; 37 | var password = req.body['password']; 38 | 39 | //TODO: use bcrypt and fix the docker installation issue 40 | configCollection.update({_id: "admin"}, 41 | {$set:{"admin.username":username, "admin.password":password}}, 42 | {upsert:true}, 43 | function(err){ 44 | if(!err){ 45 | res.redirect('/admin/login') 46 | } 47 | }); 48 | // bcrypt.genSalt(10, function(err, salt) { 49 | // bcrypt.hash(password, salt, function(err, hashpassword) { 50 | // }); 51 | // }); 52 | }); 53 | 54 | app.post('/admin/domains', function(req, res) { 55 | req.session.domain = req.body.domainId; 56 | res.json('success'); 57 | }); 58 | 59 | app.get('/admin/moderate', function(req, res) { 60 | if(!req.user){ 61 | res.redirect('/admin/login'); 62 | } 63 | 64 | var commentsPerPage = 10; 65 | var queryParams = urlModule.parse(req.url, true).query; 66 | var pageNum = parseInt(queryParams.page); 67 | 68 | var selectedDomain = req.session.domain; 69 | if (isNaN(pageNum)||!pageNum||pageNum < 0) { 70 | pageNum = 1; 71 | } 72 | var skip = commentsPerPage * (pageNum - 1) 73 | 74 | adminModel.getDomains(afterDomainsReceived) 75 | 76 | function afterDomainsReceived(err,domains) { 77 | var domainName; 78 | 79 | if(selectedDomain){ 80 | for (var i = 0; i < domains.length; i++) { 81 | if(domains[i]._id == selectedDomain ){ 82 | domainName = domains[i].name; 83 | } 84 | }; 85 | } 86 | 87 | adminModel.getComments(domainName, skip, commentsPerPage, afterCommentsReceived); 88 | function afterCommentsReceived(err, data) { 89 | 90 | res.render('admin/moderate', { 91 | comments: data.comments, 92 | users: data.users, 93 | selectedDomain:{id:selectedDomain,name:domainName}, 94 | path: 'moderate', 95 | domains: domains, 96 | nextPage: pageNum + 1, 97 | prevPage: pageNum - 1 98 | }); 99 | } 100 | } 101 | }); 102 | 103 | app.get('/admin/settings', function(req, res) { 104 | if (!req.user){ 105 | res.redirect('/admin/login'); 106 | }else{ 107 | res.render('admin/settings',{path:'settings',settings:configModal.get(),user:req.user}); 108 | } 109 | if (req.body.domainId) { 110 | req.session.domain = req.body.domainId; 111 | } 112 | // if (req.user && req.username == 'demo') { 113 | // res.render('admin/settings',{path:'settings',username:'demo'}); 114 | // }else{ 115 | // res.render('admin/settings',{path:'settings',settings:configModal.get()}); 116 | // } 117 | 118 | 119 | }); 120 | 121 | app.post('/admin/settings/submit',function(req,res){ 122 | 123 | if (req.user && req.user.username=='demo') { 124 | res.json('Demo users cannot change settings'); 125 | return; 126 | } 127 | 128 | var twConsumerKey = req.body['tw-consumerkey']; 129 | var twConsumerSecret = req.body['tw-consumer-secret']; 130 | var fbClientID = req.body['fb-client-id']; 131 | var fbClientSecret = req.body['fb-client-secret']; 132 | var googleClientID = req.body['google-client-id']; 133 | var googleClientSecret = req.body['google-client-secret']; 134 | var rootUrl = req.body['root-url']; 135 | 136 | // console.log(twConsumerKey,twConsumerSecret,fbClientID,fbClientSecret,googleClientID,googleClientSecret) 137 | if(rootUrl && twConsumerKey && twConsumerSecret && fbClientID && fbClientSecret && googleClientID && googleClientSecret){ 138 | details = {}; 139 | 140 | details.passport = { 141 | "facebook":{ 142 | "clientID": fbClientID.trim(), 143 | "clientSecret": fbClientSecret.trim() 144 | }, 145 | "google":{ 146 | "clientID": googleClientID.trim(), 147 | "clientSecret": googleClientSecret.trim() 148 | }, 149 | "twitter":{ 150 | "consumerKey": twConsumerKey.trim(), 151 | "consumerSecret": twConsumerSecret.trim() 152 | } 153 | }; 154 | details.url = rootUrl; 155 | configModal.save(details); 156 | res.json('success'); 157 | }else{ 158 | res.json('failed'); 159 | } 160 | }); 161 | 162 | } -------------------------------------------------------------------------------- /lib/routes/comments.js: -------------------------------------------------------------------------------- 1 | var urlModule = require('url'); 2 | var helpers = require('../helpers'); 3 | var marked = require('marked'); 4 | 5 | //configure markdowm 6 | marked.setOptions({ 7 | sanitize: true, 8 | tables: false, 9 | breaks: true 10 | }); 11 | 12 | module.exports = function(app, db, env) { 13 | var clientModel = env.models.client; 14 | var adminModel = env.models.admin; 15 | 16 | app.get('/comments', function(req, res) { 17 | res.send('These are the comments'); 18 | }); 19 | 20 | app.get('/comments/create', function(req, res) { 21 | var queryParams = urlModule.parse(req.url, true).query; 22 | var message = queryParams.message; 23 | var parentId = queryParams.parentId; 24 | var apiToken = queryParams.apiToken; 25 | var userId = queryParams.userId; 26 | var userToken = queryParams.userToken; 27 | var url = queryParams.url; 28 | var htmlMessage = marked(message); 29 | 30 | //TODO: valicate apiToken and the userToken(or if supported for anonymous) 31 | 32 | var commentId; 33 | 34 | if(url) { 35 | clientModel.addComment(url, userId, parentId, message, htmlMessage, afterAdded); 36 | } else { 37 | res.send(400, 'do not allow direct requests'); 38 | } 39 | 40 | function afterAdded(err, comments) { 41 | if(err) { 42 | console.error('error adding comment', {error: err.message, url: url}); 43 | res.jsonp({error: 'internal-error'}); 44 | } else { 45 | var commentId = comments[0]._id; 46 | res.jsonp({_id: commentId}); 47 | //send to the socket 48 | clientModel.getComment(commentId, afterCommentFetched); 49 | } 50 | } 51 | 52 | function afterCommentFetched(err, comment) { 53 | if(err) { 54 | console.error('error while getting the comment:', {err: err.message, _id: commentId}); 55 | } else { 56 | var urlHash = helpers.urlToHash(url); 57 | env.socketManager.commentsAdded(urlHash, [comment]); 58 | } 59 | } 60 | }); 61 | 62 | app.get('/comments/delete', function(req, res) { 63 | 64 | var queryParams = urlModule.parse(req.url, true).query; 65 | var apiToken = queryParams.apiToken; 66 | var commentId = queryParams.id; 67 | var url = queryParams.url; 68 | 69 | //todo: validate api tokens 70 | 71 | clientModel.deleteComment(commentId, function(err) { 72 | if(err) { 73 | console.error('error deleting comment', {id: commentId, error: err.message}); 74 | res.jsonp({error: 'internal-error'}); 75 | } else { 76 | res.jsonp({_id: commentId}); 77 | var urlHash = helpers.urlToHash(url); 78 | env.socketManager.commentsDeleted(urlHash, [commentId]); 79 | } 80 | }); 81 | }); 82 | 83 | //jsonp comments allowing client to get the comment via jsonp 84 | app.get('/comments/init', function(req, res) { 85 | var queryParams = urlModule.parse(req.url, true).query; 86 | var apiToken = queryParams.apiToken; 87 | var commentId = queryParams.id; 88 | var url = queryParams.url; 89 | 90 | //TODO: validate apiToken 91 | 92 | if(url) { 93 | clientModel.getInitData(url, function(err, data) { 94 | if(err) { 95 | console.error('error when getting inital comments', {apiToken: apiToken, url: url, error: err.message}); 96 | res.jsonp({error: 'internal-error'}); 97 | } else { 98 | res.jsonp(data); 99 | } 100 | }); 101 | } else { 102 | res.send(400, 'do not allow direct requests'); 103 | } 104 | }); 105 | 106 | 107 | app.post('/comments/update',function(req , res){ 108 | 109 | var action = req.body.action; 110 | var commentId = req.body.commentId; 111 | 112 | if (action=='delete' && commentId ) { 113 | adminModel.deleteComment(commentId, afterDeleted); 114 | } 115 | 116 | function afterDeleted(err, removedCount) { 117 | if(err) { 118 | console.error('error deleting comment', {error: err.message, url: url}); 119 | res.json({error: 'internal-error'}); 120 | } else { 121 | res.json({removedCount:removedCount}); 122 | } 123 | } 124 | 125 | if (action=='approve' && commentId ) { 126 | adminModel.approveComment(commentId, afterUpdated); 127 | } 128 | 129 | if (action=='reject' && commentId ) { 130 | adminModel.rejectComment(commentId, afterUpdated); 131 | } 132 | 133 | function afterUpdated(err , updatedCount){ 134 | if(err) { 135 | console.error('error updating comment', {error: err.message, url: url}); 136 | res.json({error: 'internal-error'}); 137 | } else { 138 | res.json({updatedCount:updatedCount}); 139 | } 140 | } 141 | }); 142 | } -------------------------------------------------------------------------------- /lib/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | require('./comments')(app, db, env); 3 | require('./admin')(app, db, env); 4 | require('./public')(app, db, env); 5 | }; -------------------------------------------------------------------------------- /lib/routes/public.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, db, env) { 2 | app.get('/', function(req, res) { 3 | res.render('index.ejs'); 4 | }); 5 | } -------------------------------------------------------------------------------- /lib/socketManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Usage 3 | ----- 4 | 5 | var socketManager = new SocketManager(); 6 | socketManager.addSocket(, socket); 7 | socketManager.removeSocket(, socket); 8 | 9 | var comments = {inserted: [], updated: []}; 10 | socketManager.sendComments(hash, comments); 11 | 12 | var users = {"id1": {}, "id2": {}}; 13 | socketManager.sendUsers(hash, users); 14 | */ 15 | 16 | var SocketManager = function() { 17 | this.sockets = {}; 18 | }; 19 | 20 | SocketManager.prototype.addSocket = function(hash, socket) { 21 | this._ensureHash(hash); 22 | this.sockets[hash].push(socket); 23 | }; 24 | 25 | SocketManager.prototype.removeSocket = function(hash, socket) { 26 | this._ensureHash(hash); 27 | var index = this.sockets[hash].indexOf(socket); 28 | if(index >= 0) { 29 | this.sockets[hash].splice(index, 1); 30 | } 31 | }; 32 | 33 | SocketManager.prototype.commentsAdded = function(hash, comments) { 34 | this.send(hash, {event: 'commentsAdded', data: comments}); 35 | }; 36 | 37 | SocketManager.prototype.commentsDeleted = function(hash, idList) { 38 | this.send(hash, {event: 'commentsDeleted', data: idList}); 39 | }; 40 | 41 | SocketManager.prototype.send = function(hash, jsonData) { 42 | this._ensureHash(hash); 43 | var sockets = this.sockets[hash]; 44 | for(var lc=0; lcPaste the Open Comment Box HTML Below

'; 67 | } // end sandbox_general_options_callback 68 | 69 | function sandbox_toggle_header_callback($args) { 70 | 71 | $html = ''; 72 | 73 | echo $html; 74 | 75 | } // end sandbox_toggle_header_callback 76 | 77 | 78 | add_filter( 'the_content' , custom_shortcode); 79 | 80 | function custom_shortcode($content) { 81 | return get_option('show_header'); 82 | } 83 | 84 | ?> -------------------------------------------------------------------------------- /plugins/wordpress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/plugins/wordpress.zip -------------------------------------------------------------------------------- /public/css/admin.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | body, div, p, a, h1, h2, h3, h4 { 5 | font-family: 'Source Sans Pro', sans-serif; 6 | } 7 | .navbar-brand { 8 | font-family: 'Source Sans Pro', sans-serif; 9 | font-weight: 900; 10 | } 11 | .truncate { 12 | max-height: 60px; 13 | text-overflow: ellipsis; 14 | overflow: hidden; 15 | whitespace: no-wrap; 16 | } 17 | 18 | .form-signin 19 | { 20 | max-width: 330px; 21 | padding: 15px; 22 | margin: 0 auto; 23 | } 24 | .form-signin .form-signin-heading, .form-signin .checkbox 25 | { 26 | margin-bottom: 10px; 27 | } 28 | .form-signin .checkbox 29 | { 30 | font-weight: normal; 31 | } 32 | .form-signin .form-control 33 | { 34 | position: relative; 35 | font-size: 16px; 36 | height: auto; 37 | padding: 10px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | .form-signin .form-control:focus 43 | { 44 | z-index: 2; 45 | } 46 | .form-signin input[type="text"] 47 | { 48 | margin-bottom: -1px; 49 | border-bottom-left-radius: 0; 50 | border-bottom-right-radius: 0; 51 | } 52 | .form-signin input[type="password"] 53 | { 54 | margin-bottom: 10px; 55 | border-top-left-radius: 0; 56 | border-top-right-radius: 0; 57 | } 58 | .account-wall 59 | { 60 | margin-top: 20px; 61 | padding: 40px 0px 20px 0px; 62 | background-color: #f7f7f7; 63 | -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 64 | -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 65 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 66 | } 67 | .login-title 68 | { 69 | color: #555; 70 | font-size: 18px; 71 | font-weight: 400; 72 | display: block; 73 | } 74 | .profile-img 75 | { 76 | width: 96px; 77 | height: 96px; 78 | margin: 0 auto 10px; 79 | display: block; 80 | -moz-border-radius: 50%; 81 | -webkit-border-radius: 50%; 82 | border-radius: 50%; 83 | } 84 | .need-help 85 | { 86 | margin-top: 10px; 87 | } 88 | .new-account 89 | { 90 | display: block; 91 | margin-top: 10px; 92 | } 93 | textarea{ 94 | background-color: #ffffff; 95 | border: 1px solid #cccccc; 96 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 97 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 98 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 99 | -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; 100 | -moz-transition: border linear 0.2s, box-shadow linear 0.2s; 101 | -o-transition: border linear 0.2s, box-shadow linear 0.2s; 102 | padding: 4px 6px; 103 | margin-bottom: 10px; 104 | font-size: 14px; 105 | line-height: 20px; 106 | color: #555555; 107 | vertical-align: middle; 108 | -webkit-border-radius: 4px; 109 | -moz-border-radius: 4px; 110 | border-radius: 4px; 111 | } -------------------------------------------------------------------------------- /public/css/public.css: -------------------------------------------------------------------------------- 1 | body, div, p, a, h1, h2, h3, h4 { 2 | font-family: 'Source Sans Pro', sans-serif; 3 | } 4 | 5 | img { 6 | width: 90% !important; 7 | padding: 5%; 8 | height:auto; 9 | display:block; 10 | } 11 | 12 | .navbar-brand { 13 | font-family: 'Source Sans Pro', sans-serif; 14 | font-weight: 900; 15 | } 16 | 17 | .intro-section h1 { 18 | font-family: 'Source Sans Pro', sans-serif; 19 | font-weight: 900; 20 | font-size: 70px; 21 | color: #525252; 22 | } 23 | 24 | .intro-section h2 { 25 | font-family: 'Source Sans Pro', sans-serif; 26 | font-weight: 700; 27 | font-size: 30px; 28 | margin: 0px; 29 | color: #444; 30 | } 31 | 32 | .intro-section h3 { 33 | font-family: 'Source Sans Pro', sans-serif; 34 | font-weight: 400; 35 | font-size: 25px; 36 | margin-top: 20px; 37 | border-top: 1px dashed #ccc; 38 | padding-top: 15px; 39 | color: #444; 40 | } 41 | 42 | .intro-section h4 { 43 | font-family: 'Source Sans Pro', sans-serif; 44 | margin: 0px; 45 | font-weight: 700; 46 | color: #c73b18; 47 | } 48 | 49 | .nko-vote { 50 | margin-top: 25px; 51 | } 52 | 53 | .comments-box { 54 | text-align: center; 55 | } 56 | 57 | .comments-box h4 { 58 | margin-bottom: 0px; 59 | } 60 | 61 | .comments-box .powered-by { 62 | font-size: 13px; 63 | } 64 | 65 | .video-intro { 66 | text-align: center; 67 | } 68 | 69 | .video-intro iframe{ 70 | margin: auto; 71 | } 72 | 73 | .ocb-code { 74 | -webkit-box-sizing: border-box; 75 | -moz-box-sizing: border-box; 76 | box-sizing: border-box; 77 | width: 100%; 78 | height: 102px; 79 | font-family: 'Monaco', ''; 80 | border: 1px solid #ccc; 81 | padding: 10px; 82 | margin-top: 13px; 83 | background-color: #eee; 84 | color: #777; 85 | } 86 | 87 | .moderate-link { 88 | margin-top: 20px; 89 | } 90 | 91 | .details { 92 | margin-top: 30px; 93 | } 94 | 95 | .details h3{ 96 | margin-top: 30px; 97 | font-weight: 700; 98 | } 99 | 100 | footer { 101 | border-top: 1px solid #ddd; 102 | padding: 20px; 103 | margin: 50px; 104 | text-align: center; 105 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/favicon.ico -------------------------------------------------------------------------------- /public/images/docker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/docker-logo.png -------------------------------------------------------------------------------- /public/images/ghost-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/ghost-logo.png -------------------------------------------------------------------------------- /public/images/heroku-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/heroku-logo.png -------------------------------------------------------------------------------- /public/images/modulus-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/modulus-logo.jpg -------------------------------------------------------------------------------- /public/images/nodejitsu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/nodejitsu-logo.png -------------------------------------------------------------------------------- /public/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/user.png -------------------------------------------------------------------------------- /public/images/wordpress-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/images/wordpress-logo.png -------------------------------------------------------------------------------- /public/js/admin.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('.action-btn').click(function(event) { 3 | if (this.id && !$(this).hasClass('nko-comment')) { 4 | var propertiesArray = this.id.split("-"); 5 | var action = propertiesArray[0]; 6 | var commentId = propertiesArray[1]; 7 | 8 | if (!(action == 'approve' || action == 'delete' || action=='reject')) { 9 | return; 10 | } 11 | if (action == 'delete' && commentId) { 12 | bootbox.dialog({ 13 | message: "Want to delete this comment ? Are you sure ?", 14 | title: "Delete Comment Confirm", 15 | buttons: { 16 | confirm: { 17 | label: "Delete", 18 | className: "btn-danger", 19 | callback: function() { 20 | $.ajax({ 21 | type: "POST", 22 | url: "/comments/update/", 23 | data: { 24 | commentId: commentId, 25 | action: action 26 | } 27 | }) 28 | .done(function(msg) { 29 | if (action == 'delete' && (msg.removedCount > 0)) { 30 | $("#" + "delete-" + commentId).parent().parent().parent().remove(); 31 | growlAlert.success("Comment deleted "); 32 | } 33 | }); 34 | } 35 | }, 36 | success: { 37 | label: "Cancel", 38 | className: "btn-default", 39 | callback: function() {} 40 | } 41 | } 42 | }); 43 | } 44 | 45 | if ( (action == 'approve'|| action=='reject') && commentId ) { 46 | $.ajax({ 47 | type: "POST", 48 | url: "/comments/update/", 49 | data: { 50 | commentId: commentId, 51 | action: action 52 | } 53 | }) 54 | .done(function(result) { 55 | if (action == 'approve') { 56 | if (result.updatedCount) { 57 | growlAlert.success("Comment Approved"); 58 | } 59 | } 60 | 61 | if (action == 'reject') { 62 | if (result.updatedCount) { 63 | growlAlert.success("Comment Rejected"); 64 | } 65 | } 66 | setTimeout(function(){ 67 | location.reload(); 68 | },3000); 69 | }); 70 | } 71 | }else{ 72 | growlAlert.error("You cannot moderate nodeknockout comments"); 73 | } 74 | }); 75 | 76 | $('.select-domain').click(function(event) { 77 | if (this.id) { 78 | var propertiesArray = this.id.split("-"); 79 | var action = propertiesArray[0]; 80 | var domainId = propertiesArray[1]; 81 | 82 | if (action == 'domain' && domainId) { 83 | $.ajax({ 84 | type: "POST", 85 | url: "/admin/domains/", 86 | data: { 87 | domainId: domainId 88 | } 89 | }) 90 | .done(function(result) { 91 | window.location = "/admin/moderate/"; 92 | }); 93 | } 94 | } 95 | }); 96 | 97 | $('#get-code').click(function(event){ 98 | event.preventDefault(); 99 | var domain = location.href.replace(/\/admin.*/, ''); 100 | bootbox.dialog({ 101 | message: '', 102 | title: "Embed Open Comment Box", 103 | buttons: { 104 | success: { 105 | label: "Close", 106 | className: "btn-primary", 107 | callback: function() { 108 | 109 | } 110 | } 111 | } 112 | }); 113 | }); 114 | 115 | }); -------------------------------------------------------------------------------- /public/js/settings.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | $('#frm-settings').submit(function(event) { 4 | $.ajax({ 5 | type: "POST", 6 | url: '/admin/settings/submit', 7 | data: $("#frm-settings").serialize(), 8 | success: function(result) { 9 | if (result == 'success') { 10 | growlAlert.success(" Settings Saved"); 11 | setTimeout(function() { 12 | location.reload(); 13 | }, 2000); 14 | } else { 15 | growlAlert.error(result); 16 | } 17 | } 18 | }); 19 | event.preventDefault(); 20 | 21 | }); 22 | }); -------------------------------------------------------------------------------- /public/lib/toast.js: -------------------------------------------------------------------------------- 1 | function showAlert(messageType,message){ 2 | $.bootstrapGrowl(message, { type: messageType }); 3 | } 4 | growlAlert = {}; 5 | growlAlert.error = function (message){ 6 | showAlert('danger',message); 7 | } 8 | growlAlert.success = function (message){ 9 | showAlert('success',message); 10 | } 11 | growlAlert.info = function (message){ 12 | showAlert('info',message); 13 | } -------------------------------------------------------------------------------- /public/lib/vendor/bootstrap/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | .btn-default, 10 | .btn-primary, 11 | .btn-success, 12 | .btn-info, 13 | .btn-warning, 14 | .btn-danger { 15 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); 16 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 17 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 18 | } 19 | 20 | .btn-default:active, 21 | .btn-primary:active, 22 | .btn-success:active, 23 | .btn-info:active, 24 | .btn-warning:active, 25 | .btn-danger:active, 26 | .btn-default.active, 27 | .btn-primary.active, 28 | .btn-success.active, 29 | .btn-info.active, 30 | .btn-warning.active, 31 | .btn-danger.active { 32 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 33 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 34 | } 35 | 36 | .btn:active, 37 | .btn.active { 38 | background-image: none; 39 | } 40 | 41 | .btn-default { 42 | text-shadow: 0 1px 0 #fff; 43 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#e0e0e0)); 44 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); 45 | background-image: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); 46 | background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%); 47 | background-repeat: repeat-x; 48 | border-color: #dbdbdb; 49 | border-color: #ccc; 50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 52 | } 53 | 54 | .btn-default:hover, 55 | .btn-default:focus { 56 | background-color: #e0e0e0; 57 | background-position: 0 -15px; 58 | } 59 | 60 | .btn-default:active, 61 | .btn-default.active { 62 | background-color: #e0e0e0; 63 | border-color: #dbdbdb; 64 | } 65 | 66 | .btn-primary { 67 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#2d6ca2)); 68 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 69 | background-image: -moz-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 70 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 71 | background-repeat: repeat-x; 72 | border-color: #2b669a; 73 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 74 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 75 | } 76 | 77 | .btn-primary:hover, 78 | .btn-primary:focus { 79 | background-color: #2d6ca2; 80 | background-position: 0 -15px; 81 | } 82 | 83 | .btn-primary:active, 84 | .btn-primary.active { 85 | background-color: #2d6ca2; 86 | border-color: #2b669a; 87 | } 88 | 89 | .btn-success { 90 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#419641)); 91 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 92 | background-image: -moz-linear-gradient(top, #5cb85c 0%, #419641 100%); 93 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 94 | background-repeat: repeat-x; 95 | border-color: #3e8f3e; 96 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 97 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 98 | } 99 | 100 | .btn-success:hover, 101 | .btn-success:focus { 102 | background-color: #419641; 103 | background-position: 0 -15px; 104 | } 105 | 106 | .btn-success:active, 107 | .btn-success.active { 108 | background-color: #419641; 109 | border-color: #3e8f3e; 110 | } 111 | 112 | .btn-warning { 113 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#eb9316)); 114 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 115 | background-image: -moz-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 116 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 117 | background-repeat: repeat-x; 118 | border-color: #e38d13; 119 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 120 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 121 | } 122 | 123 | .btn-warning:hover, 124 | .btn-warning:focus { 125 | background-color: #eb9316; 126 | background-position: 0 -15px; 127 | } 128 | 129 | .btn-warning:active, 130 | .btn-warning.active { 131 | background-color: #eb9316; 132 | border-color: #e38d13; 133 | } 134 | 135 | .btn-danger { 136 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c12e2a)); 137 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 138 | background-image: -moz-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 139 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 140 | background-repeat: repeat-x; 141 | border-color: #b92c28; 142 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 143 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 144 | } 145 | 146 | .btn-danger:hover, 147 | .btn-danger:focus { 148 | background-color: #c12e2a; 149 | background-position: 0 -15px; 150 | } 151 | 152 | .btn-danger:active, 153 | .btn-danger.active { 154 | background-color: #c12e2a; 155 | border-color: #b92c28; 156 | } 157 | 158 | .btn-info { 159 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#2aabd2)); 160 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 161 | background-image: -moz-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 162 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 163 | background-repeat: repeat-x; 164 | border-color: #28a4c9; 165 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 166 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 167 | } 168 | 169 | .btn-info:hover, 170 | .btn-info:focus { 171 | background-color: #2aabd2; 172 | background-position: 0 -15px; 173 | } 174 | 175 | .btn-info:active, 176 | .btn-info.active { 177 | background-color: #2aabd2; 178 | border-color: #28a4c9; 179 | } 180 | 181 | .thumbnail, 182 | .img-thumbnail { 183 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 184 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 185 | } 186 | 187 | .dropdown-menu > li > a:hover, 188 | .dropdown-menu > li > a:focus { 189 | background-color: #e8e8e8; 190 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8)); 191 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 192 | background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 193 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 194 | background-repeat: repeat-x; 195 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 196 | } 197 | 198 | .dropdown-menu > .active > a, 199 | .dropdown-menu > .active > a:hover, 200 | .dropdown-menu > .active > a:focus { 201 | background-color: #357ebd; 202 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); 203 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 204 | background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); 205 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 206 | background-repeat: repeat-x; 207 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 208 | } 209 | 210 | .navbar-default { 211 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#f8f8f8)); 212 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 213 | background-image: -moz-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 214 | background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); 215 | background-repeat: repeat-x; 216 | border-radius: 4px; 217 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 218 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 219 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 220 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 221 | } 222 | 223 | .navbar-default .navbar-nav > .active > a { 224 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f3f3f3)); 225 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 226 | background-image: -moz-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 227 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 228 | background-repeat: repeat-x; 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 230 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 231 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 232 | } 233 | 234 | .navbar-brand, 235 | .navbar-nav > li > a { 236 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); 237 | } 238 | 239 | .navbar-inverse { 240 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#3c3c3c), to(#222222)); 241 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%); 242 | background-image: -moz-linear-gradient(top, #3c3c3c 0%, #222222 100%); 243 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%); 244 | background-repeat: repeat-x; 245 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 246 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 247 | } 248 | 249 | .navbar-inverse .navbar-nav > .active > a { 250 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#222222), to(#282828)); 251 | background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%); 252 | background-image: -moz-linear-gradient(top, #222222 0%, #282828 100%); 253 | background-image: linear-gradient(to bottom, #222222 0%, #282828 100%); 254 | background-repeat: repeat-x; 255 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 256 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 257 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 258 | } 259 | 260 | .navbar-inverse .navbar-brand, 261 | .navbar-inverse .navbar-nav > li > a { 262 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 263 | } 264 | 265 | .navbar-static-top, 266 | .navbar-fixed-top, 267 | .navbar-fixed-bottom { 268 | border-radius: 0; 269 | } 270 | 271 | .alert { 272 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); 273 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 274 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 275 | } 276 | 277 | .alert-success { 278 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#c8e5bc)); 279 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 280 | background-image: -moz-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 281 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 282 | background-repeat: repeat-x; 283 | border-color: #b2dba1; 284 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 285 | } 286 | 287 | .alert-info { 288 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#b9def0)); 289 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 290 | background-image: -moz-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 291 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 292 | background-repeat: repeat-x; 293 | border-color: #9acfea; 294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 295 | } 296 | 297 | .alert-warning { 298 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#f8efc0)); 299 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 300 | background-image: -moz-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 301 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 302 | background-repeat: repeat-x; 303 | border-color: #f5e79e; 304 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 305 | } 306 | 307 | .alert-danger { 308 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#e7c3c3)); 309 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 310 | background-image: -moz-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 311 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 312 | background-repeat: repeat-x; 313 | border-color: #dca7a7; 314 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 315 | } 316 | 317 | .progress { 318 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f5f5f5)); 319 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 320 | background-image: -moz-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 321 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 322 | background-repeat: repeat-x; 323 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 324 | } 325 | 326 | .progress-bar { 327 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9)); 328 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 329 | background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%); 330 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 331 | background-repeat: repeat-x; 332 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 333 | } 334 | 335 | .progress-bar-success { 336 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44)); 337 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 338 | background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%); 339 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 340 | background-repeat: repeat-x; 341 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 342 | } 343 | 344 | .progress-bar-info { 345 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5)); 346 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 347 | background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 348 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 349 | background-repeat: repeat-x; 350 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 351 | } 352 | 353 | .progress-bar-warning { 354 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f)); 355 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 356 | background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 357 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 358 | background-repeat: repeat-x; 359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 360 | } 361 | 362 | .progress-bar-danger { 363 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c)); 364 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 365 | background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%); 366 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 367 | background-repeat: repeat-x; 368 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 369 | } 370 | 371 | .list-group { 372 | border-radius: 4px; 373 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 374 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 375 | } 376 | 377 | .list-group-item.active, 378 | .list-group-item.active:hover, 379 | .list-group-item.active:focus { 380 | text-shadow: 0 -1px 0 #3071a9; 381 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3278b3)); 382 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 383 | background-image: -moz-linear-gradient(top, #428bca 0%, #3278b3 100%); 384 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 385 | background-repeat: repeat-x; 386 | border-color: #3278b3; 387 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 388 | } 389 | 390 | .panel { 391 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 392 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 393 | } 394 | 395 | .panel-default > .panel-heading { 396 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8)); 397 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 398 | background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 399 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 400 | background-repeat: repeat-x; 401 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 402 | } 403 | 404 | .panel-primary > .panel-heading { 405 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); 406 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 407 | background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); 408 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 409 | background-repeat: repeat-x; 410 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 411 | } 412 | 413 | .panel-success > .panel-heading { 414 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#d0e9c6)); 415 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 416 | background-image: -moz-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 418 | background-repeat: repeat-x; 419 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 420 | } 421 | 422 | .panel-info > .panel-heading { 423 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#c4e3f3)); 424 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 425 | background-image: -moz-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 427 | background-repeat: repeat-x; 428 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 429 | } 430 | 431 | .panel-warning > .panel-heading { 432 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#faf2cc)); 433 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 434 | background-image: -moz-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 436 | background-repeat: repeat-x; 437 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 438 | } 439 | 440 | .panel-danger > .panel-heading { 441 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#ebcccc)); 442 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 443 | background-image: -moz-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 445 | background-repeat: repeat-x; 446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 447 | } 448 | 449 | .well { 450 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#e8e8e8), to(#f5f5f5)); 451 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 452 | background-image: -moz-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 453 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 454 | background-repeat: repeat-x; 455 | border-color: #dcdcdc; 456 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 457 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 458 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 459 | } -------------------------------------------------------------------------------- /public/lib/vendor/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e0e0e0));background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-moz-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#2d6ca2));background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-moz-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#419641));background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#eb9316));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c12e2a));background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#2aabd2));background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f3f3f3));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#222),to(#282828));background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-moz-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunoda/open-comment-box/b9a295c3b29009696068f94de06eb9a0dcaaf3fc/public/lib/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/lib/vendor/jquery.bootstrap-growl.min.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var $; 3 | 4 | $ = jQuery; 5 | 6 | $.bootstrapGrowl = function(message, options) { 7 | var $alert, css, offsetAmount; 8 | 9 | options = $.extend({}, $.bootstrapGrowl.default_options, options); 10 | $alert = $("
"); 11 | $alert.attr("class", "bootstrap-growl alert"); 12 | if (options.type) { 13 | $alert.addClass("alert-" + options.type); 14 | } 15 | if (options.allow_dismiss) { 16 | $alert.append("×"); 17 | } 18 | $alert.append(message); 19 | if (options.top_offset) { 20 | options.offset = { 21 | from: "top", 22 | amount: options.top_offset 23 | }; 24 | } 25 | offsetAmount = options.offset.amount; 26 | $(".bootstrap-growl").each(function() { 27 | return offsetAmount = Math.max(offsetAmount, parseInt($(this).css(options.offset.from)) + $(this).outerHeight() + options.stackup_spacing); 28 | }); 29 | css = { 30 | "position": (options.ele === "body" ? "fixed" : "absolute"), 31 | "margin": 0, 32 | "z-index": "9999", 33 | "display": "none" 34 | }; 35 | css[options.offset.from] = offsetAmount + "px"; 36 | $alert.css(css); 37 | if (options.width !== "auto") { 38 | $alert.css("width", options.width + "px"); 39 | } 40 | $(options.ele).append($alert); 41 | switch (options.align) { 42 | case "center": 43 | $alert.css({ 44 | "left": "50%", 45 | "margin-left": "-" + ($alert.outerWidth() / 2) + "px" 46 | }); 47 | break; 48 | case "left": 49 | $alert.css("left", "20px"); 50 | break; 51 | default: 52 | $alert.css("right", "20px"); 53 | } 54 | $alert.fadeIn(); 55 | if (options.delay > 0) { 56 | $alert.delay(options.delay).fadeOut(function() { 57 | return $(this).alert("close"); 58 | }); 59 | } 60 | return $alert; 61 | }; 62 | 63 | $.bootstrapGrowl.default_options = { 64 | ele: "body", 65 | type: "info", 66 | offset: { 67 | from: "top", 68 | amount: 20 69 | }, 70 | align: "right", 71 | width: 250, 72 | delay: 4000, 73 | allow_dismiss: true, 74 | stackup_spacing: 10 75 | }; 76 | 77 | }).call(this); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | process.chdir(__dirname) 2 | 3 | var express = require('express'); 4 | var mongodb = require('mongodb'); 5 | var passport = require('passport'); 6 | var SocketManager = require('./lib/socketManager'); 7 | 8 | var http = require('http'); 9 | var sockjs = require('sockjs'); 10 | 11 | var allowCrossDomain = function(req, res, next) { 12 | res.header('Access-Control-Allow-Origin', '*'); 13 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 14 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 15 | next(); 16 | } 17 | 18 | var app = express(); 19 | app.use(express.static('public')); 20 | app.set('view engine', 'ejs'); 21 | app.use(express.cookieParser()); 22 | app.use(express.bodyParser()); 23 | app.use(express.session({ secret: 'keyboard cat' })); 24 | app.use(passport.initialize()); 25 | app.use(passport.session()); 26 | app.use(allowCrossDomain); 27 | app.use('/static', express.static('static')); 28 | app.use(function(req, res, next) { 29 | if(process.env.NKO) { 30 | req.user = {username: 'demo'} 31 | } else { 32 | //if not NKO redirect to the admin 33 | if(req.url == '/') { 34 | req.url = '/admin'; 35 | } 36 | } 37 | next(); 38 | }); 39 | 40 | var isProduction = (process.env.NODE_ENV === 'production'); 41 | var http = require('http'); 42 | var port = process.env.PORT || (isProduction ? 80 : 8000); 43 | 44 | var sockjs_opts = {sockjs_url: "http://cdn.sockjs.org/sockjs-0.3.min.js"}; 45 | var sockets = sockjs.createServer(); 46 | 47 | //listen to the app 48 | var server = http.createServer(app); 49 | sockets.installHandlers(server, {prefix:'/rt'}); 50 | 51 | console.info('open-comment-box starting on port:', port); 52 | server.listen(port); 53 | 54 | //Env 55 | var env = {}; 56 | env.socketManager = new SocketManager(); 57 | env.sockets=sockets; 58 | 59 | //connect to mongo 60 | var MONGO_URL= process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || process.env.MONGO_URL || 'mongodb://localhost/ocb'; 61 | mongodb.MongoClient.connect(MONGO_URL, afterConnected); 62 | 63 | function afterConnected(err, db) { 64 | if (err) { 65 | throw err; 66 | } else { 67 | //load models 68 | env.models = require('./lib/models')(db); 69 | env.models.config.init(); 70 | 71 | //load Routes 72 | require('./lib/sockets')(app, db, env); 73 | require('./lib/routes')(app, db, env); 74 | require('./lib/passport')(app, db, env); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /static/client.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var base_url = document.getElementById('ocb_script_loader').dataset.base_url; 5 | var api_key = document.getElementById('ocb_script_loader').dataset.api_key; 6 | 7 | var messageRoutes = { 8 | 'OCB::resizeParentFrame' : function (e) { 9 | // console.log('OCB::resizeParentFrame'); 10 | var data = JSON.parse(e.data); 11 | var iframe = document.getElementById('ocb_iframe'); 12 | var height = parseInt(data.height) || 250; 13 | var height_buffer = 200; 14 | iframe.style.height = height + height_buffer + 'px'; 15 | } 16 | ,'OCB::scrollToComment' : function (e) { 17 | console.log('OCB::scrollToComment', e); 18 | var data = JSON.parse(e.data); 19 | var offset = data.offset + ocb_iframe.offsetTop; 20 | setTimeout(function(){scrollTo(0,offset);}, 1000); 21 | } 22 | ,'OCB::getBaseDetails' : function (e) { 23 | console.log('OCB::getBaseDetails'); 24 | var data = JSON.parse(e.data); 25 | var commentId = location.hash.substring(1,location.hash.length); 26 | e.source.postMessage({ base_url: base_url, api_key: api_key, commentId: commentId, action: 'OCB::baseDetails' }, '*'); 27 | } 28 | ,'OCB::updateHash' : function (e) { 29 | console.log('OCB::updateHash'); 30 | var data = JSON.parse(e.data); 31 | location.hash = data.hash; 32 | } 33 | }; 34 | 35 | window.addEventListener('message', function handleMessage (e) { 36 | if (e.origin !== base_url) return; 37 | var data = JSON.parse(e.data) || {}; 38 | var action = messageRoutes[data.action]; 39 | action && action(e); 40 | }, false); 41 | 42 | var ocb_script_loader = document.getElementById('ocb_script_loader'); 43 | var ocb_iframe = document.getElementById('ocb_iframe'); 44 | ocb_iframe.src = base_url + '/static/comments.html'; 45 | ocb_iframe.style.cssText = 'border-style: none; width: 100%; height: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0;'; 46 | ocb_script_loader.parentNode.insertBefore(ocb_iframe, ocb_script_loader); 47 | 48 | })(); 49 | -------------------------------------------------------------------------------- /static/comments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Open Comment Box 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | Loading comments
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | Login with 28 | 29 | , 30 | or 31 | to comment 32 |
33 |
34 | 35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /static/scripts/modules/Channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Channel = {}; 4 | 5 | // JSONP 6 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | Channel.jsonp = function (url) { 9 | console.log('[ ] Channel.jsonp()', url); 10 | var script = document.createElement('script'); 11 | script.src = url; 12 | document.head.appendChild(script); 13 | }; 14 | 15 | Channel.onData = function (data) { 16 | console.log('[ ] Channel.onData()', data); 17 | Comment.comments = data.comments; 18 | User.users = data.users || {}; 19 | Comment.render(); 20 | Interface.onReady(); 21 | }; 22 | 23 | document.addEventListener('DOMContentLoaded', function (e) { 24 | parent.postMessage(JSON.stringify({ action: 'OCB::getBaseDetails' }), '*'); 25 | window.addEventListener('message', function handleMessage (e) { 26 | var data = e.data || {}; 27 | if (data.action === 'OCB::baseDetails') { 28 | Channel.BASE_URL = data.base_url; 29 | Channel.API_TOKEN = data.api_key; 30 | Channel.SOCK_URL = Channel.BASE_URL+'/rt'; 31 | Channel.REF_COMMENT = data.commentId; 32 | Channel.sock = new SockJS(Channel.SOCK_URL); 33 | Channel.initSock(); 34 | Channel.getInitData(); 35 | } 36 | }, false); 37 | }); 38 | 39 | // SOCKJS 40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | 42 | Channel.BASE_URL = null; 43 | Channel.DOC_URL = (window.location != window.parent.location) ? document.referrer: document.location; 44 | Channel.DOC_URL = Channel.DOC_URL.split('#')[0]; 45 | Channel.SOCK_URL = null; 46 | Channel.API_TOKEN = null; 47 | Channel.REF_COMMENT = null; 48 | Channel.sock = null; 49 | 50 | Channel.sockRoutes = { 51 | 'commentsAdded' : function (data) { 52 | for (var idx=data.length; idx-->0;){ 53 | data[idx].isNewComment = true; 54 | Comment.comments.push(data[idx]); 55 | User.users[data[idx].user._id] = data[idx].user; 56 | Comment.unreadCount++; 57 | Comment.showUnread(); 58 | } 59 | Comment.render(); 60 | } 61 | ,'commentsDeleted' : function (data) { 62 | for (var idx=data.length; idx-->0;) 63 | for (var idx2=Comment.comments.length; idx2-->0;) 64 | if (Comment.comments[idx2] && Comment.comments[idx2]._id === data[idx]) 65 | delete Comment.comments[idx2]; 66 | Comment.render(); 67 | } 68 | }; 69 | 70 | Channel.getInitData = function () { 71 | console.log('[ ] Channel.getInitData()'); 72 | var url = 73 | Channel.BASE_URL 74 | + '/comments/init' 75 | + '?callback=Channel.onData' 76 | + '&apiToken=' + Channel.API_TOKEN 77 | + '&url=' + encodeURIComponent(Channel.DOC_URL); 78 | Channel.jsonp(url); 79 | } 80 | 81 | Channel.initSock = function () { 82 | 83 | Channel.sock.onopen = function (e) { 84 | console.log('[ ] Channel.sock.onopen()'); 85 | Channel.sock.send(JSON.stringify({ 86 | command: 'init' 87 | ,apiToken: Channel.API_TOKEN 88 | ,url: Channel.DOC_URL.split('#')[0] 89 | })); 90 | }; 91 | 92 | Channel.sock.onclose = function (e) { 93 | console.log('[ ] Channel.sock.onclose()'); 94 | }; 95 | 96 | Channel.sock.onmessage = function (e) { 97 | console.log('[ ] Channel.sock.onmessage()', e); 98 | var data = JSON.parse(e.data); 99 | Channel.sockRoutes[data.event] && Channel.sockRoutes[data.event](data.data); 100 | }; 101 | 102 | } 103 | -------------------------------------------------------------------------------- /static/scripts/modules/Comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Comment = {}; 4 | 5 | Comment.elCommentsWrapper = null; 6 | Comment.elUnreadComments = null; 7 | Comment.elNewCommentForm = null; 8 | Comment.elNewCommentText = null; 9 | Comment.elNewCommentSubmit = null; 10 | Comment.elNewCommentReply = null; 11 | 12 | Comment.comments = []; 13 | Comment.replyId = null; 14 | Comment.unreadCount = 0; 15 | 16 | Comment.getCommentById = function (commentId) { 17 | console.log('[ ] Comment.getCommentById()', commentId); 18 | for (var idx = Comment.comments.length; idx-->0;) 19 | if (Comment.comments[idx] && Comment.comments[idx]._id === commentId) 20 | return Comment.comments[idx]; 21 | }; 22 | 23 | Comment.validate = function () { 24 | console.log('[ ] Comment.validate()'); 25 | var message = Comment.elNewCommentText.value.trim(); 26 | return message.length > 0; 27 | } 28 | 29 | Comment.create = function () { 30 | console.log('[ ] Comment.create()'); 31 | if (!Comment.validate()) return; 32 | var message = Comment.elNewCommentText.value.trim(); 33 | Comment.elNewCommentText.value = ''; 34 | var url = 35 | Channel.BASE_URL 36 | + '/comments/create?callback=console.log' 37 | + '&apiToken=' + encodeURIComponent(Channel.API_TOKEN) 38 | + '&userId=' + encodeURIComponent(User.getId()) 39 | + '&userToken=' + encodeURIComponent(User.getToken()) 40 | + '&message=' + encodeURIComponent(message) 41 | + '&url=' + encodeURIComponent(Channel.DOC_URL); 42 | if (Comment.replyId) 43 | url += '&parentId=' + Comment.replyId; 44 | Comment.replyId = null; 45 | Comment.elNewCommentReply.innerHTML = ''; 46 | Comment.elNewCommentReply.classList.remove('visible'); 47 | Comment.elNewCommentText.style.height = '46px'; 48 | Channel.jsonp(url); 49 | }; 50 | 51 | Comment.handleNewComment = function (element, idx) { 52 | element.classList.add('new-comment'); 53 | setTimeout(function(element){element.classList.remove('new-comment');}, 2000, element); 54 | Comment.comments[idx].isNewComment = false; 55 | if (User.user._id == Comment.comments[idx].userId) { 56 | var offset = element.offsetTop; 57 | var msg = JSON.stringify({ action: 'OCB::scrollToComment', offset: offset }); 58 | parent.postMessage(msg, '*'); 59 | } 60 | } 61 | 62 | Comment.scrollToHashComment = function () { 63 | console.log('[ ] Comment.scrollToComment()'); 64 | var element = document.getElementById(Channel.REF_COMMENT); 65 | if(!element) return; 66 | var offset = element.offsetTop; 67 | var msg = JSON.stringify({ action: 'OCB::scrollToComment', offset: offset }); 68 | parent.postMessage(msg, '*'); 69 | Channel.REF_COMMENT = null; 70 | } 71 | 72 | Comment.render = function () { 73 | console.log('[ ] Comment.render()'); 74 | var comments = JSON.parse(JSON.stringify(Comment.comments)); 75 | var elements = document.getElementsByClassName('comment'); 76 | for (var idx=elements.length; idx-->0;) 77 | elements[idx].parentNode.removeChild(elements[idx]); 78 | var stop_infinite_loop_counter = 3; 79 | while(Object.keys(comments).length > 0 && stop_infinite_loop_counter--> 0) for(var idx in comments) { 80 | var comment = comments[idx]; 81 | if(!comment) continue; 82 | var element = document.createElement('div'); 83 | element.classList.add('comment'); 84 | element.id = 'comment_' + comment._id; 85 | element.dataset.commentId = comment._id; 86 | var user = User.users[comment.userId] || { 87 | name: 'Anonymous' 88 | , link: '#' 89 | , avatar: 'http://meteorhacks.2013.nodeknockout.com/images/user.png' 90 | }; 91 | var timeago = moment(comment.createdAt).fromNow(); 92 | var html = 93 | '

'+ user.name +' - '+ timeago +'

' 94 | + '' 95 | + '
'+ comment.message +'
' 96 | + '' 104 | + '
'; 105 | element.innerHTML = html 106 | if (document.getElementById('comment_'+comment._id)) return; 107 | if (comment.parentId == null) { 108 | Comment.elCommentsWrapper.appendChild(element); 109 | comment.isNewComment && Comment.handleNewComment(element, idx); 110 | delete comments[idx]; 111 | } else if (document.getElementById('comment_'+comment.parentId)) { 112 | document.getElementById('comment_'+comment.parentId).getElementsByClassName('comment_replies')[0].appendChild(element); 113 | comment.isNewComment && Comment.handleNewComment(element, idx); 114 | delete comments[idx]; 115 | } 116 | } 117 | Comment.scrollToHashComment(); 118 | }; 119 | 120 | Comment.reply = function (commentId) { 121 | console.log('[ ] Comment.reply()', commentId); 122 | Comment.replyId = commentId; 123 | var comment = Comment.getCommentById(commentId); 124 | var user = User.users[comment.userId]; 125 | var username = user ? user.name : 'Anonymous'; 126 | Comment.elNewCommentReply.innerHTML = '@' + username + ' "' + comment.message.slice(0,15) + '..."'; 127 | Comment.elNewCommentReply.classList.add('visible'); 128 | Comment.elNewCommentText.focus(); 129 | }; 130 | 131 | Comment.share = function (commentId) { 132 | console.log('[ ] Comment.share()', commentId); 133 | var element = document.getElementById('comment_' + commentId); 134 | if(!element) return; 135 | var offset = element.offsetTop; 136 | var msg = JSON.stringify({ action: 'OCB::scrollToComment', offset: offset }); 137 | parent.postMessage(msg, '*'); 138 | msg = JSON.stringify({ action: 'OCB::updateHash', hash: 'comment_' + commentId }); 139 | parent.postMessage(msg, '*'); 140 | } 141 | 142 | Comment.delete = function (commentId) { 143 | console.log('[ ] Comment.delete()', commentId); 144 | if(!confirm('Delete Comment?')) return; 145 | var url = 146 | Channel.BASE_URL 147 | + '/comments/delete' 148 | + '?callback=console.log' 149 | + '&apiToken=' + Channel.API_TOKEN 150 | + '&id=' + commentId 151 | + '&url=' + encodeURIComponent(Channel.DOC_URL); 152 | Channel.jsonp(url); 153 | for (var idx = Comment.comments.length; idx-->0;) if (Comment.comments[idx] && Comment.comments[idx]._id === commentId) 154 | delete Comment.comments[idx]; 155 | Comment.render(); 156 | } 157 | 158 | Comment.onNewComment = function (data) { 159 | console.log('[ ] Comment.onNewComment()', data); 160 | for(var idx=data.length; idx-->0;) 161 | Comment.comments.push(data[idx]); 162 | Comment.render(); 163 | }; 164 | 165 | Comment.showUnread = function () { 166 | Comment.elUnreadComments.classList.add('visible'); 167 | Comment.elUnreadComments.innerHTML = Comment.unreadCount + ' New Comments'; 168 | } 169 | 170 | Comment.hideUnread = function () { 171 | Comment.unreadCount = 0; 172 | Comment.elUnreadComments.classList.remove('visible'); 173 | } 174 | 175 | document.addEventListener('DOMContentLoaded', function (e) { 176 | Comment.elCommentsWrapper = document.getElementById('commentsWrap'); 177 | Comment.elUnreadComments = document.getElementById('unreadComments'); 178 | Comment.elNewCommentForm = document.getElementById('messageWrap'); 179 | Comment.elNewCommentText = document.getElementById('message'); 180 | Comment.elNewCommentSubmit = document.getElementById('messageSubmit'); 181 | Comment.elNewCommentReply = document.getElementById('replyTo'); 182 | Comment.elNewCommentSubmit.onclick = Comment.create; 183 | Comment.elUnreadComments.onclick = Comment.hideUnread; 184 | Comment.elUnreadComments.onmouseover = Comment.hideUnread; 185 | }); 186 | -------------------------------------------------------------------------------- /static/scripts/modules/Interface.js: -------------------------------------------------------------------------------- 1 | 2 | var Interface = new Object(); 3 | 4 | Interface.elLoadingAnimation = null; 5 | 6 | Interface.autoGrowTextarea = function (textarea) { 7 | console.log('[ ] Interface.autoGrowTextarea()'); 8 | // Based on http://stackoverflow.com/a/19408351 9 | textarea.addEventListener('keydown', function(e){ 10 | setTimeout(function (el) { 11 | var height = parseInt(el.scrollHeight); 12 | el.style.cssText = 'height:0; padding:0'; 13 | el.style.cssText = 'height:' + height + 'px'; 14 | }, 0, this); 15 | }); 16 | } 17 | 18 | Interface.resizeParentFrame = function (content) { 19 | return function () { 20 | var msg = JSON.stringify({ action: 'OCB::resizeParentFrame', height: content.scrollHeight }); 21 | parent.postMessage(msg, '*'); 22 | } 23 | } 24 | 25 | Interface.onReady = function () { 26 | console.log('[ ] Interface.onReady()'); 27 | Interface.elLoadingAnimation.classList.remove('visible'); 28 | setTimeout(function(){Interface.elLoadingAnimation.style.display = 'none';},1000); 29 | } 30 | 31 | document.addEventListener('DOMContentLoaded', function (e) { 32 | Interface.elLoadingAnimation = document.getElementById('loadingAnimation'); 33 | Interface.autoGrowTextarea(document.getElementById('message')); 34 | setInterval(Interface.resizeParentFrame(document.getElementById('content')), 1000); 35 | setInterval(function(){Comment.render()}, 30 * 1000); 36 | }); 37 | -------------------------------------------------------------------------------- /static/scripts/modules/User.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var User = new Object(); 4 | 5 | User.elLoginWrapper = null; 6 | User.elUserAvatar = null; 7 | User.elLogoutButton = null; 8 | 9 | User.users = {}; 10 | User.user = null; 11 | $ 12 | User.validateToken = function () { 13 | // @FIXME Do come real validation 14 | console.log('[ ] User.validateToken()'); 15 | return true; 16 | }; 17 | 18 | User.isLoggedIn = function () { 19 | console.log('[ ] User.isLoggedIn()'); 20 | User.user = JSON.parse(localStorage.getItem('user')); 21 | if (User.user){ 22 | console.log() 23 | return User.validateToken(); 24 | } 25 | }; 26 | 27 | User.getId = function () { 28 | console.log('[ ] User.getId()'); 29 | if (!User.isLoggedIn()) return; 30 | return User.user._id; 31 | }; 32 | 33 | User.getToken = function () { 34 | console.log('[ ] User.getToken()'); 35 | if (!User.isLoggedIn()) return; 36 | return User.user.userToken; 37 | }; 38 | 39 | User.requestLogin = function (handler) { 40 | console.log('[ ] User.requestLogin()', handler); 41 | var url = Channel.BASE_URL + '/auth/' + handler.toLowerCase(); 42 | var popup = window.open(url, 'Open Comment Box Login', 'width=500,height=400'); 43 | window.focus && popup.focus(); 44 | window.addEventListener('message', function(e) { 45 | var data = JSON.parse(e.data); 46 | if( data.action === 'OCB:authResult' ) 47 | User.login(data.data); 48 | }, false); 49 | }; 50 | 51 | User.login = function (user) { 52 | console.log('[ ] User.login()', user); 53 | User.user = user; 54 | localStorage.setItem('user', JSON.stringify(User.user)); 55 | User.elLoginWrapper.classList.remove('visible'); 56 | User.elUserAvatar.src = User.user.avatar; 57 | User.elUserAvatar.classList.add('visible'); 58 | User.elLogoutButton.classList.add('visible'); 59 | Comment.elNewCommentSubmit.classList.add('visible'); 60 | Comment.render(); 61 | }; 62 | 63 | User.logout = function () { 64 | console.log('[ ] User.logout()'); 65 | User.user = null; 66 | localStorage.removeItem('user'); 67 | Comment.elNewCommentSubmit.classList.remove('visible'); 68 | User.elUserAvatar.classList.remove('visible'); 69 | User.elLogoutButton.classList.remove('visible'); 70 | User.elLoginWrapper.classList.add('visible'); 71 | Comment.elNewCommentText.value = ''; 72 | Comment.replyId = null; 73 | Comment.elNewCommentReply.innerHTML = ''; 74 | Comment.elNewCommentReply.classList.remove('visible'); 75 | Comment.render(); 76 | }; 77 | 78 | document.addEventListener('DOMContentLoaded', function (e) { 79 | User.elLoginWrapper = document.getElementById('loginWrapper'); 80 | User.elUserAvatar = document.getElementById('avatar'); 81 | User.elLogoutButton = document.getElementById('logoutButton'); 82 | if (User.isLoggedIn()) { 83 | User.elLoginWrapper.classList.remove('visible'); 84 | User.elUserAvatar.src = User.user.avatar; 85 | User.elUserAvatar.classList.add('visible'); 86 | User.elLogoutButton.classList.add('visible'); 87 | Comment.elNewCommentSubmit.classList.add('visible'); 88 | } else { 89 | User.elUserAvatar.classList.remove('visible'); 90 | User.elLogoutButton.classList.remove('visible'); 91 | Comment.elNewCommentSubmit.classList.remove('visible'); 92 | User.elLoginWrapper.classList.add('visible'); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /static/scripts/scripts.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var Channel = {}; 5 | 6 | // JSONP 7 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | Channel.jsonp = function (url) { 10 | console.log('[ ] Channel.jsonp()', url); 11 | var script = document.createElement('script'); 12 | script.src = url; 13 | document.head.appendChild(script); 14 | }; 15 | 16 | Channel.onData = function (data) { 17 | console.log('[ ] Channel.onData()', data); 18 | Comment.comments = data.comments; 19 | User.users = data.users || {}; 20 | // @FIXME Remove Dummy User 21 | User.users['527ea4071e8bbc5bb6476835'] = { _id: '527ea4071e8bbc5bb6476835', name: 'Thanish' }; 22 | Comment.render(); 23 | }; 24 | 25 | document.addEventListener('DOMContentLoaded', function (e) { 26 | Channel.jsonp('http://localhost:8000/comments/init?callback=Channel.onData&apiToken=' + Channel.API_TOKEN); 27 | }); 28 | 29 | // SOCKJS 30 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | Channel.SOCK_URL = 'http://localhost:8000/rt'; 33 | Channel.API_TOKEN = '123456'; 34 | Channel.sock = new SockJS(Channel.SOCK_URL); 35 | 36 | Channel.sockRoutes = { 37 | 'commentsAdded' : function (data) { 38 | for (var idx=data.length; idx-->0;) 39 | Comment.comments.push(data[idx]); 40 | Comment.render(); 41 | } 42 | }; 43 | 44 | Channel.sock.onopen = function (e) { 45 | console.log('[ ] Channel.sock.onopen()'); 46 | Channel.sock.send(JSON.stringify({ 47 | command: 'init' 48 | ,apiToken: Channel.API_TOKEN 49 | ,url: document.URL 50 | })); 51 | }; 52 | 53 | Channel.sock.onclose = function (e) { 54 | console.log('[ ] Channel.sock.onclose()'); 55 | }; 56 | 57 | Channel.sock.onmessage = function (e) { 58 | console.log('[ ] Channel.sock.onmessage()'); 59 | var data = JSON.parse(e.data); 60 | Channel.sockRoutes[data.event] && Channel.sockRoutes[data.event](data.data); 61 | }; 62 | 'use strict'; 63 | 64 | var Comment = {}; 65 | 66 | Comment.elCommentsWrapper = null; 67 | Comment.elNewCommentForm = null; 68 | Comment.elNewCommentText = null; 69 | Comment.elNewCommentSubmit = null; 70 | Comment.elNewCommentReply = null; 71 | 72 | Comment.comments = []; 73 | Comment.replyId = null; 74 | 75 | Comment.getCommentById = function (commentId) { 76 | console.log('[ ] Comment.getCommentById()', commentId); 77 | for (var idx = Comment.comments.length; idx-->0;) 78 | if (Comment.comments[idx]._id === commentId) 79 | return Comment.comments[idx]; 80 | }; 81 | 82 | Comment.validate = function () { 83 | console.log('[ ] Comment.validate()'); 84 | var message = Comment.elNewCommentText.value.trim(); 85 | return message.length > 0; 86 | } 87 | 88 | Comment.create = function () { 89 | console.log('[ ] Comment.create()'); 90 | if (!Comment.validate()) return; 91 | var message = Comment.elNewCommentText.value.trim(); 92 | Comment.elNewCommentText.value = ''; 93 | var url = 94 | 'http://localhost:8000/comments/create?callback=console.log' 95 | + '&apiToken=' + encodeURIComponent(Channel.API_TOKEN) 96 | + '&userId=' + encodeURIComponent(User.getId()) 97 | + '&userToken=' + encodeURIComponent(User.getToken()) 98 | + '&message=' + encodeURIComponent(message); 99 | if (Comment.replyId) 100 | url += '&parentId=' + Comment.replyId; 101 | Comment.replyId = null; 102 | Comment.elNewCommentReply.innerHTML = ''; 103 | Comment.elNewCommentReply.classList.remove('visible'); 104 | Channel.jsonp(url); 105 | }; 106 | 107 | Comment.render = function () { 108 | console.log('[ ] Comment.render()'); 109 | var comments = JSON.parse(JSON.stringify(Comment.comments)); 110 | var elements = document.getElementsByClassName('comment'); 111 | for (var idx=elements.length; idx-->0;) 112 | elements[idx].parentNode.removeChild(elements[idx]); 113 | while(Object.keys(comments).length > 0) for(var idx in comments) { 114 | var comment = comments[idx]; 115 | var element = document.createElement('div'); 116 | element.classList.add('comment'); 117 | element.id = 'comment_' + comment._id; 118 | element.dataset.commentId = comment._id; 119 | var user = User.getDetailsById(comment.userId); 120 | var username = user ? user.name : 'Anonymous'; 121 | var timeago = moment(comment.createdAt).fromNow() 122 | element.innerHTML = 123 | '

'+ username +' - '+ timeago +'

' 124 | + '
'+ comment.message +'
' 125 | + '' 129 | + '
'; 130 | if (document.getElementById('comment_'+comment._id)) return; 131 | if (comment.parentId == null) { 132 | Comment.elCommentsWrapper.appendChild(element); 133 | delete comments[idx]; 134 | } else if (document.getElementById('comment_'+comment.parentId)) { 135 | document.getElementById('comment_'+comment.parentId).getElementsByClassName('comment_replies')[0].appendChild(element); 136 | delete comments[idx]; 137 | } 138 | } 139 | }; 140 | 141 | Comment.reply = function (commentId) { 142 | console.log('[ ] Comment.reply()', commentId); 143 | if (!User.isLoggedIn()) return; 144 | Comment.replyId = commentId; 145 | var comment = Comment.getCommentById(commentId); 146 | var user = User.getDetailsById(comment.userId); 147 | var username = user ? user.name : 'Anonymous'; 148 | Comment.elNewCommentReply.innerHTML = '@' + username + ' "' + comment.message.slice(0,15) + '..."'; 149 | Comment.elNewCommentReply.classList.add('visible'); 150 | Comment.elNewCommentText.focus(); 151 | }; 152 | 153 | Comment.share = function (commentId) { 154 | console.log('[ ] Comment.share()', commentId); 155 | }; 156 | 157 | Comment.onNewComment = function (data) { 158 | console.log('[ ] Comment.onNewComment()', data); 159 | for(var idx=data.length; idx-->0;) 160 | Comment.comments.push(data[idx]); 161 | Comment.render(); 162 | }; 163 | 164 | document.addEventListener('DOMContentLoaded', function (e) { 165 | Comment.elCommentsWrapper = document.getElementById('commentsWrap'); 166 | Comment.elNewCommentForm = document.getElementById('messageWrap'); 167 | Comment.elNewCommentText = document.getElementById('message'); 168 | Comment.elNewCommentSubmit = document.getElementById('messageSubmit'); 169 | Comment.elNewCommentReply = document.getElementById('replyTo'); 170 | Comment.elNewCommentSubmit.onclick = Comment.create; 171 | }); 172 | 173 | var Interface = new Object(); 174 | 175 | Interface.autoGrowTextarea = function (textarea) { 176 | // Based on http://stackoverflow.com/a/19408351 177 | textarea.addEventListener('keydown', function(e){ 178 | setTimeout(function (el) { 179 | var height = parseInt(el.scrollHeight); 180 | el.style.cssText = 'height:0; padding:0'; 181 | el.style.cssText = 'height:' + height + 'px'; 182 | }, 0, this); 183 | }); 184 | } 185 | 186 | Interface.resizeParentFrame = function (content) { 187 | return function () { 188 | var msg = JSON.stringify({ action: 'OCB::resizeParentFrame', height: content.scrollHeight }); 189 | parent.postMessage( msg, '*'); 190 | } 191 | } 192 | 193 | document.addEventListener('DOMContentLoaded', function (e) { 194 | Interface.autoGrowTextarea(document.getElementById('message')); 195 | setInterval(Interface.resizeParentFrame(document.getElementById('content')), 1000); 196 | }); 197 | 'use strict'; 198 | 199 | var User = new Object(); 200 | 201 | User.elLoginWrapper = null; 202 | User.elLogoutButton = null; 203 | 204 | User.users = {}; 205 | User.user = null; 206 | 207 | User.getDetailsById = function (userId) { 208 | console.log('[ ] User.getDetailsById()', userId); 209 | return User.users[userId]; 210 | }; 211 | 212 | User.validateToken = function () { 213 | // @FIXME Do come real validation 214 | console.log('[ ] User.validateToken()'); 215 | return true; 216 | }; 217 | 218 | User.isLoggedIn = function () { 219 | console.log('[ ] User.isLoggedIn()'); 220 | User.user = JSON.parse(localStorage.getItem('user')); 221 | if (User.user){ 222 | console.log() 223 | return User.validateToken(); 224 | } 225 | }; 226 | 227 | User.getId = function () { 228 | console.log('[ ] User.getId()'); 229 | if (!User.isLoggedIn()) return; 230 | return User.user._id; 231 | }; 232 | 233 | User.getToken = function () { 234 | console.log('[ ] User.getToken()'); 235 | if (!User.isLoggedIn()) return; 236 | return User.user.token; 237 | }; 238 | 239 | User.requestLogin = function (handler) { 240 | console.log('[ ] User.requestLogin()', handler); 241 | // @FIXME Replace Mock Data with Data From Real Handlers 242 | User.login({ 243 | _id: '527ea4071e8bbc5bb6476835' 244 | ,token: 'user_token' 245 | ,name: 'Thanish' 246 | }) 247 | }; 248 | 249 | User.login = function (user) { 250 | console.log('[ ] User.login()', user); 251 | User.user = user; 252 | localStorage.setItem('user', JSON.stringify(User.user)); 253 | User.elLoginWrapper.classList.remove('visible'); 254 | User.elLogoutButton.classList.add('visible'); 255 | Comment.elNewCommentSubmit.classList.add('visible'); 256 | }; 257 | 258 | User.logout = function () { 259 | console.log('[ ] User.logout()'); 260 | User.user = null; 261 | localStorage.removeItem('user'); 262 | Comment.elNewCommentSubmit.classList.remove('visible'); 263 | User.elLogoutButton.classList.remove('visible'); 264 | User.elLoginWrapper.classList.add('visible'); 265 | Comment.elNewCommentText.value = ''; 266 | Comment.replyId = null; 267 | Comment.elNewCommentReply.innerHTML = ''; 268 | Comment.elNewCommentReply.classList.remove('visible'); 269 | }; 270 | 271 | document.addEventListener('DOMContentLoaded', function (e) { 272 | User.elLoginWrapper = document.getElementById('loginWrapper'); 273 | User.elLogoutButton = document.getElementById('logoutButton'); 274 | if (User.isLoggedIn()) { 275 | User.elLoginWrapper.classList.remove('visible'); 276 | User.elLogoutButton.classList.add('visible'); 277 | Comment.elNewCommentSubmit.classList.add('visible'); 278 | } else { 279 | User.elLogoutButton.classList.remove('visible'); 280 | Comment.elNewCommentSubmit.classList.remove('visible'); 281 | User.elLoginWrapper.classList.add('visible'); 282 | } 283 | }); 284 | -------------------------------------------------------------------------------- /static/styles/loader.gif: -------------------------------------------------------------------------------- 1 | GIF89a<����  2 | ���!� NETSCAPE2.0!�,< h���0�V�a�ͩ�](�d�!f������!�,<�424$&$TVT$"$ 3 | <:<,*,���0��I��8늂Jq�f�(� Ʋ�e*�t-�A,�� ��w8�P�DE!�,<�<:<$"$TRT  DFD424dbd 4 | <><,*,\Z\LNL464dfd���<�$�di�h��� 5 | ��Sͪ�k;�YÖ��Ȥr�$���0<���H$ 6 | �)Ċݚ�!!�,<�|z|<><\Z\,.,��䌎�lnl 7 | LJL$&$���dfd464|~|DFD$"$\^\424���trt  TRT,*,���J`'�di�h��X#eN�-d0��)��@S+�Mj�@���ШttD4��sL6G"�@L�T+�10t�G�lFۡ!!�,<�dfd464���|~|TRT  ���trtDBD��� 8 | ���,.,\^\���ljl<><$&$���TVTtvtLJL������������W�'�di�h��� �Rd�si��T�1�jHE�,Q��<�D",Z�X" 9@���1<��e3h "ٸ�#ax�b��1��� pr�Y!!�,<�tvt<><���$"$���\Z\������LNL���424������  ���DFDlnl 9 | |z|DBD���,*,���dbd������TVT���<:<���������k@�pH,�Ȥ�rhˠ��@(��A0.� ��,���s�5 10 | V����d3z��'?  11 |    !~~�� 12 | ��������CA!�,<�������DFD���$"$���dbd������424������trt 13 | TRT���������<:<������,*,���lnl���LNL��䤦�dfd������464������|z|  \Z\���������<><���,.,���}@�pH,�Ȥ��� T��"0a>%��� ��UЈP�r}\�K�^K�m��\�"jl���0(  )'%  14 | *!+�l� "�/-��� &&.,�����CA!�,<����DBD���$"$dfd��䤦�TRT������464|z|��� 15 | ��씒�LJL���,.,trt���\Z\���DFD���$&$ljl���������<><|~|���  ���\^\���}@�pH,�Ȥ2)R�"�2x�#��d��R��� P�`�66��i�q�,$n�;�=gikm��G! %  ' $ ) 16 | 17 | �m�(���+&�**���)"���JA!�,<����DFD���$&$��쬪�ljl������|z|���TVT<:<��̴�����424���tvt���dbd 18 | ���LNL���,*,��쬮�lnl���������|~|���\Z\DBD��Դ��$"$���|@�pH,�Ȥr)Ĕ�)�,X!��a�4���Q�HF� ��l�PҴjb���7�Q� -jln�L*"'  ++$ 19 | %�m�#����(��+ ������mA!�,<�������DFD,*,������dfd���������\Z\464  |z|���LNL���$"$������<>< 20 | ������424������lnl���������dbd<:<������TRT���|�pH,�Ȥry�(&c#H%:��&���A� N��.�D��DM�:-�ې�9e hkm�D ' & %(#  $ 21 | "�l ����� #��$������A!�,<����DFD���$"$��䤦�dfd���424���\Z\���|z| 22 | ���,*,�����܌��TRT���lnl<><���LJL���$&$������464���\^\���|~|  ���,.,�����䔒����trt���}@�pH,�Ȥr�ܐX�B%990��+��L��f��PB)��l7 ��RU0c��c+-hjlnI.)+ 23 |   '*" , $�n������//��������GA!�,<����DBD���$&$dfd��䤦����TRTtvt��� 24 | ������464���trt��촲����\^\���LJL���,.,ljl������TVT|~|���  ���<><������{@�pH,�Ȥr�Z@����� F�#�-:��F�*D�i��ϓ�P0(�mW�!gimo$& 25 | " %' on ���������' �����JA!�,<����DFD���,*,������lnl���TVT<:<���  ���|z|������������dbdDBD 26 | ���LNL���424������tvt$"$���\Z\<><�����䄂����{@�pH,�Ȥr�DR���$��s�*��`�!|%��lA���:h4 27 | �� 3�gjlG %  '! & nm�� #"��� 28 | ��� �����mA!�,<�������DFD$"$��䤢�dbd���424��Ԕ�����trt 29 | TRT������<:<��܌��,*,���lnl������LNL��䤦�dfd���464��Ԝ�����|z|  \Z\������<><���,.,���w�pH,�Ȥr�d�$U�E�L�O��99@)Ȁ5�2"��lW!D��ZX��J��\gik(  30 | )'%  !+nn� "�#���� &&�-�������A!�,<����\Z\���<><���|z|���,.,ljlLJL������$"$�����Ԅ��dfdDFD���������\^\���DBD���|~|464trtTRT������$&$�����܌��������n@�pH,�Ȥr�lA��0z`>�� m:��GA� B���R��I�` @��"c5 " #% k�&�� 31 | 32 |  $����% ����kA!�,<�DBD������tvt������\Z\������������LNLljl���|~|���������DFD������|z|������dbd����������TVTlnl���]�pH,�Ȥr�l'D�฀J"�9(0�M�Qq��P��z�P,\� l`�Ц1x|8g�Exz| 33 |  k�������A!�,<�lnl�����ܔ�������Ԭ����������䤢������Č��|z|�����䜚������ܴ�������줦�������J`&�di�h��l�ba%S�0�qEM���k� $#`P�|���( �0�$�FHNI�+��� 34 | ,`qyN��E!!�,<���������켺���������Ĥ��������������������������������? %�di�h��l� �8��L�P ��� �BPk�@�I� (����,D�Պݚ��(!�,<����������������������������������,P�I��8�ͻ��!2��S��� ƀDrl��6\B����d!�,<����������������������h���0�I��#p��EAç~噮p,�L;images/0000755310300302511400000000000012224757200014755 5ustar preloaders.netpreloaders.netimages/sprites.gif0000644310300302511400000001161012224757200017134 0ustar preloaders.netpreloaders.netGIF89a<����  35 | ���!� NETSCAPE2.0!�,< h���0�V�a�ͩ�](�d�!f������!�,<�424$&$TVT$"$ 36 | <:<,*,���0��I��8늂Jq�f�(� Ʋ�e*�t-�A,�� ��w8�P�DE!�,<�<:<$"$TRT  DFD424dbd 37 | <><,*,\Z\LNL464dfd���<�$�di�h��� 38 | ��Sͪ�k;�YÖ��Ȥr�$���0<���H$ 39 | �)Ċݚ�!!�,<�|z|<><\Z\,.,��䌎�lnl 40 | LJL$&$���dfd464|~|DFD$"$\^\424���trt  TRT,*,���J`'�di�h��X#eN�-d0��)��@S+�Mj�@���ШttD4��sL6G"�@L�T+�10t�G�lFۡ!!�,<�dfd464���|~|TRT  ���trtDBD��� 41 | ���,.,\^\���ljl<><$&$���TVTtvtLJL������������W�'�di�h��� �Rd�si��T�1�jHE�,Q��<�D",Z�X" 9@���1<��e3h "ٸ�#ax�b��1��� pr�Y!!�,<�tvt<><���$"$���\Z\������LNL���424������  ���DFDlnl 42 | |z|DBD���,*,���dbd������TVT���<:<���������k@�pH,�Ȥ�rhˠ��@(��A0.� ��,���s�5 43 | V����d3z��'?  44 |    !~~�� 45 | ��������CA!�,<�������DFD���$"$���dbd������424������trt 46 | TRT���������<:<������,*,���lnl���LNL��䤦�dfd������464������|z|  \Z\���������<><���,.,���}@�pH,�Ȥ��� T��"0a>%��� ��UЈP�r}\�K�^K�m��\�"jl���0(  )'%  47 | *!+�l� "�/-��� &&.,�����CA!�,<����DBD���$"$dfd��䤦�TRT������464|z|��� 48 | ��씒�LJL���,.,trt���\Z\���DFD���$&$ljl���������<><|~|���  ���\^\���}@�pH,�Ȥ2)R�"�2x�#��d��R��� P�`�66��i�q�,$n�;�=gikm��G! %  ' $ ) 49 | 50 | �m�(���+&�**���)"���JA!�,<����DFD���$&$��쬪�ljl������|z|���TVT<:<��̴�����424���tvt���dbd 51 | ���LNL���,*,��쬮�lnl���������|~|���\Z\DBD��Դ��$"$���|@�pH,�Ȥr)Ĕ�)�,X!��a�4���Q�HF� ��l�PҴjb���7�Q� -jln�L*"'  ++$ 52 | %�m�#����(��+ ������mA!�,<�������DFD,*,������dfd���������\Z\464  |z|���LNL���$"$������<>< 53 | ������424������lnl���������dbd<:<������TRT���|�pH,�Ȥry�(&c#H%:��&���A� N��.�D��DM�:-�ې�9e hkm�D ' & %(#  $ 54 | "�l ����� #��$������A!�,<����DFD���$"$��䤦�dfd���424���\Z\���|z| 55 | ���,*,�����܌��TRT���lnl<><���LJL���$&$������464���\^\���|~|  ���,.,�����䔒����trt���}@�pH,�Ȥr�ܐX�B%990��+��L��f��PB)��l7 ��RU0c��c+-hjlnI.)+ 56 |   '*" , $�n������//��������GA!�,<����DBD���$&$dfd��䤦����TRTtvt��� 57 | ������464���trt��촲����\^\���LJL���,.,ljl������TVT|~|���  ���<><������{@�pH,�Ȥr�Z@����� F�#�-:��F�*D�i��ϓ�P0(�mW�!gimo$& 58 | " %' on ���������' �����JA!�,<����DFD���,*,������lnl���TVT<:<���  ���|z|������������dbdDBD 59 | ���LNL���424������tvt$"$���\Z\<><�����䄂����{@�pH,�Ȥr�DR���$��s�*��`�!|%��lA���:h4 60 | �� 3�gjlG %  '! & nm�� #"��� 61 | ��� �����mA!�,<�������DFD$"$��䤢�dbd���424��Ԕ�����trt 62 | TRT������<:<��܌��,*,���lnl������LNL��䤦�dfd���464��Ԝ�����|z|  \Z\������<><���,.,���w�pH,�Ȥr�d�$U�E�L�O��99@)Ȁ5�2"��lW!D��ZX��J��\gik(  63 | )'%  !+nn� "�#���� &&�-�������A!�,<����\Z\���<><���|z|���,.,ljlLJL������$"$�����Ԅ��dfdDFD���������\^\���DBD���|~|464trtTRT������$&$�����܌��������n@�pH,�Ȥr�lA��0z`>�� m:��GA� B���R��I�` @��"c5 " #% k�&�� 64 | 65 |  $����% ����kA!�,<�DBD������tvt������\Z\������������LNLljl���|~|���������DFD������|z|������dbd����������TVTlnl���]�pH,�Ȥr�l'D�฀J"�9(0�M�Qq��P��z�P,\� l`�Ц1x|8g�Exz| 66 |  k�������A!�,<�lnl�����ܔ�������Ԭ����������䤢������Č��|z|�����䜚������ܴ�������줦�������J`&�di�h��l�ba%S�0�qEM���k� $#`P�|���( �0�$�FHNI�+��� 67 | ,`qyN��E!!�,<���������켺���������Ĥ��������������������������������? %�di�h��l� �8��L�P ��� �BPk�@�I� (����,D�Պݚ��(!�,<����������������������������������,P�I��8�ͻ��!2��S��� ƀDrl��6\B����d!�,<����������������������h���0�I��#p��EAç~噮p,�L;10240 68 | -------------------------------------------------------------------------------- /static/styles/modules/_comment.less: -------------------------------------------------------------------------------- 1 | 2 | @comment_fontSize: 13px; 3 | @comment_padding: round(@comment_fontSize * 0.75); 4 | @comment_textColor: rgba(75,75,75,1); 5 | @comment_linkColor: rgba(150,150,150,1); 6 | @comment_linkColorHover: rgba(100,100,100,1); 7 | 8 | @keyframes blinkblink { 9 | 0% {opacity:0;} 10 | 33% {opacity:1;} 11 | 66% {opacity:0;} 12 | 100% {opacity:1;} 13 | } 14 | 15 | @-webkit-keyframes blinkblink { 16 | 0% {opacity:0;} 17 | 33% {opacity:1;} 18 | 66% {opacity:0;} 19 | 100% {opacity:1;} 20 | } 21 | 22 | .comment { 23 | 24 | position: relative; 25 | padding: @comment_padding; 26 | padding-left: @comment_padding + 37px; 27 | min-height: 60px; 28 | font-size: @comment_fontSize; 29 | color: @comment_textColor; 30 | transition: all 0.5s; 31 | border: dashed 1px rgba(200,50,50,0); 32 | 33 | a.avatar { 34 | position: absolute; 35 | padding: 0; 36 | display: block; 37 | left: 9px; 38 | top: 14px; 39 | img { 40 | width: 30px; 41 | border-radius: 15px; 42 | } 43 | } 44 | 45 | p { 46 | margin-top: 0; 47 | margin-bottom: round(@comment_fontSize * 0.5); 48 | } 49 | 50 | p.links { 51 | margin: 0; 52 | font-size: round(@comment_fontSize * 0.8); 53 | button { 54 | color: @comment_linkColor; 55 | padding: 4px 6px; 56 | border: none; 57 | background: none; 58 | transition: all 0.5s; 59 | &:hover { 60 | color: @comment_linkColorHover; 61 | } 62 | } 63 | } 64 | 65 | a { 66 | display: inline-block; 67 | padding: 2px 3px; 68 | color: @comment_linkColor; 69 | text-decoration: none; 70 | transition: all 0.5s; 71 | &:hover { 72 | color: @comment_linkColorHover; 73 | } 74 | } 75 | 76 | h3.username { 77 | font-weight: bold; 78 | margin: 0 0 3px 0; 79 | font-size: 1.1em; 80 | } 81 | 82 | .comment { 83 | margin-left: -9px; 84 | } 85 | 86 | &.new-comment, .comment.new-comment { 87 | animation: blinkblink 1.5s; 88 | -webkit-animation: blinkblink 1.5s; 89 | } 90 | 91 | &:hover { 92 | background: rgba(100,100,100,0.05); 93 | } 94 | 95 | } 96 | 97 | .comment .comment .comment .comment .links .replyTo { 98 | display: none; 99 | } 100 | -------------------------------------------------------------------------------- /static/styles/modules/_loading.less: -------------------------------------------------------------------------------- 1 | 2 | #loadingAnimation { 3 | padding-top: 26px; 4 | text-align: center; 5 | background: rgba(255,255,255,0.8); 6 | position: absolute; 7 | z-index: 100; 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | left: 0; 12 | opacity: 0; 13 | transitions: all 1s; 14 | &.visible{ 15 | opacity: 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/styles/modules/_message.less: -------------------------------------------------------------------------------- 1 | 2 | @message_textareaBorderColor: rgb(175,175,175); 3 | @message_textareaTextColor: rgba(100,100,100,1); 4 | @message_textareaPadding : 5px; 5 | @message_textareaFontSize : 12px; 6 | @message_textareaFontLineheight : 1.5; 7 | @message_textareaMinLines : 2; 8 | @message_textareaMaxLines : 15; 9 | @message_submitTextColor: rgba(0,0,0,0.3); 10 | @message_submitTextColorHover: rgba(0,0,0,0.5); 11 | @message_submitbackgroundColor: transparent; 12 | @message_submitbackgroundColorHover: rgba(0,0,0,0.02); 13 | @message_replyTextColor: rgba(150,150,150,1); 14 | 15 | #messageWrap { 16 | border: solid 1px @message_textareaBorderColor; 17 | margin-bottom: round(@message_textareaFontSize / 2); 18 | transition: box-shadow 0.5s; 19 | overflow: hidden; 20 | &:hover { box-shadow: 0px 3px 5px rgba(50, 50, 50, 0.3); } 21 | } 22 | 23 | #messageWrap small { 24 | display: none; 25 | padding: @message_textareaPadding round(@message_textareaPadding*1.25); 26 | color: @message_replyTextColor; 27 | line-height: @message_textareaFontLineheight; 28 | font-size: round(@message_textareaFontSize * 0.8); 29 | &.visible { display: block; } 30 | & * { display: inline; } 31 | } 32 | 33 | #avatar { 34 | display: none; 35 | position: absolute; 36 | top: 6px; 37 | right: 6px; 38 | width: 20px; 39 | height: 20px; 40 | &.visible { display: block; } 41 | } 42 | 43 | #logoutButton { 44 | display: none; 45 | position: absolute; 46 | width: 60px; 47 | text-align: center; 48 | top: 6px; 49 | right: 20px; 50 | color: @message_replyTextColor; 51 | border: none; 52 | padding: 3px 6px; 53 | line-height: @message_textareaFontLineheight; 54 | text-transform: uppercase; 55 | font-size: round(@message_textareaFontSize * 0.75); 56 | background: transparent; 57 | &.visible { display: block; } 58 | } 59 | 60 | #message { 61 | display: block; 62 | width: 100%; 63 | min-height: @message_textareaFontSize * 1.5 * @message_textareaMinLines + @message_textareaPadding * 2; 64 | height: @message_textareaFontSize * 1.5 * 2 + @message_textareaPadding * 2; 65 | max-height: @message_textareaFontSize * 1.5 * @message_textareaMaxLines + @message_textareaPadding * 2; 66 | resize: none; 67 | padding: @message_textareaPadding round(@message_textareaPadding*1.25); 68 | padding-right: 65px; 69 | border: none; 70 | color: @message_textareaTextColor; 71 | line-height: @message_textareaFontLineheight; 72 | font-size: @message_textareaFontSize; 73 | overflow: hidden; 74 | } 75 | 76 | #messageSubmit { 77 | display: none; 78 | width: 100%; 79 | border: none; 80 | background: @message_submitbackgroundColor; 81 | padding: @message_textareaPadding * 2; 82 | padding-top: round(@message_textareaPadding * 2 * 1.5); 83 | font-size: @message_textareaFontSize; 84 | text-transform: uppercase; 85 | line-height: 1; 86 | color: @message_submitTextColor; 87 | transition: all 0.5s; 88 | &:hover { 89 | background: @message_submitbackgroundColorHover; 90 | color: @message_submitTextColorHover; 91 | } 92 | &.visible { display: block; } 93 | } 94 | 95 | #loginWrapper { 96 | display: none; 97 | width: 100%; 98 | border: none; 99 | background: @message_submitbackgroundColor; 100 | padding: @message_textareaPadding * 2; 101 | padding-top: round(@message_textareaPadding * 2 * 1.5); 102 | text-align: center; 103 | font-size: @message_textareaFontSize; 104 | line-height: 1; 105 | color: @message_submitTextColor; 106 | transition: all 0.5s; 107 | &.visible { display: block; } 108 | button { 109 | display: inline-block; 110 | font-size: @message_textareaFontSize; 111 | line-height: 1; 112 | color: @message_submitTextColor; 113 | font-weight: bold; 114 | padding: 0 0 0 0; 115 | background: transparent; 116 | border: none; 117 | transition: all 0.5s; 118 | &:hover { 119 | color: @message_submitTextColorHover; 120 | } 121 | } 122 | &:hover { 123 | background: @message_submitbackgroundColorHover; 124 | } 125 | } 126 | 127 | #unreadComments { 128 | display: block; 129 | width: 100%; 130 | background: none; 131 | border: none; 132 | font-size: 10px; 133 | text-align: right; 134 | color: rgb(150, 150, 150); 135 | transitions: all 0.5s; 136 | opacity: 0; 137 | &.visible { 138 | opacity: 1; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /static/styles/modules/_normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address `[hidden]` styling not present in IE 8/9. 48 | * Hide the `template` element in IE, Safari, and Firefox < 22. 49 | */ 50 | 51 | [hidden], 52 | template { 53 | display: none; 54 | } 55 | 56 | /* ========================================================================== 57 | Base 58 | ========================================================================== */ 59 | 60 | /** 61 | * 1. Set default font family to sans-serif. 62 | * 2. Prevent iOS text size adjust after orientation change, without disabling 63 | * user zoom. 64 | */ 65 | 66 | html { 67 | font-family: 'Helvetica Neue', 'DejaVu Sans', Helvetica, sans-serif; /* 1 */ 68 | -ms-text-size-adjust: 100%; /* 2 */ 69 | -webkit-text-size-adjust: 100%; /* 2 */ 70 | } 71 | 72 | /** 73 | * Remove default margin. 74 | */ 75 | 76 | body { 77 | margin: 0; 78 | padding: 5px; 79 | box-sizing: border-box; 80 | } 81 | 82 | /* ========================================================================== 83 | Links 84 | ========================================================================== */ 85 | 86 | /** 87 | * Remove the gray background color from active links in IE 10. 88 | */ 89 | 90 | a { 91 | background: transparent; 92 | } 93 | 94 | /** 95 | * Address `outline` inconsistency between Chrome and other browsers. 96 | */ 97 | 98 | a:focus { 99 | outline: thin dotted; 100 | } 101 | 102 | /** 103 | * Improve readability when focused and also mouse hovered in all browsers. 104 | */ 105 | 106 | a:active, 107 | a:hover { 108 | outline: 0; 109 | } 110 | 111 | /* ========================================================================== 112 | Typography 113 | ========================================================================== */ 114 | 115 | /** 116 | * Address variable `h1` font-size and margin within `section` and `article` 117 | * contexts in Firefox 4+, Safari 5, and Chrome. 118 | */ 119 | 120 | h1 { 121 | font-size: 2em; 122 | margin: 0.67em 0; 123 | } 124 | 125 | /** 126 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 127 | */ 128 | 129 | abbr[title] { 130 | border-bottom: 1px dotted; 131 | } 132 | 133 | /** 134 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 135 | */ 136 | 137 | b, 138 | strong { 139 | font-weight: bold; 140 | } 141 | 142 | /** 143 | * Address styling not present in Safari 5 and Chrome. 144 | */ 145 | 146 | dfn { 147 | font-style: italic; 148 | } 149 | 150 | /** 151 | * Address differences between Firefox and other browsers. 152 | */ 153 | 154 | hr { 155 | -moz-box-sizing: content-box; 156 | box-sizing: content-box; 157 | height: 0; 158 | } 159 | 160 | /** 161 | * Address styling not present in IE 8/9. 162 | */ 163 | 164 | mark { 165 | background: #ff0; 166 | color: #000; 167 | } 168 | 169 | /** 170 | * Correct font family set oddly in Safari 5 and Chrome. 171 | */ 172 | 173 | code, 174 | kbd, 175 | pre, 176 | samp { 177 | font-family: monospace, serif; 178 | font-size: 1em; 179 | } 180 | 181 | /** 182 | * Improve readability of pre-formatted text in all browsers. 183 | */ 184 | 185 | pre { 186 | white-space: pre-wrap; 187 | } 188 | 189 | /** 190 | * Set consistent quote types. 191 | */ 192 | 193 | q { 194 | quotes: "\201C" "\201D" "\2018" "\2019"; 195 | } 196 | 197 | /** 198 | * Address inconsistent and variable font size in all browsers. 199 | */ 200 | 201 | small { 202 | font-size: 80%; 203 | } 204 | 205 | /** 206 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 207 | */ 208 | 209 | sub, 210 | sup { 211 | font-size: 75%; 212 | line-height: 0; 213 | position: relative; 214 | vertical-align: baseline; 215 | } 216 | 217 | sup { 218 | top: -0.5em; 219 | } 220 | 221 | sub { 222 | bottom: -0.25em; 223 | } 224 | 225 | /* ========================================================================== 226 | Embedded content 227 | ========================================================================== */ 228 | 229 | /** 230 | * Remove border when inside `a` element in IE 8/9. 231 | */ 232 | 233 | img { 234 | border: 0; 235 | } 236 | 237 | /** 238 | * Correct overflow displayed oddly in IE 9. 239 | */ 240 | 241 | svg:not(:root) { 242 | overflow: hidden; 243 | } 244 | 245 | /* ========================================================================== 246 | Figures 247 | ========================================================================== */ 248 | 249 | /** 250 | * Address margin not present in IE 8/9 and Safari 5. 251 | */ 252 | 253 | figure { 254 | margin: 0; 255 | } 256 | 257 | /* ========================================================================== 258 | Forms 259 | ========================================================================== */ 260 | 261 | /** 262 | * Define consistent border, margin, and padding. 263 | */ 264 | 265 | fieldset { 266 | border: 1px solid #c0c0c0; 267 | margin: 0 2px; 268 | padding: 0.35em 0.625em 0.75em; 269 | } 270 | 271 | /** 272 | * 1. Correct `color` not being inherited in IE 8/9. 273 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 274 | */ 275 | 276 | legend { 277 | border: 0; /* 1 */ 278 | padding: 0; /* 2 */ 279 | } 280 | 281 | /** 282 | * 1. Correct font family not being inherited in all browsers. 283 | * 2. Correct font size not being inherited in all browsers. 284 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 285 | */ 286 | 287 | button, 288 | input, 289 | select, 290 | textarea { 291 | outline: none; 292 | box-sizing: border-box; 293 | font-family: inherit; /* 1 */ 294 | font-size: 100%; /* 2 */ 295 | margin: 0; /* 3 */ 296 | } 297 | 298 | /** 299 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 300 | * the UA stylesheet. 301 | */ 302 | 303 | button, 304 | input { 305 | line-height: normal; 306 | } 307 | 308 | /** 309 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 310 | * All other form control elements do not inherit `text-transform` values. 311 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 312 | * Correct `select` style inheritance in Firefox 4+ and Opera. 313 | */ 314 | 315 | button, 316 | select { 317 | text-transform: none; 318 | } 319 | 320 | /** 321 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 322 | * and `video` controls. 323 | * 2. Correct inability to style clickable `input` types in iOS. 324 | * 3. Improve usability and consistency of cursor style between image-type 325 | * `input` and others. 326 | */ 327 | 328 | button, 329 | html input[type="button"], /* 1 */ 330 | input[type="reset"], 331 | input[type="submit"] { 332 | -webkit-appearance: button; /* 2 */ 333 | cursor: pointer; /* 3 */ 334 | } 335 | 336 | /** 337 | * Re-set default cursor for disabled elements. 338 | */ 339 | 340 | button[disabled], 341 | html input[disabled] { 342 | cursor: default; 343 | } 344 | 345 | /** 346 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 347 | * 2. Remove excess padding in IE 8/9/10. 348 | */ 349 | 350 | input[type="checkbox"], 351 | input[type="radio"] { 352 | box-sizing: border-box; /* 1 */ 353 | padding: 0; /* 2 */ 354 | } 355 | 356 | /** 357 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 358 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 359 | * (include `-moz` to future-proof). 360 | */ 361 | 362 | input[type="search"] { 363 | -webkit-appearance: textfield; /* 1 */ 364 | -moz-box-sizing: content-box; 365 | -webkit-box-sizing: content-box; /* 2 */ 366 | box-sizing: content-box; 367 | } 368 | 369 | /** 370 | * Remove inner padding and search cancel button in Safari 5 and Chrome 371 | * on OS X. 372 | */ 373 | 374 | input[type="search"]::-webkit-search-cancel-button, 375 | input[type="search"]::-webkit-search-decoration { 376 | -webkit-appearance: none; 377 | } 378 | 379 | /** 380 | * Remove inner padding and border in Firefox 4+. 381 | */ 382 | 383 | button::-moz-focus-inner, 384 | input::-moz-focus-inner { 385 | border: 0; 386 | padding: 0; 387 | } 388 | 389 | /** 390 | * 1. Remove default vertical scrollbar in IE 8/9. 391 | * 2. Improve readability and alignment in all browsers. 392 | */ 393 | 394 | textarea { 395 | overflow: auto; /* 1 */ 396 | vertical-align: top; /* 2 */ 397 | } 398 | 399 | /* ========================================================================== 400 | Tables 401 | ========================================================================== */ 402 | 403 | /** 404 | * Remove most spacing between table cells. 405 | */ 406 | 407 | table { 408 | border-collapse: collapse; 409 | border-spacing: 0; 410 | } 411 | -------------------------------------------------------------------------------- /static/styles/styles.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:'Helvetica Neue','DejaVu Sans',Helvetica,sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0;padding:5px;box-sizing:border-box}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{outline:none;box-sizing:border-box;font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}#messageWrap{border:solid 1px #afafaf;margin-bottom:6px;transition:box-shadow .5s;overflow:hidden}#messageWrap:hover{box-shadow:0 3px 5px rgba(50,50,50,0.3)}#messageWrap small{display:none;padding:5px 6px;color:#969696;line-height:1.5;font-size:10px}#messageWrap small.visible{display:block}#messageWrap small *{display:inline}#avatar{display:none;position:absolute;top:6px;right:6px;width:20px;height:20px}#avatar.visible{display:block}#logoutButton{display:none;position:absolute;width:60px;text-align:center;top:6px;right:20px;color:#969696;border:none;padding:3px 6px;line-height:1.5;text-transform:uppercase;font-size:9px;background:transparent}#logoutButton.visible{display:block}#message{display:block;width:100%;min-height:46px;height:46px;max-height:280px;resize:none;padding:5px 6px;padding-right:65px;border:none;color:#646464;line-height:1.5;font-size:12px;overflow:hidden}#messageSubmit{display:none;width:100%;border:none;background:transparent;padding:10px;padding-top:15px;font-size:12px;text-transform:uppercase;line-height:1;color:rgba(0,0,0,0.3);transition:all .5s}#messageSubmit:hover{background:rgba(0,0,0,0.02);color:rgba(0,0,0,0.5)}#messageSubmit.visible{display:block}#loginWrapper{display:none;width:100%;border:none;background:transparent;padding:10px;padding-top:15px;text-align:center;font-size:12px;line-height:1;color:rgba(0,0,0,0.3);transition:all .5s}#loginWrapper.visible{display:block}#loginWrapper button{display:inline-block;font-size:12px;line-height:1;color:rgba(0,0,0,0.3);font-weight:bold;padding:0 0 0 0;background:transparent;border:none;transition:all .5s}#loginWrapper button:hover{color:rgba(0,0,0,0.5)}#loginWrapper:hover{background:rgba(0,0,0,0.02)}#unreadComments{display:block;width:100%;background:none;border:none;font-size:10px;text-align:right;color:#969696;transitions:all .5s;opacity:0}#unreadComments.visible{opacity:1}@keyframes blinkblink{0%{opacity:0}33%{opacity:1}66%{opacity:0}100%{opacity:1}}@-webkit-keyframes blinkblink{0%{opacity:0}33%{opacity:1}66%{opacity:0}100%{opacity:1}}.comment{position:relative;padding:10px;padding-left:47px;min-height:60px;font-size:13px;color:#4b4b4b;transition:all .5s;border:dashed 1px rgba(200,50,50,0)}.comment a.avatar{position:absolute;padding:0;display:block;left:9px;top:14px}.comment a.avatar img{width:30px;border-radius:15px}.comment p{margin-top:0;margin-bottom:7px}.comment p.links{margin:0;font-size:10px}.comment p.links button{color:#969696;padding:4px 6px;border:none;background:none;transition:all .5s}.comment p.links button:hover{color:#646464}.comment a{display:inline-block;padding:2px 3px;color:#969696;text-decoration:none;transition:all .5s}.comment a:hover{color:#646464}.comment h3.username{font-weight:bold;margin:0 0 3px 0;font-size:1.1em}.comment .comment{margin-left:-9px}.comment.new-comment,.comment .comment.new-comment{animation:blinkblink 1.5s;-webkit-animation:blinkblink 1.5s}.comment:hover{background:rgba(100,100,100,0.05)}.comment .comment .comment .comment .links .replyTo{display:none}#loadingAnimation{padding-top:26px;text-align:center;background:rgba(255,255,255,0.8);position:absolute;z-index:100;width:100%;height:100%;top:0;left:0;opacity:0;transitions:all 1s}#loadingAnimation.visible{opacity:1} -------------------------------------------------------------------------------- /static/styles/styles.less: -------------------------------------------------------------------------------- 1 | @import 'modules/_normalize.less'; 2 | @import 'modules/_message.less'; 3 | @import 'modules/_comment.less'; 4 | @import 'modules/_loading.less'; 5 | -------------------------------------------------------------------------------- /test/configModel.js: -------------------------------------------------------------------------------- 1 | // var config = require('../lib/models/config.js'); 2 | 3 | 4 | // suite('Config Models', function() { 5 | // test('.deepExtend', function() { 6 | 7 | // var obj1 = { 8 | // a: 1, 9 | // b: 2, 10 | // d: { 11 | // a: 1, 12 | // b: [], 13 | // c: { test1: 123, test2: 321 } 14 | // }, 15 | // f: 5 16 | // }; 17 | 18 | // var obj2 = { 19 | // b: 3, 20 | // c: 5, 21 | // d: { 22 | // b: { first: 'one', second: 'two' }, 23 | // c: { test2: 222 } 24 | // }, 25 | // e: { one: 1, two: 2 }, 26 | // f: [] 27 | // }; 28 | 29 | // var obj3 = obj2; 30 | 31 | // config.savePartial(); 32 | // console.log(obj2.b); 33 | // assert.equal( obj2.b, 3 ); 34 | // }); 35 | 36 | // }); -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('../lib/helpers'); 3 | 4 | suite('Helpers', function() { 5 | test('.urlToHash', function() { 6 | var hash1 = helpers.urlToHash('http://google.com/aaa'); 7 | var hash2 = helpers.urlToHash('http://google.com/aaa'); 8 | 9 | assert.ok(hash1.length > 0); 10 | assert.equal(hash1, hash2); 11 | }); 12 | 13 | test('.urlToDomain', function() { 14 | var domain = helpers.urlToDomain('http://google.com/aa/ssdsd'); 15 | assert.equal(domain, 'google.com'); 16 | }); 17 | }); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd 2 | --recursive 3 | -R spec -------------------------------------------------------------------------------- /test/socketManager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var SocketManager = require('../lib/socketManager'); 3 | 4 | suite('socketManager', function() { 5 | test('.addSocket', function() { 6 | var socket1 = {}; 7 | var socket2 = {}; 8 | 9 | var sm = new SocketManager(); 10 | sm.addSocket("h1", socket1); 11 | sm.addSocket("h1", socket2); 12 | assert.equal(sm.sockets['h1'].length, 2); 13 | }); 14 | 15 | test('.removeSocket', function() { 16 | var socket1 = {}; 17 | var socket2 = {}; 18 | 19 | var sm = new SocketManager(); 20 | sm.addSocket("h1", socket1); 21 | sm.addSocket("h1", socket2); 22 | assert.equal(sm.sockets['h1'].length, 2); 23 | 24 | sm.removeSocket("h1", socket2); 25 | assert.equal(sm.sockets['h1'].length, 1); 26 | assert.equal(sm.sockets['h1'][0], socket1); 27 | }); 28 | 29 | test('.send', function(done) { 30 | var socket1 = { 31 | write: function(data) { 32 | assert.equal(data, JSON.stringify({aa: 10})); 33 | done(); 34 | } 35 | }; 36 | 37 | var sm = new SocketManager(); 38 | sm.addSocket("h1", socket1); 39 | 40 | sm.send("h1", {aa: 10}); 41 | }); 42 | }); -------------------------------------------------------------------------------- /views/admin/createadmin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Open Comment Box - Set Up a new Admin 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |

Create OCB Admin Account

28 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /views/admin/index.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= title %> 4 |
-------------------------------------------------------------------------------- /views/admin/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Open Comment Box - Admininstrator Login 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |

Sign in to OCB Admin

29 | 40 |
41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /views/admin/moderate.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Open Comment Box - Comments Moderation 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <%- include navigation.ejs %> 26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 | 36 | 49 |
50 | 51 | Comment Moderation 52 |
53 |
54 | 55 |
56 |
    57 | 58 | <% comments.forEach(function(comment){ %> 59 |
  • 60 | 61 |
    62 |
    63 | <% if (users[comment.userId]){ %> 64 | 65 | 66 | <% } else { %> 67 | 68 | <% } %> 69 | 70 |
    71 |
    72 | 73 | <% if (users[comment.userId]){ %> 74 | <%= users[comment.userId].name %> 75 | <% } else { %> 76 | Annonymous 77 | <% } %> 78 | 79 | | <%= comment.createdAt %> 80 |
    <%- comment.message %>
    81 | 82 | <% if (comment.approved) { %> 83 | 84 | 85 | 86 | 87 | <% } else { %> 88 | 89 | 90 | 91 | <% }%> 92 | 93 | 94 | 95 |
    96 | 97 |
    98 |
  • 99 | <% }) %> 100 |
101 |
    102 | <% if(prevPage) { %>
  • Previous
  • <% } %> 103 |
  • Next
  • 104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /views/admin/navigation.ejs: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /views/admin/settings.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Open Comment Box - Comments Moderation 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <%- include navigation.ejs %> 24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 | Settings 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | APP URL 40 | 41 |
42 | 43 |
44 | Facebook 45 |
46 | 47 | 48 |
49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 | 60 |
61 | Twitter 62 |
63 | 64 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 | Google 82 |
83 | 84 | 85 |
86 | 87 |
88 | 89 | 90 |
91 | 92 |
93 | 94 | 95 |
96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 | 105 |
106 |
107 |
108 | 109 |
110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /views/browser_send.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |

Redirecting...

23 | 24 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Open Comment Box 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 |

Open Comment Box

44 |

Open Source Realtime Comments Platform

45 |

Easy way to power your website with comments

46 |

[ you own the data, and you have it ]

47 |
48 | 50 |
51 | vote us on NodeKnockout - click above badge 52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 |
62 |

What you think about Open Comment Box?

63 |
(This comment box is powered by Open Comment Box)
64 | 65 | 76 |
77 |
78 |

Add comments to your web site

79 | 80 | 81 | 82 | 85 | 86 | 87 | 88 | 89 |

Deploy Open Comment Box (DIY)

90 |
91 | 97 | 103 | 109 | 115 |
116 | 117 | 118 |

Integrations

119 |
120 | 126 | 132 |
133 |
134 |
135 |
136 | 137 |
138 | Open Comment Box produdly presented by MeteorHacks 139 |
140 | 141 | 142 | 143 | 153 | 154 | 155 | 156 | --------------------------------------------------------------------------------