Initial commit.

This commit is contained in:
Insane Turkey 2016-01-25 19:45:53 -05:00
commit a7afd37268
24 changed files with 1872 additions and 0 deletions

4
.bowerrc Normal file
View file

@ -0,0 +1,4 @@
{
"directory": "bower_components",
"json": "bower.json"
}

21
.editorconfig Normal file
View file

@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/node_modules
/dist
/.tmp
/.sass-cache
/bower_components

6
.jscsrc Normal file
View file

@ -0,0 +1,6 @@
{
"requireCamelCaseOrUpperCaseIdentifiers": true,
"requireCapitalizedConstructors": true,
"requireParenthesesAroundIIFE": true,
"validateQuoteMarks": "'"
}

16
.jshintrc Normal file
View file

@ -0,0 +1,16 @@
{
"bitwise": true,
"browser": true,
"curly": true,
"eqeqeq": true,
"esnext": true,
"latedef": true,
"noarg": true,
"node": true,
"strict": true,
"undef": true,
"unused": true,
"globals": {
"angular": false
}
}

9
.travis.yml Normal file
View file

@ -0,0 +1,9 @@
sudo: false
language: node_js
node_js:
- 'iojs'
- '0.12'
- '0.10'
before_script:
- 'npm install -g bower grunt-cli'
- 'bower install'

1
.yo-rc.json Normal file
View file

@ -0,0 +1 @@
{}

445
Gruntfile.js Normal file
View file

@ -0,0 +1,445 @@
// Generated on 2016-01-05 using generator-angular 0.15.1
'use strict';
module.exports = function (grunt) {
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Automatically load required Grunt tasks
require('jit-grunt')(grunt, {
useminPrepare: 'grunt-usemin',
ngtemplates: 'grunt-angular-templates',
cdnify: 'grunt-google-cdn'
});
// Configurable paths for the application
var appConfig = {
app: require('./bower.json').appPath || 'app',
dist: 'dist'
};
// Define the configuration for all the tasks
grunt.initConfig({
// Project settings
yeoman: appConfig,
// Watches files for changes and runs tasks based on the changed files
watch: {
bower: {
files: ['bower.json'],
tasks: ['wiredep']
},
js: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
tasks: ['newer:jshint:all', 'newer:jscs:all'],
options: {
livereload: '<%= connect.options.livereload %>'
}
},
compass: {
files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
tasks: ['compass:server', 'postcss:server']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
},
// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect().use(
'/app/styles',
connect.static('./app/styles')
),
connect.static(appConfig.app)
];
}
}
},
dist: {
options: {
open: true,
base: '<%= yeoman.dist %>'
}
}
},
// Make sure there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: {
src: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
}
},
// Make sure code styles are up to par
jscs: {
options: {
config: '.jscsrc',
verbose: true
},
all: {
src: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/{,*/}*',
'!<%= yeoman.dist %>/.git{,*/}*'
]
}]
},
server: '.tmp'
},
// Add vendor prefixed styles
postcss: {
options: {
processors: [
require('autoprefixer-core')({browsers: ['last 1 version']})
]
},
server: {
options: {
map: true
},
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
},
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
},
// Automatically inject Bower components into the app
wiredep: {
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: /\.\.\//
},
sass: {
src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
ignorePath: /(\.\.\/){1,2}bower_components\//
}
},
// Compiles Sass to CSS and generates necessary files if requested
compass: {
options: {
sassDir: '<%= yeoman.app %>/styles',
cssDir: '.tmp/styles',
generatedImagesDir: '.tmp/images/generated',
imagesDir: '<%= yeoman.app %>/images',
javascriptsDir: '<%= yeoman.app %>/scripts',
fontsDir: '<%= yeoman.app %>/styles/fonts',
importPath: './bower_components',
httpImagesPath: '/images',
httpGeneratedImagesPath: '/images/generated',
httpFontsPath: '/styles/fonts',
relativeAssets: false,
assetCacheBuster: false,
raw: 'Sass::Script::Number.precision = 10\n'
},
dist: {
options: {
generatedImagesDir: '<%= yeoman.dist %>/images/generated'
}
},
server: {
options: {
sourcemap: true
}
}
},
// Renames files for browser caching purposes
filerev: {
dist: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
options: {
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
},
// Performs rewrites based on filerev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'],
options: {
assetsDirs: [
'<%= yeoman.dist %>',
'<%= yeoman.dist %>/images',
'<%= yeoman.dist %>/styles'
],
patterns: {
js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']]
}
}
},
// The following *-min tasks will produce minified files in the dist folder
// By default, your `index.html`'s <!-- Usemin block --> will take care of
// minification. These next options are pre-configured if you do not wish
// to use the Usemin blocks.
// cssmin: {
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css'
// ]
// }
// }
// },
// uglify: {
// dist: {
// files: {
// '<%= yeoman.dist %>/scripts/scripts.js': [
// '<%= yeoman.dist %>/scripts/scripts.js'
// ]
// }
// }
// },
// concat: {
// dist: {}
// },
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/images'
}]
}
},
htmlmin: {
dist: {
options: {
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true,
removeCommentsFromCDATA: true
},
files: [{
expand: true,
cwd: '<%= yeoman.dist %>',
src: ['*.html'],
dest: '<%= yeoman.dist %>'
}]
}
},
ngtemplates: {
dist: {
options: {
module: 'tnCommunityBrowserApp',
htmlmin: '<%= htmlmin.dist.options %>',
usemin: 'scripts/scripts.js'
},
cwd: '<%= yeoman.app %>',
src: 'views/{,*/}*.html',
dest: '.tmp/templateCache.js'
}
},
// ng-annotate tries to make the code safe for minification automatically
// by using the Angular long form for dependency injection.
ngAnnotate: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
},
// Replace Google CDN references
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
},
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: '<%= yeoman.dist %>',
src: [
'*.{ico,png,txt}',
'*.html',
'images/{,*/}*.{webp}',
'styles/fonts/{,*/}*.*'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/images',
src: ['generated/*']
}, {
expand: true,
cwd: '.',
src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
dest: '<%= yeoman.dist %>'
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
},
// Run some tasks in parallel to speed up the build process
concurrent: {
server: [
'compass:server'
],
dist: [
'compass:dist',
'imagemin',
'svgmin'
]
}
});
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'wiredep',
'concurrent:server',
'postcss:server',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run(['serve:' + target]);
});
grunt.registerTask('build', [
'clean:dist',
'wiredep',
'useminPrepare',
'concurrent:dist',
'postcss',
'ngtemplates',
'concat',
'ngAnnotate',
'copy:dist',
'cdnify',
'cssmin',
'uglify',
'filerev',
'usemin',
'htmlmin'
]);
grunt.registerTask('default', [
'newer:jshint',
'newer:jscs',
'build'
]);
};

