├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── server.js ├── server ├── api.js ├── config.js.SAMPLE └── models │ ├── Event.js │ └── Rsvp.js ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── admin.guard.ts │ │ ├── auth.config.ts.SAMPLE │ │ ├── auth.guard.ts │ │ ├── auth.module.ts │ │ └── auth.service.ts │ ├── core │ │ ├── api.service.ts │ │ ├── core.module.ts │ │ ├── env.config.ts │ │ ├── expand-collapse.animation.ts │ │ ├── filter-sort.service.ts │ │ ├── forms │ │ │ ├── date-range.validator.ts │ │ │ ├── date.validator.ts │ │ │ ├── formUtils.factory.ts │ │ │ └── submitting.component.ts │ │ ├── loading.component.ts │ │ ├── models │ │ │ ├── event.model.ts │ │ │ └── rsvp.model.ts │ │ └── utils.service.ts │ ├── footer │ │ ├── footer.component.html │ │ ├── footer.component.scss │ │ └── footer.component.ts │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ └── header.component.ts │ └── pages │ │ ├── admin │ │ ├── admin.component.html │ │ ├── admin.component.scss │ │ ├── admin.component.ts │ │ ├── admin.module.ts │ │ ├── admin.routes.ts │ │ ├── create-event │ │ │ ├── create-event.component.html │ │ │ ├── create-event.component.scss │ │ │ └── create-event.component.ts │ │ ├── event-form │ │ │ ├── event-form.component.html │ │ │ ├── event-form.component.scss │ │ │ ├── event-form.component.ts │ │ │ └── event-form.service.ts │ │ └── update-event │ │ │ ├── delete-event │ │ │ ├── delete-event.component.html │ │ │ ├── delete-event.component.scss │ │ │ └── delete-event.component.ts │ │ │ ├── update-event.component.html │ │ │ ├── update-event.component.scss │ │ │ └── update-event.component.ts │ │ ├── callback │ │ ├── callback.component.html │ │ ├── callback.component.scss │ │ └── callback.component.ts │ │ ├── event │ │ ├── event-detail │ │ │ ├── event-detail.component.html │ │ │ ├── event-detail.component.scss │ │ │ └── event-detail.component.ts │ │ ├── event.component.html │ │ ├── event.component.scss │ │ ├── event.component.ts │ │ ├── event.module.ts │ │ ├── event.routes.ts │ │ └── rsvp │ │ │ ├── rsvp-form │ │ │ ├── rsvp-form.component.html │ │ │ ├── rsvp-form.component.scss │ │ │ └── rsvp-form.component.ts │ │ │ ├── rsvp.component.html │ │ │ ├── rsvp.component.scss │ │ │ └── rsvp.component.ts │ │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ └── home.component.ts │ │ └── my-rsvps │ │ ├── my-rsvps.component.html │ │ ├── my-rsvps.component.scss │ │ └── my-rsvps.component.ts ├── assets │ ├── .gitkeep │ ├── images │ │ ├── calendar.svg │ │ ├── eye.svg │ │ └── loading.svg │ └── scss │ │ ├── _base.scss │ │ ├── partials │ │ ├── _layout.vars.scss │ │ └── _responsive.partial.scss │ │ └── styles.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | /server/config.js 4 | /src/app/auth/auth.config.ts 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Auth0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MEAN-RSVP-Auth0 2 | 3 | This is the sample repository for the Real-World Angular Series of tutorials. Begin the tutorials here: [Real-World Angular Series - Part 1](https://auth0.com/blog/real-world-angular-series-part-1). 4 | 5 | ## Requirements 6 | 7 | * [Node + npm](https://nodejs.org/) 8 | * [Angular CLI](https://cli.angular.io/) v6+ 9 | * [Auth0 account](https://auth0.com) with [application](https://manage.auth0.com/#/applications) 10 | * [mLab](https://mlab.com) MongoDB database 11 | 12 | This repo is intended to be supplemental to the tutorials. Reference the tutorials for full implementation details. 13 | 14 | ## Install 15 | 16 | Clone this repository, then run: 17 | 18 | ``` 19 | $ npm install 20 | ``` 21 | 22 | ## Setup 23 | 24 | * Add your Auth0 and MongoDB credentials and remove `.SAMPLE` extension: `server/config.js.SAMPLE` 25 | * Add your Auth0 credentials and remove `.SAMPLE` extension: `src/app/auth/auth.config.ts.SAMPLE` 26 | 27 | ## Development server 28 | 29 | ```bash 30 | $ npm run dev 31 | ``` 32 | 33 | App available at `http://localhost:4200`. 34 | 35 | Server available at `http://localhost:8083/api`. 36 | 37 | ## Build (local) 38 | 39 | ``` 40 | $ ng build --prod // client 41 | $ node server // server 42 | ``` 43 | 44 | App and server both available on `http://localhost:8083`. 45 | 46 | ## Deploy 47 | 48 | To deploy the app in this repo to a production environment, follow the instructions here: [Real-World Angular Series - Part 8](https://auth0.com/blog/real-world-angular-series-part-8/#deploy). 49 | 50 | ## What is Auth0? 51 | 52 | Auth0 helps you to: 53 | 54 | * Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. 55 | * Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. 56 | * Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. 57 | * Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. 58 | * Analytics of how, when and where users are logging in. 59 | * Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). 60 | 61 | ## Create a Free Auth0 Account 62 | 63 | 1. Go to [Auth0](https://auth0.com) and click Sign Up. 64 | 2. Use Google, GitHub, or Microsoft Account to log in. 65 | 66 | ## Issue Reporting 67 | 68 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 69 | 70 | ## Author 71 | 72 | [Auth0](auth0.com) 73 | 74 | ## License 75 | 76 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 77 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "mean-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/assets/scss/styles.scss" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "mean-app:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "mean-app:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "mean-app:build" 63 | } 64 | }, 65 | "test": { 66 | "builder": "@angular-devkit/build-angular:karma", 67 | "options": { 68 | "main": "src/test.ts", 69 | "karmaConfig": "./karma.conf.js", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "scripts": [], 73 | "styles": [ 74 | "src/assets/scss/styles.scss" 75 | ], 76 | "assets": [ 77 | "src/assets", 78 | "src/favicon.ico" 79 | ] 80 | } 81 | }, 82 | "lint": { 83 | "builder": "@angular-devkit/build-angular:tslint", 84 | "options": { 85 | "tsConfig": [ 86 | "src/tsconfig.app.json", 87 | "src/tsconfig.spec.json" 88 | ], 89 | "exclude": [] 90 | } 91 | } 92 | } 93 | }, 94 | "mean-app-e2e": { 95 | "root": "", 96 | "sourceRoot": "", 97 | "projectType": "application", 98 | "architect": { 99 | "e2e": { 100 | "builder": "@angular-devkit/build-angular:protractor", 101 | "options": { 102 | "protractorConfig": "./protractor.conf.js", 103 | "devServerTarget": "mean-app:serve" 104 | } 105 | }, 106 | "lint": { 107 | "builder": "@angular-devkit/build-angular:tslint", 108 | "options": { 109 | "tsConfig": [ 110 | "e2e/tsconfig.e2e.json" 111 | ], 112 | "exclude": [] 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "defaultProject": "mean-app", 119 | "schematics": { 120 | "@schematics/angular:component": { 121 | "prefix": "app", 122 | "styleext": "scss" 123 | }, 124 | "@schematics/angular:directive": { 125 | "prefix": "app" 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { MeanAppPage } from './app.po'; 2 | 3 | describe('mean-app App', () => { 4 | let page: MeanAppPage; 5 | 6 | beforeEach(() => { 7 | page = new MeanAppPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class MeanAppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | 20 | ], 21 | preprocessors: { 22 | 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean-rsvp-auth0", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "dev": "concurrently \"ng serve\" \"NODE_ENV=dev node server\"", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "6.1.3", 17 | "@angular/common": "6.1.3", 18 | "@angular/compiler": "6.1.3", 19 | "@angular/core": "6.1.3", 20 | "@angular/forms": "6.1.3", 21 | "@angular/platform-browser": "6.1.3", 22 | "@angular/platform-browser-dynamic": "6.1.3", 23 | "@angular/router": "6.1.3", 24 | "auth0-js": "^9.7.3", 25 | "body-parser": "^1.17.1", 26 | "core-js": "^2.4.1", 27 | "cors": "^2.8.3", 28 | "express": "^4.15.2", 29 | "express-jwt": "^5.3.1", 30 | "jwks-rsa": "^1.3.0", 31 | "method-override": "^2.3.8", 32 | "mongoose": "^5.0.11", 33 | "rxjs": "^6.2.2", 34 | "zone.js": "^0.8.26" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "~0.6.0", 38 | "@angular-devkit/core": "0.6.0", 39 | "@angular-devkit/schematics": "0.6.0", 40 | "@angular/cli": "6.1.4", 41 | "@angular/compiler-cli": "6.1.3", 42 | "@angular/language-service": "6.1.3", 43 | "@types/jasmine": "~2.5.53", 44 | "@types/jasminewd2": "~2.0.2", 45 | "@types/node": "~6.0.60", 46 | "ajv": "^6.1.1", 47 | "codelyzer": "4.3.0", 48 | "concurrently": "^3.5.1", 49 | "jasmine-core": "~2.6.2", 50 | "jasmine-spec-reporter": "~4.1.0", 51 | "karma": "~1.7.0", 52 | "karma-chrome-launcher": "~2.1.1", 53 | "karma-cli": "~1.0.1", 54 | "karma-coverage-istanbul-reporter": "^1.2.1", 55 | "karma-jasmine": "~1.1.0", 56 | "karma-jasmine-html-reporter": "^0.2.2", 57 | "protractor": "^5.4.0", 58 | "ts-node": "~3.2.0", 59 | "tslint": "5.10.0", 60 | "typescript": "2.9.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------- 3 | | Dependencies 4 | |-------------------------------------- 5 | */ 6 | 7 | // Modules 8 | const express = require('express'); 9 | const path = require('path'); 10 | const bodyParser = require('body-parser'); 11 | const mongoose = require('mongoose'); 12 | const methodOverride = require('method-override'); 13 | const cors = require('cors'); 14 | // Config 15 | const config = require('./server/config'); 16 | 17 | /* 18 | |-------------------------------------- 19 | | MongoDB 20 | |-------------------------------------- 21 | */ 22 | 23 | mongoose.connect(config.MONGO_URI); 24 | const monDb = mongoose.connection; 25 | 26 | monDb.on('error', function() { 27 | console.error('MongoDB Connection Error. Please make sure that', config.MONGO_URI, 'is running.'); 28 | }); 29 | 30 | monDb.once('open', function callback() { 31 | console.info('Connected to MongoDB:', config.MONGO_URI); 32 | }); 33 | 34 | /* 35 | |-------------------------------------- 36 | | App 37 | |-------------------------------------- 38 | */ 39 | 40 | const app = express(); 41 | 42 | app.use(bodyParser.json()); 43 | app.use(bodyParser.urlencoded({ extended: false })); 44 | app.use(methodOverride('X-HTTP-Method-Override')); 45 | app.use(cors()); 46 | 47 | // Set port 48 | const port = process.env.PORT || '8083'; 49 | app.set('port', port); 50 | 51 | // Set static path to Angular app in dist 52 | // Don't run in dev 53 | if (process.env.NODE_ENV !== 'dev') { 54 | app.use('/', express.static(path.join(__dirname, './dist'))); 55 | } 56 | 57 | /* 58 | |-------------------------------------- 59 | | Routes 60 | |-------------------------------------- 61 | */ 62 | 63 | require('./server/api')(app, config); 64 | 65 | // Pass routing to Angular app 66 | // Don't run in dev 67 | if (process.env.NODE_ENV !== 'dev') { 68 | app.get('*', function(req, res) { 69 | res.sendFile(path.join(__dirname, '/dist/index.html')); 70 | }); 71 | } 72 | 73 | /* 74 | |-------------------------------------- 75 | | Server 76 | |-------------------------------------- 77 | */ 78 | 79 | app.listen(port, () => console.log(`Server running on localhost:${port}`)); 80 | -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------- 3 | | Dependencies 4 | |-------------------------------------- 5 | */ 6 | 7 | const jwt = require('express-jwt'); 8 | const jwks = require('jwks-rsa'); 9 | const Event = require('./models/Event'); 10 | const Rsvp = require('./models/Rsvp'); 11 | 12 | /* 13 | |-------------------------------------- 14 | | Authentication Middleware 15 | |-------------------------------------- 16 | */ 17 | 18 | module.exports = function(app, config) { 19 | // Authentication middleware 20 | const jwtCheck = jwt({ 21 | secret: jwks.expressJwtSecret({ 22 | cache: true, 23 | rateLimit: true, 24 | jwksRequestsPerMinute: 5, 25 | jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json` 26 | }), 27 | audience: config.AUTH0_API_AUDIENCE, 28 | issuer: `https://${config.AUTH0_DOMAIN}/`, 29 | algorithm: 'RS256' 30 | }); 31 | 32 | // Check for an authenticated admin user 33 | const adminCheck = (req, res, next) => { 34 | const roles = req.user[config.NAMESPACE] || []; 35 | if (roles.indexOf('admin') > -1) { 36 | next(); 37 | } else { 38 | res.status(401).send({message: 'Not authorized for admin access'}); 39 | } 40 | } 41 | 42 | /* 43 | |-------------------------------------- 44 | | API Routes 45 | |-------------------------------------- 46 | */ 47 | 48 | const _eventListProjection = 'title startDatetime endDatetime viewPublic'; 49 | 50 | // GET API root 51 | app.get('/api/', (req, res) => { 52 | res.send('API works'); 53 | }); 54 | 55 | // GET list of public events starting in the future 56 | app.get('/api/events', (req, res) => { 57 | Event.find({viewPublic: true, startDatetime: { $gte: new Date() }}, 58 | _eventListProjection, (err, events) => { 59 | let eventsArr = []; 60 | if (err) { 61 | return res.status(500).send({message: err.message}); 62 | } 63 | if (events) { 64 | events.forEach(event => { 65 | eventsArr.push(event); 66 | }); 67 | } 68 | res.send(eventsArr); 69 | } 70 | ); 71 | }); 72 | 73 | // GET list of all events, public and private (admin only) 74 | app.get('/api/events/admin', jwtCheck, adminCheck, (req, res) => { 75 | Event.find({}, _eventListProjection, (err, events) => { 76 | let eventsArr = []; 77 | if (err) { 78 | return res.status(500).send({message: err.message}); 79 | } 80 | if (events) { 81 | events.forEach(event => { 82 | eventsArr.push(event); 83 | }); 84 | } 85 | res.send(eventsArr); 86 | } 87 | ); 88 | }); 89 | 90 | // GET event by event ID 91 | app.get('/api/event/:id', jwtCheck, (req, res) => { 92 | Event.findById(req.params.id, (err, event) => { 93 | if (err) { 94 | return res.status(500).send({message: err.message}); 95 | } 96 | if (!event) { 97 | return res.status(400).send({message: 'Event not found.'}); 98 | } 99 | res.send(event); 100 | }); 101 | }); 102 | 103 | // GET RSVPs by event ID 104 | app.get('/api/event/:eventId/rsvps', jwtCheck, (req, res) => { 105 | Rsvp.find({eventId: req.params.eventId}, (err, rsvps) => { 106 | let rsvpsArr = []; 107 | if (err) { 108 | return res.status(500).send({message: err.message}); 109 | } 110 | if (rsvps) { 111 | rsvps.forEach(rsvp => { 112 | rsvpsArr.push(rsvp); 113 | }); 114 | } 115 | res.send(rsvpsArr); 116 | }); 117 | }); 118 | 119 | // GET list of upcoming events user has RSVPed to 120 | app.get('/api/events/:userId', jwtCheck, (req, res) => { 121 | Rsvp.find({userId: req.params.userId}, 'eventId', (err, rsvps) => { 122 | const _eventIdsArr = rsvps.map(rsvp => rsvp.eventId); 123 | const _rsvpEventsProjection = 'title startDatetime endDatetime'; 124 | let eventsArr = []; 125 | 126 | if (err) { 127 | return res.status(500).send({message: err.message}); 128 | } 129 | if (rsvps) { 130 | Event.find( 131 | {_id: {$in: _eventIdsArr}, startDatetime: { $gte: new Date() }}, 132 | _rsvpEventsProjection, (err, events) => { 133 | if (err) { 134 | return res.status(500).send({message: err.message}); 135 | } 136 | if (events) { 137 | events.forEach(event => { 138 | eventsArr.push(event); 139 | }); 140 | } 141 | res.send(eventsArr); 142 | }); 143 | } 144 | }); 145 | }); 146 | 147 | // POST a new event 148 | app.post('/api/event/new', jwtCheck, adminCheck, (req, res) => { 149 | Event.findOne({ 150 | title: req.body.title, 151 | location: req.body.location, 152 | startDatetime: req.body.startDatetime}, (err, existingEvent) => { 153 | if (err) { 154 | return res.status(500).send({message: err.message}); 155 | } 156 | if (existingEvent) { 157 | return res.status(409).send({message: 'You have already created an event with this title, location, and start date/time.'}); 158 | } 159 | const event = new Event({ 160 | title: req.body.title, 161 | location: req.body.location, 162 | startDatetime: req.body.startDatetime, 163 | endDatetime: req.body.endDatetime, 164 | description: req.body.description, 165 | viewPublic: req.body.viewPublic 166 | }); 167 | event.save((err) => { 168 | if (err) { 169 | return res.status(500).send({message: err.message}); 170 | } 171 | res.send(event); 172 | }); 173 | }); 174 | }); 175 | 176 | // PUT (edit) an existing event 177 | app.put('/api/event/:id', jwtCheck, adminCheck, (req, res) => { 178 | Event.findById(req.params.id, (err, event) => { 179 | if (err) { 180 | return res.status(500).send({message: err.message}); 181 | } 182 | if (!event) { 183 | return res.status(400).send({message: 'Event not found.'}); 184 | } 185 | event.title = req.body.title; 186 | event.location = req.body.location; 187 | event.startDatetime = req.body.startDatetime; 188 | event.endDatetime = req.body.endDatetime; 189 | event.viewPublic = req.body.viewPublic; 190 | event.description = req.body.description; 191 | 192 | event.save(err => { 193 | if (err) { 194 | return res.status(500).send({message: err.message}); 195 | } 196 | res.send(event); 197 | }); 198 | }); 199 | }); 200 | 201 | // DELETE an event and all associated RSVPs 202 | app.delete('/api/event/:id', jwtCheck, adminCheck, (req, res) => { 203 | Event.findById(req.params.id, (err, event) => { 204 | if (err) { 205 | return res.status(500).send({message: err.message}); 206 | } 207 | if (!event) { 208 | return res.status(400).send({message: 'Event not found.'}); 209 | } 210 | Rsvp.find({eventId: req.params.id}, (err, rsvps) => { 211 | if (rsvps) { 212 | rsvps.forEach(rsvp => { 213 | rsvp.remove(); 214 | }); 215 | } 216 | event.remove(err => { 217 | if (err) { 218 | return res.status(500).send({message: err.message}); 219 | } 220 | res.status(200).send({message: 'Event and RSVPs successfully deleted.'}); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | // POST a new RSVP 227 | app.post('/api/rsvp/new', jwtCheck, (req, res) => { 228 | Rsvp.findOne({eventId: req.body.eventId, userId: req.body.userId}, (err, existingRsvp) => { 229 | if (err) { 230 | return res.status(500).send({message: err.message}); 231 | } 232 | if (existingRsvp) { 233 | return res.status(409).send({message: 'You have already RSVPed to this event.'}); 234 | } 235 | const rsvp = new Rsvp({ 236 | userId: req.body.userId, 237 | name: req.body.name, 238 | eventId: req.body.eventId, 239 | attending: req.body.attending, 240 | guests: req.body.guests, 241 | comments: req.body.comments 242 | }); 243 | rsvp.save((err) => { 244 | if (err) { 245 | return res.status(500).send({message: err.message}); 246 | } 247 | res.send(rsvp); 248 | }); 249 | }); 250 | }); 251 | 252 | // PUT (edit) an existing RSVP 253 | app.put('/api/rsvp/:id', jwtCheck, (req, res) => { 254 | Rsvp.findById(req.params.id, (err, rsvp) => { 255 | if (err) { 256 | return res.status(500).send({message: err.message}); 257 | } 258 | if (!rsvp) { 259 | return res.status(400).send({message: 'RSVP not found.'}); 260 | } 261 | if (rsvp.userId !== req.user.sub) { 262 | return res.status(401).send({message: 'You cannot edit someone else\'s RSVP.'}); 263 | } 264 | rsvp.name = req.body.name; 265 | rsvp.attending = req.body.attending; 266 | rsvp.guests = req.body.guests; 267 | rsvp.comments = req.body.comments; 268 | 269 | rsvp.save(err => { 270 | if (err) { 271 | return res.status(500).send({message: err.message}); 272 | } 273 | res.send(rsvp); 274 | }); 275 | }); 276 | }); 277 | 278 | }; 279 | -------------------------------------------------------------------------------- /server/config.js.SAMPLE: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AUTH0_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g., kmaida.auth0.com 3 | AUTH0_API_AUDIENCE: '[YOUR_AUTH0_API_NAME]', // e.g., 'http://localhost:8083/api/' 4 | MONGO_URI: process.env.MONGO_URI || 'mongodb://[USER]:[PASSWORD]@[DS######].mlab.com:[PORT]/[DB_NAME]', 5 | NAMESPACE: '[YOUR_AUTH0_ROLES_RULES_NAMESPACE]' // e.g., http://myapp.com/roles 6 | }; 7 | -------------------------------------------------------------------------------- /server/models/Event.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------- 3 | | Event Model 4 | |-------------------------------------- 5 | */ 6 | 7 | const mongoose = require('mongoose'); 8 | const Schema = mongoose.Schema; 9 | 10 | const eventSchema = new Schema({ 11 | title: { type: String, required: true }, 12 | location: { type: String, required: true }, 13 | startDatetime: { type: Date, required: true }, 14 | endDatetime: { type: Date, required: true }, 15 | description: String, 16 | viewPublic: { type: Boolean, required: true } 17 | }); 18 | 19 | module.exports = mongoose.model('Event', eventSchema); 20 | -------------------------------------------------------------------------------- /server/models/Rsvp.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------- 3 | | Rsvp Model 4 | |-------------------------------------- 5 | */ 6 | 7 | const mongoose = require('mongoose'); 8 | const Schema = mongoose.Schema; 9 | 10 | const rsvpSchema = new Schema({ 11 | userId: { type: String, required: true }, 12 | name: { type: String, required: true }, 13 | eventId: { type: String, required: true }, 14 | attending: { type: Boolean, required: true }, 15 | guests: Number, 16 | comments: String 17 | }); 18 | 19 | module.exports = mongoose.model('Rsvp', rsvpSchema); 20 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | // Route guards 4 | import { AuthGuard } from './auth/auth.guard'; 5 | import { AdminGuard } from './auth/admin.guard'; 6 | // Page components 7 | import { HomeComponent } from './pages/home/home.component'; 8 | import { CallbackComponent } from './pages/callback/callback.component'; 9 | import { MyRsvpsComponent } from './pages/my-rsvps/my-rsvps.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: HomeComponent 15 | }, 16 | { 17 | path: 'callback', 18 | component: CallbackComponent 19 | }, 20 | { 21 | path: 'event/:id', 22 | loadChildren: './pages/event/event.module#EventModule', 23 | canActivate: [ 24 | AuthGuard 25 | ] 26 | }, 27 | { 28 | path: 'my-rsvps', 29 | component: MyRsvpsComponent, 30 | canActivate: [ 31 | AuthGuard 32 | ] 33 | }, 34 | { 35 | path: 'admin', 36 | loadChildren: './pages/admin/admin.module#AdminModule', 37 | canActivate: [ 38 | AuthGuard, 39 | AdminGuard 40 | ] 41 | }, 42 | { 43 | path: '**', 44 | redirectTo: '', 45 | pathMatch: 'full' 46 | } 47 | ]; 48 | 49 | @NgModule({ 50 | imports: [RouterModule.forRoot(routes)], 51 | providers: [ 52 | AuthGuard, 53 | AdminGuard 54 | ], 55 | exports: [RouterModule] 56 | }) 57 | export class AppRoutingModule { } 58 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | APP COMPONENT 3 | --------------------*/ 4 | 5 | @import '../assets/scss/partials/layout.vars'; 6 | @import '../assets/scss/partials/responsive.partial'; 7 | 8 | .layout-overflow { 9 | overflow: hidden; /* necessary to handle offcanvas scrollbar behavior */ 10 | } 11 | .layout-canvas { 12 | background: #fff; 13 | backface-visibility: hidden; 14 | -webkit-backface-visibility: hidden; /* Safari: http://caniuse.com/#search=css3%203d */ 15 | position: relative; 16 | left: 0; 17 | transition: transform 250ms ease; 18 | transform: translate3d(0,0,0); 19 | width: 100%; 20 | 21 | &.nav-open { 22 | transform: translate3d(270px,0,0); 23 | } 24 | } 25 | .layout-view { 26 | padding: $padding-screen-small; 27 | 28 | @include mq($large) { 29 | margin: 0 auto; 30 | max-width: 960px; 31 | padding: $padding-screen-large; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { fromEvent } from 'rxjs'; 3 | import { debounceTime } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | navOpen: boolean; 12 | minHeight: string; 13 | private _initWinHeight = 0; 14 | 15 | constructor() { } 16 | 17 | ngOnInit() { 18 | fromEvent(window, 'resize') 19 | .pipe( 20 | debounceTime(200) 21 | ) 22 | .subscribe((event) => this._resizeFn(event)); 23 | 24 | this._initWinHeight = window.innerHeight; 25 | this._resizeFn(null); 26 | } 27 | 28 | navToggledHandler(e: boolean) { 29 | this.navOpen = e; 30 | } 31 | 32 | private _resizeFn(e) { 33 | const winHeight: number = e ? e.target.innerHeight : this._initWinHeight; 34 | this.minHeight = `${winHeight}px`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { AuthModule } from './auth/auth.module'; 6 | import { CoreModule } from './core/core.module'; 7 | import { AppRoutingModule } from './app-routing.module'; 8 | 9 | import { AppComponent } from './app.component'; 10 | import { HomeComponent } from './pages/home/home.component'; 11 | import { CallbackComponent } from './pages/callback/callback.component'; 12 | import { MyRsvpsComponent } from './pages/my-rsvps/my-rsvps.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | HomeComponent, 18 | CallbackComponent, 19 | MyRsvpsComponent 20 | ], 21 | imports: [ 22 | BrowserModule, 23 | BrowserAnimationsModule, 24 | AppRoutingModule, 25 | AuthModule.forRoot(), 26 | CoreModule.forRoot() 27 | ], 28 | providers: [], 29 | bootstrap: [AppComponent] 30 | }) 31 | export class AppModule { } 32 | -------------------------------------------------------------------------------- /src/app/auth/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class AdminGuard implements CanActivate { 8 | 9 | constructor( 10 | private auth: AuthService, 11 | private router: Router 12 | ) { } 13 | 14 | canActivate(): Observable | Promise | boolean { 15 | if (this.auth.isAdmin) { 16 | return true; 17 | } 18 | this.router.navigate(['/']); 19 | return false; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/auth/auth.config.ts.SAMPLE: -------------------------------------------------------------------------------- 1 | import { ENV } from './../core/env.config'; 2 | 3 | interface AuthConfig { 4 | CLIENT_ID: string; 5 | CLIENT_DOMAIN: string; 6 | AUDIENCE: string; 7 | REDIRECT: string; 8 | SCOPE: string; 9 | NAMESPACE: string; 10 | }; 11 | 12 | export const AUTH_CONFIG: AuthConfig = { 13 | CLIENT_ID: '[AUTH0_CLIENT_ID]', 14 | CLIENT_DOMAIN: '[AUTH0_CLIENT_DOMAIN]', // e.g., kmaida.auth0.com 15 | AUDIENCE: '[YOUR_AUTH0_API_AUDIENCE]', // e.g., http://localhost:8083/api/ 16 | REDIRECT: `${ENV.BASE_URI}/callback`, 17 | SCOPE: 'openid profile email', 18 | NAMESPACE: 'http://myapp.com/roles' 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class AuthGuard implements CanActivate { 8 | 9 | constructor(private auth: AuthService) { } 10 | 11 | canActivate( 12 | next: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ): Observable | Promise | boolean { 15 | if (!this.auth.loggedIn) { 16 | localStorage.setItem('authRedirect', state.url); 17 | } 18 | if (!this.auth.tokenValid && !this.auth.loggedIn) { 19 | this.auth.login(); 20 | return false; 21 | } 22 | if (this.auth.tokenValid && this.auth.loggedIn) { 23 | return true; 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @NgModule({ 6 | imports: [ 7 | CommonModule 8 | ], 9 | declarations: [] 10 | }) 11 | export class AuthModule { 12 | static forRoot(): ModuleWithProviders { 13 | return { 14 | ngModule: AuthModule, 15 | providers: [ 16 | AuthService 17 | ] 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { BehaviorSubject, Subscription, of, timer } from 'rxjs'; 4 | import { mergeMap } from 'rxjs/operators'; 5 | import { AUTH_CONFIG } from './auth.config'; 6 | import * as auth0 from 'auth0-js'; 7 | import { ENV } from './../core/env.config'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | // Create Auth0 web auth instance 12 | private _auth0 = new auth0.WebAuth({ 13 | clientID: AUTH_CONFIG.CLIENT_ID, 14 | domain: AUTH_CONFIG.CLIENT_DOMAIN, 15 | responseType: 'token', 16 | redirectUri: AUTH_CONFIG.REDIRECT, 17 | audience: AUTH_CONFIG.AUDIENCE, 18 | scope: AUTH_CONFIG.SCOPE 19 | }); 20 | accessToken: string; 21 | userProfile: any; 22 | expiresAt: number; 23 | isAdmin: boolean; 24 | // Create a stream of logged in status to communicate throughout app 25 | loggedIn: boolean; 26 | loggedIn$ = new BehaviorSubject(this.loggedIn); 27 | loggingIn: boolean; 28 | // Subscribe to token expiration stream 29 | refreshSub: Subscription; 30 | routeSub: Subscription; 31 | 32 | constructor(private router: Router) { 33 | // If app auth token is not expired, request new token 34 | if (JSON.parse(localStorage.getItem('expires_at')) > Date.now()) { 35 | this.renewToken(); 36 | } 37 | } 38 | 39 | setLoggedIn(value: boolean) { 40 | // Update login status behavior subject 41 | this.loggedIn$.next(value); 42 | this.loggedIn = value; 43 | } 44 | 45 | login() { 46 | // Auth0 authorize request 47 | this._auth0.authorize(); 48 | } 49 | 50 | handleAuth() { 51 | // When Auth0 hash parsed, get profile 52 | this._auth0.parseHash((err, authResult) => { 53 | if (authResult && authResult.accessToken) { 54 | window.location.hash = ''; 55 | this._getProfile(authResult); 56 | } else if (err) { 57 | this._clearRedirect(); 58 | this.router.navigate(['/']); 59 | console.error(`Error authenticating: ${err.error}`); 60 | } 61 | }); 62 | } 63 | 64 | private _getProfile(authResult) { 65 | this.loggingIn = true; 66 | // Use access token to retrieve user's profile and set session 67 | this._auth0.client.userInfo(authResult.accessToken, (err, profile) => { 68 | if (profile) { 69 | this._setSession(authResult, profile); 70 | this._redirect(); 71 | } else if (err) { 72 | console.warn(`Error retrieving profile: ${err.error}`); 73 | } 74 | }); 75 | } 76 | 77 | private _setSession(authResult, profile?) { 78 | this.expiresAt = (authResult.expiresIn * 1000) + Date.now(); 79 | // Store expiration in local storage to access in constructor 80 | localStorage.setItem('expires_at', JSON.stringify(this.expiresAt)); 81 | this.accessToken = authResult.accessToken; 82 | // If initial login, set profile and admin information 83 | if (profile) { 84 | this.userProfile = profile; 85 | this.isAdmin = this._checkAdmin(profile); 86 | } 87 | // Update login status in loggedIn$ stream 88 | this.setLoggedIn(true); 89 | this.loggingIn = false; 90 | // Schedule access token renewal 91 | this.scheduleRenewal(); 92 | } 93 | 94 | private _checkAdmin(profile) { 95 | // Check if the user has admin role 96 | const roles = profile[AUTH_CONFIG.NAMESPACE] || []; 97 | return roles.indexOf('admin') > -1; 98 | } 99 | 100 | private _redirect() { 101 | // Redirect with or without 'tab' query parameter 102 | // Note: does not support additional params besides 'tab' 103 | const fullRedirect = decodeURI(localStorage.getItem('authRedirect')); 104 | const redirectArr = fullRedirect.split('?tab='); 105 | const navArr = [redirectArr[0] || '/']; 106 | const tabObj = redirectArr[1] ? { queryParams: { tab: redirectArr[1] }} : null; 107 | 108 | if (!tabObj) { 109 | this.router.navigate(navArr); 110 | } else { 111 | this.router.navigate(navArr, tabObj); 112 | } 113 | // Redirection completed; clear redirect from storage 114 | this._clearRedirect(); 115 | } 116 | 117 | private _clearRedirect() { 118 | // Remove redirect from localStorage 119 | localStorage.removeItem('authRedirect'); 120 | } 121 | 122 | private _clearExpiration() { 123 | // Remove token expiration from localStorage 124 | localStorage.removeItem('expires_at'); 125 | } 126 | 127 | logout() { 128 | // Remove data from localStorage 129 | this._clearExpiration(); 130 | this._clearRedirect(); 131 | // End Auth0 authentication session 132 | this._auth0.logout({ 133 | clientId: AUTH_CONFIG.CLIENT_ID, 134 | returnTo: ENV.BASE_URI 135 | }); 136 | } 137 | 138 | get tokenValid(): boolean { 139 | // Check if current time is past access token's expiration 140 | return Date.now() < JSON.parse(localStorage.getItem('expires_at')); 141 | } 142 | 143 | renewToken() { 144 | // Check for valid Auth0 session 145 | this._auth0.checkSession({}, (err, authResult) => { 146 | if (authResult && authResult.accessToken) { 147 | this._getProfile(authResult); 148 | } else { 149 | this._clearExpiration(); 150 | } 151 | }); 152 | } 153 | 154 | scheduleRenewal() { 155 | // If last token is expired, do nothing 156 | if (!this.tokenValid) { return; } 157 | // Unsubscribe from previous expiration observable 158 | this.unscheduleRenewal(); 159 | // Create and subscribe to expiration observable 160 | const expiresIn$ = of(this.expiresAt).pipe( 161 | mergeMap( 162 | expires => { 163 | const now = Date.now(); 164 | // Use timer to track delay until expiration 165 | // to run the refresh at the proper time 166 | return timer(Math.max(1, expires - now)); 167 | } 168 | ) 169 | ); 170 | 171 | this.refreshSub = expiresIn$ 172 | .subscribe( 173 | () => { 174 | this.renewToken(); 175 | this.scheduleRenewal(); 176 | } 177 | ); 178 | } 179 | 180 | unscheduleRenewal() { 181 | if (this.refreshSub) { 182 | this.refreshSub.unsubscribe(); 183 | } 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/app/core/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; 3 | import { AuthService } from './../auth/auth.service'; 4 | import { throwError as ObservableThrowError, Observable } from 'rxjs'; 5 | import { catchError } from 'rxjs/operators'; 6 | import { ENV } from './env.config'; 7 | import { EventModel } from './models/event.model'; 8 | import { RsvpModel } from './models/rsvp.model'; 9 | 10 | @Injectable() 11 | export class ApiService { 12 | constructor( 13 | private http: HttpClient, 14 | private auth: AuthService 15 | ) { } 16 | 17 | private get _authHeader(): string { 18 | return `Bearer ${this.auth.accessToken}`; 19 | } 20 | 21 | // GET list of public, future events 22 | getEvents$(): Observable { 23 | return this.http 24 | .get(`${ENV.BASE_API}events`) 25 | .pipe( 26 | catchError((error) => this._handleError(error)) 27 | ); 28 | } 29 | 30 | // GET all events - private and public (admin only) 31 | getAdminEvents$(): Observable { 32 | return this.http 33 | .get(`${ENV.BASE_API}events/admin`, { 34 | headers: new HttpHeaders().set('Authorization', this._authHeader) 35 | }) 36 | .pipe( 37 | catchError((error) => this._handleError(error)) 38 | ); 39 | } 40 | 41 | // GET an event by ID (login required) 42 | getEventById$(id: string): Observable { 43 | return this.http 44 | .get(`${ENV.BASE_API}event/${id}`, { 45 | headers: new HttpHeaders().set('Authorization', this._authHeader) 46 | }) 47 | .pipe( 48 | catchError((error) => this._handleError(error)) 49 | ); 50 | } 51 | 52 | // GET RSVPs by event ID (login required) 53 | getRsvpsByEventId$(eventId: string): Observable { 54 | return this.http 55 | .get(`${ENV.BASE_API}event/${eventId}/rsvps`, { 56 | headers: new HttpHeaders().set('Authorization', this._authHeader) 57 | }) 58 | .pipe( 59 | catchError((error) => this._handleError(error)) 60 | ); 61 | } 62 | 63 | // POST new event (admin only) 64 | postEvent$(event: EventModel): Observable { 65 | return this.http 66 | .post(`${ENV.BASE_API}event/new`, event, { 67 | headers: new HttpHeaders().set('Authorization', this._authHeader) 68 | }) 69 | .pipe( 70 | catchError((error) => this._handleError(error)) 71 | ); 72 | } 73 | 74 | // PUT existing event (admin only) 75 | editEvent$(id: string, event: EventModel): Observable { 76 | return this.http 77 | .put(`${ENV.BASE_API}event/${id}`, event, { 78 | headers: new HttpHeaders().set('Authorization', this._authHeader) 79 | }) 80 | .pipe( 81 | catchError((error) => this._handleError(error)) 82 | ); 83 | } 84 | 85 | // DELETE existing event and all associated RSVPs (admin only) 86 | deleteEvent$(id: string): Observable { 87 | return this.http 88 | .delete(`${ENV.BASE_API}event/${id}`, { 89 | headers: new HttpHeaders().set('Authorization', this._authHeader) 90 | }) 91 | .pipe( 92 | catchError((error) => this._handleError(error)) 93 | ); 94 | } 95 | 96 | // GET all events a specific user has RSVPed to (login required) 97 | getUserEvents$(userId: string): Observable { 98 | return this.http 99 | .get(`${ENV.BASE_API}events/${userId}`, { 100 | headers: new HttpHeaders().set('Authorization', this._authHeader) 101 | }) 102 | .pipe( 103 | catchError((error) => this._handleError(error)) 104 | ); 105 | } 106 | 107 | // POST new RSVP (login required) 108 | postRsvp$(rsvp: RsvpModel): Observable { 109 | return this.http 110 | .post(`${ENV.BASE_API}rsvp/new`, rsvp, { 111 | headers: new HttpHeaders().set('Authorization', this._authHeader) 112 | }) 113 | .pipe( 114 | catchError((error) => this._handleError(error)) 115 | ); 116 | } 117 | 118 | // PUT existing RSVP (login required) 119 | editRsvp$(id: string, rsvp: RsvpModel): Observable { 120 | return this.http 121 | .put(`${ENV.BASE_API}rsvp/${id}`, rsvp, { 122 | headers: new HttpHeaders().set('Authorization', this._authHeader) 123 | }) 124 | .pipe( 125 | catchError((error) => this._handleError(error)) 126 | ); 127 | } 128 | 129 | private _handleError(err: HttpErrorResponse | any): Observable { 130 | const errorMsg = err.message || 'Error: Unable to complete request.'; 131 | if (err.message && err.message.indexOf('No JWT present') > -1) { 132 | this.auth.login(); 133 | } 134 | return ObservableThrowError(errorMsg); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { CommonModule } from '@angular/common'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { RouterModule } from '@angular/router'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | import { DatePipe } from '@angular/common'; 8 | import { ApiService } from './api.service'; 9 | import { UtilsService } from './utils.service'; 10 | import { FilterSortService } from './filter-sort.service'; 11 | import { SubmittingComponent } from './forms/submitting.component'; 12 | import { LoadingComponent } from './loading.component'; 13 | import { HeaderComponent } from './../header/header.component'; 14 | import { FooterComponent } from './../footer/footer.component'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | CommonModule, 19 | HttpClientModule, 20 | RouterModule, 21 | FormsModule, 22 | ReactiveFormsModule 23 | ], 24 | declarations: [ 25 | HeaderComponent, 26 | FooterComponent, 27 | LoadingComponent, 28 | SubmittingComponent 29 | ], 30 | exports: [ 31 | HttpClientModule, 32 | RouterModule, 33 | FormsModule, 34 | ReactiveFormsModule, 35 | HeaderComponent, 36 | FooterComponent, 37 | LoadingComponent, 38 | SubmittingComponent 39 | ] 40 | }) 41 | export class CoreModule { 42 | static forRoot(): ModuleWithProviders { 43 | return { 44 | ngModule: CoreModule, 45 | providers: [ 46 | Title, 47 | DatePipe, 48 | ApiService, 49 | UtilsService, 50 | FilterSortService 51 | ] 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/core/env.config.ts: -------------------------------------------------------------------------------- 1 | const _isDev = window.location.port.indexOf('4200') > -1; 2 | const getHost = () => { 3 | const protocol = window.location.protocol; 4 | const host = window.location.host; 5 | return `${protocol}//${host}`; 6 | }; 7 | const apiURI = _isDev ? 'http://localhost:8083/api/' : `/api/`; 8 | 9 | export const ENV = { 10 | BASE_URI: getHost(), 11 | BASE_API: apiURI 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/core/expand-collapse.animation.ts: -------------------------------------------------------------------------------- 1 | import { trigger, transition, style, animate, state } from '@angular/animations'; 2 | /* 3 | Component declaration: 4 | @Component({ 5 | selector: 'app-anim', 6 | animations: [expandCollapse], 7 | templateUrl: './anim.component.html', 8 | styleUrls: ['./anim.component.scss'] 9 | }) 10 | Template: 11 |
12 | */ 13 | export const expandCollapse = trigger('expandCollapse', [ 14 | state('*', style({ 15 | 'overflow-y': 'hidden', 16 | 'height': '*' 17 | })), 18 | state('void', style({ 19 | 'height': '0', 20 | 'overflow-y': 'hidden' 21 | })), 22 | transition('* => void', animate('250ms ease-out')), 23 | transition('void => *', animate('250ms ease-in')) 24 | ]); 25 | -------------------------------------------------------------------------------- /src/app/core/filter-sort.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DatePipe } from '@angular/common'; 3 | 4 | @Injectable() 5 | export class FilterSortService { 6 | 7 | constructor(private datePipe: DatePipe) { } 8 | 9 | private _objArrayCheck(array: any[]): boolean { 10 | // Checks if the first item in the array is an object 11 | // (assumes same-shape for all array items) 12 | // Necessary because some arrays passed in may have 13 | // models that don't match {[key: string]: any}[] 14 | // This check prevents uncaught reference errors 15 | const item0 = array[0]; 16 | const check = !!(array.length && item0 !== null && Object.prototype.toString.call(item0) === '[object Object]'); 17 | return check; 18 | } 19 | 20 | filter(array: any[], property: string, value: any) { 21 | // Return only items with specific key/value pair 22 | if (!property || value === undefined || !this._objArrayCheck(array)) { 23 | return array; 24 | } 25 | const filteredArray = array.filter(item => { 26 | for (const key in item) { 27 | if (item.hasOwnProperty(key)) { 28 | if (key === property && item[key] === value) { 29 | return true; 30 | } 31 | } 32 | } 33 | }); 34 | return filteredArray; 35 | } 36 | 37 | search(array: any[], query: string, excludeProps?: string|string[], dateFormat?: string) { 38 | // Match query to strings and Date objects / ISO UTC strings 39 | // Optionally exclude properties from being searched 40 | // If matching dates, can optionally pass in date format string 41 | if (!query || !this._objArrayCheck(array)) { 42 | return array; 43 | } 44 | const lQuery = query.toLowerCase(); 45 | const isoDateRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // ISO UTC 46 | const dateF = dateFormat ? dateFormat : 'medium'; 47 | const filteredArray = array.filter(item => { 48 | for (const key in item) { 49 | if (item.hasOwnProperty(key)) { 50 | if (!excludeProps || excludeProps.indexOf(key) === -1) { 51 | const thisVal = item[key]; 52 | if ( 53 | // Value is a string and NOT a UTC date 54 | typeof thisVal === 'string' && 55 | !thisVal.match(isoDateRegex) && 56 | thisVal.toLowerCase().indexOf(lQuery) !== -1 57 | ) { 58 | return true; 59 | } else if ( 60 | // Value is a Date object or UTC string 61 | (thisVal instanceof Date || thisVal.toString().match(isoDateRegex)) && 62 | // https://angular.io/docs/ts/latest/api/common/index/DatePipe-pipe.html 63 | // Matching date format string passed in as param (or default to 'medium') 64 | this.datePipe.transform(thisVal, dateF).toLowerCase().indexOf(lQuery) !== -1 65 | ) { 66 | return true; 67 | } 68 | } 69 | } 70 | } 71 | }); 72 | return filteredArray; 73 | } 74 | 75 | noSearchResults(arr: any[], query: string): boolean { 76 | // Check if array searched by query returned any results 77 | return !!(!arr.length && query); 78 | } 79 | 80 | orderByDate(array: any[], prop: string, reverse?: boolean) { 81 | // Order an array of objects by a date property 82 | // Default: ascending (1992->2017 | Jan->Dec) 83 | if (!prop || !this._objArrayCheck(array)) { 84 | return array; 85 | } 86 | const sortedArray = array.sort((a, b) => { 87 | const dateA = new Date(a[prop]).getTime(); 88 | const dateB = new Date(b[prop]).getTime(); 89 | return !reverse ? dateA - dateB : dateB - dateA; 90 | }); 91 | return sortedArray; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/app/core/forms/date-range.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | import { stringsToDate } from './formUtils.factory'; 3 | 4 | export function dateRangeValidator(c: AbstractControl): {[key: string]: any} { 5 | // Get controls in group 6 | const startDateC = c.get('startDate'); 7 | const startTimeC = c.get('startTime'); 8 | const endDateC = c.get('endDate'); 9 | const endTimeC = c.get('endTime'); 10 | // Object to return if date is invalid 11 | const invalidObj = { 'dateRange': true }; 12 | 13 | // If start and end dates are valid, can check range (with prefilled times) 14 | // Final check happens when all dates/times are valid 15 | if (startDateC.valid && endDateC.valid) { 16 | const checkStartTime = startTimeC.invalid ? '12:00 AM' : startTimeC.value; 17 | const checkEndTime = endTimeC.invalid ? '11:59 PM' : endTimeC.value; 18 | const startDatetime = stringsToDate(startDateC.value, checkStartTime); 19 | const endDatetime = stringsToDate(endDateC.value, checkEndTime); 20 | 21 | if (endDatetime >= startDatetime) { 22 | return null; 23 | } else { 24 | return invalidObj; 25 | } 26 | } 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/core/forms/date.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 2 | import { DATE_REGEX } from './formUtils.factory'; 3 | 4 | export function dateValidator(): ValidatorFn { 5 | return (control: AbstractControl): {[key: string]: any} => { 6 | const dateStr = control.value; 7 | // First check for m/d/yyyy format 8 | // If pattern is wrong, don't validate yet 9 | if (!DATE_REGEX.test(dateStr)) { 10 | return null; 11 | } 12 | // Length of months (will update for leap years) 13 | const monthLengthArr = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; 14 | // Object to return if date is invalid 15 | const invalidObj = { 'date': true }; 16 | // Parse the date input to integers 17 | const dateArr = dateStr.split('/'); 18 | const month = parseInt(dateArr[0], 10); 19 | const day = parseInt(dateArr[1], 10); 20 | const year = parseInt(dateArr[2], 10); 21 | // Today's date 22 | const now = new Date(); 23 | 24 | // Validate year and month 25 | if (year < now.getFullYear() || year > 3000 || month === 0 || month > 12) { 26 | return invalidObj; 27 | } 28 | // Adjust for leap years 29 | if (year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0)) { 30 | monthLengthArr[1] = 29; 31 | } 32 | // Validate day 33 | if (!(day > 0 && day <= monthLengthArr[month - 1])) { 34 | return invalidObj; 35 | }; 36 | // If date is properly formatted, check the date vs today to ensure future 37 | // This is done this way to account for new Date() shifting invalid 38 | // date strings. This way we know the string is a correct date first. 39 | const date = new Date(dateStr); 40 | if (date <= now) { 41 | return invalidObj; 42 | } 43 | return null; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/core/forms/formUtils.factory.ts: -------------------------------------------------------------------------------- 1 | // 0-9 2 | // https://regex101.com/r/dU0eY6/1 3 | const GUESTS_REGEX = new RegExp(/^[0-9]$/); 4 | // mm/dd/yyyy, m/d/yyyy 5 | // https://regex101.com/r/7iSsmm/2 6 | const DATE_REGEX = new RegExp(/^(\d{2}|\d)\/(\d{2}|\d)\/\d{4}$/); 7 | // h:mm am/pm, hh:mm AM/PM 8 | // https://regex101.com/r/j2Cfqd/1/ 9 | const TIME_REGEX = new RegExp(/^((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))$/); 10 | 11 | // Converts date + time strings to a Date object. 12 | // Date and time parameters should have already 13 | // been validated with DATE_REGEX and TIME_REGEX. 14 | function stringsToDate(dateStr: string, timeStr: string) { 15 | if (!DATE_REGEX.test(dateStr) || !TIME_REGEX.test(timeStr)) { 16 | console.error('Cannot convert date/time to Date object.'); 17 | return; 18 | } 19 | const date = new Date(dateStr); 20 | const timeArr = timeStr.split(/[\s:]+/); // https://regex101.com/r/H4dMvA/1 21 | let hour = parseInt(timeArr[0], 10); 22 | const min = parseInt(timeArr[1], 10); 23 | const pm = timeArr[2].toLowerCase() === 'pm'; 24 | 25 | if (!pm && hour === 12) { 26 | hour = 0; 27 | } 28 | if (pm && hour < 12) { 29 | hour += 12; 30 | } 31 | date.setHours(hour); 32 | date.setMinutes(min); 33 | return date; 34 | } 35 | 36 | export { GUESTS_REGEX, DATE_REGEX, TIME_REGEX, stringsToDate }; 37 | -------------------------------------------------------------------------------- /src/app/core/forms/submitting.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-submitting', 5 | template: ` 6 | 7 | `, 8 | styles: [` 9 | :host { 10 | display: inline-block; 11 | } 12 | img { 13 | display: inline-block; 14 | margin: 4px 3px; 15 | width: 30px; 16 | } 17 | `] 18 | }) 19 | export class SubmittingComponent { 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading', 5 | template: ` 6 | 7 | `, 8 | styles: [` 9 | :host { 10 | display: block; 11 | } 12 | img { 13 | display: block; 14 | margin: 20px auto; 15 | width: 50px; 16 | } 17 | `] 18 | }) 19 | export class LoadingComponent { 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/models/event.model.ts: -------------------------------------------------------------------------------- 1 | class EventModel { 2 | constructor( 3 | public title: string, 4 | public location: string, 5 | public startDatetime: Date, 6 | public endDatetime: Date, 7 | public viewPublic: boolean, 8 | public description?: string, 9 | public _id?: string, // _id is present if editing or returning from DB 10 | ) { } 11 | } 12 | 13 | class FormEventModel { 14 | constructor( 15 | public title: string, 16 | public location: string, 17 | public startDate: string, 18 | public startTime: string, 19 | public endDate: string, 20 | public endTime: string, 21 | public viewPublic: boolean, 22 | public description?: string 23 | ) { } 24 | } 25 | 26 | export { EventModel, FormEventModel }; 27 | -------------------------------------------------------------------------------- /src/app/core/models/rsvp.model.ts: -------------------------------------------------------------------------------- 1 | export class RsvpModel { 2 | constructor( 3 | public userId: string, 4 | public name: string, 5 | public eventId: string, 6 | public attending: boolean, 7 | public guests?: number, 8 | public comments?: string, 9 | public _id?: string, // _id is present if editing or returning from DB 10 | ) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/core/utils.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DatePipe } from '@angular/common'; 3 | 4 | @Injectable() 5 | export class UtilsService { 6 | 7 | constructor(private datePipe: DatePipe) { } 8 | 9 | isLoaded(loading: boolean): boolean { 10 | return loading === false; 11 | } 12 | 13 | eventDates(start, end): string { 14 | // Display single-day events as "Jan 7, 2018" 15 | // Display multi-day events as "Aug 12, 2017 - Aug 13, 2017" 16 | const startDate = this.datePipe.transform(start, 'mediumDate'); 17 | const endDate = this.datePipe.transform(end, 'mediumDate'); 18 | 19 | if (startDate === endDate) { 20 | return startDate; 21 | } else { 22 | return `${startDate} - ${endDate}`; 23 | } 24 | } 25 | 26 | eventDatesTimes(start, end): string { 27 | // Display single-day events as "1/7/2018, 5:30 PM - 7:30 PM" 28 | // Display multi-day events as "8/12/2017, 8:00 PM - 8/13/2017, 10:00 AM" 29 | const _shortDate = 'M/d/yyyy'; 30 | const startDate = this.datePipe.transform(start, _shortDate); 31 | const startTime = this.datePipe.transform(start, 'shortTime'); 32 | const endDate = this.datePipe.transform(end, _shortDate); 33 | const endTime = this.datePipe.transform(end, 'shortTime'); 34 | 35 | if (startDate === endDate) { 36 | return `${startDate}, ${startTime} - ${endTime}`; 37 | } else { 38 | return `${startDate}, ${startTime} - ${endDate}, ${endTime}`; 39 | } 40 | } 41 | 42 | eventPast(eventEnd): boolean { 43 | // Check if event has already ended 44 | const now = new Date(); 45 | const then = new Date(eventEnd.toString()); 46 | return now >= then; 47 | } 48 | 49 | tabIs(currentTab: string, tab: string): boolean { 50 | // Check if current tab is tab name 51 | return currentTab === tab; 52 | } 53 | 54 | displayCount(guests: number): string { 55 | // Example usage: 56 | // {{displayCount(guests)}} attending this event 57 | const persons = guests === 1 ? ' person' : ' people'; 58 | return guests + persons; 59 | } 60 | 61 | showPlusOnes(guests: number): string { 62 | // If bringing additional guest(s), show as "+n" 63 | if (guests) { 64 | return `+${guests}`; 65 | } 66 | } 67 | 68 | booleanToText(bool: boolean): string { 69 | // Change a boolean to 'Yes' or 'No' string 70 | return bool ? 'Yes' : 'No'; 71 | } 72 | 73 | capitalize(str: string): string { 74 | // Capitalize first letter of string 75 | return str.charAt(0).toUpperCase() + str.slice(1); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |

2 | MIT 2018 3 |

4 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | FOOTER 3 | --------------------*/ 4 | 5 | :host { 6 | display: block; 7 | padding-bottom: 10px; 8 | } 9 | p { 10 | font-size: 12px; 11 | margin-bottom: 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /src/app/header/header.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | HEADER 3 | --------------------*/ 4 | 5 | @import '../../assets/scss/partials/layout.vars'; 6 | 7 | /*-- Navigation --*/ 8 | 9 | .nav { 10 | background: #eee; 11 | backface-visibility: hidden; 12 | -webkit-backface-visibility: hidden; 13 | box-shadow: inset -8px 0 8px -6px rgba(0,0,0,0.2); 14 | display: none; /* deal with FOUC */ 15 | height: 100%; 16 | overflow-y: auto; 17 | padding: $padding-screen-small; 18 | position: absolute; 19 | top: 0; 20 | transform: translate3d(-100%,0,0); 21 | width: 270px; 22 | 23 | :host-context(.nav-closed) &, 24 | :host-context(.nav-open) & { 25 | display: block; /* deal with FOUC */ 26 | } 27 | .active { 28 | font-weight: bold; 29 | } 30 | &-list { 31 | list-style: none; 32 | margin-bottom: 0; 33 | padding-left: 0; 34 | 35 | a { 36 | display: block; 37 | padding: 6px; 38 | 39 | &:hover, 40 | &:active, 41 | &:focus { 42 | text-decoration: none; 43 | } 44 | } 45 | } 46 | } 47 | 48 | /*-- Hamburger toggle --*/ 49 | 50 | .toggle-offcanvas { 51 | border-right: 1px solid rgba(255,255,255,.5); 52 | display: inline-block; 53 | height: 50px; 54 | padding: 23.5px 13px; 55 | position: relative; 56 | text-align: center; 57 | width: 50px; 58 | z-index: 100; 59 | 60 | span, 61 | span:before, 62 | span:after { 63 | background: #fff; 64 | border-radius: 1px; 65 | content: ''; 66 | display: block; 67 | height: 3px; 68 | position: absolute; 69 | transition: all 250ms ease-in-out; 70 | width: 24px; 71 | } 72 | span { 73 | &:before { 74 | top: -9px; 75 | } 76 | &:after { 77 | bottom: -9px; 78 | } 79 | } 80 | :host-context(.nav-open) & { 81 | span { 82 | background-color: transparent; 83 | 84 | &:before, 85 | &:after { 86 | top: 0; 87 | } 88 | &:before { 89 | transform: rotate(45deg); 90 | } 91 | &:after { 92 | transform: rotate(-45deg); 93 | } 94 | } 95 | } 96 | } 97 | 98 | /*-- Header and title --*/ 99 | 100 | .header-page { 101 | color: #fff; 102 | height: 50px; 103 | margin-bottom: 10px; 104 | position: relative; 105 | 106 | &-siteTitle { 107 | font-size: 30px; 108 | line-height: 50px; 109 | margin: 0; 110 | padding: 0 0 0 60px; 111 | position: absolute; 112 | top: 0; 113 | width: 100%; 114 | } 115 | a { 116 | color: #fff; 117 | text-decoration: none; 118 | } 119 | &-authStatus { 120 | color: #fff; 121 | font-size: 12px; 122 | line-height: 50px; 123 | padding: 0 10px; 124 | position: absolute; 125 | right: 0; top: 0; 126 | 127 | a:hover { 128 | text-decoration: underline; 129 | } 130 | .divider { 131 | display: inline-block; 132 | opacity: .5; 133 | padding: 0 4px; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter } from '@angular/core'; 2 | import { Router, NavigationStart } from '@angular/router'; 3 | import { AuthService } from './../auth/auth.service'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'] 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | @Output() navToggled = new EventEmitter(); 13 | navOpen = false; 14 | 15 | constructor( 16 | private router: Router, 17 | public auth: AuthService 18 | ) { } 19 | 20 | ngOnInit() { 21 | // If nav is open after routing, close it 22 | this.router.events 23 | .pipe( 24 | filter(event => event instanceof NavigationStart && this.navOpen) 25 | ) 26 | .subscribe(event => this.toggleNav()); 27 | } 28 | 29 | toggleNav() { 30 | this.navOpen = !this.navOpen; 31 | this.navToggled.emit(this.navOpen); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |

{{ pageTitle }}

2 | 3 | 4 | 5 |

Welcome, {{ auth.userProfile?.name }}! You can create and administer events below.

6 | 7 |

8 | + Create New Event 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 35 | 36 | 37 |

38 | No events found for {{ query }}, sorry! 39 |

40 | 41 | 42 |
43 |
46 |
47 | 48 |
49 |
50 |
51 | 56 | 61 |
62 |
63 |

64 | Date: {{ utils.eventDates(event.startDatetime, event.endDatetime) }} 65 |

66 |

67 | Edit 70 | Delete 74 |

75 |
76 |
77 |
78 | 79 | 80 |

81 | No events have been created yet. 82 |

83 |
84 | 85 | 86 |

87 | Oops! There was an error retrieving event data. 88 |

89 | 90 |
91 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | ADMIN COMPONENT 3 | --------------------*/ 4 | 5 | .event-icon { 6 | display: inline-block; 7 | height: 16px; 8 | margin: 0 4px; 9 | width: 16px; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { AuthService } from './../../auth/auth.service'; 4 | import { ApiService } from './../../core/api.service'; 5 | import { UtilsService } from './../../core/utils.service'; 6 | import { FilterSortService } from './../../core/filter-sort.service'; 7 | import { Subscription } from 'rxjs'; 8 | import { EventModel } from './../../core/models/event.model'; 9 | 10 | @Component({ 11 | selector: 'app-admin', 12 | templateUrl: './admin.component.html', 13 | styleUrls: ['./admin.component.scss'] 14 | }) 15 | export class AdminComponent implements OnInit, OnDestroy { 16 | pageTitle = 'Admin'; 17 | loggedInSub: Subscription; 18 | eventsSub: Subscription; 19 | eventList: EventModel[]; 20 | filteredEvents: EventModel[]; 21 | loading: boolean; 22 | error: boolean; 23 | query = ''; 24 | 25 | constructor( 26 | private title: Title, 27 | public auth: AuthService, 28 | private api: ApiService, 29 | public utils: UtilsService, 30 | public fs: FilterSortService 31 | ) { } 32 | 33 | ngOnInit() { 34 | this.title.setTitle(this.pageTitle); 35 | this._getEventList(); 36 | } 37 | 38 | private _getEventList() { 39 | // Get all (admin) events 40 | this.eventsSub = this.api 41 | .getAdminEvents$() 42 | .subscribe( 43 | res => { 44 | this.eventList = res; 45 | this.filteredEvents = res; 46 | this.loading = false; 47 | }, 48 | err => { 49 | console.error(err); 50 | this.loading = false; 51 | this.error = true; 52 | } 53 | ); 54 | } 55 | 56 | searchEvents() { 57 | this.filteredEvents = this.fs.search(this.eventList, this.query, '_id', 'mediumDate'); 58 | } 59 | 60 | resetQuery() { 61 | this.query = ''; 62 | this.filteredEvents = this.eventList; 63 | } 64 | 65 | ngOnDestroy() { 66 | this.eventsSub.unsubscribe(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CoreModule } from './../../core/core.module'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ADMIN_ROUTES } from './admin.routes'; 6 | import { AdminComponent } from './admin.component'; 7 | import { CreateEventComponent } from './create-event/create-event.component'; 8 | import { UpdateEventComponent } from './update-event/update-event.component'; 9 | import { EventFormComponent } from './event-form/event-form.component'; 10 | import { DeleteEventComponent } from './update-event/delete-event/delete-event.component'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | CoreModule, 16 | RouterModule.forChild(ADMIN_ROUTES) 17 | ], 18 | declarations: [ 19 | AdminComponent, 20 | CreateEventComponent, 21 | UpdateEventComponent, 22 | EventFormComponent, 23 | DeleteEventComponent 24 | ] 25 | }) 26 | export class AdminModule { } 27 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { AdminComponent } from './admin.component'; 3 | import { CreateEventComponent } from './create-event/create-event.component'; 4 | import { UpdateEventComponent } from './update-event/update-event.component'; 5 | 6 | export const ADMIN_ROUTES: Routes = [ 7 | { 8 | path: '', 9 | component: AdminComponent, 10 | }, 11 | { 12 | path: 'event/new', 13 | component: CreateEventComponent 14 | }, 15 | { 16 | path: 'event/update/:id', 17 | component: UpdateEventComponent 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/pages/admin/create-event/create-event.component.html: -------------------------------------------------------------------------------- 1 |

{{ pageTitle }}

2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/pages/admin/create-event/create-event.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/admin/create-event/create-event.component.scss -------------------------------------------------------------------------------- /src/app/pages/admin/create-event/create-event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | 4 | @Component({ 5 | selector: 'app-create-event', 6 | templateUrl: './create-event.component.html', 7 | styleUrls: ['./create-event.component.scss'] 8 | }) 9 | export class CreateEventComponent implements OnInit { 10 | pageTitle = 'Create Event'; 11 | 12 | constructor(private title: Title) { } 13 | 14 | ngOnInit() { 15 | this.title.setTitle(this.pageTitle); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/pages/admin/event-form/event-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 11 |
15 |
16 |
17 | 18 | 19 |
20 | 21 | 27 |
31 |
32 |
33 | 34 |
37 |
38 | 39 |
40 | 41 | 48 |
52 |
53 |
54 | 55 | 56 |
57 | 58 | 65 |
69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 | 84 |
88 |
89 |
90 | 91 | 92 |
93 | 94 | 101 |
105 |
106 |
107 |
108 | 109 |

110 | Dates/times out of range: Events cannot end before they begin. Please double-check the start and end dates and times. 111 |

112 |
113 | 114 | 115 |
116 | 117 |
118 | 126 |
127 |
128 | 136 |
137 |
141 |
142 |
143 | 144 | 145 |
146 | 147 | 153 |
157 |
158 |
159 | 160 | 161 |
162 | 167 | 168 | 169 | Reset Form 174 | 175 | 176 |

177 | Error: There was a problem submitting the event. Please try again. 178 |

179 |
180 |
181 | -------------------------------------------------------------------------------- /src/app/pages/admin/event-form/event-form.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | EVENT FORM COMPONENT 3 | --------------------*/ 4 | 5 | :host { 6 | display: block; 7 | } 8 | .label-inline-group { 9 | display: block; 10 | } 11 | .has-error { 12 | border: 1px solid red; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/admin/event-form/event-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators, AbstractControl } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { Subscription } from 'rxjs'; 5 | import { ApiService } from './../../../core/api.service'; 6 | import { EventModel, FormEventModel } from './../../../core/models/event.model'; 7 | import { DatePipe } from '@angular/common'; 8 | import { dateValidator } from './../../../core/forms/date.validator'; 9 | import { dateRangeValidator } from './../../../core/forms/date-range.validator'; 10 | import { DATE_REGEX, TIME_REGEX, stringsToDate } from './../../../core/forms/formUtils.factory'; 11 | import { EventFormService } from './event-form.service'; 12 | 13 | @Component({ 14 | selector: 'app-event-form', 15 | templateUrl: './event-form.component.html', 16 | styleUrls: ['./event-form.component.scss'], 17 | providers: [ EventFormService ] 18 | }) 19 | export class EventFormComponent implements OnInit, OnDestroy { 20 | @Input() event: EventModel; 21 | isEdit: boolean; 22 | // FormBuilder form 23 | eventForm: FormGroup; 24 | datesGroup: AbstractControl; 25 | // Model storing initial form values 26 | formEvent: FormEventModel; 27 | // Form validation and disabled logic 28 | formErrors: any; 29 | formChangeSub: Subscription; 30 | // Form submission 31 | submitEventObj: EventModel; 32 | submitEventSub: Subscription; 33 | error: boolean; 34 | submitting: boolean; 35 | submitBtnText: string; 36 | 37 | constructor( 38 | private fb: FormBuilder, 39 | private api: ApiService, 40 | private datePipe: DatePipe, 41 | public ef: EventFormService, 42 | private router: Router 43 | ) { } 44 | 45 | ngOnInit() { 46 | this.formErrors = this.ef.formErrors; 47 | this.isEdit = !!this.event; 48 | this.submitBtnText = this.isEdit ? 'Update Event' : 'Create Event'; 49 | // Set initial form data 50 | this.formEvent = this._setFormEvent(); 51 | // Use FormBuilder to construct the form 52 | this._buildForm(); 53 | } 54 | 55 | private _setFormEvent() { 56 | if (!this.isEdit) { 57 | // If creating a new event, create new 58 | // FormEventModel with default null data 59 | return new FormEventModel(null, null, null, null, null, null, null); 60 | } else { 61 | // If editing existing event, create new 62 | // FormEventModel from existing data 63 | // Transform datetimes: 64 | // https://angular.io/api/common/DatePipe 65 | // _shortDate: 1/7/2017 66 | // 'shortTime': 12:05 PM 67 | const _shortDate = 'M/d/yyyy'; 68 | return new FormEventModel( 69 | this.event.title, 70 | this.event.location, 71 | this.datePipe.transform(this.event.startDatetime, _shortDate), 72 | this.datePipe.transform(this.event.startDatetime, 'shortTime'), 73 | this.datePipe.transform(this.event.endDatetime, _shortDate), 74 | this.datePipe.transform(this.event.endDatetime, 'shortTime'), 75 | this.event.viewPublic, 76 | this.event.description 77 | ); 78 | } 79 | } 80 | 81 | private _buildForm() { 82 | this.eventForm = this.fb.group({ 83 | title: [this.formEvent.title, [ 84 | Validators.required, 85 | Validators.minLength(this.ef.textMin), 86 | Validators.maxLength(this.ef.titleMax) 87 | ]], 88 | location: [this.formEvent.location, [ 89 | Validators.required, 90 | Validators.minLength(this.ef.textMin), 91 | Validators.maxLength(this.ef.locMax) 92 | ]], 93 | viewPublic: [this.formEvent.viewPublic, 94 | Validators.required 95 | ], 96 | description: [this.formEvent.description, 97 | Validators.maxLength(this.ef.descMax) 98 | ], 99 | datesGroup: this.fb.group({ 100 | startDate: [this.formEvent.startDate, [ 101 | Validators.required, 102 | Validators.maxLength(this.ef.dateMax), 103 | Validators.pattern(DATE_REGEX), 104 | dateValidator() 105 | ]], 106 | startTime: [this.formEvent.startTime, [ 107 | Validators.required, 108 | Validators.maxLength(this.ef.timeMax), 109 | Validators.pattern(TIME_REGEX) 110 | ]], 111 | endDate: [this.formEvent.endDate, [ 112 | Validators.required, 113 | Validators.maxLength(this.ef.dateMax), 114 | Validators.pattern(DATE_REGEX), 115 | dateValidator() 116 | ]], 117 | endTime: [this.formEvent.endTime, [ 118 | Validators.required, 119 | Validators.maxLength(this.ef.timeMax), 120 | Validators.pattern(TIME_REGEX) 121 | ]] 122 | }, { validator: dateRangeValidator }) 123 | }); 124 | // Set local property to eventForm datesGroup control 125 | this.datesGroup = this.eventForm.get('datesGroup'); 126 | 127 | // Subscribe to form value changes 128 | this.formChangeSub = this.eventForm 129 | .valueChanges 130 | .subscribe(data => this._onValueChanged()); 131 | 132 | // If edit: mark fields dirty to trigger immediate 133 | // validation in case editing an event that is no 134 | // longer valid (for example, an event in the past) 135 | if (this.isEdit) { 136 | const _markDirty = group => { 137 | for (const i in group.controls) { 138 | if (group.controls.hasOwnProperty(i)) { 139 | group.controls[i].markAsDirty(); 140 | } 141 | } 142 | }; 143 | _markDirty(this.eventForm); 144 | _markDirty(this.datesGroup); 145 | } 146 | 147 | this._onValueChanged(); 148 | } 149 | 150 | private _onValueChanged() { 151 | if (!this.eventForm) { return; } 152 | const _setErrMsgs = (control: AbstractControl, errorsObj: any, field: string) => { 153 | if (control && control.dirty && control.invalid) { 154 | const messages = this.ef.validationMessages[field]; 155 | for (const key in control.errors) { 156 | if (control.errors.hasOwnProperty(key)) { 157 | errorsObj[field] += messages[key] + '
'; 158 | } 159 | } 160 | } 161 | }; 162 | 163 | // Check validation and set errors 164 | for (const field in this.formErrors) { 165 | if (this.formErrors.hasOwnProperty(field)) { 166 | if (field !== 'datesGroup') { 167 | // Set errors for fields not inside datesGroup 168 | // Clear previous error message (if any) 169 | this.formErrors[field] = ''; 170 | _setErrMsgs(this.eventForm.get(field), this.formErrors, field); 171 | } else { 172 | // Set errors for fields inside datesGroup 173 | const datesGroupErrors = this.formErrors['datesGroup']; 174 | for (const dateField in datesGroupErrors) { 175 | if (datesGroupErrors.hasOwnProperty(dateField)) { 176 | // Clear previous error message (if any) 177 | datesGroupErrors[dateField] = ''; 178 | _setErrMsgs(this.datesGroup.get(dateField), datesGroupErrors, dateField); 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | private _getSubmitObj() { 187 | const startDate = this.datesGroup.get('startDate').value; 188 | const startTime = this.datesGroup.get('startTime').value; 189 | const endDate = this.datesGroup.get('endDate').value; 190 | const endTime = this.datesGroup.get('endTime').value; 191 | // Convert form startDate/startTime and endDate/endTime 192 | // to JS dates and populate a new EventModel for submission 193 | return new EventModel( 194 | this.eventForm.get('title').value, 195 | this.eventForm.get('location').value, 196 | stringsToDate(startDate, startTime), 197 | stringsToDate(endDate, endTime), 198 | this.eventForm.get('viewPublic').value, 199 | this.eventForm.get('description').value, 200 | this.event ? this.event._id : null 201 | ); 202 | } 203 | 204 | onSubmit() { 205 | this.submitting = true; 206 | this.submitEventObj = this._getSubmitObj(); 207 | 208 | if (!this.isEdit) { 209 | this.submitEventSub = this.api 210 | .postEvent$(this.submitEventObj) 211 | .subscribe( 212 | data => this._handleSubmitSuccess(data), 213 | err => this._handleSubmitError(err) 214 | ); 215 | } else { 216 | this.submitEventSub = this.api 217 | .editEvent$(this.event._id, this.submitEventObj) 218 | .subscribe( 219 | data => this._handleSubmitSuccess(data), 220 | err => this._handleSubmitError(err) 221 | ); 222 | } 223 | } 224 | 225 | private _handleSubmitSuccess(res) { 226 | this.error = false; 227 | this.submitting = false; 228 | // Redirect to event detail 229 | this.router.navigate(['/event', res._id]); 230 | } 231 | 232 | private _handleSubmitError(err) { 233 | console.error(err); 234 | this.submitting = false; 235 | this.error = true; 236 | } 237 | 238 | resetForm() { 239 | this.eventForm.reset(); 240 | } 241 | 242 | ngOnDestroy() { 243 | if (this.submitEventSub) { 244 | this.submitEventSub.unsubscribe(); 245 | } 246 | this.formChangeSub.unsubscribe(); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /src/app/pages/admin/event-form/event-form.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class EventFormService { 5 | validationMessages: any; 6 | // Set up errors object 7 | formErrors = { 8 | title: '', 9 | location: '', 10 | viewPublic: '', 11 | description: '', 12 | datesGroup: { 13 | startDate: '', 14 | startTime: '', 15 | endDate: '', 16 | endTime: '', 17 | } 18 | }; 19 | // Min/maxlength validation 20 | textMin = 3; 21 | titleMax = 36; 22 | locMax = 200; 23 | dateMax = 10; 24 | timeMax = 8; 25 | descMax = 2000; 26 | // Formats 27 | dateFormat = 'm/d/yyyy'; 28 | timeFormat = 'h:mm AM/PM'; 29 | 30 | constructor() { 31 | this.validationMessages = { 32 | title: { 33 | required: `Title is required.`, 34 | minlength: `Title must be ${this.textMin} characters or more.`, 35 | maxlength: `Title must be ${this.titleMax} characters or less.` 36 | }, 37 | location: { 38 | required: `Location is required.`, 39 | minlength: `Location must be ${this.textMin} characters or more.`, 40 | maxlength: `Location must be ${this.locMax} characters or less.` 41 | }, 42 | startDate: { 43 | required: `Start date is required.`, 44 | maxlength: `Start date cannot be longer than ${this.dateMax} characters.`, 45 | pattern: `Start date must be in the format ${this.dateFormat}.`, 46 | date: `Start date must be a valid date at least one day in the future.` 47 | }, 48 | startTime: { 49 | required: `Start time is required.`, 50 | pattern: `Start time must be a valid time in the format ${this.timeFormat}.`, 51 | maxlength: `Start time must be ${this.timeMax} characters or less.` 52 | }, 53 | endDate: { 54 | required: `End date is required.`, 55 | maxlength: `End date cannot be longer than ${this.dateMax} characters.`, 56 | pattern: `End date must be in the format ${this.dateFormat}.`, 57 | date: `End date must be a valid date at least one day in the future.` 58 | }, 59 | endTime: { 60 | required: `End time is required.`, 61 | pattern: `End time must be a valid time in the format ${this.timeFormat}.`, 62 | maxlength: `End time must be ${this.timeMax} characters or less.` 63 | }, 64 | viewPublic: { 65 | required: `You must specify whether this event should be publicly listed.` 66 | }, 67 | description: { 68 | maxlength: `Description must be ${this.descMax} characters or less.` 69 | } 70 | }; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/delete-event/delete-event.component.html: -------------------------------------------------------------------------------- 1 |

2 | You are deleting the "" event. 3 |

4 | 5 |

6 | Deleting this event will also remove all associated RSVPs. Please proceed with caution! 7 |

8 | 9 |
10 | 11 | 16 |
17 | 18 | 19 |

20 | 24 | 25 |

26 | 27 | 28 |

29 | Oops! There was an error deleting this event. Please try again. 30 |

31 | -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/delete-event/delete-event.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/admin/update-event/delete-event/delete-event.component.scss -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/delete-event/delete-event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, Input } from '@angular/core'; 2 | import { EventModel } from './../../../../core/models/event.model'; 3 | import { Subscription } from 'rxjs'; 4 | import { ApiService } from './../../../../core/api.service'; 5 | import { Router } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-delete-event', 9 | templateUrl: './delete-event.component.html', 10 | styleUrls: ['./delete-event.component.scss'] 11 | }) 12 | export class DeleteEventComponent implements OnDestroy { 13 | @Input() event: EventModel; 14 | confirmDelete: string; 15 | deleteSub: Subscription; 16 | submitting: boolean; 17 | error: boolean; 18 | 19 | constructor( 20 | private api: ApiService, 21 | private router: Router 22 | ) { } 23 | 24 | removeEvent() { 25 | this.submitting = true; 26 | // DELETE event by ID 27 | this.deleteSub = this.api 28 | .deleteEvent$(this.event._id) 29 | .subscribe( 30 | res => { 31 | this.submitting = false; 32 | this.error = false; 33 | console.log(res.message); 34 | // If successfully deleted event, redirect to Admin 35 | this.router.navigate(['/admin']); 36 | }, 37 | err => { 38 | console.error(err); 39 | this.submitting = false; 40 | this.error = true; 41 | } 42 | ); 43 | } 44 | 45 | ngOnDestroy() { 46 | if (this.deleteSub) { 47 | this.deleteSub.unsubscribe(); 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/update-event.component.html: -------------------------------------------------------------------------------- 1 |

{{ pageTitle }}

2 | 3 | 4 | 5 | 6 |
7 |
8 | 24 |
25 | 26 |
27 | 28 | 31 | 32 | 33 | 36 |
37 | 38 |
39 | 40 | 41 |

42 | Error: Event data could not be retrieved. View Admin Events. 43 |

44 |
45 | -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/update-event.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/admin/update-event/update-event.component.scss -------------------------------------------------------------------------------- /src/app/pages/admin/update-event/update-event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { AuthService } from './../../../auth/auth.service'; 4 | import { ApiService } from './../../../core/api.service'; 5 | import { UtilsService } from './../../../core/utils.service'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { Subscription } from 'rxjs'; 8 | import { EventModel } from './../../../core/models/event.model'; 9 | 10 | @Component({ 11 | selector: 'app-update-event', 12 | templateUrl: './update-event.component.html', 13 | styleUrls: ['./update-event.component.scss'] 14 | }) 15 | export class UpdateEventComponent implements OnInit, OnDestroy { 16 | pageTitle = 'Update Event'; 17 | routeSub: Subscription; 18 | eventSub: Subscription; 19 | event: EventModel; 20 | loading: boolean; 21 | submitting: boolean; 22 | error: boolean; 23 | tabSub: Subscription; 24 | tab: string; 25 | private _id: string; 26 | 27 | constructor( 28 | private route: ActivatedRoute, 29 | public auth: AuthService, 30 | private api: ApiService, 31 | public utils: UtilsService, 32 | private title: Title 33 | ) { } 34 | 35 | ngOnInit() { 36 | this.title.setTitle(this.pageTitle); 37 | 38 | // Set event ID from route params and subscribe 39 | this.routeSub = this.route.params 40 | .subscribe(params => { 41 | this._id = params['id']; 42 | this._getEvent(); 43 | }); 44 | 45 | // Subscribe to query params to watch for tab changes 46 | this.tabSub = this.route.queryParams 47 | .subscribe(queryParams => { 48 | this.tab = queryParams['tab'] || 'edit'; 49 | }); 50 | } 51 | 52 | private _getEvent() { 53 | this.loading = true; 54 | // GET event by ID 55 | this.eventSub = this.api 56 | .getEventById$(this._id) 57 | .subscribe( 58 | res => { 59 | this.event = res; 60 | this.loading = false; 61 | }, 62 | err => { 63 | console.error(err); 64 | this.loading = false; 65 | this.error = true; 66 | } 67 | ); 68 | } 69 | 70 | ngOnDestroy() { 71 | this.routeSub.unsubscribe(); 72 | this.tabSub.unsubscribe(); 73 | this.eventSub.unsubscribe(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/app/pages/callback/callback.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/pages/callback/callback.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/callback/callback.component.scss -------------------------------------------------------------------------------- /src/app/pages/callback/callback.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../auth/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-callback', 6 | templateUrl: './callback.component.html', 7 | styleUrls: ['./callback.component.scss'] 8 | }) 9 | export class CallbackComponent implements OnInit { 10 | 11 | constructor(private auth: AuthService) { 12 | // Check for authentication and handle if hash present 13 | auth.handleAuth(); 14 | } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/pages/event/event-detail/event-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Event Details

3 |
4 | 5 |
    6 |
  • 7 | When:{{ utils.eventDatesTimes(event.startDatetime, event.endDatetime) }} 8 |
  • 9 |
  • 10 | Where:{{ event.location }} (get directions) 11 |
  • 12 |
13 | 14 |
15 |

16 |
17 | 18 | 21 | -------------------------------------------------------------------------------- /src/app/pages/event/event-detail/event-detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/event/event-detail/event-detail.component.scss -------------------------------------------------------------------------------- /src/app/pages/event/event-detail/event-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { AuthService } from './../../../auth/auth.service'; 3 | import { UtilsService } from './../../../core/utils.service'; 4 | import { EventModel } from './../../../core/models/event.model'; 5 | 6 | @Component({ 7 | selector: 'app-event-detail', 8 | templateUrl: './event-detail.component.html', 9 | styleUrls: ['./event-detail.component.scss'] 10 | }) 11 | export class EventDetailComponent { 12 | @Input() event: EventModel; 13 | 14 | constructor( 15 | public utils: UtilsService, 16 | public auth: AuthService 17 | ) { } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/pages/event/event.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{ pageTitle }}

5 | 6 | 7 | 8 |

9 | This event is over. 10 |

11 | 12 |
13 | 14 |
15 | 31 |
32 | 33 | 34 | 37 | 38 | 39 | 43 |
44 |
45 | 46 | 47 |

48 | Oops! There was an error retrieving information for this event. 49 |

50 |
51 | -------------------------------------------------------------------------------- /src/app/pages/event/event.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/pages/event/event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { AuthService } from './../../auth/auth.service'; 4 | import { ApiService } from './../../core/api.service'; 5 | import { UtilsService } from './../../core/utils.service'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { Subscription } from 'rxjs'; 8 | import { EventModel } from './../../core/models/event.model'; 9 | 10 | @Component({ 11 | selector: 'app-event', 12 | templateUrl: './event.component.html', 13 | styleUrls: ['./event.component.scss'] 14 | }) 15 | export class EventComponent implements OnInit, OnDestroy { 16 | pageTitle: string; 17 | id: string; 18 | loggedInSub: Subscription; 19 | routeSub: Subscription; 20 | tabSub: Subscription; 21 | eventSub: Subscription; 22 | event: EventModel; 23 | loading: boolean; 24 | error: boolean; 25 | tab: string; 26 | eventPast: boolean; 27 | 28 | constructor( 29 | private route: ActivatedRoute, 30 | public auth: AuthService, 31 | private api: ApiService, 32 | public utils: UtilsService, 33 | private title: Title 34 | ) { } 35 | 36 | ngOnInit() { 37 | this.loggedInSub = this.auth.loggedIn$.subscribe( 38 | loggedIn => { 39 | this.loading = true; 40 | if (loggedIn) { 41 | this._routeSubs(); 42 | } 43 | } 44 | ); 45 | } 46 | 47 | private _routeSubs() { 48 | // Set event ID from route params and subscribe 49 | this.routeSub = this.route.params 50 | .subscribe(params => { 51 | this.id = params['id']; 52 | this._getEvent(); 53 | }); 54 | 55 | // Subscribe to query params to watch for tab changes 56 | this.tabSub = this.route.queryParams 57 | .subscribe(queryParams => { 58 | this.tab = queryParams['tab'] || 'details'; 59 | }); 60 | } 61 | 62 | private _getEvent() { 63 | // GET event by ID 64 | this.eventSub = this.api 65 | .getEventById$(this.id) 66 | .subscribe( 67 | res => { 68 | this.event = res; 69 | this._setPageTitle(this.event.title); 70 | this.loading = false; 71 | this.eventPast = this.utils.eventPast(this.event.endDatetime); 72 | }, 73 | err => { 74 | console.error(err); 75 | this.loading = false; 76 | this.error = true; 77 | this._setPageTitle('Event Details'); 78 | } 79 | ); 80 | } 81 | 82 | private _setPageTitle(title: string) { 83 | this.pageTitle = title; 84 | this.title.setTitle(title); 85 | } 86 | 87 | ngOnDestroy() { 88 | this.routeSub.unsubscribe(); 89 | this.tabSub.unsubscribe(); 90 | this.eventSub.unsubscribe(); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/app/pages/event/event.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CoreModule } from './../../core/core.module'; 4 | import { RouterModule } from '@angular/router'; 5 | import { EVENT_ROUTES } from './event.routes'; 6 | import { EventComponent } from './event.component'; 7 | import { EventDetailComponent } from './event-detail/event-detail.component'; 8 | import { RsvpComponent } from './rsvp/rsvp.component'; 9 | import { RsvpFormComponent } from './rsvp/rsvp-form/rsvp-form.component'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | CoreModule, 15 | RouterModule.forChild(EVENT_ROUTES) 16 | ], 17 | declarations: [ 18 | EventComponent, 19 | EventDetailComponent, 20 | RsvpComponent, 21 | RsvpFormComponent 22 | ] 23 | }) 24 | export class EventModule { } 25 | -------------------------------------------------------------------------------- /src/app/pages/event/event.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { EventComponent } from './event.component'; 3 | 4 | export const EVENT_ROUTES: Routes = [ 5 | { 6 | path: '', 7 | component: EventComponent 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp-form/rsvp-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 15 |
18 |
19 | Name is required. 20 |
21 |
22 | Name must be 3 characters or more. 23 |
24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 | 42 |
43 |
44 | 55 |
56 |
57 | 58 | 59 |
60 | 61 | 73 |
76 |
77 | Additional Guests must be an integer from 0-9. 78 |
79 |
80 |
81 | 82 | 83 |
84 | 85 | 92 |
93 | 94 | 95 |
96 | 100 | 101 | 102 | 103 |

104 | Error: There was a problem submitting your response. Please try again. 105 |

106 |
107 |
108 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp-form/rsvp-form.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | RSVP FORM COMPONENT 3 | --------------------*/ 4 | 5 | :host { 6 | display: block; 7 | margin-top: 20px; 8 | } 9 | .formGuests { 10 | &.row { 11 | margin-left: 0; 12 | margin-right: 0; 13 | } 14 | label, 15 | .formErrors.col-12 { 16 | padding-left: 0; 17 | padding-right: 0; 18 | 19 | } 20 | } 21 | .label-inline-group { 22 | display: block; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp-form/rsvp-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; 2 | import { AuthService } from './../../../../auth/auth.service'; 3 | import { Subscription } from 'rxjs'; 4 | import { ApiService } from './../../../../core/api.service'; 5 | import { RsvpModel } from './../../../../core/models/rsvp.model'; 6 | import { GUESTS_REGEX } from './../../../../core/forms/formUtils.factory'; 7 | 8 | @Component({ 9 | selector: 'app-rsvp-form', 10 | templateUrl: './rsvp-form.component.html', 11 | styleUrls: ['./rsvp-form.component.scss'] 12 | }) 13 | export class RsvpFormComponent implements OnInit, OnDestroy { 14 | @Input() eventId: string; 15 | @Input() rsvp: RsvpModel; 16 | @Output() submitRsvp = new EventEmitter(); 17 | GUESTS_REGEX = GUESTS_REGEX; 18 | isEdit: boolean; 19 | formRsvp: RsvpModel; 20 | submitRsvpSub: Subscription; 21 | submitting: boolean; 22 | error: boolean; 23 | 24 | constructor( 25 | private auth: AuthService, 26 | private api: ApiService 27 | ) { } 28 | 29 | ngOnInit() { 30 | this.isEdit = !!this.rsvp; 31 | this._setFormRsvp(); 32 | } 33 | 34 | private _setFormRsvp() { 35 | if (!this.isEdit) { 36 | // If creating a new RSVP, 37 | // create new RsvpModel with default data 38 | this.formRsvp = new RsvpModel( 39 | this.auth.userProfile.sub, 40 | this.auth.userProfile.name, 41 | this.eventId, 42 | null, 43 | 0); 44 | } else { 45 | // If editing an existing RSVP, 46 | // create new RsvpModel from existing data 47 | this.formRsvp = new RsvpModel( 48 | this.rsvp.userId, 49 | this.rsvp.name, 50 | this.rsvp.eventId, 51 | this.rsvp.attending, 52 | this.rsvp.guests, 53 | this.rsvp.comments, 54 | this.rsvp._id 55 | ); 56 | } 57 | } 58 | 59 | changeAttendanceSetGuests() { 60 | // If attendance changed to no, set guests: 0 61 | if (!this.formRsvp.attending) { 62 | this.formRsvp.guests = 0; 63 | } 64 | } 65 | 66 | onSubmit() { 67 | this.submitting = true; 68 | if (!this.isEdit) { 69 | this.submitRsvpSub = this.api 70 | .postRsvp$(this.formRsvp) 71 | .subscribe( 72 | data => this._handleSubmitSuccess(data), 73 | err => this._handleSubmitError(err) 74 | ); 75 | } else { 76 | this.submitRsvpSub = this.api 77 | .editRsvp$(this.rsvp._id, this.formRsvp) 78 | .subscribe( 79 | data => this._handleSubmitSuccess(data), 80 | err => this._handleSubmitError(err) 81 | ); 82 | } 83 | } 84 | 85 | private _handleSubmitSuccess(res) { 86 | const eventObj = { 87 | isEdit: this.isEdit, 88 | rsvp: res 89 | }; 90 | this.submitRsvp.emit(eventObj); 91 | this.error = false; 92 | this.submitting = false; 93 | } 94 | 95 | private _handleSubmitError(err) { 96 | const eventObj = { 97 | isEdit: this.isEdit, 98 | error: err 99 | }; 100 | this.submitRsvp.emit(eventObj); 101 | console.error(err); 102 | this.submitting = false; 103 | this.error = true; 104 | } 105 | 106 | ngOnDestroy() { 107 | if (this.submitRsvpSub) { 108 | this.submitRsvpSub.unsubscribe(); 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp.component.html: -------------------------------------------------------------------------------- 1 |
2 |

RSVP

3 | 4 |
5 | 6 | 7 | 8 |
9 |

You cannot RSVP to an event that has already ended.

10 |
11 | 12 | 13 | 14 | 15 |
16 |

You responded to this event with the following information:

17 |
18 | 19 |
    20 |
  • 21 | Name:{{ userRsvp.name }} 22 |
  • 23 |
  • 24 | Attending:{{ utils.booleanToText(userRsvp.attending) }} 25 |
  • 26 |
  • 27 | Additional Guests:{{ userRsvp.guests }} 28 |
  • 29 |
  • 30 | Comments: 31 |
  • 32 |
33 | 34 |
35 | 39 | 40 | 45 |
46 |
47 | 48 | 49 |
50 |

Fill out the form below to respond:

51 | 52 | 55 |
56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 |

All RSVPs

66 |

There are currently no RSVPs for this event.

67 |
68 | 69 |
    70 |
  • 71 | Attending 72 | {{ totalAttending }} 73 |
  • 74 |
  • 77 | {{ rsvp.name }} {{ utils.showPlusOnes(rsvp.guests) }} 78 |

    79 | 80 |

    81 |
  • 82 |
  • 83 | Not Attending 84 | {{ fs.filter(rsvps, 'attending', false).length }} 85 |
  • 86 |
  • 89 | {{ rsvp.name }} 90 |

    91 | 92 |

    93 |
  • 94 |
95 |
96 | 97 | 98 |
99 |

100 | Oops! There was an error retrieving RSVPs for this event. 101 |

102 |
103 |
104 | 105 | 106 | 109 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp.component.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | RSVP COMPONENT 3 | --------------------*/ 4 | 5 | .list-group-item p:last-child { 6 | margin-bottom: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/event/rsvp/rsvp.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, OnDestroy } from '@angular/core'; 2 | import { expandCollapse } from './../../../core/expand-collapse.animation'; 3 | import { AuthService } from './../../../auth/auth.service'; 4 | import { ApiService } from './../../../core/api.service'; 5 | import { UtilsService } from './../../../core/utils.service'; 6 | import { FilterSortService } from './../../../core/filter-sort.service'; 7 | import { RsvpModel } from './../../../core/models/rsvp.model'; 8 | import { Subscription } from 'rxjs'; 9 | 10 | @Component({ 11 | selector: 'app-rsvp', 12 | animations: [expandCollapse], 13 | templateUrl: './rsvp.component.html', 14 | styleUrls: ['./rsvp.component.scss'] 15 | }) 16 | export class RsvpComponent implements OnInit, OnDestroy { 17 | @Input() eventId: string; 18 | @Input() eventPast: boolean; 19 | rsvpsSub: Subscription; 20 | rsvps: RsvpModel[]; 21 | loading: boolean; 22 | error: boolean; 23 | userRsvp: RsvpModel; 24 | totalAttending: number; 25 | footerTense: string; 26 | showEditForm: boolean; 27 | editBtnText: string; 28 | showAllRsvps = false; 29 | showRsvpsText = 'View All RSVPs'; 30 | 31 | constructor( 32 | public auth: AuthService, 33 | private api: ApiService, 34 | public utils: UtilsService, 35 | public fs: FilterSortService 36 | ) { } 37 | 38 | ngOnInit() { 39 | this.footerTense = !this.eventPast ? 'plan to attend this event.' : 'attended this event.'; 40 | this._getRSVPs(); 41 | this.toggleEditForm(false); 42 | } 43 | 44 | private _getRSVPs() { 45 | this.loading = true; 46 | // Get RSVPs by event ID 47 | this.rsvpsSub = this.api 48 | .getRsvpsByEventId$(this.eventId) 49 | .subscribe( 50 | res => { 51 | this.rsvps = res; 52 | this._updateRsvpState(); 53 | this.loading = false; 54 | }, 55 | err => { 56 | console.error(err); 57 | this.loading = false; 58 | this.error = true; 59 | } 60 | ); 61 | } 62 | 63 | toggleEditForm(setVal?: boolean) { 64 | this.showEditForm = setVal !== undefined ? setVal : !this.showEditForm; 65 | this.editBtnText = this.showEditForm ? 'Cancel Edit' : 'Edit My RSVP'; 66 | } 67 | 68 | toggleShowRsvps() { 69 | this.showAllRsvps = !this.showAllRsvps; 70 | this.showRsvpsText = this.showAllRsvps ? 'Hide RSVPs' : 'Show All RSVPs'; 71 | } 72 | 73 | onSubmitRsvp(e) { 74 | if (e.rsvp) { 75 | this.userRsvp = e.rsvp; 76 | this._updateRsvpState(true); 77 | this.toggleEditForm(false); 78 | } 79 | } 80 | 81 | private _updateRsvpState(changed?: boolean) { 82 | // If RSVP matching user ID is already 83 | // in RSVP array, set as initial RSVP 84 | const _initialUserRsvp = this.rsvps.filter(rsvp => { 85 | return rsvp.userId === this.auth.userProfile.sub; 86 | })[0]; 87 | 88 | // If user has not RSVPed before and has made 89 | // a change, push new RSVP to local RSVPs store 90 | if (!_initialUserRsvp && this.userRsvp && changed) { 91 | this.rsvps.push(this.userRsvp); 92 | } 93 | this._setUserRsvpGetAttending(changed); 94 | } 95 | 96 | private _setUserRsvpGetAttending(changed?: boolean) { 97 | // Iterate over RSVPs to get/set user's RSVP 98 | // and get total number of attending guests 99 | let guests = 0; 100 | const rsvpArr = this.rsvps.map(rsvp => { 101 | // If user has an existing RSVP 102 | if (rsvp.userId === this.auth.userProfile.sub) { 103 | if (changed) { 104 | // If user edited their RSVP, set with updated data 105 | rsvp = this.userRsvp; 106 | } else { 107 | // If no changes were made, set userRsvp property 108 | // (This applies on ngOnInit) 109 | this.userRsvp = rsvp; 110 | } 111 | } 112 | // Count total number of attendees 113 | // + additional guests 114 | if (rsvp.attending) { 115 | guests++; 116 | if (rsvp.guests) { 117 | guests += rsvp.guests; 118 | } 119 | } 120 | return rsvp; 121 | }); 122 | this.rsvps = rsvpArr; 123 | this.totalAttending = guests; 124 | } 125 | 126 | ngOnDestroy() { 127 | this.rsvpsSub.unsubscribe(); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |

{{ pageTitle }}

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 26 | 27 | 28 |

29 | No events found for {{ query }}, sorry! 30 |

31 | 32 | 33 |
34 | 38 |
39 |
40 | {{ utils.eventDates(event.startDatetime, event.endDatetime) }} 41 |
42 |
43 |
44 |
45 | 46 | 47 |

48 | No upcoming public events available. 49 |

50 |
51 | 52 | 53 |

54 | Oops! There was an error retrieving event data. 55 |

56 | 57 |
58 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/home/home.component.scss -------------------------------------------------------------------------------- /src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { ApiService } from './../../core/api.service'; 4 | import { UtilsService } from './../../core/utils.service'; 5 | import { FilterSortService } from './../../core/filter-sort.service'; 6 | import { Subscription } from 'rxjs'; 7 | import { EventModel } from './../../core/models/event.model'; 8 | 9 | @Component({ 10 | selector: 'app-home', 11 | templateUrl: './home.component.html', 12 | styleUrls: ['./home.component.scss'] 13 | }) 14 | export class HomeComponent implements OnInit, OnDestroy { 15 | pageTitle = 'Events'; 16 | eventListSub: Subscription; 17 | eventList: EventModel[]; 18 | filteredEvents: EventModel[]; 19 | loading: boolean; 20 | error: boolean; 21 | query: ''; 22 | 23 | constructor( 24 | private title: Title, 25 | public utils: UtilsService, 26 | private api: ApiService, 27 | public fs: FilterSortService 28 | ) { } 29 | 30 | ngOnInit() { 31 | this.title.setTitle(this.pageTitle); 32 | this._getEventList(); 33 | } 34 | 35 | private _getEventList() { 36 | this.loading = true; 37 | // Get future, public events 38 | this.eventListSub = this.api 39 | .getEvents$() 40 | .subscribe( 41 | res => { 42 | this.eventList = res; 43 | this.filteredEvents = res; 44 | this.loading = false; 45 | }, 46 | err => { 47 | console.error(err); 48 | this.loading = false; 49 | this.error = true; 50 | } 51 | ); 52 | } 53 | 54 | searchEvents() { 55 | this.filteredEvents = this.fs.search(this.eventList, this.query, '_id', 'mediumDate'); 56 | } 57 | 58 | resetQuery() { 59 | this.query = ''; 60 | this.filteredEvents = this.eventList; 61 | } 62 | 63 | get noSearchResults(): boolean { 64 | return !!(!this.filteredEvents.length && this.query); 65 | } 66 | 67 | ngOnDestroy() { 68 | this.eventListSub.unsubscribe(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/app/pages/my-rsvps/my-rsvps.component.html: -------------------------------------------------------------------------------- 1 |

{{ pageTitle }}

2 |

3 | Hello, ! 4 | 5 | You may create and administer events. 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | You have not RSVPed to any events yet. Check out the homepage to see a list of upcoming events. 16 |

17 | 18 | 19 |

You have RSVPed for the following upcoming events:

20 | 21 | 22 | 35 |
36 |
37 | 38 | 39 |

40 | Oops! There was an error getting your RSVP data. 41 |

42 |
43 | -------------------------------------------------------------------------------- /src/app/pages/my-rsvps/my-rsvps.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/app/pages/my-rsvps/my-rsvps.component.scss -------------------------------------------------------------------------------- /src/app/pages/my-rsvps/my-rsvps.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { AuthService } from './../../auth/auth.service'; 4 | import { ApiService } from './../../core/api.service'; 5 | import { UtilsService } from './../../core/utils.service'; 6 | import { FilterSortService } from './../../core/filter-sort.service'; 7 | import { Subscription } from 'rxjs'; 8 | import { EventModel } from './../../core/models/event.model'; 9 | 10 | @Component({ 11 | selector: 'app-my-rsvps', 12 | templateUrl: './my-rsvps.component.html', 13 | styleUrls: ['./my-rsvps.component.scss'] 14 | }) 15 | export class MyRsvpsComponent implements OnInit, OnDestroy { 16 | pageTitle = 'My RSVPs'; 17 | loggedInSub: Subscription; 18 | eventListSub: Subscription; 19 | eventList: EventModel[]; 20 | loading: boolean; 21 | error: boolean; 22 | userIdp: string; 23 | 24 | constructor( 25 | private title: Title, 26 | public auth: AuthService, 27 | private api: ApiService, 28 | public fs: FilterSortService, 29 | public utils: UtilsService 30 | ) { } 31 | 32 | ngOnInit() { 33 | this.loggedInSub = this.auth.loggedIn$.subscribe( 34 | loggedIn => { 35 | this.loading = true; 36 | if (loggedIn) { 37 | this._getEventList(); 38 | } 39 | } 40 | ); 41 | this.title.setTitle(this.pageTitle); 42 | } 43 | 44 | private _getEventList() { 45 | // Get events user has RSVPed to 46 | this.eventListSub = this.api 47 | .getUserEvents$(this.auth.userProfile.sub) 48 | .subscribe( 49 | res => { 50 | this.eventList = res; 51 | this.loading = false; 52 | }, 53 | err => { 54 | console.error(err); 55 | this.loading = false; 56 | this.error = true; 57 | } 58 | ); 59 | } 60 | 61 | ngOnDestroy() { 62 | this.loggedInSub.unsubscribe(); 63 | this.eventListSub.unsubscribe(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/scss/_base.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | BASICS 3 | --------------------*/ 4 | 5 | body { 6 | min-width: 320px; 7 | } 8 | 9 | /*-- Cursor --*/ 10 | 11 | a, 12 | input[type=button], 13 | input[type=submit], 14 | button { 15 | cursor: pointer; 16 | } 17 | 18 | /*-- Link Buttons --*/ 19 | 20 | .btn-link { 21 | color: #0275d8; 22 | 23 | &:hover { 24 | text-decoration: underline !important; 25 | } 26 | &:focus { 27 | box-shadow: none !important; 28 | color: #0275d8; 29 | text-decoration: none; 30 | } 31 | } 32 | 33 | /*-- Forms --*/ 34 | 35 | input[type="text"], 36 | input[type="number"], 37 | input[type="password"], 38 | input[type="date"], 39 | select option, 40 | textarea { 41 | font-size: 16px; /* for iOS to prevent autozoom */ 42 | } 43 | .formErrors { 44 | padding-top: 6px; 45 | } 46 | .ng-invalid.ng-dirty, 47 | .ng-invalid.ng-dirty:focus { 48 | border-color: #D9534E !important; 49 | } 50 | input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 51 | color: rgba(0,0,0,.25) !important; 52 | opacity: 1 !important; 53 | } 54 | input::-moz-placeholder { /* Firefox 19+ */ 55 | color: rgba(0,0,0,.25) !important; 56 | opacity: 1 !important; 57 | } 58 | input:-moz-placeholder { /* Firefox 18- */ 59 | color: rgba(0,0,0,.25) !important; 60 | opacity: 1 !important; 61 | } 62 | input:-ms-input-placeholder { /* IE 10+ */ 63 | color: rgba(0,0,0,.25) !important; 64 | opacity: 1 !important; 65 | } 66 | 67 | /*-- Helpers --*/ 68 | 69 | .list-group-item > strong { 70 | padding-right: 5px; 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/scss/partials/_layout.vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | LAYOUT VARIABLES 3 | --------------------*/ 4 | 5 | $padding-screen-small: 3%; 6 | $padding-screen-large: 1.5% 3%; 7 | -------------------------------------------------------------------------------- /src/assets/scss/partials/_responsive.partial.scss: -------------------------------------------------------------------------------- 1 | /*-------------------- 2 | RESPONSIVE 3 | --------------------*/ 4 | 5 | /*-- Variables --*/ 6 | 7 | $large: 'screen and (min-width: 768px)'; 8 | 9 | /*-- Mixins --*/ 10 | 11 | @mixin mq($mqString) { 12 | @media #{$mqString} { 13 | @content; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | // partials 2 | @import 'partials/layout.vars'; 3 | @import 'partials/responsive.partial'; 4 | 5 | // global styles 6 | @import 'base'; 7 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mean-rsvp-auth0/0ef1fd599ff4bf77244823b5e476373aca7fae7f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RSVP 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | (window as any).global = window; 18 | 19 | /*************************************************************************************************** 20 | * BROWSER POLYFILLS 21 | */ 22 | 23 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 24 | // import 'core-js/es6/symbol'; 25 | // import 'core-js/es6/object'; 26 | // import 'core-js/es6/function'; 27 | // import 'core-js/es6/parse-int'; 28 | // import 'core-js/es6/parse-float'; 29 | // import 'core-js/es6/number'; 30 | // import 'core-js/es6/math'; 31 | // import 'core-js/es6/string'; 32 | // import 'core-js/es6/date'; 33 | // import 'core-js/es6/array'; 34 | // import 'core-js/es6/regexp'; 35 | // import 'core-js/es6/map'; 36 | // import 'core-js/es6/set'; 37 | 38 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 39 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 40 | 41 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 42 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 43 | 44 | 45 | /** Evergreen browsers require these. **/ 46 | import 'core-js/es6/reflect'; 47 | import 'core-js/es7/reflect'; 48 | 49 | 50 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | 62 | /*************************************************************************************************** 63 | * APPLICATION IMPORTS 64 | */ 65 | 66 | /** 67 | * Date, currency, decimal and percent pipes. 68 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 69 | */ 70 | // import 'intl'; // Run `npm install --save intl`. 71 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts", 15 | "polyfills.ts" 16 | ], 17 | "include": [ 18 | "**/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "preserveWhitespaces": false 22 | }, 23 | "exclude": [ 24 | "test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | --------------------------------------------------------------------------------