From a7afd372684826a546508cada9635fb84520cb71 Mon Sep 17 00:00:00 2001 From: Insane Turkey Date: Mon, 25 Jan 2016 19:45:53 -0500 Subject: [PATCH] Initial commit. --- .bowerrc | 4 + .editorconfig | 21 ++ .gitattributes | 1 + .gitignore | 5 + .jscsrc | 6 + .jshintrc | 16 ++ .travis.yml | 9 + .yo-rc.json | 1 + Gruntfile.js | 445 ++++++++++++++++++++++++++++++++++ README.md | 4 + app/404.html | 152 ++++++++++++ app/favicon.ico | 0 app/index.html | 66 ++++++ app/robots.txt | 4 + app/scripts/app.js | 527 +++++++++++++++++++++++++++++++++++++++++ app/styles/main.scss | 150 ++++++++++++ app/views/create.html | 48 ++++ app/views/login.html | 41 ++++ app/views/player.html | 55 +++++ app/views/profile.html | 81 +++++++ app/views/search.html | 39 +++ app/views/tribe.html | 134 +++++++++++ bower.json | 24 ++ package.json | 39 +++ 24 files changed, 1872 insertions(+) create mode 100644 .bowerrc create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .jscsrc create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 .yo-rc.json create mode 100644 Gruntfile.js create mode 100644 README.md create mode 100644 app/404.html create mode 100644 app/favicon.ico create mode 100644 app/index.html create mode 100644 app/robots.txt create mode 100644 app/scripts/app.js create mode 100644 app/styles/main.scss create mode 100644 app/views/create.html create mode 100644 app/views/login.html create mode 100644 app/views/player.html create mode 100644 app/views/profile.html create mode 100644 app/views/search.html create mode 100644 app/views/tribe.html create mode 100644 bower.json create mode 100644 package.json 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" + } +}