This commit is contained in:
root 2022-11-27 20:58:22 +08:00
commit c4b358452c
99 changed files with 2543 additions and 0 deletions

3
.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "public/libs"
}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
public/libs
coverage
npm-debug.log

21
.travis.yml Normal file
View File

@ -0,0 +1,21 @@
language: node_js
node_js:
- "5.5.0"
services:
- mongodb
before_install:
- npm install -g grunt-cli
install:
- npm install
before_script:
- bower install
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- ./node_modules/protractor/bin/webdriver-manager update --standalone
- npm run start-test &
- sleep 5
script:
- grunt protractor_webdriver
- node_modules/.bin/protractor test/e2e/conf.js --browser=firefox
- npm run test-jasmine
- grunt karma

57
Gruntfile.js Normal file
View File

@ -0,0 +1,57 @@
var path = require('path');
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
mongobin: {
options: {
host: 'localhost',
port: '27017',
db: 'makers-achievements-test'
}
},
express: {
options: {
port: process.env.PORT || 8080,
hostname: 'localhost'
},
test: {
options: {
server: path.resolve('./app')
},
}
},
karma: {
options: {
configFile: './test/front_end/karma.conf.js'
},
run: {
}
},
protractor: {
options: {
configFile: './test/e2e/conf.js',
keepAlive: true
},
run: {
}
},
protractor_webdriver: {
start: {
options: {
path: 'node_modules/protractor/bin/',
command: 'webdriver-manager start'
}
}
}
});
grunt.loadNpmTasks('grunt-mongo-bin');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-express');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-protractor-runner');
grunt.loadNpmTasks('grunt-protractor-webdriver');
grunt.registerTask('e2e', ['express:test', 'protractor_webdriver', 'protractor']);
};

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# ![Alt text](http://i.imgur.com/9ptL8yf.png, 'Netstix') [![Build Status](https://travis-ci.org/michaellennox/netstix.svg?branch=master)](https://travis-ci.org/michaellennox/netstix)
An online achievement platform for tracking and displaying the brilliant things Makers students do.
Initially inspired out of the Makers sticker system we have expanded the idea to be a fully interactive achievement system which allows users to keep track of any awesome things they do, create new achievements if they believe there is something other's should have a crack at and feel their competitive spirits rush as they race to the top of the leaderboard.
![Alt text](http://i.imgur.com/7bfktU1.png, 'screenshot app')
## So What's Here?
Right now there's just the basics of the NetStix platform:
* Local user authentication, sign up, sign in and sign out.
* The ability to create an achievement, view a list of achievements and view further detail on a specific achievement
* The ability to view a leaderboard of all users and their scores, ability to view a specific user and see everything they have done
* The ability to make submissions to an achievement and view proof from other locations
## Technologies
__API/Server__
* NodeJS
* ExpressJS
* MongoDB/Mongoose
* User Authentication with Passport
* Tested with jasmine-node and Frisby
__Client__
* AngularJS
* Tested with Jasmine, Karma and Protractor
## Installation Instructions
You can try the app remotely:
>[https://netstix.herokuapp.com/](https://netstix.herokuapp.com/)
or install it locally:<br>
Clone down from github and cd into the directory
```
$ git clone git@github.com:michaellennox/netstix.git
$ cd netstix
```
If you don't have MongoDB installed you will have to get it:
```
$ brew update
$ brew install mongodb
$ sudo mkdir -p /data/db
$ sudo chown <your username> /data/db
```
Install any dependencies then run the app
```
$ npm install
$ npm start
```
Visit `http://localhost:8080/#/` and enjoy your new, beautiful and awesome achievement system.
## Future Improvements
* BETA testing for achievements (think like codewars BETA tasks)
* Submission should be approved before applying to a user's profile
* Restrictions for different levels of users for eg voting BETA achievements through, approving submissions
## Contributions
Feel free to get involved! Our waffleboard is available at https://waffle.io/michaellennox/netstix.
## Contributors
* [Michael Lennox](https://github.com/michaellennox)
* [Tom Bradley](https://github.com/trbradley)
* [Giamir Buoncristiani](https://github.com/giamir)
* [Andrew Htun](https://github.com/Htunny)

80
app.js Normal file
View File

@ -0,0 +1,80 @@
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var indexRouter = require('./app/routes/indexRouter');
var achievementsRouter = require('./app/routes/achievementsRouter');
var usersRouter = require('./app/routes/usersRouter');
var sessionsRouter = require('./app/routes/sessionsRouter');
var app = express();
app.set('views', path.join(__dirname, 'app/views'));
app.set('view engine', 'jade');
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '/public')));
app.use(require('express-session')({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
// routing config
app.use('/', indexRouter);
app.use('/achievements', achievementsRouter);
app.use('/users', usersRouter);
app.use('/sessions', sessionsRouter);
// passport config
var User = require('./app/models/user');
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;

View File

@ -0,0 +1,45 @@
var Achievement = require('../models/achievement');
var controller = {};
controller.list = function(req, res) {
Achievement.find(function(err, achievements) {
if(err) {
res.send(err);
}
res.json(achievements);
});
};
controller.create = function(req, res) {
var achievement = new Achievement();
achievement.title = req.body.title;
achievement.criteria = req.body.criteria;
achievement.points = req.body.points;
achievement.badgeLink = req.body.badgeLink;
achievement.challengeRepo = req.body.challengeRepo;
achievement.save(function(err) {
if(err) {
res.send(err);
}
res.json({ message: 'Achievement created!'});
});
};
controller.read = function(req, res) {
Achievement.findById(req.params.id)
.populate({
path: 'submissions',
populate: {
path: 'user'
}
})
.exec(function(err, achievement) {
if(err) {
res.send(err);
}
res.json(achievement);
});
};
module.exports = controller;

View File

@ -0,0 +1,28 @@
var passport = require('passport');
var User = require('../models/user');
var controller = {};
controller.create = function(req, res) {
passport.authenticate('local', function(err, user, info) {
if(err) {
return res.status(500).json({ err: err });
}
if(!user) {
return res.status(401).json({ err: info });
}
req.logIn(user, function(err) {
if(err) {
return res.status(500).json({ err: 'Could not log in user' });
}
res.status(200).json({ status: 'Login successful!', user: user });
});
})(req, res);
};
controller.destroy = function(req, res) {
req.logout();
res.status(200).json({ status: 'Signed out successfully!' });
};
module.exports = controller;

View File

@ -0,0 +1,45 @@
var Submission = require('../models/submission');
var User = require('../models/user');
var Achievement = require('../models/achievement');
var controller = {};
controller.create = function(req, res) {
var submission = new Submission();
submission.link = req.body.link;
submission.comment = req.body.comment;
Achievement.findById(req.params.id, function(err, achievement) {
if(err) {
res.send(err);
}
submission.achievement = achievement.id;
achievement.submissions.push(submission.id);
achievement.save(function(err) {
if(err) {
res.send(err);
}
User.findById(req.user._id, function(err, user) {
if(err) {
res.send(err);
}
user.score += achievement.points;
submission.user = user.id;
user.submissions.push(submission.id);
user.save(function(err) {
if(err) {
res.send(err);
}
submission.save(function(err) {
if(err) {
res.send(err);
}
res.json({ message: 'Submission created!' });
});
});
});
});
});
};
module.exports = controller;

View File

@ -0,0 +1,47 @@
var passport = require('passport');
var User = require('../models/user');
var controller = {};
controller.list = function(req, res) {
User.find(function(err, users) {
if(err) {
res.send(err);
}
res.json(users);
});
};
controller.create = function(req, res) {
var newUser = new User({
username: req.body.username
});
User.register(
newUser, req.body.password, function(err, account) {
if(err) {
return res.status(500).json({ err: err });
}
passport.authenticate('local')(req, res, function() {
return res.status(200).json({ status: 'Registration successful!', user: newUser });
});
}
);
};
controller.read = function(req, res) {
User.findById(req.params.id)
.populate({
path: 'submissions',
populate: {
path: 'achievement'
}
})
.exec(function(err, user) {
if(err) {
res.send(err);
}
res.json(user);
});
};
module.exports = controller;

14
app/models/achievement.js Normal file
View File

@ -0,0 +1,14 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;
var achievementSchema = new Schema({
title : String,
criteria : String,
challengeRepo : String,
points : { type: Number, default: 0},
badgeLink : { type: String, default: '/images/badges/nobadge.png' },
submissions : [{ type: ObjectId, ref: 'Submission' }]
});
module.exports = mongoose.model('Achievement', achievementSchema);

12
app/models/submission.js Normal file
View File

@ -0,0 +1,12 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;
var submissionSchema = new Schema({
link : String,
comment : String,
achievement : { type: ObjectId, ref: 'Achievement' },
user : { type: ObjectId, ref: 'User' }
});
module.exports = mongoose.model('Submission', submissionSchema);

16
app/models/user.js Normal file
View File

@ -0,0 +1,16 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;
var passportLocalMongoose = require('passport-local-mongoose');
var userSchema = new Schema({
username : String,
password : String,
score : { type: Number, default: 0 },
submissions : [{ type: ObjectId, ref: 'Submission' }]
});
userSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model('User', userSchema);

View File

@ -0,0 +1,12 @@
var express = require('express');
var router = express.Router();
var achievementsController = require('../controllers/achievementsController');
router.route('/').get(achievementsController.list).post(achievementsController.create);
router.route('/:id').get(achievementsController.read);
var submissionsRouter = require('./submissionsRouter');
router.use('/:id/submissions', submissionsRouter);
module.exports = router;

View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
var path = require('path');
router.get('/', function(req, res) {
res.sendFile(path.join(__dirname, '../../public/views', 'index.html'));
});
module.exports = router;

View File

@ -0,0 +1,7 @@
var express = require('express');
var router = express.Router();
var sessionsController = require('../controllers/sessionsController');
router.route('/').post(sessionsController.create).delete(sessionsController.destroy);
module.exports = router;

View File

@ -0,0 +1,8 @@
var express = require('express');
var router = express.Router({ mergeParams: true });
var submissionsController = require('../controllers/submissionsController');
router.route('/').post(submissionsController.create);
module.exports = router;

View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
var usersController = require('../controllers/usersController');
router.route('/').post(usersController.create).get(usersController.list);
router.route('/:id').get(usersController.read);
module.exports = router;

6
app/views/error.jade Normal file
View File

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

5
app/views/layout.jade Normal file
View File

@ -0,0 +1,5 @@
doctype html
html
head
body
block content

31
bower.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "netstix",
"description": "achievement application",
"main": "",
"authors": [
"michaellennox",
"trbradley",
"htunny",
"giamir"
],
"license": "MIT",
"moduleType": [],
"homepage": "",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "^2.2.0",
"bootstrap": "^3.3.6",
"angular": "^1.4.9",
"angular-resource": "^1.4.9",
"angular-route": "^1.4.9",
"angular-mocks": "^1.4.9",
"angular-bootstrap": "~1.1.2",
"angular-loading-bar": "^0.8.0"
}
}

99
config/env/dev vendored Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../../app');
var debug = require('debug')('makers-achievements:server');
var http = require('http');
var mongoose = require('mongoose');
/**
* Connect to MongoDB
*/
mongoose.connect('mongodb://localhost/makers-achievements-development', function(err, res) {
if(err) {
console.log('Error connecting to the database. ' + err);
} else {
console.log('Connected to Database: ' + 'mongodb://localhost/makers-achievements-development');
}
});
var port = normalizePort(process.env.PORT || '8080');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

103
config/env/test vendored Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../../app');
var debug = require('debug')('makers-achievements:server');
var http = require('http');
var mongoose = require('mongoose');
/**
* Connect to MongoDB
*/
mongoose.connect('mongodb://localhost/makers-achievements-test', function(err, res) {
if(err) {
console.log('Error connecting to the database. ' + err);
} else {
console.log('Connected to Database: ' + 'mongodb://localhost/makers-achievements-test');
}
});
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '8080');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

99
config/env/www vendored Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../../app');
var debug = require('debug')('makers-achievements:server');
var http = require('http');
var mongoose = require('mongoose');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '8080');
app.set('port', port);
mongoose.connect(process.env.MONGOLAB_URI, function(err, res) {
if(err) {
console.log('Error connecting to the database. ' + err);
} else {
console.log('Connected to Database: ' + process.env.MONGOLAB_URI);
}
});
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "netstix",
"version": "0.0.0",
"description": "achievement application",
"scripts": {
"start": "node ./config/env/www",
"start-dev": "nodemon --debug ./config/env/dev",
"start-test": "nodemon --debug ./config/env/test",
"test": "npm run test-protractor && npm run test-karma && npm run test-jasmine",
"update-webdriver": "./node_modules/protractor/bin/webdriver-manager update --standalone --chrome",
"test-protractor": "npm run update-webdriver && protractor test/e2e/conf.js",
"test-karma": "grunt karma",
"test-jasmine": "npm run drop-test-db; jasmine-node test/server/controllers/userAuthSpec.js; jasmine-node test/server/controllers/achievementsSpec.js; jasmine-node test/server/controllers/submissionsSpec.js; npm run drop-test-db",
"drop-test-db": "mongo makers-achievements-test --eval 'db.dropDatabase()'",
"postinstall": "bower install"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/michaellennox/netstix.git"
},
"author": "michaellennox, trbradley, htunny, giamir",
"license": "MIT",
"bugs": {
"url": "https://github.com/michaellennox/netstix/issues"
},
"homepage": "https://github.com/michaellennox/netstix#readme",
"dependencies": {
"body-parser": "^1.14.2",
"bower": "^1.7.7",
"cookie-parser": "^1.4.1",
"debug": "^2.2.0",
"express": "^4.13.4",
"express-session": "^1.13.0",
"grunt": "^0.4.5",
"jade": "^1.11.0",
"kerberos": "0.0.18",
"mongoose": "^4.3.7",
"mongoose-autopopulate": "^0.4.0",
"morgan": "^1.6.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^4.0.0",
"serve-favicon": "^2.3.0"
},
"devDependencies": {
"frisby": "^0.8.5",
"grunt-contrib-watch": "^0.6.1",
"grunt-express": "^1.4.1",
"grunt-express-server": "^0.5.1",
"grunt-karma": "^0.12.1",
"grunt-mongo-bin": "^0.1.0",
"grunt-parallel": "^0.4.1",
"grunt-protractor-runner": "^3.0.0",
"grunt-protractor-webdriver": "^0.2.5",
"jasmine-core": "^2.4.1",
"jasmine-node": "^1.14.5",
"jasmine-spec-reporter": "^2.4.0",
"karma": "^0.13.19",
"karma-chrome-launcher": "^0.2.2",
"karma-coverage": "^0.5.3",
"karma-jasmine": "^0.3.7",
"karma-phantomjs-launcher": "^1.0.0",
"karma-spec-reporter": "0.0.23",
"nodemon": "^1.8.1",
"phantomjs": "^2.1.3",
"phantomjs-prebuilt": "^2.1.3",
"protractor": "^3.0.0",
"protractor-http-mock": "^0.2.1",
"webdriver-manager": "^8.0.0"
}
}

302
public/css/style.css Normal file
View File

@ -0,0 +1,302 @@
/* universal */
@media (min-width: 1200px) {
.container{
max-width: 800px;
}
}
html, body {
background: #fafafa;
color: #4b4f54;
font-family: 'proxima-nova', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-size: 14px;
height: 100%;
}
body.container {
padding: 0;
}
a {
color: #125688;
font-weight: 600;
}
a:hover{
text-decoration: none;
color: #4b4f54;
}
.form{
min-height: 500px;
margin-bottom: 50px;
}
.form label{
font-weight: normal;
}
h1{
display: inline-block;
margin: 0;
color: #4b4f54;
font-size: 36px;
}
header{
margin: 0 0 30px 0;
padding: 0 0 10px 0;
border-bottom: 2px solid #4b4f54;
}
header b{
font-size: 16px;
padding: 10px 10px 10px 0;
}
header a i{
font-size: 20px;
padding-right: 10px;
}
header a{
padding: 10px 10px 10px 0;
}
form h2{
margin-bottom: 40px;
}
input[type="text"],
input[type="password"],
input[type="url"],
input[type="number"],
textarea {
outline: none;
box-shadow:none !important;
border: none;
}
input.form-control, textarea{
background: #fafafa;
margin-bottom: 20px;
border-radius: 0;
border-bottom: 1px solid #ccc;
}
button[type='submit']{
background: #e9e9e9;
margin: 20px 0 30px;
border: none;
border-radius: 0;
color: #4b4f54;
font-weight: 600;
}
button[type='submit']:hover{
background: #ccc;
border-radius: 0;
color: #4b4f54;
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0 500px #fafafa inset !important;
}
.alert{
font-size: 10px;
background: #125688;
color: white;
text-align: center;
display: block;
text-transform: uppercase;
margin: 0 auto;
}
p.alert{
margin: 0;
}
/* navbar */
nav {
width: 100%;
height: 70px;
background: white;
border-radius: 0;
position: absolute;
top: 0;
left: 0;
border-bottom: 1px solid #edeeee;
}
.navbar{
margin-bottom: 0px;
}
.navbar-toggle:before{
content:"\e055";;
font-family:"Glyphicons Halflings";
line-height:1;
margin:10px;
display:inline-block;
}
.navbar-toggle{
margin-bottom: -16px;
}
.nav > li{
background-color: white;
padding-left: 10px;
}
.nav > li > a{
font-size:14px;
padding: 24px 7px;
}
.nav > li > a:hover, .nav > li > a:focus, .nav > li > a:visited{
background: white;
}
.navbar-brand {
background: url('../images/netstix.png') no-repeat center center;
display:block;
height:29px;
text-indent:-9999px;
width:104px;
margin-top: 21px;
margin-left: 10px;
}
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus{
background-color: white;
color: #125688;
}
.dropdown-menu > li > a, .dropdown-menu > li > button{
background: white;
color: #125688;
font-weight: 600;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > button:hover{
text-decoration: none;
background: white;
color: #4b4f54;
}
.navbar-right .dropdown-menu{
text-align: center;
left: -50px
}
/* content */
.content{
min-height: 100%;
}
.content > div{
margin-top: 120px;
padding: 30px 50px;
padding-bottom: 80px;
}
.description{
margin-bottom: 120px;
font-size: 20px;
}
.description figure{
padding-top: 25px;
}
.description .criteria h3{
margin: 0 0 15px;
padding: 0 0 5px;
border-bottom: 2px solid #4b4f54;
}
li.list-group-item{
background: transparent;
border:0;
padding:25px;
border-bottom: 1px solid #e0e0e0;
}
li.list-group-item > i{
font-size: 20px;
padding-right: 25px;
}
li.list-group-item > img{
margin-right: 25px;
}
li.list-group-item .pull-right{
padding-top: 10px;
}
/* footer */
footer {
padding: 10px;
height: 80px;
text-align: center;
font-size: 12px;
position: relative;
margin-top: -80px;
clear:both;
}
/* mobile */
@media (max-width: 767px){
body{
text-align: center;
}
h1{
font-size: 24px;
}
h2{
font-size: 20px;
}
h3{
font-size: 18px;
}
header{
margin-bottom: 20px;
}
.achievements li > a, .user li > a {
display: block;
}
.achievements ul > li > img, .user ul > li > img {
margin: 10px auto;
}
figure img.pull-right{
width: 100px;
height: 100px;
display: block;
margin: 0 auto;
float: none !important;
}
.nav{
position: absolute;
width: 100%;
z-index: 9999;
background: white;
padding-bottom: 20px;
}
.nav > li > a {
padding: 7px 10px 7px 0;
}
.nav > li > button{
color: #125688;
font-weight: 600;
}
.content > div{
margin-top: 22px;
}
.description{
margin-bottom: 80px;
font-size: 16px;
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/images/netstix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

37
public/js/app.js Normal file
View File

@ -0,0 +1,37 @@
var netstix = angular.module('Netstix', [
'ngResource',
'ngRoute',
'ui.bootstrap',
'angular-loading-bar'
]);
netstix.config(['$routeProvider',
function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: '../views/partials/users/leaderboard.html'
})
.when('/achievements/', {
templateUrl: '../views/partials/achievements/index.html'
})
.when('/achievements/new', {
templateUrl: '../views/partials/achievements/new.html'
})
.when('/achievements/:id', {
templateUrl: '../views/partials/achievements/achievement.html'
})
.when('/login', {
templateUrl: '../views/partials/users/login.html'})
.when('/register', {
templateUrl: '../views/partials/users/register.html'})
.when('/users/:id', {
templateUrl: '../views/partials/users/user.html'
})
.when('/achievements/:id/submissions/new', {
templateUrl: '../views/partials/submissions/new.html'
})
.otherwise({
redirectTo: '/'
});
}
]);

View File

@ -0,0 +1,17 @@
netstix.controller('AchievementController', ['AchievementsResource', 'UserAuth', '$routeParams', function(AchievementsResource, UserAuth, $routeParams) {
var self = this;
self.id = $routeParams.id;
self.init = function() {
AchievementsResource.getAchievement(self.id)
.then(function(response) {
self.achievement = response.data;
});
};
self.isLoggedIn = function() {
return UserAuth.isLoggedIn();
};
self.init();
}]);

View File

@ -0,0 +1,16 @@
netstix.controller('AchievementsController', ['AchievementsResource', 'UserAuth', function(AchievementsResource, UserAuth) {
var self = this;
self.init = function() {
AchievementsResource.getAchievements()
.then(function(response) {
self.achievements = response.data;
});
};
self.isLoggedIn = function() {
return UserAuth.isLoggedIn();
};
self.init();
}]);

View File

@ -0,0 +1,21 @@
netstix.controller('LoginController', ['UserAuth', '$window', '$scope', function(UserAuth, $window, $scope) {
var self = this;
self.login = function() {
$scope.error = false;
$scope.disabled = true;
UserAuth.login($scope.loginForm.username, $scope.loginForm.password)
.then(function () {
$window.location.href ='/#/achievements';
$scope.disabled = false;
$scope.loginForm = {};
})
.catch(function () {
$scope.error = true;
$scope.errorMessage = "Invalid username and/or password";
$scope.disabled = false;
$scope.loginForm = {};
});
};
}]);

View File

@ -0,0 +1,18 @@
netstix.controller('LogoutController', ['UserAuth', '$window', '$scope', function(UserAuth, $window, $scope) {
var self = this;
self.logout = function () {
UserAuth.logout()
.then(function () {
$window.location.href = '/#/achievements';
});
};
self.isLoggedIn = function() {
return UserAuth.isLoggedIn();
};
self.getUser = function() {
return UserAuth.getUserStatus();
};
}]);

View File

@ -0,0 +1,11 @@
netstix.controller('NewAchievementController', ['AchievementsResource', '$window', function(AchievementsResource, $window) {
var self = this;
self.createNewAchievement = function() {
AchievementsResource.postAchievements(self.title, self.criteria, self.points, self.challengeRepo, self.badgeLink)
.then(function() {
$window.location.href ='/#/achievements';
});
};
}]);

View File

@ -0,0 +1,12 @@
netstix.controller('NewSubmissionController', ['AchievementsResource', '$window', '$routeParams', function(AchievementsResource, $window, $routeParams) {
var self = this;
self.id = $routeParams.id;
self.createNewSubmission = function() {
AchievementsResource.postSubmissions(self.link, self.comment, self.id)
.then(function() {
$window.location.href = ('/#/achievements/' + self.id);
});
};
}]);

View File

@ -0,0 +1,21 @@
netstix.controller('RegisterController', ['UserAuth', '$window', '$scope', function(UserAuth, $window, $scope) {
var self = this;
self.register = function () {
$scope.error = false;
$scope.disabled = true;
UserAuth.register($scope.registerForm.username, $scope.registerForm.password)
.then(function () {
$window.location.href ='/#/achievements';
$scope.disabled = false;
$scope.registerForm = {};
})
.catch(function () {
$scope.error = true;
$scope.errorMessage = "Something went wrong!";
$scope.disabled = false;
$scope.registerForm = {};
});
};
}]);

View File

@ -0,0 +1,13 @@
netstix.controller('UserController', ['UsersResource', '$routeParams', function(UsersResource, $routeParams) {
var self = this;
self.id = $routeParams.id;
self.init = function() {
UsersResource.getData(self.id)
.then(function(response) {
self.user = response.data;
});
};
self.init();
}]);

View File

@ -0,0 +1,12 @@
netstix.controller('UsersController', ['UsersResource', function(UsersResource) {
var self = this;
self.init = function() {
UsersResource.getData()
.then(function(response) {
self.users = response.data;
});
};
self.init();
}]);

View File

@ -0,0 +1,51 @@
netstix.factory('AchievementsResource', ['$http', '$q', function($http, $q) {
var achievementsResource = {};
achievementsResource.postAchievements = function(title, criteria, points, challengeRepo, badgeLink) {
var deferred = $q.defer();
$http.post('/achievements', {title: title, criteria: criteria, points: points, challengeRepo: challengeRepo, badgeLink: badgeLink})
.success(function (data, status) {
if(status === 200){
deferred.resolve(data);
} else {
deferred.reject();
}
})
.error(function (data) {
deferred.reject();
});
return deferred.promise;
};
achievementsResource.getAchievement = function(id) {
return $http({
url: ('/achievements/' + id),
method: 'GET'
});
};
achievementsResource.getAchievements = function() {
return $http({
url: '/achievements',
method: 'GET'
});
};
achievementsResource.postSubmissions = function(link, comment, id) {
var deferred = $q.defer();
$http.post('/achievements/' + id + '/submissions', {link: link, comment: comment})
.success(function (data, status) {
if(status === 200){
deferred.resolve(data);
} else {
deferred.reject();
}
})
.error(function (data) {
deferred.reject();
});
return deferred.promise;
};
return achievementsResource;
}]);

View File

@ -0,0 +1,75 @@
netstix.factory('UserAuth', ['$q', '$timeout', '$http', function ($q, $timeout, $http) {
var user = null;
function isLoggedIn() {
if(user) {
return true;
} else {
return false;
}
}
function getUserStatus() {
return user;
}
function login(username, password) {
var deferred = $q.defer();
$http.post('/sessions', {username: username, password: password})
.success(function (data, status) {
if(status === 200 && data.status){
user = data.user;
deferred.resolve();
} else {
user = false;
deferred.reject();
}
})
.error(function (data) {
user = false;
deferred.reject();
});
return deferred.promise;
}
function logout() {
var deferred = $q.defer();
$http.delete('/sessions')
.success(function (data) {
user = false;
deferred.resolve();
})
.error(function (data) {
user = false;
deferred.reject();
});
return deferred.promise;
}
function register(username, password) {
var deferred = $q.defer();
$http.post('/users', {username: username, password: password})
.success(function (data, status) {
if(status === 200 && data.status){
user = data.user;
deferred.resolve();
} else {
user = false;
deferred.reject();
}
})
.error(function (data) {
user = false;
deferred.reject();
});
return deferred.promise;
}
return ({
isLoggedIn: isLoggedIn,
getUserStatus: getUserStatus,
login: login,
logout: logout,
register: register
});
}]);

View File

@ -0,0 +1,11 @@
netstix.factory('UsersResource', ['$http', function($http) {
return {
getData: function(id) {
id = typeof id !== 'undefined' ? id : '';
return $http({
url: 'users/' + id,
method: 'GET'
});
}
};
}]);

52
public/views/index.html Normal file
View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en" ng-app="Netstix">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico"> -->
<title>Netstix</title>
<link rel="stylesheet" href="../libs/bootstrap/dist/css/bootstrap.min.css">
<link rel='stylesheet' href='../libs/angular-loading-bar/build/loading-bar.min.css'>
<link rel="stylesheet" href="../css/style.css">
<script src="../libs/jquery/dist/jquery.min.js"></script>
<script src="../libs/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../libs/angular/angular.min.js"></script>
<script src="../libs/angular-resource/angular-resource.min.js"></script>
<script src="../libs/angular-route/angular-route.min.js"></script>
<script src="../libs/angular-mocks/angular-mocks.js"></script>
<script src="../libs/angular-bootstrap/ui-bootstrap.min.js"></script>
<script src='../libs/angular-loading-bar/build/loading-bar.min.js'></script>
<script src="../js/app.js"></script>
<script src="../js/controllers/loginController.js"></script>
<script src="../js/controllers/logoutController.js"></script>
<script src="../js/controllers/registerController.js"></script>
<script src="../js/controllers/achievementsController.js"></script>
<script src="../js/controllers/newAchievementController.js"></script>
<script src="../js/controllers/newSubmissionController.js"></script>
<script src="../js/controllers/achievementController.js"></script>
<script src="../js/controllers/usersController.js"></script>
<script src="../js/controllers/userController.js"></script>
<script src="../js/factories/userAuthFactory.js"></script>
<script src="../js/factories/achievementsResourceFactory.js"></script>
<script src="../js/factories/usersResourceFactory.js"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<ng-include src="'views/partials/navbar.html'"></ng-include>
<div ng-view class="content container"></div>
<ng-include src="'views/partials/footer.html'"></ng-include>
</body>
</html>

View File

@ -0,0 +1,26 @@
<div class="container achievement row" ng-controller="AchievementController as ctrl">
<header>
<h1>{{ctrl.achievement.title}}</h1>
<a ng-href="/#/achievements/{{ctrl.achievement._id}}/submissions/new" class="hidden-xs pull-right" ng-show="ctrl.isLoggedIn()"><i class="glyphicon glyphicon-download-alt"></i> Submit</a>
</header>
<section class="description row">
<section class="col-xs-12 criteria col-md-7">
<h3>criteria</h3>
<p>{{ctrl.achievement.criteria}}</p>
<p ng-show="ctrl.achievement.challengeRepo"><a ng-href="{{ctrl.achievement.challengeRepo}}" target="blank">challenge details</a></p>
<p><b>#{{ctrl.achievement.points}} points</b></p>
</section>
<figure class="col-xs-12 col-md-offset-2 col-md-3">
<img class="pull-right" ng-src="{{ctrl.achievement.badgeLink}}" width="100px" height="100px"/>
</figure>
</section>
<a ng-href="/#/achievements/{{ctrl.achievement._id}}/submissions/new" class="hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()"><i class="glyphicon glyphicon-download-alt"></i> Submit</a>
<header>
<h2>rockstars</h2>
</header>
<ul class="items-result list-group">
<li class="list-group-item" ng-repeat="submission in ctrl.achievement.submissions">
<i class="glyphicon glyphicon-user hidden-xs "></i><a ng-href="/#/users/{{submission.user._id}}">{{submission.user.username}}</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,14 @@
<div class="container achievements" ng-controller="AchievementsController as ctrl">
<header>
<h1>achievements</h1>
<a href="#/achievements/new" class="pull-right hidden-xs " ng-show="ctrl.isLoggedIn()" title="Create a new achievement"><i class="glyphicon glyphicon-plus"></i>Create a new achievement</a>
</header>
<ul class="items-result list-group" ng-show="ctrl.achievements.length">
<li class="list-group-item" ng-repeat="achievement in ctrl.achievements">
<img ng-src="{{achievement.badgeLink}}" width="50px" height="50px"/>
<a ng-href="/#/achievements/{{achievement._id}}">{{achievement.title}}</a>
<span class="pull-right hidden-xs "><b>{{achievement.points}} pts</b></span>
</li>
</ul>
<a href="#/achievements/new" class="pull-right hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()" title="Create a new achievement">Create a new achievement</a>
</div>

View File

@ -0,0 +1,25 @@
<div class="row" ng-controller="NewAchievementController as ctrl">
<div ng-show="error" class="alert alert-danger"></div>
<form class="col-xs-12 col-md-offset-3 col-md-6 form" ng-submit="ctrl.createNewAchievement()">
<h2>create achievement</h2>
<div class="form-group">
<input type="text" class="form-control" name="title" placeholder="title" ng-model="ctrl.title" autocomplete="off" required>
</div>
<div class="form-group">
<input type="number" class="form-control" name="points" placeholder="points" ng-model="ctrl.points" autocomplete="off" min="10" max="100" required>
</div>
<div class="form-group">
<input type="url" class="form-control" name="challengeRepo" placeholder="challenge repo (optional)" ng-model="ctrl.challengeRepo" autocomplete="off">
</div>
<div class="form-group">
<input type="text" class="form-control" name="badgeLink" placeholder="badge link (optional)" ng-model="ctrl.badgeLink" autocomplete="off">
</div>
<div class="form-group">
<textarea rows="5" class="form-control" name="criteria" placeholder="criteria" ng-model="ctrl.criteria" autocomplete="off" required></textarea>
</div>
<div>
<button type="submit" class="btn btn-default" ng-disabled="disabled">Create</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,3 @@
<footer>
<p>© copyright 2016 <a href="https://github.com/michaellennox/netstix">netstix</a> its authors and contributors.</p>
</footer>

View File

@ -0,0 +1,32 @@
<nav class="navbar" ng-controller="LogoutController as ctrl">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" ng-init="navCollapsed = true" ng-click="navCollapsed = !navCollapsed">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/#/" class="navbar-brand">Netstix</a>
</div>
<div class="collapse navbar-collapse" uib-collapse="navCollapsed">
<ul class="nav navbar-nav navbar-right">
<li ng-hide="ctrl.isLoggedIn()"><a ng-click="navCollapsed = !navCollapsed" href="/#/login">login</a></li>
<li ng-hide="ctrl.isLoggedIn()"><a ng-click="navCollapsed = !navCollapsed" href="/#/register">register</a></li>
<li ng-hide="ctrl.isLoggedIn()"><a ng-click="navCollapsed = !navCollapsed" href="/#/achievements">achievements</a></li>
<li class="dropdown hidden-xs ">
<a ng-show="ctrl.isLoggedIn()" class="dropdown-toggle hidden-xs " data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ctrl.getUser().username}}</a>
<ul class="dropdown-menu">
<li><a ng-click="navCollapsed = !navCollapsed" href="/#/achievements">get achievements</a></li>
<li><a ng-click="navCollapsed = !navCollapsed" href="/#/users/{{ctrl.getUser()._id}}">my achievements</a></li>
<li><button ng-click="ctrl.logout()" ng-click="navCollapsed = !navCollapsed" class="btn btn-link btn-logout">logout</button></li>
</ul>
</li>
<li class="hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()"><a>{{ctrl.getUser().username}}</a></li>
<li class="hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()"><a ng-click="navCollapsed = !navCollapsed" href="/#/achievements">get achievements</a></li>
<li class="hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()"><a ng-click="navCollapsed = !navCollapsed" href="/#/users/{{ctrl.getUser()._id}}">my achievements</a></li>
<li class="hidden-md hidden-lg" ng-show="ctrl.isLoggedIn()"><button ng-click="ctrl.logout()" ng-click="navCollapsed = !navCollapsed" class="btn btn-link btn-logout">logout</button></li>
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,16 @@
<div class="row" ng-controller="NewSubmissionController as ctrl">
<div ng-show="error" class="alert alert-danger">{{errorMessage}}</div>
<form class="col-xs-12 col-md-offset-3 col-md-6 form" ng-submit="ctrl.createNewSubmission()">
<h2>send new submission</h2>
<div class="form-group">
<input type="url" class="form-control" name="link" placeholder="github link" ng-model="ctrl.link" autocomplete="off" required>
</div>
<div class="form-group">
<textarea rows="5" class="form-control" name="comment" placeholder="comments" ng-model="ctrl.comment" autocomplete="off"></textarea>
</div>
<div>
<button type="submit" class="btn btn-default" ng-disabled="disabled">Send</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="container" ng-controller="UsersController as ctrl">
<header>
<h1>leaderboard</h1>
</header>
<ul class="items-result list-group" ng-show="ctrl.users.length">
<li class="list-group-item" ng-repeat="user in ctrl.users | orderBy:'-score'">
<i class="glyphicon glyphicon-user hidden-xs"></i><a ng-href="#/users/{{user._id}}">{{user.username}}</a>
<span class="pull-right hidden-xs "><b>{{user.score}} pts</b></span>
</li>
</ul>
</div>

