├── .gitattributes ├── app ├── .buildignore ├── robots.txt ├── favicon.ico ├── images │ └── yeoman.png ├── styles │ ├── import.scss │ ├── main.scss │ └── print.scss ├── views │ ├── partials │ │ ├── print.html │ │ ├── import.html │ │ └── main.html │ ├── 404.html │ └── index.html ├── scripts │ ├── services │ │ └── user.js │ ├── controllers │ │ ├── import.js │ │ └── print.js │ └── app.js └── .htaccess ├── .bowerrc ├── lib ├── config │ ├── env │ │ ├── test.js │ │ ├── development.js │ │ ├── all.js │ │ └── production.js │ ├── config.js │ └── express.js ├── .jshintrc ├── routes.js └── controllers │ ├── index.js │ └── api.js ├── .travis.yml ├── .gitignore ├── CardGamePrototyper.sublime-project ├── test ├── client │ ├── runner.html │ ├── spec │ │ ├── services │ │ │ └── user.js │ │ └── controllers │ │ │ ├── main.js │ │ │ ├── print.js │ │ │ └── import.js │ └── .jshintrc ├── server │ └── thing │ │ └── api.js └── karma.conf.js ├── .editorconfig ├── bower.json ├── .jshintrc ├── server.js ├── package.json ├── README.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /lib/config/env/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | env: 'test' 5 | }; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GrantGamesGames/CardGamePrototyper/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /lib/config/env/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | env: 'development' 5 | }; -------------------------------------------------------------------------------- /app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GrantGamesGames/CardGamePrototyper/HEAD/app/images/yeoman.png -------------------------------------------------------------------------------- /app/styles/import.scss: -------------------------------------------------------------------------------- 1 | .csv-import { 2 | 3 | textarea { 4 | width: 100%; 5 | min-height: 600px; 6 | } 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | heroku 7 | /views 8 | dist 9 | .grunt 10 | 11 | *.sublime-workspace -------------------------------------------------------------------------------- /lib/config/env/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var rootPath = path.normalize(__dirname + '/../../..'); 6 | 7 | module.exports = { 8 | root: rootPath, 9 | ip: '0.0.0.0', 10 | port: process.env.PORT || 9000 11 | }; -------------------------------------------------------------------------------- /lib/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Load environment configuration 7 | */ 8 | module.exports = _.merge( 9 | require('./env/all.js'), 10 | require('./env/' + process.env.NODE_ENV + '.js') || {}); -------------------------------------------------------------------------------- /lib/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "latedef": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "regexp": true, 11 | "undef": true, 12 | "smarttabs": true 13 | } -------------------------------------------------------------------------------- /CardGamePrototyper.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "folder_exclude_patterns": [ 8 | "dist", 9 | ".tmp", 10 | ".sass-cache", 11 | "node_modules" 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/client/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/config/env/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | env: 'production', 5 | ip: process.env.OPENSHIFT_NODEJS_IP || 6 | process.env.IP || 7 | '0.0.0.0', 8 | port: process.env.OPENSHIFT_NODEJS_PORT || 9 | process.env.PORT || 10 | 8080 11 | }; -------------------------------------------------------------------------------- /app/views/partials/print.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |

{{card.type.Hint}}

5 |
6 |

{{card.type.Name}}

7 |

{{card.type.Description}}

8 |
9 |
10 |
-------------------------------------------------------------------------------- /app/views/partials/import.html: -------------------------------------------------------------------------------- 1 |

Import your CSV data

2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /app/views/partials/main.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
6 |
7 |
8 |

{{value.type.Name}}

9 |

{{value.type.Description}}

