├── .prettierrc.json
├── src
├── assets
│ └── .gitkeep
├── app
│ ├── components
│ │ ├── error-page
│ │ │ ├── error-page.component.scss
│ │ │ ├── error-page.component.html
│ │ │ ├── error-page.component.spec.ts
│ │ │ └── error-page.component.ts
│ │ ├── not-found-page
│ │ │ ├── not-found-page.component.scss
│ │ │ ├── not-found-page.component.html
│ │ │ ├── not-found-page.component.ts
│ │ │ └── not-found-page.component.spec.ts
│ │ ├── post-list-item
│ │ │ ├── post-list-item.component.scss
│ │ │ ├── post-list-item.component.html
│ │ │ ├── post-list-item.component.ts
│ │ │ └── post-list-item.component.spec.ts
│ │ ├── post-list-page
│ │ │ ├── post-list-page.component.scss
│ │ │ ├── post-list-page.component.html
│ │ │ ├── post-list-page.component.ts
│ │ │ └── post-list-page.component.spec.ts
│ │ ├── post-view-page
│ │ │ ├── post-view-page.component.scss
│ │ │ ├── post-view-page.component.html
│ │ │ ├── post-view-page.component.spec.ts
│ │ │ └── post-view-page.component.ts
│ │ ├── comments-widget
│ │ │ ├── comments-widget.component.scss
│ │ │ ├── comments-widget.component.html
│ │ │ ├── comments-widget.component.spec.ts
│ │ │ └── comments-widget.component.ts
│ │ ├── post-list
│ │ │ ├── post-list.component.scss
│ │ │ ├── post-list.component.html
│ │ │ ├── post-list.component.ts
│ │ │ └── post-list.component.spec.ts
│ │ ├── comment-list
│ │ │ ├── comment-list.component.scss
│ │ │ ├── comment-list.component.html
│ │ │ ├── comment-list.component.ts
│ │ │ └── comment-list.component.spec.ts
│ │ ├── app
│ │ │ ├── app.component.scss
│ │ │ ├── app.component.ts
│ │ │ └── app.component.html
│ │ ├── comment-form
│ │ │ ├── comment-form.component.scss
│ │ │ ├── comment-form.component.spec.ts
│ │ │ ├── comment-form.component.html
│ │ │ └── comment-form.component.ts
│ │ ├── comment-author-dialog
│ │ │ ├── comment-author-dialog.component.scss
│ │ │ ├── comment-author-dialog.component.spec.ts
│ │ │ ├── comment-author-dialog.component.html
│ │ │ └── comment-author-dialog.component.ts
│ │ └── comment-list-item
│ │ │ ├── comment-list-item.component.scss
│ │ │ ├── comment-list-item.component.html
│ │ │ ├── comment-list-item.component.spec.ts
│ │ │ └── comment-list-item.component.ts
│ ├── state
│ │ └── app.reducer.ts
│ ├── errors.ts
│ ├── resolvers
│ │ ├── post.resolver.ts
│ │ ├── posts.resolver.ts
│ │ └── post-comments.resolver.ts
│ ├── types.d.ts
│ ├── error-handler.ts
│ ├── services
│ │ ├── posts.service.ts
│ │ └── comments.service.ts
│ ├── app-routing.module.ts
│ └── app.module.ts
├── guestbook
│ ├── components
│ │ ├── record-form
│ │ │ ├── record-form.component.scss
│ │ │ ├── record-form.component.spec.ts
│ │ │ ├── record-form.component.html
│ │ │ └── record-form.component.ts
│ │ ├── records-list-item
│ │ │ ├── records-list-item.component.scss
│ │ │ ├── records-list-item.component.html
│ │ │ ├── records-list-item.component.ts
│ │ │ └── records-list-item.component.spec.ts
│ │ ├── records-page
│ │ │ ├── records-page.component.scss
│ │ │ ├── records-page.component.html
│ │ │ ├── records-page.component.spec.ts
│ │ │ └── records-page.component.ts
│ │ └── records-list
│ │ │ ├── records-list.component.scss
│ │ │ ├── records-list.component.html
│ │ │ ├── records-list.component.ts
│ │ │ └── records-list.component.spec.ts
│ ├── types.d.ts
│ ├── guestbook-routing.module.ts
│ ├── state
│ │ └── guestbook.reducer.ts
│ └── guestbook.module.ts
├── favicon.ico
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── styles.scss
├── main.ts
├── index.html
├── test.ts
└── polyfills.ts
├── .prettierignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── .editorconfig
├── tsconfig.app.json
├── tsconfig.spec.json
├── .browserslistrc
├── .gitignore
├── tsconfig.json
├── README.md
├── package.json
├── karma.conf.js
└── angular.json
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/error-page/error-page.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/not-found-page/not-found-page.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/post-list-item/post-list-item.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/post-list-page/post-list-page.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/state/app.reducer.ts:
--------------------------------------------------------------------------------
1 | export type AppState = {};
2 |
--------------------------------------------------------------------------------
/src/guestbook/components/record-form/record-form.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | .angular
3 | .vscode
4 |
--------------------------------------------------------------------------------
/src/app/errors.ts:
--------------------------------------------------------------------------------
1 | export class PageNotFoundError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list-item/records-list-item.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/connected/demo/master/src/favicon.ico
--------------------------------------------------------------------------------
/src/app/components/not-found-page/not-found-page.component.html:
--------------------------------------------------------------------------------
1 |
Page not found :(
2 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/src/app/components/post-list-page/post-list-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/components/post-view-page/post-view-page.component.scss:
--------------------------------------------------------------------------------
1 | .b-post-view-page {
2 | &__post {
3 | margin-bottom: 40px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/components/comments-widget/comments-widget.component.scss:
--------------------------------------------------------------------------------
1 | .b-comments-widget {
2 | &__comment-list {
3 | margin-bottom: 50px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-page/records-page.component.scss:
--------------------------------------------------------------------------------
1 | .b-guestbook-records-page {
2 | &__records {
3 | margin-bottom: 60px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/components/post-list/post-list.component.scss:
--------------------------------------------------------------------------------
1 | .b-blog-post-list {
2 | &__item {
3 | &:not(:last-child) {
4 | margin-bottom: 40px;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/components/comment-list/comment-list.component.scss:
--------------------------------------------------------------------------------
1 | .b-comment-list {
2 | &__item {
3 | &:not(:last-child) {
4 | margin-bottom: 30px;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list/records-list.component.scss:
--------------------------------------------------------------------------------
1 | .b-guestbook-records-list {
2 | &__item {
3 | &:not(:last-child) {
4 | margin-bottom: 30px;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/components/comment-list/comment-list.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/guestbook/types.d.ts:
--------------------------------------------------------------------------------
1 | export type GuestbookRecord = {
2 | id: number;
3 | name: string;
4 | text: string;
5 | };
6 |
7 | export type GuestbookRecordFormData = {
8 | name: string;
9 | text: string;
10 | };
11 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list-item/records-list-item.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ record.name }}
3 |
4 | {{ record.text }}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | html,
4 | body {
5 | height: 100%;
6 | }
7 | body {
8 | margin: 0;
9 | font-family: Roboto, "Helvetica Neue", sans-serif;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/components/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .b-app {
2 | &__header {
3 | margin-bottom: 60px;
4 | }
5 |
6 | &__body {
7 | width: 100%;
8 | max-width: 600px;
9 | margin: 0 auto;
10 | padding: 0 40px 100px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/components/post-list/post-list.component.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/app/components/comment-form/comment-form.component.scss:
--------------------------------------------------------------------------------
1 | .b-comment-form {
2 | overflow: hidden;
3 |
4 | &__row {
5 | display: flex;
6 | margin: 0 -10px;
7 | }
8 |
9 | &__col {
10 | flex: 1 1 auto;
11 | padding: 0 10px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/components/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss'],
7 | })
8 | export class AppComponent {
9 | title = 'Some title';
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/src/app/components/error-page/error-page.component.html:
--------------------------------------------------------------------------------
1 | Something went wrong...
2 |
3 | Log
4 |
7 |
8 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-page/records-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/app/components/not-found-page/not-found-page.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-not-found-page',
5 | templateUrl: './not-found-page.component.html',
6 | styleUrls: ['./not-found-page.component.scss'],
7 | })
8 | export class NotFoundPageComponent implements OnInit {
9 | constructor() {}
10 |
11 | ngOnInit(): void {}
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/components/comments-widget/comments-widget.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Comments
4 |
5 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/components/post-list-item/post-list-item.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ post.author }}
3 | {{ post.title }}
4 |
5 | {{ post.previewText }}
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch((err) => console.error(err));
14 |
--------------------------------------------------------------------------------
/src/app/components/comment-author-dialog/comment-author-dialog.component.scss:
--------------------------------------------------------------------------------
1 | .b-comment--author-dialog {
2 | &__body {
3 | display: flow-root;
4 | }
5 |
6 | &__footer {
7 | display: flex;
8 | justify-content: center;
9 | }
10 |
11 | &__avatar {
12 | float: left;
13 | margin-right: 15px;
14 | }
15 |
16 | &__avatar-image {
17 | border-radius: 4px;
18 | }
19 |
20 | &__details {
21 | overflow: hidden;
22 | padding-top: 5px;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/guestbook/guestbook-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { RecordsPageComponent } from './components/records-page/records-page.component';
4 |
5 | const routes: Routes = [
6 | {
7 | path: 'guestbook',
8 | pathMatch: 'full',
9 | component: RecordsPageComponent,
10 | },
11 | ];
12 |
13 | @NgModule({
14 | imports: [RouterModule.forChild(routes)],
15 | exports: [RouterModule],
16 | })
17 | export class GuestbookRoutingModule {}
18 |
--------------------------------------------------------------------------------
/src/app/components/app/app.component.html:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list/records-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
0" [ngIfElse]="noRecords">
3 |
4 |
7 |
8 |
9 |
10 |
11 | No records yet. Be the first one to leave a feedback.
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "pwa-chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/components/comment-list-item/comment-list-item.component.scss:
--------------------------------------------------------------------------------
1 | .b-comment-list-item {
2 | display: flow-root;
3 |
4 | &__left-col {
5 | float: left;
6 | padding-top: 4px;
7 | margin-right: 20px;
8 | }
9 |
10 | &__right-col {
11 | overflow: hidden;
12 | padding-top: 5px;
13 | }
14 |
15 | &__avatar {
16 | display: block;
17 | border-radius: 4px;
18 | cursor: pointer;
19 | }
20 |
21 | &__author {
22 | font-size: 12px;
23 | font-weight: 500;
24 | }
25 |
26 | &__comment {
27 | font-size: 12px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/components/post-list/post-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { BlogPost } from '../../types';
8 |
9 | @Component({
10 | selector: 'blog-post-list',
11 | templateUrl: './post-list.component.html',
12 | styleUrls: ['./post-list.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class PostListComponent implements OnInit {
16 | @Input() blogPosts: BlogPost[] = [];
17 |
18 | constructor() {}
19 |
20 | ngOnInit(): void {}
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/components/comment-list-item/comment-list-item.component.html:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/app/components/comment-list/comment-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { Comment } from '../../types';
8 |
9 | @Component({
10 | selector: 'comment-list',
11 | templateUrl: './comment-list.component.html',
12 | styleUrls: ['./comment-list.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class CommentListComponent implements OnInit {
16 | @Input() comments: Comment[] = [];
17 |
18 | constructor() {}
19 |
20 | ngOnInit(): void {}
21 | }
22 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list/records-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { GuestbookRecord } from '../../types';
8 |
9 | @Component({
10 | selector: 'guestbook-records-list',
11 | templateUrl: './records-list.component.html',
12 | styleUrls: ['./records-list.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class RecordsListComponent implements OnInit {
16 | @Input() records: GuestbookRecord[] = [];
17 |
18 | constructor() {}
19 |
20 | ngOnInit(): void {}
21 | }
22 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 |
--------------------------------------------------------------------------------
/src/app/resolvers/post.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | ActivatedRouteSnapshot,
4 | Resolve,
5 | RouterStateSnapshot,
6 | } from '@angular/router';
7 | import { PostsService } from '../services/posts.service';
8 | import { BlogPost } from '../types';
9 |
10 | @Injectable({
11 | providedIn: 'root',
12 | })
13 | export class PostResolver implements Resolve {
14 | constructor(private postsService: PostsService) {}
15 |
16 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
17 | // @todo Implement error handler
18 | return this.postsService.fetchPost(route.params['id']);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/components/post-list-page/post-list-page.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { BlogPost } from '../../types';
3 | import { ActivatedRoute } from '@angular/router';
4 |
5 | @Component({
6 | selector: 'blog-post-list-page',
7 | templateUrl: './post-list-page.component.html',
8 | styleUrls: ['./post-list-page.component.scss'],
9 | })
10 | export class PostListPageComponent implements OnInit {
11 | blogPosts: BlogPost[] = [];
12 |
13 | constructor(private activatedRoute: ActivatedRoute) {}
14 |
15 | ngOnInit(): void {
16 | this.activatedRoute.data.subscribe(({ posts }) => {
17 | this.blogPosts = posts;
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/resolvers/posts.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | Resolve,
4 | RouterStateSnapshot,
5 | ActivatedRouteSnapshot,
6 | } from '@angular/router';
7 | import { PostsService } from '../services/posts.service';
8 | import { Observable } from 'rxjs';
9 | import { BlogPost } from '../types';
10 |
11 | @Injectable({
12 | providedIn: 'root',
13 | })
14 | export class PostsResolver implements Resolve> {
15 | constructor(private postsService: PostsService) {}
16 |
17 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
18 | // @todo Implement error handler
19 | return this.postsService.fetchPosts();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/components/post-view-page/post-view-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ post.author }}
5 | {{ post.title }}
6 |
7 | {{ post.fullText }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list-item/records-list-item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { GuestbookRecord } from '../../types';
8 |
9 | @Component({
10 | selector: 'guestbook-records-list-item',
11 | templateUrl: './records-list-item.component.html',
12 | styleUrls: ['./records-list-item.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class RecordsListItemComponent implements OnInit {
16 | @Input() record: GuestbookRecord = {
17 | id: NaN,
18 | name: 'Unknown',
19 | text: 'No content',
20 | };
21 |
22 | constructor() {}
23 |
24 | ngOnInit(): void {}
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/*
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demo
6 |
7 |
8 |
9 |
10 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/app/components/post-list/post-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PostListComponent } from './post-list.component';
4 |
5 | describe('PostListComponent', () => {
6 | let component: PostListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [PostListComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(PostListComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/resolvers/post-comments.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | Resolve,
4 | RouterStateSnapshot,
5 | ActivatedRouteSnapshot,
6 | } from '@angular/router';
7 | import { Observable } from 'rxjs';
8 | import { Comment } from '../types';
9 | import { CommentsService } from '../services/comments.service';
10 |
11 | @Injectable({
12 | providedIn: 'root',
13 | })
14 | export class PostCommentsResolver implements Resolve> {
15 | constructor(private commentsService: CommentsService) {}
16 |
17 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
18 | // @todo Implement error handler
19 | return this.commentsService.fetchComments(route.params['id']);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/components/error-page/error-page.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ErrorPageComponent } from './error-page.component';
4 |
5 | describe('ErrorPageComponent', () => {
6 | let component: ErrorPageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ErrorPageComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(ErrorPageComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/post-list-item/post-list-item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { BlogPost } from '../../types';
8 |
9 | @Component({
10 | selector: 'blog-post-list-item',
11 | templateUrl: './post-list-item.component.html',
12 | styleUrls: ['./post-list-item.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class PostListItemComponent implements OnInit {
16 | @Input() post: BlogPost = {
17 | id: NaN,
18 | author: 'Unknown',
19 | title: 'Untitled',
20 | previewText: 'No content',
21 | fullText: 'No content',
22 | };
23 |
24 | constructor() {}
25 |
26 | ngOnInit(): void {}
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/components/comment-form/comment-form.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CommentFormComponent } from './comment-form.component';
4 |
5 | describe('CommentFormComponent', () => {
6 | let component: CommentFormComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [CommentFormComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(CommentFormComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/comment-list/comment-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CommentListComponent } from './comment-list.component';
4 |
5 | describe('CommentListComponent', () => {
6 | let component: CommentListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [CommentListComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(CommentListComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/guestbook/components/record-form/record-form.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RecordFormComponent } from './record-form.component';
4 |
5 | describe('RecordFormComponent', () => {
6 | let component: RecordFormComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [RecordFormComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(RecordFormComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/types.d.ts:
--------------------------------------------------------------------------------
1 | export type BlogPost = {
2 | id: number;
3 | author: string;
4 | title: string;
5 | previewText: string;
6 | fullText: string;
7 | };
8 |
9 | export type JpBlogPost = {
10 | userId: number;
11 | id: number;
12 | title: string;
13 | body: string;
14 | };
15 |
16 | export type BlogPostWithComments = {
17 | post: BlogPost;
18 | comments: Comment[];
19 | };
20 |
21 | export type Comment = {
22 | id: number;
23 | author: string;
24 | email: string;
25 | text: string;
26 | };
27 |
28 | export type JpComment = {
29 | id: number;
30 | postId: number;
31 | name: string;
32 | email: string;
33 | body: string;
34 | };
35 |
36 | export type CommentFormData = {
37 | name: string;
38 | email: string;
39 | body: string;
40 | };
41 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list/records-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RecordsListComponent } from './records-list.component';
4 |
5 | describe('RecordsListComponent', () => {
6 | let component: RecordsListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [RecordsListComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(RecordsListComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-page/records-page.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RecordsPageComponent } from './records-page.component';
4 |
5 | describe('RecordsPageComponent', () => {
6 | let component: RecordsPageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [RecordsPageComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(RecordsPageComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/not-found-page/not-found-page.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { NotFoundPageComponent } from './not-found-page.component';
4 |
5 | describe('NotFoundPageComponent', () => {
6 | let component: NotFoundPageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [NotFoundPageComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(NotFoundPageComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/post-list-item/post-list-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PostListItemComponent } from './post-list-item.component';
4 |
5 | describe('PostListItemComponent', () => {
6 | let component: PostListItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [PostListItemComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(PostListItemComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/post-list-page/post-list-page.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PostListPageComponent } from './post-list-page.component';
4 |
5 | describe('PostListPageComponent', () => {
6 | let component: PostListPageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [PostListPageComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(PostListPageComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/post-view-page/post-view-page.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PostViewPageComponent } from './post-view-page.component';
4 |
5 | describe('PostViewPageComponent', () => {
6 | let component: PostViewPageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [PostViewPageComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(PostViewPageComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/comments-widget/comments-widget.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CommentsWidgetComponent } from './comments-widget.component';
4 |
5 | describe('CommentsWidgetComponent', () => {
6 | let component: CommentsWidgetComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [CommentsWidgetComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(CommentsWidgetComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/comment-list-item/comment-list-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CommentListItemComponent } from './comment-list-item.component';
4 |
5 | describe('CommentListItemComponent', () => {
6 | let component: CommentListItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [CommentListItemComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(CommentListItemComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-list-item/records-list-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RecordsListItemComponent } from './records-list-item.component';
4 |
5 | describe('RecordsListItemComponent', () => {
6 | let component: RecordsListItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [RecordsListItemComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(RecordsListItemComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/comment-author-dialog/comment-author-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CommentAuthorDialogComponent } from './comment-author-dialog.component';
4 |
5 | describe('CommentAuthorDialogComponent', () => {
6 | let component: CommentAuthorDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [CommentAuthorDialogComponent],
12 | }).compileComponents();
13 |
14 | fixture = TestBed.createComponent(CommentAuthorDialogComponent);
15 | component = fixture.componentInstance;
16 | fixture.detectChanges();
17 | });
18 |
19 | it('should create', () => {
20 | expect(component).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/components/comments-widget/comments-widget.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { BlogPost, Comment } from '../../types';
8 |
9 | @Component({
10 | selector: 'comments-widget',
11 | templateUrl: './comments-widget.component.html',
12 | styleUrls: ['./comments-widget.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class CommentsWidgetComponent implements OnInit {
16 | @Input() postId: BlogPost['id'] = NaN;
17 | @Input() comments: Comment[] = [];
18 |
19 | constructor() {}
20 |
21 | ngOnInit(): void {}
22 |
23 | onCreateComment(comment: Comment) {
24 | // Not sure if this is legit to assign to @Input() prop.
25 | this.comments = [...this.comments, comment];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/components/comment-author-dialog/comment-author-dialog.component.html:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/app/components/error-page/error-page.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { Location } from '@angular/common';
3 | import { ErrorLocationState } from '../../error-handler';
4 |
5 | @Component({
6 | selector: 'app-error-page',
7 | templateUrl: './error-page.component.html',
8 | styleUrls: ['./error-page.component.scss'],
9 | changeDetection: ChangeDetectionStrategy.OnPush,
10 | })
11 | export class ErrorPageComponent implements OnInit {
12 | errorMessage!: string;
13 | errorTrace!: string;
14 |
15 | constructor(private location: Location) {}
16 |
17 | ngOnInit(): void {
18 | const locationState = this.location.getState();
19 |
20 | this.errorMessage = locationState.message ?? 'Unknown error';
21 | this.errorTrace = locationState.trace ?? 'No stack trace available';
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting,
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: {
11 | context(
12 | path: string,
13 | deep?: boolean,
14 | filter?: RegExp
15 | ): {
16 | (id: string): T;
17 | keys(): string[];
18 | };
19 | };
20 |
21 | // First, initialize the Angular testing environment.
22 | getTestBed().initTestEnvironment(
23 | BrowserDynamicTestingModule,
24 | platformBrowserDynamicTesting()
25 | );
26 |
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().forEach(context);
31 |
--------------------------------------------------------------------------------
/src/guestbook/components/record-form/record-form.component.html:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/app/components/comment-author-dialog/comment-author-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject, OnInit } from '@angular/core';
2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
3 | import gravatarUrl from 'gravatar-url';
4 |
5 | export type AuthorDialogData = {
6 | author: string;
7 | email: string;
8 | };
9 |
10 | @Component({
11 | selector: 'app-comment-author-dialog',
12 | templateUrl: './comment-author-dialog.component.html',
13 | styleUrls: ['./comment-author-dialog.component.scss'],
14 | })
15 | export class CommentAuthorDialogComponent implements OnInit {
16 | constructor(
17 | public dialogRef: MatDialogRef,
18 | @Inject(MAT_DIALOG_DATA) public data: AuthorDialogData
19 | ) {}
20 |
21 | get gravatarUrl() {
22 | return gravatarUrl(this.data.email, { size: 80 });
23 | }
24 |
25 | ngOnInit(): void {}
26 |
27 | onCloseButtonClick() {
28 | this.dialogRef.close();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "es2020",
20 | "module": "es2020",
21 | "lib": [
22 | "es2020",
23 | "dom"
24 | ]
25 | },
26 | "angularCompilerOptions": {
27 | "enableI18nLegacyMessageIdFormat": false,
28 | "strictInjectionParameters": true,
29 | "strictInputAccessModifiers": true,
30 | "strictTemplates": true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/components/post-view-page/post-view-page.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { BlogPost, BlogPostWithComments, Comment } from '../../types';
4 |
5 | @Component({
6 | selector: 'app-post-view-page',
7 | templateUrl: './post-view-page.component.html',
8 | styleUrls: ['./post-view-page.component.scss'],
9 | changeDetection: ChangeDetectionStrategy.OnPush,
10 | })
11 | export class PostViewPageComponent implements OnInit {
12 | comments: Comment[] = [];
13 | post: BlogPost = {
14 | id: NaN,
15 | author: 'Unknown',
16 | title: 'Untitled',
17 | previewText: 'No content',
18 | fullText: 'No content',
19 | };
20 |
21 | constructor(private activatedRoute: ActivatedRoute) {}
22 |
23 | ngOnInit(): void {
24 | const { post, comments } = (
25 | this.activatedRoute.snapshot.data
26 | );
27 |
28 | this.post = post;
29 | this.comments = comments;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/src/guestbook/state/guestbook.reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createAction,
3 | createFeatureSelector,
4 | createReducer,
5 | createSelector,
6 | on,
7 | props,
8 | } from '@ngrx/store';
9 | import { GuestbookRecord } from '../types';
10 | import { AppState } from '../../app/state/app.reducer';
11 |
12 | type GuestbookState = AppState & {
13 | records: GuestbookRecord[];
14 | };
15 |
16 | function description(actionDescription: TemplateStringsArray) {
17 | return `[Guestbook] ${actionDescription}`;
18 | }
19 |
20 | export const getGuestbookFeatureState =
21 | createFeatureSelector('guestbook');
22 | export const getGuestbookRecords = createSelector(
23 | getGuestbookFeatureState,
24 | (state) => state.records
25 | );
26 |
27 | export const createGuestbookRecord = createAction(
28 | description`Create record`,
29 | props<{ record: GuestbookRecord }>()
30 | );
31 |
32 | export const guestbookReducer = createReducer(
33 | {
34 | records: [],
35 | },
36 | on(createGuestbookRecord, (state, action) => ({
37 | ...state,
38 | records: [...state.records, action.record],
39 | }))
40 | );
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Demo
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.1.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
28 |
--------------------------------------------------------------------------------
/src/guestbook/components/records-page/records-page.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import {
4 | createGuestbookRecord,
5 | getGuestbookRecords,
6 | } from '../../state/guestbook.reducer';
7 | import { GuestbookRecord, GuestbookRecordFormData } from '../../types';
8 | import { AppState } from "../../../app/state/app.reducer";
9 |
10 | @Component({
11 | selector: 'app-records-page',
12 | templateUrl: './records-page.component.html',
13 | styleUrls: ['./records-page.component.scss'],
14 | changeDetection: ChangeDetectionStrategy.OnPush,
15 | })
16 | export class RecordsPageComponent implements OnInit {
17 | records: GuestbookRecord[] = [];
18 |
19 | constructor(private store: Store) {}
20 |
21 | ngOnInit(): void {
22 | this.store.select(getGuestbookRecords).subscribe((records) => {
23 | this.records = records;
24 | });
25 | }
26 |
27 | onRecordSubmit(record: GuestbookRecordFormData) {
28 | this.store.dispatch(
29 | createGuestbookRecord({
30 | record: {
31 | id: Math.round(Math.random() * 1000),
32 | name: record.name,
33 | text: record.text,
34 | },
35 | })
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/error-handler.ts:
--------------------------------------------------------------------------------
1 | import { ErrorHandler, Injectable, NgZone } from '@angular/core';
2 | import { NavigationExtras, Router } from '@angular/router';
3 | import { PageNotFoundError } from './errors';
4 |
5 | export type ErrorLocationState = {
6 | message?: string;
7 | trace?: string;
8 | };
9 |
10 | function isResolverError(error: any): error is UncaughtPromiseError {
11 | return typeof error === 'object' && error.rejection != null;
12 | }
13 |
14 | @Injectable()
15 | export class BlogErrorHandler implements ErrorHandler {
16 | constructor(private router: Router, private zone: NgZone) {}
17 |
18 | handleError(error: Error | UncaughtPromiseError) {
19 | const sourceError = isResolverError(error) ? error.rejection : error;
20 | const targetRoute =
21 | sourceError instanceof PageNotFoundError ? ['/404'] : ['/500'];
22 |
23 | const locationState: ErrorLocationState = {
24 | message: sourceError.message,
25 | trace: sourceError.stack,
26 | };
27 |
28 | const navigationState: NavigationExtras = { state: locationState };
29 |
30 | this.zone.run(() => {
31 | this.router
32 | .navigate(targetRoute, navigationState)
33 | .catch((error) =>
34 | console.log('Navigation failed with following error:', error)
35 | )
36 | .finally(() => console.log(sourceError));
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "deploy": "ng deploy --base-href=/demo/",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "watch": "ng build --watch --configuration development",
10 | "test": "ng test"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^14.0.0",
15 | "@angular/cdk": "14.2.0",
16 | "@angular/common": "^14.0.0",
17 | "@angular/compiler": "^14.0.0",
18 | "@angular/core": "^14.0.0",
19 | "@angular/forms": "^14.0.0",
20 | "@angular/material": "14.2.0",
21 | "@angular/platform-browser": "^14.0.0",
22 | "@angular/platform-browser-dynamic": "^14.0.0",
23 | "@angular/router": "^14.0.0",
24 | "@ngrx/store": "14.3.1",
25 | "gravatar-url": "4.0.1",
26 | "rxjs": "~7.5.0",
27 | "tslib": "^2.3.0",
28 | "zone.js": "~0.11.4"
29 | },
30 | "devDependencies": {
31 | "@angular-devkit/build-angular": "^14.2.1",
32 | "@angular/cli": "~14.2.1",
33 | "@angular/compiler-cli": "^14.0.0",
34 | "@types/jasmine": "~4.0.0",
35 | "angular-cli-ghpages": "1.0.3",
36 | "jasmine-core": "~4.3.0",
37 | "karma": "~6.4.0",
38 | "karma-chrome-launcher": "~3.1.0",
39 | "karma-coverage": "~2.2.0",
40 | "karma-jasmine": "~5.1.0",
41 | "karma-jasmine-html-reporter": "~2.0.0",
42 | "prettier": "2.7.1",
43 | "typescript": "~4.7.2"
44 | }
45 | }
--------------------------------------------------------------------------------
/src/app/services/posts.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BlogPost, JpBlogPost } from '../types';
3 | import { HttpClient } from '@angular/common/http';
4 | import { map } from 'rxjs';
5 |
6 | const RANDOM_AUTHORS = [
7 | 'Math Blank',
8 | 'Oris Lega',
9 | 'Dino Paths',
10 | 'Noris Jsonson',
11 | 'Emma Walson',
12 | ];
13 |
14 | function jpBlogPostToBlogPost(jpBlogPost: JpBlogPost): BlogPost {
15 | return {
16 | id: jpBlogPost.id,
17 | author: RANDOM_AUTHORS[jpBlogPost.id % RANDOM_AUTHORS.length],
18 | title: jpBlogPost.title[0].toUpperCase() + jpBlogPost.title.substring(1),
19 | fullText: jpBlogPost.body,
20 | previewText: jpBlogPost.body.substring(0, 300).trim() + '...',
21 | };
22 | }
23 |
24 | @Injectable({
25 | providedIn: 'root',
26 | })
27 | export class PostsService {
28 | constructor(private http: HttpClient) {}
29 |
30 | fetchPost(postId: BlogPost['id']) {
31 | return this.http
32 | .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
33 | .pipe(map((jsBlogPost) => jpBlogPostToBlogPost(jsBlogPost)));
34 | }
35 |
36 | fetchPosts() {
37 | return this.http
38 | .get('https://jsonplaceholder.typicode.com/posts')
39 | .pipe(
40 | map((jsBlogPosts) =>
41 | jsBlogPosts
42 | .slice(0, 5)
43 | .map((jsBlogPost) => jpBlogPostToBlogPost(jsBlogPost))
44 | )
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/guestbook/components/record-form/record-form.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | ElementRef,
5 | EventEmitter,
6 | OnInit,
7 | Output,
8 | ViewChild,
9 | } from '@angular/core';
10 | import { NonNullableFormBuilder, Validators } from '@angular/forms';
11 | import { GuestbookRecordFormData } from '../../types';
12 |
13 | @Component({
14 | selector: 'guestbook-record-form',
15 | templateUrl: './record-form.component.html',
16 | styleUrls: ['./record-form.component.scss'],
17 | changeDetection: ChangeDetectionStrategy.OnPush,
18 | })
19 | export class RecordFormComponent implements OnInit {
20 | @Output() submitEvent = new EventEmitter();
21 | @ViewChild('formElement') formElement!: ElementRef;
22 |
23 | form = this.formBuilder.group(
24 | {
25 | name: ['', [Validators.required]],
26 | text: ['', [Validators.required]],
27 | },
28 | {
29 | updateOn: 'submit',
30 | }
31 | );
32 |
33 | constructor(private formBuilder: NonNullableFormBuilder) {}
34 |
35 | ngOnInit(): void {}
36 |
37 | onSubmit() {
38 | if (!this.form.valid) {
39 | return;
40 | }
41 |
42 | const formData: GuestbookRecordFormData = {
43 | name: this.form.value.name!,
44 | text: this.form.value.text!,
45 | };
46 |
47 | this.form.reset()
48 | this.formElement.nativeElement.reset()
49 |
50 | this.submitEvent.emit(formData);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/guestbook/guestbook.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RecordFormComponent } from './components/record-form/record-form.component';
4 | import { RecordsPageComponent } from './components/records-page/records-page.component';
5 | import { RecordsListComponent } from './components/records-list/records-list.component';
6 | import { RecordsListItemComponent } from './components/records-list-item/records-list-item.component';
7 | import { GuestbookRoutingModule } from './guestbook-routing.module';
8 | import { StoreModule } from '@ngrx/store';
9 | import { guestbookReducer } from './state/guestbook.reducer';
10 | import { MatCardModule } from '@angular/material/card';
11 | import { MatFormFieldModule } from '@angular/material/form-field';
12 | import { ReactiveFormsModule } from '@angular/forms';
13 | import { MatInputModule } from '@angular/material/input';
14 | import { MatButtonModule } from '@angular/material/button';
15 |
16 | @NgModule({
17 | declarations: [
18 | RecordFormComponent,
19 | RecordsPageComponent,
20 | RecordsListComponent,
21 | RecordsListItemComponent,
22 | ],
23 | imports: [
24 | CommonModule,
25 | GuestbookRoutingModule,
26 | StoreModule.forFeature('guestbook', guestbookReducer),
27 | MatCardModule,
28 | MatFormFieldModule,
29 | ReactiveFormsModule,
30 | MatInputModule,
31 | MatButtonModule,
32 | ],
33 | })
34 | export class GuestbookModule {}
35 |
--------------------------------------------------------------------------------
/src/app/components/comment-list-item/comment-list-item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Input,
5 | OnInit,
6 | } from '@angular/core';
7 | import { Comment } from '../../types';
8 | import gravatarUrl from 'gravatar-url';
9 | import { MatDialog } from '@angular/material/dialog';
10 | import {
11 | AuthorDialogData,
12 | CommentAuthorDialogComponent,
13 | } from '../comment-author-dialog/comment-author-dialog.component';
14 |
15 | @Component({
16 | selector: 'comment-list-item',
17 | templateUrl: './comment-list-item.component.html',
18 | styleUrls: ['./comment-list-item.component.scss'],
19 | changeDetection: ChangeDetectionStrategy.OnPush,
20 | })
21 | export class CommentListItemComponent implements OnInit {
22 | @Input() comment: Comment = {
23 | id: NaN,
24 | author: 'Unknown',
25 | text: 'No content',
26 | email: 'unknown',
27 | };
28 |
29 | constructor(public dialog: MatDialog) {}
30 |
31 | ngOnInit(): void {}
32 |
33 | get gravatarUrl() {
34 | return gravatarUrl(this.comment.email, { size: 80 });
35 | }
36 |
37 | onAuthorAvatarClick() {
38 | this.openAuthorDialog();
39 | }
40 |
41 | openAuthorDialog() {
42 | const authorDialogData: AuthorDialogData = {
43 | author: this.comment.author,
44 | email: this.comment.email,
45 | };
46 |
47 | this.dialog.open(CommentAuthorDialogComponent, {
48 | width: '400px',
49 | data: authorDialogData,
50 | });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/components/comment-form/comment-form.component.html:
--------------------------------------------------------------------------------
1 |
45 |
--------------------------------------------------------------------------------
/src/app/services/comments.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Comment, CommentFormData, JpComment } from '../types';
3 | import { BlogPost } from '../types';
4 | import { HttpClient } from '@angular/common/http';
5 | import { map } from 'rxjs';
6 |
7 | function jpCommentToComment(jpComment: JpComment): Comment {
8 | return {
9 | author: jpComment.email.split('@')[0],
10 | email: jpComment.email,
11 | id: jpComment.id,
12 | text: jpComment.body,
13 | };
14 | }
15 |
16 | @Injectable({
17 | providedIn: 'root',
18 | })
19 | export class CommentsService {
20 | constructor(private http: HttpClient) {}
21 |
22 | fetchComments(postId: BlogPost['id']) {
23 | return this.http
24 | .get(
25 | `http://jsonplaceholder.typicode.com/posts/${postId}/comments`
26 | )
27 | .pipe(
28 | map((jpComments) =>
29 | jpComments.map((jpComment) => jpCommentToComment(jpComment))
30 | )
31 | );
32 | }
33 |
34 | createComment(postId: BlogPost['id'], data: CommentFormData) {
35 | return (
36 | this.http
37 | .post(
38 | `http://jsonplaceholder.typicode.com/posts/${postId}/comments`,
39 | data
40 | )
41 | // Mock new comment in response
42 | .pipe(
43 | map(() => ({
44 | id: Math.round(Math.random() * 1000),
45 | text: data.body,
46 | author: data.name,
47 | email: data.email,
48 | }))
49 | )
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | jasmine: {
17 | // you can add configuration options for Jasmine here
18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
19 | // for example, you can disable the random execution with `random: false`
20 | // or set a specific seed with `seed: 4321`
21 | },
22 | clearContext: false // leave Jasmine Spec Runner output visible in browser
23 | },
24 | jasmineHtmlReporter: {
25 | suppressAll: true // removes the duplicated traces
26 | },
27 | coverageReporter: {
28 | dir: require('path').join(__dirname, './coverage/demo'),
29 | subdir: '.',
30 | reporters: [
31 | { type: 'html' },
32 | { type: 'text-summary' }
33 | ]
34 | },
35 | reporters: ['progress', 'kjhtml'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: config.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: false,
42 | restartOnFileChange: true
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { NotFoundPageComponent } from './components/not-found-page/not-found-page.component';
4 | import { ErrorPageComponent } from './components/error-page/error-page.component';
5 | import { PostListPageComponent } from './components/post-list-page/post-list-page.component';
6 | import { PostsResolver } from './resolvers/posts.resolver';
7 | import { PostViewPageComponent } from './components/post-view-page/post-view-page.component';
8 | import { PostResolver } from './resolvers/post.resolver';
9 | import { PostCommentsResolver } from './resolvers/post-comments.resolver';
10 |
11 | const routes: Routes = [
12 | {
13 | path: '',
14 | pathMatch: 'full',
15 | redirectTo: '/posts',
16 | },
17 | {
18 | path: 'posts',
19 | pathMatch: 'full',
20 | component: PostListPageComponent,
21 | resolve: {
22 | posts: PostsResolver,
23 | },
24 | },
25 | {
26 | path: 'posts/:id',
27 | pathMatch: 'full',
28 | component: PostViewPageComponent,
29 | resolve: {
30 | post: PostResolver,
31 | comments: PostCommentsResolver,
32 | },
33 | },
34 | {
35 | path: '404',
36 | pathMatch: 'full',
37 | component: NotFoundPageComponent,
38 | },
39 | {
40 | path: '500',
41 | pathMatch: 'full',
42 | component: ErrorPageComponent,
43 | },
44 | {
45 | path: '**',
46 | redirectTo: '404',
47 | },
48 | ];
49 |
50 | @NgModule({
51 | imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'top' })],
52 | exports: [RouterModule],
53 | })
54 | export class AppRoutingModule {}
55 |
--------------------------------------------------------------------------------
/src/app/components/comment-form/comment-form.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | ElementRef,
5 | EventEmitter,
6 | Input,
7 | OnInit,
8 | Output,
9 | ViewChild,
10 | } from '@angular/core';
11 | import { NonNullableFormBuilder, Validators } from '@angular/forms';
12 | import { CommentsService } from '../../services/comments.service';
13 | import { BlogPost, Comment, CommentFormData } from '../../types';
14 |
15 | @Component({
16 | selector: 'comment-form',
17 | templateUrl: './comment-form.component.html',
18 | styleUrls: ['./comment-form.component.scss'],
19 | changeDetection: ChangeDetectionStrategy.OnPush,
20 | })
21 | export class CommentFormComponent implements OnInit {
22 | @Input() postId: BlogPost['id'] = NaN;
23 | @Output() createCommentEvent = new EventEmitter();
24 |
25 | @ViewChild('formElement') formElement!: ElementRef;
26 |
27 | form = this.formBuilder.group(
28 | {
29 | author: ['', [Validators.required]],
30 | email: ['', [Validators.required, Validators.email]],
31 | text: ['', [Validators.required]],
32 | },
33 | {
34 | updateOn: 'submit',
35 | }
36 | );
37 |
38 | constructor(
39 | private formBuilder: NonNullableFormBuilder,
40 | private commentsService: CommentsService
41 | ) {}
42 |
43 | ngOnInit(): void {}
44 |
45 | onSubmit() {
46 | if (!this.form.valid) {
47 | return;
48 | }
49 |
50 | const formData: CommentFormData = {
51 | body: this.form.value.text!,
52 | email: this.form.value.email!,
53 | name: this.form.value.author!,
54 | };
55 |
56 | this.form.disable();
57 |
58 | this.commentsService
59 | .createComment(this.postId, formData)
60 | .subscribe((comment) => {
61 | this.createCommentEvent.emit(comment);
62 |
63 | this.form.enable();
64 | this.form.reset();
65 |
66 | this.formElement.nativeElement.reset();
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including
12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 | /***************************************************************************************************
51 | * APPLICATION IMPORTS
52 | */
53 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { ErrorHandler, NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { AppRoutingModule } from './app-routing.module';
4 | import { AppComponent } from './components/app/app.component';
5 | import { MatToolbarModule } from '@angular/material/toolbar';
6 | import { MatButtonModule } from '@angular/material/button';
7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
8 | import { ErrorPageComponent } from './components/error-page/error-page.component';
9 | import { BlogErrorHandler } from './error-handler';
10 | import { MatInputModule } from '@angular/material/input';
11 | import { CommentListComponent } from './components/comment-list/comment-list.component';
12 | import { CommentListItemComponent } from './components/comment-list-item/comment-list-item.component';
13 | import { CommentsWidgetComponent } from './components/comments-widget/comments-widget.component';
14 | import { CommentFormComponent } from './components/comment-form/comment-form.component';
15 | import { CommentAuthorDialogComponent } from './components/comment-author-dialog/comment-author-dialog.component';
16 | import { MatFormFieldModule } from '@angular/material/form-field';
17 | import { MatIconModule } from '@angular/material/icon';
18 | import { CommonModule } from '@angular/common';
19 | import { ReactiveFormsModule } from '@angular/forms';
20 | import { MatDialogModule } from '@angular/material/dialog';
21 | import { HttpClientModule } from '@angular/common/http';
22 | import { PostListComponent } from './components/post-list/post-list.component';
23 | import { PostListItemComponent } from './components/post-list-item/post-list-item.component';
24 | import { PostListPageComponent } from './components/post-list-page/post-list-page.component';
25 | import { PostViewPageComponent } from './components/post-view-page/post-view-page.component';
26 | import { NotFoundPageComponent } from './components/not-found-page/not-found-page.component';
27 | import { MatCardModule } from '@angular/material/card';
28 | import { MatDividerModule } from '@angular/material/divider';
29 | import { StoreModule } from '@ngrx/store';
30 | import { GuestbookModule } from '../guestbook/guestbook.module';
31 |
32 | @NgModule({
33 | declarations: [
34 | AppComponent,
35 | ErrorPageComponent,
36 | CommentListComponent,
37 | CommentListItemComponent,
38 | CommentsWidgetComponent,
39 | CommentFormComponent,
40 | CommentAuthorDialogComponent,
41 | PostListComponent,
42 | PostListItemComponent,
43 | PostListPageComponent,
44 | PostViewPageComponent,
45 | NotFoundPageComponent,
46 | ],
47 | imports: [
48 | BrowserModule,
49 | GuestbookModule,
50 | AppRoutingModule,
51 | MatToolbarModule,
52 | MatButtonModule,
53 | MatInputModule,
54 | MatFormFieldModule,
55 | MatIconModule,
56 | MatInputModule,
57 | CommonModule,
58 | MatButtonModule,
59 | ReactiveFormsModule,
60 | MatDialogModule,
61 | HttpClientModule,
62 | BrowserAnimationsModule,
63 | CommonModule,
64 | MatCardModule,
65 | MatDividerModule,
66 | MatButtonModule,
67 | HttpClientModule,
68 | StoreModule.forRoot({}, {}),
69 | ],
70 | providers: [
71 | {
72 | provide: ErrorHandler,
73 | useClass: BlogErrorHandler,
74 | },
75 | ],
76 | bootstrap: [AppComponent],
77 | })
78 | export class AppModule {}
79 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "./demo",
5 | "projects": {
6 | "demo": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "dist/demo",
21 | "index": "src/index.html",
22 | "main": "src/main.ts",
23 | "polyfills": "src/polyfills.ts",
24 | "tsConfig": "tsconfig.app.json",
25 | "inlineStyleLanguage": "scss",
26 | "assets": [
27 | "src/favicon.ico",
28 | "src/assets"
29 | ],
30 | "styles": [
31 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
32 | "src/styles.scss"
33 | ],
34 | "scripts": []
35 | },
36 | "configurations": {
37 | "production": {
38 | "budgets": [
39 | {
40 | "type": "initial",
41 | "maximumWarning": "500kb",
42 | "maximumError": "1mb"
43 | },
44 | {
45 | "type": "anyComponentStyle",
46 | "maximumWarning": "2kb",
47 | "maximumError": "4kb"
48 | }
49 | ],
50 | "fileReplacements": [
51 | {
52 | "replace": "src/environments/environment.ts",
53 | "with": "src/environments/environment.prod.ts"
54 | }
55 | ],
56 | "outputHashing": "all"
57 | },
58 | "development": {
59 | "buildOptimizer": false,
60 | "optimization": false,
61 | "vendorChunk": true,
62 | "extractLicenses": false,
63 | "sourceMap": true,
64 | "namedChunks": true
65 | }
66 | },
67 | "defaultConfiguration": "production"
68 | },
69 | "serve": {
70 | "builder": "@angular-devkit/build-angular:dev-server",
71 | "configurations": {
72 | "production": {
73 | "browserTarget": "demo:build:production"
74 | },
75 | "development": {
76 | "browserTarget": "demo:build:development"
77 | }
78 | },
79 | "defaultConfiguration": "development"
80 | },
81 | "extract-i18n": {
82 | "builder": "@angular-devkit/build-angular:extract-i18n",
83 | "options": {
84 | "browserTarget": "demo:build"
85 | }
86 | },
87 | "test": {
88 | "builder": "@angular-devkit/build-angular:karma",
89 | "options": {
90 | "main": "src/test.ts",
91 | "polyfills": "src/polyfills.ts",
92 | "tsConfig": "tsconfig.spec.json",
93 | "karmaConfig": "karma.conf.js",
94 | "inlineStyleLanguage": "scss",
95 | "assets": [
96 | "src/favicon.ico",
97 | "src/assets"
98 | ],
99 | "styles": [
100 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
101 | "src/styles.scss"
102 | ],
103 | "scripts": []
104 | }
105 | },
106 | "deploy": {
107 | "builder": "angular-cli-ghpages:deploy"
108 | }
109 | }
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------