View File

@ -0,0 +1,16 @@
<div class="row" ng-controller="LoginController as ctrl">
<div ng-show="error" class="alert alert-danger">{{errorMessage}}</div>
<form class="col-xs-12 col-md-offset-3 col-md-6 form" ng-submit="ctrl.login()">
<h2>login</h2>
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="username" ng-model="loginForm.username" autocomplete="off" required>
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="password" ng-model="loginForm.password" autocomplete="off" required>
</div>
<div>
<button type="submit" class="btn btn-default" ng-disabled="disabled">Login</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,16 @@
<div class="row" ng-controller="RegisterController as ctrl">
<div ng-show="error" class="alert alert-danger">{{errorMessage}}</div>
<form class="col-xs-12 col-md-offset-3 col-md-6 form" ng-submit="ctrl.register()">
<h2>register</h2>
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="username" ng-model="registerForm.username" autocomplete="off" required>
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="password" ng-model="registerForm.password" autocomplete="off" required>
</div>
<div>
<button type="submit" class="btn btn-default" ng-disabled="disabled">Register</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,19 @@
<div class="container row user" ng-controller="UserController as ctrl">
<header>
<h1>{{ctrl.user.username}}</h1>
<b class="pull-right hidden-xs ">{{ctrl.user.score}} pts</b>
</header>
<section class="description">
<p class="hidden-md hidden-lg"><b>{{ctrl.user.score}}</b> pts</p>
<p><b>#{{ctrl.user.submissions.length}}</b> badges</p>
</section>
<header>
<h2>trophy cabinet</h2>
</header>
<ul class="items-result list-group">
<li class="list-group-item" ng-repeat="submission in ctrl.user.submissions">
<img ng-src="{{submission.achievement.badgeLink}}" width="50px" height="50px"/><a ng-href="/#/achievements/{{submission.achievement._id}}">{{submission.achievement.title}}</a>
<a class="pull-right hidden-xs " target="_blank" ng-href="{{submission.link}}"><i class="glyphicon glyphicon-pencil"></i>&nbsp;solution</a>
</li>
</ul>
</div>