4
README.md Normal file
View file

@ -0,0 +1,4 @@
TribesNext Community Browser
==========
A web-based user interface for interacting with the TribesNext community features.

152
app/404.html Normal file
View file

@ -0,0 +1,152 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Not Found :(</title>
<style>
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
html {
padding: 30px 10px;
font-size: 20px;
line-height: 1.4;
color: #737373;
background: #f0f0f0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
html,
input {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
body {
max-width: 500px;
padding: 30px 20px 50px;
border: 1px solid #b3b3b3;
border-radius: 4px;
margin: 0 auto;
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
background: #fcfcfc;
}
h1 {
margin: 0 10px;
font-size: 50px;
text-align: center;
}
h1 span {
color: #bbb;
}
h3 {
margin: 1.5em 0 0.5em;
}
p {
margin: 1em 0;
}
ul {
padding: 0 0 0 40px;
margin: 1em 0;
}
.container {
max-width: 380px;
margin: 0 auto;
}
/* google search */
#goog-fixurl ul {
list-style: none;
padding: 0;
margin: 0;
}
#goog-fixurl form {
margin: 0;
}
#goog-wm-qt,
#goog-wm-sb {
border: 1px solid #bbb;
font-size: 16px;
line-height: normal;
vertical-align: top;
color: #444;
border-radius: 2px;
}
#goog-wm-qt {
width: 220px;
height: 20px;
padding: 5px;
margin: 5px 10px 0 0;
box-shadow: inset 0 1px 1px #ccc;
}
#goog-wm-sb {
display: inline-block;
height: 32px;
padding: 0 10px;
margin: 5px 0 0;
white-space: nowrap;
cursor: pointer;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
#goog-wm-sb:hover,
#goog-wm-sb:focus {
border-color: #aaa;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
background-color: #f8f8f8;
}
#goog-wm-qt:hover,
#goog-wm-qt:focus {
border-color: #105cb6;
outline: 0;
color: #222;
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Not found <span>:(</span></h1>
<p>Sorry, but the page you were trying to view does not exist.</p>
<p>It looks like this was the result of either:</p>
<ul>
<li>a mistyped address</li>
<li>an out-of-date link</li>
</ul>
<script>
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
</script>
<script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
</div>
</body>
</html>

0
app/favicon.ico Normal file
View file

66
app/index.html Normal file
View file

@ -0,0 +1,66 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>TribesNext Community Browser</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<!-- build:css(.) styles/vendor.css -->
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/angular-xeditable/dist/css/xeditable.css" />
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="bower_components/bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css" />
<!-- endbower -->
<!-- endbuild -->
<!-- build:css(.tmp) styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
</head>
<body ng-app="tnCommunityBrowserApp">
<!--[if lte IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<header>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">TribesNext Community Browser</a>
</div>
</div>
</nav>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li ng-hide="isLoggedIn" ng-class="{active: isActive('/login')}"><a href="#/login">Login</a></li>
<li ng-show="isLoggedIn" ng-class="{active: isActive('/profile')}"><a href="#/profile">My Profile</a></li>
<li ng-show="isLoggedIn" ng-class="{active: isActive('/search')}"><a href="#/search">Search</a></li>
<li ng-show="isLoggedIn" ng-class="{active: isActive('/create')}"><a href="#/create">Create Tribe</a></li>
<li ng-show="isLoggedIn"><a href="#/logout" logout-button>Logout</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" ng-view></div>
</div>
</div>
<!-- build:js(.) scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/underscore/underscore.js"></script>
<script src="bower_components/underscore.string/dist/underscore.string.js"></script>
<script src="bower_components/angular-xeditable/dist/js/xeditable.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/bootstrap-switch/dist/js/bootstrap-switch.js"></script>
<script src="bower_components/angular-bootstrap-switch/dist/angular-bootstrap-switch.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<!-- endbuild -->
</body>
</html>

4
app/robots.txt Normal file
View file

@ -0,0 +1,4 @@
# robotstxt.org
User-agent: *
Disallow:

527
app/scripts/app.js Normal file
View file

@ -0,0 +1,527 @@
'use strict';
var entityMap = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': '&quot;', "'": '&#39;', "/": '&#x2F;' };
function escapeHtml(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); }
angular
.module('tnCommunityBrowserApp', ['ngRoute','frapontillo.bootstrap-switch','xeditable'])
.run(['$rootScope', '$location', 'editableOptions', 'TNSession', function ($rootScope, $location, editableOptions, TNSession) {
editableOptions.theme = 'bs3';
_.mixin(s.exports());
var openRoutes = ['/login'];
var cleanRoute = function (route) {
return _.find(openRoutes, function (r) { return _.startsWith(route, r)});
};
$rootScope.$on('$routeChangeStart', function (event,next,current) {
if (!cleanRoute($location.url()) && !TNSession.isLoggedIn())
$location.path('/login');
});
$rootScope.isActive = function (path) {
return _.startsWith($location.path(), path);
}
var updateSession = function (e) {
$rootScope.isLoggedIn = TNSession.isLoggedIn();
$rootScope.username = TNSession.getUsername();
$rootScope.guid = TNSession.getGuid();
};
var handleLogout = function (e) {
updateSession();
if (!_.startsWith($location.url(), '/login'))
$location.path('/login');
};
$rootScope.$on('TNSession.login', updateSession);
$rootScope.$on('TNSession.logout', handleLogout);
$rootScope.$on('TNSession.expired', handleLogout);
}])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/login', {templateUrl:'views/login.html', controller:'LoginCtrl', controllerAs:'loginCtrl'})
.when('/profile', {templateUrl:'views/profile.html', controller:'ProfileCtrl', controllerAs:'profile'})
.when('/player/:guid', {templateUrl:'views/player.html', controller:'PlayerCtrl', controllerAs:'player'})
.when('/tribe/:tribeId', {templateUrl:'views/tribe.html', controller:'TribeCtrl', controllerAs:'tribe'})
.when('/search', {templateUrl:'views/search.html', controller:'SearchCtrl', controllerAs:'search'})
.when('/create', {templateUrl:'views/create.html', controller:'CreateTribeCtrl', controllerAs:'tribe'})
.otherwise({ redirectTo: '/login' });
}])
.filter('preserve_space', function () { return function (input) {return (input||'').replace(/ /g, '\xa0')} })
.filter('yes_no', function () { return function (input) {return (input=='1')?'yes':'no'}})
.filter('titleize', function () {return function (input) {return _.titleize(input)}})
.directive('tribeLink', function () {
return {
restrict: 'E',
scope: {
id: '=',
tag: '=',
name: '=',
append: '='
},
template: '<a href="#/tribe/{{id}}"><span class="tag" ng-show="append==0">{{tag|preserve_space}}</span>{{name|preserve_space}}<span class="tag" ng-show="append==1">{{tag|preserve_space}}</span></a>'
};
})
.directive('playerLink', function () {
return {
restrict: 'E',
scope: {
guid: '=',
tag: '=',
name: '=',
append: '='
},
template: '<a href="#/player/{{guid}}"><span class="tag" ng-show="append==0">{{tag|preserve_space}}</span>{{name|preserve_space}}<span class="tag" ng-show="append==1">{{tag|preserve_space}}</span></a>'
};
})
.directive('historyItem', function () {
return {
link: function (scope, element, attrs) {
var template = (scope.item.template || '');
// Populate with @payload components...
var payload = scope.item.payload || '';
var payloads = payload.split('\t');
template = template.replace(/@payload(;(\d+))?[\^]?/gi, function (_,__,g) {
if (g !== undefined) return payloads[g]; return payload;
});
// Escape HTML
template = escapeHtml(template);
// Populate with @clan and @user link(s)
template = template.replace(/@(clan|player[12]?)/gi, function (w) {
var result;
switch (w) {
case '@clan':
var clan = scope.item.clan;
var tag = '<span class="tag">' + escapeHtml((clan.tag || '').replace(/ /g, '\xa0')) + '</span>';
var name = escapeHtml((clan.name || '').replace(/ /g, '\xa0'));
if (clan.append == '0')
result = tag + name;
else
result = name + tag;
return '<em><a href="#/tribe/' + clan.id + '">' + result + '</a></em>';
case '@player':
var player = scope.item.player;
var tag = '<span class="tag">' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + '</span>';
var name = escapeHtml((player.name || '').replace(/ /g, '\xa0'));
if (player.append == '0')
result = tag + name;
else
result = name + tag;
return '<em><a href="#/player/' + player.guid + '">' + result + '</a></em>';
case '@player1':
var player = scope.item.user1;
var tag = '<span class="tag">' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + '</span>';
var name = escapeHtml((player.name || '').replace(/ /g, '\xa0'));
if (player.append == '0')
result = tag + name;
else
result = name + tag;
return '<em><a href="#/player/' + player.guid + '">' + result + '</a></em>';
case '@player2':
var player = scope.item.user2;
var tag = '<span class="tag">' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + '</span>';
var name = escapeHtml((player.name || '').replace(/ /g, '\xa0'));
if (player.append == '0')
result = tag + name;
else
result = name + tag;
return '<em><a href="#/player/' + player.guid + '">' + result + '</a></em>';
}
});
// Output
element.html(template);
}
}
})
.directive('logoutButton', ['TNSession', function (TNSession) {
return { link: function (scope, element, attrs) {
element.bind('click', function () { if (TNSession.isLoggedIn()) TNSession.logout()})}}
}])
.controller('LoginCtrl', ['$scope', '$timeout', '$location', 'Notice', 'TNSession', function ($scope, $timeout, $location, notice, TNSession) {
$scope.messages = notice.messages;
$scope.login = function (username, password) {
TNSession
.login(username, password)
.then(
function (response) { $location.path('/profile'); },
function (error) { notice.error(error) })
.finally(function () { $scope.password = null; });
};
$scope.logout = function () {
TNSession
.logout()
.then(
function () { notice.success('You have been logged out.'); },
function (error) { notice.error(error) });
};
/*
$scope.$watch('TNSession.expired', function() {
notice.info('You have been logged out due to inactivity.', 0);
});
*/
}])
.controller('ProfileCtrl', ['$scope', 'Notice', 'TNSession', function ($scope, notice, TNSession) {
$scope.messages = notice.messages;
var getProfile = function () {
TNSession.sendRequest('userview', {id: TNSession.getGuid()})
.then(function (r) {$scope.info = r},
function (e) {notice.error('An error was encountered while retrieving profile details.')});
};
var getHistory = function () {
TNSession.sendRequest('userhistory', {id: TNSession.getGuid()})
.then(function (r) {$scope.history = r},
function (e) {notice.error('An error was encountered while retrieving profile history.')});
};
var getInvites = function () {
TNSession.sendRequest('userinvites', {})
.then(function (r) {$scope.invites = r},
function (e) {notice.error('An error was encountered while retrieving tribe invitations.')});
};
$scope.$watch('TNSession.login', function (e) {
$scope.messages = [];
getProfile();
getHistory();
getInvites();
});
$scope.$watch('TNSession.logout', function (e) {
$scope.info = null;
$scope.history = null;
});
$scope.setDescription = function (description) { return TNSession.sendRequest('userinfo', {info:description}) };
$scope.setWebsite = function (website) { return TNSession.sendRequest('usersite', {site:website}) };
$scope.setName = function (name) { return TNSession.sendRequest('username', {name:name}) };
$scope.setActiveTribe = function (tribeId,tag,append) {
TNSession.sendRequest('userclan', {id:tribeId})
.then(function (r) {$scope.info.append=append; return true;},
function (e) {getProfile(); notice.error('Unable to set active tribe: ' + e)});
};
$scope.acceptInvite = function (invite) {
return TNSession.sendRequest('useraccept', {id:invite.clan.id})
.then(function () {
notice.success('You have joined ' + invite.clan.name + '.')
var index = $scope.invites.indexOf(invite);
if (index !== -1) {
$scope.invites.splice(index,1);
}
getProfile();
},
function (e) {notice.error('Error accepting invitation to ' + invite.clan.name + ': ' + e)});
};
$scope.rejectInvite = function (invite) {
return TNSession.sendRequest('userreject', {id:invite.clan.id})
.then(function () {
notice.success('Invitation to ' + invite.clan.name + ' rejected.');
var index = $scope.invites.indexOf(invite);
if (index !== -1) {
$scope.invites.splice(index,1);
}
},
function (e) {notice.error('Error rejecting invitation to ' + invite.clan.name + ': ' + e)});
};
$scope.leaveTribe = function (tribe) {
return TNSession.sendRequest('userleave', {id:tribe.id})
.then(function () {
notice.success('You have left ' + tribe.name);
getProfile();
return true;
});
};
}])
.controller('PlayerCtrl', ['$scope', '$routeParams', 'Notice', 'TNSession', function ($scope, $routeParams, notice, TNSession) {
var guid = $routeParams.guid;
TNSession.sendRequest('userview', {id: guid})
.then(function (r) {$scope.info = r},
function (e) {notice.error('An error was encountered while retrieving player details.\n' + e)});
TNSession.sendRequest('userhistory', {id: guid})
.then(function (r) {$scope.history = r},
function (e) {notice.error('An error was encountered while retrieving player history.\n' + e)});
}])
.controller('TribeCtrl', ['$scope', '$routeParams', '$filter', 'Notice', 'TNSession', function ($scope,$routeParams,$filter,notice,TNSession) {
$scope.messages = notice.messages;
var tribeId = $routeParams.tribeId;
$scope.canKick = function (member) { return $scope.isMember && $scope.canKickMembers && $scope.membership.rank > member.rank; };
$scope.canSetRank = function (member) { return $scope.isMember && $scope.canSetMemberRanks && (member.guid == TNSession.getGuid() || $scope.membership.rank > member.rank); };
var clearPermissions = function () {
$scope.membership = null;
$scope.isMember = false;
$scope.availableRanks = [];
$scope.canSetTag = $scope.canSetName = $scope.canSetWebsite = $scope.canSetPicture =
$scope.canSetInfo = $scope.canSetRecruiting = $scope.canSendInvite = $scope.canViewInvites =
$scope.canDisband = $scope.canKickMembers = $scope.canSetMemberRanks = false;
};
var updatePermissions = function () {
var results = $filter('filter')($scope.info.members, {guid:TNSession.getGuid()}, true);
$scope.membership = results.pop();
$scope.isMember = $scope.membership != null;
$scope.canViewInvites = $scope.isMember;
if ($scope.isMember) {
var rank = $scope.membership.rank;
if (rank >= 2) {
$scope.canSetRecruiting = $scope.canSendInvite = true;
$scope.getInvites();
}
if (rank >= 3) {
$scope.canSetTag = $scope.canSetName = $scope.canSetWebsite = $scope.canSetPicture =
$scope.canSetInfo = $scope.canKickMembers = $scope.canSetMemberRanks = true;
}
if (rank >= 4) {
$scope.canDisband = true;
}
if ($scope.canSetMemberRanks) {
var ranks = [{key:'0', value:'0 - Probationary Member'},{key:'1',value:'1 - Member'},
{key:'2',value:'2 - Senior Member'},{key:'3',value:'3 - Administrator'}];
if (rank >= 4) ranks.push({key:'4',value:'4 - Leader'});
$scope.availableRanks = ranks;
}
}
};
$scope.getInfo = function () {
clearPermissions();
TNSession.sendRequest('clanview', {id: tribeId})
.then(function (r) {$scope.info = r; updatePermissions(); },
function (e) {notice.error('An error was encountered while retrieving tribe details.\n' + e)});
};
$scope.getHistory = function () {
TNSession.sendRequest('clanhistory', {id: tribeId})
.then(function (r) {$scope.history = r},
function (e) {notice.error('An error was encountered while retrieving tribe history.\n' + e)});
};
$scope.getInvites = function () {
TNSession.sendRequest('clanviewinvites', {id:tribeId})
.then(function (r) {$scope.invites = r;},
function (e) {notice.error('An error was encountered while retrieving tribe invitations.\n' + e)});
};
$scope.setRecruiting = function (isRecruiting) {
return TNSession.sendRequest('clanrecruit', {id:tribeId, v:(isRecruiting?'1':'0')})
.then(null, function (e) {return e});
}
$scope.setDescription = function (description) { return TNSession.sendRequest('claninfo', {id:tribeId, v:description}) };
var oldTag; var oldAppend;
$scope.presetTag = function (tag,append) {oldTag=tag; oldAppend=append;};
$scope.setTag = function (tag,append) {
return TNSession.sendRequest('clantag', {id:tribeId, tag:tag, append:append})
.then(null, function (e) {$scope.info.tag=oldTag; $scope.info.append=oldAppend;return e;});
};
$scope.setWebsite = function (website) { return TNSession.sendRequest('clansite', {id:tribeId, v:website}) };
$scope.setName = function (name) { return TNSession.sendRequest('clanname', {id:tribeId, v:name}) };
$scope.setPicture = function (path) { return TNSession.sendRequest('clanpicture', {id:tribeId, v:path}) };
$scope.setRank = function (target,rank,title) { return TNSession.sendRequest('clanrank', {id:tribeId, to:target, rank:rank, title:title}) };
$scope.disband = function (authorize) { return TNSession.sendRequest('clandisband', {id:tribeId, v:authorize})};
$scope.kickMember = function (target,accept) {
if ('Yes'==accept)
return TNSession.sendRequest('clankick', {id:tribeId, to:target})
.then(function (r) {$scope.getInfo(); return true;});
return false;
};
$scope.searchResults = [];
$scope.showSearchResults = false;
$scope.searchAlert = null;
$scope.searchAlertType = null;
$scope.searchPlayers = function (name) {
$scope.searchResults = [];
$scope.searchAlert = $scope.searchAlertType = null;
$scope.showSearchResults = false;
TNSession.sendRequest('usersearch', {q:name})
.then(function (r) {
$scope.searchResults = r;
$scope.showSearchResults = (r != null && r.length > 0);
$scope.searchAlertType = 'info';
if (!$scope.showSearchResults)
$scope.searchAlert = 'No results found';
},
function (e) {$scope.searchAlertType='danger'; $scope.searchAlert = 'Search failed: ' + e;});
};
$scope.sendInvite = function(target,accept) {
if ('Yes'==accept)
return TNSession.sendRequest('claninvite', {id:tribeId, to:target})
.then(function (r) {$scope.getInvites(); return true;});
return false;
};
$scope.getInfo();
$scope.getHistory();
}])
.controller('SearchCtrl', ['$scope', '$timeout', 'Notice', 'TNSession', function ($scope, $timeout, notice, TNSession) {
$scope.messages = notice.messages;
$scope.searchMode = 'Tribes';
$scope.searchModes = ['Tribes', 'Players'];
$scope.resultMode = null;
$scope.showResults = false;
$scope.playerResults = [];
$scope.tribeResults = [];
var clearResults = function () {
$scope.resultMode = null;
$scope.showResults = false;
$scope.playerResults = [];
$scope.tribeResults = [];
};
var setResults = function (mode, results) {
switch (mode) {
case 'Players': $scope.playerResults = results;
case 'Tribes': $scope.tribeResults = results;
}
$scope.showResults = (results != null && results.length > 0);
if (!$scope.showResults)
notice.info('No results found');
$scope.resultMode = mode;
};
$scope.search = function (name, mode) {
var method = ('Tribes' == mode) ? 'clansearch' : 'usersearch';
clearResults();
TNSession.sendRequest(method, {q:name})
.then(function (r) { setResults(mode,r); },
function (e) { notice.error('Search failed: ' + e); });
};
}])
.controller('CreateTribeCtrl', ['$scope', '$timeout', 'Notice', 'TNSession', function ($scope, $timeout, notice, TNSession) {
$scope.messages = notice.messages;
$scope.resetInputs = function () {
$scope.name = $scope.tag = $scope.info = null;
$scope.append = $scope.recruiting = 'Yes';
};
$scope.resetInputs();
$scope.createTribe = function (name, tag, append, recruiting, info) {
append = ('Yes' == append) ? '1':'0';
recruiting = ('Yes' == recruiting) ? '1':'0';
TNSession.sendRequest('createclan', {name:name, tag:tag, append:append, recruiting:recruiting,info:info})
.then(function (r) { notice.success('Tribe created successfully.'); $scope.resetInputs()},
function (e) { notice.error(e)});
};
}])
.factory('Notice', ['$timeout', function($timeout) {
var service = {messages: []};
var removeAlert = function (alert) {
var index = service.messages.indexOf(alert);
if (index !== -1) {
alert = service.messages.splice(index,1)[0];
if (undefined !== alert.expire)
$timeout.cancel(alert.expire);
}
};
var addAlert = function (type, message, timeout) {
if (undefined === timeout) { timeout = 5000; }
var alert= {type:type, message:message};
alert.pop = function() {removeAlert(alert)};
if (timeout > 0)
alert.expire = $timeout(alert.pop, timeout);
service.messages.push(alert);
};
service.push = addAlert;
service.info = function (message, timeout) {addAlert('info', message, timeout)};
service.warn = function (message, timeout) {addAlert('warning', message, timeout)};
service.error = function (message, timeout) {addAlert('danger', message, timeout)};
service.success = function (message, timeout) {addAlert('success', message, timeout)};
service.clear = function () {service.messages = []};
return service;
}])
.factory('TNSession', ['$timeout', '$rootScope', '$http', '$q', function ($timeout,$root, $http, $q) {
var service = { _isLoggedIn:false, username:null, guid:null, uuid:null, expires:null };
var sendSessionRequest = function (payload) {
var params = angular.extend({jsonp:'JSON_CALLBACK'}, payload);
var deferred = $q.defer();
$http.jsonp('http://thyth.com/tn/json/json_session.php', {params:params})
.then(
function (response) {
if ('error' === response.data.status) { deferred.reject(response.data.message); }
else { deferred.resolve(response.data); }
},
function (error) { deferred.reject(error.data || 'Request failed'); });
return deferred.promise;
};
var clearSessionTimeout = function (reset) {
var expires = service.expires;
if (expires)
$timeout.cancel(expires);
if (reset)
expires = $timeout(timeoutSession, 15*60000);
else
expires = null;
service.expires = expires;
return expires;
};
var clearSession = function () {
clearSessionTimeout(false);
service._isLoggedIn = false;
service.username = service.uuid = service.guid = service.expires = null;
};
var timeoutSession = function () {
clearSession();
$root.$broadcast('TNSession.expired');
};
service.isLoggedIn = function () { return service._isLoggedIn; };
service.getUsername = function () { return service.username; };
service.getGuid = function () { return service.guid; };
service.login = function (username, password) {
clearSession();
return sendSessionRequest({method:'login', un:username, pw:password})
.then(function (response) {
service._isLoggedIn = true;
service.username = username;
service.uuid = response.uuid;
service.guid = response.guid;
service.expires = clearSessionTimeout(true);
$root.$broadcast('TNSession.login');
return response;
});
};
service.logout = function () {
return sendSessionRequest({method:'logout', uuid:service.uuid, guid:service.guid})
.then(function () { clearSession(); $root.$broadcast('TNSession.logout'); });
};
service.sendRequest = function (method, payload) {
var deferred = $q.defer();
var params = {
jsonp: 'JSON_CALLBACK',
method: method,
uuid: service.uuid,
guid: service.guid,
payload: JSON.stringify(payload)
};
$http.jsonp('http://thyth.com/tn/json/json_browser.php', {method:'POST', params:params})
.then(
function (response) {
switch (response.data.status) {
case 'error': deferred.reject(response.data.msg); break;
case 'success': deferred.resolve(response.data['payload']); break;
default: deferred.resolve(response.data); break;
}
clearSessionTimeout(true);
},
function (error) { deferred.reject(error.data || 'Request failed') });
return deferred.promise;
};
return service;
}]);

