.eslintrc.json

    {
      "root": true,
      "ignorePatterns": [
        "projects/**/*"
      ],
      "overrides": [
        {
          "files": [
            "*.ts"
          ],
          "parserOptions": {
            "project": [
              "tsconfig.json",
              "e2e/tsconfig.json"
            ],
            "createDefaultProgram": true
          },
          "extends": [
            "plugin:@angular-eslint/recommended",
            "plugin:@angular-eslint/template/process-inline-templates"
          ],
          "rules": {
            "@angular-eslint/component-selector": [
              "error",
              {
                "prefix": "app",
                "style": "kebab-case",
                "type": "element"
              }
            ],
            "@angular-eslint/directive-selector": [
              "error",
              {
                "prefix": "app",
                "style": "camelCase",
                "type": "attribute"
              }
            ]
          }
        },
        {
          "files": [
            "*.html"
          ],
          "extends": [
            "plugin:@angular-eslint/template/recommended"
          ],
          "rules": {}
        }
      ]
    }
    
   .gitignore

    # See http://help.github.com/ignore-files/ for more about ignoring files.

    # compiled output
    /dist
    /tmp
    /out-tsc

    # dependencies
    /node_modules

    # IDEs and editors
    .vscode/
    /.idea
    .project
    .classpath
    .c9/
    *.launch
    .settings/
    *.sublime-workspace

    # IDE - VSCode
    .vscode/*
    !.vscode/settings.json
    !.vscode/tasks.json
    !.vscode/launch.json
    !.vscode/extensions.json

    # misc
    /.angular/cache
    /.sass-cache
    /connect.lock
    /coverage
    /libpeerconnection.log
    npm-debug.log
    testem.log
    /typings
    yarn-error.log

    # e2e
    /e2e/*.js
    /e2e/*.map

    # System Files
    .DS_Store
    Thumbs.db
    
   .travis.yml

    language: node_js
    node_js:
      - "12.3.1"

    branches:
      only:
        - master

    before_script:
      - yarn install --frozen-lockfile

    env:
      - NG_CLI_ANALYTICS=ci

    script:
      - yarn build

    deploy:
      provider: pages
      skip-cleanup: true
      github-token: $GITHUB_TOKEN  # Set in travis-ci.org dashboard, marked secure
      keep-history: true
      on:
        branch: master
      local_dir: dist
    
   LICENSE.txt

    MIT License

    Copyright (c) [2022] [Khaled Osman]

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    
   README.md

    [![RealWorld Frontend](https://img.shields.io/badge/realworld-frontend-%23783578.svg)](http://realworld.io)
    [![Netlify Status](https://api.netlify.com/api/v1/badges/b0c71b0c-d430-4547-a10e-c84ce57cd2a1/deploy-status)](https://app.netlify.com/sites/angular-realworld/deploys)

    # ![Angular Example App](logo.png)

    > ### Angular codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API.


    <a href="https://stackblitz.com/edit/angular-realworld" target="_blank"><img width="187" src="https://github.com/gothinkster/realworld/blob/master/media/edit_on_blitz.png?raw=true" /></a>&nbsp;&nbsp;<a href="https://thinkster.io/tutorials/building-real-world-angular-2-apps" target="_blank"><img width="384" src="https://raw.githubusercontent.com/gothinkster/realworld/master/media/learn-btn-hr.png" /></a>

    ### [Demo](https://angular-realworld.netlify.app/)&nbsp;&nbsp;&nbsp;&nbsp;[RealWorld](https://github.com/gothinkster/realworld)



    This codebase was created to demonstrate a fully fledged application built with Angular that interacts with an actual backend server including CRUD operations, authentication, routing, pagination, and more. We've gone to great lengths to adhere to the [Angular Styleguide](https://angular.io/styleguide) & best practices.

    Additionally, there is an Angular 1.5 version of this codebase that you can [fork](https://github.com/gothinkster/angularjs-realworld-example-app) and/or [learn how to recreate](https://thinkster.io/angularjs-es6-tutorial).


    # How it works

    We're currently working on some docs for the codebase (explaining where functionality is located, how it works, etc) but the codebase should be straightforward to follow as is. We've also released a [step-by-step tutorial w/ screencasts](https://thinkster.io/tutorials/building-real-world-angular-2-apps) that teaches you how to recreate the codebase from scratch.

    ### Making requests to the backend API

    For convenience, we have a live API server running at https://api.realworld.io/api for the application to make requests against. You can view [the API spec here](https://github.com/GoThinkster/productionready/blob/master/api) which contains all routes & responses for the server.

    The source code for the backend server (available for Node, Rails and Django) can be found in the [main RealWorld repo](https://github.com/gothinkster/realworld).

    If you want to change the API URL to a local server, simply edit `src/environments/environment.ts` and change `api_url` to the local server's URL (i.e. `localhost:3000/api`)


    # Getting started

    Make sure you have the [Angular CLI](https://github.com/angular/angular-cli#installation) installed globally. We use [Yarn](https://yarnpkg.com) to manage the dependencies, so we strongly recommend you to use it. you can install it from [Here](https://yarnpkg.com/en/docs/install), then run `yarn install` to resolve all dependencies (might take a minute).

    Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.

    ### Building the project
    Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.


    ## Functionality overview

    The example application is a social blogging site (i.e. a Medium.com clone) called "Conduit". It uses a custom API for all requests, including authentication. You can view a live demo over at https://angular.realworld.io

    **General functionality:**

    - Authenticate users via JWT (login/signup pages + logout button on settings page)
    - CRU* users (sign up & settings page - no deleting required)
    - CRUD Articles
    - CR*D Comments on articles (no updating required)
    - GET and display paginated lists of articles
    - Favorite articles
    - Follow other users

    **The general page breakdown looks like this:**

    - Home page (URL: /#/ )
        - List of tags
        - List of articles pulled from either Feed, Global, or by Tag
        - Pagination for list of articles
    - Sign in/Sign up pages (URL: /#/login, /#/register )
        - Uses JWT (store the token in localStorage)
        - Authentication can be easily switched to session/cookie based
    - Settings page (URL: /#/settings )
    - Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here )
    - Article page (URL: /#/article/article-slug-here )
        - Delete article button (only shown to article's author)
        - Render markdown from server client side
        - Comments section at bottom of page
        - Delete comment button (only shown to comment's author)
    - Profile page (URL: /#/profile/:username, /#/profile/:username/favorites )
        - Show basic user info
        - List of articles populated from author's created articles or author's favorited articles


    <br />

    [![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io)
    
   angular.json

    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "ang2-conduit": {
          "root": "",
          "sourceRoot": "src",
          "projectType": "application",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "outputPath": "dist",
                "index": "src/index.html",
                "main": "src/main.ts",
                "tsConfig": "src/tsconfig.app.json",
                "polyfills": "src/polyfills.ts",
                "assets": [
                  "src/assets",
                  "src/favicon.ico",
                  "src/manifest.webmanifest"
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [],
                "namedChunks": true,
                "vendorChunk": true,
                "extractLicenses": false,
                "buildOptimizer": false,
                "sourceMap": true,
                "optimization": false,
                "aot": true
              },
              "configurations": {
                "production": {
                  "optimization": true,
                  "outputHashing": "all",
                  "sourceMap": false,
                  "extractLicenses": true,
                  "vendorChunk": false,
                  "buildOptimizer": true,
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "2mb",
                      "maximumError": "5mb"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "6kb"
                    }
                  ],
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "serviceWorker": true,
                  "ngswConfigPath": "ngsw-config.json"
                }
              },
              "defaultConfiguration": ""
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "options": {
                "browserTarget": "ang2-conduit:build"
              },
              "configurations": {
                "production": {
                  "browserTarget": "ang2-conduit:build:production"
                }
              }
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "ang2-conduit:build"
              }
            },
            "test": {
              "builder": "@angular-devkit/build-angular:karma",
              "options": {
                "main": "src/test.ts",
                "karmaConfig": "./karma.conf.js",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.spec.json",
                "scripts": [],
                "styles": [
                  "src/styles.css"
                ],
                "assets": [
                  "src/assets",
                  "src/favicon.ico",
                  "src/manifest.webmanifest"
                ]
              }
            },
            "lint": {
              "builder": "@angular-eslint/builder:lint",
              "options": {
                "lintFilePatterns": [
                  "src/**/*.ts",
                  "src/**/*.html"
                ]
              }
            }
          }
        },
        "ang2-conduit-e2e": {
          "root": "e2e",
          "sourceRoot": "e2e",
          "projectType": "application",
          "architect": {
            "e2e": {
              "builder": "@angular-devkit/build-angular:protractor",
              "options": {
                "protractorConfig": "./protractor.conf.js",
                "devServerTarget": "ang2-conduit:serve"
              }
            }
          }
        }
      },
      "defaultProject": "ang2-conduit",
      "schematics": {
        "@schematics/angular:component": {
          "prefix": "app",
          "style": "css"
        },
        "@schematics/angular:directive": {
          "prefix": "app"
        }
      },
      "cli": {
        "analytics": false,
        "defaultCollection": "@angular-eslint/schematics"
      }
    }
    
   e2e
   +---- app.e2e-spec.ts

    import { Ng2RealApp } from './app.po';

    describe('ng-demo App', () => {
      let page: Ng2RealApp;

      beforeEach(() => {
        page = new Ng2RealApp();
      });

      it('should display message saying app works', () => {
        page.navigateTo();
        expect(page.getParagraphText()).toContain('conduit');
      });
    });
    
   +---- app.po.ts

    import { browser, element, by } from 'protractor';

    export class Ng2RealApp {
      navigateTo() {
        return browser.get('/');
      }

        getParagraphText() {
        return element(by.css('.logo-font')).getText();
      }
    }
    
   +---- tsconfig.e2e.json

    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/e2e",
        "baseUrl": "./",
        "module": "commonjs",
        "target": "es5",
        "types": [
          "jasmine",
          "jasminewd2",
          "node"
        ]
      }
    }
    
   karma.conf.js

    // Karma configuration file, see link for more information
    // https://karma-runner.github.io/1.0/config/configuration-file.html

    module.exports = function(config) {
      config.set({
        basePath: '',
        frameworks: ['jasmine', '@angular-devkit/build-angular'],
        plugins: [
          require('karma-jasmine'),
          require('karma-chrome-launcher'),
          require('karma-jasmine-html-reporter'),
          require('karma-coverage-istanbul-reporter'),
          require('@angular-devkit/build-angular/plugins/karma')
        ],
        client: {
          clearContext: false // leave Jasmine Spec Runner output visible in browser
        },
        coverageIstanbulReporter: {
          dir: require('path').join(__dirname, 'coverage'), reports: ['html', 'lcovonly'],
          fixWebpackSourcePaths: true
        },

        reporters:
          config.angularCli && config.angularCli.codeCoverage
            ? ['progress', 'coverage-istanbul']
            : ['progress', 'kjhtml'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['Chrome'],
        singleRun: false
      });
    };
    
   ngsw-config.json

    {
      "$schema": "./node_modules/@angular/service-worker/config/schema.json",
      "index": "/index.html",
      "assetGroups": [
        {
          "name": "app",
          "installMode": "prefetch",
          "resources": {
            "files": [
              "/favicon.ico",
              "/index.html",
              "/*.css",
              "/*.js",
              "/manifest.webmanifest"
            ]
          }
        }, {
          "name": "assets",
          "installMode": "lazy",
          "updateMode": "prefetch",
          "resources": {
            "files": [
              "/assets/**",
              "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
            ]
          }
        }
      ]
    }
    
   package.json

    {
      "name": "ang2-conduit",
      "version": "0.0.0",
      "license": "MIT",
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "build": "ng build --configuration production  --base-href ./ && cp CNAME dist/CNAME",
        "test": "ng test",
        "lint": "ng lint --force",
        "e2e": "ng e2e",
        "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
      },
      "pre-commit": [
        "lint"
      ],
      "private": true,
      "dependencies": {
        "@angular/animations": "13.3.2",
        "@angular/common": "13.3.2",
        "@angular/compiler": "13.3.2",
        "@angular/core": "13.3.2",
        "@angular/forms": "13.3.2",
        "@angular/platform-browser": "13.3.2",
        "@angular/platform-browser-dynamic": "13.3.2",
        "@angular/router": "13.3.2",
        "@angular/service-worker": "13.3.2",
        "core-js": "^3.21.1",
        "marked": "^4.0.14",
        "ngx-quicklink": "^0.2.7",
        "rxjs": "^7.5.5",
        "tslib": "^2.3.1",
        "zone.js": "~0.11.5"
      },
      "devDependencies": {
        "@angular-devkit/build-angular": "~13.3.2",
        "@angular-eslint/builder": "13.2.0",
        "@angular-eslint/eslint-plugin": "13.2.0",
        "@angular-eslint/eslint-plugin-template": "13.2.0",
        "@angular-eslint/schematics": "13.2.0",
        "@angular-eslint/template-parser": "13.2.0",
        "@angular/cli": "^13.3.2",
        "@angular/compiler-cli": "13.3.2",
        "@angular/language-service": "13.3.2",
        "@types/jasmine": "~4.0.2",
        "@types/jasminewd2": "~2.0.10",
        "@types/node": "^17.0.23",
        "@typescript-eslint/eslint-plugin": "5.17.0",
        "@typescript-eslint/parser": "5.17.0",
        "eslint": "^8.12.0",
        "jasmine-core": "~4.1.0",
        "jasmine-spec-reporter": "~7.0.0",
        "karma": "~6.3.18",
        "karma-chrome-launcher": "~3.1.1",
        "karma-cli": "~2.0.0",
        "karma-coverage-istanbul-reporter": "~3.0.3",
        "karma-jasmine": "~5.0.0",
        "karma-jasmine-html-reporter": "^1.7.0",
        "pre-commit": "^1.2.2",
        "protractor": "~7.0.0",
        "ts-node": "~10.7.0",
        "typescript": "4.6.3"
      }
    }
   protractor.conf.js

    // Protractor configuration file, see link for more information
    // https://github.com/angular/protractor/blob/master/lib/config.ts

    const { SpecReporter } = require('jasmine-spec-reporter');

    exports.config = {
      allScriptsTimeout: 11000,
      specs: [
        './e2e/**/*.e2e-spec.ts'
      ],
      capabilities: {
        'browserName': 'chrome'
      },
      directConnect: true,
      baseUrl: 'http://localhost:4200/',
      framework: 'jasmine',
      jasmineNodeOpts: {
        showColors: true,
        defaultTimeoutInterval: 30000,
        print: function() {}
      },
      onPrepare() {
        require('ts-node').register({
          project: 'e2e/tsconfig.e2e.json'
        });
        jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
      }
    };
    
   src
   +---- app
   |      +---- app-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { QuicklinkModule, QuicklinkStrategy } from 'ngx-quicklink';

    const routes: Routes = [
      {
        path: 'settings',
        loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
      },
      {
        path: 'profile',
        loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule)
      },
      {
        path: 'editor',
        loadChildren: () => import('./editor/editor.module').then(m => m.EditorModule)
      },
      {
        path: 'article',
        loadChildren: () => import('./article/article.module').then(m => m.ArticleModule)
      }
    ];

    @NgModule({
      imports: [
        QuicklinkModule,
        RouterModule.forRoot(routes, {
        // preload all modules; optionally we could
        // implement a custom preloading strategy for just some
        // of the modules (PRs welcome 😉)
        preloadingStrategy: QuicklinkStrategy,
        relativeLinkResolution: 'legacy'
    })],
      exports: [RouterModule]
    })
    export class AppRoutingModule {}
    
   |      +---- app.component.html

    <app-layout-header></app-layout-header>

    <router-outlet></router-outlet>

    <app-layout-footer></app-layout-footer>
    
   |      +---- app.component.ts

    import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';

    import { UserService } from "./core";

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class AppComponent implements OnInit {
      constructor(private userService: UserService) {}

      ngOnInit() {
        this.userService.populate();
      }
    }
    
   |      +---- app.module.ts

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';

    import { AppComponent } from './app.component';
    import { AuthModule } from './auth/auth.module';
    import { HomeModule } from './home/home.module';
    import {
      FooterComponent,
      HeaderComponent,
      SharedModule
    } from './shared';
    import { AppRoutingModule } from './app-routing.module';
    import { CoreModule } from './core/core.module';
    import { ServiceWorkerModule } from '@angular/service-worker';
    import { environment } from '../environments/environment';

    @NgModule({
      declarations: [AppComponent, FooterComponent, HeaderComponent],
      imports: [
        BrowserModule,
        CoreModule,
        SharedModule,
        HomeModule,
        AuthModule,
        AppRoutingModule,
        ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    
   |      +---- article
   |            +---- article-comment.component.html

    <div class="card">
      <div class="card-block">
        <p class="card-text">
          {{ comment.body }}
        </p>
      </div>
      <div class="card-footer">
        <a class="comment-author" [routerLink]="['/profile', comment.author.username]">
          <img [src]="comment.author.image || 'https://static.productionready.io/images/smiley-cyrus.jpg'" alt="author image" class="comment-author-img" />
        </a>
        &nbsp;
        <a class="comment-author" [routerLink]="['/profile', comment.author.username]">
          {{ comment.author.username }}
        </a>
        <span class="date-posted">
          {{ comment.createdAt | date: 'longDate' }}
        </span>
        <span class="mod-options" [hidden]="!canModify">
          <i class="ion-trash-a" (click)="deleteClicked()"></i>
        </span>
      </div>
    </div>
    
   |            +---- article-comment.component.ts

    import { Component, EventEmitter, Input, Output, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

    import { Comment, User, UserService } from '../core';
    import { Subscription } from 'rxjs';

    @Component({
      selector: 'app-article-comment',
      templateUrl: './article-comment.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ArticleCommentComponent implements OnInit, OnDestroy {
      constructor(
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) {}

      private subscription: Subscription;

      @Input() comment: Comment;
      @Output() deleteComment = new EventEmitter<boolean>();

      canModify: boolean;

      ngOnInit() {
        // Load the current user's data
        this.subscription = this.userService.currentUser.subscribe(
          (userData: User) => {
            this.canModify = (userData.username === this.comment.author.username);
            this.cd.markForCheck();
          }
        );
      }

      ngOnDestroy() {
        this.subscription.unsubscribe();
      }

      deleteClicked() {
        this.deleteComment.emit(true);
      }


    }
    
   |            +---- article-resolver.service.ts

    import { Injectable, } from '@angular/core';
    import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { Article, ArticlesService, UserService } from '../core';
    import { catchError } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ArticleResolver implements Resolve<Article> {
      constructor(
        private articlesService: ArticlesService,
        private router: Router,
        private userService: UserService
      ) {}

      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<any> {

        return this.articlesService.get(route.params['slug'])
          .pipe(catchError((err) => this.router.navigateByUrl('/')));
      }
    }
    
   |            +---- article-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { ArticleComponent } from './article.component';
    import { ArticleResolver } from './article-resolver.service';

    const routes: Routes = [
      {
        path: ':slug',
        component: ArticleComponent,
        resolve: {
          article: ArticleResolver
        }
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class ArticleRoutingModule {}
    
   |            +---- article.component.html

    <div class="article-page">

      <div class="banner">
        <div class="container">
          <h1>{{ article.title }}</h1>

          <app-article-meta [article]="article">

              <span [hidden]="!canModify">
              <a class="btn btn-sm btn-outline-secondary"
                 [routerLink]="['/editor', article.slug]">
                <i class="ion-edit"></i> Edit Article
              </a>

              <button class="btn btn-sm btn-outline-danger"
                [ngClass]="{disabled: isDeleting}"
                (click)="deleteArticle()">
                <i class="ion-trash-a"></i> Delete Article
              </button>
            </span>

            <span [hidden]="canModify">
              <app-follow-button
                [profile]="article.author"
                (toggle)="onToggleFollowing($event)">
              </app-follow-button>

              <app-favorite-button
                [article]="article"
                (toggle)="onToggleFavorite($event)">
                {{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article <span class="counter">({{ article.favoritesCount }})</span>
              </app-favorite-button>
            </span>

          </app-article-meta>
        </div>
      </div>

      <div class="container page">

        <div class="row article-content">
          <div class="col-md-12">

            <div [innerHTML]="article.body | markdown"></div>

            <ul class="tag-list">
              <li *ngFor="let tag of article.tagList; trackBy: trackByFn"
                class="tag-default tag-pill tag-outline">
                {{ tag }}
              </li>
            </ul>

          </div>
        </div>

        <hr />

        <div class="article-actions">
          <app-article-meta [article]="article">

              <span [hidden]="!canModify">
              <a class="btn btn-sm btn-outline-secondary"
                 [routerLink]="['/editor', article.slug]">
                <i class="ion-edit"></i> Edit Article
              </a>

              <button class="btn btn-sm btn-outline-danger"
                [ngClass]="{disabled: isDeleting}"
                (click)="deleteArticle()">
                <i class="ion-trash-a"></i> Delete Article
              </button>
            </span>

            <span [hidden]="canModify">
              <app-follow-button
                [profile]="article.author"
                (toggle)="onToggleFollowing($event)">
              </app-follow-button>

              <app-favorite-button
                [article]="article"
                (toggle)="onToggleFavorite($event)">
                {{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article <span class="counter">({{ article.favoritesCount }})</span>
              </app-favorite-button>
            </span>

          </app-article-meta>
        </div>

        <div class="row">
          <div class="col-xs-12 col-md-8 offset-md-2">

            <div *appShowAuthed="true">
              <app-list-errors [errors]="commentFormErrors"></app-list-errors>
              <form class="card comment-form" (ngSubmit)="addComment()">
                <fieldset [disabled]="isSubmitting">
                  <div class="card-block">
                    <textarea class="form-control"
                      placeholder="Write a comment..."
                      rows="3"
                      [formControl]="commentControl"
                    ></textarea>
                  </div>
                  <div class="card-footer">
                    <img [src]="currentUser.image || 'https://static.productionready.io/images/smiley-cyrus.jpg'" alt="user image" class="comment-author-img" />
                    <button class="btn btn-sm btn-primary" type="submit">
                     Post Comment
                    </button>
                  </div>
                </fieldset>
              </form>
            </div>

            <div *appShowAuthed="false">
              <a [routerLink]="['/login']">Sign in</a> or <a [routerLink]="['/register']">sign up</a> to add comments on this article.
            </div>

            <app-article-comment
              *ngFor="let comment of comments; trackBy: trackByFn"
              [comment]="comment"
              (deleteComment)="onDeleteComment(comment)">
            </app-article-comment>

          </div>
        </div>

      </div>
    </div>
    
   |            +---- article.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { FormControl } from '@angular/forms';
    import { ActivatedRoute, Router } from '@angular/router';

    import {
      Article,
      ArticlesService,
      Comment,
      CommentsService,
      User,
      UserService
    } from '../core';

    @Component({
      selector: 'app-article-page',
      templateUrl: './article.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ArticleComponent implements OnInit {
      article: Article;
      currentUser: User;
      canModify: boolean;
      comments: Comment[];
      commentControl = new FormControl();
      commentFormErrors = {};
      isSubmitting = false;
      isDeleting = false;

      constructor(
        private route: ActivatedRoute,
        private articlesService: ArticlesService,
        private commentsService: CommentsService,
        private router: Router,
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) { }

      ngOnInit() {
        // Retreive the prefetched article
        this.route.data.subscribe(
          (data: { article: Article }) => {
            this.article = data.article;

            // Load the comments on this article
            this.populateComments();
            this.cd.markForCheck();
          }
        );

        // Load the current user's data
        this.userService.currentUser.subscribe(
          (userData: User) => {
            this.currentUser = userData;

            this.canModify = (this.currentUser.username === this.article.author.username);
            this.cd.markForCheck();
          }
        );
      }

      onToggleFavorite(favorited: boolean) {
        this.article.favorited = favorited;

        if (favorited) {
          this.article.favoritesCount++;
        } else {
          this.article.favoritesCount--;
        }
      }

      trackByFn(index, item) {
        return index;
      }

      onToggleFollowing(following: boolean) {
        this.article.author.following = following;
      }

      deleteArticle() {
        this.isDeleting = true;

        this.articlesService.destroy(this.article.slug)
          .subscribe(
            success => {
              this.router.navigateByUrl('/');
            }
          );
      }

      populateComments() {
        this.commentsService.getAll(this.article.slug)
          .subscribe(comments => {
            this.comments = comments;
            this.cd.markForCheck();
          });
      }

      addComment() {
        this.isSubmitting = true;
        this.commentFormErrors = {};

        const commentBody = this.commentControl.value;
        this.commentsService
          .add(this.article.slug, commentBody)
          .subscribe(
            comment => {
              this.comments.unshift(comment);
              this.commentControl.reset('');
              this.isSubmitting = false;
              this.cd.markForCheck();
            },
            errors => {
              this.isSubmitting = false;
              this.commentFormErrors = errors;
              this.cd.markForCheck();
            }
          );
      }

      onDeleteComment(comment) {
        this.commentsService.destroy(comment.id, this.article.slug)
          .subscribe(
            success => {
              this.comments = this.comments.filter((item) => item !== comment);
              this.cd.markForCheck();
            }
          );
      }

    }
    
   |            +---- article.module.ts

    import { NgModule } from '@angular/core';

    import { ArticleComponent } from './article.component';
    import { ArticleCommentComponent } from './article-comment.component';
    import { MarkdownPipe } from './markdown.pipe';
    import { SharedModule } from '../shared';
    import { ArticleRoutingModule } from './article-routing.module';

    @NgModule({
      imports: [
        SharedModule,
        ArticleRoutingModule
      ],
      declarations: [
        ArticleComponent,
        ArticleCommentComponent,
        MarkdownPipe
      ],

      providers: [
      ]
    })
    export class ArticleModule {}
    
   |            +---- markdown.pipe.ts

    import { Pipe, PipeTransform } from '@angular/core';
    import * as marked from 'marked';

    @Pipe({name: 'markdown'})
    export class MarkdownPipe implements PipeTransform {
      transform(content: string): string {
        return marked(content, { sanitize: true });
      }
    }
    
   |      +---- auth
   |            +---- auth-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { AuthComponent } from './auth.component';
    import { NoAuthGuard } from './no-auth-guard.service';

    const routes: Routes = [
      {
        path: 'login',
        component: AuthComponent,
        canActivate: [NoAuthGuard]
      },
      {
        path: 'register',
        component: AuthComponent,
        canActivate: [NoAuthGuard]
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class AuthRoutingModule {}
    
   |            +---- auth.component.html

    <div class="auth-page">
      <div class="container page">
        <div class="row">

          <div class="col-md-6 offset-md-3 col-xs-12">
            <h1 class="text-xs-center">{{ title }}</h1>
            <p class="text-xs-center">
              <a [routerLink]="['/login']" *ngIf="authType === 'register'">Have an account?</a>
              <a [routerLink]="['/register']" *ngIf="authType === 'login'">Need an account?</a>
            </p>
            <app-list-errors [errors]="errors"></app-list-errors>
            <form [formGroup]="authForm" (ngSubmit)="submitForm()">
              <fieldset [disabled]="isSubmitting">
                <fieldset class="form-group">
                  <input
                    formControlName="username"
                    placeholder="Username"
                    class="form-control form-control-lg"
                    type="text"
                    *ngIf="authType === 'register'" />
                </fieldset>
                <fieldset class="form-group">
                  <input
                    formControlName="email"
                    placeholder="Email"
                    class="form-control form-control-lg"
                    type="text" />
                </fieldset>
                <fieldset class="form-group">
                  <input
                    formControlName="password"
                    placeholder="Password"
                    class="form-control form-control-lg"
                    type="password" />
                </fieldset>
                <button class="btn btn-lg btn-primary pull-xs-right" [disabled]="!authForm.valid" type="submit">
                  {{ title }}
                </button>
              </fieldset>
            </form>
          </div>

        </div>
      </div>
    </div>
    
   |            +---- auth.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
    import { ActivatedRoute, Router } from '@angular/router';

    import { Errors, UserService } from '../core';

    @Component({
      selector: 'app-auth-page',
      templateUrl: './auth.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class AuthComponent implements OnInit {
      authType: String = '';
      title: String = '';
      errors: Errors = {errors: {}};
      isSubmitting = false;
      authForm: FormGroup;

      constructor(
        private route: ActivatedRoute,
        private router: Router,
        private userService: UserService,
        private fb: FormBuilder,
        private cd: ChangeDetectorRef
      ) {
        // use FormBuilder to create a form group
        this.authForm = this.fb.group({
          'email': ['', Validators.required],
          'password': ['', Validators.required]
        });
      }

      ngOnInit() {
        this.route.url.subscribe(data => {
          // Get the last piece of the URL (it's either 'login' or 'register')
          this.authType = data[data.length - 1].path;
          // Set a title for the page accordingly
          this.title = (this.authType === 'login') ? 'Sign in' : 'Sign up';
          // add form control for username if this is the register page
          if (this.authType === 'register') {
            this.authForm.addControl('username', new FormControl());
          }
          this.cd.markForCheck();
        });
      }

      submitForm() {
        this.isSubmitting = true;
        this.errors = {errors: {}};

        const credentials = this.authForm.value;
        this.userService
        .attemptAuth(this.authType, credentials)
        .subscribe(
          data => this.router.navigateByUrl('/'),
          err => {
            this.errors = err;
            this.isSubmitting = false;
            this.cd.markForCheck();
          }
        );
      }
    }
    
   |            +---- auth.module.ts

    import { NgModule } from '@angular/core';

    import { AuthComponent } from './auth.component';
    import { NoAuthGuard } from './no-auth-guard.service';
    import { SharedModule } from '../shared';
    import { AuthRoutingModule } from './auth-routing.module';

    @NgModule({
      imports: [
        SharedModule,
        AuthRoutingModule
      ],
      declarations: [
        AuthComponent
      ],
      providers: [
        NoAuthGuard
      ]
    })
    export class AuthModule {}
    
   |            +---- no-auth-guard.service.ts

    import { Injectable } from '@angular/core';
    import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { UserService } from '../core';
    import { map ,  take } from 'rxjs/operators';

    @Injectable()
    export class NoAuthGuard implements CanActivate {
      constructor(
        private router: Router,
        private userService: UserService
      ) {}

      canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<boolean> {

        return this.userService.isAuthenticated.pipe(take(1), map(isAuth => !isAuth));

      }
    }
    
   |      +---- core
   |            +---- core.module.ts

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { HttpTokenInterceptor } from './interceptors/http.token.interceptor';

    @NgModule({
      imports: [
        CommonModule
      ],
      providers: [
        { provide: HTTP_INTERCEPTORS, useClass: HttpTokenInterceptor, multi: true }
      ],
      declarations: []
    })
    export class CoreModule { }
    
   |            +---- index.ts

    export * from './core.module';
    export * from './services';
    export * from './models';
    export * from './interceptors';
    
   |            +---- interceptors
   |                  +---- http.token.interceptor.ts

    import { Injectable, Injector } from '@angular/core';
    import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
    import { Observable } from 'rxjs';

    import { JwtService } from '../services';

    @Injectable()
    export class HttpTokenInterceptor implements HttpInterceptor {
      constructor(private jwtService: JwtService) {}

      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const headersConfig = {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        };

        const token = this.jwtService.getToken();

        if (token) {
          headersConfig['Authorization'] = `Token ${token}`;
        }

        const request = req.clone({ setHeaders: headersConfig });
        return next.handle(request);
      }
    }
    
   |                  +---- index.ts

    export * from './http.token.interceptor';
    
   |            +---- models
   |                  +---- article-list-config.model.ts

    export interface ArticleListConfig {
      type: string;

      filters: {
        tag?: string,
        author?: string,
        favorited?: string,
        limit?: number,
        offset?: number
      };
    }
    
   |                  +---- article.model.ts

    import { Profile } from './profile.model';

    export interface Article {
      slug: string;
      title: string;
      description: string;
      body: string;
      tagList: string[];
      createdAt: string;
      updatedAt: string;
      favorited: boolean;
      favoritesCount: number;
      author: Profile;
    }
    
   |                  +---- comment.model.ts

    import { Profile } from './profile.model';

    export interface Comment {
      id: number;
      body: string;
      createdAt: string;
      author: Profile;
    }
    
   |                  +---- errors.model.ts

    export interface Errors {
      errors: {[key: string]: string};
    }
    
   |                  +---- index.ts

    export * from './article.model';
    export * from './article-list-config.model';
    export * from './comment.model';
    export * from './errors.model';
    export * from './profile.model';
    export * from './user.model';
    
   |                  +---- profile.model.ts

    export interface Profile {
      username: string;
      bio: string;
      image: string;
      following: boolean;
    }
    
   |                  +---- user.model.ts

    export interface User {
      email: string;
      token: string;
      username: string;
      bio: string;
      image: string;
    }
    
   |            +---- services
   |                  +---- api.service.ts

    import { Injectable } from '@angular/core';
    import { environment } from '../../../environments/environment';
    import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http';
    import { Observable ,  throwError } from 'rxjs';

    import { catchError } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ApiService {
      constructor(
        private http: HttpClient
      ) {}

      private formatErrors(error: any) {
        return  throwError(error.error);
      }

      get(path: string, params: HttpParams = new HttpParams()): Observable<any> {
        return this.http.get(`${environment.api_url}${path}`, { params })
          .pipe(catchError(this.formatErrors));
      }

      put(path: string, body: Object = {}): Observable<any> {
        return this.http.put(
          `${environment.api_url}${path}`,
          JSON.stringify(body)
        ).pipe(catchError(this.formatErrors));
      }

      post(path: string, body: Object = {}): Observable<any> {
        return this.http.post(
          `${environment.api_url}${path}`,
          JSON.stringify(body)
        ).pipe(catchError(this.formatErrors));
      }

      delete(path): Observable<any> {
        return this.http.delete(
          `${environment.api_url}${path}`
        ).pipe(catchError(this.formatErrors));
      }
    }
    
   |                  +---- articles.service.ts

    import { Injectable } from '@angular/core';
    import { HttpParams } from '@angular/common/http';
    import { Observable } from 'rxjs';

    import { ApiService } from './api.service';
    import { Article, ArticleListConfig } from '../models';
    import { map } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ArticlesService {
      constructor (
        private apiService: ApiService
      ) {}

      query(config: ArticleListConfig): Observable<{articles: Article[], articlesCount: number}> {
        // Convert any filters over to Angular's URLSearchParams
        const params = {};

        Object.keys(config.filters)
        .forEach((key) => {
          params[key] = config.filters[key];
        });

        return this.apiService
        .get(
          '/articles' + ((config.type === 'feed') ? '/feed' : ''),
          new HttpParams({ fromObject: params })
        );
      }

      get(slug): Observable<Article> {
        return this.apiService.get('/articles/' + slug)
          .pipe(map(data => data.article));
      }

      destroy(slug) {
        return this.apiService.delete('/articles/' + slug);
      }

      save(article): Observable<Article> {
        // If we're updating an existing article
        if (article.slug) {
          return this.apiService.put('/articles/' + article.slug, {article: article})
            .pipe(map(data => data.article));

        // Otherwise, create a new article
        } else {
          return this.apiService.post('/articles/', {article: article})
            .pipe(map(data => data.article));
        }
      }

      favorite(slug): Observable<Article> {
        return this.apiService.post('/articles/' + slug + '/favorite');
      }

      unfavorite(slug): Observable<Article> {
        return this.apiService.delete('/articles/' + slug + '/favorite');
      }


    }
    
   |                  +---- auth-guard.service.ts

    import { Injectable } from '@angular/core';
    import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { UserService } from './user.service';
    import { take } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class AuthGuard implements CanActivate {
      constructor(
        private router: Router,
        private userService: UserService
      ) {}

      canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<boolean> {

        return this.userService.isAuthenticated.pipe(take(1));

      }
    }
    
   |                  +---- comments.service.ts

    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs';

    import { ApiService } from './api.service';
    import { Comment } from '../models';
    import { map } from 'rxjs/operators';


    @Injectable({
      providedIn: 'root'
    })
    export class CommentsService {
      constructor (
        private apiService: ApiService
      ) {}

      add(slug, payload): Observable<Comment> {
        return this.apiService
        .post(
          `/articles/${slug}/comments`,
          { comment: { body: payload } }
        ).pipe(map(data => data.comment));
      }

      getAll(slug): Observable<Comment[]> {
        return this.apiService.get(`/articles/${slug}/comments`)
          .pipe(map(data => data.comments));
      }

      destroy(commentId, articleSlug) {
        return this.apiService
               .delete(`/articles/${articleSlug}/comments/${commentId}`);
      }

    }
    
   |                  +---- index.ts

    export * from './api.service';
    export * from './articles.service';
    export * from './auth-guard.service';
    export * from './comments.service';
    export * from './jwt.service';
    export * from './profiles.service';
    export * from './tags.service';
    export * from './user.service';
    
   |                  +---- jwt.service.ts

    import { Injectable } from '@angular/core';


    @Injectable({
      providedIn: 'root'
    })
    export class JwtService {

      getToken(): String {
        return window.localStorage['jwtToken'];
      }

      saveToken(token: String) {
        window.localStorage['jwtToken'] = token;
      }

      destroyToken() {
        window.localStorage.removeItem('jwtToken');
      }

    }
    
   |                  +---- profiles.service.ts

    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs';

    import { ApiService } from './api.service';
    import { Profile } from '../models';
    import { map } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ProfilesService {
      constructor (
        private apiService: ApiService
      ) {}

      get(username: string): Observable<Profile> {
        return this.apiService.get('/profiles/' + username)
          .pipe(map((data: {profile: Profile}) => data.profile));
      }

      follow(username: string): Observable<Profile> {
        return this.apiService.post('/profiles/' + username + '/follow');
      }

      unfollow(username: string): Observable<Profile> {
        return this.apiService.delete('/profiles/' + username + '/follow');
      }

    }
    
   |                  +---- tags.service.ts

    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs';

    import { ApiService } from './api.service';
    import { map } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class TagsService {
      constructor (
        private apiService: ApiService
      ) {}

      getAll(): Observable<[string]> {
        return this.apiService.get('/tags')
              .pipe(map(data => data.tags));
      }

    }
    
   |                  +---- user.service.ts

    import { Injectable } from '@angular/core';
    import { Observable ,  BehaviorSubject ,  ReplaySubject } from 'rxjs';

    import { ApiService } from './api.service';
    import { JwtService } from './jwt.service';
    import { User } from '../models';
    import { map ,  distinctUntilChanged } from 'rxjs/operators';


    @Injectable({
      providedIn: 'root'
    })
    export class UserService {
      private currentUserSubject = new BehaviorSubject<User>({} as User);
      public currentUser = this.currentUserSubject.asObservable().pipe(distinctUntilChanged());

      private isAuthenticatedSubject = new ReplaySubject<boolean>(1);
      public isAuthenticated = this.isAuthenticatedSubject.asObservable();

      constructor (
        private apiService: ApiService,
        private jwtService: JwtService
      ) {}

      // Verify JWT in localstorage with server & load user's info.
      // This runs once on application startup.
      populate() {
        // If JWT detected, attempt to get & store user's info
        if (this.jwtService.getToken()) {
          this.apiService.get('/user')
          .subscribe(
            data => this.setAuth(data.user),
            err => this.purgeAuth()
          );
        } else {
          // Remove any potential remnants of previous auth states
          this.purgeAuth();
        }
      }

      setAuth(user: User) {
        // Save JWT sent from server in localstorage
        this.jwtService.saveToken(user.token);
        // Set current user data into observable
        this.currentUserSubject.next(user);
        // Set isAuthenticated to true
        this.isAuthenticatedSubject.next(true);
      }

      purgeAuth() {
        // Remove JWT from localstorage
        this.jwtService.destroyToken();
        // Set current user to an empty object
        this.currentUserSubject.next({} as User);
        // Set auth status to false
        this.isAuthenticatedSubject.next(false);
      }

      attemptAuth(type, credentials): Observable<User> {
        const route = (type === 'login') ? '/login' : '';
        return this.apiService.post(`/users${route}`, {user: credentials})
          .pipe(map(
          data => {
            this.setAuth(data.user);
            return data;
          }
        ));
      }

      getCurrentUser(): User {
        return this.currentUserSubject.value;
      }

      // Update the user on the server (email, pass, etc)
      update(user): Observable<User> {
        return this.apiService
        .put('/user', { user })
        .pipe(map(data => {
          // Update the currentUser observable
          this.currentUserSubject.next(data.user);
          return data.user;
        }));
      }

    }
    
   |      +---- editor
   |            +---- editable-article-resolver.service.ts

    import { Injectable } from '@angular/core';
    import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { Article, ArticlesService, UserService } from '../core';
    import { catchError ,  map } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class EditableArticleResolver implements Resolve<Article> {
      constructor(
        private articlesService: ArticlesService,
        private router: Router,
        private userService: UserService
      ) { }

      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<any> {

        return this.articlesService.get(route.params['slug'])
          .pipe(
            map(
              article => {
                if (this.userService.getCurrentUser().username === article.author.username) {
                  return article;
                } else {
                  this.router.navigateByUrl('/');
                }
              }
            ),
            catchError((err) => this.router.navigateByUrl('/'))
          );
      }
    }
    
   |            +---- editor-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { EditorComponent } from './editor.component';
    import { EditableArticleResolver } from './editable-article-resolver.service';
    import { AuthGuard } from '../core';
    import { SharedModule } from '../shared';

    const routes: Routes = [
      {
        path: '',
        component: EditorComponent,
        canActivate: [AuthGuard]
      },
      {
        path: ':slug',
        component: EditorComponent,
        canActivate: [AuthGuard],
        resolve: {
          article: EditableArticleResolver
        }
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class EditorRoutingModule {}
    
   |            +---- editor.component.html

    <div class="editor-page">
      <div class="container page">
        <div class="row">
          <div class="col-md-10 offset-md-1 col-xs-12">

            <app-list-errors [errors]="errors"></app-list-errors>

            <form [formGroup]="articleForm">
              <fieldset [disabled]="isSubmitting">

                <fieldset class="form-group">
                  <input class="form-control form-control-lg"
                    formControlName="title"
                    type="text"
                    placeholder="Article Title" />
                </fieldset>

                <fieldset class="form-group">
                  <input class="form-control"
                    formControlName="description"
                    type="text"
                    placeholder="What's this article about?" />
                </fieldset>

                <fieldset class="form-group">
                  <textarea class="form-control"
                    formControlName="body"
                    rows="8"
                    placeholder="Write your article (in markdown)">
                  </textarea>
                </fieldset>

                <fieldset class="form-group">
                  <input class="form-control"
                    type="text"
                    placeholder="Enter tags"
                    [formControl]="tagField"
                    (keyup.enter)="addTag()" />

                  <div class="tag-list">
                    <span *ngFor="let tag of article.tagList; trackBy: trackByFn"
                      class="tag-default tag-pill">
                      <i class="ion-close-round" (click)="removeTag(tag)"></i>
                      {{ tag }}
                    </span>
                  </div>
                </fieldset>

                <button class="btn btn-lg pull-xs-right btn-primary" type="button" (click)="submitForm()">
                  Publish Article
                </button>

              </fieldset>
            </form>

          </div>
        </div>
      </div>
    </div>
    
   |            +---- editor.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
    import { ActivatedRoute, Router } from '@angular/router';

    import { Article, ArticlesService } from '../core';

    @Component({
      selector: 'app-editor-page',
      templateUrl: './editor.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class EditorComponent implements OnInit {
      article: Article = {} as Article;
      articleForm: FormGroup;
      tagField = new FormControl();
      errors: Object = {};
      isSubmitting = false;

      constructor(
        private articlesService: ArticlesService,
        private route: ActivatedRoute,
        private router: Router,
        private fb: FormBuilder,
        private cd: ChangeDetectorRef
      ) {
        // use the FormBuilder to create a form group
        this.articleForm = this.fb.group({
          title: '',
          description: '',
          body: ''
        });

        // Initialized tagList as empty array
        this.article.tagList = [];

        // Optional: subscribe to value changes on the form
        // this.articleForm.valueChanges.subscribe(value => this.updateArticle(value));
      }

      ngOnInit() {
        // If there's an article prefetched, load it
        this.route.data.subscribe((data: { article: Article }) => {
          if (data.article) {
            this.article = data.article;
            this.articleForm.patchValue(data.article);
            this.cd.markForCheck();
          }
        });
      }

      trackByFn(index, item) {
        return index;
      }

      addTag() {
        // retrieve tag control
        const tag = this.tagField.value;
        // only add tag if it does not exist yet
        if (this.article.tagList.indexOf(tag) < 0) {
          this.article.tagList.push(tag);
        }
        // clear the input
        this.tagField.reset('');
      }

      removeTag(tagName: string) {
        this.article.tagList = this.article.tagList.filter(tag => tag !== tagName);
      }

      submitForm() {
        this.isSubmitting = true;

        // update the model
        this.updateArticle(this.articleForm.value);

        // post the changes
        this.articlesService.save(this.article).subscribe(
          article => {
            this.router.navigateByUrl('/article/' + article.slug);
            this.cd.markForCheck();
          },
          err => {
            this.errors = err;
            this.isSubmitting = false;
            this.cd.markForCheck();
          }
        );
      }

      updateArticle(values: Object) {
        Object.assign(this.article, values);
      }
    }
    
   |            +---- editor.module.ts

    import { NgModule } from '@angular/core';


    import { EditorComponent } from './editor.component';

    import { SharedModule } from '../shared';
    import { EditorRoutingModule } from './editor-routing.module';

    @NgModule({
      imports: [SharedModule, EditorRoutingModule],
      declarations: [EditorComponent],
      providers: []
    })
    export class EditorModule {}
    
   |      +---- home
   |            +---- home-auth-resolver.service.ts

    import { Injectable } from '@angular/core';
    import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { UserService } from '../core';
    import { take } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class HomeAuthResolver implements Resolve<boolean> {
      constructor(
        private router: Router,
        private userService: UserService
      ) {}

      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<boolean> {

        return this.userService.isAuthenticated.pipe(take(1));

      }
    }
    
   |            +---- home-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { HomeAuthResolver } from './home-auth-resolver.service';

    const routes: Routes = [
      {
        path: '',
        component: HomeComponent,
        resolve: {
          isAuthenticated: HomeAuthResolver
        }
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class HomeRoutingModule {}
    
   |            +---- home.component.css

    .nav-link {
      cursor:pointer;
    }

    .tag-pill{
      cursor:pointer;
    }
    
   |            +---- home.component.html

    <div class="home-page">

      <div class="banner" *appShowAuthed="false">
        <div class="container">
          <h1 class="logo-font">conduit</h1>
          <p>A place to share your <i>Angular</i> knowledge.</p>
        </div>
      </div>

      <div class="container page">
        <div class="row">

          <div class="col-md-9">
            <div class="feed-toggle">
              <ul class="nav nav-pills outline-active">
                <li class="nav-item">
                  <a class="nav-link"
                     [ngClass]="{'active': listConfig.type === 'feed'}"
                     (click)="setListTo('feed')">
                     Your Feed
                  </a>
                </li>
                <li class="nav-item">
                  <a class="nav-link"
                     [ngClass]="{'active': listConfig.type === 'all' && !listConfig.filters.tag}"
                     (click)="setListTo('all')">
                     Global Feed
                  </a>
                </li>
                <li class="nav-item" [hidden]="!listConfig.filters.tag">
                  <a class="nav-link active">
                    <i class="ion-pound"></i> {{ listConfig.filters.tag }}
                  </a>
                </li>
              </ul>
            </div>

            <app-article-list [limit]="10" [config]="listConfig"></app-article-list>
          </div>

          <div class="col-md-3">
            <div class="sidebar">
              <p>Popular Tags</p>

              <div class="tag-list">
                <a *ngFor="let tag of tags; trackBy: trackByFn"
                   (click)="setListTo('all', {tag: tag})"
                   class="tag-default tag-pill">
                   {{ tag }}
                </a>
              </div>

              <div [hidden]="tagsLoaded">
                Loading tags...
              </div>

              <div [hidden]="!tagsLoaded || tags.length > 0">
                No tags are here... yet.
              </div>
            </div>
          </div>

        </div>
      </div>
    </div>
    
   |            +---- home.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { Router } from '@angular/router';

    import { ArticleListConfig, TagsService, UserService } from '../core';

    @Component({
      selector: 'app-home-page',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.css'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class HomeComponent implements OnInit {
      constructor(
        private router: Router,
        private tagsService: TagsService,
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) {}

      isAuthenticated: boolean;
      listConfig: ArticleListConfig = {
        type: 'all',
        filters: {}
      };
      tags: Array<string> = [];
      tagsLoaded = false;

      ngOnInit() {
        this.userService.isAuthenticated.subscribe(
          (authenticated) => {
            this.isAuthenticated = authenticated;

            // set the article list accordingly
            if (authenticated) {
              this.setListTo('feed');
            } else {
              this.setListTo('all');
            }
            this.cd.markForCheck();
          }
        );

        this.tagsService.getAll()
        .subscribe(tags => {
          this.tags = tags;
          this.tagsLoaded = true;
          this.cd.markForCheck();
        });
      }

      trackByFn(index, item) {
        return index;
      }

      setListTo(type: string = '', filters: Object = {}) {
        // If feed is requested but user is not authenticated, redirect to login
        if (type === 'feed' && !this.isAuthenticated) {
          this.router.navigateByUrl('/login');
          return;
        }

        // Otherwise, set the list object
        this.listConfig = {type: type, filters: filters};
      }
    }
    
   |            +---- home.module.ts

    import { NgModule } from '@angular/core';

    import { HomeComponent } from './home.component';
    import { SharedModule } from '../shared';
    import { HomeRoutingModule } from './home-routing.module';

    @NgModule({
      imports: [
        SharedModule,
        HomeRoutingModule
      ],
      declarations: [
        HomeComponent
      ],
      providers: [
      ]
    })
    export class HomeModule {}
    
   |      +---- index.ts

    export * from './app.component';
    export * from './app.module';
    
   |      +---- profile
   |            +---- profile-articles.component.html

    <app-article-list [limit]="10" [config]="articlesConfig">
    </app-article-list>
    
   |            +---- profile-articles.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';

    import { ArticleListConfig, Profile } from '../core';

    @Component({
      selector: 'app-profile-articles',
      templateUrl: './profile-articles.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ProfileArticlesComponent implements OnInit {
      constructor(
        private route: ActivatedRoute,
        private router: Router,
        private cd: ChangeDetectorRef
      ) {}

      profile: Profile;
      articlesConfig: ArticleListConfig = {
        type: 'all',
        filters: {}
      };

      ngOnInit() {
        this.route.parent.data.subscribe(
          (data: {profile: Profile}) => {
            this.profile = data.profile;
            this.articlesConfig = {
              type: 'all',
              filters: {}
            }; // Only method I found to refresh article load on swap
            this.articlesConfig.filters.author = this.profile.username;
            this.cd.markForCheck();
          }
        );
      }

    }
    
   |            +---- profile-favorites.component.html

    <app-article-list [limit]="10" [config]="favoritesConfig">
    </app-article-list>
    
   |            +---- profile-favorites.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';

    import { ArticleListConfig, Profile } from '../core';

    @Component({
      selector: 'app-profile-favorites',
      templateUrl: './profile-favorites.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ProfileFavoritesComponent implements OnInit {
      constructor(
        private route: ActivatedRoute,
        private cd: ChangeDetectorRef
      ) {}

      profile: Profile;
      favoritesConfig: ArticleListConfig = {
        type: 'all',
        filters: {}
      };

      ngOnInit() {
        this.route.parent.data.subscribe(
          (data: {profile: Profile}) => {
            this.profile = data.profile;
            this.favoritesConfig = {...this.favoritesConfig};
            this.favoritesConfig.filters.favorited = this.profile.username;
            this.cd.markForCheck();
          }
        );
      }

    }
    
   |            +---- profile-resolver.service.ts

    import { Injectable, } from '@angular/core';
    import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';

    import { Profile, ProfilesService } from '../core';
    import { catchError } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ProfileResolver implements Resolve<Profile> {
      constructor(
        private profilesService: ProfilesService,
        private router: Router
      ) {}

      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<any> {

        return this.profilesService.get(route.params['username'])
          .pipe(catchError((err) => this.router.navigateByUrl('/')));

      }
    }
    
   |            +---- profile-routing.module.ts

    import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import { ProfileArticlesComponent } from './profile-articles.component';
    import { ProfileFavoritesComponent } from './profile-favorites.component';
    import { ProfileResolver } from './profile-resolver.service';
    import { ProfileComponent } from './profile.component';


    const routes: Routes = [
      {
        path: ':username',
        component: ProfileComponent,
        resolve: {
          profile: ProfileResolver
        },
        children: [
          {
            path: '',
            component: ProfileArticlesComponent
          },
          {
            path: 'favorites',
            component: ProfileFavoritesComponent
          }
        ]
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class ProfileRoutingModule {}
    
   |            +---- profile.component.html

    <div class="profile-page">

      <div class="user-info">
        <div class="container">
          <div class="row">

            <div class="col-xs-12 col-md-10 offset-md-1">
              <img [src]="profile.image || 'https://static.productionready.io/images/smiley-cyrus.jpg'" alt="user image" class="user-img" />
              <h4>{{ profile.username }}</h4>
              <p>{{ profile.bio }}</p>
              <app-follow-button
                [hidden]="isUser"
                [profile]="profile"
                (toggle)="onToggleFollowing($event)">
              </app-follow-button>
               <a [routerLink]="['/settings']"
                  [hidden]="!isUser"
                  class="btn btn-sm btn-outline-secondary action-btn">
                  <i class="ion-gear-a"></i> Edit Profile Settings
                </a>
            </div>

          </div>
        </div>
      </div>

      <div class="container">
        <div class="row">

          <div class="col-xs-12 col-md-10 offset-md-1">
            <div class="articles-toggle">
              <ul class="nav nav-pills outline-active">
                <li class="nav-item">
                  <a class="nav-link"
                     routerLinkActive="active"
                     [routerLinkActiveOptions]="{ exact: true }"
                     [routerLink]="['/profile', profile.username]">
                     My Posts
                  </a>
                </li>
                <li class="nav-item">
                  <a class="nav-link"
                     routerLinkActive="active"
                     [routerLinkActiveOptions]="{ exact: true }"
                     [routerLink]="['/profile', profile.username, 'favorites']">
                     Favorited Posts
                  </a>
                </li>
              </ul>
            </div>

            <router-outlet></router-outlet>
          </div>

        </div>
      </div>

    </div>
    
   |            +---- profile.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';

    import { User, UserService, Profile } from '../core';
    import { concatMap ,  tap } from 'rxjs/operators';

    @Component({
      selector: 'app-profile-page',
      templateUrl: './profile.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ProfileComponent implements OnInit {
      constructor(
        private route: ActivatedRoute,
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) { }

      profile: Profile;
      currentUser: User;
      isUser: boolean;

      ngOnInit() {
        this.route.data.pipe(
          concatMap((data: { profile: Profile }) => {
            this.profile = data.profile;
            // Load the current user's data.
            return this.userService.currentUser.pipe(tap(
              (userData: User) => {
                this.currentUser = userData;
                this.isUser = (this.currentUser.username === this.profile.username);
              }
            ));
          })
        ).subscribe((() => {
          this.cd.markForCheck();
        }));
      }

      onToggleFollowing(following: boolean) {
        this.profile.following = following;
      }

    }
    
   |            +---- profile.module.ts

    import { NgModule } from '@angular/core';

    import { ProfileArticlesComponent } from './profile-articles.component';
    import { ProfileComponent } from './profile.component';
    import { ProfileFavoritesComponent } from './profile-favorites.component';
    import { SharedModule } from '../shared';
    import { ProfileRoutingModule } from './profile-routing.module';

    @NgModule({
      imports: [
        SharedModule,
        ProfileRoutingModule
      ],
      declarations: [
        ProfileArticlesComponent,
        ProfileComponent,
        ProfileFavoritesComponent
      ],
      providers: [
      ]
    })
    export class ProfileModule {}
    
   |      +---- settings
   |            +---- settings-routing.module.ts

    import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import { AuthGuard } from '../core';
    import { SettingsComponent } from './settings.component';

    const routes: Routes = [
      {
        path: '',
        component: SettingsComponent,
        canActivate: [AuthGuard]
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class SettingsRoutingModule {}
    
   |            +---- settings.component.html

    <div class="settings-page">
      <div class="container page">
        <div class="row">
          <div class="col-md-6 offset-md-3 col-xs-12">

            <h1 class="text-xs-center">Your Settings</h1>

            <app-list-errors [errors]="errors"></app-list-errors>

            <form [formGroup]="settingsForm" (ngSubmit)="submitForm()">
              <fieldset [disabled]="isSubmitting">

                <fieldset class="form-group">
                  <input class="form-control"
                    type="text"
                    placeholder="URL of profile picture"
                    formControlName="image" />
                </fieldset>

                <fieldset class="form-group">
                  <input class="form-control form-control-lg"
                    type="text"
                    placeholder="Username"
                    formControlName="username" />
                </fieldset>

                <fieldset class="form-group">
                  <textarea class="form-control form-control-lg"
                    rows="8"
                    placeholder="Short bio about you"
                    formControlName="bio">
                  </textarea>
                </fieldset>

                <fieldset class="form-group">
                  <input class="form-control form-control-lg"
                    type="email"
                    placeholder="Email"
                    formControlName="email" />
                </fieldset>

                <fieldset class="form-group">
                  <input class="form-control form-control-lg"
                    type="password"
                    placeholder="New Password"
                    formControlName="password" />
                </fieldset>

                <button class="btn btn-lg btn-primary pull-xs-right"
                  type="submit">
                  Update Settings
                </button>

              </fieldset>
            </form>

            <!-- Line break for logout button -->
            <hr />

            <button class="btn btn-outline-danger"
              (click)="logout()">
              Or click here to logout.
            </button>

          </div>
        </div>
      </div>
    </div>
    
   |            +---- settings.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { FormBuilder, FormGroup } from '@angular/forms';
    import { Router } from '@angular/router';

    import { User, UserService } from '../core';

    @Component({
      selector: 'app-settings-page',
      templateUrl: './settings.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class SettingsComponent implements OnInit {
      user: User = {} as User;
      settingsForm: FormGroup;
      errors: Object = {};
      isSubmitting = false;

      constructor(
        private router: Router,
        private userService: UserService,
        private fb: FormBuilder,
        private cd: ChangeDetectorRef
      ) {
        // create form group using the form builder
        this.settingsForm = this.fb.group({
          image: '',
          username: '',
          bio: '',
          email: '',
          password: ''
        });
        // Optional: subscribe to changes on the form
        // this.settingsForm.valueChanges.subscribe(values => this.updateUser(values));
      }

      ngOnInit() {
        // Make a fresh copy of the current user's object to place in editable form fields
        Object.assign(this.user, this.userService.getCurrentUser());
        // Fill the form
        this.settingsForm.patchValue(this.user);
      }

      logout() {
        this.userService.purgeAuth();
        this.router.navigateByUrl('/');
      }

      submitForm() {
        this.isSubmitting = true;

        // update the model
        this.updateUser(this.settingsForm.value);

        this.userService
        .update(this.user)
        .subscribe(
          updatedUser => this.router.navigateByUrl('/profile/' + updatedUser.username),
          err => {
            this.errors = err;
            this.isSubmitting = false;
            this.cd.markForCheck();
          }
        );
      }

      updateUser(values: Object) {
        Object.assign(this.user, values);
      }

    }
    
   |            +---- settings.module.ts

    import { NgModule } from '@angular/core';

    import { SettingsComponent } from './settings.component';
    import { SharedModule } from '../shared';
    import { SettingsRoutingModule } from './settings-routing.module';

    @NgModule({
      imports: [
        SharedModule,
        SettingsRoutingModule
      ],
      declarations: [
        SettingsComponent
      ]
    })
    export class SettingsModule {}
    
   |      +---- shared
   |            +---- article-helpers
   |                  +---- article-list.component.css

    .page-link {
      cursor: pointer;
    }
    
   |                  +---- article-list.component.html

    <app-article-preview
      *ngFor="let article of results; trackBy: trackByFn"
      [article]="article">
    </app-article-preview>

    <div class="app-article-preview"
      [hidden]="!loading">
      Loading articles...
    </div>

    <div class="app-article-preview"
      [hidden]="loading || results.length">
      No articles are here... yet.
    </div>

    <nav [hidden]="loading || totalPages.length <= 1">
      <ul class="pagination">

        <li class="page-item"
          [ngClass]="{'active': pageNumber === currentPage}"
          *ngFor="let pageNumber of totalPages; trackBy: trackByFn"
          (click)="setPageTo(pageNumber)">

          <a class="page-link" >{{ pageNumber }}</a>

        </li>

      </ul>
    </nav>
    
   |                  +---- article-list.component.ts

    import { Component, Input, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';

    import { Article, ArticleListConfig, ArticlesService } from '../../core';
    @Component({
      selector: 'app-article-list',
      styleUrls: ['article-list.component.css'],
      templateUrl: './article-list.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ArticleListComponent {
      constructor (
        private articlesService: ArticlesService,
        private cd: ChangeDetectorRef
      ) {}

      @Input() limit: number;
      @Input()
      set config(config: ArticleListConfig) {
        if (config) {
          this.query = config;
          this.currentPage = 1;
          this.runQuery();
        }
      }

      query: ArticleListConfig;
      results: Article[];
      loading = false;
      currentPage = 1;
      totalPages: Array<number> = [1];

      setPageTo(pageNumber) {
        this.currentPage = pageNumber;
        this.runQuery();
      }

      trackByFn(index, item) {
        return index;
      }

      runQuery() {
        this.loading = true;
        this.results = [];

        // Create limit and offset filter (if necessary)
        if (this.limit) {
          this.query.filters.limit = this.limit;
          this.query.filters.offset =  (this.limit * (this.currentPage - 1));
        }

        this.articlesService.query(this.query)
        .subscribe(data => {
          this.loading = false;
          this.results = data.articles;

          // Used from http://www.jstips.co/en/create-range-0...n-easily-using-one-line/
          this.totalPages = Array.from(new Array(Math.ceil(data.articlesCount / this.limit)), (val, index) => index + 1);
          this.cd.markForCheck();
        });
      }
    }
    
   |                  +---- article-meta.component.html

    <div class="article-meta">
      <a [routerLink]="['/profile', article.author.username]">
        <img [src]="article.author.image || 'https://static.productionready.io/images/smiley-cyrus.jpg'" alt="author image" />
      </a>

      <div class="info">
        <a class="author"
          [routerLink]="['/profile', article.author.username]">
          {{ article.author.username }}
        </a>
        <span class="date">
          {{ article.createdAt | date: 'longDate' }}
        </span>
      </div>

      <ng-content></ng-content>
    </div>
    
   |                  +---- article-meta.component.ts

    import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

    import { Article } from '../../core';

    @Component({
      selector: 'app-article-meta',
      templateUrl: './article-meta.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ArticleMetaComponent {
      @Input() article: Article;
    }
    
   |                  +---- article-preview.component.html

    <div class="article-preview">
      <app-article-meta [article]="article">
        <app-favorite-button
          [article]="article"
          (toggle)="onToggleFavorite($event)"
          class="pull-xs-right">
          {{article.favoritesCount}}
        </app-favorite-button>
      </app-article-meta>

      <a [routerLink]="['/article', article.slug]" class="preview-link">
        <h1>{{ article.title }}</h1>
        <p>{{ article.description }}</p>
        <span>Read more...</span>
        <ul class="tag-list">
          <li class="tag-default tag-pill tag-outline"
            *ngFor="let tag of article.tagList; trackBy: trackByFn">
            {{ tag }}
          </li>
        </ul>
      </a>
    </div>
    
   |                  +---- article-preview.component.ts

    import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

    import { Article } from '../../core';

    @Component({
      selector: 'app-article-preview',
      templateUrl: './article-preview.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ArticlePreviewComponent {
      @Input() article: Article;

      trackByFn(index, item) {
        return index;
      }

      onToggleFavorite(favorited: boolean) {
        this.article['favorited'] = favorited;

        if (favorited) {
          this.article['favoritesCount']++;
        } else {
          this.article['favoritesCount']--;
        }
      }
    }
    
   |                  +---- index.ts

    export * from './article-list.component';
    export * from './article-meta.component';
    export * from './article-preview.component';
    
   |            +---- buttons
   |                  +---- favorite-button.component.html

    <button class="btn btn-sm"
     [ngClass]="{ 'disabled' : isSubmitting,
                  'btn-outline-primary': !article.favorited,
                  'btn-primary': article.favorited }"
     (click)="toggleFavorite()">
      <i class="ion-heart"></i> <ng-content></ng-content>
    </button>
    
   |                  +---- favorite-button.component.ts

    import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { Router } from '@angular/router';

    import { Article, ArticlesService, UserService } from '../../core';
    import { of } from 'rxjs';
    import { concatMap ,  tap } from 'rxjs/operators';

    @Component({
      selector: 'app-favorite-button',
      templateUrl: './favorite-button.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class FavoriteButtonComponent {
      constructor(
        private articlesService: ArticlesService,
        private router: Router,
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) {}

      @Input() article: Article;
      @Output() toggle = new EventEmitter<boolean>();
      isSubmitting = false;

      toggleFavorite() {
        this.isSubmitting = true;

        this.userService.isAuthenticated.pipe(concatMap(
          (authenticated) => {
            // Not authenticated? Push to login screen
            if (!authenticated) {
              this.router.navigateByUrl('/login');
              return of(null);
            }

            // Favorite the article if it isn't favorited yet
            if (!this.article.favorited) {
              return this.articlesService.favorite(this.article.slug)
              .pipe(tap(
                data => {
                  this.isSubmitting = false;
                  this.toggle.emit(true);
                },
                err => this.isSubmitting = false
              ));

            // Otherwise, unfavorite the article
            } else {
              return this.articlesService.unfavorite(this.article.slug)
              .pipe(tap(
                data => {
                  this.isSubmitting = false;
                  this.toggle.emit(false);
                },
                err => this.isSubmitting = false
              ));
            }

          }
        )).subscribe(() => {
          this.cd.markForCheck();
        });
      }
    }
    
   |                  +---- follow-button.component.html

    <button
      class="btn btn-sm action-btn"
      [ngClass]="{ 'disabled': isSubmitting,
                   'btn-outline-secondary': !profile.following,
                   'btn-secondary': profile.following }"
      (click)="toggleFollowing()">
      <i class="ion-plus-round"></i>
      &nbsp;
      {{ profile.following ? 'Unfollow' : 'Follow' }} {{ profile.username }}
    </button>
    
   |                  +---- follow-button.component.ts

    import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
    import { Router } from '@angular/router';

    import { Profile, ProfilesService, UserService } from '../../core';
    import { concatMap ,  tap } from 'rxjs/operators';
    import { of } from 'rxjs';

    @Component({
      selector: 'app-follow-button',
      templateUrl: './follow-button.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class FollowButtonComponent {
      constructor(
        private profilesService: ProfilesService,
        private router: Router,
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) {}

      @Input() profile: Profile;
      @Output() toggle = new EventEmitter<boolean>();
      isSubmitting = false;

      toggleFollowing() {
        this.isSubmitting = true;
        // TODO: remove nested subscribes, use mergeMap

        this.userService.isAuthenticated.pipe(concatMap(
          (authenticated) => {
            // Not authenticated? Push to login screen
            if (!authenticated) {
              this.router.navigateByUrl('/login');
              return of(null);
            }

            // Follow this profile if we aren't already
            if (!this.profile.following) {
              return this.profilesService.follow(this.profile.username)
              .pipe(tap(
                data => {
                  this.isSubmitting = false;
                  this.toggle.emit(true);
                },
                err => this.isSubmitting = false
              ));

            // Otherwise, unfollow this profile
            } else {
              return this.profilesService.unfollow(this.profile.username)
              .pipe(tap(
                data => {
                  this.isSubmitting = false;
                  this.toggle.emit(false);
                },
                err => this.isSubmitting = false
              ));
            }
          }
        )).subscribe(() => {
          this.cd.markForCheck();
        });
      }
    }
    
   |                  +---- index.ts

    export * from './favorite-button.component';
    export * from './follow-button.component';
    
   |            +---- index.ts

    export * from './article-helpers';
    export * from './buttons';
    export * from './layout';
    export * from './list-errors.component';
    export * from './shared.module';
    export * from './show-authed.directive';
    
   |            +---- layout
   |                  +---- footer.component.html

    <footer>
      <div class="container">
        <a class="logo-font" routerLink="/">conduit</a>
        <span class="attribution">
          &copy; {{ today | date: 'yyyy' }}.
          An interactive learning project from <a href="https://thinkster.io">Thinkster</a>.
          Code licensed under MIT.
        </span>
      </div>
    </footer>
    
   |                  +---- footer.component.ts

    import { Component, ChangeDetectionStrategy } from '@angular/core';

    @Component({
      selector: 'app-layout-footer',
      templateUrl: './footer.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class FooterComponent {
      today: number = Date.now();
    }
    
   |                  +---- header.component.html

    <nav class="navbar navbar-light">
      <div class="container">
        <a class="navbar-brand" routerLink="/">conduit</a>

        <!-- Show this for logged out users -->
        <ul *appShowAuthed="false"
          class="nav navbar-nav pull-xs-right">

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/">
              Home
            </a>
          </li>

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/login"
              routerLinkActive="active">
              Sign in
            </a>
          </li>

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/register"
              routerLinkActive="active">
              Sign up
            </a>
          </li>

        </ul>

        <!-- Show this for logged in users -->
        <ul *appShowAuthed="true"
          class="nav navbar-nav pull-xs-right">

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/"
              routerLinkActive="active"
              [routerLinkActiveOptions]="{ exact: true }">
              Home
            </a>
          </li>

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/editor"
              routerLinkActive="active">
              <i class="ion-compose"></i>&nbsp;New Article
            </a>
          </li>

          <li class="nav-item">
            <a class="nav-link"
              routerLink="/settings"
              routerLinkActive="active">
              <i class="ion-gear-a"></i>&nbsp;Settings
            </a>
          </li>

          <li class="nav-item">
            <a class="nav-link"
              [routerLink]="['/profile', currentUser.username]"
              routerLinkActive="active">
              <img [src]="currentUser.image || 'https://static.productionready.io/images/smiley-cyrus.jpg'" *ngIf="currentUser.image" class="user-pic" alt="user image" />
              {{ currentUser.username }}
            </a>
          </li>

        </ul>

      </div>
    </nav>
    
   |                  +---- header.component.ts

    import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

    import { User, UserService } from '../../core';

    @Component({
      selector: 'app-layout-header',
      templateUrl: './header.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class HeaderComponent implements OnInit {
      constructor(
        private userService: UserService,
        private cd: ChangeDetectorRef
      ) {}

      currentUser: User;

      ngOnInit() {
        this.userService.currentUser.subscribe(
          (userData) => {
            this.currentUser = userData;
            this.cd.markForCheck();
          }
        );
      }
    }
    
   |                  +---- index.ts

    export * from './footer.component';
    export * from './header.component';
    
   |            +---- list-errors.component.html

    <ul class="error-messages" *ngIf="errorList">
      <li *ngFor="let error of errorList; trackBy: trackByFn">
        {{ error }}
      </li>
    </ul>
    
   |            +---- list-errors.component.ts

    import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

    import { Errors } from '../core';

    @Component({
      selector: 'app-list-errors',
      templateUrl: './list-errors.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ListErrorsComponent {
      formattedErrors: Array<string> = [];

      @Input()
      set errors(errorList: Errors) {
        this.formattedErrors = Object.keys(errorList.errors || {})
          .map(key => `${key} ${errorList.errors[key]}`);
      }

      get errorList() { return this.formattedErrors; }

      trackByFn(index, item) {
        return index;
      }
    }
    
   |            +---- shared.module.ts

    import { CommonModule } from '@angular/common';
    import { NgModule } from '@angular/core';
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    import { HttpClientModule } from '@angular/common/http';
    import { RouterModule } from '@angular/router';

    import { ArticleListComponent, ArticleMetaComponent, ArticlePreviewComponent } from './article-helpers';
    import { FavoriteButtonComponent, FollowButtonComponent } from './buttons';
    import { ListErrorsComponent } from './list-errors.component';
    import { ShowAuthedDirective } from './show-authed.directive';

    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        HttpClientModule,
        RouterModule
      ],
      declarations: [
        ArticleListComponent,
        ArticleMetaComponent,
        ArticlePreviewComponent,
        FavoriteButtonComponent,
        FollowButtonComponent,
        ListErrorsComponent,
        ShowAuthedDirective
      ],
      exports: [
        ArticleListComponent,
        ArticleMetaComponent,
        ArticlePreviewComponent,
        CommonModule,
        FavoriteButtonComponent,
        FollowButtonComponent,
        FormsModule,
        ReactiveFormsModule,
        HttpClientModule,
        ListErrorsComponent,
        RouterModule,
        ShowAuthedDirective
      ]
    })
    export class SharedModule {}
    
   |            +---- show-authed.directive.ts

    import {
      Directive,
      Input,
      OnInit,
      TemplateRef,
      ViewContainerRef
    } from '@angular/core';

    import { UserService } from '../core';

    @Directive({ selector: '[appShowAuthed]' })
    export class ShowAuthedDirective implements OnInit {
      constructor(
        private templateRef: TemplateRef<any>,
        private userService: UserService,
        private viewContainer: ViewContainerRef
      ) {}

      condition: boolean;

      ngOnInit() {
        this.userService.isAuthenticated.subscribe(
          (isAuthenticated) => {
            if (isAuthenticated && this.condition || !isAuthenticated && !this.condition) {
              this.viewContainer.createEmbeddedView(this.templateRef);
            } else {
              this.viewContainer.clear();
            }
          }
        );
      }

      @Input() set appShowAuthed(condition: boolean) {
        this.condition = condition;
      }

    }
    
   +---- assets
   |      +---- icons
   +---- environments
   |      +---- environment.prod.ts

    export const environment = {
      production: true,
      api_url: 'https://api.realworld.io/api'
    };
    
   |      +---- environment.ts

    // The file contents for the current environment will overwrite these during build.
    // The build system defaults to the dev environment which uses `environment.ts`, but if you do
    // `ng build --env=prod` then `environment.prod.ts` will be used instead.
    // The list of which env maps to which file can be found in `angular-cli.json`.

    export const environment = {
      production: false,
      api_url: 'https://api.realworld.io/api'
    };
    
   +---- index.html

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="Description" content="Angular realworld example">
      <title>Conduit</title>
      <base href="/">

      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
      <link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic&display=swap" rel="stylesheet" type="text/css">
      <link rel="stylesheet" href="//demo.productionready.io/main.css">

      <link rel="manifest" href="manifest.webmanifest">
      <meta name="theme-color" content="#1976d2">
    </head>
    <body>
      <app-root>Loading...</app-root>
      <noscript>Please enable JavaScript to continue using this application.</noscript>
    </body>
    </html>
    
   +---- main.ts

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';

    if (environment.production) {
      enableProdMode();
    }

    const bootstrapPromise =  platformBrowserDynamic().bootstrapModule(AppModule);

    // Logging bootstrap information
    bootstrapPromise.then(success => console.log(`Bootstrap success`))
      .catch(err => console.error(err));
    
   +---- polyfills.ts

    /**
     * This file includes polyfills needed by Angular and is loaded before the app.
     * You can add your own extra polyfills to this file.
     *
     * This file is divided into 2 sections:
     *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
     *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
     *      file.
     *
     * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
     * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
     * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
     *
     * Learn more in https://angular.io/guide/browser-support
     */

    /***************************************************************************************************
    * BROWSER POLYFILLS
    */

    /** IE10 and IE11 requires the following for the Reflect API. */
    // import 'core-js/es6/reflect';

    /** Evergreen browsers require these. **/
    // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
    // import 'core-js/es7/reflect';

    /***************************************************************************************************
     * Zone JS is required by default for Angular itself.
     */
    import 'zone.js'; // Included with Angular CLI.

    /***************************************************************************************************
     * APPLICATION IMPORTS
     */
    
   +---- styles.css

    /* You can add global styles to this file, and also import other style files */
    
   +---- test.ts

    // This file is required by karma.conf.js and loads recursively all the .spec and framework files

    import 'zone.js/dist/long-stack-trace-zone';
    import 'zone.js/dist/proxy.js';
    import 'zone.js/dist/sync-test';
    import 'zone.js/dist/jasmine-patch';
    import 'zone.js/dist/async-test';
    import 'zone.js/dist/fake-async-test';
    import { getTestBed } from '@angular/core/testing';
    import {
      BrowserDynamicTestingModule,
      platformBrowserDynamicTesting
    } from '@angular/platform-browser-dynamic/testing';

    // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
    declare const __karma__: any;
    declare const require: any;

    // Prevent Karma from running prematurely.
    __karma__.loaded = function () {};

    // First, initialize the Angular testing environment.
    getTestBed().initTestEnvironment(
      BrowserDynamicTestingModule,
      platformBrowserDynamicTesting(), {
        teardown: { destroyAfterEach: false }
    }
    );
    // Then we find all the tests.
    const context = require.context('./', true, /\.spec\.ts$/);
    // And load the modules.
    context.keys().map(context);
    // Finally, start Karma to run the tests.
    __karma__.start();
    
   +---- tsconfig.app.json

    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/app",
        "baseUrl": "./",
        "types": []
      },
      "files": [
        "main.ts",
        "polyfills.ts"
      ],
      "include": [
        "src/**/*.d.ts"
      ]
    }
    
   +---- tsconfig.spec.json

    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/spec",
        "baseUrl": "./",
        "types": [
          "jasmine",
          "node"
        ]
      },
      "files": [
        "test.ts",
        "polyfills.ts"
      ],
      "include": [
        "**/*.spec.ts",
        "**/*.d.ts"
      ]
    }
    
   +---- typings.d.ts

    /* SystemJS module definition */
    declare var module: NodeModule;
    interface NodeModule {
      id: string;
    }
    
   tsconfig.json

    {
      "compileOnSave": false,
      "compilerOptions": {
        "importHelpers": true,
        "downlevelIteration": true,
        "outDir": "./dist/out-tsc",
        "sourceMap": true,
        "declaration": false,
        "module": "es2020",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "es2015",
        "typeRoots": [
          "node_modules/@types"
        ],
        "lib": [
          "es2018",
          "dom"
        ]
      }
    }
    

Back to Main Page