16
test/e2e/conf.js Normal file
View File

@ -0,0 +1,16 @@
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
capabilities: { 'browserName': 'chrome' },
specs: ['features/*Feature.js'],
jasmineNodeOpts: {
showColors: true,
print: function() {}
},
onPrepare: function() {
var SpecReporter = require('jasmine-spec-reporter');
jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: 'all'}));
}
};

View File

@ -0,0 +1,66 @@
var mongoose = require('mongoose');
beforeEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
afterEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
describe('Achievements Features', function() {
it('a user can create an achievement, view a list of achievements then view a specific achievement', function() {
browser.get('http://localhost:8080/#/achievements');
var signUpLink = element(by.css('a[href*="#/register"]'));
signUpLink.click();
var usernameInput = element(by.css('input[name="username"]'));
var passwordInput = element(by.css('input[name="password"]'));
var signUpForm = element(by.css('form'));
usernameInput.sendKeys('test user');
passwordInput.sendKeys('epicpassword');
signUpForm.submit();
var achievementsList = element.all(by.repeater('achievement in ctrl.achievements'));
var firstAchievementTitle = achievementsList.get(0).element(by.css('a'));
expect(achievementsList.count()).toEqual(0);
var header = element(by.css('header'));
var createAchievementLink = header.element(by.css('a[href*="#/achievements/new"]'));
createAchievementLink.click();
expect(browser.getCurrentUrl()).toContain('#/achievements/new');
var achievementTitleInput = element(by.css('input[name="title"]'));
var achievementCriteriaInput = element(by.css('textarea[name="criteria"]'));
var newAchievementForm = element(by.css('form'));
achievementTitleInput.sendKeys('Create an achievement for the app');
achievementCriteriaInput.sendKeys('This is where a user should write criteria');
newAchievementForm.submit();
expect(achievementsList.count()).toEqual(1);
expect(browser.getCurrentUrl()).toContain('#/achievements');
expect(firstAchievementTitle.getText()).toEqual('Create an achievement for the app');
var viewAchievementLink = achievementsList.get(0).element(by.css('a[href*="#/achievements/"]'));
viewAchievementLink.click();
var achievementTitle = element(by.binding('achievement.title'));
var achievementCriteria = element(by.binding('achievement.criteria'));
expect(browser.getCurrentUrl()).toContain('#/achievements/');
expect(achievementTitle.getText()).toEqual('Create an achievement for the app');
expect(achievementCriteria.getText()).toEqual('This is where a user should write criteria');
});
});

