├── .buildignore ├── .gitattributes ├── client ├── robots.txt ├── favicon.ico ├── app │ ├── assets │ │ └── images │ │ │ ├── icon_144.png │ │ │ ├── icon_48.png │ │ │ └── logo.svg │ ├── components │ │ ├── topNav │ │ │ ├── topNav.html │ │ │ └── topNav.directive.js │ │ ├── footer │ │ │ ├── footer.html │ │ │ └── footer.directive.js │ │ └── event-map │ │ │ ├── event-map.html │ │ │ └── event-map.directive.js │ ├── moments │ │ ├── moment-timezone.js │ │ └── moments.directive.js │ ├── appInvite │ │ ├── appInvite.controller.js │ │ └── appInvite.html │ ├── main │ │ ├── main.css │ │ ├── main.controller.spec.js │ │ ├── eventList.html │ │ ├── main.html │ │ └── main.controller.js │ ├── shorturl │ │ ├── shorturl.css │ │ ├── shorturlEvent.controller.js │ │ └── shorturlEvent.html │ ├── firefly.module.js │ ├── util │ │ └── theme.service.js │ ├── firefly.config.js │ └── app.css ├── .well-known │ └── assetlinks.json ├── manifest.json ├── index.html └── .htaccess ├── .bowerrc ├── .firebaserc ├── .gitignore ├── bs-config.json ├── .travis.yml ├── firebase.json ├── e2e └── main │ ├── main.po.js │ └── main.spec.js ├── .editorconfig ├── local.env.sample.js ├── .jshintrc ├── bower.json ├── .yo-rc.json ├── protractor.conf.js ├── .jscsrc ├── package.json ├── README.md ├── karma.conf.js ├── LICENSE └── Gruntfile.js /.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /client/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "client/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "firefly-1c312" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdg-x/firefly/HEAD/client/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .idea 4 | bower_components 5 | dist 6 | local.env.js 7 | *.log 8 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 5000, 3 | "files": ["./client/**/*"], 4 | "server": { "baseDir": "./client" } 5 | } 6 | -------------------------------------------------------------------------------- /client/app/assets/images/icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdg-x/firefly/HEAD/client/app/assets/images/icon_144.png -------------------------------------------------------------------------------- /client/app/assets/images/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdg-x/firefly/HEAD/client/app/assets/images/icon_48.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | before_script: 5 | - npm install -g bower grunt-cli 6 | - bower install 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/public", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/app/components/topNav/topNav.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | {{ tag.title }} 5 |
6 |
7 | -------------------------------------------------------------------------------- /client/app/components/topNav/topNav.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp').directive('fireflyTopNav', fireflyTopNav); 4 | 5 | function fireflyTopNav() { 6 | return { 7 | templateUrl: 'app/components/topNav/topNav.html', 8 | restrict: 'EA', 9 | scope: { 10 | tag: '=' 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /client/app/moments/moment-timezone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('moment-timezone', []) 4 | .service('MomentTimezone', ['$http', function ($http) { 5 | var promise = $http.get('/bower_components/moment-timezone/moment-timezone.json').success(function(resp) { 6 | moment.tz.add(resp); 7 | }); 8 | return { 9 | promise: promise 10 | }; 11 | }]); 12 | -------------------------------------------------------------------------------- /client/app/appInvite/appInvite.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp') 4 | .controller('AppInviteCtrl', function ($rootScope, $http, config) { 5 | var vm = this; 6 | vm.domain = config.DOMAIN; 7 | 8 | $http.jsonp(config.HUB_IP + 'api/v1/tags/android?callback=JSON_CALLBACK').success(function(data) { 9 | $rootScope.tag = data; 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/components/footer/footer.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /e2e/main/main.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file uses the Page Object pattern to define the main page for tests 3 | * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var MainPage = function() { 9 | this.heroEl = element(by.css('.hero-unit')); 10 | this.h1El = this.heroEl.element(by.css('h1')); 11 | this.imgEl = this.heroEl.element(by.css('img')); 12 | }; 13 | 14 | module.exports = new MainPage(); 15 | 16 | -------------------------------------------------------------------------------- /e2e/main/main.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Main View', function() { 4 | var page; 5 | 6 | beforeEach(function() { 7 | browser.get('/'); 8 | page = require('./main.po'); 9 | }); 10 | 11 | it('should include jumbotron with correct data', function() { 12 | expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); 13 | expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); 14 | expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /local.env.sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use local.env.js for environment variables that grunt will set when the server is built. 4 | // Use for your api keys, secrets, etc. The local.env.js file with your settings should not be tracked by git. 5 | // 6 | // You will need to set these to the production values and build with grunt before deploying to production. 7 | 8 | module.exports = { 9 | DOMAIN: 'localhost', 10 | // Hub Server 11 | HUB_IP: 'https://hub.gdgx.io/', 12 | GOOGLE_API_KEY: 'YOUR-KEY-HERE' 13 | }; 14 | -------------------------------------------------------------------------------- /client/app/components/footer/footer.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp') 4 | .directive('fireflyFooter', function (themeService) { 5 | return { 6 | templateUrl: 'app/components/footer/footer.html', 7 | restrict: 'EA', 8 | scope: { 9 | tag: '=' 10 | }, 11 | controller: function () { 12 | 13 | }, 14 | link: function (scope) { 15 | scope.url = document.URL; 16 | scope.convertHex = themeService.convertHex; 17 | } 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/main/main.css: -------------------------------------------------------------------------------- 1 | .thing-form { 2 | margin: 20px 0; 3 | } 4 | #banner { 5 | border-bottom: none; 6 | margin-top: -20px; 7 | } 8 | #banner h1 { 9 | font-size: 60px; 10 | line-height: 1; 11 | letter-spacing: -1px; 12 | } 13 | .hero-unit { 14 | position: relative; 15 | padding: 30px 15px; 16 | color: #F5F5F5; 17 | text-align: center; 18 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); 19 | background: #4393B9; 20 | } 21 | #card-box { 22 | margin-top: 16px; 23 | } 24 | .eventListContainer { 25 | height: 400px; 26 | } 27 | -------------------------------------------------------------------------------- /client/app/components/event-map/event-map.html: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 |
7 | {{marker.title}} 8 |
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /client/.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "relation": ["delegate_permission/common.handle_all_urls"], 3 | "target": { 4 | "namespace": "android_app", 5 | "package_name": "org.gdg.frisbee.android", 6 | "sha256_cert_fingerprints": 7 | ["BE:08:3F:53:18:E4:17:FB:D3:21:25:3A:6D:F9:38:D5:F4:48:4B:6E:FF:1B:70:F1:14:0C:69:71:5E:6B:96:0F"] 8 | } 9 | }, 10 | { 11 | "relation": ["delegate_permission/common.handle_all_urls"], 12 | "target": { 13 | "namespace": "android_app", 14 | "package_name": "org.gdg.frisbee.android.debug", 15 | "sha256_cert_fingerprints": 16 | ["61:45:49:E5:3D:F0:99:BB:92:C7:E6:5A:79:14:78:0B:60:8C:9A:75:2E:00:32:44:83:37:3D:C4:90:38:D3:7A"] 17 | } 18 | }] 19 | -------------------------------------------------------------------------------- /client/app/shorturl/shorturl.css: -------------------------------------------------------------------------------- 1 | .event-info { 2 | margin: 16px 16px 0 16px; 3 | height: 160px; 4 | max-width: 600px !important; 5 | } 6 | #event-card-container md-card { 7 | height: 170px; 8 | width: 400px; 9 | } 10 | .event-info-image { 11 | margin-top: 16px; 12 | float: right; 13 | } 14 | .event-text { 15 | padding-left: 14px; 16 | } 17 | .event-text a { 18 | text-decoration: none; 19 | } 20 | .event-text a:hover { 21 | text-decoration: underline; 22 | } 23 | .event-info .chapter-name { 24 | font-weight: 500; 25 | } 26 | .event-description { 27 | text-align: center; 28 | } 29 | .event-description a { 30 | text-decoration: none; 31 | } 32 | .event-description a:hover { 33 | text-decoration: underline; 34 | } 35 | -------------------------------------------------------------------------------- /client/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Global Google Developers Group (GDG) Event Directory", 3 | "short_name": "GDG Events", 4 | "lang": "en-US", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "theme_color": "#2196F3", 8 | "background_color": "#111111", 9 | "icons": [ 10 | { 11 | "src": "images/icon_48.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "images/icon_144.png", 17 | "sizes": "144x144", 18 | "type": "image/png" 19 | } 20 | ], 21 | "related_applications": [{ 22 | "platform": "web" 23 | }, { 24 | "platform": "play", 25 | "url": "https://play.google.com/store/apps/details?id=org.gdg.frisbee.android" 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /client/app/firefly.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp', ['ngCookies', 'ngResource', 'ngSanitize', 'ngRoute', 'ngMaterial', 'ngAria', 4 | 'googlechart', 'uiGmapgoogle-maps', 'ngGeolocation', 'linkify', 'viewhead', 'ja.qr' 5 | ]).run(function($rootScope, $window, $geolocation) { 6 | $geolocation.watchPosition({ 7 | timeout: 60000, 8 | maximumAge: 250, 9 | enableHighAccuracy: true 10 | }); 11 | 12 | $rootScope.$on('$geolocation.position.changed', function(event, value) { 13 | if (!$rootScope.geo) { 14 | $rootScope.geo = { 15 | latitude: value.coords.latitude, 16 | longitude: value.coords.longitude, 17 | timestamp: value.timestamp 18 | }; 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /client/app/util/theme.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp').service('themeService', themeService); 4 | 5 | function themeService() { 6 | return { 7 | convertHex: convertHex 8 | }; 9 | 10 | /** 11 | * @param hex color 12 | * @param opacity percentage 13 | * @returns {string} formatted as rgba 14 | */ 15 | function convertHex(hex, opacity) { 16 | if (!opacity) { 17 | opacity = 100; 18 | } 19 | if (!hex) { 20 | return 'rgba(0, 0, 0, ' + opacity / 100 + ')'; 21 | } 22 | hex = hex.replace('#', ''); 23 | return 'rgba(' + parseInt(hex.substring(0, 2), 16) + ',' + parseInt(hex.substring(2, 4), 16) + 24 | ',' + parseInt(hex.substring(4, 6), 16) + ',' + opacity / 100 + ')'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.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": "nofunc", 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 | "jQuery": true, 23 | "angular": true, 24 | "console": true, 25 | "$": true, 26 | "_": true, 27 | "moment": true, 28 | "describe": true, 29 | "beforeEach": true, 30 | "module": true, 31 | "inject": true, 32 | "it": true, 33 | "expect": true, 34 | "browser": true, 35 | "element": true, 36 | "by": true, 37 | "ClipboardEvent": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly", 3 | "version": "1.4.0", 4 | "dependencies": { 5 | "json3": "3.3.2", 6 | "es5-shim": "3.0.2", 7 | "angular": "1.5.8", 8 | "angular-animate": "1.5.8", 9 | "angular-aria": "1.5.8", 10 | "angular-resource": "1.5.8", 11 | "angular-cookies": "1.5.8", 12 | "angular-sanitize": "1.5.8", 13 | "angular-route": "1.5.8", 14 | "angular-material": "1.0.0", 15 | "angular-google-chart": "0.0.11", 16 | "angular-google-maps": "2.3.4", 17 | "font-awesome": "4.3.0", 18 | "ngGeolocation": "0.0.8", 19 | "lodash": "3.9.3", 20 | "moment": "2.10.6", 21 | "moment-timezone": "0.3.1", 22 | "angular-linkify": "1.2.0", 23 | "angularjs-viewhead": "0.0.1", 24 | "devintent-qr": "0.2.3" 25 | }, 26 | "devDependencies": { 27 | "angular-mocks": "1.5.8" 28 | }, 29 | "resolutions": { 30 | "angular": "1.5.8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/app/appInvite/appInvite.html: -------------------------------------------------------------------------------- 1 |
2 | GDG App Invite 3 |
4 |

5 | Hello, you have been invited to use the GDG app! 6 |

7 |

8 | Find out more about it now or browse the GDG events around the world at {{ vm.domain }}. 9 |

10 |
11 |
12 | Get it on Google Play 13 |
14 |
15 | Google Play and the Google Play logo are trademarks of Google Inc. 16 |
17 |
18 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angular-fullstack": { 3 | "insertRoutes": true, 4 | "registerRoutesFile": "server/routes.js", 5 | "routesNeedle": "// Insert routes below", 6 | "routesBase": "/api/", 7 | "pluralizeRoutes": true, 8 | "insertSockets": true, 9 | "registerSocketsFile": "server/config/socketio.js", 10 | "socketsNeedle": "// Insert sockets below", 11 | "filters": { 12 | "js": true, 13 | "html": true, 14 | "css": true, 15 | "ngroute": true, 16 | "bootstrap": true, 17 | "uibootstrap": true, 18 | "mongoose": true 19 | } 20 | }, 21 | "generator-ng-component": { 22 | "routeDirectory": "client/app/", 23 | "directiveDirectory": "client/app/", 24 | "filterDirectory": "client/app/", 25 | "serviceDirectory": "client/app/", 26 | "basePath": "client", 27 | "filters": [ 28 | "ngroute" 29 | ], 30 | "extensions": [ 31 | "js", 32 | "html", 33 | "css" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/app/main/main.controller.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | var MainCtrl, scope, $httpBackend, config; 5 | 6 | // load the controller's module 7 | //noinspection JSValidateTypes 8 | beforeEach(module('fireflyApp')); 9 | // Initialize the controller and a mock scope 10 | beforeEach(inject(function (_$httpBackend_, $controller, $rootScope, _config_) { 11 | $httpBackend = _$httpBackend_; 12 | scope = $rootScope.$new(); 13 | MainCtrl = $controller('MainCtrl', { 14 | $scope: scope 15 | }); 16 | config = _config_; 17 | })); 18 | 19 | it('should attach a list of things to the scope', function () { 20 | $httpBackend.expectJSONP( 21 | config.HUB_IP + 'api/v1/tags/' + scope.prefix + '?callback=JSON_CALLBACK') 22 | .respond([]); 23 | $httpBackend.expectJSONP( 24 | config.HUB_IP + 'api/v1/events/stats?callback=JSON_CALLBACK') 25 | .respond({ upcoming_top_tags: ['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express'] }); // jshint ignore:line 26 | $httpBackend.expectJSONP( 27 | config.HUB_IP + 'api/v1/events/tag/' + scope.prefix + '/upcoming?perpage=999&callback=JSON_CALLBACK') 28 | .respond([]); 29 | 30 | $httpBackend.flush(); 31 | expect(MainCtrl.tags.length).toBe(4); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /client/app/firefly.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp') 4 | .constant('GOOGLE_API_KEY', 'Set in /local.env.js') 5 | .config(function ($routeProvider, $locationProvider, $mdThemingProvider) { 6 | $locationProvider.html5Mode(true); 7 | 8 | $mdThemingProvider.theme('default') 9 | .primaryPalette('grey') 10 | .accentPalette('indigo'); 11 | 12 | $routeProvider 13 | .when('/', { 14 | templateUrl: 'app/main/main.html', 15 | controller: 'MainCtrl', 16 | controllerAs: 'vm' 17 | }) 18 | .when('/invite', { 19 | templateUrl: 'app/appInvite/appInvite.html', 20 | controller: 'AppInviteCtrl', 21 | controllerAs: 'vm' 22 | }) 23 | .when('/:tag/events', { 24 | templateUrl: 'app/main/eventList.html', 25 | controller: 'MainCtrl', 26 | controllerAs: 'vm' 27 | }) 28 | .when('/event/:hash', { 29 | templateUrl: 'app/shorturl/shorturlEvent.html', 30 | controller: 'ShorturlEventCtrl', 31 | controllerAs: 'vm' 32 | }) 33 | .when('/:tag', { 34 | redirectTo: '/:tag/events' 35 | }) 36 | .otherwise({ redirectTo: '/' }); 37 | }) 38 | .config(function (uiGmapGoogleMapApiProvider, GOOGLE_API_KEY) { 39 | uiGmapGoogleMapApiProvider.configure({ 40 | key: GOOGLE_API_KEY, 41 | v: '3.25', 42 | libraries: 'weather,geometry,visualization' 43 | }); 44 | }) 45 | .service('config', config); 46 | 47 | function config(GOOGLE_API_KEY) { 48 | return { 49 | GOOGLE_API_KEY: GOOGLE_API_KEY, 50 | HUB_IP: 'https://hub.gdgx.io/', 51 | DOMAIN: 'gdg.events', 52 | DEFAULT_PREFIX: 'devfest' 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration 2 | // https://github.com/angular/protractor/blob/master/referenceConf.js 3 | 4 | 'use strict'; 5 | 6 | exports.config = { 7 | // The timeout for each script run on the browser. This should be longer 8 | // than the maximum time your application needs to stabilize between tasks. 9 | allScriptsTimeout: 110000, 10 | 11 | // A base URL for your application under test. Calls to protractor.get() 12 | // with relative paths will be prepended with this. 13 | baseUrl: 'http://localhost:' + (process.env.PORT || '9000'), 14 | 15 | // If true, only chromedriver will be started, not a standalone selenium. 16 | // Tests for browsers other than chrome will not run. 17 | chromeOnly: true, 18 | 19 | // list of files / patterns to load in the browser 20 | specs: [ 21 | 'e2e/**/*.spec.js' 22 | ], 23 | 24 | // Patterns to exclude. 25 | exclude: [], 26 | 27 | // ----- Capabilities to be passed to the webdriver instance ---- 28 | // 29 | // For a full list of available capabilities, see 30 | // https://code.google.com/p/selenium/wiki/DesiredCapabilities 31 | // and 32 | // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js 33 | capabilities: { 34 | 'browserName': 'chrome' 35 | }, 36 | 37 | // ----- The test framework ----- 38 | // 39 | // Jasmine and Cucumber are fully supported as a test and assertion framework. 40 | // Mocha has limited beta support. You will need to include your own 41 | // assertion framework if working with mocha. 42 | framework: 'jasmine', 43 | 44 | // ----- Options to be passed to minijasminenode ----- 45 | // 46 | // See the full list at https://github.com/juliemr/minijasminenode 47 | jasmineNodeOpts: { 48 | defaultTimeoutInterval: 30000 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /client/app/shorturl/shorturlEvent.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp') 4 | .controller('ShorturlEventCtrl', function ($http, $routeParams, $location, $log, config, themeService) { 5 | var vm = this; 6 | vm.convertHex = themeService.convertHex; 7 | 8 | $http.jsonp(config.HUB_IP + 'api/v1/events/' + $routeParams.hash + '?callback=JSON_CALLBACK') 9 | .success(processEventData) 10 | .catch(function() { 11 | $log.warn('Event "' + $routeParams.hash + '" not found.'); 12 | $location.path('/'); 13 | }); 14 | 15 | function processEventData(eventData) { 16 | if (eventData.geo) { 17 | eventData.geo.center = { 18 | latitude: eventData.geo.lat, 19 | longitude: eventData.geo.lng 20 | }; 21 | // Specified separately from the center, as the center can change if the map is dragged/panned. 22 | eventData.geo.location = { 23 | latitude: eventData.geo.lat, 24 | longitude: eventData.geo.lng 25 | }; 26 | eventData.geo.zoom = 11; 27 | eventData.geo.control = {}; 28 | eventData.geo.options = { 29 | scrollwheel: false, 30 | draggable: false 31 | }; 32 | } 33 | vm.event = eventData; 34 | vm.hubIp = config.HUB_IP; 35 | 36 | var chapterUrl = 'https://www.googleapis.com/plus/v1/people/' + vm.event.chapter + 37 | '?fields=image&key=' + config.GOOGLE_API_KEY; 38 | $http.get(chapterUrl) 39 | .success(function (data) { 40 | vm.image = data.image.url.replace('sz=50', 'sz=70'); 41 | } 42 | ); 43 | 44 | $http.jsonp(config.HUB_IP + 'api/v1/chapters/' + vm.event.chapter + '?callback=JSON_CALLBACK') 45 | .success(function (chapterData) { 46 | vm.chapter = chapterData; 47 | } 48 | ); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": ["node_modules/**", "bower_components/**"], 3 | 4 | "requireCurlyBraces": [ 5 | "if", 6 | "else", 7 | "for", 8 | "while", 9 | "do", 10 | "try", 11 | "catch" 12 | ], 13 | "requireOperatorBeforeLineBreak": true, 14 | "requireCamelCaseOrUpperCaseIdentifiers": null, 15 | "maximumLineLength": { 16 | "value": 120, 17 | "allowComments": true, 18 | "allowRegex": true 19 | }, 20 | "validateIndentation": 2, 21 | "validateQuoteMarks": "'", 22 | 23 | "disallowMultipleLineStrings": true, 24 | "disallowMixedSpacesAndTabs": true, 25 | "disallowTrailingWhitespace": true, 26 | "disallowSpaceAfterPrefixUnaryOperators": true, 27 | "disallowMultipleVarDecl": null, 28 | 29 | "requireSpaceAfterKeywords": [ 30 | "if", 31 | "else", 32 | "for", 33 | "while", 34 | "do", 35 | "switch", 36 | "return", 37 | "try", 38 | "catch" 39 | ], 40 | "requireSpaceBeforeBinaryOperators": [ 41 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 42 | "&=", "|=", "^=", "+=", 43 | 44 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 45 | "|", "^", "&&", "||", "===", "==", ">=", 46 | "<=", "<", ">", "!=", "!==" 47 | ], 48 | "requireSpaceAfterBinaryOperators": true, 49 | "requireSpacesInConditionalExpression": true, 50 | "requireSpaceBeforeBlockStatements": true, 51 | "requireLineFeedAtFileEnd": true, 52 | "disallowSpacesInsideObjectBrackets": null, 53 | "disallowSpacesInsideArrayBrackets": null, 54 | "disallowSpacesInsideParentheses": true, 55 | 56 | "disallowMultipleLineBreaks": true, 57 | 58 | "disallowCommaBeforeLineBreak": null, 59 | "disallowDanglingUnderscores": null, 60 | "disallowEmptyBlocks": null, 61 | "disallowTrailingComma": null, 62 | "requireCommaBeforeLineBreak": null, 63 | "requireDotNotation": null, 64 | "requireMultipleVarDecl": null, 65 | "requireParenthesesAroundIIFE": true 66 | } 67 | -------------------------------------------------------------------------------- /client/app/moments/moments.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp').directive('timeTimezone', ['$http', function() { 4 | return { 5 | restrict: 'E', 6 | template: '{{formatDate}}', 7 | scope: { 8 | date: '=', 9 | timezone: '=' 10 | }, 11 | link: function(scope) { 12 | 13 | var update = function upd() { 14 | if (scope.timezone && scope.date) { 15 | scope.formatDate = moment(scope.date).tz(scope.timezone).format('llll'); 16 | } 17 | }; 18 | 19 | scope.$watch('date', function() { 20 | update(); 21 | }); 22 | 23 | scope.$watch('timezone', function() { 24 | update(); 25 | }); 26 | } 27 | }; 28 | }]).directive('timeAgo', ['$timeout', function($timeout) { 29 | return { 30 | restrict: 'E', 31 | template: '{{formatDate}}', 32 | scope: { 33 | date: '=' 34 | }, 35 | link: function(scope) { 36 | var promise = null; 37 | 38 | var update = function() { 39 | if (scope.date) { 40 | scope.formatDate = moment(scope.date).fromNow(); 41 | promise = $timeout(update, 60000, false); 42 | } 43 | }; 44 | 45 | scope.$watch('date', function() { 46 | if (promise) { 47 | $timeout.cancel(promise); 48 | } 49 | update(); 50 | }); 51 | } 52 | }; 53 | }]).directive('localTime', [function() { 54 | return { 55 | restrict: 'E', 56 | template: '{{formatDate}}', 57 | scope: { 58 | date: '=' 59 | }, 60 | link: function(scope) { 61 | var update = function upd() { 62 | scope.formatDate = moment(scope.date).format('llll'); 63 | }; 64 | 65 | scope.$watch('date', function() { 66 | update(); 67 | }); 68 | } 69 | }; 70 | }]).directive('time', [function() { 71 | return { 72 | restrict: 'E', 73 | template: '{{formatDate}}', 74 | scope: { 75 | date: '=' 76 | }, 77 | link: function(scope) { 78 | var update = function upd() { 79 | scope.formatDate = moment(scope.date).utc().format('llll'); 80 | }; 81 | 82 | scope.$watch('date', function() { 83 | update(); 84 | }); 85 | } 86 | }; 87 | }]); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly", 3 | "version": "1.4.0", 4 | "license": "Apache-2.0", 5 | "main": "client/index.html", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "bower": "1.7.9", 9 | "connect-livereload": "0.4.0", 10 | "grunt": "1.0.1", 11 | "grunt-angular-templates": "1.1.0", 12 | "grunt-asset-injector": "0.1.0", 13 | "grunt-autoprefixer": "3.0.4", 14 | "grunt-build-control": "0.7.1", 15 | "grunt-cli": "1.2.0", 16 | "grunt-concurrent": "2.3.1", 17 | "grunt-contrib-clean": "1.0.0", 18 | "grunt-contrib-concat": "1.0.1", 19 | "grunt-contrib-copy": "1.0.0", 20 | "grunt-contrib-cssmin": "1.0.1", 21 | "grunt-contrib-htmlmin": "2.0.0", 22 | "grunt-contrib-imagemin": "1.0.1", 23 | "grunt-contrib-jshint": "1.0.0", 24 | "grunt-contrib-uglify": "2.0.0", 25 | "grunt-contrib-watch": "1.0.0", 26 | "grunt-dom-munger": "3.4.0", 27 | "grunt-env": "0.4.4", 28 | "grunt-google-cdn": "0.4.3", 29 | "grunt-jscs": "3.0.1", 30 | "grunt-karma": "2.0.0", 31 | "grunt-newer": "1.2.0", 32 | "grunt-ng-annotate": "2.0.2", 33 | "grunt-protractor-runner": "3.2.0", 34 | "grunt-replace": "1.0.1", 35 | "grunt-rev": "0.1.0", 36 | "grunt-svgmin": "3.3.0", 37 | "grunt-usemin": "3.1.1", 38 | "grunt-wiredep": "3.0.1", 39 | "jit-grunt": "0.10.0", 40 | "jshint-stylish": "2.2.1", 41 | "karma": "1.2.0", 42 | "karma-chrome-launcher": "1.0.1", 43 | "karma-firefox-launcher": "1.0.0", 44 | "karma-html2js-preprocessor": "1.0.0", 45 | "karma-jasmine": "1.0.2", 46 | "karma-ng-html2js-preprocessor": "1.0.0", 47 | "karma-ng-scenario": "1.0.0", 48 | "karma-phantomjs-launcher": "1.0.1", 49 | "karma-requirejs": "1.0.0", 50 | "karma-script-launcher": "1.0.0", 51 | "lite-server": "2.2.2", 52 | "phantomjs-prebuilt": "2.1.12", 53 | "requirejs": "2.2.0", 54 | "should": "11.1.0", 55 | "supertest": "2.0.0", 56 | "time-grunt": "1.4.0" 57 | }, 58 | "engines": { 59 | "node": ">=0.10.0" 60 | }, 61 | "scripts": { 62 | "auth": "firebase login", 63 | "deploy": "firebase deploy", 64 | "postinstall": "bower install", 65 | "start": "lite-server", 66 | "test": "grunt test", 67 | "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" 68 | }, 69 | "private": true 70 | } 71 | -------------------------------------------------------------------------------- /client/app/app.css: -------------------------------------------------------------------------------- 1 | /** 2 | *Font Awesome Fonts 3 | */ 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0'); 7 | src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), 8 | url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), 9 | url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), 10 | url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | /** 15 | * App-wide Styles 16 | */ 17 | .angular-google-map-container { 18 | height: 400px; 19 | } 20 | @media screen and (min-device-height: 600px) { 21 | .angular-google-map-container { 22 | height: 400px; 23 | } 24 | } 25 | @media screen and (min-device-height: 800px) { 26 | .angular-google-map-container { 27 | height: 500px; 28 | } 29 | } 30 | @media screen and (min-device-height: 1000px) { 31 | .angular-google-map-container { 32 | height: 600px; 33 | } 34 | } 35 | @media screen and (max-device-height: 780px) { 36 | .angular-google-map-container { 37 | height: 500px; 38 | } 39 | } 40 | @media screen and (max-device-height: 680px) { 41 | .angular-google-map-container { 42 | height: 480px; 43 | } 44 | } 45 | @media screen and (max-device-height: 580px) { 46 | .angular-google-map-container { 47 | height: 380px; 48 | } 49 | } 50 | @media screen and (max-device-height: 480px) { 51 | .angular-google-map-container { 52 | height: 300px; 53 | } 54 | } 55 | @media screen and (max-device-height: 420px) { 56 | .angular-google-map-container { 57 | height: 240px; 58 | } 59 | } 60 | @media screen and (max-device-height: 375px) { 61 | .angular-google-map-container { 62 | height: 230px; 63 | } 64 | } 65 | @media screen and (max-device-height: 320px) { 66 | .angular-google-map-container { 67 | height: 180px; 68 | } 69 | } 70 | .footer {; 71 | font-size: 10pt; 72 | padding: 10px; 73 | margin-top: 20px; 74 | border-top: 1px solid #E5E5E5; 75 | } 76 | .browsehappy { 77 | margin: 0.2em 0; 78 | background: #ccc; 79 | color: #000; 80 | padding: 0.2em 0; 81 | } 82 | .footer a { 83 | text-decoration: none; 84 | color: rgba(0, 0, 0, 0.87); 85 | font-weight: 500; 86 | } 87 | .footer a:hover { 88 | text-decoration: underline; 89 | } 90 | .md-title, md-card-content > p { 91 | margin-left: 16px; 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Firefly 2 | ======= 3 | [![Build Status](https://travis-ci.org/gdg-x/firefly.png)](https://travis-ci.org/gdg-x/firefly) 4 | 5 | This project is to help organizers to better promote their events. 6 | 7 | It relies on the data stored in the GDG[x] Hub. 8 | 9 | **List of events** 10 | Organizers can use URLs like [gdg.events/devfest/events](https://gdg.events/devfest/events) to promote DevFest events. 11 | 12 | There is no need for organizers to clone this repo or host this project on their servers. 13 | 14 | Contribute 15 | ================= 16 | 17 | Get involved: [![Join the chat at https://gitter.im/gdg-x/firefly](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gdg-x/firefly?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 18 | 19 | Discuss [issues](https://github.com/gdg-x/firefly/issues) and [pull requests](https://github.com/gdg-x/firefly/pulls). 20 | 21 | 22 | Local Development 23 | ================= 24 | 1. Clone the git repository. 25 | 1. Create and customize `local.env.js` based on `local.env.sample.js`. 26 | 1. Define the **GOOGLE_API_KEY**: The API key for your project, available from the [Cloud Console](https://cloud.google.com/console) 27 | 1. Create a new project then go to APIs & Auth->APIs, activate Google+ and Google Maps JavaScript v3 APIs. 28 | 1. Go to APIs & Auth->Credentials. Add Credentials->API key->Browser key->Create (keep `Any referrer allowed` set). 29 | 1. `cd firefly` 30 | 1. `npm install` 31 | 1. `npm start` 32 | 33 | Prod Deployment 34 | =============== 35 | 1. `cd firefly` 36 | 1. `npm install -g firebase-tools` 37 | 1. `firebase login` 38 | 1. `grunt` 39 | 1. `firebase deploy` 40 | 1. Check the server at https://gdg.events 41 | 42 | ### Contributors 43 | See [list of contributors](https://github.com/gdg-x/firefly/graphs/contributors) 44 | 45 | Maintainer: None. 46 | 47 | ###### GDG Apps, GDG[x] is not endorsed and/or supported by Google, the corporation. 48 | 49 | License 50 | -------- 51 | 52 | © 2013-2017 GDG[x] 53 | 54 | Licensed under the Apache License, Version 2.0 (the "License"); 55 | you may not use this file except in compliance with the License. 56 | You may obtain a copy of the License at 57 | 58 | http://www.apache.org/licenses/LICENSE-2.0 59 | 60 | Unless required by applicable law or agreed to in writing, software 61 | distributed under the License is distributed on an "AS IS" BASIS, 62 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 63 | See the License for the specific language governing permissions and 64 | limitations under the License. 65 | -------------------------------------------------------------------------------- /client/app/main/eventList.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | {{ tag.title }} 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |

About {{ tag.title }}

14 |

{{ tag.description }}

15 |
16 |
17 | 18 | 19 | 20 | 21 |

22 | {{ tag.title }} Events near you 23 |

24 | 25 | 26 | 28 |
29 |

{{ event.title }}

30 |

{{ event.start | date : 'medium' : event.timezone }}

31 |
32 |
33 |
34 |
35 | 36 | Waiting for location... 37 | 38 |
39 |
40 | 41 | 42 | 43 |

44 | Upcoming {{ tag.title }} Events 45 |

46 | 47 | 48 | 50 |
51 |

{{ event.title }}

52 |

{{ event.start | date : 'medium' : event.timezone }}

53 |
54 |
55 |
56 |
57 | 58 | No upcoming events in this series. 59 | 60 |
61 |
62 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 'use strict'; 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | // testing framework to use (jasmine/mocha) 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'client/bower_components/angular/angular.js', 16 | 'client/bower_components/angular-mocks/angular-mocks.js', 17 | 'client/bower_components/angular-resource/angular-resource.js', 18 | 'client/bower_components/angular-cookies/angular-cookies.js', 19 | 'client/bower_components/angular-sanitize/angular-sanitize.js', 20 | 'client/bower_components/angular-route/angular-route.js', 21 | 'client/bower_components/angular-aria/angular-aria.js', 22 | 'client/bower_components/angular-animate/angular-animate.js', 23 | 'client/bower_components/angular-material/angular-material.js', 24 | 'client/bower_components/angular-google-chart/ng-google-chart.js', 25 | 'client/bower_components/lodash/lodash.js', 26 | 'client/bower_components/angular-simple-logger/dist/angular-simple-logger.js', 27 | 'client/bower_components/angular-google-maps/dist/angular-google-maps.js', 28 | 'client/bower_components/ngGeolocation/ngGeolocation.js', 29 | 'client/bower_components/angular-linkify/angular-linkify.js', 30 | 'client/bower_components/angularjs-viewhead/angularjs-viewhead.js', 31 | 'client/bower_components/devintent-qr/src/angular-qr.js', 32 | 'client/app/firefly.module.js', 33 | 'client/app/firefly.config.js', 34 | 'client/app/**/*.js', 35 | 'client/app/**/*.html' 36 | ], 37 | 38 | preprocessors: { 39 | '**/*.html': 'html2js' 40 | }, 41 | 42 | ngHtml2JsPreprocessor: { 43 | stripPrefix: 'client/' 44 | }, 45 | 46 | // list of files / patterns to exclude 47 | exclude: [], 48 | 49 | // web server port 50 | port: 8080, 51 | 52 | // level of logging 53 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | // enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: false, 58 | 59 | // Start these browsers, currently available: 60 | // - Chrome 61 | // - ChromeCanary 62 | // - Firefox 63 | // - Opera 64 | // - Safari (only Mac) 65 | // - PhantomJS 66 | // - IE (only Windows) 67 | browsers: ['PhantomJS'], 68 | 69 | // Continuous Integration mode 70 | // if true, it capture browsers, run tests and exit 71 | singleRun: false 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /client/app/components/event-map/event-map.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp').directive('eventMap', eventMap); 4 | 5 | eventMap.$inject = ['$http', 'uiGmapGoogleMapApi', 'config']; 6 | 7 | function eventMap($http, uiGmapGoogleMapApi, config) { 8 | return { 9 | templateUrl: 'app/components/event-map/event-map.html', 10 | restrict: 'EA', 11 | scope: { 12 | position: '=', 13 | tag: '=' 14 | }, 15 | controller: function($scope) { 16 | $scope.map = { 17 | center: { 18 | latitude: 45, 19 | longitude: -73 20 | }, 21 | zoom: 5, 22 | control: {}, 23 | cluster: { 24 | maxZoom: 7 25 | }, 26 | options: { 27 | scrollwheel: false, 28 | draggable: true 29 | } 30 | }; 31 | 32 | $scope.markers = []; 33 | 34 | }, 35 | link: function(scope) { 36 | 37 | uiGmapGoogleMapApi.then(function(maps) { 38 | scope.maps = maps; 39 | maps.event.addListener(scope.map.control.getGMap(), 'tilesloaded', function() { 40 | maps.event.trigger(scope.map.control.getGMap(), 'resize'); 41 | }); 42 | }); 43 | 44 | scope.$watch('position', function(position) { 45 | if (position) { 46 | scope.map.center = position; 47 | } 48 | }); 49 | 50 | var processEvents = function(data) { 51 | var i; 52 | for (i = 0; i < data.items.length; i++) { 53 | var event = data.items[i]; 54 | 55 | if (event.geo) { 56 | var marker = { 57 | id: event._id, 58 | chapter: event.chapter, 59 | eventUrl: event.eventUrl, 60 | title: event.title, 61 | start: event.start, 62 | end: event.end, 63 | timezone: event.timezone, 64 | show: false, 65 | coordinates: { 66 | latitude: event.geo.lat, 67 | longitude: event.geo.lng 68 | } 69 | }; 70 | 71 | marker.onClick = function(marker) { 72 | marker.show = !marker.show; 73 | }.bind(marker, marker); 74 | 75 | scope.markers.push(marker); 76 | } 77 | } 78 | 79 | if (scope.maps) { 80 | scope.maps.event.trigger(scope.map.control.getGMap(), 'resize'); 81 | } 82 | }; 83 | 84 | if (scope.tag) { 85 | $http.jsonp(config.HUB_IP + 'api/v1/events/tag/' + scope.tag + 86 | '/upcoming?perpage=1000&callback=JSON_CALLBACK').success(processEvents); 87 | } else { 88 | $http.jsonp(config.HUB_IP + 'api/v1/events/upcoming?perpage=1000&callback=JSON_CALLBACK') 89 | .success(processEvents); 90 | } 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /client/app/main/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | {{ tag.title }} 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |

14 | {{ tag.title }} Events near you 15 |

16 | 17 | 19 |
20 |

{{ ::event.title }}

21 |

{{ ::event.start | date : 'medium' : event.timezone }}

22 |
23 |
24 |
25 | 26 | Waiting for location... 27 | 28 |
29 |
30 | 31 | 32 | 33 |

34 | Upcoming {{ tag.title }} Events 35 |

36 | 37 | 39 |
40 |

{{ ::event.title }}

41 |

{{ ::event.start | date : 'medium' : event.timezone }}

42 |
43 |
44 |
45 | 46 | No upcoming events in this series. 47 | 48 |
49 |
50 | 51 | 52 | 53 |

About {{ tag.title }}

54 |

{{ tag.description }}

55 |
56 |
57 | 58 | 59 | 60 |

Popular topics

61 | 62 | 64 |
65 |

{{ ::key }}

66 |

{{ ::value }} events

67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 | -------------------------------------------------------------------------------- /client/app/shorturl/shorturlEvent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 |

{{ vm.event.title }}

9 | 10 | 11 |
12 | 13 |

event Hosted by

14 |

{{ vm.chapter.name }}

15 | 16 | Visit Chapter Site 17 | 18 |
19 | 20 |
21 |

web Event site

22 | 23 | 24 | Google+ event 25 | 26 | 27 | Visit Event Site 28 | 29 |
30 |
31 | 32 |
33 | 35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 |

45 | location_on 46 | Location 47 |

48 |
{{ vm.event.location }}
49 | 50 | Get directions 51 | 52 |
53 |
54 | 55 | 56 | 57 |

58 | access_time 59 | Your Time 60 |

61 |

62 | Begins:
63 | Ends:
64 | Timezone: Local 65 |

66 |
67 |
68 | 69 | 70 | 71 |

72 | access_time 73 | Event Time 74 |

75 |

76 | Begins: 77 | 78 | 79 |
80 | Ends: 81 | 82 | 83 |
84 | Timezone: {{ vm.event.timezone }} 85 |

86 |
87 |
88 | 89 |
90 |
91 | -------------------------------------------------------------------------------- /client/app/main/main.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('fireflyApp') 4 | .controller('MainCtrl', function ($rootScope, $filter, $routeParams, $http, $location, $window, $timeout, config) { 5 | var vm = this; 6 | vm.domain = config.DOMAIN; 7 | 8 | if ($routeParams.tag) { 9 | $rootScope.prefix = $routeParams.tag; 10 | vm.all = false; 11 | } else { 12 | vm.all = false; 13 | $rootScope.prefix = config.DEFAULT_PREFIX; 14 | } 15 | 16 | $rootScope.$on('$geolocation.position.changed', function() { 17 | vm.nearEvents = $filter('orderBy')(vm.nearEvents, distanceFromHere); 18 | }); 19 | 20 | $http.jsonp(config.HUB_IP + 'api/v1/tags/' + $rootScope.prefix + '?callback=JSON_CALLBACK') 21 | .success(function (data) { 22 | $rootScope.tag = data; 23 | }); 24 | 25 | $http.jsonp(config.HUB_IP + 'api/v1/events/stats?callback=JSON_CALLBACK') 26 | .success(function(data) { 27 | vm.tags = data.upcoming_top_tags; // jshint ignore:line 28 | } 29 | ); 30 | 31 | vm.openEvent = function (eventId) { 32 | $location.path('/event/' + eventId); 33 | }; 34 | 35 | vm.openTag = function (path) { 36 | $location.path(path + '/events/'); 37 | }; 38 | 39 | function distanceFromHere(_item, _startPoint) { 40 | var start = null; 41 | 42 | if (!_item.geo) { 43 | return Number.MAX_VALUE; 44 | } 45 | 46 | var radiansTo = function (start, end) { 47 | if (!start || !end) { 48 | return 0; 49 | } 50 | 51 | var d2r = Math.PI / 180.0; 52 | var lat1rad = start.latitude * d2r; 53 | var long1rad = start.longitude * d2r; 54 | var lat2rad = end.latitude * d2r; 55 | var long2rad = end.longitude * d2r; 56 | var deltaLat = lat1rad - lat2rad; 57 | var deltaLong = long1rad - long2rad; 58 | var sinDeltaLatDiv2 = Math.sin(deltaLat / 2); 59 | var sinDeltaLongDiv2 = Math.sin(deltaLong / 2); 60 | // Square of half the straight line chord distance between both points. 61 | var a = ((sinDeltaLatDiv2 * sinDeltaLatDiv2) + 62 | (Math.cos(lat1rad) * Math.cos(lat2rad) * 63 | sinDeltaLongDiv2 * sinDeltaLongDiv2)); 64 | a = Math.min(1.0, a); 65 | return 2 * Math.asin(Math.sqrt(a)); 66 | }; 67 | 68 | if ($rootScope.geo) { 69 | start = $rootScope.geo; 70 | } 71 | start = _startPoint || start; 72 | 73 | var end = { 74 | longitude: _item.geo.lng, 75 | latitude: _item.geo.lat 76 | }; 77 | 78 | var num = radiansTo(start, end) * 3958.8; 79 | return Math.round(num * 100) / 100; 80 | } 81 | 82 | var processNextEvent = function(data) { 83 | if (data && data.items) { 84 | vm.nextEvent = data.items[0]; 85 | vm.nearEvents = $filter('orderBy')(data.items, distanceFromHere); 86 | vm.nextEvents = $filter('orderBy')(data.items, 'start'); 87 | } 88 | }; 89 | 90 | if ($rootScope.prefix) { 91 | if (vm.all) { 92 | $http.jsonp(config.HUB_IP + 'api/v1/events/tag/' + $rootScope.prefix + 93 | '?perpage=999&callback=JSON_CALLBACK').success(processNextEvent); 94 | } else { 95 | $http.jsonp(config.HUB_IP + 'api/v1/events/tag/' + $rootScope.prefix + 96 | '/upcoming?perpage=999&callback=JSON_CALLBACK').success(processNextEvent); 97 | } 98 | } else { 99 | if (vm.all) { 100 | $http.jsonp(config.HUB_IP + 'api/v1/events?perpage=100&callback=JSON_CALLBACK') 101 | .success(processNextEvent); 102 | } else { 103 | $http.jsonp(config.HUB_IP + 'api/v1/events/upcoming?perpage=100&callback=JSON_CALLBACK') 104 | .success(processNextEvent); 105 | } 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GDG events 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 47 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /client/app/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 50 | 56 | 59 | 62 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var localConfig; 3 | try { 4 | localConfig = require('./local.env'); 5 | } catch (e) { 6 | localConfig = {}; 7 | } 8 | const DOMAIN = localConfig.DOMAIN || process.env.DOMAIN || 'localhost'; 9 | const HUB_IP = localConfig.HUB_IP || 'https://hub.gdgx.io/'; 10 | 11 | // Load grunt tasks automatically, when needed 12 | require('jit-grunt')(grunt, { 13 | useminPrepare: 'grunt-usemin', 14 | ngtemplates: 'grunt-angular-templates', 15 | cdnify: 'grunt-google-cdn', 16 | protractor: 'grunt-protractor-runner', 17 | injector: 'grunt-asset-injector', 18 | buildcontrol: 'grunt-build-control', 19 | replace: 'grunt-replace' 20 | }); 21 | 22 | // Time how long tasks take. Can help when optimizing build times 23 | require('time-grunt')(grunt); 24 | 25 | // Define the configuration for all the tasks 26 | grunt.initConfig({ 27 | 28 | // Project settings 29 | pkg: grunt.file.readJSON('package.json'), 30 | yeoman: { 31 | // configurable paths 32 | client: require('./bower.json').appPath || 'client', 33 | dist: 'dist' 34 | }, 35 | watch: { 36 | injectJS: { 37 | files: [ 38 | '<%= yeoman.client %>/{app,components}/**/*.js', 39 | '!<%= yeoman.client %>/{app,components}/**/*.spec.js', 40 | '!<%= yeoman.client %>/{app,components}/**/*.mock.js', 41 | '!<%= yeoman.client %>/app/firefly.module.js', 42 | '!<%= yeoman.client %>/app/firefly.config.js'], 43 | tasks: ['injector:scripts'] 44 | }, 45 | injectCss: { 46 | files: [ 47 | '<%= yeoman.client %>/{app,components}/**/*.css' 48 | ], 49 | tasks: ['injector:css'] 50 | }, 51 | jsTest: { 52 | files: [ 53 | '<%= yeoman.client %>/{app,components}/**/*.spec.js', 54 | '<%= yeoman.client %>/{app,components}/**/*.mock.js' 55 | ], 56 | tasks: ['newer:jshint:all', 'karma'] 57 | }, 58 | gruntfile: { 59 | files: ['Gruntfile.js'] 60 | }, 61 | livereload: { 62 | files: [ 63 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css', 64 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html', 65 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', 66 | '!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js', 67 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js', 68 | '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' 69 | ], 70 | options: { 71 | livereload: true 72 | } 73 | } 74 | }, 75 | 76 | // Static Code Analysis to check that there are no obvious mistakes 77 | jshint: { 78 | options: { 79 | jshintrc: '.jshintrc', 80 | reporter: require('jshint-stylish') 81 | }, 82 | all: [ 83 | '<%= yeoman.client %>/{app,components}/**/*.js', 84 | '!<%= yeoman.client %>/{app,components}/**/*.spec.js', 85 | '!<%= yeoman.client %>/{app,components}/**/*.mock.js' 86 | ], 87 | test: { 88 | src: [ 89 | '<%= yeoman.client %>/{app,components}/**/*.spec.js', 90 | '<%= yeoman.client %>/{app,components}/**/*.mock.js' 91 | ] 92 | } 93 | }, 94 | 95 | // Make sure code styles are up to par 96 | jscs: { 97 | src: [ 98 | '**/*.js', 99 | '!node_modules/**/*.js', 100 | '!client/bower_components/**/*.js', 101 | '!.tmp/**/*.js', 102 | '!dist/**/*.js' 103 | ], 104 | options: { 105 | config: '.jscsrc' 106 | } 107 | }, 108 | 109 | // Empties folders to start fresh 110 | clean: { 111 | dist: { 112 | files: [{ 113 | dot: true, 114 | src: [ 115 | '.tmp', 116 | '<%= yeoman.dist %>/*', 117 | '!<%= yeoman.dist %>/.git*', 118 | '!<%= yeoman.dist %>/Procfile' 119 | ] 120 | }] 121 | } 122 | }, 123 | 124 | // Add vendor prefixed styles 125 | autoprefixer: { 126 | options: { 127 | browsers: ['last 1 version'] 128 | }, 129 | dist: { 130 | files: [{ 131 | expand: true, 132 | cwd: '.tmp/', 133 | src: '{,*/}*.css', 134 | dest: '.tmp/' 135 | }] 136 | } 137 | }, 138 | 139 | // Debugging with node inspector 140 | 'node-inspector': { 141 | custom: { 142 | options: { 143 | 'web-host': 'localhost' 144 | } 145 | } 146 | }, 147 | 148 | // Automatically inject Bower components into the app 149 | wiredep: { 150 | target: { 151 | src: '<%= yeoman.client %>/index.html', 152 | ignorePath: '<%= yeoman.client %>/', 153 | exclude: [ 154 | /bootstrap-sass-official/, 155 | /bootstrap.js/, 156 | /json3/, 157 | /es5-shim/, 158 | /markerclustererplus/, 159 | /google-maps-utility-library-v3/, 160 | /js-rich-marker/ 161 | ] 162 | } 163 | }, 164 | 165 | // Replaces specific config strings at build time 166 | replace: { 167 | dist: { 168 | options: { 169 | patterns: [ 170 | { 171 | match: /(DOMAIN: ')(.*)(')/g, 172 | replacement: '$1' + DOMAIN + '$3' 173 | }, 174 | { 175 | match: /(HUB_IP: ')(.*)(')/g, 176 | replacement: '$1' + HUB_IP + '$3' 177 | }, 178 | { 179 | match: /('GOOGLE_API_KEY', ')(.*)(')/g, 180 | replacement: '$1' + localConfig.GOOGLE_API_KEY + '$3' 181 | } 182 | ] 183 | }, 184 | files: [ 185 | { 186 | expand: true, 187 | flatten: true, 188 | src: ['<%= yeoman.client %>/app/firefly.config.js'], 189 | dest: '<%= yeoman.client %>/app/' 190 | } 191 | ] 192 | } 193 | }, 194 | 195 | // Renames files for browser caching purposes 196 | rev: { 197 | dist: { 198 | files: { 199 | src: [ 200 | '<%= yeoman.dist %>/public/{,*/}*.js', 201 | '<%= yeoman.dist %>/public/{,*/}*.css', 202 | '<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 203 | '<%= yeoman.dist %>/public/assets/fonts/*' 204 | ] 205 | } 206 | } 207 | }, 208 | 209 | // Reads HTML for usemin blocks to enable smart builds that automatically 210 | // concat, minify and revision files. Creates configurations in memory so 211 | // additional tasks can operate on them 212 | useminPrepare: { 213 | html: ['<%= yeoman.client %>/index.html'], 214 | options: { 215 | dest: '<%= yeoman.dist %>/public' 216 | } 217 | }, 218 | 219 | // Performs rewrites based on rev and the useminPrepare configuration 220 | usemin: { 221 | html: ['<%= yeoman.dist %>/public/{,*/}*.html'], 222 | css: ['<%= yeoman.dist %>/public/{,*/}*.css'], 223 | js: ['<%= yeoman.dist %>/public/{,*/}*.js'], 224 | options: { 225 | assetsDirs: [ 226 | '<%= yeoman.dist %>/public', 227 | '<%= yeoman.dist %>/public/assets/images' 228 | ], 229 | // This is so we update image references in our ng-templates 230 | patterns: { 231 | js: [ 232 | [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images'] 233 | ] 234 | } 235 | } 236 | }, 237 | 238 | // The following *-min tasks produce minified files in the dist folder 239 | imagemin: { 240 | dist: { 241 | files: [{ 242 | expand: true, 243 | cwd: '<%= yeoman.client %>/assets/images', 244 | src: '{,*/}*.{png,jpg,jpeg,gif}', 245 | dest: '<%= yeoman.dist %>/public/assets/images' 246 | }] 247 | } 248 | }, 249 | 250 | svgmin: { 251 | dist: { 252 | files: [{ 253 | expand: true, 254 | cwd: '<%= yeoman.client %>/assets/images', 255 | src: '{,*/}*.svg', 256 | dest: '<%= yeoman.dist %>/public/assets/images' 257 | }] 258 | } 259 | }, 260 | 261 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 262 | // minsafe compatible so Uglify does not destroy the ng references 263 | ngAnnotate: { 264 | dist: { 265 | files: [{ 266 | expand: true, 267 | cwd: '.tmp/concat', 268 | src: '*/**.js', 269 | dest: '.tmp/concat' 270 | }] 271 | } 272 | }, 273 | 274 | // Package all the html partials into a single javascript payload 275 | ngtemplates: { 276 | options: { 277 | // This should be the name of your apps angular module 278 | module: 'fireflyApp', 279 | htmlmin: { 280 | collapseBooleanAttributes: true, 281 | collapseWhitespace: true, 282 | removeAttributeQuotes: true, 283 | removeEmptyAttributes: true, 284 | removeRedundantAttributes: true, 285 | removeScriptTypeAttributes: true, 286 | removeStyleLinkTypeAttributes: true 287 | }, 288 | usemin: 'app/app.js' 289 | }, 290 | main: { 291 | cwd: '<%= yeoman.client %>', 292 | src: ['{app,components}/**/*.html'], 293 | dest: '.tmp/templates.js' 294 | }, 295 | tmp: { 296 | cwd: '.tmp', 297 | src: ['{app,components}/**/*.html'], 298 | dest: '.tmp/tmp-templates.js' 299 | } 300 | }, 301 | 302 | // Replace Google CDN references 303 | cdnify: { 304 | dist: { 305 | html: ['<%= yeoman.dist %>/public/*.html'] 306 | } 307 | }, 308 | 309 | // Copies remaining files to places other tasks can use 310 | copy: { 311 | dist: { 312 | files: [{ 313 | expand: true, 314 | dot: true, 315 | cwd: '<%= yeoman.client %>', 316 | dest: '<%= yeoman.dist %>/public', 317 | src: [ 318 | '*.{ico,png,txt}', 319 | '.htaccess', 320 | 'bower_components/**/*', 321 | 'app/assets/images/**/*', 322 | 'app/assets/fonts/**/*', 323 | 'index.html', 324 | '.well-known/assetlinks.json' 325 | ] 326 | }, { 327 | expand: true, 328 | cwd: '.tmp/images', 329 | dest: '<%= yeoman.dist %>/public/assets/images', 330 | src: ['generated/*'] 331 | }] 332 | }, 333 | styles: { 334 | expand: true, 335 | cwd: '<%= yeoman.client %>', 336 | dest: '.tmp/', 337 | src: ['{app,components}/**/*.css'] 338 | } 339 | }, 340 | 341 | buildcontrol: { 342 | options: { 343 | dir: 'dist', 344 | commit: true, 345 | push: true, 346 | connectCommits: false, 347 | message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%' 348 | }, 349 | heroku: { 350 | options: { 351 | remote: 'heroku', 352 | branch: 'master' 353 | } 354 | } 355 | }, 356 | 357 | // Run some tasks in parallel to speed up the build process 358 | concurrent: { 359 | test: [], 360 | debug: { 361 | tasks: [ 362 | 'nodemon', 363 | 'node-inspector' 364 | ], 365 | options: { 366 | logConcurrentOutput: true 367 | } 368 | }, 369 | dist: [ 370 | 'imagemin', 371 | 'svgmin' 372 | ] 373 | }, 374 | 375 | // Test settings 376 | karma: { 377 | unit: { 378 | configFile: 'karma.conf.js', 379 | singleRun: true 380 | } 381 | }, 382 | 383 | protractor: { 384 | options: { 385 | configFile: 'protractor.conf.js' 386 | }, 387 | chrome: { 388 | options: { 389 | args: { 390 | browser: 'chrome' 391 | } 392 | } 393 | } 394 | }, 395 | 396 | env: { 397 | test: { 398 | NODE_ENV: 'test' 399 | }, 400 | prod: { 401 | NODE_ENV: 'production' 402 | }, 403 | all: localConfig 404 | }, 405 | 406 | injector: { 407 | options: { 408 | 409 | }, 410 | // Inject application script files into index.html (doesn't include bower) 411 | scripts: { 412 | options: { 413 | transform: function(filePath) { 414 | filePath = filePath.replace('/client/', ''); 415 | filePath = filePath.replace('/.tmp/', ''); 416 | return ''; 417 | }, 418 | starttag: '', 419 | endtag: '' 420 | }, 421 | files: { 422 | '<%= yeoman.client %>/index.html': [ 423 | ['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', 424 | '!{.tmp,<%= yeoman.client %>}/app/firefly.module.js', 425 | '!{.tmp,<%= yeoman.client %>}/app/firefly.config.js', 426 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js', 427 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js'] 428 | ] 429 | } 430 | }, 431 | 432 | // Inject component css into index.html 433 | css: { 434 | options: { 435 | transform: function(filePath) { 436 | filePath = filePath.replace('/client/', ''); 437 | filePath = filePath.replace('/.tmp/', ''); 438 | return ''; 439 | }, 440 | starttag: '', 441 | endtag: '' 442 | }, 443 | files: { 444 | '<%= yeoman.client %>/index.html': [ 445 | '<%= yeoman.client %>/{app,components}/**/*.css' 446 | ] 447 | } 448 | } 449 | } 450 | }); 451 | 452 | // Used for delaying livereload until after server has restarted 453 | grunt.registerTask('wait', function () { 454 | grunt.log.ok('Waiting for server reload...'); 455 | 456 | var done = this.async(); 457 | 458 | setTimeout(function () { 459 | grunt.log.writeln('Done waiting!'); 460 | done(); 461 | }, 1500); 462 | }); 463 | 464 | grunt.registerTask('test', function(target) { 465 | if (target === 'client') { 466 | return grunt.task.run([ 467 | 'env:all', 468 | 'concurrent:test', 469 | 'injector', 470 | 'autoprefixer', 471 | 'karma' 472 | ]); 473 | } 474 | 475 | else if (target === 'e2e') { 476 | return grunt.task.run([ 477 | 'env:all', 478 | 'env:test', 479 | 'concurrent:test', 480 | 'injector', 481 | 'wiredep', 482 | 'autoprefixer', 483 | 'protractor' 484 | ]); 485 | } 486 | 487 | else { 488 | grunt.task.run([ 489 | 'test:client' 490 | ]); 491 | } 492 | }); 493 | 494 | grunt.registerTask('build', [ 495 | 'clean:dist', 496 | 'concurrent:dist', 497 | 'injector', 498 | 'replace', 499 | 'wiredep', 500 | 'useminPrepare', 501 | 'autoprefixer', 502 | 'ngtemplates', 503 | 'concat', 504 | 'ngAnnotate', 505 | 'copy:dist', 506 | 'cdnify', 507 | 'cssmin', 508 | 'uglify', 509 | 'rev', 510 | 'usemin' 511 | ]); 512 | 513 | grunt.registerTask('validate', [ 514 | 'jshint', 515 | 'jscs', 516 | 'test' 517 | ]); 518 | 519 | grunt.registerTask('default', [ 520 | 'newer:jshint', 521 | 'jscs', 522 | 'test', 523 | 'build' 524 | ]); 525 | }; 526 | -------------------------------------------------------------------------------- /client/.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 | --------------------------------------------------------------------------------