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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------