150
app/styles/main.scss Normal file
View file

@ -0,0 +1,150 @@
$icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/";
// bower:scss
@import "bootstrap-sass-official/assets/stylesheets/_bootstrap.scss";
// endbower
.browsehappy {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
body {
padding: 0;
padding-top: 50px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-left: 15px;
padding-right: 15px;
}
.page-header { margin-left: 10px; }
/* Custom page header */
.header {
border-bottom: 1px solid #e5e5e5;
margin-bottom: 10px;
/* Make the masthead heading the same height as the navigation */
h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
padding-bottom: 19px;
}
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
.tag { color:goldenrod; }
.row.alerts .alert {
padding: 5px;
}
.row.alerts .btn {
padding: 0;
float: right;
}
span.btn-link:hover {cursor:pointer;}
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #428bca;
}
.navbar-form.nav-sidebar input[type=text] {
margin-bottom: 5px;
}
#login h2 {
margin-top:0;
}
#create .input-group-addon {min-width:8em;}
#create .input-group-addon.top { vertical-align:top !important; }
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
.btn {
font-size: 21px;
padding: 14px 24px;
}
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
p + h4 {
margin-top: 28px;
}
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
.container {
max-width: 730px;
}
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-left: 0;
padding-right: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}

48
app/views/create.html Normal file
View file