View File

@ -0,0 +1,91 @@
var mongoose = require('mongoose');
beforeEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
afterEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
describe('Profiles/leaderboard Features', function() {
it('a user can see the leaderboard after submitting an achievement and view their own profile with individual achievements', function() {
browser.get('http://localhost:8080/#/achievements');
var signUpLink = element(by.css('a[href*="#/register"]'));
signUpLink.click();
var usernameInput = element(by.css('input[name="username"]'));
var passwordInput = element(by.css('input[name="password"]'));
var signUpForm = element(by.css('form'));
usernameInput.sendKeys('test user');
passwordInput.sendKeys('epicpassword');
signUpForm.submit();
var header = element(by.css('header'));
var createAchievementLink = header.element(by.css('a[href*="#/achievements/new"]'));
createAchievementLink.click();
var achievementTitleInput = element(by.css('input[name="title"]'));
var achievementCriteriaInput = element(by.css('textarea[name="criteria"]'));
var newAchievementForm = element(by.css('form'));
achievementTitleInput.sendKeys('Create an achievement for the app');
achievementCriteriaInput.sendKeys('This is where a user should write criteria');
newAchievementForm.submit();
var achievementsList = element.all(by.repeater('achievement in ctrl.achievements'));
var viewAchievementLink = achievementsList.get(0).element(by.css('a[href*="#/achievements/"]'));
viewAchievementLink.click();
var createSubmissionLink = element(by.css('a[href*="/submissions/new"]'));
createSubmissionLink.click();
expect(browser.getCurrentUrl()).toContain('/submissions/new');
var submissionLinkInput = element(by.css('input[name="link"]'));
var submissionCommentInput = element(by.css('textarea[name="comment"]'));
var newSubmissionForm = element(by.css('form'));
submissionLinkInput.sendKeys('A link to the relevant code or example');
submissionCommentInput.sendKeys('A comment about the submission');
newSubmissionForm.submit();
var achievementSubmissionsList = element.all(by.repeater('submission in ctrl.achievement.submissions'));
expect(browser.getCurrentUrl()).toContain('/achievements/');
expect(achievementSubmissionsList.count()).toEqual(1);
expect(achievementSubmissionsList.get(0).getText()).toEqual('test user');
var viewLeaderboardLink = element(by.css('.navbar-brand'));
viewLeaderboardLink.click();
var leaderboardList = element.all(by.repeater('user in ctrl.users'));
expect(browser.getCurrentUrl()).toContain('#/');
expect(leaderboardList.count()).toEqual(1);
expect(leaderboardList.get(0).getText()).toContain('test user');
var viewProfileLink = leaderboardList.get(0).element(by.css('a[href*="#/users/"]'));
viewProfileLink.click();
var trophyCabinetList = element.all(by.repeater('submission in ctrl.user.submissions'));
expect(browser.getCurrentUrl()).toContain('#/users/');
expect(trophyCabinetList.count()).toEqual(1);
expect(trophyCabinetList.get(0).getText()).toContain('Create an achievement for the app');
});
});

