commit a7afd372684826a546508cada9635fb84520cb71 Author: Insane Turkey Date: Mon Jan 25 19:45:53 2016 -0500 Initial commit. diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..dd7c6b1 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "bower_components", + "json": "bower.json" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2cdfb8 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc5576c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/dist +/.tmp +/.sass-cache +/bower_components diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..f8bf9ba --- /dev/null +++ b/.jscsrc @@ -0,0 +1,6 @@ +{ + "requireCamelCaseOrUpperCaseIdentifiers": true, + "requireCapitalizedConstructors": true, + "requireParenthesesAroundIIFE": true, + "validateQuoteMarks": "'" +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..4572e5d --- /dev/null +++ b/.jshintrc @@ -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 + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6fb6850 --- /dev/null +++ b/.travis.yml @@ -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' diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1 @@ +{} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..b957d03 --- /dev/null +++ b/Gruntfile.js @@ -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 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' + ]); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e75787 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +TribesNext Community Browser +========== + +A web-based user interface for interacting with the TribesNext community features. diff --git a/app/404.html b/app/404.html new file mode 100644 index 0000000..899828a --- /dev/null +++ b/app/404.html @@ -0,0 +1,152 @@ + + + + + Page Not Found :( + + + +
+

Not found :(

+

Sorry, but the page you were trying to view does not exist.

+

It looks like this was the result of either:

+ + + +
+ + diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..1c4a40d --- /dev/null +++ b/app/index.html @@ -0,0 +1,66 @@ + + + + + TribesNext Community Browser + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/app/robots.txt b/app/robots.txt new file mode 100644 index 0000000..4d521f9 --- /dev/null +++ b/app/robots.txt @@ -0,0 +1,4 @@ +# robotstxt.org + +User-agent: * +Disallow: diff --git a/app/scripts/app.js b/app/scripts/app.js new file mode 100644 index 0000000..0fb4875 --- /dev/null +++ b/app/scripts/app.js @@ -0,0 +1,527 @@ +'use strict'; + +var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; +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: '{{tag|preserve_space}}{{name|preserve_space}}{{tag|preserve_space}}' + }; + }) + .directive('playerLink', function () { + return { + restrict: 'E', + scope: { + guid: '=', + tag: '=', + name: '=', + append: '=' + }, + template: '{{tag|preserve_space}}{{name|preserve_space}}{{tag|preserve_space}}' + }; + }) + .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 = '' + escapeHtml((clan.tag || '').replace(/ /g, '\xa0')) + ''; + var name = escapeHtml((clan.name || '').replace(/ /g, '\xa0')); + if (clan.append == '0') + result = tag + name; + else + result = name + tag; + return '' + result + ''; + case '@player': + var player = scope.item.player; + var tag = '' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + ''; + var name = escapeHtml((player.name || '').replace(/ /g, '\xa0')); + if (player.append == '0') + result = tag + name; + else + result = name + tag; + return '' + result + ''; + case '@player1': + var player = scope.item.user1; + var tag = '' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + ''; + var name = escapeHtml((player.name || '').replace(/ /g, '\xa0')); + if (player.append == '0') + result = tag + name; + else + result = name + tag; + return '' + result + ''; + case '@player2': + var player = scope.item.user2; + var tag = '' + escapeHtml((player.tag || '').replace(/ /g, '\xa0')) + ''; + var name = escapeHtml((player.name || '').replace(/ /g, '\xa0')); + if (player.append == '0') + result = tag + name; + else + result = name + tag; + return '' + result + ''; + } + }); + // 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; + }]); diff --git a/app/styles/main.scss b/app/styles/main.scss new file mode 100644 index 0000000..2a9c78f --- /dev/null +++ b/app/styles/main.scss @@ -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; + } +} diff --git a/app/views/create.html b/app/views/create.html new file mode 100644 index 0000000..3531083 --- /dev/null +++ b/app/views/create.html @@ -0,0 +1,48 @@ +
+
+

Create a Tribe

+
+
+
+ {{alert.message}} + +
+
+
+
+
+
+ Name + + +
+
+ Tag + + +
+
+ Append Tag + + +
+
+ Recruiting + + +
+
+ Info + + +
+
+
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/app/views/login.html b/app/views/login.html new file mode 100644 index 0000000..e0f0dae --- /dev/null +++ b/app/views/login.html @@ -0,0 +1,41 @@ +
+
+
+ +
+
+
+
+ {{alert.message}} + +
+
+
+
+
+

Please sign in

+ + + + + +
+
+
+
+
+
+

Sign out

+ + + +
+
+
+
\ No newline at end of file diff --git a/app/views/player.html b/app/views/player.html new file mode 100644 index 0000000..98f63ef --- /dev/null +++ b/app/views/player.html @@ -0,0 +1,55 @@ +
+
+

+ {{info.tag|preserve_space}}{{info.name|preserve_space}}{{info.tag|preserve_space}} +

+
+
+
+ {{alert.message}} + +
+
+
+
+

Profile Details

+ + + + + + + + + + + + + +
GUIDNameWebsite
{{info.guid}}{{info.name|preserve_space}}{{info.website}}
JoinedOnline
{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}{{info.online|yes_no|titleize}}
Info
{{info.info}}
+

Tribe Membership

+ + + + + + + + + + +
IDNameTitleRank
{{tribe.id}}{{tribe.title}}{{tribe.rank}}
+
+
+

History

+ + + + + + + +
EventTimeDescription
{{item.type}}{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}
+
+
+
\ No newline at end of file diff --git a/app/views/profile.html b/app/views/profile.html new file mode 100644 index 0000000..5197563 --- /dev/null +++ b/app/views/profile.html @@ -0,0 +1,81 @@ +
+
+

+ {{info.tag|preserve_space}} + {{info.name|preserve_space}} + {{info.tag|preserve_space}} +

+
+
+
+ {{alert.message}} + +
+
+
+
+

Profile Details

+ + + + + + + + + + + + + +
GUIDNameWebsite
{{info.guid}}{{info.name|preserve_space}}{{info.website}}
JoinedOnline
{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}{{info.online|yes_no|titleize}}
Info
{{info.info}}
+

Tribe Membership

+ + + + + + + + + + + + +
IDNameTitleRankActive
{{tribe.id}}{{tribe.title}}{{tribe.rank}} + + +
+ + +
+
+ +
+
+

Tribe Invitations

+ + + + + + + + + + + +
FromToExpires
{{invite.expire*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}AcceptReject
+
+
+

History

+ + + + + + +
TimeDescription
{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss'}}
+
+
+
\ No newline at end of file diff --git a/app/views/search.html b/app/views/search.html new file mode 100644 index 0000000..bf59090 --- /dev/null +++ b/app/views/search.html @@ -0,0 +1,39 @@ + \ No newline at end of file diff --git a/app/views/tribe.html b/app/views/tribe.html new file mode 100644 index 0000000..971d693 --- /dev/null +++ b/app/views/tribe.html @@ -0,0 +1,134 @@ +
+
+

+ {{info.tag|preserve_space}}{{info.name|preserve_space}}{{info.tag|preserve_space}} +

+
+
+
+ {{alert.message}} + +
+
+
+
+

Tribe Details

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDNameTag
{{info.id}} + {{info.name|preserve_space}} + {{info.name|preserve_space}} + + {{info.tag|preserve_space}} +
+
+ {{info.tag|preserve_space}} + {{info.append|yes_no|titleize}} +
+ {{info.tag|preserve_space}} + + + + +
+
Website
{{info.website}}{{info.website}}
CreatedRecruitingActive
{{info.creation*1000|date:'yyyy-MM-dd HH:mm:ss Z'}} + {{info.recruiting|yes_no|titleize}} + {{info.recruiting|yes_no|titleize}} + {{info.active|yes_no|titleize}}
InfoPicture
+
{{info.info}}
+
{{info.info}}
+
+ {{info.picture || 'Not set'}} + {{info.picture || 'Not set'}} +
Disband
+ Disband
+

Tribe Members

+ + + + + + + + + + + + + +
GUIDNameTitleRankOnlineKickEdit
{{player.guid}} + {{player.title}} + {{player.title}} + + {{player.rank}} + {{player.rank}} + {{player.online|yes_no|titleize}}Kick
+
+ + +
+ +
+
+

Send Invitation

+
+ + + + {{searchAlert}} +
+ + + + + + +
Results
GUIDNameInvite
{{player.guid}} + Invite
+
+
+

Tribe Invitations

+ + + + + + +
SenderRecipientExpires
{{invite.expire*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}
+
+
+
+

History

+ + + + + + +
TimeDescription
{{item.time*1000|date:'yyyy-MM-dd HH:mm:ss Z'}}
+
+
+
\ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..f368bc1 --- /dev/null +++ b/bower.json @@ -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" + ] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4189380 --- /dev/null +++ b/package.json @@ -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" + } +}