@ -0,0 +1,48 @@
<div id="create" ng-controller="CreateTribeCtrl as create">
<div class="row">
<h1 class="page-header">Create a Tribe</h1>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row main">
<div class="col-md-4">
<form role="form" ng-submit="createTribe(name,tag,append,recruiting,info)">
<div class="input-group">
<span class="input-group-addon">Name</span>
<label for="txtName" class="sr-only">Name</label>
<input type="text" id="txtName" class="form-control" ng-model="name" placeholder="Name" required autofocus/>
</div>
<div class="input-group">
<span class="input-group-addon">Tag</span>
<label for="txtTag" class="sr-only">Tag</label>
<input type="text" id="txtTag" class="form-control" ng-model="tag" placeholder="Tag" required/>
</div>
<div class="input-group">
<span class="input-group-addon">Append Tag</span>
<label for="cmbAppend" class="sr-only">Append Tag</label>
<select id="cmbAppend" class="form-control" ng-model="append" ng-options="v for v in ['Yes','No']"></select>
</div>
<div class="input-group">
<span class="input-group-addon">Recruiting</span>
<label for="cmbRecruiting" class="sr-only">Recruiting</label>
<select id="cmbRecruiting" class="form-control" ng-model="recruiting" ng-options="v for v in ['Yes','No']"></select>
</div>
<div class="input-group">
<span class="input-group-addon top">Info</span>
<label for="txtInfo" class="sr-only">Tribe Information</label>
<textarea id="txtInfo" class="form-control" ng-model="info" rows="5" placeholder="Tribe information"></textarea>
</div>
<div class="form-group">
<div class="col-md-offset-4 col-md-4" style="margin-top:1em;">
<button class="btn btn-success" type="submit">Create</button>
<button class="btn btn-link" type="button" ng-click="resetInputs()">Reset</button>
</div>
</div>
</form>
</div>
</div>
</div>