View File

@ -0,0 +1,70 @@
var mongoose = require('mongoose');
beforeEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
afterEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
describe('Submissions Features', function() {
it('a user can make a submission for a specific achievement', function() {
browser.get('http://localhost:8080/#/achievements');
var signUpLink = element(by.css('a[href*="#/register"]'));
signUpLink.click();
var usernameInput = element(by.css('input[name="username"]'));
var passwordInput = element(by.css('input[name="password"]'));
var signUpForm = element(by.css('form'));
usernameInput.sendKeys('test user');
passwordInput.sendKeys('epicpassword');
signUpForm.submit();
var header = element(by.css('header'));
var createAchievementLink = header.element(by.css('a[href*="#/achievements/new"]'));
createAchievementLink.click();
var achievementTitleInput = element(by.css('input[name="title"]'));
var achievementCriteriaInput = element(by.css('textarea[name="criteria"]'));
var newAchievementForm = element(by.css('form'));
achievementTitleInput.sendKeys('Create an achievement for the app');
achievementCriteriaInput.sendKeys('This is where a user should write criteria');
newAchievementForm.submit();
var achievementsList = element.all(by.repeater('achievement in ctrl.achievements'));
var viewAchievementLink = achievementsList.get(0).element(by.css('a[href*="#/achievements/"]'));
viewAchievementLink.click();
var createSubmissionLink = element(by.css('a[href*="/submissions/new"]'));
createSubmissionLink.click();
expect(browser.getCurrentUrl()).toContain('/submissions/new');
var submissionLinkInput = element(by.css('input[name="link"]'));
var submissionCommentInput = element(by.css('textarea[name="comment"]'));
var newSubmissionForm = element(by.css('form'));
submissionLinkInput.sendKeys('A link to the relevant code or example');
submissionCommentInput.sendKeys('A comment about the submission');
newSubmissionForm.submit();
var achievementSubmissionsList = element.all(by.repeater('submission in ctrl.achievement.submissions'));
expect(browser.getCurrentUrl()).toContain('/achievements/');
expect(achievementSubmissionsList.count()).toEqual(1);
expect(achievementSubmissionsList.get(0).getText()).toEqual('test user');
});
});

