From f319b531641f8948d770234b1feec977b5a182cf Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 13:37:51 +0300 Subject: [PATCH 01/36] Create Github Action --- .github/workflows/main.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..39b74e8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,28 @@ +name: Build and Deploy Script +on: + push: + branches: + - main + - training/* + +jobs: + build: + name: Build and Deploy + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Adding Node.js + uses: actions/setup-node@v2 + with: + node-version: 22.15.1 + - name: Install Dependencies + run: npm install + - name: Build Angular App + run: npm run build + # - name: Deploy to gh pages + # run: | + # npx angular-cli-ghpages --dir=dist/training-app + # env: + # CI: true + # GH_TOKEN: ${{ secrets.GH_TOKEN }} From 702930008f8fdbbea2897b9ecd08618abf0effb2 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:00:30 +0300 Subject: [PATCH 02/36] Update main.yml --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39b74e8..950b946 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,9 +20,9 @@ jobs: run: npm install - name: Build Angular App run: npm run build - # - name: Deploy to gh pages - # run: | - # npx angular-cli-ghpages --dir=dist/training-app - # env: - # CI: true - # GH_TOKEN: ${{ secrets.GH_TOKEN }} + - name: Deploy to gh pages + run: | + npx angular-cli-ghpages --dir=dist/training-app + env: + CI: true + GH_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} From ba4ad760ab4e590d7b5558736d30d6a60835839f Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:07:57 +0300 Subject: [PATCH 03/36] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 950b946..8b983c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: run: npm run build - name: Deploy to gh pages run: | - npx angular-cli-ghpages --dir=dist/training-app + npx angular-cli-ghpages --dir=dist/angular-template env: CI: true GH_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} From 5712f888df3ead53014aebcaad387d6704442b7c Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:12:33 +0300 Subject: [PATCH 04/36] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b983c3..14949cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: run: npm run build - name: Deploy to gh pages run: | - npx angular-cli-ghpages --dir=dist/angular-template + npx angular-cli-ghpages --base-href=/angular-template/ env: CI: true GH_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} From e98348d8a536398730f4d752673600aa8cd1e991 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:15:30 +0300 Subject: [PATCH 05/36] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14949cd..8b983c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: run: npm run build - name: Deploy to gh pages run: | - npx angular-cli-ghpages --base-href=/angular-template/ + npx angular-cli-ghpages --dir=dist/angular-template env: CI: true GH_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} From 6067ee5a6b151d515adfbf7158db1c1dfce7be81 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:22:55 +0300 Subject: [PATCH 06/36] Update main.yml --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b983c3..c4884b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,9 +20,9 @@ jobs: run: npm install - name: Build Angular App run: npm run build - - name: Deploy to gh pages - run: | - npx angular-cli-ghpages --dir=dist/angular-template - env: - CI: true - GH_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + GITHUB_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} + BRANCH: gh-pages + FOLDER: dist/angular-template From 1aeec1b449d52da0ab263bb6ee19c01523fbdf2f Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:32:47 +0300 Subject: [PATCH 07/36] Rename deployment step in workflow from "Deploy to GitHub Pages" to "Deploy to Pages" --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4884b1..73c82fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: run: npm install - name: Build Angular App run: npm run build - - name: Deploy to GitHub Pages + - name: Deploy to Pages uses: JamesIves/github-pages-deploy-action@v4 with: GITHUB_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} From dcc1661566b08d09eafc82aa9b877eddb14f5d6c Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:36:50 +0300 Subject: [PATCH 08/36] Fix GitHub token reference in deployment step --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73c82fa..9912a18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,6 @@ jobs: - name: Deploy to Pages uses: JamesIves/github-pages-deploy-action@v4 with: - GITHUB_TOKEN: ${{ secrets.GH_TRAINING_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages FOLDER: dist/angular-template From 85716bd58bd36f436081df355932ec9cb6e913d2 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 14:47:43 +0300 Subject: [PATCH 09/36] Refactor deployment step in workflow and update Angular CLI configuration --- .github/workflows/main.yml | 5 ++--- angular.json | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9912a18..bb35ab7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,5 @@ jobs: - name: Deploy to Pages uses: JamesIves/github-pages-deploy-action@v4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: dist/angular-template + token: ${{ secrets.GITHUB_TOKEN }} + folder: dist/angular-template diff --git a/angular.json b/angular.json index b5db2fb..b44e321 100644 --- a/angular.json +++ b/angular.json @@ -113,6 +113,7 @@ "cli": { "schematicCollections": [ "@angular-eslint/schematics" - ] + ], + "analytics": false } } From 7c0167d9e751586e2e91168974e7eee4cfd1afa1 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:08:59 +0300 Subject: [PATCH 10/36] Update title in index.html to "AngularTemplate1" --- src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index 98d504c..1d918ee 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - AngularTemplate + AngularTemplate1 From a0cd7590871dd5f8935d5ea0cdc112c1cd87a109 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:22:19 +0300 Subject: [PATCH 11/36] Add gbook and post components with initial templates and tests --- angular.json | 3 ++- .../gbook-edit/gbook-edit.component.html | 1 + .../gbook-edit/gbook-edit.component.scss | 0 .../gbook-edit/gbook-edit.component.spec.ts | 23 +++++++++++++++++++ .../gbook/gbook-edit/gbook-edit.component.ts | 11 +++++++++ .../gbook-page/gbook-page.component.html | 1 + .../gbook-page/gbook-page.component.scss | 0 .../gbook-page/gbook-page.component.spec.ts | 23 +++++++++++++++++++ .../gbook/gbook-page/gbook-page.component.ts | 11 +++++++++ .../posts/post-page/post-page.component.html | 1 + .../posts/post-page/post-page.component.scss | 0 .../post-page/post-page.component.spec.ts | 23 +++++++++++++++++++ .../posts/post-page/post-page.component.ts | 11 +++++++++ .../posts-list/posts-list.component.html | 1 + .../posts-list/posts-list.component.scss | 0 .../posts-list/posts-list.component.spec.ts | 23 +++++++++++++++++++ .../posts/posts-list/posts-list.component.ts | 11 +++++++++ 17 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.html create mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.scss create mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.spec.ts create mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.ts create mode 100644 src/app/gbook/gbook-page/gbook-page.component.html create mode 100644 src/app/gbook/gbook-page/gbook-page.component.scss create mode 100644 src/app/gbook/gbook-page/gbook-page.component.spec.ts create mode 100644 src/app/gbook/gbook-page/gbook-page.component.ts create mode 100644 src/app/posts/post-page/post-page.component.html create mode 100644 src/app/posts/post-page/post-page.component.scss create mode 100644 src/app/posts/post-page/post-page.component.spec.ts create mode 100644 src/app/posts/post-page/post-page.component.ts create mode 100644 src/app/posts/posts-list/posts-list.component.html create mode 100644 src/app/posts/posts-list/posts-list.component.scss create mode 100644 src/app/posts/posts-list/posts-list.component.spec.ts create mode 100644 src/app/posts/posts-list/posts-list.component.ts diff --git a/angular.json b/angular.json index b5db2fb..b44e321 100644 --- a/angular.json +++ b/angular.json @@ -113,6 +113,7 @@ "cli": { "schematicCollections": [ "@angular-eslint/schematics" - ] + ], + "analytics": false } } diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.html b/src/app/gbook/gbook-edit/gbook-edit.component.html new file mode 100644 index 0000000..cb40a70 --- /dev/null +++ b/src/app/gbook/gbook-edit/gbook-edit.component.html @@ -0,0 +1 @@ +

gbook-edit works!

diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.scss b/src/app/gbook/gbook-edit/gbook-edit.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.spec.ts b/src/app/gbook/gbook-edit/gbook-edit.component.spec.ts new file mode 100644 index 0000000..52194f9 --- /dev/null +++ b/src/app/gbook/gbook-edit/gbook-edit.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GbookEditComponent } from './gbook-edit.component'; + +describe('GbookEditComponent', () => { + let component: GbookEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GbookEditComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GbookEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.ts b/src/app/gbook/gbook-edit/gbook-edit.component.ts new file mode 100644 index 0000000..727da4e --- /dev/null +++ b/src/app/gbook/gbook-edit/gbook-edit.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-gbook-edit', + imports: [], + templateUrl: './gbook-edit.component.html', + styleUrl: './gbook-edit.component.scss' +}) +export class GbookEditComponent { + +} diff --git a/src/app/gbook/gbook-page/gbook-page.component.html b/src/app/gbook/gbook-page/gbook-page.component.html new file mode 100644 index 0000000..6e5358b --- /dev/null +++ b/src/app/gbook/gbook-page/gbook-page.component.html @@ -0,0 +1 @@ +

gbook-page works!

diff --git a/src/app/gbook/gbook-page/gbook-page.component.scss b/src/app/gbook/gbook-page/gbook-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/gbook/gbook-page/gbook-page.component.spec.ts b/src/app/gbook/gbook-page/gbook-page.component.spec.ts new file mode 100644 index 0000000..c4e9639 --- /dev/null +++ b/src/app/gbook/gbook-page/gbook-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GbookPageComponent } from './gbook-page.component'; + +describe('GbookPageComponent', () => { + let component: GbookPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GbookPageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GbookPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/gbook/gbook-page/gbook-page.component.ts b/src/app/gbook/gbook-page/gbook-page.component.ts new file mode 100644 index 0000000..2264754 --- /dev/null +++ b/src/app/gbook/gbook-page/gbook-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-gbook-page', + imports: [], + templateUrl: './gbook-page.component.html', + styleUrl: './gbook-page.component.scss' +}) +export class GbookPageComponent { + +} diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/posts/post-page/post-page.component.html new file mode 100644 index 0000000..afd2aad --- /dev/null +++ b/src/app/posts/post-page/post-page.component.html @@ -0,0 +1 @@ +

post-page works!

diff --git a/src/app/posts/post-page/post-page.component.scss b/src/app/posts/post-page/post-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/post-page/post-page.component.spec.ts new file mode 100644 index 0000000..6bb5a12 --- /dev/null +++ b/src/app/posts/post-page/post-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PostPageComponent } from './post-page.component'; + +describe('PostPageComponent', () => { + let component: PostPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostPageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PostPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts new file mode 100644 index 0000000..2a119c8 --- /dev/null +++ b/src/app/posts/post-page/post-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-post-page', + imports: [], + templateUrl: './post-page.component.html', + styleUrl: './post-page.component.scss' +}) +export class PostPageComponent { + +} diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/posts/posts-list/posts-list.component.html new file mode 100644 index 0000000..559c7fe --- /dev/null +++ b/src/app/posts/posts-list/posts-list.component.html @@ -0,0 +1 @@ +

posts-list works!

diff --git a/src/app/posts/posts-list/posts-list.component.scss b/src/app/posts/posts-list/posts-list.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/posts/posts-list/posts-list.component.spec.ts b/src/app/posts/posts-list/posts-list.component.spec.ts new file mode 100644 index 0000000..00dd16e --- /dev/null +++ b/src/app/posts/posts-list/posts-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PostsListComponent } from './posts-list.component'; + +describe('PostsListComponent', () => { + let component: PostsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostsListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PostsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/posts/posts-list/posts-list.component.ts new file mode 100644 index 0000000..039179a --- /dev/null +++ b/src/app/posts/posts-list/posts-list.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-posts-list', + imports: [], + templateUrl: './posts-list.component.html', + styleUrl: './posts-list.component.scss' +}) +export class PostsListComponent { + +} From 1783db91e552f29c9103d1d6a47a8893fd1a059c Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:27:05 +0300 Subject: [PATCH 12/36] Update deployment action in workflow to use AhsanAyaz/angular-deploy-gh-pages-actions --- .github/workflows/main.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb35ab7..f04fe23 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,14 @@ jobs: - name: Build Angular App run: npm run build - name: Deploy to Pages - uses: JamesIves/github-pages-deploy-action@v4 + uses: AhsanAyaz/angular-deploy-gh-pages-actions@v1.4.0 with: - token: ${{ secrets.GITHUB_TOKEN }} - folder: dist/angular-template + github_access_token: ${{ secrets.GITHUB_TOKEN }} + build_configuration: production + base_href: /angular-template/ + deploy_branch: gh-pages + angular_dist_build_folder: dist/angular-template + +permissions: + contents: write # Allow write permission to GITHUB_TOKEN to commit to deploy branch. + From ae9d769389978723bfed9666ea1df6105d19742f Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:32:13 +0300 Subject: [PATCH 13/36] Update angular_dist_build_folder path in deployment workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f04fe23..823b40e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: build_configuration: production base_href: /angular-template/ deploy_branch: gh-pages - angular_dist_build_folder: dist/angular-template + angular_dist_build_folder: dist/angular-template/browser permissions: contents: write # Allow write permission to GITHUB_TOKEN to commit to deploy branch. From bbcbd808863f8815025ee0e124569489072dc301 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:41:06 +0300 Subject: [PATCH 14/36] Add test execution step in deployment workflow --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 823b40e..5837aea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,6 +18,8 @@ jobs: node-version: 22.15.1 - name: Install Dependencies run: npm install + - name: Run tests + run: npm run test --no-watch --no-progress --browsers=ChromeHeadless - name: Build Angular App run: npm run build - name: Deploy to Pages @@ -30,5 +32,5 @@ jobs: angular_dist_build_folder: dist/angular-template/browser permissions: - contents: write # Allow write permission to GITHUB_TOKEN to commit to deploy branch. + contents: write From c6367b56d706be36a8bc559054d566ed32116dd5 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 15:53:10 +0300 Subject: [PATCH 15/36] Add CI test script to package.json for headless browser testing --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6584917..8026a11 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", + "test:ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless", "lint": "ng lint" }, "private": true, From 94b53fd84d8b9bd6a42b8e7846cc0e20c0137f90 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 16:02:24 +0300 Subject: [PATCH 16/36] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5837aea..2456733 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - name: Install Dependencies run: npm install - name: Run tests - run: npm run test --no-watch --no-progress --browsers=ChromeHeadless + run: npm run test:ci - name: Build Angular App run: npm run build - name: Deploy to Pages From 26283f2450d6cbe789ecaf93ca1d2615bcfd6caf Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 17:24:34 +0300 Subject: [PATCH 17/36] feat: Update app title and added posts feature - Changed app title from 'angular-template' to 'Training Angular Project'. - Introduced Post model interface for better type management. - Enhanced posts-list component to display posts with detailed information. - Created posts-page component to manage posts loading and error handling. - Implemented NgRx state management for posts, including actions, effects, reducers, and selectors. - Added PostsService for API interactions with JSONPlaceholder. - Set up environment configuration for API URL. - Updated main.ts to include NgRx store and effects providers. --- .github/workflows/main.yml | 2 +- package-lock.json | 91 ++++ package.json | 4 + src/app/app-routes.ts | 12 +- src/app/app.component.html | 492 +----------------- src/app/app.component.ts | 2 +- .../posts/post-page/post-page.component.html | 1 - .../posts/post-page/post-page.component.ts | 11 - src/app/posts/post.model.ts | 6 + .../posts-list/posts-list.component.html | 18 +- .../posts/posts-list/posts-list.component.ts | 7 +- .../posts-page/posts-page.component.html | 13 + .../posts-page.component.scss} | 0 .../posts-page.component.spec.ts} | 12 +- .../posts/posts-page/posts-page.component.ts | 27 + src/app/posts/posts.routes.ts | 9 + src/app/posts/posts.service.spec.ts | 16 + src/app/posts/posts.service.ts | 33 ++ src/app/posts/state/posts.actions.ts | 18 + src/app/posts/state/posts.effects.ts | 28 + src/app/posts/state/posts.reducer.ts | 37 ++ src/app/posts/state/posts.selectors.ts | 19 + src/environments/environment.ts | 4 + src/main.ts | 18 +- 24 files changed, 367 insertions(+), 513 deletions(-) delete mode 100644 src/app/posts/post-page/post-page.component.html delete mode 100644 src/app/posts/post-page/post-page.component.ts create mode 100644 src/app/posts/post.model.ts create mode 100644 src/app/posts/posts-page/posts-page.component.html rename src/app/posts/{post-page/post-page.component.scss => posts-page/posts-page.component.scss} (100%) rename src/app/posts/{post-page/post-page.component.spec.ts => posts-page/posts-page.component.spec.ts} (53%) create mode 100644 src/app/posts/posts-page/posts-page.component.ts create mode 100644 src/app/posts/posts.routes.ts create mode 100644 src/app/posts/posts.service.spec.ts create mode 100644 src/app/posts/posts.service.ts create mode 100644 src/app/posts/state/posts.actions.ts create mode 100644 src/app/posts/state/posts.effects.ts create mode 100644 src/app/posts/state/posts.reducer.ts create mode 100644 src/app/posts/state/posts.selectors.ts create mode 100644 src/environments/environment.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5837aea..2456733 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - name: Install Dependencies run: npm install - name: Run tests - run: npm run test --no-watch --no-progress --browsers=ChromeHeadless + run: npm run test:ci - name: Build Angular App run: npm run build - name: Deploy to Pages diff --git a/package-lock.json b/package-lock.json index 5e4c46a..00de835 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,10 @@ "@angular/platform-browser": "^19.2.10", "@angular/platform-browser-dynamic": "^19.2.10", "@angular/router": "^19.2.10", + "@ngrx/effects": "^19.2.0", + "@ngrx/entity": "^19.2.0", + "@ngrx/store": "^19.2.0", + "@ngrx/store-devtools": "^19.2.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -4957,6 +4961,61 @@ "node": ">= 10" } }, + "node_modules/@ngrx/effects": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.2.0.tgz", + "integrity": "sha512-DIoFdEdSehAMHUNTWIdl94HjhSh1ZRx0Rgtgp1TjHHyjLiS+vbMmDgPjrCkBv5lT/pEaKbHKnYxjY3CQiW2Hsg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^19.0.0", + "@ngrx/store": "19.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/entity": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-19.2.0.tgz", + "integrity": "sha512-JxKFBk0LAHrmCGLQFQeT8mZhwTZPKzq0m0gqCtXgmtzHj9B/ln3yluTtBWgOEf07dDAkEX/q42Sr+kO+5kctvg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^19.0.0", + "@ngrx/store": "19.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.0.tgz", + "integrity": "sha512-k2n/jLJZ75Z5rd5vPa2mXPYG/On2rFLiNdrccs9Dw2r+oJosORMlN5TbdsGHhVDFfjzbY9a7JbHUE3YOa69gqw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^19.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store-devtools": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.2.0.tgz", + "integrity": "sha512-AKlXHsuSRJgYYxmrXZ8WWnDxqgKMG0+HP+IIDmk5h5Z5RIkOLHk6ZGKbakhIiFlL8d16N+GcJ76rqUajaHT+0w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^19.0.0", + "@ngrx/store": "19.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@ngtools/webpack": { "version": "19.2.12", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.12.tgz", @@ -18844,6 +18903,38 @@ "dev": true, "optional": true }, + "@ngrx/effects": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.2.0.tgz", + "integrity": "sha512-DIoFdEdSehAMHUNTWIdl94HjhSh1ZRx0Rgtgp1TjHHyjLiS+vbMmDgPjrCkBv5lT/pEaKbHKnYxjY3CQiW2Hsg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/entity": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-19.2.0.tgz", + "integrity": "sha512-JxKFBk0LAHrmCGLQFQeT8mZhwTZPKzq0m0gqCtXgmtzHj9B/ln3yluTtBWgOEf07dDAkEX/q42Sr+kO+5kctvg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.0.tgz", + "integrity": "sha512-k2n/jLJZ75Z5rd5vPa2mXPYG/On2rFLiNdrccs9Dw2r+oJosORMlN5TbdsGHhVDFfjzbY9a7JbHUE3YOa69gqw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store-devtools": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.2.0.tgz", + "integrity": "sha512-AKlXHsuSRJgYYxmrXZ8WWnDxqgKMG0+HP+IIDmk5h5Z5RIkOLHk6ZGKbakhIiFlL8d16N+GcJ76rqUajaHT+0w==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "19.2.12", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.12.tgz", diff --git a/package.json b/package.json index 6584917..74302d7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "@angular/platform-browser": "^19.2.10", "@angular/platform-browser-dynamic": "^19.2.10", "@angular/router": "^19.2.10", + "@ngrx/effects": "^19.2.0", + "@ngrx/entity": "^19.2.0", + "@ngrx/store": "^19.2.0", + "@ngrx/store-devtools": "^19.2.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index a1cf04d..69586cb 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,3 +1,13 @@ import { Routes } from '@angular/router'; +import { PostsService } from './posts/posts.service'; -export const appRoutes: Routes = []; +export const appRoutes: Routes = [ + { + path: '', + loadChildren: () => + import('./posts/posts.routes').then((mod) => mod.routes), + providers: [ + PostsService + ], + }, +]; diff --git a/src/app/app.component.html b/src/app/app.component.html index f28834e..79efb01 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,488 +1,4 @@ - - - - - -
- - -
- - - Rocket Ship - - - - - - - - - - {{ title }} app is running! - - - Rocket Ship Smoke - - - -
- - -

Resources

-

Here are some links to help you get started:

- - - - -

Next Steps

-

What do you want to do next with your app?

- - - -
- - - - - - - - - - - -
- - -
-@switch (selection.value) { - @default { -
ng generate component xyz
-} - @case ('material') { -
ng add @angular/material
-} - @case ('pwa') { -
ng add @angular/pwa
-} - @case ('dependency') { -
ng add _____
-} - @case ('test') { -
ng test
-} - @case ('build') { -
ng build
-} - } -
- - - - - - - - - Gray Clouds Background - - - -
- - - - - - - - - - +
+
+ +
\ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c53dbc9..2c12239 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -9,5 +9,5 @@ import { RouterOutlet } from '@angular/router'; styleUrls: ['./app.component.scss'], }) export class AppComponent { - title = 'angular-template'; + title = 'Training Angular Project'; } diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/posts/post-page/post-page.component.html deleted file mode 100644 index afd2aad..0000000 --- a/src/app/posts/post-page/post-page.component.html +++ /dev/null @@ -1 +0,0 @@ -

post-page works!

diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts deleted file mode 100644 index 2a119c8..0000000 --- a/src/app/posts/post-page/post-page.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-post-page', - imports: [], - templateUrl: './post-page.component.html', - styleUrl: './post-page.component.scss' -}) -export class PostPageComponent { - -} diff --git a/src/app/posts/post.model.ts b/src/app/posts/post.model.ts new file mode 100644 index 0000000..21f1b84 --- /dev/null +++ b/src/app/posts/post.model.ts @@ -0,0 +1,6 @@ +export interface Post { + id: number; + userId: number; + title: string; + body: string; +} \ No newline at end of file diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/posts/posts-list/posts-list.component.html index 559c7fe..bec691d 100644 --- a/src/app/posts/posts-list/posts-list.component.html +++ b/src/app/posts/posts-list/posts-list.component.html @@ -1 +1,17 @@ -

posts-list works!

+
+
+
+

Posts

+
+
+
    +
  • +
    +
    id: {{ post.id }}
    +
    userId: {{ post.userId }}
    +
    title: {{ post.title }}
    +
    body: {{ post.body }}
    +
    +
  • +
+
\ No newline at end of file diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/posts/posts-list/posts-list.component.ts index 039179a..d61f52d 100644 --- a/src/app/posts/posts-list/posts-list.component.ts +++ b/src/app/posts/posts-list/posts-list.component.ts @@ -1,11 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { AsyncPipe, NgFor } from '@angular/common'; +import { Post } from '../post.model'; @Component({ selector: 'app-posts-list', - imports: [], + imports: [NgFor], templateUrl: './posts-list.component.html', styleUrl: './posts-list.component.scss' }) export class PostsListComponent { + @Input() posts: Post[] | null = []; } diff --git a/src/app/posts/posts-page/posts-page.component.html b/src/app/posts/posts-page/posts-page.component.html new file mode 100644 index 0000000..d49ac61 --- /dev/null +++ b/src/app/posts/posts-page/posts-page.component.html @@ -0,0 +1,13 @@ +
+ Error: {{ errorMessage$ | async }} +
+ +
+
+ +
+
+ +Loading... diff --git a/src/app/posts/post-page/post-page.component.scss b/src/app/posts/posts-page/posts-page.component.scss similarity index 100% rename from src/app/posts/post-page/post-page.component.scss rename to src/app/posts/posts-page/posts-page.component.scss diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/posts-page/posts-page.component.spec.ts similarity index 53% rename from src/app/posts/post-page/post-page.component.spec.ts rename to src/app/posts/posts-page/posts-page.component.spec.ts index 6bb5a12..16d023d 100644 --- a/src/app/posts/post-page/post-page.component.spec.ts +++ b/src/app/posts/posts-page/posts-page.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { PostPageComponent } from './post-page.component'; +import { PostsPageComponent } from './posts-page.component'; -describe('PostPageComponent', () => { - let component: PostPageComponent; - let fixture: ComponentFixture; +describe('PostsPageComponent', () => { + let component: PostsPageComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PostPageComponent] + imports: [PostsPageComponent] }) .compileComponents(); - fixture = TestBed.createComponent(PostPageComponent); + fixture = TestBed.createComponent(PostsPageComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/posts/posts-page/posts-page.component.ts b/src/app/posts/posts-page/posts-page.component.ts new file mode 100644 index 0000000..150bffa --- /dev/null +++ b/src/app/posts/posts-page/posts-page.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; +import { Store } from '@ngrx/store'; +import { PostsPageActions } from '../state/posts.actions'; +import { AsyncPipe, NgIf } from '@angular/common'; +import { PostsListComponent } from '../posts-list/posts-list.component'; + +@Component({ + selector: 'app-posts-page', + imports: [NgIf, AsyncPipe, PostsListComponent], + templateUrl: './posts-page.component.html', + styleUrl: './posts-page.component.scss' +}) +export class PostsPageComponent { + posts$ = this.store.select(selectPosts); + loading$ = this.store.select(selectPostsLoading); + errorMessage$ = this.store.select(selectPostsErrorMessage); + + constructor(private store: Store) { + this.store.subscribe((state) => console.log('state', state)); + } + + ngOnInit() { + this.store.dispatch(PostsPageActions.loadPosts()); + } + +} diff --git a/src/app/posts/posts.routes.ts b/src/app/posts/posts.routes.ts new file mode 100644 index 0000000..609f53c --- /dev/null +++ b/src/app/posts/posts.routes.ts @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; +import { PostsPageComponent } from './posts-page/posts-page.component'; + +export const routes: Routes = [ + { + path: '', + component: PostsPageComponent, + } +]; diff --git a/src/app/posts/posts.service.spec.ts b/src/app/posts/posts.service.spec.ts new file mode 100644 index 0000000..4fc9b5d --- /dev/null +++ b/src/app/posts/posts.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PostsService } from './posts.service'; + +describe('PostsService', () => { + let service: PostsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PostsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/posts/posts.service.ts b/src/app/posts/posts.service.ts new file mode 100644 index 0000000..c7a941d --- /dev/null +++ b/src/app/posts/posts.service.ts @@ -0,0 +1,33 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Post } from './post.model'; +import { environment } from 'src/environments/environment'; +import { catchError, throwError } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PostsService { + private url = environment.postsApiUrl + '/posts'; + + constructor(private http: HttpClient) { } + + getAll() { + return this.http + .get(this.url) + .pipe(catchError(this.handleError)); + } + + getById(id: number) { + return this.http + .get(`${this.url}/${id}`) + .pipe(catchError(this.handleError)); + } + + + private handleError({ status }: HttpErrorResponse) { + return throwError( + () => `${status}: ERROR - Unable to fetch posts` + ); + } +} diff --git a/src/app/posts/state/posts.actions.ts b/src/app/posts/state/posts.actions.ts new file mode 100644 index 0000000..59a5271 --- /dev/null +++ b/src/app/posts/state/posts.actions.ts @@ -0,0 +1,18 @@ +import { createActionGroup, emptyProps, props } from "@ngrx/store"; +import { Post } from "../post.model"; + +export const PostsPageActions = createActionGroup({ + source: 'Posts Page', + events: { + 'Load Posts': emptyProps(), + }, +}); + +export const PostsAPIActions = createActionGroup({ + source: 'Posts API', + events: { + 'Load Posts': emptyProps(), + 'Posts Loaded Success': props<{ posts: Post[] }>(), + 'Posts Loaded Fail': props<{ message: string }>(), + }, +}); diff --git a/src/app/posts/state/posts.effects.ts b/src/app/posts/state/posts.effects.ts new file mode 100644 index 0000000..6d64f17 --- /dev/null +++ b/src/app/posts/state/posts.effects.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { PostsService } from "../posts.service"; +import { PostsAPIActions, PostsPageActions } from "./posts.actions"; +import { catchError, concatMap, map, of } from "rxjs"; + +@Injectable() +export class PostsEffects { + + constructor( + private postsServiss: PostsService, + private actions$: Actions + ) { } + + loadPosts$ = createEffect(() => + this.actions$.pipe( + ofType(PostsPageActions.loadPosts), + concatMap(() => + this.postsServiss.getAll().pipe( + map((posts) => PostsAPIActions.postsLoadedSuccess({ posts })), + catchError((error) => + of(PostsAPIActions.postsLoadedFail({ message: error })) + ) + ) + ) + ) + ); +} \ No newline at end of file diff --git a/src/app/posts/state/posts.reducer.ts b/src/app/posts/state/posts.reducer.ts new file mode 100644 index 0000000..cb49dde --- /dev/null +++ b/src/app/posts/state/posts.reducer.ts @@ -0,0 +1,37 @@ +import { createFeature, createReducer, on } from "@ngrx/store"; +import { PostsAPIActions, PostsPageActions } from "./posts.actions"; +import { Post } from "../post.model"; + +export interface PostsState { + loading: boolean; + errorMessage: string; + posts: Post[]; +} +const initialState: PostsState = { + loading: false, + errorMessage: '', + posts: [], +}; + +export const postsFeature = createFeature({ + name: 'posts', + reducer: createReducer( + initialState, + on(PostsPageActions.loadPosts, (state) => ({ + ...state, + loading: true, + errorMessage: '', + posts: [], + })), + on(PostsAPIActions.postsLoadedSuccess, (state, { posts }) => ({ + ...state, + loading: false, + posts: posts, + })), + on(PostsAPIActions.postsLoadedFail, (state, { message }) => ({ + ...state, + errorMessage: message, + loading: false, + })), + ), +}); \ No newline at end of file diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/posts/state/posts.selectors.ts new file mode 100644 index 0000000..7e3d205 --- /dev/null +++ b/src/app/posts/state/posts.selectors.ts @@ -0,0 +1,19 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { PostsState } from "./posts.reducer"; + +export const selectPostsState = createFeatureSelector('posts'); + +export const selectPostsLoading = createSelector( + selectPostsState, + ({ loading }) => loading +); + +export const selectPostsErrorMessage = createSelector( + selectPostsState, + ({ errorMessage }) => errorMessage +); + +export const selectPosts = createSelector( + selectPostsState, + ({ posts }) => posts +); \ No newline at end of file diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..c354d28 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + postsApiUrl: 'http://jsonplaceholder.typicode.com/', +}; diff --git a/src/main.ts b/src/main.ts index 75a3f6a..f515291 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,8 +2,24 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { appRoutes } from './app/app-routes'; import { AppComponent } from './app/app.component'; +import { provideState, provideStore } from '@ngrx/store'; +import { provideStoreDevtools } from '@ngrx/store-devtools'; +import { provideEffects } from '@ngrx/effects'; +import { postsFeature } from './app/posts/state/posts.reducer'; +import { PostsEffects } from './app/posts/state/posts.effects'; +import { provideHttpClient } from '@angular/common/http'; bootstrapApplication(AppComponent, { - providers: [provideRouter(appRoutes)] + providers: [ + provideRouter(appRoutes), + provideHttpClient(), + provideStore({}), + provideStoreDevtools({ + maxAge: 25, + logOnly: false + }), + provideState(postsFeature), + provideEffects(PostsEffects), + ] }) .catch(err => console.error(err)); From 2b6289ca4694d94ae6a505fbb496f5d00b2ef42c Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 17:29:24 +0300 Subject: [PATCH 18/36] Update workflow to trigger on specific training branch --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5837aea..d49bbbf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - training/* + - training jobs: build: From 14b2683a6529f7eac0060b223d8ca2a763025fb7 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 17:30:26 +0300 Subject: [PATCH 19/36] Remove main branch trigger from build and deploy workflow --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3dc2173..d128848 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,6 @@ name: Build and Deploy Script on: push: branches: - - main - training jobs: From 375ab11f0fb1b09589cb2becf9d6836f39031ca5 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 17:54:27 +0300 Subject: [PATCH 20/36] Update unit tests for AppComponent and PostsPageComponent; add HTTP tests for PostsService --- src/app/app.component.spec.ts | 11 +----- .../posts-page/posts-page.component.spec.ts | 9 ++++- src/app/posts/posts.service.spec.ts | 38 ++++++++++++++++++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a59d261..7a2cd6c 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -13,16 +13,9 @@ describe('AppComponent', () => { expect(app).toBeTruthy(); }); - it(`should have as title 'angular-template'`, () => { + it(`should have as title 'Training Angular Project'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('angular-template'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('.content span')?.textContent).toContain('angular-template app is running!'); + expect(app.title).toEqual('Training Angular Project'); }); }); diff --git a/src/app/posts/posts-page/posts-page.component.spec.ts b/src/app/posts/posts-page/posts-page.component.spec.ts index 16d023d..ca7f1e8 100644 --- a/src/app/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/posts/posts-page/posts-page.component.spec.ts @@ -1,14 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { PostsPageComponent } from './posts-page.component'; +import { Store } from '@ngrx/store'; describe('PostsPageComponent', () => { let component: PostsPageComponent; let fixture: ComponentFixture; + let storeSpy: jasmine.SpyObj>; beforeEach(async () => { + storeSpy = jasmine.createSpyObj('Store', ['select', 'dispatch', 'subscribe']); + await TestBed.configureTestingModule({ - imports: [PostsPageComponent] + imports: [PostsPageComponent], + providers: [{ provide: Store, useValue: storeSpy }] }) .compileComponents(); @@ -20,4 +24,5 @@ describe('PostsPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + }); diff --git a/src/app/posts/posts.service.spec.ts b/src/app/posts/posts.service.spec.ts index 4fc9b5d..0bd0188 100644 --- a/src/app/posts/posts.service.spec.ts +++ b/src/app/posts/posts.service.spec.ts @@ -1,16 +1,52 @@ import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { PostsService } from './posts.service'; +import { environment } from 'src/environments/environment'; +import { Post } from './post.model'; describe('PostsService', () => { let service: PostsService; + let httpMock: HttpTestingController; + const apiUrl = environment.postsApiUrl + '/posts'; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [PostsService] + }); service = TestBed.inject(PostsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should fetch all posts', () => { + const dummyPosts: Post[] = [ + { id: 1, userId: 1, title: 'Test 1', body: 'Body 1' }, + { id: 2, userId: 1, title: 'Test 2', body: 'Body 2' } + ]; + service.getAll().subscribe(posts => { + expect(posts).toEqual(dummyPosts); + }); + const req = httpMock.expectOne(apiUrl); + expect(req.request.method).toBe('GET'); + req.flush(dummyPosts); + }); + + it('should fetch post by id', () => { + const dummyPost: Post = { id: 1, userId: 1, title: 'Test', body: 'Body' }; + service.getById(1).subscribe(post => { + expect(post).toEqual(dummyPost); + }); + const req = httpMock.expectOne(`${apiUrl}/1`); + expect(req.request.method).toBe('GET'); + req.flush(dummyPost); + }); }); From 6a90673f483de085c9ed112332ebc928b8e5ad6a Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 18:00:06 +0300 Subject: [PATCH 21/36] Update postsApiUrl to use HTTPS for improved security --- src/environments/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/environments/environment.ts b/src/environments/environment.ts index c354d28..df116be 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - postsApiUrl: 'http://jsonplaceholder.typicode.com/', + postsApiUrl: 'https://jsonplaceholder.typicode.com/', }; From 7211072a45d5d421005bd3df0207e5f24f091bf2 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Mon, 26 May 2025 23:42:52 +0300 Subject: [PATCH 22/36] Add Angular Material components and update styles for improved UI - Integrated Angular Material components including MatToolbar, MatCard, and MatButton. - Updated app.component.html to include a navigation toolbar. - Refactored posts-list.component.html to use MatCard for displaying posts. - Enhanced styles in app.component.scss and posts-list.component.scss for better layout. - Updated package.json and package-lock.json to include Angular Material and CDK dependencies. --- angular.json | 2 + package-lock.json | 82 +++++++++++++++++-- package.json | 5 +- src/app/app.component.html | 11 ++- src/app/app.component.scss | 11 +++ src/app/app.component.ts | 10 ++- .../posts-list/posts-list.component.html | 32 ++++---- .../posts-list/posts-list.component.scss | 10 +++ .../posts/posts-list/posts-list.component.ts | 8 +- src/app/posts/state/posts.selectors.ts | 7 +- src/index.html | 4 +- src/main.ts | 2 + src/styles.scss | 3 + 13 files changed, 157 insertions(+), 30 deletions(-) diff --git a/angular.json b/angular.json index b44e321..40c6676 100644 --- a/angular.json +++ b/angular.json @@ -31,6 +31,7 @@ "src/assets" ], "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], "scripts": [], @@ -93,6 +94,7 @@ "src/assets" ], "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], "scripts": [] diff --git a/package-lock.json b/package-lock.json index 00de835..fc2a062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,18 @@ "version": "0.0.0", "dependencies": { "@angular/animations": "^19.2.10", + "@angular/cdk": "^19.2.17", "@angular/common": "^19.2.10", "@angular/compiler": "^19.2.10", "@angular/core": "^19.2.10", "@angular/forms": "^19.2.10", + "@angular/material": "^19.2.17", "@angular/platform-browser": "^19.2.10", "@angular/platform-browser-dynamic": "^19.2.10", "@angular/router": "^19.2.10", "@ngrx/effects": "^19.2.0", "@ngrx/entity": "^19.2.0", + "@ngrx/router-store": "^19.2.0", "@ngrx/store": "^19.2.0", "@ngrx/store-devtools": "^19.2.0", "rxjs": "~7.8.0", @@ -1253,6 +1256,21 @@ } } }, + "node_modules/@angular/cdk": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.17.tgz", + "integrity": "sha512-3jG33S+5+kqymCRwQlcSEWlY5rYwkKxe0onln+NXxT0/kteR02vWvv1+Li4/QqSr5JvsGHEhAFsZaR9QtOzbdA==", + "license": "MIT", + "dependencies": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "19.2.12", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz", @@ -1514,6 +1532,23 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.17.tgz", + "integrity": "sha512-IyA+KP+uUj3r9loqGJrj7qAiEBckj7EVIdV0jlYwqWIUyKWeJ3R88GmLPMH2BgtBU3R/WkS2blXDI0yvRhKfww==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "19.2.17", + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "@angular/forms": "^19.0.0 || ^20.0.0", + "@angular/platform-browser": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "19.2.10", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.10.tgz", @@ -4989,6 +5024,22 @@ "rxjs": "^6.5.3 || ^7.5.0" } }, + "node_modules/@ngrx/router-store": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-19.2.0.tgz", + "integrity": "sha512-emR6Y+NIcFxFt1QsyDdMIVhkuGEzawGZM5yOo8A6kUZljzf88S/7tHXQRKLz1Vy2fpDRZDO6r/0eagW0JDMfLA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@angular/router": "^19.0.0", + "@ngrx/store": "19.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@ngrx/store": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.0.tgz", @@ -12820,7 +12871,6 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -12861,7 +12911,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -16762,6 +16811,15 @@ } } }, + "@angular/cdk": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.17.tgz", + "integrity": "sha512-3jG33S+5+kqymCRwQlcSEWlY5rYwkKxe0onln+NXxT0/kteR02vWvv1+Li4/QqSr5JvsGHEhAFsZaR9QtOzbdA==", + "requires": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + } + }, "@angular/cli": { "version": "19.2.12", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz", @@ -16908,6 +16966,14 @@ "tslib": "^2.3.0" } }, + "@angular/material": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.17.tgz", + "integrity": "sha512-IyA+KP+uUj3r9loqGJrj7qAiEBckj7EVIdV0jlYwqWIUyKWeJ3R88GmLPMH2BgtBU3R/WkS2blXDI0yvRhKfww==", + "requires": { + "tslib": "^2.3.0" + } + }, "@angular/platform-browser": { "version": "19.2.10", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.10.tgz", @@ -18919,6 +18985,14 @@ "tslib": "^2.0.0" } }, + "@ngrx/router-store": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-19.2.0.tgz", + "integrity": "sha512-emR6Y+NIcFxFt1QsyDdMIVhkuGEzawGZM5yOo8A6kUZljzf88S/7tHXQRKLz1Vy2fpDRZDO6r/0eagW0JDMfLA==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngrx/store": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.0.tgz", @@ -24234,7 +24308,6 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, "requires": { "entities": "^6.0.0" }, @@ -24242,8 +24315,7 @@ "entities": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", - "dev": true + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==" } } }, diff --git a/package.json b/package.json index a7c06bf..10576f0 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,18 @@ "private": true, "dependencies": { "@angular/animations": "^19.2.10", + "@angular/cdk": "^19.2.17", "@angular/common": "^19.2.10", "@angular/compiler": "^19.2.10", "@angular/core": "^19.2.10", "@angular/forms": "^19.2.10", + "@angular/material": "^19.2.17", "@angular/platform-browser": "^19.2.10", "@angular/platform-browser-dynamic": "^19.2.10", "@angular/router": "^19.2.10", "@ngrx/effects": "^19.2.0", "@ngrx/entity": "^19.2.0", + "@ngrx/router-store": "^19.2.0", "@ngrx/store": "^19.2.0", "@ngrx/store-devtools": "^19.2.0", "rxjs": "~7.8.0", @@ -50,4 +53,4 @@ "prettier": "^3.3.3", "typescript": "~5.8.3" } -} +} \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index 79efb01..d280e94 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,11 @@ -
+ + +
- +
+ +
\ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index e69de29..1ca3630 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -0,0 +1,11 @@ +.posts-container { + width: 60%; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; +} + +mat-toolbar { + margin-bottom: 24px; +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2c12239..8cf82a6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,10 +1,18 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [ + RouterOutlet, + MatCardModule, + MatButtonModule, + MatToolbarModule + ], templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/posts/posts-list/posts-list.component.html index bec691d..eb0069a 100644 --- a/src/app/posts/posts-list/posts-list.component.html +++ b/src/app/posts/posts-list/posts-list.component.html @@ -1,17 +1,15 @@ -
-
-
-

Posts

-
-
-
    -
  • -
    -
    id: {{ post.id }}
    -
    userId: {{ post.userId }}
    -
    title: {{ post.title }}
    -
    body: {{ post.body }}
    -
    -
  • -
-
\ No newline at end of file +
+
+ + {{ post.title }} + +

{{ post.body }}

+
+ + + +
+
+
\ No newline at end of file diff --git a/src/app/posts/posts-list/posts-list.component.scss b/src/app/posts/posts-list/posts-list.component.scss index e69de29..8664a48 100644 --- a/src/app/posts/posts-list/posts-list.component.scss +++ b/src/app/posts/posts-list/posts-list.component.scss @@ -0,0 +1,10 @@ +.posts-list { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.post-item { + margin-bottom: 16px; +} \ No newline at end of file diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/posts/posts-list/posts-list.component.ts index d61f52d..945e124 100644 --- a/src/app/posts/posts-list/posts-list.component.ts +++ b/src/app/posts/posts-list/posts-list.component.ts @@ -1,14 +1,18 @@ import { Component, Input } from '@angular/core'; -import { AsyncPipe, NgFor } from '@angular/common'; import { Post } from '../post.model'; +import { NgFor } from '@angular/common'; +import { MatCard, MatCardActions, MatCardContent, MatCardTitle } from '@angular/material/card'; @Component({ selector: 'app-posts-list', - imports: [NgFor], + imports: [NgFor, MatCard, MatCardTitle, MatCardActions, MatCardContent], templateUrl: './posts-list.component.html', styleUrl: './posts-list.component.scss' }) export class PostsListComponent { @Input() posts: Post[] | null = []; + showComments(post: Post) { + //todo + } } diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/posts/state/posts.selectors.ts index 7e3d205..2a5aeee 100644 --- a/src/app/posts/state/posts.selectors.ts +++ b/src/app/posts/state/posts.selectors.ts @@ -16,4 +16,9 @@ export const selectPostsErrorMessage = createSelector( export const selectPosts = createSelector( selectPostsState, ({ posts }) => posts -); \ No newline at end of file +); + +export const selectPostById = (id: number) => createSelector( + selectPosts, + (posts) => posts.find(post => post.id === id) +); diff --git a/src/index.html b/src/index.html index 1d918ee..704ff0b 100644 --- a/src/index.html +++ b/src/index.html @@ -6,8 +6,10 @@ + + - + diff --git a/src/main.ts b/src/main.ts index f515291..cfd2877 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import { provideEffects } from '@ngrx/effects'; import { postsFeature } from './app/posts/state/posts.reducer'; import { PostsEffects } from './app/posts/state/posts.effects'; import { provideHttpClient } from '@angular/common/http'; +import { provideRouterStore } from '@ngrx/router-store'; bootstrapApplication(AppComponent, { providers: [ @@ -18,6 +19,7 @@ bootstrapApplication(AppComponent, { maxAge: 25, logOnly: false }), + provideRouterStore(), provideState(postsFeature), provideEffects(PostsEffects), ] diff --git a/src/styles.scss b/src/styles.scss index 90d4ee0..7e7239a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1 +1,4 @@ /* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } From fd1d8523faefb5e77227532f6007afc37ab05358 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Tue, 27 May 2025 15:55:22 +0300 Subject: [PATCH 23/36] Refactor posts and comments modules; implement routing and state management for comments --- src/app/app-routes.ts | 12 ++++-- src/app/app.component.html | 4 +- src/app/app.component.ts | 4 +- src/app/comments/comment.model.ts | 7 ++++ src/app/comments/comment.service.ts | 28 +++++++++++++ src/app/comments/state/comments.actions.ts | 19 +++++++++ src/app/comments/state/comments.effects.ts | 28 +++++++++++++ src/app/comments/state/comments.reducer.ts | 37 +++++++++++++++++ src/app/comments/state/comments.selectors.ts | 20 +++++++++ .../post-detail/post-detail.component.html | 37 +++++++++++++++++ .../post-detail/post-detail.component.scss | 32 +++++++++++++++ .../post-detail/post-detail.component.spec.ts | 28 +++++++++++++ .../post-detail/post-detail.component.ts | 35 ++++++++++++++++ .../posts/post-page/post-page.component.html | 17 ++++++++ .../posts/post-page/post-page.component.scss | 0 .../post-page/post-page.component.spec.ts | 41 +++++++++++++++++++ .../posts/post-page/post-page.component.ts | 30 ++++++++++++++ ...s.service.spec.ts => post.service.spec.ts} | 10 ++--- .../{posts.service.ts => post.service.ts} | 11 +++-- .../posts-list/posts-list.component.html | 4 +- .../posts/posts-list/posts-list.component.ts | 7 +++- .../posts/posts-page/posts-page.component.ts | 8 +--- src/app/posts/posts.routes.ts | 14 +++++-- src/app/posts/state/posts.effects.ts | 8 +++- src/app/posts/state/posts.selectors.ts | 8 +++- src/environments/environment.ts | 2 +- src/main.ts | 15 ++++--- 27 files changed, 424 insertions(+), 42 deletions(-) create mode 100644 src/app/comments/comment.model.ts create mode 100644 src/app/comments/comment.service.ts create mode 100644 src/app/comments/state/comments.actions.ts create mode 100644 src/app/comments/state/comments.effects.ts create mode 100644 src/app/comments/state/comments.reducer.ts create mode 100644 src/app/comments/state/comments.selectors.ts create mode 100644 src/app/posts/post-detail/post-detail.component.html create mode 100644 src/app/posts/post-detail/post-detail.component.scss create mode 100644 src/app/posts/post-detail/post-detail.component.spec.ts create mode 100644 src/app/posts/post-detail/post-detail.component.ts create mode 100644 src/app/posts/post-page/post-page.component.html create mode 100644 src/app/posts/post-page/post-page.component.scss create mode 100644 src/app/posts/post-page/post-page.component.spec.ts create mode 100644 src/app/posts/post-page/post-page.component.ts rename src/app/posts/{posts.service.spec.ts => post.service.spec.ts} (86%) rename src/app/posts/{posts.service.ts => post.service.ts} (75%) diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 69586cb..7416278 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,13 +1,19 @@ import { Routes } from '@angular/router'; -import { PostsService } from './posts/posts.service'; +import { PostService } from './posts/post.service'; +import { GbookPageComponent } from './gbook/gbook-page/gbook-page.component'; -export const appRoutes: Routes = [ +export const routes: Routes = [ { path: '', loadChildren: () => import('./posts/posts.routes').then((mod) => mod.routes), providers: [ - PostsService + PostService ], }, + { + path: 'guestbook', + component: GbookPageComponent + , + }, ]; diff --git a/src/app/app.component.html b/src/app/app.component.html index d280e94..fbfafdf 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,7 +1,7 @@
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8cf82a6..d8ac2f8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -9,6 +9,8 @@ import { MatToolbarModule } from '@angular/material/toolbar'; selector: 'app-root', imports: [ RouterOutlet, + RouterLinkActive, + RouterLink, MatCardModule, MatButtonModule, MatToolbarModule diff --git a/src/app/comments/comment.model.ts b/src/app/comments/comment.model.ts new file mode 100644 index 0000000..5b55a25 --- /dev/null +++ b/src/app/comments/comment.model.ts @@ -0,0 +1,7 @@ +export interface Comment { + id: number; + postId: number; + name: string; + email: string; + body: string; +} \ No newline at end of file diff --git a/src/app/comments/comment.service.ts b/src/app/comments/comment.service.ts new file mode 100644 index 0000000..ce43d8a --- /dev/null +++ b/src/app/comments/comment.service.ts @@ -0,0 +1,28 @@ +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { catchError, Observable, throwError } from "rxjs"; +import { environment } from "src/environments/environment"; +import { Post } from "../posts/post.model"; +import { Comment } from "src/app/comments/comment.model"; + +@Injectable({ + providedIn: 'root' +}) +export class CommentService { + private url = environment.apiUrl + '/comments'; + + constructor(private http: HttpClient) { } + + getByPost(post: Post): Observable { + return this.http + .get(`${this.url}/?postId=${post.id}`) + .pipe(catchError(this.handleError)); + } + + + private handleError({ status }: HttpErrorResponse) { + return throwError( + () => `${status}: ERROR - Unable to fetch posts` + ); + } +} \ No newline at end of file diff --git a/src/app/comments/state/comments.actions.ts b/src/app/comments/state/comments.actions.ts new file mode 100644 index 0000000..3e1d203 --- /dev/null +++ b/src/app/comments/state/comments.actions.ts @@ -0,0 +1,19 @@ +import { createActionGroup, emptyProps, props } from "@ngrx/store"; +import { Post } from "src/app/posts/post.model"; +import { Comment } from "src/app/comments/comment.model"; + +export const CommentsPageActions = createActionGroup({ + source: 'Comments Page', + events: { + 'Load Comments': props<{post: Post}>(), + }, +}); + +export const CommentsAPIActions = createActionGroup({ + source: 'Comments API', + events: { + 'Load Comments': props<{post: Post}>(), + 'Comments Loaded Success': props<{ comments: Comment[] }>(), + 'Comments Loaded Fail': props<{ message: string }>(), + }, +}); diff --git a/src/app/comments/state/comments.effects.ts b/src/app/comments/state/comments.effects.ts new file mode 100644 index 0000000..47ed9a4 --- /dev/null +++ b/src/app/comments/state/comments.effects.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { catchError, concatMap, map, of } from "rxjs"; +import { CommentService } from "../comment.service"; +import { CommentsAPIActions, CommentsPageActions } from "./comments.actions"; + +@Injectable() +export class CommentsEffects { + + constructor( + private commentsService: CommentService, + private actions$: Actions + ) { } + + loadComments$ = createEffect(() => + this.actions$.pipe( + ofType(CommentsPageActions.loadComments), + concatMap(({ post }) => + this.commentsService.getByPost(post).pipe( + map((comments) => CommentsAPIActions.commentsLoadedSuccess({ comments })), + catchError((error) => + of(CommentsAPIActions.commentsLoadedFail({ message: error })) + ) + ) + ) + ) + ); +} \ No newline at end of file diff --git a/src/app/comments/state/comments.reducer.ts b/src/app/comments/state/comments.reducer.ts new file mode 100644 index 0000000..7d712d4 --- /dev/null +++ b/src/app/comments/state/comments.reducer.ts @@ -0,0 +1,37 @@ +import { createFeature, createReducer, on } from "@ngrx/store"; +import { CommentsAPIActions, CommentsPageActions } from "./comments.actions"; +import { Comment } from "src/app/comments/comment.model"; + +export interface CommentsState { + loading: boolean; + errorMessage: string; + comments: Comment[]; +} +const initialState: CommentsState = { + loading: false, + errorMessage: '', + comments: [], +}; + +export const commentsFeature = createFeature({ + name: 'comments', + reducer: createReducer( + initialState, + on(CommentsPageActions.loadComments, (state) => ({ + ...state, + loading: true, + errorMessage: '', + comments: [], + })), + on(CommentsAPIActions.commentsLoadedSuccess, (state, { comments }) => ({ + ...state, + loading: false, + comments: comments, + })), + on(CommentsAPIActions.commentsLoadedFail, (state, { message }) => ({ + ...state, + errorMessage: message, + loading: false, + })), + ), +}); \ No newline at end of file diff --git a/src/app/comments/state/comments.selectors.ts b/src/app/comments/state/comments.selectors.ts new file mode 100644 index 0000000..2732beb --- /dev/null +++ b/src/app/comments/state/comments.selectors.ts @@ -0,0 +1,20 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { CommentsState } from "./comments.reducer"; + + +export const selectCommentsState = createFeatureSelector('comments'); + +export const selectCommentsLoading = createSelector( + selectCommentsState, + ({ loading }) => loading +); + +export const selectCommentsErrorMessage = createSelector( + selectCommentsState, + ({ errorMessage }) => errorMessage +); + +export const selectComments = createSelector( + selectCommentsState, + ({ comments }) => comments +); diff --git a/src/app/posts/post-detail/post-detail.component.html b/src/app/posts/post-detail/post-detail.component.html new file mode 100644 index 0000000..c502679 --- /dev/null +++ b/src/app/posts/post-detail/post-detail.component.html @@ -0,0 +1,37 @@ +
+ +
+ +
+ + {{ post.title }} + +

{{ post.body }}

+
+
+
+ +
+ + Comments + + + +
+ {{ comment.name }} +

{{ comment.body }}

+
+
+
+
+
+
+ + +

No comments yet.

+
+ +Loading comments... \ No newline at end of file diff --git a/src/app/posts/post-detail/post-detail.component.scss b/src/app/posts/post-detail/post-detail.component.scss new file mode 100644 index 0000000..484e003 --- /dev/null +++ b/src/app/posts/post-detail/post-detail.component.scss @@ -0,0 +1,32 @@ +.back-container { + padding: 24px 0; + display: flex; + + button[mat-fab] { + display: flex; + align-items: center; + background: none; + border: none; + } +} + +.post-card-container { + margin-bottom: 32px; +} + +.comments-card-container { + margin-top: 12px; +} + +.comment-content { + word-break: break-word; + white-space: pre-line; + width: 60%; + background: none; + +} + +.comment-card { + background: #fff; + margin-bottom: 12px; +} \ No newline at end of file diff --git a/src/app/posts/post-detail/post-detail.component.spec.ts b/src/app/posts/post-detail/post-detail.component.spec.ts new file mode 100644 index 0000000..a8b41e7 --- /dev/null +++ b/src/app/posts/post-detail/post-detail.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PostsDetailComponent } from './post-detail.component'; +import { provideMockStore } from '@ngrx/store/testing'; +import { Router } from '@angular/router'; + +describe('PostsDetailComponent', () => { + let component: PostsDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostsDetailComponent], + providers: [ + provideMockStore(), + { provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PostsDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/posts/post-detail/post-detail.component.ts b/src/app/posts/post-detail/post-detail.component.ts new file mode 100644 index 0000000..929b196 --- /dev/null +++ b/src/app/posts/post-detail/post-detail.component.ts @@ -0,0 +1,35 @@ +import { Component, Input } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { AsyncPipe, NgFor, NgIf } from '@angular/common'; +import { Post } from '../post.model'; +import { Store } from '@ngrx/store'; +import { Comment } from 'src/app/comments/comment.model'; +import { MatIconModule } from '@angular/material/icon'; +import { Router } from '@angular/router'; +import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors'; + +@Component({ + selector: 'app-posts-detail', + standalone: true, + imports: [ + MatCardModule, + NgIf, + NgFor, + MatIconModule, + AsyncPipe + ], + templateUrl: './post-detail.component.html', + styleUrl: './post-detail.component.scss' +}) +export class PostsDetailComponent { + @Input() post: Post | undefined; + @Input() comments: Comment[] | null = [] ; + loadingComments$ = this.store.select(selectCommentsLoading); + + + constructor(private store: Store, private router: Router) { } + + goBack() { + this.router.navigate(['/']); + } +} diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/posts/post-page/post-page.component.html new file mode 100644 index 0000000..52f4cce --- /dev/null +++ b/src/app/posts/post-page/post-page.component.html @@ -0,0 +1,17 @@ +
+ Error: {{ errorMessage$ | async }} +
+ +
+ + + + +
+ +Loading... + +
This post does not exist.
+
\ No newline at end of file diff --git a/src/app/posts/post-page/post-page.component.scss b/src/app/posts/post-page/post-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/post-page/post-page.component.spec.ts new file mode 100644 index 0000000..468b8cb --- /dev/null +++ b/src/app/posts/post-page/post-page.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideMockStore } from '@ngrx/store/testing'; +import { PostPageComponent } from './post-page.component'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { Store } from '@ngrx/store'; + +describe('PostPageComponent', () => { + let component: PostPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostPageComponent], + providers: [ + provideMockStore(), + { + provide: Store, + useValue: { + select: jasmine.createSpy('select'), + selectSignal: jasmine.createSpy('selectSignal').and.returnValue(() => ({ + id: 1, + userId: 1, + title: 'Test', + body: 'Test body' + })), + dispatch: jasmine.createSpy('dispatch') + } + } + ] + }).compileComponents(); + fixture = TestBed.createComponent(PostPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts new file mode 100644 index 0000000..eca8ef1 --- /dev/null +++ b/src/app/posts/post-page/post-page.component.ts @@ -0,0 +1,30 @@ +import { Component, effect } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { selectPostsErrorMessage, selectPostsLoading, selectPostById } from '../state/posts.selectors'; +import { PostsPageActions } from '../state/posts.actions'; +import { AsyncPipe, NgIf } from '@angular/common'; +import { PostsDetailComponent } from '../post-detail/post-detail.component'; +import { selectComments, selectCommentsLoading } from 'src/app/comments/state/comments.selectors'; +import { CommentsPageActions } from 'src/app/comments/state/comments.actions'; + +@Component({ + selector: 'app-post-page', + imports: [NgIf, AsyncPipe, PostsDetailComponent], + templateUrl: './post-page.component.html', + styleUrl: './post-page.component.scss' +}) +export class PostPageComponent { + loading$ = this.store.select(selectPostsLoading); + errorMessage$ = this.store.select(selectPostsErrorMessage); + post = this.store.selectSignal(selectPostById); + comments$ = this.store.select(selectComments); + + constructor(private store: Store) { + effect(() => { + const post = this.post(); + if (post) { + this.store.dispatch(CommentsPageActions.loadComments({ post })); + } + }); + } +} diff --git a/src/app/posts/posts.service.spec.ts b/src/app/posts/post.service.spec.ts similarity index 86% rename from src/app/posts/posts.service.spec.ts rename to src/app/posts/post.service.spec.ts index 0bd0188..0274208 100644 --- a/src/app/posts/posts.service.spec.ts +++ b/src/app/posts/post.service.spec.ts @@ -1,21 +1,21 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { PostsService } from './posts.service'; +import { PostService } from './post.service'; import { environment } from 'src/environments/environment'; import { Post } from './post.model'; describe('PostsService', () => { - let service: PostsService; + let service: PostService; let httpMock: HttpTestingController; - const apiUrl = environment.postsApiUrl + '/posts'; + const apiUrl = environment.apiUrl + '/posts'; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [PostsService] + providers: [PostService] }); - service = TestBed.inject(PostsService); + service = TestBed.inject(PostService); httpMock = TestBed.inject(HttpTestingController); }); diff --git a/src/app/posts/posts.service.ts b/src/app/posts/post.service.ts similarity index 75% rename from src/app/posts/posts.service.ts rename to src/app/posts/post.service.ts index c7a941d..796b55b 100644 --- a/src/app/posts/posts.service.ts +++ b/src/app/posts/post.service.ts @@ -2,29 +2,28 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Post } from './post.model'; import { environment } from 'src/environments/environment'; -import { catchError, throwError } from 'rxjs'; +import { catchError, Observable, throwError } from 'rxjs'; @Injectable({ providedIn: 'root' }) -export class PostsService { - private url = environment.postsApiUrl + '/posts'; +export class PostService { + private url = environment.apiUrl + '/posts'; constructor(private http: HttpClient) { } - getAll() { + getAll(): Observable { return this.http .get(this.url) .pipe(catchError(this.handleError)); } - getById(id: number) { + getById(id: number): Observable { return this.http .get(`${this.url}/${id}`) .pipe(catchError(this.handleError)); } - private handleError({ status }: HttpErrorResponse) { return throwError( () => `${status}: ERROR - Unable to fetch posts` diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/posts/posts-list/posts-list.component.html index eb0069a..5d8789a 100644 --- a/src/app/posts/posts-list/posts-list.component.html +++ b/src/app/posts/posts-list/posts-list.component.html @@ -6,8 +6,8 @@

{{ post.body }}

- diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/posts/posts-list/posts-list.component.ts index 945e124..52a955f 100644 --- a/src/app/posts/posts-list/posts-list.component.ts +++ b/src/app/posts/posts-list/posts-list.component.ts @@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core'; import { Post } from '../post.model'; import { NgFor } from '@angular/common'; import { MatCard, MatCardActions, MatCardContent, MatCardTitle } from '@angular/material/card'; +import { Router } from '@angular/router'; @Component({ selector: 'app-posts-list', @@ -12,7 +13,9 @@ import { MatCard, MatCardActions, MatCardContent, MatCardTitle } from '@angular/ export class PostsListComponent { @Input() posts: Post[] | null = []; - showComments(post: Post) { - //todo + constructor(private router: Router) {} + + showPost(post: Post) { + this.router.navigate(['./post', post.id]); } } diff --git a/src/app/posts/posts-page/posts-page.component.ts b/src/app/posts/posts-page/posts-page.component.ts index 150bffa..19a2266 100644 --- a/src/app/posts/posts-page/posts-page.component.ts +++ b/src/app/posts/posts-page/posts-page.component.ts @@ -16,12 +16,6 @@ export class PostsPageComponent { loading$ = this.store.select(selectPostsLoading); errorMessage$ = this.store.select(selectPostsErrorMessage); - constructor(private store: Store) { - this.store.subscribe((state) => console.log('state', state)); - } - - ngOnInit() { - this.store.dispatch(PostsPageActions.loadPosts()); - } + constructor(private store: Store) { } } diff --git a/src/app/posts/posts.routes.ts b/src/app/posts/posts.routes.ts index 609f53c..7848f51 100644 --- a/src/app/posts/posts.routes.ts +++ b/src/app/posts/posts.routes.ts @@ -1,9 +1,15 @@ import { Routes } from '@angular/router'; import { PostsPageComponent } from './posts-page/posts-page.component'; +import { PostsDetailComponent } from './post-detail/post-detail.component'; +import { PostPageComponent } from './post-page/post-page.component'; export const routes: Routes = [ - { - path: '', - component: PostsPageComponent, - } + { + path: 'post/:id', + component: PostPageComponent, + }, + { + path: '', + component: PostsPageComponent, + } ]; diff --git a/src/app/posts/state/posts.effects.ts b/src/app/posts/state/posts.effects.ts index 6d64f17..26093bb 100644 --- a/src/app/posts/state/posts.effects.ts +++ b/src/app/posts/state/posts.effects.ts @@ -1,14 +1,18 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { PostsService } from "../posts.service"; +import { PostService } from "../post.service"; import { PostsAPIActions, PostsPageActions } from "./posts.actions"; import { catchError, concatMap, map, of } from "rxjs"; @Injectable() export class PostsEffects { + ngrxOnInitEffects() { + return PostsPageActions.loadPosts(); + } + constructor( - private postsServiss: PostsService, + private postsServiss: PostService, private actions$: Actions ) { } diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/posts/state/posts.selectors.ts index 2a5aeee..3918c46 100644 --- a/src/app/posts/state/posts.selectors.ts +++ b/src/app/posts/state/posts.selectors.ts @@ -1,5 +1,6 @@ import { createFeatureSelector, createSelector } from "@ngrx/store"; import { PostsState } from "./posts.reducer"; +import { getRouterSelectors } from "@ngrx/router-store"; export const selectPostsState = createFeatureSelector('posts'); @@ -18,7 +19,10 @@ export const selectPosts = createSelector( ({ posts }) => posts ); -export const selectPostById = (id: number) => createSelector( +export const { selectRouteParams } = getRouterSelectors(); + +export const selectPostById = createSelector( selectPosts, - (posts) => posts.find(post => post.id === id) + selectRouteParams, + (posts, { id }) => posts.find(post => post.id === parseInt(id)) ); diff --git a/src/environments/environment.ts b/src/environments/environment.ts index df116be..a6580e6 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - postsApiUrl: 'https://jsonplaceholder.typicode.com/', + apiUrl: 'https://jsonplaceholder.typicode.com/', }; diff --git a/src/main.ts b/src/main.ts index cfd2877..c6318d6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,27 +1,32 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; -import { appRoutes } from './app/app-routes'; +import { routes } from './app/app-routes'; import { AppComponent } from './app/app.component'; import { provideState, provideStore } from '@ngrx/store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; import { provideEffects } from '@ngrx/effects'; import { postsFeature } from './app/posts/state/posts.reducer'; +import { commentsFeature } from './app/comments/state/comments.reducer'; import { PostsEffects } from './app/posts/state/posts.effects'; +import { CommentsEffects } from './app/comments/state/comments.effects'; import { provideHttpClient } from '@angular/common/http'; -import { provideRouterStore } from '@ngrx/router-store'; +import { provideRouterStore, routerReducer } from '@ngrx/router-store'; bootstrapApplication(AppComponent, { providers: [ - provideRouter(appRoutes), provideHttpClient(), - provideStore({}), + provideStore({ + router: routerReducer, + }), provideStoreDevtools({ maxAge: 25, logOnly: false }), provideRouterStore(), + provideRouter(routes), provideState(postsFeature), - provideEffects(PostsEffects), + provideState(commentsFeature), + provideEffects(PostsEffects, CommentsEffects), ] }) .catch(err => console.error(err)); From c344e041476b97e7499dec32b0b6be07d7067534 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Tue, 27 May 2025 23:53:30 +0300 Subject: [PATCH 24/36] Update styles and improve UI components; switch to indigo-pink theme, enhance post and comment layouts --- angular.json | 2 +- src/app/app.component.scss | 4 +-- .../post-detail/post-detail.component.html | 27 ++++++++++++++----- .../post-detail/post-detail.component.scss | 18 +++---------- .../post-detail/post-detail.component.ts | 4 ++- .../posts/post-page/post-page.component.html | 2 +- .../posts/post-page/post-page.component.scss | 3 +++ .../posts-list/posts-list.component.html | 12 +++++---- .../posts-list/posts-list.component.scss | 11 +++++--- .../posts/posts-list/posts-list.component.ts | 11 +++++--- src/styles.scss | 1 + 11 files changed, 57 insertions(+), 38 deletions(-) diff --git a/angular.json b/angular.json index 40c6676..1b91161 100644 --- a/angular.json +++ b/angular.json @@ -31,7 +31,7 @@ "src/assets" ], "styles": [ - "@angular/material/prebuilt-themes/azure-blue.css", + "@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" ], "scripts": [], diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 1ca3630..d1385aa 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,6 +1,6 @@ .posts-container { - width: 60%; - margin: 0 auto; + width: 80%; + margin: auto; display: flex; flex-direction: column; align-items: center; diff --git a/src/app/posts/post-detail/post-detail.component.html b/src/app/posts/post-detail/post-detail.component.html index c502679..57e1fab 100644 --- a/src/app/posts/post-detail/post-detail.component.html +++ b/src/app/posts/post-detail/post-detail.component.html @@ -1,5 +1,5 @@
- @@ -7,7 +7,9 @@
- {{ post.title }} + + {{ post.title }} +

{{ post.body }}

@@ -16,14 +18,25 @@
- Comments + + Comments + -
- {{ comment.name }} -

{{ comment.body }}

-
+
+ + + {{ comment.name }} + + + {{ comment.email }} + + + +

{{ comment.body }}

+
+
diff --git a/src/app/posts/post-detail/post-detail.component.scss b/src/app/posts/post-detail/post-detail.component.scss index 484e003..179faeb 100644 --- a/src/app/posts/post-detail/post-detail.component.scss +++ b/src/app/posts/post-detail/post-detail.component.scss @@ -1,29 +1,19 @@ -.back-container { - padding: 24px 0; - display: flex; - - button[mat-fab] { - display: flex; - align-items: center; - background: none; - border: none; - } -} - .post-card-container { margin-bottom: 32px; + font-size: medium; + white-space: pre-line; + padding-top: 16px; } .comments-card-container { margin-top: 12px; + font-size: small; } .comment-content { word-break: break-word; white-space: pre-line; - width: 60%; background: none; - } .comment-card { diff --git a/src/app/posts/post-detail/post-detail.component.ts b/src/app/posts/post-detail/post-detail.component.ts index 929b196..23388e6 100644 --- a/src/app/posts/post-detail/post-detail.component.ts +++ b/src/app/posts/post-detail/post-detail.component.ts @@ -5,6 +5,7 @@ import { Post } from '../post.model'; import { Store } from '@ngrx/store'; import { Comment } from 'src/app/comments/comment.model'; import { MatIconModule } from '@angular/material/icon'; +import { MatButton } from '@angular/material/button'; import { Router } from '@angular/router'; import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors'; @@ -16,7 +17,8 @@ import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors NgIf, NgFor, MatIconModule, - AsyncPipe + AsyncPipe, + MatButton ], templateUrl: './post-detail.component.html', styleUrl: './post-detail.component.scss' diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/posts/post-page/post-page.component.html index 52f4cce..40bd286 100644 --- a/src/app/posts/post-page/post-page.component.html +++ b/src/app/posts/post-page/post-page.component.html @@ -2,7 +2,7 @@ Error: {{ errorMessage$ | async }}
-
+
- - {{ post.title }} - + + + {{ post.title }} + +

{{ post.body }}

-
diff --git a/src/app/posts/posts-list/posts-list.component.scss b/src/app/posts/posts-list/posts-list.component.scss index 8664a48..ee78570 100644 --- a/src/app/posts/posts-list/posts-list.component.scss +++ b/src/app/posts/posts-list/posts-list.component.scss @@ -1,10 +1,13 @@ .posts-list { - width: 100%; display: flex; - flex-direction: column; - align-items: center; + flex-wrap: wrap; + gap: 16px; } .post-item { - margin-bottom: 16px; + width: 260px; +} + +.post-content { + padding: 16px; } \ No newline at end of file diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/posts/posts-list/posts-list.component.ts index 52a955f..4b94322 100644 --- a/src/app/posts/posts-list/posts-list.component.ts +++ b/src/app/posts/posts-list/posts-list.component.ts @@ -1,19 +1,24 @@ import { Component, Input } from '@angular/core'; import { Post } from '../post.model'; import { NgFor } from '@angular/common'; -import { MatCard, MatCardActions, MatCardContent, MatCardTitle } from '@angular/material/card'; +import { MatCardModule } from '@angular/material/card'; +import { MatButton } from '@angular/material/button'; import { Router } from '@angular/router'; @Component({ selector: 'app-posts-list', - imports: [NgFor, MatCard, MatCardTitle, MatCardActions, MatCardContent], + imports: [ + NgFor, + MatCardModule, + MatButton, + ], templateUrl: './posts-list.component.html', styleUrl: './posts-list.component.scss' }) export class PostsListComponent { @Input() posts: Post[] | null = []; - constructor(private router: Router) {} + constructor(private router: Router) { } showPost(post: Post) { this.router.navigate(['./post', post.id]); diff --git a/src/styles.scss b/src/styles.scss index 7e7239a..31d9634 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,5 @@ /* You can add global styles to this file, and also import other style files */ +@import "@angular/material/prebuilt-themes/indigo-pink.css"; html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } From 6ba3144d1963d6b37c80fb74779a183b44d4ced3 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 08:50:24 +0300 Subject: [PATCH 25/36] Refactor components and services to use selectSignal for state management; improve error handling and loading states in templates --- src/app/app.component.scss | 2 +- src/app/app.component.spec.ts | 2 +- src/app/comments/comment.service.spec.ts | 58 +++++++++++++++++++ src/app/comments/state/comments.actions.ts | 6 +- .../post-detail/post-detail.component.html | 28 ++++----- .../post-detail/post-detail.component.spec.ts | 2 +- .../post-detail/post-detail.component.ts | 8 +-- .../posts/post-page/post-page.component.html | 10 ++-- .../posts/post-page/post-page.component.ts | 15 +++-- .../posts-list/posts-list.component.html | 2 +- .../posts-page/posts-page.component.html | 12 ++-- .../posts-page/posts-page.component.spec.ts | 2 +- .../posts/posts-page/posts-page.component.ts | 11 ++-- src/app/posts/posts.routes.ts | 1 - src/app/posts/state/posts.selectors.ts | 4 +- 15 files changed, 106 insertions(+), 57 deletions(-) create mode 100644 src/app/comments/comment.service.spec.ts diff --git a/src/app/app.component.scss b/src/app/app.component.scss index d1385aa..1de8dd7 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -8,4 +8,4 @@ mat-toolbar { margin-bottom: 24px; -} +} \ No newline at end of file diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 7a2cd6c..d2b642b 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -5,7 +5,7 @@ import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ imports: [RouterTestingModule, AppComponent] -})); + })); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); diff --git a/src/app/comments/comment.service.spec.ts b/src/app/comments/comment.service.spec.ts new file mode 100644 index 0000000..a46f9ec --- /dev/null +++ b/src/app/comments/comment.service.spec.ts @@ -0,0 +1,58 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { CommentService } from './comment.service'; +import { environment } from 'src/environments/environment'; +import { Post } from '../posts/post.model'; +import { Comment } from './comment.model'; + +describe('CommentService', () => { + let service: CommentService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [CommentService] + }); + service = TestBed.inject(CommentService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch comments by post', () => { + const post: Post = { id: 1 } as Post; + const mockComments: Comment[] = [ + { id: 1, postId: 1, body: 'Test comment' } as Comment + ]; + + service.getByPost(post).subscribe(comments => { + expect(comments).toEqual(mockComments); + }); + + const req = httpMock.expectOne(`${environment.apiUrl}/comments/?postId=1`); + expect(req.request.method).toBe('GET'); + req.flush(mockComments); + }); + + it('should handle error when fetching comments', () => { + const post: Post = { id: 2 } as Post; + const errorMsg = '404: ERROR - Unable to fetch posts'; + + service.getByPost(post).subscribe({ + next: () => fail('should have failed with error'), + error: (error) => { + expect(error).toBe(errorMsg); + } + }); + + const req = httpMock.expectOne(`${environment.apiUrl}/comments/?postId=2`); + req.flush('Not Found', { status: 404, statusText: 'Not Found' }); + }); +}); diff --git a/src/app/comments/state/comments.actions.ts b/src/app/comments/state/comments.actions.ts index 3e1d203..90e1e17 100644 --- a/src/app/comments/state/comments.actions.ts +++ b/src/app/comments/state/comments.actions.ts @@ -1,18 +1,18 @@ -import { createActionGroup, emptyProps, props } from "@ngrx/store"; +import { createActionGroup, props } from "@ngrx/store"; import { Post } from "src/app/posts/post.model"; import { Comment } from "src/app/comments/comment.model"; export const CommentsPageActions = createActionGroup({ source: 'Comments Page', events: { - 'Load Comments': props<{post: Post}>(), + 'Load Comments': props<{ post: Post }>(), }, }); export const CommentsAPIActions = createActionGroup({ source: 'Comments API', events: { - 'Load Comments': props<{post: Post}>(), + 'Load Comments': props<{ post: Post }>(), 'Comments Loaded Success': props<{ comments: Comment[] }>(), 'Comments Loaded Fail': props<{ message: string }>(), }, diff --git a/src/app/posts/post-detail/post-detail.component.html b/src/app/posts/post-detail/post-detail.component.html index 57e1fab..bd28586 100644 --- a/src/app/posts/post-detail/post-detail.component.html +++ b/src/app/posts/post-detail/post-detail.component.html @@ -16,7 +16,7 @@
-
+
Comments @@ -24,19 +24,19 @@ -
- - - {{ comment.name }} - - - {{ comment.email }} - - - -

{{ comment.body }}

-
-
+
+ + + {{ comment.name }} + + + {{ comment.email }} + + + +

{{ comment.body }}

+
+
diff --git a/src/app/posts/post-detail/post-detail.component.spec.ts b/src/app/posts/post-detail/post-detail.component.spec.ts index a8b41e7..51be87c 100644 --- a/src/app/posts/post-detail/post-detail.component.spec.ts +++ b/src/app/posts/post-detail/post-detail.component.spec.ts @@ -15,7 +15,7 @@ describe('PostsDetailComponent', () => { { provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } } ] }) - .compileComponents(); + .compileComponents(); fixture = TestBed.createComponent(PostsDetailComponent); component = fixture.componentInstance; diff --git a/src/app/posts/post-detail/post-detail.component.ts b/src/app/posts/post-detail/post-detail.component.ts index 23388e6..b509703 100644 --- a/src/app/posts/post-detail/post-detail.component.ts +++ b/src/app/posts/post-detail/post-detail.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; -import { AsyncPipe, NgFor, NgIf } from '@angular/common'; +import { NgFor, NgIf } from '@angular/common'; import { Post } from '../post.model'; import { Store } from '@ngrx/store'; import { Comment } from 'src/app/comments/comment.model'; @@ -17,7 +17,6 @@ import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors NgIf, NgFor, MatIconModule, - AsyncPipe, MatButton ], templateUrl: './post-detail.component.html', @@ -25,9 +24,8 @@ import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors }) export class PostsDetailComponent { @Input() post: Post | undefined; - @Input() comments: Comment[] | null = [] ; - loadingComments$ = this.store.select(selectCommentsLoading); - + @Input() comments: Comment[] | null = []; + loadingComments = this.store.selectSignal(selectCommentsLoading); constructor(private store: Store, private router: Router) { } diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/posts/post-page/post-page.component.html index 40bd286..a80d1c7 100644 --- a/src/app/posts/post-page/post-page.component.html +++ b/src/app/posts/post-page/post-page.component.html @@ -1,12 +1,10 @@ -
- Error: {{ errorMessage$ | async }} +
+ Error: {{ errorMessage() }}
-
+
- +
diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts index eca8ef1..1abce73 100644 --- a/src/app/posts/post-page/post-page.component.ts +++ b/src/app/posts/post-page/post-page.component.ts @@ -1,24 +1,23 @@ import { Component, effect } from '@angular/core'; import { Store } from '@ngrx/store'; import { selectPostsErrorMessage, selectPostsLoading, selectPostById } from '../state/posts.selectors'; -import { PostsPageActions } from '../state/posts.actions'; -import { AsyncPipe, NgIf } from '@angular/common'; +import { NgIf } from '@angular/common'; import { PostsDetailComponent } from '../post-detail/post-detail.component'; -import { selectComments, selectCommentsLoading } from 'src/app/comments/state/comments.selectors'; +import { selectComments } from 'src/app/comments/state/comments.selectors'; import { CommentsPageActions } from 'src/app/comments/state/comments.actions'; @Component({ selector: 'app-post-page', - imports: [NgIf, AsyncPipe, PostsDetailComponent], + imports: [NgIf, PostsDetailComponent], templateUrl: './post-page.component.html', styleUrl: './post-page.component.scss' }) export class PostPageComponent { - loading$ = this.store.select(selectPostsLoading); - errorMessage$ = this.store.select(selectPostsErrorMessage); + loading = this.store.selectSignal(selectPostsLoading); + errorMessage = this.store.selectSignal(selectPostsErrorMessage); post = this.store.selectSignal(selectPostById); - comments$ = this.store.select(selectComments); - + comments = this.store.selectSignal(selectComments); + constructor(private store: Store) { effect(() => { const post = this.post(); diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/posts/posts-list/posts-list.component.html index 83b65c9..17ff1c4 100644 --- a/src/app/posts/posts-list/posts-list.component.html +++ b/src/app/posts/posts-list/posts-list.component.html @@ -1,6 +1,6 @@
- + {{ post.title }} diff --git a/src/app/posts/posts-page/posts-page.component.html b/src/app/posts/posts-page/posts-page.component.html index d49ac61..c845981 100644 --- a/src/app/posts/posts-page/posts-page.component.html +++ b/src/app/posts/posts-page/posts-page.component.html @@ -1,13 +1,11 @@ -
- Error: {{ errorMessage$ | async }} +
+ Error: {{ errorMessage() }}
-
+
- +
-Loading... +Loading... \ No newline at end of file diff --git a/src/app/posts/posts-page/posts-page.component.spec.ts b/src/app/posts/posts-page/posts-page.component.spec.ts index ca7f1e8..85657f6 100644 --- a/src/app/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/posts/posts-page/posts-page.component.spec.ts @@ -14,7 +14,7 @@ describe('PostsPageComponent', () => { imports: [PostsPageComponent], providers: [{ provide: Store, useValue: storeSpy }] }) - .compileComponents(); + .compileComponents(); fixture = TestBed.createComponent(PostsPageComponent); component = fixture.componentInstance; diff --git a/src/app/posts/posts-page/posts-page.component.ts b/src/app/posts/posts-page/posts-page.component.ts index 19a2266..41acf61 100644 --- a/src/app/posts/posts-page/posts-page.component.ts +++ b/src/app/posts/posts-page/posts-page.component.ts @@ -1,20 +1,19 @@ import { Component } from '@angular/core'; import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; import { Store } from '@ngrx/store'; -import { PostsPageActions } from '../state/posts.actions'; -import { AsyncPipe, NgIf } from '@angular/common'; +import { NgIf } from '@angular/common'; import { PostsListComponent } from '../posts-list/posts-list.component'; @Component({ selector: 'app-posts-page', - imports: [NgIf, AsyncPipe, PostsListComponent], + imports: [NgIf, PostsListComponent], templateUrl: './posts-page.component.html', styleUrl: './posts-page.component.scss' }) export class PostsPageComponent { - posts$ = this.store.select(selectPosts); - loading$ = this.store.select(selectPostsLoading); - errorMessage$ = this.store.select(selectPostsErrorMessage); + posts = this.store.selectSignal(selectPosts); + loading = this.store.selectSignal(selectPostsLoading); + errorMessage = this.store.selectSignal(selectPostsErrorMessage); constructor(private store: Store) { } diff --git a/src/app/posts/posts.routes.ts b/src/app/posts/posts.routes.ts index 7848f51..f4d513c 100644 --- a/src/app/posts/posts.routes.ts +++ b/src/app/posts/posts.routes.ts @@ -1,6 +1,5 @@ import { Routes } from '@angular/router'; import { PostsPageComponent } from './posts-page/posts-page.component'; -import { PostsDetailComponent } from './post-detail/post-detail.component'; import { PostPageComponent } from './post-page/post-page.component'; export const routes: Routes = [ diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/posts/state/posts.selectors.ts index 3918c46..2e13843 100644 --- a/src/app/posts/state/posts.selectors.ts +++ b/src/app/posts/state/posts.selectors.ts @@ -15,8 +15,8 @@ export const selectPostsErrorMessage = createSelector( ); export const selectPosts = createSelector( - selectPostsState, - ({ posts }) => posts + selectPostsState, + ({ posts }) => posts ); export const { selectRouteParams } = getRouterSelectors(); From c72bc6bbceec7213895cd8013ff0a3840ea660f2 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 09:43:44 +0300 Subject: [PATCH 26/36] Rename Gbook components to Guestbook; implement new GuestbookEdit and GuestbookPage components with templates and tests --- src/app/app-routes.ts | 4 ++-- src/app/gbook/gbook-edit/gbook-edit.component.html | 1 - src/app/gbook/gbook-edit/gbook-edit.component.ts | 11 ----------- src/app/gbook/gbook-page/gbook-page.component.html | 1 - src/app/gbook/gbook-page/gbook-page.component.ts | 11 ----------- .../guestbook-edit/guestbook-edit.component.html | 1 + .../guestbook-edit/guestbook-edit.component.scss} | 0 .../guestbook-edit/guestbook-edit.component.spec.ts} | 12 ++++++------ .../guestbook-edit/guestbook-edit.component.ts | 11 +++++++++++ .../guestbook-page/guestbook-page.component.html | 1 + .../guestbook-page/guestbook-page.component.scss} | 0 .../guestbook-page/guestbook-page.component.spec.ts} | 12 ++++++------ .../guestbook-page/guestbook-page.component.ts | 11 +++++++++++ 13 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.html delete mode 100644 src/app/gbook/gbook-edit/gbook-edit.component.ts delete mode 100644 src/app/gbook/gbook-page/gbook-page.component.html delete mode 100644 src/app/gbook/gbook-page/gbook-page.component.ts create mode 100644 src/app/guestbook/guestbook-edit/guestbook-edit.component.html rename src/app/{gbook/gbook-edit/gbook-edit.component.scss => guestbook/guestbook-edit/guestbook-edit.component.scss} (100%) rename src/app/{gbook/gbook-page/gbook-page.component.spec.ts => guestbook/guestbook-edit/guestbook-edit.component.spec.ts} (51%) create mode 100644 src/app/guestbook/guestbook-edit/guestbook-edit.component.ts create mode 100644 src/app/guestbook/guestbook-page/guestbook-page.component.html rename src/app/{gbook/gbook-page/gbook-page.component.scss => guestbook/guestbook-page/guestbook-page.component.scss} (100%) rename src/app/{gbook/gbook-edit/gbook-edit.component.spec.ts => guestbook/guestbook-page/guestbook-page.component.spec.ts} (51%) create mode 100644 src/app/guestbook/guestbook-page/guestbook-page.component.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 7416278..ab7d7a3 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,6 +1,6 @@ import { Routes } from '@angular/router'; import { PostService } from './posts/post.service'; -import { GbookPageComponent } from './gbook/gbook-page/gbook-page.component'; +import { GuestbookPageComponent } from './guestbook/guestbook-page/guestbook-page.component'; export const routes: Routes = [ { @@ -13,7 +13,7 @@ export const routes: Routes = [ }, { path: 'guestbook', - component: GbookPageComponent + component: GuestbookPageComponent , }, ]; diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.html b/src/app/gbook/gbook-edit/gbook-edit.component.html deleted file mode 100644 index cb40a70..0000000 --- a/src/app/gbook/gbook-edit/gbook-edit.component.html +++ /dev/null @@ -1 +0,0 @@ -

gbook-edit works!

diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.ts b/src/app/gbook/gbook-edit/gbook-edit.component.ts deleted file mode 100644 index 727da4e..0000000 --- a/src/app/gbook/gbook-edit/gbook-edit.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-gbook-edit', - imports: [], - templateUrl: './gbook-edit.component.html', - styleUrl: './gbook-edit.component.scss' -}) -export class GbookEditComponent { - -} diff --git a/src/app/gbook/gbook-page/gbook-page.component.html b/src/app/gbook/gbook-page/gbook-page.component.html deleted file mode 100644 index 6e5358b..0000000 --- a/src/app/gbook/gbook-page/gbook-page.component.html +++ /dev/null @@ -1 +0,0 @@ -

gbook-page works!

diff --git a/src/app/gbook/gbook-page/gbook-page.component.ts b/src/app/gbook/gbook-page/gbook-page.component.ts deleted file mode 100644 index 2264754..0000000 --- a/src/app/gbook/gbook-page/gbook-page.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-gbook-page', - imports: [], - templateUrl: './gbook-page.component.html', - styleUrl: './gbook-page.component.scss' -}) -export class GbookPageComponent { - -} diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.html b/src/app/guestbook/guestbook-edit/guestbook-edit.component.html new file mode 100644 index 0000000..a3fd30e --- /dev/null +++ b/src/app/guestbook/guestbook-edit/guestbook-edit.component.html @@ -0,0 +1 @@ +

guestbook-edit works!

diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.scss b/src/app/guestbook/guestbook-edit/guestbook-edit.component.scss similarity index 100% rename from src/app/gbook/gbook-edit/gbook-edit.component.scss rename to src/app/guestbook/guestbook-edit/guestbook-edit.component.scss diff --git a/src/app/gbook/gbook-page/gbook-page.component.spec.ts b/src/app/guestbook/guestbook-edit/guestbook-edit.component.spec.ts similarity index 51% rename from src/app/gbook/gbook-page/gbook-page.component.spec.ts rename to src/app/guestbook/guestbook-edit/guestbook-edit.component.spec.ts index c4e9639..50090e0 100644 --- a/src/app/gbook/gbook-page/gbook-page.component.spec.ts +++ b/src/app/guestbook/guestbook-edit/guestbook-edit.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { GbookPageComponent } from './gbook-page.component'; +import { GuestbookEditComponent } from './guestbook-edit.component'; -describe('GbookPageComponent', () => { - let component: GbookPageComponent; - let fixture: ComponentFixture; +describe('GuestbookEditComponent', () => { + let component: GuestbookEditComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GbookPageComponent] + imports: [GuestbookEditComponent] }) .compileComponents(); - fixture = TestBed.createComponent(GbookPageComponent); + fixture = TestBed.createComponent(GuestbookEditComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.ts b/src/app/guestbook/guestbook-edit/guestbook-edit.component.ts new file mode 100644 index 0000000..96658e7 --- /dev/null +++ b/src/app/guestbook/guestbook-edit/guestbook-edit.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-guestbook-edit', + imports: [], + templateUrl: './guestbook-edit.component.html', + styleUrl: './guestbook-edit.component.scss' +}) +export class GuestbookEditComponent { + +} diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.html b/src/app/guestbook/guestbook-page/guestbook-page.component.html new file mode 100644 index 0000000..2f592cd --- /dev/null +++ b/src/app/guestbook/guestbook-page/guestbook-page.component.html @@ -0,0 +1 @@ +

guestbook-page works!

diff --git a/src/app/gbook/gbook-page/gbook-page.component.scss b/src/app/guestbook/guestbook-page/guestbook-page.component.scss similarity index 100% rename from src/app/gbook/gbook-page/gbook-page.component.scss rename to src/app/guestbook/guestbook-page/guestbook-page.component.scss diff --git a/src/app/gbook/gbook-edit/gbook-edit.component.spec.ts b/src/app/guestbook/guestbook-page/guestbook-page.component.spec.ts similarity index 51% rename from src/app/gbook/gbook-edit/gbook-edit.component.spec.ts rename to src/app/guestbook/guestbook-page/guestbook-page.component.spec.ts index 52194f9..99a7f9f 100644 --- a/src/app/gbook/gbook-edit/gbook-edit.component.spec.ts +++ b/src/app/guestbook/guestbook-page/guestbook-page.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { GbookEditComponent } from './gbook-edit.component'; +import { GuestbookPageComponent } from './guestbook-page.component'; -describe('GbookEditComponent', () => { - let component: GbookEditComponent; - let fixture: ComponentFixture; +describe('GuestbookPageComponent', () => { + let component: GuestbookPageComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GbookEditComponent] + imports: [GuestbookPageComponent] }) .compileComponents(); - fixture = TestBed.createComponent(GbookEditComponent); + fixture = TestBed.createComponent(GuestbookPageComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.ts b/src/app/guestbook/guestbook-page/guestbook-page.component.ts new file mode 100644 index 0000000..be6e986 --- /dev/null +++ b/src/app/guestbook/guestbook-page/guestbook-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-guestbook-page', + imports: [], + templateUrl: './guestbook-page.component.html', + styleUrl: './guestbook-page.component.scss' +}) +export class GuestbookPageComponent { + +} From 1aad1a339fff8d5ae17f3c65b1b4bbd3aa0baf8e Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 09:43:58 +0300 Subject: [PATCH 27/36] Enhance comments section in post detail view; display comment count and improve styling --- .../post-detail/post-detail.component.html | 4 ++-- .../post-detail/post-detail.component.scss | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/app/posts/post-detail/post-detail.component.html b/src/app/posts/post-detail/post-detail.component.html index bd28586..c9e2b97 100644 --- a/src/app/posts/post-detail/post-detail.component.html +++ b/src/app/posts/post-detail/post-detail.component.html @@ -18,8 +18,8 @@
- - Comments + + Comments ({{ comments?.length }}) diff --git a/src/app/posts/post-detail/post-detail.component.scss b/src/app/posts/post-detail/post-detail.component.scss index 179faeb..8cea088 100644 --- a/src/app/posts/post-detail/post-detail.component.scss +++ b/src/app/posts/post-detail/post-detail.component.scss @@ -8,15 +8,20 @@ .comments-card-container { margin-top: 12px; font-size: small; -} -.comment-content { - word-break: break-word; - white-space: pre-line; - background: none; -} + .comment-content { + word-break: break-word; + white-space: pre-line; + background: none; + } + + .comment-card { + margin-top: 12px; + margin-bottom: 12px; + } -.comment-card { - background: #fff; - margin-bottom: 12px; + .comment-header { + padding: 8px 16px; + border-bottom: 1px solid #e0e0e0; + } } \ No newline at end of file From 05b7a682900ec0d3fbaeccbfbd1a8eee5c9feee2 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 10:16:26 +0300 Subject: [PATCH 28/36] Add environment file for development and update main.ts for production flag --- angular.json | 8 +++++- .../post-detail/post-detail.component.spec.ts | 28 +++++++++++++++---- .../post-page/post-page.component.spec.ts | 2 -- .../posts-page/posts-page.component.spec.ts | 23 ++++++++++----- src/environments/environment.development.ts | 4 +++ src/main.ts | 3 +- 6 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 src/environments/environment.development.ts diff --git a/angular.json b/angular.json index 1b91161..ce6f9e5 100644 --- a/angular.json +++ b/angular.json @@ -54,6 +54,12 @@ "outputHashing": "all" }, "development": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ], "optimization": false, "extractLicenses": false, "sourceMap": true, @@ -118,4 +124,4 @@ ], "analytics": false } -} +} \ No newline at end of file diff --git a/src/app/posts/post-detail/post-detail.component.spec.ts b/src/app/posts/post-detail/post-detail.component.spec.ts index 51be87c..cf92c2f 100644 --- a/src/app/posts/post-detail/post-detail.component.spec.ts +++ b/src/app/posts/post-detail/post-detail.component.spec.ts @@ -1,22 +1,34 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostsDetailComponent } from './post-detail.component'; -import { provideMockStore } from '@ngrx/store/testing'; +import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; describe('PostsDetailComponent', () => { let component: PostsDetailComponent; let fixture: ComponentFixture; + let store: MockStore; + let router: jasmine.SpyObj; + + const initialState = { + comments: { + loading: false + } + }; beforeEach(async () => { + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + await TestBed.configureTestingModule({ imports: [PostsDetailComponent], providers: [ - provideMockStore(), - { provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } } + provideMockStore({ initialState }), + { provide: Router, useValue: routerSpy } ] - }) - .compileComponents(); + }).compileComponents(); + store = TestBed.inject(Store) as MockStore; + router = TestBed.inject(Router) as jasmine.SpyObj; fixture = TestBed.createComponent(PostsDetailComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -25,4 +37,10 @@ describe('PostsDetailComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should navigate back when goBack is called', () => { + component.goBack(); + expect(router.navigate).toHaveBeenCalledWith(['/']); + }); + }); diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/post-page/post-page.component.spec.ts index 468b8cb..b777259 100644 --- a/src/app/posts/post-page/post-page.component.spec.ts +++ b/src/app/posts/post-page/post-page.component.spec.ts @@ -1,8 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { PostPageComponent } from './post-page.component'; -import { ActivatedRoute } from '@angular/router'; -import { of } from 'rxjs'; import { Store } from '@ngrx/store'; describe('PostPageComponent', () => { diff --git a/src/app/posts/posts-page/posts-page.component.spec.ts b/src/app/posts/posts-page/posts-page.component.spec.ts index 85657f6..1103874 100644 --- a/src/app/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/posts/posts-page/posts-page.component.spec.ts @@ -1,21 +1,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostsPageComponent } from './posts-page.component'; import { Store } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; describe('PostsPageComponent', () => { let component: PostsPageComponent; let fixture: ComponentFixture; - let storeSpy: jasmine.SpyObj>; + let store: MockStore; - beforeEach(async () => { - storeSpy = jasmine.createSpyObj('Store', ['select', 'dispatch', 'subscribe']); + const initialState = { + posts: { + posts: [], + loading: false, + error: null + } + }; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PostsPageComponent], - providers: [{ provide: Store, useValue: storeSpy }] - }) - .compileComponents(); + providers: [ + provideMockStore({ initialState }) + ] + }).compileComponents(); + store = TestBed.inject(Store) as MockStore; fixture = TestBed.createComponent(PostsPageComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -24,5 +33,5 @@ describe('PostsPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - + }); diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts new file mode 100644 index 0000000..ac5834a --- /dev/null +++ b/src/environments/environment.development.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiUrl: 'https://jsonplaceholder.typicode.com/', +}; diff --git a/src/main.ts b/src/main.ts index c6318d6..cd7a630 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { PostsEffects } from './app/posts/state/posts.effects'; import { CommentsEffects } from './app/comments/state/comments.effects'; import { provideHttpClient } from '@angular/common/http'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; +import { environment } from './environments/environment'; bootstrapApplication(AppComponent, { providers: [ @@ -20,7 +21,7 @@ bootstrapApplication(AppComponent, { }), provideStoreDevtools({ maxAge: 25, - logOnly: false + logOnly: environment.production }), provideRouterStore(), provideRouter(routes), From a94ad7fa2668c677d1b37e0be20636eef7af87b4 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 13:17:45 +0300 Subject: [PATCH 29/36] Refactor comments and posts state management; implement NgRx entity adapter, update selectors, and enhance effects for loading comments and posts --- src/app/app-routes.ts | 11 +++- src/app/comments/comment.service.ts | 1 - src/app/comments/state/comments.actions.ts | 8 +-- src/app/comments/state/comments.effects.ts | 6 +- src/app/comments/state/comments.reducer.ts | 49 ++++++++------- src/app/comments/state/comments.selectors.ts | 7 +-- .../post-page/post-page.component.spec.ts | 45 ++++++++------ .../posts/post-page/post-page.component.ts | 7 ++- .../posts-page/posts-page.component.spec.ts | 60 ++++++++++++++++-- src/app/posts/state/posts.reducer.ts | 61 ++++++++++++------- src/app/posts/state/posts.selectors.ts | 15 +++-- src/main.ts | 3 - 12 files changed, 184 insertions(+), 89 deletions(-) diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index ab7d7a3..2d957ac 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,6 +1,12 @@ import { Routes } from '@angular/router'; import { PostService } from './posts/post.service'; import { GuestbookPageComponent } from './guestbook/guestbook-page/guestbook-page.component'; +import { provideState } from '@ngrx/store'; +import { postsFeature } from './posts/state/posts.reducer'; +import { provideEffects } from '@ngrx/effects'; +import { PostsEffects } from './posts/state/posts.effects'; +import { CommentsEffects } from './comments/state/comments.effects'; +import { commentsFeature } from './comments/state/comments.reducer'; export const routes: Routes = [ { @@ -8,7 +14,10 @@ export const routes: Routes = [ loadChildren: () => import('./posts/posts.routes').then((mod) => mod.routes), providers: [ - PostService + PostService, + provideState(postsFeature), + provideState(commentsFeature), + provideEffects(PostsEffects, CommentsEffects), ], }, { diff --git a/src/app/comments/comment.service.ts b/src/app/comments/comment.service.ts index ce43d8a..3ff0beb 100644 --- a/src/app/comments/comment.service.ts +++ b/src/app/comments/comment.service.ts @@ -19,7 +19,6 @@ export class CommentService { .pipe(catchError(this.handleError)); } - private handleError({ status }: HttpErrorResponse) { return throwError( () => `${status}: ERROR - Unable to fetch posts` diff --git a/src/app/comments/state/comments.actions.ts b/src/app/comments/state/comments.actions.ts index 90e1e17..2c64781 100644 --- a/src/app/comments/state/comments.actions.ts +++ b/src/app/comments/state/comments.actions.ts @@ -5,15 +5,15 @@ import { Comment } from "src/app/comments/comment.model"; export const CommentsPageActions = createActionGroup({ source: 'Comments Page', events: { - 'Load Comments': props<{ post: Post }>(), + 'Load Post Comments': props<{ post: Post }>(), }, }); export const CommentsAPIActions = createActionGroup({ source: 'Comments API', events: { - 'Load Comments': props<{ post: Post }>(), - 'Comments Loaded Success': props<{ comments: Comment[] }>(), - 'Comments Loaded Fail': props<{ message: string }>(), + 'Load Post Comments': props<{ post: Post }>(), + 'Post Comments Loaded Success': props<{ comments: Comment[] }>(), + 'Post Comments Loaded Fail': props<{ message: string }>(), }, }); diff --git a/src/app/comments/state/comments.effects.ts b/src/app/comments/state/comments.effects.ts index 47ed9a4..8c544eb 100644 --- a/src/app/comments/state/comments.effects.ts +++ b/src/app/comments/state/comments.effects.ts @@ -14,12 +14,12 @@ export class CommentsEffects { loadComments$ = createEffect(() => this.actions$.pipe( - ofType(CommentsPageActions.loadComments), + ofType(CommentsPageActions.loadPostComments), concatMap(({ post }) => this.commentsService.getByPost(post).pipe( - map((comments) => CommentsAPIActions.commentsLoadedSuccess({ comments })), + map((comments) => CommentsAPIActions.postCommentsLoadedSuccess({ comments })), catchError((error) => - of(CommentsAPIActions.commentsLoadedFail({ message: error })) + of(CommentsAPIActions.postCommentsLoadedFail({ message: error })) ) ) ) diff --git a/src/app/comments/state/comments.reducer.ts b/src/app/comments/state/comments.reducer.ts index 7d712d4..5896fa5 100644 --- a/src/app/comments/state/comments.reducer.ts +++ b/src/app/comments/state/comments.reducer.ts @@ -1,37 +1,44 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { CommentsAPIActions, CommentsPageActions } from "./comments.actions"; import { Comment } from "src/app/comments/comment.model"; +import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; -export interface CommentsState { +export interface CommentsState extends EntityState { loading: boolean; errorMessage: string; - comments: Comment[]; } -const initialState: CommentsState = { + +const adapter: EntityAdapter = createEntityAdapter({}); + +const initialState: CommentsState = adapter.getInitialState({ loading: false, errorMessage: '', - comments: [], -}; +}); + +const { selectAll } = adapter.getSelectors(); +export const selectAllComments = selectAll; export const commentsFeature = createFeature({ name: 'comments', reducer: createReducer( initialState, - on(CommentsPageActions.loadComments, (state) => ({ - ...state, - loading: true, - errorMessage: '', - comments: [], - })), - on(CommentsAPIActions.commentsLoadedSuccess, (state, { comments }) => ({ - ...state, - loading: false, - comments: comments, - })), - on(CommentsAPIActions.commentsLoadedFail, (state, { message }) => ({ - ...state, - errorMessage: message, - loading: false, - })), + on(CommentsPageActions.loadPostComments, (state) => + adapter.setAll([], { + ...state, + loading: true, + errorMessage: '', + })), + on(CommentsAPIActions.postCommentsLoadedSuccess, (state, { comments }) => + adapter.setAll(comments, { + ...state, + loading: false, + comments: comments, + })), + on(CommentsAPIActions.postCommentsLoadedFail, (state, { message }) => + adapter.setAll([], { + ...state, + errorMessage: message, + loading: false, + })), ), }); \ No newline at end of file diff --git a/src/app/comments/state/comments.selectors.ts b/src/app/comments/state/comments.selectors.ts index 2732beb..022e93e 100644 --- a/src/app/comments/state/comments.selectors.ts +++ b/src/app/comments/state/comments.selectors.ts @@ -1,8 +1,7 @@ import { createFeatureSelector, createSelector } from "@ngrx/store"; -import { CommentsState } from "./comments.reducer"; +import * as fromComments from './comments.reducer'; - -export const selectCommentsState = createFeatureSelector('comments'); +export const selectCommentsState = createFeatureSelector('comments'); export const selectCommentsLoading = createSelector( selectCommentsState, @@ -16,5 +15,5 @@ export const selectCommentsErrorMessage = createSelector( export const selectComments = createSelector( selectCommentsState, - ({ comments }) => comments + fromComments.selectAllComments ); diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/post-page/post-page.component.spec.ts index b777259..cfe7ec6 100644 --- a/src/app/posts/post-page/post-page.component.spec.ts +++ b/src/app/posts/post-page/post-page.component.spec.ts @@ -1,32 +1,44 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideMockStore } from '@ngrx/store/testing'; import { PostPageComponent } from './post-page.component'; -import { Store } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; +import { selectComments } from 'src/app/comments/state/comments.selectors'; + describe('PostPageComponent', () => { let component: PostPageComponent; let fixture: ComponentFixture; + let store: MockStore; + + const initialState = { + posts: { + loading: false, + errorMessage: '', + entities: [], + ids: [] + }, + comments: { + comments: [] + } + }; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PostPageComponent], providers: [ - provideMockStore(), - { - provide: Store, - useValue: { - select: jasmine.createSpy('select'), - selectSignal: jasmine.createSpy('selectSignal').and.returnValue(() => ({ - id: 1, - userId: 1, - title: 'Test', - body: 'Test body' - })), - dispatch: jasmine.createSpy('dispatch') - } - } + provideMockStore({ + initialState, + selectors: [ + { selector: selectPostsLoading, value: false }, + { selector: selectPostsErrorMessage, value: '' }, + { selector: selectPostById, value: null }, + { selector: selectComments, value: [] } + ] + }) ] }).compileComponents(); + + store = TestBed.inject(MockStore); fixture = TestBed.createComponent(PostPageComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -36,4 +48,3 @@ describe('PostPageComponent', () => { expect(component).toBeTruthy(); }); }); - diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts index 1abce73..d0cc488 100644 --- a/src/app/posts/post-page/post-page.component.ts +++ b/src/app/posts/post-page/post-page.component.ts @@ -18,11 +18,12 @@ export class PostPageComponent { post = this.store.selectSignal(selectPostById); comments = this.store.selectSignal(selectComments); + constructor(private store: Store) { effect(() => { - const post = this.post(); - if (post) { - this.store.dispatch(CommentsPageActions.loadComments({ post })); + const currentPost = this.post(); + if (currentPost) { + this.store.dispatch(CommentsPageActions.loadPostComments({ post: currentPost })); } }); } diff --git a/src/app/posts/posts-page/posts-page.component.spec.ts b/src/app/posts/posts-page/posts-page.component.spec.ts index 1103874..485482e 100644 --- a/src/app/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/posts/posts-page/posts-page.component.spec.ts @@ -1,18 +1,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostsPageComponent } from './posts-page.component'; -import { Store } from '@ngrx/store'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; describe('PostsPageComponent', () => { let component: PostsPageComponent; let fixture: ComponentFixture; let store: MockStore; + const mockPosts = [ + { id: 1, title: 'Test Post 1', body: 'Test Body 1', userId: 1 }, + { id: 2, title: 'Test Post 2', body: 'Test Body 2', userId: 1 } + ]; + const initialState = { posts: { - posts: [], loading: false, - error: null + errorMessage: '', + ids: [1, 2], + entities: { + 1: mockPosts[0], + 2: mockPosts[1] + } } }; @@ -20,11 +29,18 @@ describe('PostsPageComponent', () => { await TestBed.configureTestingModule({ imports: [PostsPageComponent], providers: [ - provideMockStore({ initialState }) + provideMockStore({ + initialState, + selectors: [ + { selector: selectPostsLoading, value: false }, + { selector: selectPostsErrorMessage, value: '' }, + { selector: selectPosts, value: mockPosts } + ] + }) ] }).compileComponents(); - store = TestBed.inject(Store) as MockStore; + store = TestBed.inject(MockStore); fixture = TestBed.createComponent(PostsPageComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -33,5 +49,37 @@ describe('PostsPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - + + it('should show loading state', () => { + store.overrideSelector(selectPostsLoading, true); + store.refreshState(); + fixture.detectChanges(); + + expect(component.loading()).toBe(true); + }); + + it('should show error message', () => { + const errorMessage = 'Test error'; + store.overrideSelector(selectPostsErrorMessage, errorMessage); + store.refreshState(); + fixture.detectChanges(); + + expect(component.errorMessage()).toBe(errorMessage); + }); + + it('should display posts', () => { + store.overrideSelector(selectPosts, mockPosts); + store.refreshState(); + fixture.detectChanges(); + + expect(component.posts()).toEqual(mockPosts); + }); + + it('should be empty posts', () => { + store.overrideSelector(selectPosts, []); + store.refreshState(); + fixture.detectChanges(); + + expect(component.posts()).toEqual([]); + }); }); diff --git a/src/app/posts/state/posts.reducer.ts b/src/app/posts/state/posts.reducer.ts index cb49dde..af1f800 100644 --- a/src/app/posts/state/posts.reducer.ts +++ b/src/app/posts/state/posts.reducer.ts @@ -1,37 +1,56 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { PostsAPIActions, PostsPageActions } from "./posts.actions"; import { Post } from "../post.model"; +import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; -export interface PostsState { +export interface PostsState extends EntityState { loading: boolean; errorMessage: string; - posts: Post[]; } -const initialState: PostsState = { + +const adapter: EntityAdapter = createEntityAdapter({ + selectId, + sortComparer +}); + +export function selectId(post: Post): number { + return post.id; +} + +export function sortComparer(a: Post, b: Post): number { + return b.id - a.id; +} + +const initialState: PostsState = adapter.getInitialState({ loading: false, errorMessage: '', - posts: [], -}; +}); + +const { selectEntities, selectAll } = adapter.getSelectors(); + +export const selectPostEntities = selectEntities; +export const selectAllPosts = selectAll; export const postsFeature = createFeature({ name: 'posts', reducer: createReducer( initialState, - on(PostsPageActions.loadPosts, (state) => ({ - ...state, - loading: true, - errorMessage: '', - posts: [], - })), - on(PostsAPIActions.postsLoadedSuccess, (state, { posts }) => ({ - ...state, - loading: false, - posts: posts, - })), - on(PostsAPIActions.postsLoadedFail, (state, { message }) => ({ - ...state, - errorMessage: message, - loading: false, - })), + on(PostsPageActions.loadPosts, (state) => + adapter.setAll([], { + ...state, + loading: true, + errorMessage: '', + })), + on(PostsAPIActions.postsLoadedSuccess, (state, { posts }) => + adapter.setAll(posts, { + ...state, + loading: false, + })), + on(PostsAPIActions.postsLoadedFail, (state, { message }) => + adapter.setAll([], { + ...state, + errorMessage: message, + loading: false, + })), ), }); \ No newline at end of file diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/posts/state/posts.selectors.ts index 2e13843..f6e30b7 100644 --- a/src/app/posts/state/posts.selectors.ts +++ b/src/app/posts/state/posts.selectors.ts @@ -1,8 +1,8 @@ import { createFeatureSelector, createSelector } from "@ngrx/store"; -import { PostsState } from "./posts.reducer"; import { getRouterSelectors } from "@ngrx/router-store"; +import * as fromPosts from './posts.reducer'; -export const selectPostsState = createFeatureSelector('posts'); +export const selectPostsState = createFeatureSelector('posts'); export const selectPostsLoading = createSelector( selectPostsState, @@ -16,13 +16,18 @@ export const selectPostsErrorMessage = createSelector( export const selectPosts = createSelector( selectPostsState, - ({ posts }) => posts + fromPosts.selectAllPosts +); + +export const selectPostsEntitites = createSelector( + selectPostsState, + fromPosts.selectPostEntities ); export const { selectRouteParams } = getRouterSelectors(); export const selectPostById = createSelector( - selectPosts, + selectPostsEntitites, selectRouteParams, - (posts, { id }) => posts.find(post => post.id === parseInt(id)) + ((entities, { id }) => entities[id]) ); diff --git a/src/main.ts b/src/main.ts index cd7a630..4d46ca4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,9 +25,6 @@ bootstrapApplication(AppComponent, { }), provideRouterStore(), provideRouter(routes), - provideState(postsFeature), - provideState(commentsFeature), - provideEffects(PostsEffects, CommentsEffects), ] }) .catch(err => console.error(err)); From f79bb325d45f1b62c6f91f16e6ad7a37c47045b0 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 13:31:24 +0300 Subject: [PATCH 30/36] Rename CommentsPageActions to CommentsComponentActions for consistency; update imports and usages across the application --- src/app/comments/state/comments.actions.ts | 4 ++-- src/app/comments/state/comments.effects.ts | 4 ++-- src/app/comments/state/comments.reducer.ts | 4 ++-- src/app/posts/post-detail/post-detail.component.spec.ts | 5 +---- src/app/posts/post-page/post-page.component.spec.ts | 4 +--- src/app/posts/post-page/post-page.component.ts | 4 ++-- src/main.ts | 7 +------ 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/app/comments/state/comments.actions.ts b/src/app/comments/state/comments.actions.ts index 2c64781..89432b8 100644 --- a/src/app/comments/state/comments.actions.ts +++ b/src/app/comments/state/comments.actions.ts @@ -2,8 +2,8 @@ import { createActionGroup, props } from "@ngrx/store"; import { Post } from "src/app/posts/post.model"; import { Comment } from "src/app/comments/comment.model"; -export const CommentsPageActions = createActionGroup({ - source: 'Comments Page', +export const CommentsComponentActions = createActionGroup({ + source: 'Comments Component', events: { 'Load Post Comments': props<{ post: Post }>(), }, diff --git a/src/app/comments/state/comments.effects.ts b/src/app/comments/state/comments.effects.ts index 8c544eb..4ee5177 100644 --- a/src/app/comments/state/comments.effects.ts +++ b/src/app/comments/state/comments.effects.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; import { catchError, concatMap, map, of } from "rxjs"; import { CommentService } from "../comment.service"; -import { CommentsAPIActions, CommentsPageActions } from "./comments.actions"; +import { CommentsAPIActions, CommentsComponentActions } from "./comments.actions"; @Injectable() export class CommentsEffects { @@ -14,7 +14,7 @@ export class CommentsEffects { loadComments$ = createEffect(() => this.actions$.pipe( - ofType(CommentsPageActions.loadPostComments), + ofType(CommentsComponentActions.loadPostComments), concatMap(({ post }) => this.commentsService.getByPost(post).pipe( map((comments) => CommentsAPIActions.postCommentsLoadedSuccess({ comments })), diff --git a/src/app/comments/state/comments.reducer.ts b/src/app/comments/state/comments.reducer.ts index 5896fa5..020e8f2 100644 --- a/src/app/comments/state/comments.reducer.ts +++ b/src/app/comments/state/comments.reducer.ts @@ -1,5 +1,5 @@ import { createFeature, createReducer, on } from "@ngrx/store"; -import { CommentsAPIActions, CommentsPageActions } from "./comments.actions"; +import { CommentsAPIActions, CommentsComponentActions } from "./comments.actions"; import { Comment } from "src/app/comments/comment.model"; import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; @@ -22,7 +22,7 @@ export const commentsFeature = createFeature({ name: 'comments', reducer: createReducer( initialState, - on(CommentsPageActions.loadPostComments, (state) => + on(CommentsComponentActions.loadPostComments, (state) => adapter.setAll([], { ...state, loading: true, diff --git a/src/app/posts/post-detail/post-detail.component.spec.ts b/src/app/posts/post-detail/post-detail.component.spec.ts index cf92c2f..cc2240a 100644 --- a/src/app/posts/post-detail/post-detail.component.spec.ts +++ b/src/app/posts/post-detail/post-detail.component.spec.ts @@ -1,13 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostsDetailComponent } from './post-detail.component'; -import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { provideMockStore } from '@ngrx/store/testing'; describe('PostsDetailComponent', () => { let component: PostsDetailComponent; let fixture: ComponentFixture; - let store: MockStore; let router: jasmine.SpyObj; const initialState = { @@ -27,7 +25,6 @@ describe('PostsDetailComponent', () => { ] }).compileComponents(); - store = TestBed.inject(Store) as MockStore; router = TestBed.inject(Router) as jasmine.SpyObj; fixture = TestBed.createComponent(PostsDetailComponent); component = fixture.componentInstance; diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/posts/post-page/post-page.component.spec.ts index cfe7ec6..2ab9e57 100644 --- a/src/app/posts/post-page/post-page.component.spec.ts +++ b/src/app/posts/post-page/post-page.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostPageComponent } from './post-page.component'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { provideMockStore } from '@ngrx/store/testing'; import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; import { selectComments } from 'src/app/comments/state/comments.selectors'; @@ -8,7 +8,6 @@ import { selectComments } from 'src/app/comments/state/comments.selectors'; describe('PostPageComponent', () => { let component: PostPageComponent; let fixture: ComponentFixture; - let store: MockStore; const initialState = { posts: { @@ -38,7 +37,6 @@ describe('PostPageComponent', () => { ] }).compileComponents(); - store = TestBed.inject(MockStore); fixture = TestBed.createComponent(PostPageComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/posts/post-page/post-page.component.ts index d0cc488..e1fc183 100644 --- a/src/app/posts/post-page/post-page.component.ts +++ b/src/app/posts/post-page/post-page.component.ts @@ -4,7 +4,7 @@ import { selectPostsErrorMessage, selectPostsLoading, selectPostById } from '../ import { NgIf } from '@angular/common'; import { PostsDetailComponent } from '../post-detail/post-detail.component'; import { selectComments } from 'src/app/comments/state/comments.selectors'; -import { CommentsPageActions } from 'src/app/comments/state/comments.actions'; +import { CommentsComponentActions } from 'src/app/comments/state/comments.actions'; @Component({ selector: 'app-post-page', @@ -23,7 +23,7 @@ export class PostPageComponent { effect(() => { const currentPost = this.post(); if (currentPost) { - this.store.dispatch(CommentsPageActions.loadPostComments({ post: currentPost })); + this.store.dispatch(CommentsComponentActions.loadPostComments({ post: currentPost })); } }); } diff --git a/src/main.ts b/src/main.ts index 4d46ca4..3b9b998 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,13 +2,8 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { routes } from './app/app-routes'; import { AppComponent } from './app/app.component'; -import { provideState, provideStore } from '@ngrx/store'; +import { provideStore } from '@ngrx/store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; -import { provideEffects } from '@ngrx/effects'; -import { postsFeature } from './app/posts/state/posts.reducer'; -import { commentsFeature } from './app/comments/state/comments.reducer'; -import { PostsEffects } from './app/posts/state/posts.effects'; -import { CommentsEffects } from './app/comments/state/comments.effects'; import { provideHttpClient } from '@angular/common/http'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; import { environment } from './environments/environment'; From 88843aabfcbb25c857b8b49d6caa44cd1d42e6e3 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 13:32:11 +0300 Subject: [PATCH 31/36] Add linter step to GitHub Actions workflow for code quality checks --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d128848..64795a7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,8 @@ jobs: node-version: 22.15.1 - name: Install Dependencies run: npm install + - name: Run Linter + run: npm run lint - name: Run tests run: npm run test:ci - name: Build Angular App From d01592f37dafd61b4b89de870e11792c06a0cd9a Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 15:32:44 +0300 Subject: [PATCH 32/36] change project structure --- .../comment.service.spec.ts | 4 ++-- src/app/{comments => apis}/comment.service.ts | 4 ++-- src/app/{posts => apis}/post.service.spec.ts | 2 +- src/app/{posts => apis}/post.service.ts | 2 +- src/app/app-routes.ts | 16 ++++++------- .../guestbook-edit.component.html | 0 .../guestbook-edit.component.scss | 0 .../guestbook-edit.component.spec.ts | 0 .../guestbook-edit.component.ts | 0 .../guestbook-list.component.html | 1 + .../guestbook-list.component.scss} | 0 .../guestbook-list.component.spec.ts | 23 +++++++++++++++++++ .../guestbook-list.component.ts | 11 +++++++++ .../guestbook-page.component.html | 0 .../guestbook-page.component.scss} | 0 .../guestbook-page.component.spec.ts | 0 .../guestbook-page.component.ts | 0 src/app/features/guestbook/guestbook.model.ts | 7 ++++++ .../guestbook/guestbook.service.spec.ts | 16 +++++++++++++ .../features/guestbook/guestbook.service.ts | 9 ++++++++ .../post-detail/post-detail.component.html | 0 .../post-detail/post-detail.component.scss | 0 .../post-detail/post-detail.component.spec.ts | 0 .../post-detail/post-detail.component.ts | 6 ++--- .../posts/post-page/post-page.component.html | 0 .../posts/post-page/post-page.component.scss | 0 .../post-page/post-page.component.spec.ts | 2 +- .../posts/post-page/post-page.component.ts | 6 ++--- .../posts-list/posts-list.component.html | 0 .../posts-list/posts-list.component.scss | 0 .../posts-list/posts-list.component.spec.ts | 0 .../posts/posts-list/posts-list.component.ts | 2 +- .../posts-page/posts-page.component.html | 0 .../posts-page/posts-page.component.scss | 0 .../posts-page/posts-page.component.spec.ts | 2 +- .../posts/posts-page/posts-page.component.ts | 2 +- src/app/{ => features}/posts/posts.routes.ts | 0 .../comment}/comments.actions.ts | 4 ++-- .../comment}/comments.effects.ts | 2 +- .../comment}/comments.reducer.ts | 2 +- .../comment}/comments.selectors.ts | 0 .../state => store/post}/posts.actions.ts | 2 +- .../state => store/post}/posts.effects.ts | 2 +- .../state => store/post}/posts.reducer.ts | 2 +- .../state => store/post}/posts.selectors.ts | 0 src/app/{comments => types}/comment.model.ts | 0 src/app/{posts => types}/post.model.ts | 0 47 files changed, 98 insertions(+), 31 deletions(-) rename src/app/{comments => apis}/comment.service.spec.ts (95%) rename src/app/{comments => apis}/comment.service.ts (88%) rename src/app/{posts => apis}/post.service.spec.ts (97%) rename src/app/{posts => apis}/post.service.ts (94%) rename src/app/{ => features}/guestbook/guestbook-edit/guestbook-edit.component.html (100%) rename src/app/{ => features}/guestbook/guestbook-edit/guestbook-edit.component.scss (100%) rename src/app/{ => features}/guestbook/guestbook-edit/guestbook-edit.component.spec.ts (100%) rename src/app/{ => features}/guestbook/guestbook-edit/guestbook-edit.component.ts (100%) create mode 100644 src/app/features/guestbook/guestbook-list/guestbook-list.component.html rename src/app/{guestbook/guestbook-page/guestbook-page.component.scss => features/guestbook/guestbook-list/guestbook-list.component.scss} (100%) create mode 100644 src/app/features/guestbook/guestbook-list/guestbook-list.component.spec.ts create mode 100644 src/app/features/guestbook/guestbook-list/guestbook-list.component.ts rename src/app/{ => features}/guestbook/guestbook-page/guestbook-page.component.html (100%) rename src/app/{posts/posts-page/posts-page.component.scss => features/guestbook/guestbook-page/guestbook-page.component.scss} (100%) rename src/app/{ => features}/guestbook/guestbook-page/guestbook-page.component.spec.ts (100%) rename src/app/{ => features}/guestbook/guestbook-page/guestbook-page.component.ts (100%) create mode 100644 src/app/features/guestbook/guestbook.model.ts create mode 100644 src/app/features/guestbook/guestbook.service.spec.ts create mode 100644 src/app/features/guestbook/guestbook.service.ts rename src/app/{ => features}/posts/post-detail/post-detail.component.html (100%) rename src/app/{ => features}/posts/post-detail/post-detail.component.scss (100%) rename src/app/{ => features}/posts/post-detail/post-detail.component.spec.ts (100%) rename src/app/{ => features}/posts/post-detail/post-detail.component.ts (82%) rename src/app/{ => features}/posts/post-page/post-page.component.html (100%) rename src/app/{ => features}/posts/post-page/post-page.component.scss (100%) rename src/app/{ => features}/posts/post-page/post-page.component.spec.ts (96%) rename src/app/{ => features}/posts/post-page/post-page.component.ts (76%) rename src/app/{ => features}/posts/posts-list/posts-list.component.html (100%) rename src/app/{ => features}/posts/posts-list/posts-list.component.scss (100%) rename src/app/{ => features}/posts/posts-list/posts-list.component.spec.ts (100%) rename src/app/{ => features}/posts/posts-list/posts-list.component.ts (92%) rename src/app/{ => features}/posts/posts-page/posts-page.component.html (100%) create mode 100644 src/app/features/posts/posts-page/posts-page.component.scss rename src/app/{ => features}/posts/posts-page/posts-page.component.spec.ts (97%) rename src/app/{ => features}/posts/posts-page/posts-page.component.ts (93%) rename src/app/{ => features}/posts/posts.routes.ts (100%) rename src/app/{comments/state => store/comment}/comments.actions.ts (82%) rename src/app/{comments/state => store/comment}/comments.effects.ts (93%) rename src/app/{comments/state => store/comment}/comments.reducer.ts (96%) rename src/app/{comments/state => store/comment}/comments.selectors.ts (100%) rename src/app/{posts/state => store/post}/posts.actions.ts (90%) rename src/app/{posts/state => store/post}/posts.effects.ts (94%) rename src/app/{posts/state => store/post}/posts.reducer.ts (97%) rename src/app/{posts/state => store/post}/posts.selectors.ts (100%) rename src/app/{comments => types}/comment.model.ts (100%) rename src/app/{posts => types}/post.model.ts (100%) diff --git a/src/app/comments/comment.service.spec.ts b/src/app/apis/comment.service.spec.ts similarity index 95% rename from src/app/comments/comment.service.spec.ts rename to src/app/apis/comment.service.spec.ts index a46f9ec..2575ed2 100644 --- a/src/app/comments/comment.service.spec.ts +++ b/src/app/apis/comment.service.spec.ts @@ -2,8 +2,8 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { CommentService } from './comment.service'; import { environment } from 'src/environments/environment'; -import { Post } from '../posts/post.model'; -import { Comment } from './comment.model'; +import { Post } from '../types/post.model'; +import { Comment } from '../types/comment.model'; describe('CommentService', () => { let service: CommentService; diff --git a/src/app/comments/comment.service.ts b/src/app/apis/comment.service.ts similarity index 88% rename from src/app/comments/comment.service.ts rename to src/app/apis/comment.service.ts index 3ff0beb..f0c1f1e 100644 --- a/src/app/comments/comment.service.ts +++ b/src/app/apis/comment.service.ts @@ -2,8 +2,8 @@ import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { catchError, Observable, throwError } from "rxjs"; import { environment } from "src/environments/environment"; -import { Post } from "../posts/post.model"; -import { Comment } from "src/app/comments/comment.model"; +import { Post } from "../types/post.model"; +import { Comment } from "../types/comment.model"; @Injectable({ providedIn: 'root' diff --git a/src/app/posts/post.service.spec.ts b/src/app/apis/post.service.spec.ts similarity index 97% rename from src/app/posts/post.service.spec.ts rename to src/app/apis/post.service.spec.ts index 0274208..0976fd1 100644 --- a/src/app/posts/post.service.spec.ts +++ b/src/app/apis/post.service.spec.ts @@ -3,7 +3,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { PostService } from './post.service'; import { environment } from 'src/environments/environment'; -import { Post } from './post.model'; +import { Post } from '../types/post.model'; describe('PostsService', () => { let service: PostService; diff --git a/src/app/posts/post.service.ts b/src/app/apis/post.service.ts similarity index 94% rename from src/app/posts/post.service.ts rename to src/app/apis/post.service.ts index 796b55b..a7c06e9 100644 --- a/src/app/posts/post.service.ts +++ b/src/app/apis/post.service.ts @@ -1,8 +1,8 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Post } from './post.model'; import { environment } from 'src/environments/environment'; import { catchError, Observable, throwError } from 'rxjs'; +import { Post } from '../types/post.model'; @Injectable({ providedIn: 'root' diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 2d957ac..0c41c89 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,18 +1,18 @@ import { Routes } from '@angular/router'; -import { PostService } from './posts/post.service'; -import { GuestbookPageComponent } from './guestbook/guestbook-page/guestbook-page.component'; -import { provideState } from '@ngrx/store'; -import { postsFeature } from './posts/state/posts.reducer'; import { provideEffects } from '@ngrx/effects'; -import { PostsEffects } from './posts/state/posts.effects'; -import { CommentsEffects } from './comments/state/comments.effects'; -import { commentsFeature } from './comments/state/comments.reducer'; +import { provideState } from '@ngrx/store'; +import { PostService } from './apis/post.service'; +import { postsFeature } from './store/post/posts.reducer'; +import { commentsFeature } from './store/comment/comments.reducer'; +import { PostsEffects } from './store/post/posts.effects'; +import { CommentsEffects } from './store/comment/comments.effects'; +import { GuestbookPageComponent } from './features/guestbook/guestbook-page/guestbook-page.component'; export const routes: Routes = [ { path: '', loadChildren: () => - import('./posts/posts.routes').then((mod) => mod.routes), + import('./features/posts/posts.routes').then((mod) => mod.routes), providers: [ PostService, provideState(postsFeature), diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.html b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html similarity index 100% rename from src/app/guestbook/guestbook-edit/guestbook-edit.component.html rename to src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.scss b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss similarity index 100% rename from src/app/guestbook/guestbook-edit/guestbook-edit.component.scss rename to src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.spec.ts b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts similarity index 100% rename from src/app/guestbook/guestbook-edit/guestbook-edit.component.spec.ts rename to src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts diff --git a/src/app/guestbook/guestbook-edit/guestbook-edit.component.ts b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts similarity index 100% rename from src/app/guestbook/guestbook-edit/guestbook-edit.component.ts rename to src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html new file mode 100644 index 0000000..122fe5a --- /dev/null +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html @@ -0,0 +1 @@ +

guestbook-list works!

diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.scss b/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss similarity index 100% rename from src/app/guestbook/guestbook-page/guestbook-page.component.scss rename to src/app/features/guestbook/guestbook-list/guestbook-list.component.scss diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.spec.ts b/src/app/features/guestbook/guestbook-list/guestbook-list.component.spec.ts new file mode 100644 index 0000000..3e70731 --- /dev/null +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GuestbookListComponent } from './guestbook-list.component'; + +describe('GuestbookListComponent', () => { + let component: GuestbookListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GuestbookListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GuestbookListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts new file mode 100644 index 0000000..53ceb06 --- /dev/null +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-guestbook-list', + imports: [], + templateUrl: './guestbook-list.component.html', + styleUrl: './guestbook-list.component.scss' +}) +export class GuestbookListComponent { + +} diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.html b/src/app/features/guestbook/guestbook-page/guestbook-page.component.html similarity index 100% rename from src/app/guestbook/guestbook-page/guestbook-page.component.html rename to src/app/features/guestbook/guestbook-page/guestbook-page.component.html diff --git a/src/app/posts/posts-page/posts-page.component.scss b/src/app/features/guestbook/guestbook-page/guestbook-page.component.scss similarity index 100% rename from src/app/posts/posts-page/posts-page.component.scss rename to src/app/features/guestbook/guestbook-page/guestbook-page.component.scss diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.spec.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts similarity index 100% rename from src/app/guestbook/guestbook-page/guestbook-page.component.spec.ts rename to src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts diff --git a/src/app/guestbook/guestbook-page/guestbook-page.component.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts similarity index 100% rename from src/app/guestbook/guestbook-page/guestbook-page.component.ts rename to src/app/features/guestbook/guestbook-page/guestbook-page.component.ts diff --git a/src/app/features/guestbook/guestbook.model.ts b/src/app/features/guestbook/guestbook.model.ts new file mode 100644 index 0000000..b2af408 --- /dev/null +++ b/src/app/features/guestbook/guestbook.model.ts @@ -0,0 +1,7 @@ +export interface Guestbook { + id: number; + name: string; + email: string; + message: string; + createdAt: Date; +} \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook.service.spec.ts b/src/app/features/guestbook/guestbook.service.spec.ts new file mode 100644 index 0000000..828d92c --- /dev/null +++ b/src/app/features/guestbook/guestbook.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { GuestbookService } from './guestbook.service'; + +describe('GuestbookService', () => { + let service: GuestbookService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuestbookService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/guestbook/guestbook.service.ts b/src/app/features/guestbook/guestbook.service.ts new file mode 100644 index 0000000..e6faeb5 --- /dev/null +++ b/src/app/features/guestbook/guestbook.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class GuestbookService { + + constructor() { } +} diff --git a/src/app/posts/post-detail/post-detail.component.html b/src/app/features/posts/post-detail/post-detail.component.html similarity index 100% rename from src/app/posts/post-detail/post-detail.component.html rename to src/app/features/posts/post-detail/post-detail.component.html diff --git a/src/app/posts/post-detail/post-detail.component.scss b/src/app/features/posts/post-detail/post-detail.component.scss similarity index 100% rename from src/app/posts/post-detail/post-detail.component.scss rename to src/app/features/posts/post-detail/post-detail.component.scss diff --git a/src/app/posts/post-detail/post-detail.component.spec.ts b/src/app/features/posts/post-detail/post-detail.component.spec.ts similarity index 100% rename from src/app/posts/post-detail/post-detail.component.spec.ts rename to src/app/features/posts/post-detail/post-detail.component.spec.ts diff --git a/src/app/posts/post-detail/post-detail.component.ts b/src/app/features/posts/post-detail/post-detail.component.ts similarity index 82% rename from src/app/posts/post-detail/post-detail.component.ts rename to src/app/features/posts/post-detail/post-detail.component.ts index b509703..23c2406 100644 --- a/src/app/posts/post-detail/post-detail.component.ts +++ b/src/app/features/posts/post-detail/post-detail.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { NgFor, NgIf } from '@angular/common'; -import { Post } from '../post.model'; import { Store } from '@ngrx/store'; -import { Comment } from 'src/app/comments/comment.model'; import { MatIconModule } from '@angular/material/icon'; import { MatButton } from '@angular/material/button'; import { Router } from '@angular/router'; -import { selectCommentsLoading } from 'src/app/comments/state/comments.selectors'; +import { selectCommentsLoading } from 'src/app/store/comment/comments.selectors'; +import { Comment } from 'src/app/types/comment.model'; +import { Post } from 'src/app/types/post.model'; @Component({ selector: 'app-posts-detail', diff --git a/src/app/posts/post-page/post-page.component.html b/src/app/features/posts/post-page/post-page.component.html similarity index 100% rename from src/app/posts/post-page/post-page.component.html rename to src/app/features/posts/post-page/post-page.component.html diff --git a/src/app/posts/post-page/post-page.component.scss b/src/app/features/posts/post-page/post-page.component.scss similarity index 100% rename from src/app/posts/post-page/post-page.component.scss rename to src/app/features/posts/post-page/post-page.component.scss diff --git a/src/app/posts/post-page/post-page.component.spec.ts b/src/app/features/posts/post-page/post-page.component.spec.ts similarity index 96% rename from src/app/posts/post-page/post-page.component.spec.ts rename to src/app/features/posts/post-page/post-page.component.spec.ts index 2ab9e57..88e4ca1 100644 --- a/src/app/posts/post-page/post-page.component.spec.ts +++ b/src/app/features/posts/post-page/post-page.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostPageComponent } from './post-page.component'; import { provideMockStore } from '@ngrx/store/testing'; -import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; +import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from '../../../store/post/posts.selectors'; import { selectComments } from 'src/app/comments/state/comments.selectors'; diff --git a/src/app/posts/post-page/post-page.component.ts b/src/app/features/posts/post-page/post-page.component.ts similarity index 76% rename from src/app/posts/post-page/post-page.component.ts rename to src/app/features/posts/post-page/post-page.component.ts index e1fc183..c297420 100644 --- a/src/app/posts/post-page/post-page.component.ts +++ b/src/app/features/posts/post-page/post-page.component.ts @@ -1,10 +1,10 @@ import { Component, effect } from '@angular/core'; import { Store } from '@ngrx/store'; -import { selectPostsErrorMessage, selectPostsLoading, selectPostById } from '../state/posts.selectors'; import { NgIf } from '@angular/common'; +import { selectComments } from 'src/app/store/comment/comments.selectors'; +import { CommentsComponentActions } from 'src/app/store/comment/comments.actions'; import { PostsDetailComponent } from '../post-detail/post-detail.component'; -import { selectComments } from 'src/app/comments/state/comments.selectors'; -import { CommentsComponentActions } from 'src/app/comments/state/comments.actions'; +import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from 'src/app/store/post/posts.selectors'; @Component({ selector: 'app-post-page', diff --git a/src/app/posts/posts-list/posts-list.component.html b/src/app/features/posts/posts-list/posts-list.component.html similarity index 100% rename from src/app/posts/posts-list/posts-list.component.html rename to src/app/features/posts/posts-list/posts-list.component.html diff --git a/src/app/posts/posts-list/posts-list.component.scss b/src/app/features/posts/posts-list/posts-list.component.scss similarity index 100% rename from src/app/posts/posts-list/posts-list.component.scss rename to src/app/features/posts/posts-list/posts-list.component.scss diff --git a/src/app/posts/posts-list/posts-list.component.spec.ts b/src/app/features/posts/posts-list/posts-list.component.spec.ts similarity index 100% rename from src/app/posts/posts-list/posts-list.component.spec.ts rename to src/app/features/posts/posts-list/posts-list.component.spec.ts diff --git a/src/app/posts/posts-list/posts-list.component.ts b/src/app/features/posts/posts-list/posts-list.component.ts similarity index 92% rename from src/app/posts/posts-list/posts-list.component.ts rename to src/app/features/posts/posts-list/posts-list.component.ts index 4b94322..1c74973 100644 --- a/src/app/posts/posts-list/posts-list.component.ts +++ b/src/app/features/posts/posts-list/posts-list.component.ts @@ -1,9 +1,9 @@ import { Component, Input } from '@angular/core'; -import { Post } from '../post.model'; import { NgFor } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatButton } from '@angular/material/button'; import { Router } from '@angular/router'; +import { Post } from 'src/app/types/post.model'; @Component({ selector: 'app-posts-list', diff --git a/src/app/posts/posts-page/posts-page.component.html b/src/app/features/posts/posts-page/posts-page.component.html similarity index 100% rename from src/app/posts/posts-page/posts-page.component.html rename to src/app/features/posts/posts-page/posts-page.component.html diff --git a/src/app/features/posts/posts-page/posts-page.component.scss b/src/app/features/posts/posts-page/posts-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/posts/posts-page/posts-page.component.spec.ts b/src/app/features/posts/posts-page/posts-page.component.spec.ts similarity index 97% rename from src/app/posts/posts-page/posts-page.component.spec.ts rename to src/app/features/posts/posts-page/posts-page.component.spec.ts index 485482e..3b188b3 100644 --- a/src/app/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/features/posts/posts-page/posts-page.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostsPageComponent } from './posts-page.component'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; +import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../../../store/post/posts.selectors'; describe('PostsPageComponent', () => { let component: PostsPageComponent; diff --git a/src/app/posts/posts-page/posts-page.component.ts b/src/app/features/posts/posts-page/posts-page.component.ts similarity index 93% rename from src/app/posts/posts-page/posts-page.component.ts rename to src/app/features/posts/posts-page/posts-page.component.ts index 41acf61..690e561 100644 --- a/src/app/posts/posts-page/posts-page.component.ts +++ b/src/app/features/posts/posts-page/posts-page.component.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; -import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../state/posts.selectors'; import { Store } from '@ngrx/store'; import { NgIf } from '@angular/common'; import { PostsListComponent } from '../posts-list/posts-list.component'; +import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from 'src/app/store/post/posts.selectors'; @Component({ selector: 'app-posts-page', diff --git a/src/app/posts/posts.routes.ts b/src/app/features/posts/posts.routes.ts similarity index 100% rename from src/app/posts/posts.routes.ts rename to src/app/features/posts/posts.routes.ts diff --git a/src/app/comments/state/comments.actions.ts b/src/app/store/comment/comments.actions.ts similarity index 82% rename from src/app/comments/state/comments.actions.ts rename to src/app/store/comment/comments.actions.ts index 89432b8..22fda9b 100644 --- a/src/app/comments/state/comments.actions.ts +++ b/src/app/store/comment/comments.actions.ts @@ -1,6 +1,6 @@ import { createActionGroup, props } from "@ngrx/store"; -import { Post } from "src/app/posts/post.model"; -import { Comment } from "src/app/comments/comment.model"; +import { Comment } from "src/app/types/comment.model"; +import { Post } from "src/app/types/post.model"; export const CommentsComponentActions = createActionGroup({ source: 'Comments Component', diff --git a/src/app/comments/state/comments.effects.ts b/src/app/store/comment/comments.effects.ts similarity index 93% rename from src/app/comments/state/comments.effects.ts rename to src/app/store/comment/comments.effects.ts index 4ee5177..0e85e52 100644 --- a/src/app/comments/state/comments.effects.ts +++ b/src/app/store/comment/comments.effects.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; import { catchError, concatMap, map, of } from "rxjs"; -import { CommentService } from "../comment.service"; import { CommentsAPIActions, CommentsComponentActions } from "./comments.actions"; +import { CommentService } from "src/app/apis/comment.service"; @Injectable() export class CommentsEffects { diff --git a/src/app/comments/state/comments.reducer.ts b/src/app/store/comment/comments.reducer.ts similarity index 96% rename from src/app/comments/state/comments.reducer.ts rename to src/app/store/comment/comments.reducer.ts index 020e8f2..1fae1a1 100644 --- a/src/app/comments/state/comments.reducer.ts +++ b/src/app/store/comment/comments.reducer.ts @@ -1,7 +1,7 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { CommentsAPIActions, CommentsComponentActions } from "./comments.actions"; -import { Comment } from "src/app/comments/comment.model"; import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; +import { Comment } from "src/app/types/comment.model"; export interface CommentsState extends EntityState { loading: boolean; diff --git a/src/app/comments/state/comments.selectors.ts b/src/app/store/comment/comments.selectors.ts similarity index 100% rename from src/app/comments/state/comments.selectors.ts rename to src/app/store/comment/comments.selectors.ts diff --git a/src/app/posts/state/posts.actions.ts b/src/app/store/post/posts.actions.ts similarity index 90% rename from src/app/posts/state/posts.actions.ts rename to src/app/store/post/posts.actions.ts index 59a5271..f33a896 100644 --- a/src/app/posts/state/posts.actions.ts +++ b/src/app/store/post/posts.actions.ts @@ -1,5 +1,5 @@ import { createActionGroup, emptyProps, props } from "@ngrx/store"; -import { Post } from "../post.model"; +import { Post } from "../../types/post.model"; export const PostsPageActions = createActionGroup({ source: 'Posts Page', diff --git a/src/app/posts/state/posts.effects.ts b/src/app/store/post/posts.effects.ts similarity index 94% rename from src/app/posts/state/posts.effects.ts rename to src/app/store/post/posts.effects.ts index 26093bb..fa56c3f 100644 --- a/src/app/posts/state/posts.effects.ts +++ b/src/app/store/post/posts.effects.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { PostService } from "../post.service"; import { PostsAPIActions, PostsPageActions } from "./posts.actions"; import { catchError, concatMap, map, of } from "rxjs"; +import { PostService } from "src/app/apis/post.service"; @Injectable() export class PostsEffects { diff --git a/src/app/posts/state/posts.reducer.ts b/src/app/store/post/posts.reducer.ts similarity index 97% rename from src/app/posts/state/posts.reducer.ts rename to src/app/store/post/posts.reducer.ts index af1f800..b92374e 100644 --- a/src/app/posts/state/posts.reducer.ts +++ b/src/app/store/post/posts.reducer.ts @@ -1,6 +1,6 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { PostsAPIActions, PostsPageActions } from "./posts.actions"; -import { Post } from "../post.model"; +import { Post } from "../../types/post.model"; import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; export interface PostsState extends EntityState { diff --git a/src/app/posts/state/posts.selectors.ts b/src/app/store/post/posts.selectors.ts similarity index 100% rename from src/app/posts/state/posts.selectors.ts rename to src/app/store/post/posts.selectors.ts diff --git a/src/app/comments/comment.model.ts b/src/app/types/comment.model.ts similarity index 100% rename from src/app/comments/comment.model.ts rename to src/app/types/comment.model.ts diff --git a/src/app/posts/post.model.ts b/src/app/types/post.model.ts similarity index 100% rename from src/app/posts/post.model.ts rename to src/app/types/post.model.ts From a656ef132afd970c1f6585f02c68e98b8ef4a0c4 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 16:41:53 +0300 Subject: [PATCH 33/36] Implement guestbook feature with NgRx state management, effects, and service; update components and templates for guestbook entries display --- src/app/app-routes.ts | 17 ++++-- .../guestbook-list.component.html | 13 ++++- .../guestbook-list.component.scss | 3 + .../guestbook-list.component.ts | 9 ++- .../guestbook-page.component.html | 14 ++++- .../guestbook-page.component.spec.ts | 32 ++++++++++- .../guestbook-page.component.ts | 12 +++- .../features/guestbook/guestbook.service.ts | 29 +++++++++- .../posts/post-page/post-page.component.html | 2 + .../post-page/post-page.component.spec.ts | 4 +- .../posts-list/posts-list.component.html | 2 +- .../posts/posts-list/posts-list.component.ts | 9 +-- .../posts-page/posts-page.component.html | 2 + .../posts-page/posts-page.component.spec.ts | 8 ++- src/app/store/guestbook/guestbook.actions.ts | 21 +++++++ src/app/store/guestbook/guestbook.effects.ts | 43 +++++++++++++++ src/app/store/guestbook/guestbook.reducer.ts | 55 +++++++++++++++++++ src/app/store/guestbook/guestbook.selector.ts | 19 +++++++ .../guestbook => types}/guestbook.model.ts | 2 +- 19 files changed, 269 insertions(+), 27 deletions(-) create mode 100644 src/app/store/guestbook/guestbook.actions.ts create mode 100644 src/app/store/guestbook/guestbook.effects.ts create mode 100644 src/app/store/guestbook/guestbook.reducer.ts create mode 100644 src/app/store/guestbook/guestbook.selector.ts rename src/app/{features/guestbook => types}/guestbook.model.ts (71%) diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 0c41c89..0933a0a 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -7,8 +7,20 @@ import { commentsFeature } from './store/comment/comments.reducer'; import { PostsEffects } from './store/post/posts.effects'; import { CommentsEffects } from './store/comment/comments.effects'; import { GuestbookPageComponent } from './features/guestbook/guestbook-page/guestbook-page.component'; +import { GuestbookService } from './features/guestbook/guestbook.service'; +import { guestbookFeature } from './store/guestbook/guestbook.reducer'; +import { GuestbookEffects } from './store/guestbook/guestbook.effects'; export const routes: Routes = [ + { + path: 'guestbook', + component: GuestbookPageComponent, + providers: [ + GuestbookService, + provideState(guestbookFeature), + provideEffects(GuestbookEffects), + ], + }, { path: '', loadChildren: () => @@ -20,9 +32,4 @@ export const routes: Routes = [ provideEffects(PostsEffects, CommentsEffects), ], }, - { - path: 'guestbook', - component: GuestbookPageComponent - , - }, ]; diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html index 122fe5a..c75bd75 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html @@ -1 +1,12 @@ -

guestbook-list works!

+
+
+ + + {{ gPost.name }} + + +

{{ gPost.message }}

+
+
+
+
\ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss b/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss index e69de29..348cafa 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss @@ -0,0 +1,3 @@ +.guest-post-item { + padding: 8px; +} \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts index 53ceb06..5825619 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts @@ -1,11 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { GuestbookEntry } from '../../../types/guestbook.model'; +import { NgFor } from '@angular/common'; +import { MatCardModule } from '@angular/material/card'; @Component({ selector: 'app-guestbook-list', - imports: [], + imports: [NgFor, MatCardModule], templateUrl: './guestbook-list.component.html', styleUrl: './guestbook-list.component.scss' }) export class GuestbookListComponent { - + @Input() guestPosts: GuestbookEntry[] | null = []; } diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.html b/src/app/features/guestbook/guestbook-page/guestbook-page.component.html index 2f592cd..d211ebe 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.html +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.html @@ -1 +1,13 @@ -

guestbook-page works!

+

Guest Book

+ +
+ Error: {{ errorMessage }} +
+ +
+
+ +
+
+ +Loading... \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts index 99a7f9f..59a9246 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts @@ -1,17 +1,45 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { GuestbookPageComponent } from './guestbook-page.component'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectGuestbookEntries } from 'src/app/store/guestbook/guestbook.selector'; describe('GuestbookPageComponent', () => { let component: GuestbookPageComponent; let fixture: ComponentFixture; + let store: MockStore; + + const mockGuestbookEntries = [ + { id: 1, name: 'Test Guest 1', message: 'Test Message 1' }, + { id: 2, name: 'Test Guest 2', message: 'Test Message 2' } + ]; + + const initialState = { + guestbook: { + loading: false, + errorMessage: '', + ids: [1, 2], + entities: { + 1: mockGuestbookEntries[0], + 2: mockGuestbookEntries[1] + } + } + }; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GuestbookPageComponent] + imports: [GuestbookPageComponent], + providers: [ + provideMockStore({ + initialState, + selectors: [ + { selector: selectGuestbookEntries, value: mockGuestbookEntries } + ] + }) + ] }) .compileComponents(); + store = TestBed.inject(MockStore); fixture = TestBed.createComponent(GuestbookPageComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts index be6e986..4dd1931 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts @@ -1,11 +1,21 @@ +import { NgIf } from '@angular/common'; import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { GuestbookListComponent } from '../guestbook-list/guestbook-list.component'; +import { selectGuestbookEntries } from 'src/app/store/guestbook/guestbook.selector'; +import { MatCardModule } from '@angular/material/card'; @Component({ selector: 'app-guestbook-page', - imports: [], + imports: [NgIf, MatCardModule, GuestbookListComponent], templateUrl: './guestbook-page.component.html', styleUrl: './guestbook-page.component.scss' }) export class GuestbookPageComponent { + guestPosts = this.store.selectSignal(selectGuestbookEntries); + loading = false; + errorMessage = ''; + + constructor(private store: Store) { } } diff --git a/src/app/features/guestbook/guestbook.service.ts b/src/app/features/guestbook/guestbook.service.ts index e6faeb5..ec3048e 100644 --- a/src/app/features/guestbook/guestbook.service.ts +++ b/src/app/features/guestbook/guestbook.service.ts @@ -1,9 +1,36 @@ import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { GuestbookEntry } from 'src/app/types/guestbook.model'; @Injectable({ providedIn: 'root' }) export class GuestbookService { + private entries: GuestbookEntry[] = [ + { + id: 876, + name: 'John Doe', + email: 'john@example.com', + message: 'Hello world!', + createdAt: new Date() + }, + { + id: 343, + name: 'Jane Smith', + email: 'jane@example.com', + message: 'This is a test message.', + createdAt: new Date() + } + ]; + + loadEntries(): Observable { + return of(this.entries); + } + + add(entry: GuestbookEntry): Observable { + entry = { ...entry, id: Math.floor(Math.random() * 1000), createdAt: new Date() }; + this.entries.push(entry); + return of(entry); + } - constructor() { } } diff --git a/src/app/features/posts/post-page/post-page.component.html b/src/app/features/posts/post-page/post-page.component.html index a80d1c7..0b2c380 100644 --- a/src/app/features/posts/post-page/post-page.component.html +++ b/src/app/features/posts/post-page/post-page.component.html @@ -1,3 +1,5 @@ +

Post Details

+
Error: {{ errorMessage() }}
diff --git a/src/app/features/posts/post-page/post-page.component.spec.ts b/src/app/features/posts/post-page/post-page.component.spec.ts index 88e4ca1..ca184ef 100644 --- a/src/app/features/posts/post-page/post-page.component.spec.ts +++ b/src/app/features/posts/post-page/post-page.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostPageComponent } from './post-page.component'; import { provideMockStore } from '@ngrx/store/testing'; -import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from '../../../store/post/posts.selectors'; -import { selectComments } from 'src/app/comments/state/comments.selectors'; +import { selectComments } from 'src/app/store/comment/comments.selectors'; +import { selectPostById, selectPostsErrorMessage, selectPostsLoading } from 'src/app/store/post/posts.selectors'; describe('PostPageComponent', () => { diff --git a/src/app/features/posts/posts-list/posts-list.component.html b/src/app/features/posts/posts-list/posts-list.component.html index 17ff1c4..a31f603 100644 --- a/src/app/features/posts/posts-list/posts-list.component.html +++ b/src/app/features/posts/posts-list/posts-list.component.html @@ -8,7 +8,7 @@

{{ post.body }}

- diff --git a/src/app/features/posts/posts-list/posts-list.component.ts b/src/app/features/posts/posts-list/posts-list.component.ts index 1c74973..2228f34 100644 --- a/src/app/features/posts/posts-list/posts-list.component.ts +++ b/src/app/features/posts/posts-list/posts-list.component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { NgFor } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatButton } from '@angular/material/button'; -import { Router } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { Post } from 'src/app/types/post.model'; @Component({ @@ -11,16 +11,11 @@ import { Post } from 'src/app/types/post.model'; NgFor, MatCardModule, MatButton, + RouterLink ], templateUrl: './posts-list.component.html', styleUrl: './posts-list.component.scss' }) export class PostsListComponent { @Input() posts: Post[] | null = []; - - constructor(private router: Router) { } - - showPost(post: Post) { - this.router.navigate(['./post', post.id]); - } } diff --git a/src/app/features/posts/posts-page/posts-page.component.html b/src/app/features/posts/posts-page/posts-page.component.html index c845981..33a85b5 100644 --- a/src/app/features/posts/posts-page/posts-page.component.html +++ b/src/app/features/posts/posts-page/posts-page.component.html @@ -1,3 +1,5 @@ +

Posts

+
Error: {{ errorMessage() }}
diff --git a/src/app/features/posts/posts-page/posts-page.component.spec.ts b/src/app/features/posts/posts-page/posts-page.component.spec.ts index 3b188b3..b44109e 100644 --- a/src/app/features/posts/posts-page/posts-page.component.spec.ts +++ b/src/app/features/posts/posts-page/posts-page.component.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { PostsPageComponent } from './posts-page.component'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from '../../../store/post/posts.selectors'; +import { selectPosts, selectPostsErrorMessage, selectPostsLoading } from 'src/app/store/post/posts.selectors'; describe('PostsPageComponent', () => { let component: PostsPageComponent; @@ -27,7 +28,10 @@ describe('PostsPageComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PostsPageComponent], + imports: [ + PostsPageComponent, + RouterTestingModule + ], providers: [ provideMockStore({ initialState, diff --git a/src/app/store/guestbook/guestbook.actions.ts b/src/app/store/guestbook/guestbook.actions.ts new file mode 100644 index 0000000..3025080 --- /dev/null +++ b/src/app/store/guestbook/guestbook.actions.ts @@ -0,0 +1,21 @@ +import { createActionGroup, emptyProps, props } from "@ngrx/store"; +import { GuestbookEntry } from "src/app/types/guestbook.model"; + +export const GuestbookPageActions = createActionGroup({ + source: 'Guestbook Page', + events: { + 'Load Guestbook Entries': emptyProps(), + 'Add Guestbook Entry': props<{ entry: GuestbookEntry }>(), + }, +}); + +export const GuestbookAPIActions = createActionGroup({ + source: 'Guestbook API', + events: { + 'Load Guestbook Entries': emptyProps(), + 'Guestbook Entries Loaded Success': props<{ entries: GuestbookEntry[] }>(), + 'Guestbook Entries Loaded Fail': props<{ message: string }>(), + 'Guestbook Entry Added Success': props<{ entry: GuestbookEntry }>(), + 'Guestbook Entry Added Fail': props<{ message: string }>(), + }, +}); \ No newline at end of file diff --git a/src/app/store/guestbook/guestbook.effects.ts b/src/app/store/guestbook/guestbook.effects.ts new file mode 100644 index 0000000..d5acbaa --- /dev/null +++ b/src/app/store/guestbook/guestbook.effects.ts @@ -0,0 +1,43 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { GuestbookAPIActions, GuestbookPageActions } from "./guestbook.actions"; +import { catchError, concatMap, map, of } from 'rxjs'; +import { GuestbookService } from "src/app/features/guestbook/guestbook.service"; + + +@Injectable() +export class GuestbookEffects { + + ngrxOnInitEffects() { + return GuestbookPageActions.loadGuestbookEntries(); + } + + constructor( + private guestbookService: GuestbookService, + private actions$: Actions + ) { } + + loadGuestbookEntries$ = createEffect(() => + this.actions$.pipe( + ofType(GuestbookPageActions.loadGuestbookEntries), + concatMap(() => + this.guestbookService.loadEntries().pipe( + map(entries => GuestbookAPIActions.guestbookEntriesLoadedSuccess({ entries })), + catchError(message => of(GuestbookAPIActions.guestbookEntriesLoadedFail({ message }))) + ) + ) + ) + ); + + addGuestbookEntry$ = createEffect(() => + this.actions$.pipe( + ofType(GuestbookPageActions.addGuestbookEntry), + concatMap(({ entry }) => + this.guestbookService.add(entry).pipe( + map((newEntry) => GuestbookAPIActions.guestbookEntryAddedSuccess({ entry: newEntry })), + catchError((error) => of(GuestbookAPIActions.guestbookEntryAddedFail({ message: error }))) + ) + ) + ) + ); +} \ No newline at end of file diff --git a/src/app/store/guestbook/guestbook.reducer.ts b/src/app/store/guestbook/guestbook.reducer.ts new file mode 100644 index 0000000..2c760a5 --- /dev/null +++ b/src/app/store/guestbook/guestbook.reducer.ts @@ -0,0 +1,55 @@ +import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; +import { createFeature, createReducer, on } from "@ngrx/store"; +import { GuestbookEntry } from "src/app/types/guestbook.model"; +import { GuestbookAPIActions, GuestbookPageActions } from "./guestbook.actions"; + +export interface GuestbookState extends EntityState { + loading: boolean; + errorMessage: string; +} + +const adapter: EntityAdapter = createEntityAdapter({}); + +const initialState: GuestbookState = adapter.getInitialState({ + loading: false, + errorMessage: '', +}); + +const { selectAll } = adapter.getSelectors(); + +export const selectAllGuestbookEntries = selectAll; + +export const guestbookFeature = createFeature({ + name: 'guestbook', + reducer: createReducer( + initialState, + on(GuestbookPageActions.loadGuestbookEntries, (state) => + adapter.setAll([], { + ...state, + loading: true, + errorMessage: '', + })), + on(GuestbookAPIActions.guestbookEntriesLoadedSuccess, (state, { entries }) => + adapter.setAll(entries, { + ...state, + loading: false, + })), + on(GuestbookAPIActions.guestbookEntriesLoadedFail, (state, { message }) => + adapter.setAll([], { + ...state, + errorMessage: message, + loading: false, + })), + on(GuestbookAPIActions.guestbookEntryAddedSuccess, (state, { entry }) => + adapter.addOne(entry, { + ...state, + loading: false, + }) + ), + on(GuestbookAPIActions.guestbookEntryAddedFail, (state, { message }) => ({ + ...state, + loading: false, + errorMessage: message, + })), + ), +}); \ No newline at end of file diff --git a/src/app/store/guestbook/guestbook.selector.ts b/src/app/store/guestbook/guestbook.selector.ts new file mode 100644 index 0000000..967775c --- /dev/null +++ b/src/app/store/guestbook/guestbook.selector.ts @@ -0,0 +1,19 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import * as fromGuestbook from './guestbook.reducer'; + +export const selectGuestbookState = createFeatureSelector('guestbook'); + +export const selectGuestbookLoading = createSelector( + selectGuestbookState, + ({ loading }) => loading +); + +export const selectGuestbookErrorMessage = createSelector( + selectGuestbookState, + ({ errorMessage }) => errorMessage +); + +export const selectGuestbookEntries = createSelector( + selectGuestbookState, + fromGuestbook.selectAllGuestbookEntries +); \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook.model.ts b/src/app/types/guestbook.model.ts similarity index 71% rename from src/app/features/guestbook/guestbook.model.ts rename to src/app/types/guestbook.model.ts index b2af408..b0a80d7 100644 --- a/src/app/features/guestbook/guestbook.model.ts +++ b/src/app/types/guestbook.model.ts @@ -1,4 +1,4 @@ -export interface Guestbook { +export interface GuestbookEntry { id: number; name: string; email: string; From 94e92d74605e3166c67253af49b6ecaa8420b84b Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Wed, 28 May 2025 23:23:09 +0300 Subject: [PATCH 34/36] Refactor guestbook page component tests; remove unused MockStore import and variable --- .../guestbook-page/guestbook-page.component.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts index 59a9246..a1a190a 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts @@ -1,12 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GuestbookPageComponent } from './guestbook-page.component'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { provideMockStore } from '@ngrx/store/testing'; import { selectGuestbookEntries } from 'src/app/store/guestbook/guestbook.selector'; describe('GuestbookPageComponent', () => { let component: GuestbookPageComponent; let fixture: ComponentFixture; - let store: MockStore; const mockGuestbookEntries = [ { id: 1, name: 'Test Guest 1', message: 'Test Message 1' }, @@ -37,9 +36,8 @@ describe('GuestbookPageComponent', () => { }) ] }) - .compileComponents(); + .compileComponents(); - store = TestBed.inject(MockStore); fixture = TestBed.createComponent(GuestbookPageComponent); component = fixture.componentInstance; fixture.detectChanges(); From c8b0cb70006c86f2959935a25db84e975a733517 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Thu, 29 May 2025 13:21:01 +0300 Subject: [PATCH 35/36] Refactor guestbook and post services; update model imports and restructure guestbook entry handling --- src/app/apis/comment.service.spec.ts | 4 +- src/app/apis/comment.service.ts | 4 +- src/app/apis/post.service.spec.ts | 2 +- src/app/apis/post.service.ts | 2 +- src/app/app-routes.ts | 4 -- .../guestbook-edit.component.html | 43 ++++++++++++++++++- .../guestbook-edit.component.scss | 12 ++++++ .../guestbook-edit.component.spec.ts | 34 ++++++++++++++- .../guestbook-edit.component.ts | 40 ++++++++++++++++- .../guestbook-list.component.html | 2 +- .../guestbook-list.component.ts | 2 +- .../guestbook-page.component.html | 11 +++-- .../guestbook-page.component.scss | 3 ++ .../guestbook-page.component.spec.ts | 4 +- .../guestbook-page.component.ts | 24 ++++++++--- .../features/guestbook/guestbook.service.ts | 36 ---------------- .../services/guestbook-dialog.service.spec.ts | 16 +++++++ .../services/guestbook-dialog.service.ts | 34 +++++++++++++++ .../{ => services}/guestbook.service.spec.ts | 0 .../guestbook/services/guestbook.service.ts | 39 +++++++++++++++++ .../post-detail/post-detail.component.ts | 4 +- .../posts/posts-list/posts-list.component.ts | 2 +- src/app/models/author.model.ts | 4 ++ src/app/{types => models}/comment.model.ts | 0 src/app/models/guestbook.model.ts | 14 ++++++ src/app/{types => models}/post.model.ts | 0 src/app/store/comment/comments.actions.ts | 4 +- src/app/store/comment/comments.reducer.ts | 2 +- src/app/store/guestbook/guestbook.actions.ts | 2 +- src/app/store/guestbook/guestbook.effects.ts | 2 +- src/app/store/guestbook/guestbook.reducer.ts | 20 ++++++++- src/app/store/post/posts.actions.ts | 2 +- src/app/store/post/posts.reducer.ts | 2 +- src/app/types/guestbook.model.ts | 7 --- 34 files changed, 301 insertions(+), 80 deletions(-) delete mode 100644 src/app/features/guestbook/guestbook.service.ts create mode 100644 src/app/features/guestbook/services/guestbook-dialog.service.spec.ts create mode 100644 src/app/features/guestbook/services/guestbook-dialog.service.ts rename src/app/features/guestbook/{ => services}/guestbook.service.spec.ts (100%) create mode 100644 src/app/features/guestbook/services/guestbook.service.ts create mode 100644 src/app/models/author.model.ts rename src/app/{types => models}/comment.model.ts (100%) create mode 100644 src/app/models/guestbook.model.ts rename src/app/{types => models}/post.model.ts (100%) delete mode 100644 src/app/types/guestbook.model.ts diff --git a/src/app/apis/comment.service.spec.ts b/src/app/apis/comment.service.spec.ts index 2575ed2..6f4771d 100644 --- a/src/app/apis/comment.service.spec.ts +++ b/src/app/apis/comment.service.spec.ts @@ -2,8 +2,8 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { CommentService } from './comment.service'; import { environment } from 'src/environments/environment'; -import { Post } from '../types/post.model'; -import { Comment } from '../types/comment.model'; +import { Post } from '../models/post.model'; +import { Comment } from '../models/comment.model'; describe('CommentService', () => { let service: CommentService; diff --git a/src/app/apis/comment.service.ts b/src/app/apis/comment.service.ts index f0c1f1e..9dab129 100644 --- a/src/app/apis/comment.service.ts +++ b/src/app/apis/comment.service.ts @@ -2,8 +2,8 @@ import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { catchError, Observable, throwError } from "rxjs"; import { environment } from "src/environments/environment"; -import { Post } from "../types/post.model"; -import { Comment } from "../types/comment.model"; +import { Post } from "../models/post.model"; +import { Comment } from "../models/comment.model"; @Injectable({ providedIn: 'root' diff --git a/src/app/apis/post.service.spec.ts b/src/app/apis/post.service.spec.ts index 0976fd1..4d43b46 100644 --- a/src/app/apis/post.service.spec.ts +++ b/src/app/apis/post.service.spec.ts @@ -3,7 +3,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { PostService } from './post.service'; import { environment } from 'src/environments/environment'; -import { Post } from '../types/post.model'; +import { Post } from '../models/post.model'; describe('PostsService', () => { let service: PostService; diff --git a/src/app/apis/post.service.ts b/src/app/apis/post.service.ts index a7c06e9..d7727da 100644 --- a/src/app/apis/post.service.ts +++ b/src/app/apis/post.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { catchError, Observable, throwError } from 'rxjs'; -import { Post } from '../types/post.model'; +import { Post } from '../models/post.model'; @Injectable({ providedIn: 'root' diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 0933a0a..09ca3b7 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,13 +1,11 @@ import { Routes } from '@angular/router'; import { provideEffects } from '@ngrx/effects'; import { provideState } from '@ngrx/store'; -import { PostService } from './apis/post.service'; import { postsFeature } from './store/post/posts.reducer'; import { commentsFeature } from './store/comment/comments.reducer'; import { PostsEffects } from './store/post/posts.effects'; import { CommentsEffects } from './store/comment/comments.effects'; import { GuestbookPageComponent } from './features/guestbook/guestbook-page/guestbook-page.component'; -import { GuestbookService } from './features/guestbook/guestbook.service'; import { guestbookFeature } from './store/guestbook/guestbook.reducer'; import { GuestbookEffects } from './store/guestbook/guestbook.effects'; @@ -16,7 +14,6 @@ export const routes: Routes = [ path: 'guestbook', component: GuestbookPageComponent, providers: [ - GuestbookService, provideState(guestbookFeature), provideEffects(GuestbookEffects), ], @@ -26,7 +23,6 @@ export const routes: Routes = [ loadChildren: () => import('./features/posts/posts.routes').then((mod) => mod.routes), providers: [ - PostService, provideState(postsFeature), provideState(commentsFeature), provideEffects(PostsEffects, CommentsEffects), diff --git a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html index a3fd30e..6f31799 100644 --- a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html +++ b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.html @@ -1 +1,42 @@ -

guestbook-edit works!

+

Guest Book Entry

+ +
+ + Name + + + Name is required + + + + + Email + + + Email is required + + + Please enter a valid email + + + + + Message + + + Message is required + + + Message must be at least 20 characters long + + +
+
+
+ + + + +
\ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss index e69de29..9d9c67f 100644 --- a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss +++ b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.scss @@ -0,0 +1,12 @@ +.guest-book-entry-button { + display: flex; + justify-content: end; + padding-bottom: 16px; + padding-right: 16px; +} + +.guest-post-form { + display: flex; + flex-direction: column; + gap: 16px; +} \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts index 50090e0..c18f0b6 100644 --- a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts +++ b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.spec.ts @@ -1,14 +1,28 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { GuestbookEditComponent } from './guestbook-edit.component'; +import { GuestbookEntryForm } from 'src/app/models/guestbook.model'; describe('GuestbookEditComponent', () => { let component: GuestbookEditComponent; let fixture: ComponentFixture; + let mockDialogRef: jasmine.SpyObj>; beforeEach(async () => { + mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']); + await TestBed.configureTestingModule({ - imports: [GuestbookEditComponent] + imports: [ + GuestbookEditComponent, + ReactiveFormsModule, + NoopAnimationsModule + ], + providers: [ + { provide: MatDialogRef, useValue: mockDialogRef } + ] }) .compileComponents(); @@ -20,4 +34,22 @@ describe('GuestbookEditComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should submit valid form', () => { + const formData: GuestbookEntryForm = { + name: 'John Doe', + email: 'john@example.com', + message: 'This is a test message This is a test message This is a test message' + }; + + component.guestBookForm.patchValue(formData); + component.onSubmit(); + + expect(mockDialogRef.close).toHaveBeenCalledWith( + jasmine.objectContaining({ + ...formData, + createdAt: jasmine.any(Date) + }) + ); + }); }); diff --git a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts index 96658e7..d0d59ff 100644 --- a/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts +++ b/src/app/features/guestbook/guestbook-edit/guestbook-edit.component.ts @@ -1,11 +1,49 @@ +import { NgIf } from '@angular/common'; import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'app-guestbook-edit', - imports: [], + imports: [ + NgIf, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + ReactiveFormsModule + ], templateUrl: './guestbook-edit.component.html', styleUrl: './guestbook-edit.component.scss' }) export class GuestbookEditComponent { + guestBookForm: FormGroup; + constructor( + private guestPostEntryForm: FormBuilder, + private guestBookEntryDialogRef: MatDialogRef, + ) { + this.guestBookForm = this.guestPostEntryForm.group({ + name: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + message: ['', [Validators.required, Validators.minLength(20)]] + }); + } + + onSubmit(): void { + if (this.guestBookForm.valid) { + const entry = { + ...this.guestBookForm.value, + createdAt: new Date() + }; + this.guestBookEntryDialogRef.close(entry); + } + } + + onCancel(): void { + this.guestBookEntryDialogRef.close(); + } } diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html index c75bd75..2034ab6 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html @@ -2,7 +2,7 @@
- {{ gPost.name }} + {{ gPost.author.name }}

{{ gPost.message }}

diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts index 5825619..4c64a3c 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { GuestbookEntry } from '../../../types/guestbook.model'; +import { GuestbookEntry } from '../../../models/guestbook.model'; import { NgFor } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.html b/src/app/features/guestbook/guestbook-page/guestbook-page.component.html index d211ebe..b293128 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.html +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.html @@ -1,10 +1,15 @@

Guest Book

-
- Error: {{ errorMessage }} +
+ Error: {{ errorMessage() }}
-
+
+ +
+
diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.scss b/src/app/features/guestbook/guestbook-page/guestbook-page.component.scss index e69de29..3dcd486 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.scss +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.scss @@ -0,0 +1,3 @@ +.guest-book-add-entry { + padding-bottom: 16px; +} \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts index a1a190a..3919df7 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.spec.ts @@ -8,8 +8,8 @@ describe('GuestbookPageComponent', () => { let fixture: ComponentFixture; const mockGuestbookEntries = [ - { id: 1, name: 'Test Guest 1', message: 'Test Message 1' }, - { id: 2, name: 'Test Guest 2', message: 'Test Message 2' } + { id: 1, author: {name: 'Test Guest 1'}, message: 'Test Message 1' }, + { id: 2, author: {name: 'Test Guest 2'}, message: 'Test Message 2' } ]; const initialState = { diff --git a/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts b/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts index 4dd1931..1f8e801 100644 --- a/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts +++ b/src/app/features/guestbook/guestbook-page/guestbook-page.component.ts @@ -1,21 +1,35 @@ import { NgIf } from '@angular/common'; import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; +import { MatButtonModule } from '@angular/material/button'; import { GuestbookListComponent } from '../guestbook-list/guestbook-list.component'; -import { selectGuestbookEntries } from 'src/app/store/guestbook/guestbook.selector'; +import { selectGuestbookEntries, selectGuestbookLoading, selectGuestbookErrorMessage } from 'src/app/store/guestbook/guestbook.selector'; +import { GuestbookPageActions } from 'src/app/store/guestbook/guestbook.actions'; import { MatCardModule } from '@angular/material/card'; +import { GuestbookDialogService } from '../services/guestbook-dialog.service'; @Component({ selector: 'app-guestbook-page', - imports: [NgIf, MatCardModule, GuestbookListComponent], + imports: [NgIf, MatCardModule, MatButtonModule, GuestbookListComponent], templateUrl: './guestbook-page.component.html', styleUrl: './guestbook-page.component.scss' }) export class GuestbookPageComponent { guestPosts = this.store.selectSignal(selectGuestbookEntries); - loading = false; - errorMessage = ''; + loading = this.store.selectSignal(selectGuestbookLoading); + errorMessage = this.store.selectSignal(selectGuestbookErrorMessage); - constructor(private store: Store) { } + constructor( + private store: Store, + private guestbookDialogService: GuestbookDialogService + ) { } + openGuestBookDialog(): void { + this.guestbookDialogService.openGuestBookDialog().subscribe((result) => { + if (result) { + const entry = this.guestbookDialogService.createGuestbookEntry(result); + this.store.dispatch(GuestbookPageActions.addGuestbookEntry({ entry })); + } + }); + } } diff --git a/src/app/features/guestbook/guestbook.service.ts b/src/app/features/guestbook/guestbook.service.ts deleted file mode 100644 index ec3048e..0000000 --- a/src/app/features/guestbook/guestbook.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; -import { GuestbookEntry } from 'src/app/types/guestbook.model'; - -@Injectable({ - providedIn: 'root' -}) -export class GuestbookService { - private entries: GuestbookEntry[] = [ - { - id: 876, - name: 'John Doe', - email: 'john@example.com', - message: 'Hello world!', - createdAt: new Date() - }, - { - id: 343, - name: 'Jane Smith', - email: 'jane@example.com', - message: 'This is a test message.', - createdAt: new Date() - } - ]; - - loadEntries(): Observable { - return of(this.entries); - } - - add(entry: GuestbookEntry): Observable { - entry = { ...entry, id: Math.floor(Math.random() * 1000), createdAt: new Date() }; - this.entries.push(entry); - return of(entry); - } - -} diff --git a/src/app/features/guestbook/services/guestbook-dialog.service.spec.ts b/src/app/features/guestbook/services/guestbook-dialog.service.spec.ts new file mode 100644 index 0000000..bdd8185 --- /dev/null +++ b/src/app/features/guestbook/services/guestbook-dialog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; +import { GuestbookDialogService } from './guestbook-dialog.service'; + + +describe('GuestbookDialogService', () => { + let service: GuestbookDialogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuestbookDialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/guestbook/services/guestbook-dialog.service.ts b/src/app/features/guestbook/services/guestbook-dialog.service.ts new file mode 100644 index 0000000..3911746 --- /dev/null +++ b/src/app/features/guestbook/services/guestbook-dialog.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import { GuestbookEditComponent } from '../guestbook-edit/guestbook-edit.component'; +import { GuestbookEntry, GuestbookEntryForm } from 'src/app/models/guestbook.model'; + +@Injectable({ + providedIn: 'root' +}) +export class GuestbookDialogService { + + constructor(private dialog: MatDialog) { } + + openGuestBookDialog(): Observable { + const guestBookDialogRef = this.dialog.open(GuestbookEditComponent, { + width: '500px', + disableClose: true + }); + + return guestBookDialogRef.afterClosed(); + } + + createGuestbookEntry(formResult: GuestbookEntryForm): GuestbookEntry { + return { + id: Math.floor(Math.random() * 1000), + author: { + name: formResult.name, + email: formResult.email + }, + message: formResult.message, + createdAt: new Date() + }; + } +} diff --git a/src/app/features/guestbook/guestbook.service.spec.ts b/src/app/features/guestbook/services/guestbook.service.spec.ts similarity index 100% rename from src/app/features/guestbook/guestbook.service.spec.ts rename to src/app/features/guestbook/services/guestbook.service.spec.ts diff --git a/src/app/features/guestbook/services/guestbook.service.ts b/src/app/features/guestbook/services/guestbook.service.ts new file mode 100644 index 0000000..f05f3de --- /dev/null +++ b/src/app/features/guestbook/services/guestbook.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { GuestbookEntry } from 'src/app/models/guestbook.model'; + +@Injectable({ + providedIn: 'root' +}) +export class GuestbookService { + private entries: GuestbookEntry[] = [ + { + id: 876, + author: { + name: 'John Doe', + email: 'john@example.com' + }, + message: 'This is a test message. This is a test message. This is a test message. This is a test message.', + createdAt: new Date() + }, + { + id: 343, + author: { + name: 'Jane Smith', + email: 'jane@example.com' + }, + message: 'This is a test message. This is a test message. This is a test message. This is a test message.', + createdAt: new Date() + } + ]; + + loadEntries(): Observable { + return of(this.entries); + } + + add(entry: GuestbookEntry): Observable { + this.entries = [...this.entries, entry]; + return of(entry); + } + +} diff --git a/src/app/features/posts/post-detail/post-detail.component.ts b/src/app/features/posts/post-detail/post-detail.component.ts index 23c2406..3d234c1 100644 --- a/src/app/features/posts/post-detail/post-detail.component.ts +++ b/src/app/features/posts/post-detail/post-detail.component.ts @@ -6,8 +6,8 @@ import { MatIconModule } from '@angular/material/icon'; import { MatButton } from '@angular/material/button'; import { Router } from '@angular/router'; import { selectCommentsLoading } from 'src/app/store/comment/comments.selectors'; -import { Comment } from 'src/app/types/comment.model'; -import { Post } from 'src/app/types/post.model'; +import { Comment } from 'src/app/models/comment.model'; +import { Post } from 'src/app/models/post.model'; @Component({ selector: 'app-posts-detail', diff --git a/src/app/features/posts/posts-list/posts-list.component.ts b/src/app/features/posts/posts-list/posts-list.component.ts index 2228f34..3df9e55 100644 --- a/src/app/features/posts/posts-list/posts-list.component.ts +++ b/src/app/features/posts/posts-list/posts-list.component.ts @@ -3,7 +3,7 @@ import { NgFor } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatButton } from '@angular/material/button'; import { RouterLink } from '@angular/router'; -import { Post } from 'src/app/types/post.model'; +import { Post } from 'src/app/models/post.model'; @Component({ selector: 'app-posts-list', diff --git a/src/app/models/author.model.ts b/src/app/models/author.model.ts new file mode 100644 index 0000000..c38d383 --- /dev/null +++ b/src/app/models/author.model.ts @@ -0,0 +1,4 @@ +export interface Author { + name: string; + email: string; +} \ No newline at end of file diff --git a/src/app/types/comment.model.ts b/src/app/models/comment.model.ts similarity index 100% rename from src/app/types/comment.model.ts rename to src/app/models/comment.model.ts diff --git a/src/app/models/guestbook.model.ts b/src/app/models/guestbook.model.ts new file mode 100644 index 0000000..9998a4d --- /dev/null +++ b/src/app/models/guestbook.model.ts @@ -0,0 +1,14 @@ +import { Author } from "./author.model"; + +export interface GuestbookEntry { + id: number; + author: Author + message: string; + createdAt: Date; +} + +export interface GuestbookEntryForm { + name: string, + email: string; + message: string; +} \ No newline at end of file diff --git a/src/app/types/post.model.ts b/src/app/models/post.model.ts similarity index 100% rename from src/app/types/post.model.ts rename to src/app/models/post.model.ts diff --git a/src/app/store/comment/comments.actions.ts b/src/app/store/comment/comments.actions.ts index 22fda9b..0bb54e6 100644 --- a/src/app/store/comment/comments.actions.ts +++ b/src/app/store/comment/comments.actions.ts @@ -1,6 +1,6 @@ import { createActionGroup, props } from "@ngrx/store"; -import { Comment } from "src/app/types/comment.model"; -import { Post } from "src/app/types/post.model"; +import { Comment } from "src/app/models/comment.model"; +import { Post } from "src/app/models/post.model"; export const CommentsComponentActions = createActionGroup({ source: 'Comments Component', diff --git a/src/app/store/comment/comments.reducer.ts b/src/app/store/comment/comments.reducer.ts index 1fae1a1..9dd3b25 100644 --- a/src/app/store/comment/comments.reducer.ts +++ b/src/app/store/comment/comments.reducer.ts @@ -1,7 +1,7 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { CommentsAPIActions, CommentsComponentActions } from "./comments.actions"; import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; -import { Comment } from "src/app/types/comment.model"; +import { Comment } from "src/app/models/comment.model"; export interface CommentsState extends EntityState { loading: boolean; diff --git a/src/app/store/guestbook/guestbook.actions.ts b/src/app/store/guestbook/guestbook.actions.ts index 3025080..e68b07d 100644 --- a/src/app/store/guestbook/guestbook.actions.ts +++ b/src/app/store/guestbook/guestbook.actions.ts @@ -1,5 +1,5 @@ import { createActionGroup, emptyProps, props } from "@ngrx/store"; -import { GuestbookEntry } from "src/app/types/guestbook.model"; +import { GuestbookEntry } from "src/app/models/guestbook.model"; export const GuestbookPageActions = createActionGroup({ source: 'Guestbook Page', diff --git a/src/app/store/guestbook/guestbook.effects.ts b/src/app/store/guestbook/guestbook.effects.ts index d5acbaa..33e3b33 100644 --- a/src/app/store/guestbook/guestbook.effects.ts +++ b/src/app/store/guestbook/guestbook.effects.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; import { GuestbookAPIActions, GuestbookPageActions } from "./guestbook.actions"; import { catchError, concatMap, map, of } from 'rxjs'; -import { GuestbookService } from "src/app/features/guestbook/guestbook.service"; +import { GuestbookService } from "src/app/features/guestbook/services/guestbook.service"; @Injectable() diff --git a/src/app/store/guestbook/guestbook.reducer.ts b/src/app/store/guestbook/guestbook.reducer.ts index 2c760a5..98f22de 100644 --- a/src/app/store/guestbook/guestbook.reducer.ts +++ b/src/app/store/guestbook/guestbook.reducer.ts @@ -1,6 +1,6 @@ import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; import { createFeature, createReducer, on } from "@ngrx/store"; -import { GuestbookEntry } from "src/app/types/guestbook.model"; +import { GuestbookEntry } from "src/app/models/guestbook.model"; import { GuestbookAPIActions, GuestbookPageActions } from "./guestbook.actions"; export interface GuestbookState extends EntityState { @@ -8,7 +8,18 @@ export interface GuestbookState extends EntityState { errorMessage: string; } -const adapter: EntityAdapter = createEntityAdapter({}); +const adapter: EntityAdapter = createEntityAdapter({ + selectId, + sortComparer, +}); + +export function selectId(entry: GuestbookEntry): number { + return entry.id; +} + +export function sortComparer(a: GuestbookEntry, b: GuestbookEntry): number { + return a.createdAt < b.createdAt ? 1 : -1; +} const initialState: GuestbookState = adapter.getInitialState({ loading: false, @@ -40,6 +51,11 @@ export const guestbookFeature = createFeature({ errorMessage: message, loading: false, })), + on(GuestbookPageActions.addGuestbookEntry, (state) => ({ + ...state, + loading: true, + errorMessage: '', + })), on(GuestbookAPIActions.guestbookEntryAddedSuccess, (state, { entry }) => adapter.addOne(entry, { ...state, diff --git a/src/app/store/post/posts.actions.ts b/src/app/store/post/posts.actions.ts index f33a896..50df0a7 100644 --- a/src/app/store/post/posts.actions.ts +++ b/src/app/store/post/posts.actions.ts @@ -1,5 +1,5 @@ import { createActionGroup, emptyProps, props } from "@ngrx/store"; -import { Post } from "../../types/post.model"; +import { Post } from "../../models/post.model"; export const PostsPageActions = createActionGroup({ source: 'Posts Page', diff --git a/src/app/store/post/posts.reducer.ts b/src/app/store/post/posts.reducer.ts index b92374e..efdae2c 100644 --- a/src/app/store/post/posts.reducer.ts +++ b/src/app/store/post/posts.reducer.ts @@ -1,6 +1,6 @@ import { createFeature, createReducer, on } from "@ngrx/store"; import { PostsAPIActions, PostsPageActions } from "./posts.actions"; -import { Post } from "../../types/post.model"; +import { Post } from "../../models/post.model"; import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; export interface PostsState extends EntityState { diff --git a/src/app/types/guestbook.model.ts b/src/app/types/guestbook.model.ts deleted file mode 100644 index b0a80d7..0000000 --- a/src/app/types/guestbook.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface GuestbookEntry { - id: number; - name: string; - email: string; - message: string; - createdAt: Date; -} \ No newline at end of file From a76316a2bd9fc7522753784a486aa8376ad4be53 Mon Sep 17 00:00:00 2001 From: Pavel Trifonov Date: Thu, 29 May 2025 16:30:43 +0300 Subject: [PATCH 36/36] Add guestbook author dialog component and service --- .../guestbook-author.component.html | 15 ++++++ .../guestbook-author.component.scss | 0 .../guestbook-author.component.spec.ts | 46 +++++++++++++++++++ .../guestbook-author.component.ts | 22 +++++++++ .../guestbook-list.component.html | 6 ++- .../guestbook-list.component.scss | 5 ++ .../guestbook-list.component.ts | 8 ++++ .../guestbook-author-dialog.service.spec.ts | 16 +++++++ .../guestbook-author-dialog.service.ts | 18 ++++++++ src/index.html | 2 +- 10 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/app/features/guestbook/guestbook-author/guestbook-author.component.html create mode 100644 src/app/features/guestbook/guestbook-author/guestbook-author.component.scss create mode 100644 src/app/features/guestbook/guestbook-author/guestbook-author.component.spec.ts create mode 100644 src/app/features/guestbook/guestbook-author/guestbook-author.component.ts create mode 100644 src/app/features/guestbook/services/guestbook-author-dialog.service.spec.ts create mode 100644 src/app/features/guestbook/services/guestbook-author-dialog.service.ts diff --git a/src/app/features/guestbook/guestbook-author/guestbook-author.component.html b/src/app/features/guestbook/guestbook-author/guestbook-author.component.html new file mode 100644 index 0000000..ebd6f7b --- /dev/null +++ b/src/app/features/guestbook/guestbook-author/guestbook-author.component.html @@ -0,0 +1,15 @@ +
+ +
+
+ Name: + {{ authorName() }} +
+ +
+ Email: + {{ email() }} +
+
+
+
\ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-author/guestbook-author.component.scss b/src/app/features/guestbook/guestbook-author/guestbook-author.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/guestbook/guestbook-author/guestbook-author.component.spec.ts b/src/app/features/guestbook/guestbook-author/guestbook-author.component.spec.ts new file mode 100644 index 0000000..c13dadc --- /dev/null +++ b/src/app/features/guestbook/guestbook-author/guestbook-author.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +import { GuestbookAuthorComponent } from './guestbook-author.component'; +import { Author } from 'src/app/models/author.model'; + +describe('GuestbookAuthorComponent', () => { + let component: GuestbookAuthorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GuestbookAuthorComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: {} } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GuestbookAuthorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should init', () => { + const mockAuthor: Author = { name: 'John', email: 'john@' }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [GuestbookAuthorComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: mockAuthor } + ] + }); + + const testFixture = TestBed.createComponent(GuestbookAuthorComponent); + const testComponent = testFixture.componentInstance; + + expect(testComponent.authorName()).toBe('John'); + expect(testComponent.email()).toBe('john@'); + }); +}); diff --git a/src/app/features/guestbook/guestbook-author/guestbook-author.component.ts b/src/app/features/guestbook/guestbook-author/guestbook-author.component.ts new file mode 100644 index 0000000..97b826f --- /dev/null +++ b/src/app/features/guestbook/guestbook-author/guestbook-author.component.ts @@ -0,0 +1,22 @@ +import { Component, Inject, signal } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { Author } from 'src/app/models/author.model'; + +@Component({ + selector: 'app-guestbook-author', + imports: [MatDialogModule], + templateUrl: './guestbook-author.component.html', + styleUrl: './guestbook-author.component.scss' +}) +export class GuestbookAuthorComponent { + readonly authorName = signal('') + readonly email = signal('') + + constructor(@Inject(MAT_DIALOG_DATA) public data: Author) { + if (data) { + this.authorName.set(data.name || ''); + this.email.set(data.email || ''); + } + } + +} diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html index 2034ab6..cd5b5d4 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.html +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.html @@ -2,7 +2,11 @@
- {{ gPost.author.name }} + + {{ gPost.author.name }} +

{{ gPost.message }}

diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss b/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss index 348cafa..9099e7b 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.scss @@ -1,3 +1,8 @@ .guest-post-item { padding: 8px; +} + +.clickable-author { + cursor: pointer; + color: var(--primary-color); } \ No newline at end of file diff --git a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts index 4c64a3c..c7d504c 100644 --- a/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts +++ b/src/app/features/guestbook/guestbook-list/guestbook-list.component.ts @@ -2,6 +2,8 @@ import { Component, Input } from '@angular/core'; import { GuestbookEntry } from '../../../models/guestbook.model'; import { NgFor } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; +import { Author } from 'src/app/models/author.model'; +import { GuestbookAuthorDialogService } from '../services/guestbook-author-dialog.service'; @Component({ selector: 'app-guestbook-list', @@ -11,4 +13,10 @@ import { MatCardModule } from '@angular/material/card'; }) export class GuestbookListComponent { @Input() guestPosts: GuestbookEntry[] | null = []; + + constructor(private authorDialogService: GuestbookAuthorDialogService) { } + + openAuthorDialog(author: Author): void { + this.authorDialogService.openAuthorDialog(author); + } } diff --git a/src/app/features/guestbook/services/guestbook-author-dialog.service.spec.ts b/src/app/features/guestbook/services/guestbook-author-dialog.service.spec.ts new file mode 100644 index 0000000..8be1e7c --- /dev/null +++ b/src/app/features/guestbook/services/guestbook-author-dialog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { GuestbookAuthorDialogService } from './guestbook-author-dialog.service'; + +describe('GuestbookAuthorDialogService', () => { + let service: GuestbookAuthorDialogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuestbookAuthorDialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/guestbook/services/guestbook-author-dialog.service.ts b/src/app/features/guestbook/services/guestbook-author-dialog.service.ts new file mode 100644 index 0000000..f5ce87c --- /dev/null +++ b/src/app/features/guestbook/services/guestbook-author-dialog.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { GuestbookAuthorComponent } from '../guestbook-author/guestbook-author.component'; +import { Author } from 'src/app/models/author.model'; + +@Injectable({ + providedIn: 'root' +}) +export class GuestbookAuthorDialogService { + + constructor(private dialog: MatDialog) { } + + openAuthorDialog(author: Author): void { + this.dialog.open(GuestbookAuthorComponent, { + data: author + }); + } +} diff --git a/src/index.html b/src/index.html index 704ff0b..962c936 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - AngularTemplate1 + AngularTemplate