41
app/views/login.html Normal file
View file

@ -0,0 +1,41 @@
<div id="login" ng-controller="LoginCtrl as loginCtrl">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="alert alert-warning" role="alert">
<h1 style="margin-top:0;">Warning!</h1>
<p class="lead">
Your password is sent to the community server unprotected, and could be intercepted by a skilled attacker.
If you use this password elsewhere, it is advised that you change it at those locations.
</p>
</div>
</div>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 col-md-offset-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row" ng-hide="isLoggedIn">
<div class="col-md-4 col-md-offset-4">
<form name="loginForm" ng-submit="login(username, password)">
<h2 class="sub-heading">Please sign in</h2>
<label for="inputUsername" class="sr-only">Username</label>
<input type="text" id="inputUsername" class="form-control" placeholder="Username" required autofocus ng-model="username"/>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required ng-model="password"/>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</div>
<div class="row" ng-show="isLoggedIn">
<div class="col-md-4 col-md-offset-4">
<form name="logoutForm" ng-submit="logout()">
<h2 class="sub-heading">Sign out</h2>
<label for="displayUsername" class="sr-only">Username</label>
<input type="text" id="displayUsername" class="form-control" readonly ng-model="username"/>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign out</button>
</form>
</div>
</div>
</div>

55
app/views/player.html Normal file
View file