View File

@ -0,0 +1,63 @@
var mongoose = require('mongoose');
beforeEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
afterEach(function() {
mongoose.connect('mongodb://localhost/makers-achievements-test', function() {
mongoose.connection.db.dropDatabase();
});
});
describe('User Authentication', function() {
it('a user can sign up, sign out and sign in', function() {
browser.get('http://localhost:8080/#/achievements');
var signUpLink = element(by.css('a[href*="#/register"]'));
var signInLink = element(by.css('a[href*="#/login"]'));
expect(signUpLink.isDisplayed()).toBeTruthy();
expect(signInLink.isDisplayed()).toBeTruthy();
signUpLink.click();
expect(browser.getCurrentUrl()).toContain('#/register');
var usernameInput = element(by.css('input[name="username"]'));
var passwordInput = element(by.css('input[name="password"]'));
var signUpForm = element(by.css('form'));
usernameInput.sendKeys('test user');
passwordInput.sendKeys('epicpassword');
signUpForm.submit();
expect(browser.getCurrentUrl()).toContain('#/achievements');
expect(signUpLink.isDisplayed()).toBeFalsy();
expect(signInLink.isDisplayed()).toBeFalsy();
var usernameDropdown = element(by.css('.dropdown-toggle'));
var signOutButton = element(by.css('.btn-logout'));
usernameDropdown.click();
signOutButton.click();
expect(browser.getCurrentUrl()).toContain('#/achievements');
signInLink.click();
expect(browser.getCurrentUrl()).toContain('#/login');
var signInForm = element(by.css('form'));
usernameInput.sendKeys('test user');
passwordInput.sendKeys('epicpassword');
signInForm.submit();
expect(browser.getCurrentUrl()).toContain('#/achievements');
expect(signUpLink.isDisplayed()).toBeFalsy();
expect(signInLink.isDisplayed()).toBeFalsy();
});
});