10 |
11 |
12 |
-------------------------------------------------------------------------------- /test/client/spec/services/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: user', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('cardGamePrototyper')); 7 | 8 | // instantiate service 9 | var user; 10 | beforeEach(inject(function (_user_) { 11 | user = _user_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!user).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck-builder", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": ">=1.2.*", 6 | "json3": "~3.2.6", 7 | "es5-shim": "~2.1.0", 8 | "jquery": "~1.11.0", 9 | "bootstrap-sass-official": "~3.1.1", 10 | "underscore": "~1.6.0", 11 | "angular-ui-router": "~0.2.10", 12 | "comma-separated-values": "~3.2.0" 13 | }, 14 | "devDependencies": { 15 | "angular-mocks": ">=1.2.*", 16 | "angular-scenario": ">=1.2.*" 17 | }, 18 | "testPath": "test/client/spec" 19 | } 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": false, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "_": false, 24 | "ga": false, 25 | "CSV": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/scripts/services/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('cardGamePrototyper') 4 | .service('user', function user() { 5 | // AngularJS will instantiate a singleton by calling "new" on this function 6 | var userService = { 7 | setCsvWithStr: _setCsvWithStr, 8 | data: null 9 | }; 10 | return userService; 11 | 12 | function _setCsvWithStr(csvStr) { 13 | var csvOptions = { 14 | cast: false, 15 | header: true, 16 | line: '\n' 17 | }; 18 | var csvData = new CSV(csvStr, csvOptions); 19 | userService.data = csvData.parse(); 20 | } 21 | 22 | }); -------------------------------------------------------------------------------- /app/scripts/controllers/import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('cardGamePrototyper') 4 | .config(function ($stateProvider) { 5 | $stateProvider.state('import', { 6 | url: '/import', 7 | title: 'Import your data', 8 | templateUrl: 'partials/import', 9 | controller: 'ImportCtrl' 10 | }); 11 | }) 12 | .controller('ImportCtrl', function($scope, user, $state) { 13 | $scope.import = function() { 14 | if ($scope.data) { 15 | user.setCsvWithStr($scope.data); 16 | ga('send', 'event', 'import', 'csv'); 17 | $state.go('print'); 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /test/server/thing/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'), 4 | app = require('../../../server'), 5 | request = require('supertest'); 6 | 7 | describe('GET /api/awesomeThings', function() { 8 | 9 | it('should respond with JSON array', function(done) { 10 | request(app) 11 | .get('/api/awesomeThings') 12 | .expect(200) 13 | .expect('Content-Type', /json/) 14 | .end(function(err, res) { 15 | if (err) return done(err); 16 | res.body.should.be.instanceof(Array); 17 | done(); 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var api = require('./controllers/api'), 4 | index = require('./controllers'); 5 | 6 | /** 7 | * Application routes 8 | */ 9 | module.exports = function(app) { 10 | 11 | // Server API Routes 12 | app.route('/api/awesomeThings') 13 | .get(api.awesomeThings); 14 | 15 | 16 | // All undefined api routes should return a 404 17 | app.route('/api/*') 18 | .get(function(req, res) { 19 | res.send(404); 20 | }); 21 | 22 | // All other routes to use Angular routing in app/scripts/app.js 23 | app.route('/partials/*') 24 | .get(index.partials); 25 | app.route('/*') 26 | .get( index.index); 27 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | 5 | /** 6 | * Main application file 7 | */ 8 | 9 | // Set default node environment to development 10 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 11 | 12 | var config = require('./lib/config/config'); 13 | 14 | // Setup Express 15 | var app = express(); 16 | require('./lib/config/express')(app); 17 | require('./lib/routes')(app); 18 | 19 | // Start server 20 | app.listen(config.port, config.ip, function () { 21 | console.log('Express server listening on %s:%d, in %s mode', config.ip, config.port, app.get('env')); 22 | }); 23 | 24 | // Expose app 25 | exports = module.exports = app; 26 | -------------------------------------------------------------------------------- /lib/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | /** 6 | * Send partial, or 404 if it doesn't exist 7 | */ 8 | exports.partials = function(req, res) { 9 | var stripped = req.url.split('.')[0]; 10 | var requestedView = path.join('./', stripped); 11 | res.render(requestedView, function(err, html) { 12 | if(err) { 13 | console.log("Error rendering partial '" + requestedView + "'\n", err); 14 | res.status(404); 15 | res.send(404); 16 | } else { 17 | res.send(html); 18 | } 19 | }); 20 | }; 21 | 22 | /** 23 | * Send our single page app 24 | */ 25 | exports.index = function(req, res) { 26 | res.render('index'); 27 | }; 28 | -------------------------------------------------------------------------------- /test/client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/scripts/controllers/print.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('cardGamePrototyper') 4 | .config(function ($stateProvider) { 5 | $stateProvider.state('print', { 6 | url: '/print', 7 | title: 'Print your data', 8 | templateUrl: 'partials/print', 9 | controller: 'PrintCtrl', 10 | resolve: { 11 | userData: ['user', '$q', function(user, $q) { 12 | if(user.data) { 13 | return user.data; 14 | } else { 15 | return $q.reject(); 16 | } 17 | }] 18 | } 19 | }); 20 | }) 21 | .controller('PrintCtrl', function(userData, $scope, user, $state) { 22 | $scope.cardList = []; 23 | 24 | _.each(user.data, function(cardDef) { 25 | _.times(cardDef.Quantity, function() { 26 | var card = { 27 | type: cardDef 28 | }; 29 | $scope.cardList.push(card); 30 | }); 31 | }); 32 | }); -------------------------------------------------------------------------------- /test/client/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('cardGamePrototyper')); 7 | 8 | var MainCtrl, 9 | scope, 10 | $httpBackend; 11 | 12 | // Initialize the controller and a mock scope 13 | beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { 14 | $httpBackend = _$httpBackend_; 15 | $httpBackend.expectGET('/api/awesomeThings') 16 | .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); 17 | scope = $rootScope.$new(); 18 | MainCtrl = $controller('MainCtrl', { 19 | $scope: scope 20 | }); 21 | })); 22 | 23 | it('should attach a list of awesomeThings to the scope', function () { 24 | expect(scope.awesomeThings).toBeUndefined(); 25 | $httpBackend.flush(); 26 | expect(scope.awesomeThings.length).toBe(4); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/client/spec/controllers/print.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: PrintCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('cardGamePrototyper')); 7 | 8 | var PrintCtrl, 9 | scope, 10 | $httpBackend; 11 | 12 | // Initialize the controller and a mock scope 13 | beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { 14 | $httpBackend = _$httpBackend_; 15 | $httpBackend.expectGET('/api/awesomeThings') 16 | .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); 17 | scope = $rootScope.$new(); 18 | PrintCtrl = $controller('PrintCtrl', { 19 | $scope: scope 20 | }); 21 | })); 22 | 23 | it('should attach a list of awesomeThings to the scope', function () { 24 | expect(scope.awesomeThings).toBeUndefined(); 25 | $httpBackend.flush(); 26 | expect(scope.awesomeThings.length).toBe(4); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/client/spec/controllers/import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ImportCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('cardGamePrototyper')); 7 | 8 | var ImportCtrl, 9 | scope, 10 | $httpBackend; 11 | 12 | // Initialize the controller and a mock scope 13 | beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { 14 | $httpBackend = _$httpBackend_; 15 | $httpBackend.expectGET('/api/awesomeThings') 16 | .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); 17 | scope = $rootScope.$new(); 18 | ImportCtrl = $controller('ImportCtrl', { 19 | $scope: scope 20 | }); 21 | })); 22 | 23 | it('should attach a list of awesomeThings to the scope', function () { 24 | expect(scope.awesomeThings).toBeUndefined(); 25 | $httpBackend.flush(); 26 | expect(scope.awesomeThings.length).toBe(4); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/"; 2 | 3 | @import 'bootstrap-sass-official/vendor/assets/stylesheets/bootstrap'; 4 | 5 | .browsehappy { 6 | margin: 0.2em 0; 7 | background: #ccc; 8 | color: #000; 9 | padding: 0.2em 0; 10 | } 11 | 12 | /* Space out content a bit */ 13 | body { 14 | padding-top: 20px; 15 | padding-bottom: 20px; 16 | } 17 | 18 | .viewLoader { 19 | height: 0; 20 | opacity: 0; 21 | position: relative; 22 | /* top: -30px; */ 23 | -webkit-transition: opacity .5s ease-out; 24 | -moz-transition: opacity .5s ease-out; 25 | -o-transition: opacity .5s ease-out; 26 | -ms-transition: opacity .5s ease-out; 27 | transition: opacity .5s ease-out; 28 | } 29 | 30 | .viewLoader.loading { 31 | opacity: 1; 32 | pointer-events: none; 33 | } 34 | 35 | .viewLoader p { 36 | text-align: center; 37 | background-color: black; 38 | color: white; 39 | padding: 10px; 40 | } -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('cardGamePrototyper', ['ui.router']) 4 | .config(function($locationProvider, $urlRouterProvider) { 5 | $locationProvider.html5Mode(true); 6 | $urlRouterProvider.otherwise('/import'); 7 | 8 | }) 9 | .run(function($rootScope, user, $state) { 10 | 11 | // loading animation 12 | $rootScope.setLoading = function() { 13 | $rootScope.isViewLoading = true; 14 | }; 15 | $rootScope.unsetLoading = function() { 16 | $rootScope.isViewLoading = false; 17 | }; 18 | 19 | $rootScope.unsetLoading(); 20 | 21 | $rootScope.$on('$stateChangeStart', function(ev, to, toParams, from, fromParams) { 22 | $rootScope.setLoading(); 23 | }); 24 | 25 | $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) { 26 | $rootScope.unsetLoading(); 27 | }); 28 | 29 | $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) { 30 | $rootScope.unsetLoading(); 31 | if(to.name !== 'import') { 32 | $state.go('import'); 33 | } 34 | }); 35 | } 36 | ); -------------------------------------------------------------------------------- /app/styles/print.scss: -------------------------------------------------------------------------------- 1 | // Print Styles 2 | $card-border-size: 1px; 3 | 4 | @media all { 5 | .page-break { 6 | display: none; 7 | } 8 | } 9 | 10 | @media print { 11 | .page-break { 12 | display: block; 13 | page-break-after: always; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | } 19 | 20 | } 21 | 22 | .cardItem { 23 | display: inline-flex; //its like a block, but it floats and breaks for printing! 24 | // font-size: 0; 25 | margin-top: -$card-border-size; // to overlap borders 26 | margin-right: -$card-border-size; // to overlap borders 27 | margin-left: auto; 28 | margin-bottom: auto; 29 | 30 | .card { 31 | position: relative; 32 | border: $card-border-size solid #DDD; 33 | text-align: center; 34 | margin: 0; 35 | width: 2.5in; 36 | height: 3.5in; 37 | overflow: hidden; 38 | 39 | .cardContent { 40 | position: relative; 41 | top: 50%; 42 | left: 0; 43 | padding: 0 6% 6% 6%; 44 | -webkit-touch-callout: none; 45 | -webkit-user-select: none; 46 | -khtml-user-select: none; 47 | -moz-user-select: none; 48 | -ms-user-select: none; 49 | user-select: none; 50 | font-size: 15px; 51 | color: black !important; 52 | 53 | h2 { 54 | margin-top: 0; 55 | font-size: 18px; 56 | } 57 | } 58 | 59 | .hint { 60 | position: absolute; 61 | top: 6%; 62 | left: 10%; 63 | margin: 0; 64 | } 65 | } 66 | } 67 | 68 | 69 | @media print { 70 | .doNotPrint { 71 | display: none; 72 | } 73 | } -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-06-22 using 4 | // generator-karma 0.8.2 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: true, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | frameworks: ['jasmine'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: [], 19 | 20 | // list of files / patterns to exclude 21 | exclude: [], 22 | 23 | // web server port 24 | port: 8080, 25 | 26 | // Start these browsers, currently available: 27 | // - Chrome 28 | // - ChromeCanary 29 | // - Firefox 30 | // - Opera 31 | // - Safari (only Mac) 32 | // - PhantomJS 33 | // - IE (only Windows) 34 | browsers: [ 35 | 'PhantomJS' 36 | ], 37 | 38 | // Which plugins to enable 39 | plugins: [ 40 | 'karma-phantomjs-launcher', 41 | 'karma-jasmine' 42 | ], 43 | 44 | // Continuous Integration mode 45 | // if true, it capture browsers, run tests and exit 46 | singleRun: false, 47 | 48 | colors: true, 49 | 50 | // level of logging 51 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | // Uncomment the following lines if you are using grunt's server to run the tests 55 | // proxies: { 56 | // '/': 'http://localhost:9000/' 57 | // }, 58 | // URL root prevent conflicts with the site root 59 | // urlRoot: '_karma_' 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /lib/config/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'), 4 | favicon = require('static-favicon'), 5 | morgan = require('morgan'), 6 | compression = require('compression'), 7 | bodyParser = require('body-parser'), 8 | methodOverride = require('method-override'), 9 | cookieParser = require('cookie-parser'), 10 | session = require('express-session'), 11 | errorHandler = require('errorhandler'), 12 | path = require('path'), 13 | config = require('./config'); 14 | 15 | /** 16 | * Express configuration 17 | */ 18 | module.exports = function(app) { 19 | var env = app.get('env'); 20 | 21 | if ('development' === env) { 22 | app.use(require('connect-livereload')()); 23 | 24 | // Disable caching of scripts for easier testing 25 | app.use(function noCache(req, res, next) { 26 | if (req.url.indexOf('/scripts/') === 0) { 27 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 28 | res.header('Pragma', 'no-cache'); 29 | res.header('Expires', 0); 30 | } 31 | next(); 32 | }); 33 | 34 | app.use(express.static(path.join(config.root, '.tmp'))); 35 | app.use(express.static(path.join(config.root, 'app'))); 36 | app.set('views', config.root + '/app/views'); 37 | } 38 | 39 | if ('production' === env) { 40 | app.use(compression()); 41 | app.use(favicon(path.join(config.root, 'public', 'favicon.ico'))); 42 | app.use(express.static(path.join(config.root, 'public'))); 43 | app.set('views', config.root + '/views'); 44 | } 45 | 46 | app.engine('html', require('ejs').renderFile); 47 | app.set('view engine', 'html'); 48 | app.use(morgan('dev')); 49 | app.use(bodyParser()); 50 | app.use(methodOverride()); 51 | 52 | // Error handler - has to be last 53 | if ('development' === app.get('env')) { 54 | app.use(errorHandler()); 55 | } 56 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cardGamePrototyper", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "express": "~4.0.0", 6 | "morgan": "~1.0.0", 7 | "body-parser": "~1.0.0", 8 | "method-override": "~1.0.0", 9 | "static-favicon": "~1.0.1", 10 | "cookie-parser": "~1.0.1", 11 | "express-session": "~1.0.2", 12 | "errorhandler": "~1.0.0", 13 | "compression": "~1.0.1", 14 | "lodash": "~2.4.1", 15 | "ejs": "~0.8.4" 16 | }, 17 | "devDependencies": { 18 | "grunt": "~0.4.1", 19 | "grunt-autoprefixer": "~0.4.0", 20 | "grunt-bower-install": "~0.7.0", 21 | "grunt-concurrent": "~0.4.1", 22 | "grunt-contrib-clean": "~0.5.0", 23 | "grunt-contrib-coffee": "~0.7.0", 24 | "grunt-contrib-compass": "~0.6.0", 25 | "grunt-contrib-concat": "~0.3.0", 26 | "grunt-contrib-copy": "~0.4.1", 27 | "grunt-contrib-cssmin": "~0.7.0", 28 | "grunt-contrib-htmlmin": "~0.1.3", 29 | "grunt-contrib-imagemin": "~0.5.0", 30 | "grunt-contrib-jshint": "~0.7.1", 31 | "grunt-contrib-uglify": "~0.2.0", 32 | "grunt-contrib-watch": "~0.5.2", 33 | "grunt-google-cdn": "~0.2.0", 34 | "grunt-newer": "~0.5.4", 35 | "grunt-ngmin": "~0.0.2", 36 | "grunt-rev": "~0.1.0", 37 | "grunt-svgmin": "~0.2.0", 38 | "grunt-usemin": "~2.0.0", 39 | "jshint-stylish": "~0.1.3", 40 | "load-grunt-tasks": "~0.2.0", 41 | "time-grunt": "~0.2.1", 42 | "grunt-express-server": "~0.4.5", 43 | "grunt-open": "~0.2.0", 44 | "connect-livereload": "~0.3.0", 45 | "karma-ng-scenario": "~0.1.0", 46 | "grunt-karma": "~0.6.2", 47 | "karma-firefox-launcher": "~0.1.3", 48 | "karma-script-launcher": "~0.1.0", 49 | "karma-html2js-preprocessor": "~0.1.0", 50 | "karma-jasmine": "~0.1.5", 51 | "karma-chrome-launcher": "~0.1.2", 52 | "requirejs": "~2.1.10", 53 | "karma-requirejs": "~0.2.1", 54 | "karma-coffee-preprocessor": "~0.1.2", 55 | "karma-phantomjs-launcher": "~0.1.1", 56 | "karma": "~0.10.9", 57 | "karma-ng-html2js-preprocessor": "~0.1.0", 58 | "grunt-mocha-test": "~0.8.1", 59 | "supertest": "~0.8.2", 60 | "should": "~2.1.0", 61 | "grunt-env": "~0.4.1", 62 | "grunt-node-inspector": "~0.1.3", 63 | "grunt-nodemon": "~0.2.0", 64 | "open": "~0.0.4" 65 | }, 66 | "engines": { 67 | "node": ">=0.10.0" 68 | }, 69 | "scripts": { 70 | "test": "grunt test" 71 | } 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CardGamePrototyper 2 | 3 | [![Join the chat at https://gitter.im/astrism/CardGamePrototyper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/astrism/CardGamePrototyper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | CSV -> printable card game. 6 | 7 | ## What is this? 8 | 9 | This tool takes in CSV take and generates a printable card game. Works in Chrome, Safari, Firefox as far as I know. I recommend Chrome as its handling of printing is the best. 10 | 11 | ## How do I use this? 12 | 13 | Grab a CSV file, I use Google Spreadsheets to put mine together and most spreadsheet apps can export to csv. Your CSV can have any fields but the app only uses these: 14 | 15 | * Name 16 | * Quantity 17 | * Hint 18 | * Description 19 | 20 | This is a basic Werewolf example which uses all the required fields listed above, and even has an extra field, Probability, that the app ignores. 21 | 22 | | Name | Quantity | Hint | Description | Probability | 23 | |----------|----------|------|-----------------------------------------------------------------------------------------|-------------| 24 | | Seer | 1 | S | When instructed, pick one player and the moderator will tell you if they're a werewolf. | 14% | 25 | | Werewolf | 2 | W | At nightfall open your eyes and pick a victim | 29% | 26 | | Villager | 4 | V | At daybreak try to find the werewolf | | 27 | 28 |

To recreate the doc above:

29 | * Open the [Google Docs Template](https://docs.google.com/spreadsheets/d/1W1_9gVEfpQlfFlWHbc9E0ZqS37XCZpAQnUs1sawd0gc/edit?usp=sharing) 30 | * In Google Docs go to *File* -> *Make a copy...* 31 | * Customise for your game 32 | * To get the csv from Google Docs, use *File* -> *Download as* -> *Comma Separated Values (.csv, current sheet)* 33 | 34 | Which would produce when exported: 35 | 36 | ```csv 37 | Name,Quantity,Hint,Description,Probability 38 | Seer,1,S,"When instructed, pick one player and the moderator will tell you if they're a werewolf.",14% 39 | Werewolf,2,W,At nightfall open your eyes and pick a victim,29% 40 | Villager,4,V,At daybreak try to find the werewolf,57% 41 | ``` 42 | 43 | Next paste that csv data into the app and press **Import Data**. This will convert the CSV to printable Poker Size (2.5" * 3.5") cards. 44 | 45 | Now print! When printing I recommend Landscape, which fits the most poker size cards. 46 | 47 | ## Is this printer friendly? 48 | Yes. We use css to hide the instructions. Just go file print in your browser. For best results print in landscape mode. 49 | 50 | ## How often will you be making updates to this tool? 51 | We will be trying to update every two weeks with a rev. The tool is also open source so if your an awesome developer we welcome your support. 52 | 53 | ## How do I get feature request in the builds? 54 | Wow! You want a lot for nothing. No seriously. Thanks for the feedback. Post in the [issues tracker on GitHub](https://github.com/astrism/CardGamePrototyper/issues) or email team AT godhatesgames.com -------------------------------------------------------------------------------- /lib/controllers/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get awesome things 5 | */ 6 | exports.awesomeThings = function(req, res) { 7 | res.json([ 8 | { 9 | "Type":"Hand", 10 | "Name":"Straight Flush", 11 | "Quantity":1, 12 | "Description":"2" 13 | }, 14 | { 15 | "Type":"Hand", 16 | "Name":"Royal Flush", 17 | "Quantity":1, 18 | "Description":"1" 19 | }, 20 | { 21 | "Type":"Hand", 22 | "Name":"Four of a Kind", 23 | "Quantity":1, 24 | "Description":"3" 25 | }, 26 | { 27 | "Type":"Hand", 28 | "Name":"Full House", 29 | "Quantity":1, 30 | "Description":"4" 31 | }, 32 | { 33 | "Type":"Hnad", 34 | "Name":"Flush", 35 | "Quantity":2, 36 | "Description":"5" 37 | }, 38 | { 39 | "Type":"Hand", 40 | "Name":"Straight", 41 | "Quantity":2, 42 | "Description":"6" 43 | }, 44 | { 45 | "Type":"Hand", 46 | "Name":"Three of a Kind", 47 | "Quantity":3, 48 | "Description":"7" 49 | }, 50 | { 51 | "Type":"Hand", 52 | "Name":"Two Pair", 53 | "Quantity":3, 54 | "Description":"8" 55 | }, 56 | { 57 | "Type":"Hand", 58 | "Name":"One Pair", 59 | "Quantity":4, 60 | "Description":"9" 61 | }, 62 | { 63 | "Type":"Hand", 64 | "Name":"High Card", 65 | "Quantity":5, 66 | "Description":"10" 67 | }, 68 | { 69 | "Type":"Action", 70 | "Name":"Cheat", 71 | "Quantity":5, 72 | "Description":"Look at any players hand" 73 | }, 74 | { 75 | "Type":"Action", 76 | "Name":"Spot", 77 | "Quantity":5, 78 | "Description":"Stop a cheat. Can be used to stop a cheat on any player" 79 | }, 80 | { 81 | "Type":"Action", 82 | "Name":"Slight Of Hand", 83 | "Quantity":3, 84 | "Description":"Switch a Hand CARD With Another Player" 85 | }, 86 | { 87 | "Type":"Action", 88 | "Name":"Mark", 89 | "Quantity":3, 90 | "Description":"Randomly Trade A Hand Card with another Another Player" 91 | }, 92 | { 93 | "Type":"Action", 94 | "Name":"Threat", 95 | "Quantity":2, 96 | "Description":"Force a player to fold" 97 | }, 98 | { 99 | "Type":"Action", 100 | "Name":"Shoot", 101 | "Quantity":2, 102 | "Description":"Assignate another player" 103 | }, 104 | { 105 | "Type":"Action", 106 | "Name":"Arrest", 107 | "Quantity":2, 108 | "Description":"Remove a player from the hand. They can't use any Actions" 109 | }, 110 | { 111 | "Type":"Player", 112 | "Name":"Judge Roy Bean", 113 | "Quantity":1, 114 | "Description":"The Judge Can Arrest Anyone who Shoots someone else" 115 | }, 116 | { 117 | "Type":"Player", 118 | "Name":"\"Doc\" Holliday", 119 | "Quantity":1, 120 | "Description":"Doc Can Shoot anyone who has shot someone else" 121 | }, 122 | { 123 | "Type":"Player", 124 | "Name":"Billy the Kid", 125 | "Quantity":1, 126 | "Description":"Can use a Shoot as a threat." 127 | }, 128 | { 129 | "Type":"Player", 130 | "Name":"Calamity Jane", 131 | "Quantity":1, 132 | "Description":"Can't be cheated" 133 | }, 134 | { 135 | "Type":"Player", 136 | "Name":"Buffalo Bill", 137 | "Quantity":1, 138 | "Description":"Can discard hand card and draw another hand card" 139 | } 140 | ]); 141 | }; -------------------------------------------------------------------------------- /app/views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

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

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /app/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 |
31 | 32 |
34 |

Loading...

35 |
36 |
38 |
39 | 40 |
41 |
42 |

43 | CardGamePrototyper

45 | 46 |

CSV -> printable card game.

47 | 48 |

49 | What is this?

51 | 52 |

This tool takes in CSV take and generates a printable card game. Works in Chrome, Safari, Firefox as far as I know. I recommend Chrome as its handling of printing is the best.

53 | 54 |

55 | How do I use this?

57 | 58 |

Grab a CSV file, I use Google Spreadsheets to put mine together and most spreadsheet apps can export to csv. Your CSV can have any fields but the app only uses these:

59 | 60 |
    61 |
  • Name
  • 62 |
  • Quantity
  • 63 |
  • Hint
  • 64 |
  • Description
  • 65 |
66 | 67 |

This is a basic Werewolf example which uses all the required fields listed above, and even has an extra field, Probability, that the app ignores.

68 | 69 | 72 | 73 |

To recreate the doc above:

74 |
    75 |
  • Open the 76 | 78 | Google Docs Template 79 |
  • 80 |
  • In Google Docs go to File -> Make a copy...
  • 81 |
  • Customise for your game
  • 82 |
  • To get the csv from Google Docs, use File -> Download as -> Comma Separated Values (.csv, current sheet)
  • 83 |
84 | 85 |

Which would produce when exported:

86 | 87 |
Name,Quantity,Hint,Description,Probability
 88 |           Seer,1,S,"When instructed, pick one player and the moderator will tell you if they're a werewolf.",14%
 89 |           Werewolf,2,W,At nightfall open your eyes and pick a victim,29%
 90 |           Villager,4,V,At daybreak try to find the werewolf,57%
91 | 92 |

Next paste that csv data into the app and press 93 | Import Data. This will convert the CSV to printable Poker Size (2.5" * 3.5") cards.

94 | 95 |

Now print! When printing I recommend Landscape, which fits the most poker size cards.

96 | 97 |

Is this printer friendly?

98 |

Yes. We use css to hide the instructions. Just go file print in your browser. For best results print in landscape mode.

99 | 100 |

How do I get feature request in the builds?

101 |

Wow! You want a lot for nothing. No seriously. Thanks for the feedback. Post in the issues tracker on GitHub or email team AT godhatesgames.com

102 |
103 |
104 | 105 | 112 | 113 |
114 | 115 | 116 | 126 | 127 | 128 | 129 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-06-22 using generator-angular-fullstack 1.4.3 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Define the configuration for all the tasks 19 | grunt.initConfig({ 20 | 21 | // Project settings 22 | yeoman: { 23 | // configurable paths 24 | app: require('./bower.json').appPath || 'app', 25 | dist: 'dist' 26 | }, 27 | express: { 28 | options: { 29 | port: process.env.PORT || 9000 30 | }, 31 | dev: { 32 | options: { 33 | script: 'server.js', 34 | debug: true 35 | } 36 | }, 37 | prod: { 38 | options: { 39 | script: 'dist/server.js', 40 | node_env: 'production' 41 | } 42 | } 43 | }, 44 | open: { 45 | server: { 46 | url: 'http://localhost:<%= express.options.port %>' 47 | } 48 | }, 49 | watch: { 50 | js: { 51 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 52 | tasks: ['newer:jshint:all'], 53 | options: { 54 | livereload: true 55 | } 56 | }, 57 | mochaTest: { 58 | files: ['test/server/{,*/}*.js'], 59 | tasks: ['env:test', 'mochaTest'] 60 | }, 61 | jsTest: { 62 | files: ['test/client/spec/{,*/}*.js'], 63 | tasks: ['newer:jshint:test', 'karma'] 64 | }, 65 | compass: { 66 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 67 | tasks: ['compass:server', 'autoprefixer'] 68 | }, 69 | gruntfile: { 70 | files: ['Gruntfile.js'] 71 | }, 72 | livereload: { 73 | files: [ 74 | '<%= yeoman.app %>/views/{,*//*}*.{html,jade}', 75 | '{.tmp,<%= yeoman.app %>}/styles/{,*//*}*.css', 76 | '{.tmp,<%= yeoman.app %>}/scripts/{,*//*}*.js', 77 | '<%= yeoman.app %>/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' 78 | ], 79 | 80 | options: { 81 | livereload: true 82 | } 83 | }, 84 | express: { 85 | files: [ 86 | 'server.js', 87 | 'lib/**/*.{js,json}' 88 | ], 89 | tasks: ['newer:jshint:server', 'express:dev', 'wait'], 90 | options: { 91 | livereload: true, 92 | nospawn: true //Without this option specified express won't be reloaded 93 | } 94 | } 95 | }, 96 | 97 | // Make sure code styles are up to par and there are no obvious mistakes 98 | jshint: { 99 | options: { 100 | jshintrc: '.jshintrc', 101 | reporter: require('jshint-stylish') 102 | }, 103 | server: { 104 | options: { 105 | jshintrc: 'lib/.jshintrc' 106 | }, 107 | src: [ 'lib/{,*/}*.js'] 108 | }, 109 | all: [ 110 | '<%= yeoman.app %>/scripts/{,*/}*.js' 111 | ], 112 | test: { 113 | options: { 114 | jshintrc: 'test/client/.jshintrc' 115 | }, 116 | src: ['test/client/spec/{,*/}*.js'] 117 | } 118 | }, 119 | 120 | // Empties folders to start fresh 121 | clean: { 122 | dist: { 123 | files: [{ 124 | dot: true, 125 | src: [ 126 | '.tmp', 127 | '<%= yeoman.dist %>/*', 128 | '!<%= yeoman.dist %>/.git*', 129 | '!<%= yeoman.dist %>/Procfile' 130 | ] 131 | }] 132 | }, 133 | heroku: { 134 | files: [{ 135 | dot: true, 136 | src: [ 137 | 'heroku/*', 138 | '!heroku/.git*', 139 | '!heroku/Procfile' 140 | ] 141 | }] 142 | }, 143 | server: '.tmp' 144 | }, 145 | 146 | // Add vendor prefixed styles 147 | autoprefixer: { 148 | options: { 149 | browsers: ['last 1 version'] 150 | }, 151 | dist: { 152 | files: [{ 153 | expand: true, 154 | cwd: '.tmp/styles/', 155 | src: '{,*/}*.css', 156 | dest: '.tmp/styles/' 157 | }] 158 | } 159 | }, 160 | 161 | // Debugging with node inspector 162 | 'node-inspector': { 163 | custom: { 164 | options: { 165 | 'web-host': 'localhost' 166 | } 167 | } 168 | }, 169 | 170 | // Use nodemon to run server in debug mode with an initial breakpoint 171 | nodemon: { 172 | debug: { 173 | script: 'server.js', 174 | options: { 175 | nodeArgs: ['--debug-brk'], 176 | env: { 177 | PORT: process.env.PORT || 9000 178 | }, 179 | callback: function (nodemon) { 180 | nodemon.on('log', function (event) { 181 | console.log(event.colour); 182 | }); 183 | 184 | // opens browser on initial server start 185 | nodemon.on('config:update', function () { 186 | setTimeout(function () { 187 | require('open')('http://localhost:8080/debug?port=5858'); 188 | }, 500); 189 | }); 190 | } 191 | } 192 | } 193 | }, 194 | 195 | // Automatically inject Bower components into the app 196 | 'bower-install': { 197 | app: { 198 | html: '<%= yeoman.app %>/views/index.html', 199 | ignorePath: '<%= yeoman.app %>/', 200 | exclude: ['bootstrap-sass'] 201 | } 202 | }, 203 | 204 | // Compiles Sass to CSS and generates necessary files if requested 205 | compass: { 206 | options: { 207 | sassDir: '<%= yeoman.app %>/styles', 208 | cssDir: '.tmp/styles', 209 | generatedImagesDir: '.tmp/images/generated', 210 | imagesDir: '<%= yeoman.app %>/images', 211 | javascriptsDir: '<%= yeoman.app %>/scripts', 212 | fontsDir: '<%= yeoman.app %>/styles/fonts', 213 | importPath: '<%= yeoman.app %>/bower_components', 214 | httpImagesPath: '/images', 215 | httpGeneratedImagesPath: '/images/generated', 216 | httpFontsPath: '/styles/fonts', 217 | relativeAssets: false, 218 | assetCacheBuster: false, 219 | raw: 'Sass::Script::Number.precision = 10\n' 220 | }, 221 | dist: { 222 | options: { 223 | generatedImagesDir: '<%= yeoman.dist %>/public/images/generated' 224 | } 225 | }, 226 | server: { 227 | options: { 228 | debugInfo: true 229 | } 230 | } 231 | }, 232 | 233 | // Renames files for browser caching purposes 234 | rev: { 235 | dist: { 236 | files: { 237 | src: [ 238 | '<%= yeoman.dist %>/public/scripts/{,*/}*.js', 239 | '<%= yeoman.dist %>/public/styles/{,*/}*.css', 240 | '<%= yeoman.dist %>/public/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 241 | '<%= yeoman.dist %>/public/styles/fonts/*' 242 | ] 243 | } 244 | } 245 | }, 246 | 247 | // Reads HTML for usemin blocks to enable smart builds that automatically 248 | // concat, minify and revision files. Creates configurations in memory so 249 | // additional tasks can operate on them 250 | useminPrepare: { 251 | html: ['<%= yeoman.app %>/views/index.html', 252 | '<%= yeoman.app %>/views/index.jade'], 253 | options: { 254 | dest: '<%= yeoman.dist %>/public' 255 | } 256 | }, 257 | 258 | // Performs rewrites based on rev and the useminPrepare configuration 259 | usemin: { 260 | html: ['<%= yeoman.dist %>/views/{,*/}*.html', 261 | '<%= yeoman.dist %>/views/{,*/}*.jade'], 262 | css: ['<%= yeoman.dist %>/public/styles/{,*/}*.css'], 263 | options: { 264 | assetsDirs: ['<%= yeoman.dist %>/public'] 265 | } 266 | }, 267 | 268 | // The following *-min tasks produce minified files in the dist folder 269 | imagemin: { 270 | options : { 271 | cache: false 272 | }, 273 | dist: { 274 | files: [{ 275 | expand: true, 276 | cwd: '<%= yeoman.app %>/images', 277 | src: '{,*/}*.{png,jpg,jpeg,gif}', 278 | dest: '<%= yeoman.dist %>/public/images' 279 | }] 280 | } 281 | }, 282 | 283 | svgmin: { 284 | dist: { 285 | files: [{ 286 | expand: true, 287 | cwd: '<%= yeoman.app %>/images', 288 | src: '{,*/}*.svg', 289 | dest: '<%= yeoman.dist %>/public/images' 290 | }] 291 | } 292 | }, 293 | 294 | htmlmin: { 295 | dist: { 296 | options: { 297 | //collapseWhitespace: true, 298 | //collapseBooleanAttributes: true, 299 | //removeCommentsFromCDATA: true, 300 | //removeOptionalTags: true 301 | }, 302 | files: [{ 303 | expand: true, 304 | cwd: '<%= yeoman.app %>/views', 305 | src: ['*.html', 'partials/**/*.html'], 306 | dest: '<%= yeoman.dist %>/views' 307 | }] 308 | } 309 | }, 310 | 311 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 312 | // minsafe compatible so Uglify does not destroy the ng references 313 | ngmin: { 314 | dist: { 315 | files: [{ 316 | expand: true, 317 | cwd: '.tmp/concat/scripts', 318 | src: '*.js', 319 | dest: '.tmp/concat/scripts' 320 | }] 321 | } 322 | }, 323 | 324 | // Replace Google CDN references 325 | cdnify: { 326 | dist: { 327 | html: ['<%= yeoman.dist %>/views/*.html'] 328 | } 329 | }, 330 | 331 | // Copies remaining files to places other tasks can use 332 | copy: { 333 | dist: { 334 | files: [{ 335 | expand: true, 336 | dot: true, 337 | cwd: '<%= yeoman.app %>', 338 | dest: '<%= yeoman.dist %>/public', 339 | src: [ 340 | '*.{ico,png,txt}', 341 | '.htaccess', 342 | 'bower_components/**/*', 343 | 'images/{,*/}*.{webp}', 344 | 'fonts/**/*' 345 | ] 346 | }, { 347 | expand: true, 348 | dot: true, 349 | cwd: '<%= yeoman.app %>/views', 350 | dest: '<%= yeoman.dist %>/views', 351 | src: '**/*.jade' 352 | }, { 353 | expand: true, 354 | cwd: '.tmp/images', 355 | dest: '<%= yeoman.dist %>/public/images', 356 | src: ['generated/*'] 357 | }, { 358 | expand: true, 359 | dest: '<%= yeoman.dist %>', 360 | src: [ 361 | 'package.json', 362 | 'server.js', 363 | 'lib/**/*' 364 | ] 365 | }] 366 | }, 367 | styles: { 368 | expand: true, 369 | cwd: '<%= yeoman.app %>/styles', 370 | dest: '.tmp/styles/', 371 | src: '{,*/}*.css' 372 | } 373 | }, 374 | 375 | // Run some tasks in parallel to speed up the build process 376 | concurrent: { 377 | server: [ 378 | 'compass:server' 379 | ], 380 | test: [ 381 | 'compass' 382 | ], 383 | debug: { 384 | tasks: [ 385 | 'nodemon', 386 | 'node-inspector' 387 | ], 388 | options: { 389 | logConcurrentOutput: true 390 | } 391 | }, 392 | dist: [ 393 | 'compass:dist', 394 | 'imagemin', 395 | 'svgmin', 396 | 'htmlmin' 397 | ] 398 | }, 399 | 400 | // By default, your `index.html`'s will take care of 401 | // minification. These next options are pre-configured if you do not wish 402 | // to use the Usemin blocks. 403 | // cssmin: { 404 | // dist: { 405 | // files: { 406 | // '<%= yeoman.dist %>/styles/main.css': [ 407 | // '.tmp/styles/{,*/}*.css', 408 | // '<%= yeoman.app %>/styles/{,*/}*.css' 409 | // ] 410 | // } 411 | // } 412 | // }, 413 | // uglify: { 414 | // dist: { 415 | // files: { 416 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 417 | // '<%= yeoman.dist %>/scripts/scripts.js' 418 | // ] 419 | // } 420 | // } 421 | // }, 422 | // concat: { 423 | // dist: {} 424 | // }, 425 | 426 | // Test settings 427 | karma: { 428 | unit: { 429 | configFile: 'karma.conf.js', 430 | singleRun: true 431 | } 432 | }, 433 | 434 | mochaTest: { 435 | options: { 436 | reporter: 'spec' 437 | }, 438 | src: ['test/server/**/*.js'] 439 | }, 440 | 441 | env: { 442 | test: { 443 | NODE_ENV: 'test' 444 | } 445 | } 446 | }); 447 | 448 | // Used for delaying livereload until after server has restarted 449 | grunt.registerTask('wait', function () { 450 | grunt.log.ok('Waiting for server reload...'); 451 | 452 | var done = this.async(); 453 | 454 | setTimeout(function () { 455 | grunt.log.writeln('Done waiting!'); 456 | done(); 457 | }, 500); 458 | }); 459 | 460 | grunt.registerTask('express-keepalive', 'Keep grunt running', function() { 461 | this.async(); 462 | }); 463 | 464 | grunt.registerTask('serve', function (target) { 465 | if (target === 'dist') { 466 | return grunt.task.run(['build', 'express:prod', 'open', 'express-keepalive']); 467 | } 468 | 469 | if (target === 'debug') { 470 | return grunt.task.run([ 471 | 'clean:server', 472 | 'bower-install', 473 | 'concurrent:server', 474 | 'autoprefixer', 475 | 'concurrent:debug' 476 | ]); 477 | } 478 | 479 | grunt.task.run([ 480 | 'clean:server', 481 | 'bower-install', 482 | 'concurrent:server', 483 | 'autoprefixer', 484 | 'express:dev', 485 | 'open', 486 | 'watch' 487 | ]); 488 | }); 489 | 490 | grunt.registerTask('server', function () { 491 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 492 | grunt.task.run(['serve']); 493 | }); 494 | 495 | grunt.registerTask('test', function(target) { 496 | if (target === 'server') { 497 | return grunt.task.run([ 498 | 'env:test', 499 | 'mochaTest' 500 | ]); 501 | } 502 | 503 | else if (target === 'client') { 504 | return grunt.task.run([ 505 | 'clean:server', 506 | 'concurrent:test', 507 | 'autoprefixer', 508 | 'karma' 509 | ]); 510 | } 511 | 512 | else grunt.task.run([ 513 | 'test:server', 514 | 'test:client' 515 | ]); 516 | }); 517 | 518 | grunt.registerTask('build', [ 519 | 'clean:dist', 520 | 'bower-install', 521 | 'useminPrepare', 522 | 'concurrent:dist', 523 | 'autoprefixer', 524 | 'concat', 525 | 'ngmin', 526 | 'copy:dist', 527 | 'cdnify', 528 | 'cssmin', 529 | 'uglify', 530 | 'rev', 531 | 'usemin' 532 | ]); 533 | 534 | grunt.registerTask('heroku', function () { 535 | grunt.log.warn('The `heroku` task has been deprecated. Use `grunt build` to build for deployment.'); 536 | grunt.task.run(['build']); 537 | }); 538 | 539 | grunt.registerTask('default', [ 540 | 'newer:jshint', 541 | 'test', 542 | 'build' 543 | ]); 544 | }; 545 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | --------------------------------------------------------------------------------