@ -0,0 +1,55 @@
<div id="player" ng-controller="PlayerCtrl as player">
<div class="row">
<h1 class="page-header">
<span class="tag" ng-show="info.append==0">{{info.tag|preserve_space}}</span>{{info.name|preserve_space}}<span class="tag" ng-show="info.append==1">{{info.tag|preserve_space}}</span>
</h1>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 col-md-offset-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row main">
<div class="col-md-6">
<h2 class="sub-header">Profile Details</h2>
<table class="table table-striped table-bordered table-rounded">
<tbody>
<tr><th>GUID</th><th>Name</th><th>Website</th></tr>
<tr>
<td>{{info.guid}}</td>
<td>{{info.name|preserve_space}}</td>
<td>{{info.website}}</td>
</tr>
<tr><th colspan="2">Joined</th><th>Online</th></tr>
<tr><td colspan="2">{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td><td>{{info.online|yes_no|titleize}}</td></tr>
<tr><th colspan="3">Info</th></tr>
<tr><td colspan="3"><pre>{{info.info}}</pre></td></tr>
</tbody>
</table>
<h2 class="sub-header">Tribe Membership</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>ID</th><th>Name</th><th>Title</th><th>Rank</th></tr></thead>
<tbody>
<tr ng-repeat="tribe in info.memberships">
<td>{{tribe.id}}</td>
<td><tribe-link id="tribe.id" tag="tribe.tag" name="tribe.name" append="tribe.append"></tribe-link></td>
<td>{{tribe.title}}</td>
<td>{{tribe.rank}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<h2 class="sub-header">History</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>Event</th><th>Time</th><th>Description</th></tr></thead>
<tr ng-repeat="item in history">
<td>{{item.type}}</td>
<td>{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td>
<td history-item></td>
</tr>
</table>
</div>
</div>
</div>

81
app/views/profile.html Normal file
View file

@ -0,0 +1,81 @@
<div id="profile" ng-controller="ProfileCtrl as profile">
<div class="row">
<h1 class="page-header">
<span class="tag" ng-show="info.append==0">{{info.tag|preserve_space}}</span>
{{info.name|preserve_space}}
<span class="tag" ng-show="info.append==1">{{info.tag|preserve_space}}</span>
</h1>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 col-md-offset-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row main">
<div class="col-md-7">
<h2 class="sub-header">Profile Details</h2>
<table class="table table-striped table-bordered table-rounded">
<tbody>
<tr><th>GUID</th><th>Name</th><th>Website</th></tr>
<tr>
<td>{{info.guid}}</td>
<td><a href='#' editable-text="info.name" e-required onbeforesave="setName($data)">{{info.name|preserve_space}}</a></td>
<td><a href='#' editable-text="info.website" onbeforesave="setWebsite($data)">{{info.website}}</a></td>
</tr>
<tr><th colspan="2">Joined</th><th>Online</th></tr>
<tr><td colspan="2">{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td><td>{{info.online|yes_no|titleize}}</td></tr>
<tr><th colspan="3">Info</th></tr>
<tr><td colspan="3"><a href='#' e-rows="7" e-cols="60" editable-textarea="info.info" onbeforesave="setDescription($data)"><pre>{{info.info}}</pre></a></td></tr>
</tbody>
</table>
<h2 class="sub-header">Tribe Membership</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>ID</th><th>Name</th><th>Title</th><th>Rank</th><th>Active</th><th></th></tr></thead>
<tbody>
<tr ng-repeat="tribe in info.memberships">
<td>{{tribe.id}}</td>
<td><tribe-link id="tribe.id" tag="tribe.tag" name="tribe.name" append="tribe.append"></tribe-link></td>
<td>{{tribe.title}}</td>
<td>{{tribe.rank}}</td>
<td>
<input bs-switch type="radio" switch-size="small" ng-model="info.tag" ng-value="tribe.tag" ng-change="setActiveTribe(tribe.id,tribe.tag,tribe.append)"/>
</td>
<td>
<form editable-form name="rowform" onbeforesave="leaveTribe(tribe)" ng-show="rowform.$visible" class="form-buttons form-inline">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-primary">Yes</button>
<button type="button" ng-disabled="rowform.$waiting" class="btn btn-link" ng-click="rowform.$cancel()">Cancel</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn-link" ng-click="rowform.$show()">Leave</button>
</div>
</td>
</tr>
</tbody>
</table>
<h2 class="sub-header">Tribe Invitations</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>From</th><th>To</th><th>Expires</th><th></th><th></th></tr></thead>
<tbody>
<tr ng-repeat="invite in invites">
<td><player-link guid="invite.sender.guid" tag="invite.sender.tag" append="invite.sender.append" name="invite.sender.name"></player-link></td>
<td><tribe-link id="invite.clan.id" tag="invite.clan.tag" append="invite.clan.append" name="invite.clan.name"></tribe-link></td>
<td>{{invite.expire*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td>
<td><span class="btn-link" ng-click="acceptInvite(invite)">Accept</span></td>
<td><span class="btn-link" ng-click="rejectInvite(invite)">Reject</span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-5">
<h2 class="sub-header">History</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>Time</th><th>Description</th></tr></thead>
<tr ng-repeat="item in history">
<td>{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss'}}</td>
<td history-item></td>
</tr>
</table>
</div>
</div>
</div>

39
app/views/search.html Normal file
View file

@ -0,0 +1,39 @@
<div id="search" ng-controller="SearchCtrl as search">
<div class="container-fluid">
<div class="navbar navbar-default">
<form class="navbar-form" ng-submit="search(searchName, searchMode)">
<label for="txtSearch" class="sr-only">Name</label>
<input type="text" id="txtSearch" class="form-control" required autofocus placeholder="Name..." ng-model="searchName" />
<label for="cmbMode" class="sr-only">Mode</label>
<select class="form-control" ng-model="searchMode" ng-options="v for v in searchModes"></select>
<button class="btn btn-primary glyphicon glyphicon-search"></button>
</form>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 col-md-offset-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row" ng-show="showResults">
<table class="table table-striped table-bordered table-rounded" ng-show="resultMode=='Tribes'">
<caption>Results</caption>
<thead><tr><th>ID</th><th>Name</th></tr></thead>
<tbody>
<tr ng-repeat="tribe in tribeResults">
<td>{{tribe.id}}</td><td><tribe-link id="tribe.id" tag="" append="0" name="tribe.name"></tribe-link></td>
</tr>
</tbody>
</table>
<table class="table table-striped table-bordered table-rounded" ng-show="resultMode=='Players'">
<caption>Results</caption>
<thead><tr><th>Guid</th><th>Name</th></tr></thead>
<tbody>
<tr ng-repeat="player in playerResults">
<td>{{player.guid}}</td><td><player-link guid="player.guid" tag="player.tag" append="player.append" name="player.name"></player-link></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

134
app/views/tribe.html Normal file
View file

@ -0,0 +1,134 @@
<div id="tribe" ng-controller="TribeCtrl as tribe">
<div class="row">
<h1 class="page-header">
<span class="tag" ng-show="info.append==0">{{info.tag|preserve_space}}</span>{{info.name|preserve_space}}<span class="tag" ng-show="info.append==1">{{info.tag|preserve_space}}</span>
</h1>
</div>
<div class="row alerts" ng-repeat="alert in messages">
<div class="col-md-4 col-md-offset-4 alert alert-{{alert.type}}">
<span>{{alert.message}}</span>
<span class="glyphicon glyphicon-remove btn" ng-click="alert.pop()"></span>
</div>
</div>
<div class="row main">
<div class="col-md-6">
<h2 class="sub-header">Tribe Details</h2>
<table class="table table-striped table-bordered table-rounded">
<tbody>
<tr><th>ID</th><th colspan="2">Name</th><th colspan="2">Tag</th></tr>
<tr>
<td>{{info.id}}</td>
<td colspan="2">
<span ng-hide="canSetName">{{info.name|preserve_space}}</span>
<a href="#" ng-show="canSetName" editable-text="info.name" e-required onbeforesave="setName($data)">{{info.name|preserve_space}}</a>
</td>
<td colspan="2">
<span ng-hide="canSetTag">{{info.tag|preserve_space}}</span>
<form ng-show="canSetTag" editable-form name="tagform" class="form-buttons form-inline" onbeforesave="presetTag(info.tag,info.append)">
<div ng-show="tagform.$visible">
<span editable-text="info.tag" e-name="tag" e-required onaftersave="setTag(info.tag,info.append)">{{info.tag|preserve_space}}</span>
<span editable-select="info.append" e-name="append" e-required e-ng-options="s.k as s.v for s in [{k:'1',v:'Append'},{k:'0',v:'Prepend'}]">{{info.append|yes_no|titleize}}</span>
</div>
<span type="button" class="btn-link" ng-click="tagform.$show()" ng-hide="tagform.$visible">{{info.tag|preserve_space}}</span>
<span ng-show="tagform.$visible">
<button type="submit" class="btn btn-primary glyphicon glyphicon-ok" ng-disabled="tagform.$waiting"></button>
<button type="button" class="btn btn-default glyphicon glyphicon-remove" ng-disabled="tagform.$waiting" ng-click="tagform.$cancel()"></button>
</span>
</form>
</td>
</tr>
<tr><th colspan="5">Website</th></tr>
<tr><td colspan="5"><span ng-hide="canSetWebsite">{{info.website}}</span><a href='#' ng-show="canSetWebsite" editable-text="info.website" onbeforesave="setWebsite($data)">{{info.website}}</a></td></tr>
<tr><th colspan="3">Created</th><th>Recruiting</th><th>Active</th></tr>
<tr>
<td colspan="3">{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td>
<td>
<span ng-hide="canSetRecruiting">{{info.recruiting|yes_no|titleize}}</span>
<a href='#' ng-show="canSetRecruiting" editable-select="info.recruiting" e-ng-options="s.key as s.value for s in [{key:'1',value:'Yes'}, {key:'0', value:'No'}]">{{info.recruiting|yes_no|titleize}}</a>
</td>
<td>{{info.active|yes_no|titleize}}</td>
</tr>
<tr><th colspan="3">Info</th><td colspan="2">Picture</td></tr>
<tr>
<td colspan="3">
<pre ng-hide="canSetInfo">{{info.info}}</pre>
<a href='#' ng-show="canSetInfo" e-rows="7" e-cols="60" editable-textarea="info.info" onbeforesave="setDescription($data)"><pre>{{info.info}}</pre></a>
</td>
<td colspan="2">
<span ng-hide="canSetPicture">{{info.picture || 'Not set'}}</span>
<a href='#' ng-show="canSetPicture" editable-text="info.picture" onbeforesave="setPicture($data)">{{info.picture || 'Not set'}}</a>
</td>
</tr>
<tr ng-show="canDisband"><th colspan="5">Disband</th></tr>
<tr ng-show="canDisband"><td colspan="5">
<a href='#' editable-select="info.disband" e-ng-options="s.k as s.v for s in [{k:'0',v:'No'},{k:'1',v:'Yes'}]" onbeforesave="disband($data)">Disband</a></td>
</tr>
</tbody>
</table>
<h2 class="sub-header">Tribe Members</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>GUID</th><th>Name</th><th>Title</th><th>Rank</th><th>Online</th><th ng-show="canKickMembers">Kick</th><th ng-show="canSetMemberRanks">Edit</th></tr></thead>
<tbody>
<tr ng-repeat="player in info.members">
<td>{{player.guid}}</td>
<td><player-link guid="player.guid" tag="player.tag" name="player.name" append="player.append"></tribe-link></td>
<td>
<span ng-hide="canSetRank(player)">{{player.title}}</span>
<span ng-show="canSetRank(player)" editable-text="player.title" e-name="title" e-form="rowform">{{player.title}}</span>
</td>
<td>
<span ng-hide="canSetRank(player)">{{player.rank}}</span>
<span ng-show="canSetRank(player)" editable-select="player.rank" e-name="rank" e-form="rowform" e-ng-options="s.key as s.value for s in availableRanks">{{player.rank}}</span>
</td>
<td>{{player.online|yes_no|titleize}}</td>
<td ng-show="canKickMembers"><a href='#' ng-show="canKick(player)" editable-select="player.kick" e-ng-options="s for s in ['Yes','No']" onbeforesave="kickMember(player.guid,$data)">Kick</a></td>
<td ng-show="canSetMemberRanks"><div ng-show="canSetRank(player)">
<form editable-form name="rowform" ng-show="rowform.$visible" class="form-buttons form-inline" onbeforesave="setRank(player.guid,$data.rank,$data.title)">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-primary">Save</button>
<button type="button" ng-disabled="rowform.$waiting" class="btn btn-link" ng-click="rowform.$cancel()">Cancel</button>
</form>
<button ng-hide="rowform.$visible" ng-click="rowform.$show()" class="btn btn-default">edit</button>
</div></td>
</tr>
</tbody>
</table>
<div ng-show="canSendInvite">
<h2 class="sub-header">Send Invitation</h2>
<form class="form-buttons form-inline" ng-submit="searchPlayers(searchName)">
<label class="sr-only" for="srchName">Player name</label>
<input type="text" class="form-control" ng-model="searchName" required placeholder="Player name"/>
<button type="submit" class="btn btn-primary glyphicon glyphicon-search"></button>
<span ng-show="searchAlert != null" class="alert alert-{{searchAlertType}}">{{searchAlert}}</span>
</form>
<table class="table table-striped table-bordered table-rounded" ng-show="showSearchResults">
<caption>Results</caption>
<thead><tr><th>GUID</th><th>Name</th><th>Invite</th></tr></thead>
<tbody>
<tr ng-repeat="player in searchResults"><td>{{player.guid}}</td><td><player-link guid="player.guid" tag="player.tag" append="player.append" name="player.name"></player-link></td><td>
<a href='#' editable-select="player.invite" e-ng-options="s for s in ['Yes','No']" onbeforesave="sendInvite(player.guid,$data)">Invite</a></td></tr>
</tbody>
</table>
</div>
<div ng-show="canViewInvites">
<h2 class="sub-header">Tribe Invitations</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>Sender</th><th>Recipient</th><th>Expires</th></tr></thead>
<tbody>
<tr ng-repeat="invite in invites"><td><player-link guid="invite.sender.guid" tag="invite.sender.tag" append="invite.sender.append" name="invite.sender.name"></player-link></td><td><player-link guid="invite.recipient.guid" tag="invite.recipient.tag" append="invite.recipient.append" name="invite.recipient.name"></player-link></td><td>{{invite.expire*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td></tr>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2 class="sub-header">History</h2>
<table class="table table-striped table-bordered table-rounded">
<thead><tr><th>Time</th><th>Description</th></tr></thead>
<tr ng-repeat="item in history">
<td>{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}</td>
<td history-item></td>
</tr>
</table>
</div>
</div>
</div>

24
bower.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "tn-community-browser",
"version": "0.0.0",
"dependencies": {
"angular": "^1.4.0",
"bootstrap-sass-official": "^3.2.0",
"angular-route": "^1.4.0",
"underscore": "~1.8.3",
"underscore.string": "~3.2.3",
"angular-xeditable": "~0.1.9",
"angular-bootstrap-switch": "~0.4.1"
},
"appPath": "app",
"moduleName": "tnCommunityBrowserApp",
"overrides": {
"bootstrap": {
"main": [
"less/bootstrap.less",
"dist/css/bootstrap.css",
"dist/js/bootstrap.js"
]
}
}
}

39
package.json Normal file
View file

@ -0,0 +1,39 @@
{
"name": "tncommunitybrowser",
"private": true,
"devDependencies": {
"autoprefixer-core": "^5.2.1",
"grunt": "^0.4.5",
"grunt-angular-templates": "^0.5.7",
"grunt-concurrent": "^1.0.0",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-compass": "^1.0.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-connect": "^0.9.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-cssmin": "^0.12.0",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-imagemin": "^1.0.0",
"grunt-contrib-jshint": "^0.11.0",
"grunt-contrib-uglify": "^0.7.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-filerev": "^2.1.2",
"grunt-google-cdn": "^0.4.3",
"grunt-jscs": "^1.8.0",
"grunt-newer": "^1.1.0",
"grunt-ng-annotate": "^0.9.2",
"grunt-postcss": "^0.5.5",
"grunt-svgmin": "^2.0.0",
"grunt-usemin": "^3.0.0",
"grunt-wiredep": "^2.0.0",
"jit-grunt": "^0.9.1",
"jshint-stylish": "^1.0.0",
"time-grunt": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"underscore": "^1.8.3"
}
}