View File

@ -0,0 +1,27 @@
describe('AchievementController', function() {
var response = {
data: { title: 'codewars', criteria: '150pts on codewars' }
};
var ctrl;
var scope;
var AchievementsResourceFactoryMock;
beforeEach(function() {
AchievementsResourceFactoryMock = jasmine.createSpyObj('AchievementsResource', ['getAchievement']);
module('Netstix', {
AchievementsResource: AchievementsResourceFactoryMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
AchievementsResourceFactoryMock.getAchievement.and.returnValue($q.when(response));
ctrl = $controller('AchievementController', { $routeParams: {id: '2'} });
scope = $rootScope;
}));
it('initializes with achievement info from the GetAchievement Factory', function() {
scope.$digest();
expect(ctrl.achievement)
.toEqual(response.data);
});
});

View File

@ -0,0 +1,27 @@
describe('AchievementsController', function() {
var response = {
data: [{ title: 'codewars', criteria: '150pts on codewars' }]
};
var ctrl;
var scope;
var AchievementsResourceFactoryMock;
beforeEach(function() {
AchievementsResourceFactoryMock = jasmine.createSpyObj('AchievementsResource', ['getAchievements']);
module('Netstix', {
AchievementsResource: AchievementsResourceFactoryMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
AchievementsResourceFactoryMock.getAchievements.and.returnValue($q.when(response));
ctrl = $controller('AchievementsController');
scope = $rootScope;
}));
it('initializes with achievements from the AchievementsResource Factory', function() {
scope.$digest();
expect(ctrl.achievements)
.toEqual(response.data);
});
});

View File

@ -0,0 +1,32 @@
describe('NewAchievementController', function() {
var ctrl;
var scope;
var AchievementsResourceMock;
var windowMock;
beforeEach(function() {
windowMock = { location: { href: jasmine.createSpy() } };
AchievementsResourceMock = jasmine.createSpyObj(
'AchievementsResource', ['postAchievements']
);
module('Netstix', {
AchievementsResource: AchievementsResourceMock,
$window: windowMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
AchievementsResourceMock.postAchievements
.and.returnValue($q.when({}));
ctrl = $controller('NewAchievementController');
scope = $rootScope;
}));
describe('#createNewAchievement()', function() {
it('redirects to /#/achievements', function() {
ctrl.createNewAchievement();
scope.$digest();
expect(windowMock.location.href).toEqual('/#/achievements');
});
});
});

View File

@ -0,0 +1,32 @@
describe('NewSubmissionController', function() {
var response = { message: 'ok' };
var ctrl;
var scope;
var AchievementsResourceFactoryMock;
var windowMock;
var idMock;
beforeEach(function() {
windowMock = { location: { href: jasmine.createSpy() } };
AchievementsResourceFactoryMock = jasmine.createSpyObj('AchievementsResource', ['postSubmissions']);
module('Netstix', {
AchievementsResource: AchievementsResourceFactoryMock,
$window: windowMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
AchievementsResourceFactoryMock.postSubmissions.and.returnValue($q.when(response));
ctrl = $controller('NewSubmissionController');
scope = $rootScope;
}));
describe('#createNewSubmission()', function() {
it('redirects to /#/achievements/:id when successful', function() {
ctrl.id = 55;
ctrl.createNewSubmission();
scope.$digest();
expect(windowMock.location.href).toEqual('/#/achievements/55');
});
});
});

View File

@ -0,0 +1,27 @@
describe('UserController', function() {
var response = {
data: { username: 'codewars', id: '3' }
};
var ctrl;
var scope;
var UsersResourceFactoryMock;
beforeEach(function() {
UsersResourceFactoryMock = jasmine.createSpyObj('UsersResource', ['getData']);
module('Netstix', {
UsersResource: UsersResourceFactoryMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
UsersResourceFactoryMock.getData.and.returnValue($q.when(response));
ctrl = $controller('UserController', { $routeParams: {id: '3'} });
scope = $rootScope;
}));
it('initializes with user info from the UsersResource Factory', function() {
scope.$digest();
expect(ctrl.user)
.toEqual(response.data);
});
});

View File

@ -0,0 +1,27 @@
describe('UsersController', function() {
var response = {
data: [{ username: 'giamir', id: '2' }]
};
var ctrl;
var scope;
var UsersResourceFactoryMock;
beforeEach(function() {
UsersResourceFactoryMock = jasmine.createSpyObj('UsersResourceFactory', ['getData']);
module('Netstix', {
UsersResource: UsersResourceFactoryMock
});
});
beforeEach(inject(function($controller, $q, $rootScope) {
UsersResourceFactoryMock.getData.and.returnValue($q.when(response));
ctrl = $controller('UsersController');
scope = $rootScope;
}));
it('initializes with users from the UsersResources Factory', function() {
scope.$digest();
expect(ctrl.users)
.toEqual(response.data);
});
});

View File

@ -0,0 +1,74 @@
describe('factory: AchievementsResource', function() {
var achievementsResource;
beforeEach(module('Netstix'));
beforeEach(inject(function(AchievementsResource) {
achievementsResource = AchievementsResource;
}));
beforeEach(inject(function($httpBackend) {
httpBackend = $httpBackend;
httpBackend
.whenPOST('/achievements').respond(function(){
return [200, { message: 'Achievement created!'}, {}];
});
httpBackend
.whenGET("/achievements/2").respond(
{ title: 'codewars', criteria: '150pts on codewars' }
);
httpBackend
.whenGET("/achievements").respond(
[{ title: 'codewars', criteria: '150pts on codewars' }]
);
httpBackend
.whenPOST('/achievements/55/submissions').respond(function(){
return [200, { message: 'Submission sent!'}, {}];
});
}));
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
describe('#postAchievements', function() {
it('returns a success message if the achievement has been created', function() {
achievementsResource.postAchievements('a title', 'a criteria')
.then(function(data) {
expect(data.message).toEqual('Achievement created!');
});
httpBackend.flush();
});
});
describe('#getAchievement', function() {
it('returns achievement hash', function() {
achievementsResource.getAchievement(2)
.then(function(response) {
expect(response.data).toEqual({ title: 'codewars', criteria: '150pts on codewars' });
});
httpBackend.flush();
});
});
describe('#getAchievements', function() {
it('returns achievements array', function() {
achievementsResource.getAchievements()
.then(function(response) {
expect(response.data[0]).toEqual({ title: 'codewars', criteria: '150pts on codewars' });
});
httpBackend.flush();
});
});
describe('#postSubmissions', function() {
it('returns a success message if the submission has been sent', function() {
achievementsResource.postSubmissions('https://github.com/Htunny', 'a comment', 55)
.then(function(data) {
expect(data.message).toEqual('Submission sent!');
});
httpBackend.flush();
});
});
});

View File

@ -0,0 +1,52 @@
describe('factory: UsersResource', function() {
var usersResource;
var scope;
beforeEach(module('Netstix'));
beforeEach(inject(function(UsersResource) {
usersResource = UsersResource;
}));
beforeEach(inject(function($httpBackend, $rootScope) {
httpBackend = $httpBackend;
httpBackend
.when(
"GET",
"users/"
)
.respond(
[{ username: 'giamir', id: '2' }]
);
httpBackend
.when(
"GET",
"users/2"
)
.respond(
{ username: 'giamir', id: '2' }
);
}));
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
describe('#getData', function() {
it('returns an array of users if no argument is passed', function() {
usersResource.getData()
.then(function(response) {
expect(response.data[0]).toEqual({ username: 'giamir', id: '2' });
});
httpBackend.flush();
});
it('returns a specific user if the id is passed', function() {
usersResource.getData(2)
.then(function(response) {
expect(response.data).toEqual({ username: 'giamir', id: '2' });
});
httpBackend.flush();
});
});
});

View File

@ -0,0 +1,44 @@
module.exports = function(config) {
config.set({
basePath: '../',
frameworks: ['jasmine'],
files: [
'../public/libs/angular/angular.js',
'../public/libs/angular-route/angular-route.js',
'../public/libs/angular-resource/angular-resource.js',
'../public/libs/angular-mocks/angular-mocks.js',
'../public/libs/angular-bootstrap/ui-bootstrap.min.js',
'../public/libs/angular-loading-bar/build/loading-bar.min.js',
'../public/js/**/*.js',
'./front_end/**/*.spec.js'
],
exclude: [],
preprocessors: {
'../public/js/**/*.js': ['coverage']
},
reporters: ['spec', 'coverage'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
// browsers: ['Chrome'],
browsers: ['PhantomJS'],
singleRun: true,
coverageReporter: {
type : 'html',
dir : 'front_end/coverage/'
}
});
};

View File

@ -0,0 +1,38 @@
var frisby = require('frisby');
var mongoose = require('mongoose');
var URL = 'http://localhost:8080/achievements/';
frisby.create('api call to add an achievement')
.post(URL, {
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.after(function() {
frisby.create('view list of achievements')
.get(URL)
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.expectJSON('?', {
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.afterJSON(function(achievements) {
var achievement = achievements[0];
frisby.create('view individual achievement')
.get(URL + achievement._id)
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.expectJSON({
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.toss();
})
.toss();
})
.toss();

View File

@ -0,0 +1,45 @@
var frisby = require('frisby');
var mongoose = require('mongoose');
var URL = 'http://localhost:8080/achievements/';
frisby.create('api call to add an achievement')
.post(URL, {
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.after(function() {
frisby.create('view list of achievements')
.get(URL)
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.expectJSON('?', {
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.afterJSON(function(achievements) {
var achievement = achievements[0];
frisby.create('view individual achievement')
.get(URL + achievement._id)
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.expectJSON({
title: 'Bug Hunter',
criteria: 'Find an error in the Makers Academy materials'
})
.toss();
})
.afterJSON(function(achievement) {
frisby.create('make a submission')
.post(URL + achievement._id + '/submissions/')
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.toss();
})
.toss();
})
.toss();

View File

@ -0,0 +1,47 @@
var frisby = require('frisby');
var mongoose = require('mongoose');
var URL = 'http://localhost:8080/';
frisby.create('api call to create user')
.post(URL + 'users', {
username: 'testuser1',
password: 'password1'
})
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json; charset=utf-8')
.after(function() {
frisby.create('can sign user out')
.delete(URL + 'sessions', {
})
.expectStatus(200)
.expectJSON({
status: 'Signed out successfully!'
})
.after(function() {
frisby.create('an invalid user cannot sign in')
.post(URL + 'sessions', {
username: 'invaliduser',
password: 'notpassword'
})
.expectStatus(401)
.after(function() {
frisby.create('a valid user can sign in')
.post(URL + 'sessions', {
username: 'testuser1',
password: 'password1'
})
.expectStatus(200)
.expectJSON({
status: 'Login successful!'
})
.toss();
})
.toss();
})
.toss();
})
.toss();

View File

@ -0,0 +1,8 @@
{
"spec_dir": "test/server",
"spec_files": [
"**/*[sS]pec.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}