) {}
43 |
44 | cancel() {
45 | this.ref.close(false);
46 | }
47 |
48 | confirm() {
49 | this.ref.close(true);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/auth/components/user-home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import * as fromStore from '@app/state';
4 | import { Store } from '@ngrx/store';
5 | import { Logout } from '@app/auth/actions/auth.actions';
6 |
7 | @Component({
8 | selector: 'abl-user-home',
9 | template: `
10 |
11 |
Welcome Home!
12 |
13 |
14 |
15 | `,
16 | styles: [
17 | `
18 | :host {
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | padding: 128px 0 0 0;
23 | }
24 |
25 | div {
26 | width: 100%;
27 | min-width: 250px;
28 | max-width: 300px;
29 | }
30 | `
31 | ]
32 | })
33 | export class UserHomeComponent {
34 | constructor(private store: Store, private router: Router) {}
35 |
36 | goToBooks() {
37 | this.router.navigate(['/books']);
38 | }
39 |
40 | logout() {
41 | this.store.dispatch(new Logout());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/auth/effects/auth.effects.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { Actions, Effect } from '@ngrx/effects';
4 | import { tap, exhaustMap, map, catchError } from 'rxjs/operators';
5 | import { MatDialog } from '@angular/material';
6 | import * as fromAuth from '../actions/auth.actions';
7 | import { LogoutPromptComponent } from '@app/auth/components/logout-prompt.component';
8 | import { AuthService } from '@app/auth/services/auth.service';
9 | import { of, empty } from 'rxjs';
10 |
11 | @Injectable()
12 | export class AuthEffects {
13 | @Effect({ dispatch: false })
14 | login$ = this.actions$.ofType(fromAuth.AuthActionTypes.Login).pipe(
15 | tap(() => {
16 | return this.authService.login();
17 | })
18 | );
19 |
20 | @Effect()
21 | loginComplete$ = this.actions$
22 | .ofType(fromAuth.AuthActionTypes.LoginComplete)
23 | .pipe(
24 | exhaustMap(() => {
25 | return this.authService.parseHash$().pipe(
26 | map((authResult: any) => {
27 | if (authResult && authResult.accessToken) {
28 | this.authService.setAuth(authResult);
29 | window.location.hash = '';
30 | return new fromAuth.LoginSuccess();
31 | }
32 | }),
33 | catchError(error => of(new fromAuth.LoginFailure(error)))
34 | );
35 | })
36 | );
37 |
38 | @Effect({ dispatch: false })
39 | loginRedirect$ = this.actions$
40 | .ofType(fromAuth.AuthActionTypes.LoginSuccess)
41 | .pipe(
42 | tap(() => {
43 | this.router.navigate([this.authService.authSuccessUrl]);
44 | })
45 | );
46 |
47 | @Effect({ dispatch: false })
48 | loginErrorRedirect$ = this.actions$
49 | .ofType(fromAuth.AuthActionTypes.LoginFailure)
50 | .pipe(
51 | map(action => action.payload),
52 | tap((err: any) => {
53 | if (err.error_description) {
54 | console.error(`Error: ${err.error_description}`);
55 | } else {
56 | console.error(`Error: ${JSON.stringify(err)}`);
57 | }
58 | this.router.navigate([this.authService.authFailureUrl]);
59 | })
60 | );
61 |
62 | @Effect()
63 | checkLogin$ = this.actions$.ofType(fromAuth.AuthActionTypes.CheckLogin).pipe(
64 | exhaustMap(() => {
65 | if (this.authService.authenticated) {
66 | return this.authService.checkSession$({}).pipe(
67 | map((authResult: any) => {
68 | if (authResult && authResult.accessToken) {
69 | this.authService.setAuth(authResult);
70 | return new fromAuth.LoginSuccess();
71 | }
72 | }),
73 | catchError(error => {
74 | this.authService.resetAuthFlag();
75 | return of(new fromAuth.LoginFailure({ error }));
76 | })
77 | );
78 | } else {
79 | return empty();
80 | }
81 | })
82 | );
83 |
84 | @Effect()
85 | logoutConfirmation$ = this.actions$.ofType(fromAuth.AuthActionTypes.Logout).pipe(
86 | exhaustMap(() =>
87 | this.dialogService
88 | .open(LogoutPromptComponent)
89 | .afterClosed()
90 | .pipe(
91 | map(confirmed => {
92 | if (confirmed) {
93 | return new fromAuth.LogoutConfirmed();
94 | } else {
95 | return new fromAuth.LogoutCancelled();
96 | }
97 | })
98 | )
99 | )
100 | );
101 |
102 | @Effect({ dispatch: false })
103 | logout$ = this.actions$
104 | .ofType(fromAuth.AuthActionTypes.LogoutConfirmed)
105 | .pipe(tap(() => this.authService.logout()));
106 |
107 | constructor(
108 | private actions$: Actions,
109 | private authService: AuthService,
110 | private router: Router,
111 | private dialogService: MatDialog
112 | ) {}
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/auth/services/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { CanActivate, Router } from '@angular/router';
3 | import { of } from 'rxjs';
4 | import { mergeMap, map, take } from 'rxjs/operators';
5 | import { Store } from '@ngrx/store';
6 |
7 | import { AuthService } from '@app/auth/services/auth.service';
8 | import * as fromStore from '@app/state';
9 |
10 | @Injectable({
11 | providedIn: 'root'
12 | })
13 | export class AuthGuard implements CanActivate {
14 | constructor(
15 | private authService: AuthService,
16 | private store: Store,
17 | private router: Router
18 | ) {}
19 |
20 | canActivate() {
21 | return this.checkStoreAuthentication().pipe(
22 | mergeMap(storeAuth => {
23 | if (storeAuth) {
24 | return of(true);
25 | }
26 |
27 | return this.checkApiAuthentication();
28 | }),
29 | map(storeOrApiAuth => {
30 | if (!storeOrApiAuth) {
31 | this.router.navigate(['/login']);
32 | return false;
33 | }
34 |
35 | return true;
36 | })
37 | );
38 | }
39 |
40 | checkStoreAuthentication() {
41 | return this.store.select(fromStore.selectIsLoggedIn).pipe(take(1));
42 | }
43 |
44 | checkApiAuthentication() {
45 | return of(this.authService.authenticated);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/auth/services/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { bindNodeCallback } from 'rxjs';
3 | import * as auth0 from 'auth0-js';
4 | import { environment } from '../../../environments/environment';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class AuthService {
10 | // Create Auth0 web auth instance
11 | // @TODO: Update environment variables and remove .example
12 | // extension in src/environments/environment.ts.example
13 | private _Auth0 = new auth0.WebAuth({
14 | clientID: environment.auth.clientID,
15 | domain: environment.auth.domain,
16 | responseType: 'token',
17 | redirectUri: environment.auth.redirect,
18 | scope: environment.auth.scope
19 | });
20 | // Track whether or not to renew token
21 | private _authFlag = 'isLoggedIn';
22 | // Authentication navigation
23 | private _onAuthSuccessUrl = '/home';
24 | private _onAuthFailureUrl = '/login';
25 | private _logoutUrl = 'http://localhost:4200';
26 | private _expiresAt: number;
27 |
28 | // Create observable of Auth0 parseHash method to gather auth results
29 | parseHash$ = bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0));
30 |
31 | // Create observable of Auth0 checkSession method to
32 | // verify authorization server session and renew tokens
33 | checkSession$ = bindNodeCallback(this._Auth0.checkSession.bind(this._Auth0));
34 |
35 | constructor() { }
36 |
37 | get authSuccessUrl(): string {
38 | return this._onAuthSuccessUrl;
39 | }
40 |
41 | get authFailureUrl(): string {
42 | return this._onAuthFailureUrl;
43 | }
44 |
45 | get authenticated(): boolean {
46 | return JSON.parse(localStorage.getItem(this._authFlag));
47 | }
48 |
49 | resetAuthFlag() {
50 | localStorage.removeItem(this._authFlag);
51 | }
52 |
53 | login() {
54 | this._Auth0.authorize();
55 | }
56 |
57 | setAuth(authResult) {
58 | this._expiresAt = authResult.expiresIn * 1000 + Date.now();
59 | // Set flag in local storage stating this app is logged in
60 | localStorage.setItem(this._authFlag, JSON.stringify(true));
61 | }
62 |
63 | logout() {
64 | // Set authentication status flag in local storage to false
65 | localStorage.setItem(this._authFlag, JSON.stringify(false));
66 | // This does a refresh and redirects back to homepage
67 | // Make sure you have the logout URL in your Auth0
68 | // Dashboard Application settings in Allowed Logout URLs
69 | this._Auth0.logout({
70 | returnTo: this._logoutUrl,
71 | clientID: environment.auth.clientID
72 | });
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/books/actions/books-api.actions.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '@ngrx/store';
2 | import { Book } from '../models/book';
3 |
4 | export enum BooksApiActionTypes {
5 | LoadSuccess = '[Books API] Load Success',
6 | LoadFail = '[Books API] Load Fail',
7 | }
8 |
9 | export class LoadSuccess implements Action {
10 | readonly type = BooksApiActionTypes.LoadSuccess;
11 |
12 | constructor(public payload: Book[]) {}
13 | }
14 |
15 | export class LoadFail implements Action {
16 | readonly type = BooksApiActionTypes.LoadFail;
17 |
18 | constructor(public payload: any) {}
19 | }
20 |
21 | export type BooksApiActionsUnion =
22 | | LoadSuccess
23 | | LoadFail;
24 |
--------------------------------------------------------------------------------
/src/app/books/actions/books-page.actions.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '@ngrx/store';
2 | import { Book } from '../models/book';
3 |
4 | export enum BooksPageActionTypes {
5 | Load = '[Books Page] Load Books'
6 | }
7 |
8 | export class Load implements Action {
9 | readonly type = BooksPageActionTypes.Load;
10 | }
11 |
12 | export type BooksPageActionsUnion =
13 | | Load;
14 |
--------------------------------------------------------------------------------
/src/app/books/books.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RouterModule } from '@angular/router';
4 | import { StoreModule } from '@ngrx/store';
5 | import { EffectsModule } from '@ngrx/effects';
6 |
7 | import { ComponentsModule } from './components';
8 | import { MaterialModule } from '../material';
9 |
10 | import { reducers } from './reducers';
11 | import { BooksPageEffects } from './effects/books-page.effects';
12 | import { BooksPageComponent } from './components/books-page.component';
13 | import { AuthGuard } from '@app/auth/services/auth.guard';
14 |
15 | @NgModule({
16 | imports: [
17 | CommonModule,
18 | MaterialModule,
19 | ComponentsModule,
20 | RouterModule.forChild([{ path: '', component: BooksPageComponent, canActivate: [AuthGuard] }]),
21 | StoreModule.forFeature('books', reducers),
22 | EffectsModule.forFeature([BooksPageEffects])
23 | ]
24 | })
25 | export class BooksModule {}
26 |
--------------------------------------------------------------------------------
/src/app/books/components/book-authors.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | import { Book } from '../models/book';
4 |
5 | @Component({
6 | selector: 'abl-book-authors',
7 | template: `
8 | Written By:
9 |
10 | {{ authors | bcAddCommas }}
11 |
12 | `,
13 | styles: [
14 | `
15 | h5 {
16 | margin-bottom: 5px;
17 | }
18 | `,
19 | ],
20 | })
21 | export class BookAuthorsComponent {
22 | @Input() book: Book;
23 |
24 | get authors() {
25 | return this.book.volumeInfo.authors;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/books/components/book-detail.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from '@angular/core';
2 | import { Book } from '../models/book';
3 |
4 | @Component({
5 | selector: 'abl-book-detail',
6 | template: `
7 |
8 |
9 | {{ title }}
10 | {{ subtitle }}
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
23 |
24 |
27 |
28 |
29 |
30 | `,
31 | styles: [
32 | `
33 | :host {
34 | display: flex;
35 | justify-content: center;
36 | margin: 75px 0;
37 | }
38 | mat-card {
39 | max-width: 600px;
40 | }
41 | mat-card-title-group {
42 | margin-left: 0;
43 | }
44 | img {
45 | width: 60px;
46 | min-width: 60px;
47 | margin-left: 5px;
48 | }
49 | mat-card-content {
50 | margin: 15px 0 50px;
51 | }
52 | mat-card-actions {
53 | margin: 25px 0 0 !important;
54 | }
55 | mat-card-footer {
56 | padding: 0 25px 25px;
57 | position: relative;
58 | }
59 | `,
60 | ],
61 | })
62 | export class BookDetailComponent {
63 | @Input() book: Book;
64 | @Input() inCollection: boolean;
65 | @Output() add = new EventEmitter();
66 | @Output() remove = new EventEmitter();
67 |
68 | get id() {
69 | return this.book.id;
70 | }
71 |
72 | get title() {
73 | return this.book.volumeInfo.title;
74 | }
75 |
76 | get subtitle() {
77 | return this.book.volumeInfo.subtitle;
78 | }
79 |
80 | get description() {
81 | return this.book.volumeInfo.description;
82 | }
83 |
84 | get thumbnail() {
85 | return (
86 | this.book.volumeInfo.imageLinks &&
87 | this.book.volumeInfo.imageLinks.smallThumbnail
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/books/components/book-preview-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Book } from '../models/book';
3 |
4 | @Component({
5 | selector: 'abl-book-preview-list',
6 | template: `
7 |
8 | `,
9 | styles: [
10 | `
11 | :host {
12 | display: flex;
13 | flex-wrap: wrap;
14 | justify-content: center;
15 | }
16 | `,
17 | ],
18 | })
19 | export class BookPreviewListComponent {
20 | @Input() books: Book[];
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/books/components/book-preview.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Book } from '../models/book';
3 |
4 | @Component({
5 | selector: 'abl-book-preview',
6 | template: `
7 |
8 |
9 |
10 | {{ title | bcEllipsis:35 }}
11 | {{ subtitle | bcEllipsis:40 }}
12 |
13 |
14 | {{ description | bcEllipsis }}
15 |
16 |
17 |
18 |
19 |
20 | `,
21 | styles: [
22 | `
23 | :host {
24 | display: flex;
25 | }
26 |
27 | :host a {
28 | display: flex;
29 | }
30 |
31 | mat-card {
32 | width: 400px;
33 | margin: 15px;
34 | display: flex;
35 | flex-flow: column;
36 | justify-content: space-between;
37 | }
38 |
39 | @media only screen and (max-width: 768px) {
40 | mat-card {
41 | margin: 15px 0 !important;
42 | }
43 | }
44 | mat-card:hover {
45 | box-shadow: 3px 3px 16px -2px rgba(0, 0, 0, .5);
46 | }
47 | mat-card-title {
48 | margin-right: 10px;
49 | }
50 | mat-card-title-group {
51 | margin: 0;
52 | }
53 | a {
54 | color: inherit;
55 | text-decoration: none;
56 | }
57 | img {
58 | width: 60px;
59 | min-width: 60px;
60 | margin-left: 5px;
61 | }
62 | mat-card-content {
63 | margin-top: 15px;
64 | margin: 15px 0 0;
65 | }
66 | span {
67 | display: inline-block;
68 | font-size: 13px;
69 | }
70 | mat-card-footer {
71 | padding: 0 25px 25px;
72 | }
73 | `,
74 | ],
75 | })
76 | export class BookPreviewComponent {
77 | @Input() book: Book;
78 |
79 | get id() {
80 | return this.book.id;
81 | }
82 |
83 | get title() {
84 | return this.book.volumeInfo.title;
85 | }
86 |
87 | get subtitle() {
88 | return this.book.volumeInfo.subtitle;
89 | }
90 |
91 | get description() {
92 | return this.book.volumeInfo.description;
93 | }
94 |
95 | get thumbnail(): string | boolean {
96 | if (this.book.volumeInfo.imageLinks) {
97 | return this.book.volumeInfo.imageLinks.smallThumbnail;
98 | }
99 |
100 | return false;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/books/components/books-page.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { select, Store } from '@ngrx/store';
3 | import { Observable } from 'rxjs';
4 |
5 | import * as BooksPageActions from '../actions/books-page.actions';
6 | import { Book } from '../models/book';
7 | import * as fromBooks from '../reducers';
8 | import { Logout } from '@app/auth/actions/auth.actions';
9 |
10 | @Component({
11 | selector: 'abl-books-page',
12 | changeDetection: ChangeDetectionStrategy.OnPush,
13 | template: `
14 |
15 | My Collection
16 |
17 |
18 |
19 |
20 |
21 |
22 | `,
23 | styles: [
24 | `
25 | mat-card-title {
26 | display: flex;
27 | justify-content: center;
28 | }
29 |
30 | mat-card-actions {
31 | display: flex;
32 | justify-content: center;
33 | }
34 | `
35 | ]
36 | })
37 | export class BooksPageComponent implements OnInit {
38 | books$: Observable;
39 |
40 | constructor(private store: Store) {
41 | this.books$ = store.pipe(select(fromBooks.getAllBooks));
42 | }
43 |
44 | ngOnInit() {
45 | this.store.dispatch(new BooksPageActions.Load());
46 | }
47 |
48 | logout() {
49 | this.store.dispatch(new Logout());
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/books/components/index.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { ReactiveFormsModule } from '@angular/forms';
4 | import { RouterModule } from '@angular/router';
5 |
6 | import { BookAuthorsComponent } from './book-authors.component';
7 | import { BookDetailComponent } from './book-detail.component';
8 | import { BookPreviewComponent } from './book-preview.component';
9 | import { BookPreviewListComponent } from './book-preview-list.component';
10 |
11 | import { PipesModule } from '../../shared/pipes';
12 | import { MaterialModule } from '../../material';
13 | import { BooksPageComponent } from './books-page.component';
14 |
15 | export const COMPONENTS = [
16 | BookAuthorsComponent,
17 | BookDetailComponent,
18 | BookPreviewComponent,
19 | BookPreviewListComponent,
20 | BooksPageComponent
21 | ];
22 |
23 | @NgModule({
24 | imports: [
25 | CommonModule,
26 | ReactiveFormsModule,
27 | MaterialModule,
28 | RouterModule,
29 | PipesModule,
30 | ],
31 | declarations: COMPONENTS,
32 | exports: COMPONENTS,
33 | })
34 | export class ComponentsModule {}
35 |
--------------------------------------------------------------------------------
/src/app/books/effects/books-page.effects.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Actions, Effect, ofType } from '@ngrx/effects';
3 | import { Action } from '@ngrx/store';
4 | import { Observable, of } from 'rxjs';
5 | import { catchError, map, exhaustMap, tap } from 'rxjs/operators';
6 |
7 | import {
8 | BooksPageActionTypes,
9 | } from './../actions/books-page.actions';
10 | import {
11 | BooksApiActionTypes,
12 | LoadSuccess,
13 | LoadFail
14 | } from './../actions/books-api.actions';
15 | import { GoogleBooksService } from '@app/services/google-books';
16 | import { Book } from '../models/book';
17 |
18 | @Injectable()
19 | export class BooksPageEffects {
20 | @Effect()
21 | loadCollection$: Observable = this.actions$.pipe(
22 | ofType(BooksPageActionTypes.Load),
23 | exhaustMap(() =>
24 | this.googleBooksService.searchBooks('oauth')
25 | .pipe(
26 | map((books: Book[]) => new LoadSuccess(books),
27 | catchError(error => of(new LoadFail(error)))
28 | )
29 | )
30 | )
31 | );
32 |
33 | constructor(
34 | private actions$: Actions,
35 | private googleBooksService: GoogleBooksService
36 | ) {}
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/books/models/book.ts:
--------------------------------------------------------------------------------
1 | export interface Book {
2 | id: string;
3 | volumeInfo: {
4 | title: string;
5 | subtitle: string;
6 | authors: string[];
7 | publisher: string;
8 | publishDate: string;
9 | description: string;
10 | averageRating: number;
11 | ratingsCount: number;
12 | imageLinks: {
13 | thumbnail: string;
14 | smallThumbnail: string;
15 | };
16 | };
17 | }
18 |
19 | export function generateMockBook(): Book {
20 | return {
21 | id: '1',
22 | volumeInfo: {
23 | title: 'title',
24 | subtitle: 'subtitle',
25 | authors: ['author'],
26 | publisher: 'publisher',
27 | publishDate: '',
28 | description: 'description',
29 | averageRating: 3,
30 | ratingsCount: 5,
31 | imageLinks: {
32 | thumbnail: 'string',
33 | smallThumbnail: 'string',
34 | },
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/books/reducers/books.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from '@ngrx/store';
2 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
3 | import { Book } from '../models/book';
4 | import { BooksApiActionTypes, BooksApiActionsUnion } from '../actions/books-api.actions';
5 |
6 | export interface State extends EntityState {}
7 |
8 | export const adapter: EntityAdapter = createEntityAdapter({
9 | selectId: (book: Book) => book.id,
10 | sortComparer: false,
11 | });
12 |
13 | export const initialState: State = adapter.getInitialState();
14 |
15 | export function reducer(
16 | state = initialState,
17 | action: BooksApiActionsUnion
18 | ): State {
19 | switch (action.type) {
20 | case BooksApiActionTypes.LoadSuccess: {
21 | return adapter.addAll(action.payload, state);
22 | }
23 |
24 | default: {
25 | return state;
26 | }
27 | }
28 | }
29 |
30 | export const {
31 | selectIds: getBookIds,
32 | selectEntities: getBookEntities,
33 | selectAll: getAllBooks,
34 | selectTotal: getTotalBooks,
35 | } = adapter.getSelectors();
36 |
--------------------------------------------------------------------------------
/src/app/books/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createSelector,
3 | createFeatureSelector,
4 | ActionReducerMap,
5 | } from '@ngrx/store';
6 | import * as fromBooks from './books';
7 | import * as fromRoot from '../../reducers';
8 |
9 | export interface BooksState {
10 | books: fromBooks.State;
11 | }
12 |
13 | export interface State extends fromRoot.State {
14 | books: BooksState;
15 | }
16 |
17 | export const reducers: ActionReducerMap = {
18 | books: fromBooks.reducer,
19 | };
20 |
21 | export const getBooksState = createFeatureSelector('books');
22 |
23 | export const getBookEntitiesState = createSelector(
24 | getBooksState,
25 | state => state.books
26 | );
27 |
28 | export const getBookEntities = createSelector(getBookEntitiesState, fromBooks.getBookEntities)
29 | export const getBookIds = createSelector(getBookEntitiesState, fromBooks.getBookIds);
30 |
31 | export const getAllBooks = createSelector(
32 | getBookEntities,
33 | getBookIds,
34 | (entities, ids: string[]) => {
35 | return ids.map(id => entities[id]);
36 | }
37 | );
--------------------------------------------------------------------------------
/src/app/material/index.ts:
--------------------------------------------------------------------------------
1 | export * from './material.module';
2 |
--------------------------------------------------------------------------------
/src/app/material/material.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import {
4 | MatInputModule,
5 | MatCardModule,
6 | MatButtonModule,
7 | MatSidenavModule,
8 | MatListModule,
9 | MatIconModule,
10 | MatToolbarModule,
11 | MatProgressSpinnerModule,
12 | } from '@angular/material';
13 |
14 | @NgModule({
15 | imports: [
16 | MatInputModule,
17 | MatCardModule,
18 | MatButtonModule,
19 | MatSidenavModule,
20 | MatListModule,
21 | MatIconModule,
22 | MatToolbarModule,
23 | MatProgressSpinnerModule,
24 | ],
25 | exports: [
26 | MatInputModule,
27 | MatCardModule,
28 | MatButtonModule,
29 | MatSidenavModule,
30 | MatListModule,
31 | MatIconModule,
32 | MatToolbarModule,
33 | MatProgressSpinnerModule,
34 | ],
35 | })
36 | export class MaterialModule {}
37 |
--------------------------------------------------------------------------------
/src/app/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { ActionReducerMap } from '@ngrx/store';
2 |
3 | export interface State { }
4 |
5 | export const reducers: ActionReducerMap = {};
6 |
--------------------------------------------------------------------------------
/src/app/services/google-books.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { map } from 'rxjs/operators';
5 |
6 | import { Book } from '../books/models/book';
7 |
8 | @Injectable({
9 | providedIn: 'root'
10 | })
11 | export class GoogleBooksService {
12 | private API_PATH = 'https://www.googleapis.com/books/v1/volumes';
13 |
14 | constructor(private http: HttpClient) {}
15 |
16 | searchBooks(queryTitle: string): Observable {
17 | return this.http
18 | .get<{ items: Book[] }>(`${this.API_PATH}?q=${queryTitle}`)
19 | .pipe(map(books => books.items || []));
20 | }
21 |
22 | retrieveBook(volumeId: string): Observable {
23 | return this.http.get(`${this.API_PATH}/${volumeId}`);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/add-commas.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({ name: 'bcAddCommas' })
4 | export class AddCommasPipe implements PipeTransform {
5 | transform(authors: null | string[]) {
6 | if (!authors) {
7 | return 'Author Unknown';
8 | }
9 |
10 | switch (authors.length) {
11 | case 0:
12 | return 'Author Unknown';
13 | case 1:
14 | return authors[0];
15 | case 2:
16 | return authors.join(' and ');
17 | default:
18 | const last = authors[authors.length - 1];
19 | const remaining = authors.slice(0, -1);
20 | return `${remaining.join(', ')}, and ${last}`;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/ellipsis.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({ name: 'bcEllipsis' })
4 | export class EllipsisPipe implements PipeTransform {
5 | transform(str: string, strLength: number = 250) {
6 | const withoutHtml = str.replace(/(<([^>]+)>)/gi, '');
7 |
8 | if (str.length >= strLength) {
9 | return `${withoutHtml.slice(0, strLength)}...`;
10 | }
11 |
12 | return withoutHtml;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/index.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import { AddCommasPipe } from './add-commas';
4 | import { EllipsisPipe } from './ellipsis';
5 |
6 | export const PIPES = [AddCommasPipe, EllipsisPipe];
7 |
8 | @NgModule({
9 | declarations: PIPES,
10 | exports: PIPES,
11 | })
12 | export class PipesModule {}
13 |
--------------------------------------------------------------------------------
/src/app/state/auth.reducer.ts:
--------------------------------------------------------------------------------
1 | import { AuthActions, AuthActionTypes } from '@app/auth/actions/auth.actions';
2 |
3 | export interface State {
4 | isLoggedIn: boolean;
5 | }
6 |
7 | export const initialState: State = {
8 | isLoggedIn: false
9 | };
10 |
11 | export function reducer(state = initialState, action: AuthActions): State {
12 | switch (action.type) {
13 | case AuthActionTypes.LoginSuccess:
14 | return { ...state, isLoggedIn: true };
15 |
16 | case AuthActionTypes.LogoutConfirmed:
17 | return initialState; // the initial state has isLoggedIn set to false
18 |
19 | default:
20 | return state;
21 | }
22 | }
23 |
24 | export const selectIsLoggedIn = (state: State) => state.isLoggedIn;
25 |
--------------------------------------------------------------------------------
/src/app/state/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActionReducerMap,
3 | createFeatureSelector,
4 | createSelector,
5 | MetaReducer
6 | } from '@ngrx/store';
7 | import { environment } from '../../environments/environment';
8 | import * as fromAuth from './auth.reducer';
9 |
10 | export interface State {
11 | auth: fromAuth.State;
12 | }
13 |
14 | export const reducers: ActionReducerMap = {
15 | auth: fromAuth.reducer
16 | };
17 |
18 | export const metaReducers: MetaReducer[] = !environment.production
19 | ? []
20 | : [];
21 |
22 | export const selectAuthState = createFeatureSelector('auth');
23 | export const selectIsLoggedIn = createSelector(
24 | selectAuthState,
25 | fromAuth.selectIsLoggedIn
26 | );
27 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/auth0-blog/ngrx-auth/a6fe28add9564c3389004ac44e8517df5c953482/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/browserslist:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | #
5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
6 |
7 | > 0.5%
8 | last 2 versions
9 | Firefox ESR
10 | not dead
11 | not IE 9-11
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | auth: {
8 | clientID: 'YOUR-AUTH0-CLIENT-ID',
9 | domain: 'YOUR-AUTH0-DOMAIN', // e.g., you.auth0.com
10 | redirect: 'http://localhost:4200/callback',
11 | scope: 'openid profile email'
12 | }
13 | };
14 |
15 | /*
16 | * In development mode, for easier debugging, you can ignore zone related error
17 | * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the
18 | * below file. Don't forget to comment it out in production mode
19 | * because it will have a performance impact when errors are thrown
20 | */
21 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
22 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/auth0-blog/ngrx-auth/a6fe28add9564c3389004ac44e8517df5c953482/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Auth0 Book Library
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/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 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: ['html', 'lcovonly'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
--------------------------------------------------------------------------------
/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 | .catch(err => console.log(err));
13 |
--------------------------------------------------------------------------------
/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 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Web Animations `@angular/platform-browser/animations`
51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
53 | **/
54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
55 |
56 | /**
57 | * By default, zone.js will patch all possible macroTask and DomEvents
58 | * user can disable parts of macroTask/DomEvents patch by setting following flags
59 | */
60 |
61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
64 |
65 | /*
66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
68 | */
69 | // (window as any).__Zone_enable_cross_context_check = true;
70 |
71 | /***************************************************************************************************
72 | * Zone JS is required by default for Angular itself.
73 | */
74 | import 'zone.js/dist/zone'; // Included with Angular CLI.
75 |
76 |
77 |
78 | /***************************************************************************************************
79 | * APPLICATION IMPORTS
80 | */
81 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import '~@angular/material/prebuilt-themes/indigo-pink.css';
3 |
4 | html {
5 | height: 100%;
6 | }
7 |
8 | body {
9 | font-family: 'Roboto', sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | height: 100%;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
18 |
--------------------------------------------------------------------------------
/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/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2015",
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 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills.ts"
13 | ],
14 | "include": [
15 | "**/*.spec.ts",
16 | "**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "abl",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "abl",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "src",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "target": "es5",
13 | "typeRoots": [
14 | "node_modules/@types"
15 | ],
16 | "lib": [
17 | "es2017",
18 | "dom"
19 | ],
20 | "paths": {
21 | "@app/*": ["./app/*"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-use-before-declare": true,
76 | "no-var-keyword": true,
77 | "object-literal-sort-keys": false,
78 | "one-line": [
79 | true,
80 | "check-open-brace",
81 | "check-catch",
82 | "check-else",
83 | "check-whitespace"
84 | ],
85 | "prefer-const": true,
86 | "quotemark": [
87 | true,
88 | "single"
89 | ],
90 | "radix": true,
91 | "semicolon": [
92 | true,
93 | "always"
94 | ],
95 | "triple-equals": [
96 | true,
97 | "allow-null-check"
98 | ],
99 | "typedef-whitespace": [
100 | true,
101 | {
102 | "call-signature": "nospace",
103 | "index-signature": "nospace",
104 | "parameter": "nospace",
105 | "property-declaration": "nospace",
106 | "variable-declaration": "nospace"
107 | }
108 | ],
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "no-output-on-prefix": true,
120 | "use-input-property-decorator": true,
121 | "use-output-property-decorator": true,
122 | "use-host-property-decorator": true,
123 | "no-input-rename": true,
124 | "no-output-rename": true,
125 | "use-life-cycle-interface": true,
126 | "use-pipe-transform-interface": true,
127 | "component-class-suffix": true,
128 | "directive-class-suffix": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------