From cb28067a2eae8a342f16fccd6e4db29bc69e9db9 Mon Sep 17 00:00:00 2001 From: Valery Melou Date: Fri, 12 Jul 2024 22:27:37 +0100 Subject: [PATCH] feat: init article page --- apps/website/src/app/app.routes.ts | 8 ++++ .../data-access/src/lib/article.service.ts | 16 +++++++ libs/blog/data-access/src/lib/article.ts | 4 +- libs/blog/feature-article/.eslintrc.json | 36 +++++++++++++++ libs/blog/feature-article/README.md | 7 +++ libs/blog/feature-article/jest.config.ts | 22 ++++++++++ libs/blog/feature-article/project.json | 20 +++++++++ libs/blog/feature-article/src/index.ts | 1 + .../src/lib/blog-article.component.html | 23 ++++++++++ .../src/lib/blog-article.component.spec.ts | 21 +++++++++ .../src/lib/blog-article.component.ts | 44 +++++++++++++++++++ libs/blog/feature-article/src/test-setup.ts | 8 ++++ libs/blog/feature-article/tsconfig.json | 29 ++++++++++++ libs/blog/feature-article/tsconfig.lib.json | 17 +++++++ libs/blog/feature-article/tsconfig.spec.json | 16 +++++++ .../src/lib/blog-home.component.ts | 4 +- .../layout/src/lib/navbar/navbar.component.ts | 2 +- package.json | 1 + tsconfig.base.json | 7 +-- yarn.lock | 6 +++ 20 files changed, 285 insertions(+), 7 deletions(-) create mode 100644 libs/blog/feature-article/.eslintrc.json create mode 100644 libs/blog/feature-article/README.md create mode 100644 libs/blog/feature-article/jest.config.ts create mode 100644 libs/blog/feature-article/project.json create mode 100644 libs/blog/feature-article/src/index.ts create mode 100644 libs/blog/feature-article/src/lib/blog-article.component.html create mode 100644 libs/blog/feature-article/src/lib/blog-article.component.spec.ts create mode 100644 libs/blog/feature-article/src/lib/blog-article.component.ts create mode 100644 libs/blog/feature-article/src/test-setup.ts create mode 100644 libs/blog/feature-article/tsconfig.json create mode 100644 libs/blog/feature-article/tsconfig.lib.json create mode 100644 libs/blog/feature-article/tsconfig.spec.json diff --git a/apps/website/src/app/app.routes.ts b/apps/website/src/app/app.routes.ts index 600d1343..9711b564 100644 --- a/apps/website/src/app/app.routes.ts +++ b/apps/website/src/app/app.routes.ts @@ -36,6 +36,14 @@ export const appRoutes: Route[] = [ import('@valerymelou/blog/home').then((c) => c.BlogHomeComponent), data: { animation: 'BlogHomePage' }, }, + { + path: 'blog/:slug', + loadComponent: () => + import('@valerymelou/blog/article').then( + (c) => c.BlogArticleComponent, + ), + data: { animation: 'BlogHomePage' }, + }, ], }, ]; diff --git a/libs/blog/data-access/src/lib/article.service.ts b/libs/blog/data-access/src/lib/article.service.ts index f5d169bd..e0344d08 100644 --- a/libs/blog/data-access/src/lib/article.service.ts +++ b/libs/blog/data-access/src/lib/article.service.ts @@ -38,4 +38,20 @@ export class ArticleService { }), ); } + + getOne(slug: string): Observable
{ + return this.contentfulService + .getEntries(this.contentType, { 'fields.slug[match]': slug }) + .pipe( + map( + (entries: EntryCollection) => { + if (entries.items.length === 0) { + throw new Error('Article not found'); + } + + return Article.fromEntry(entries.items[0], entries.includes?.Asset); + }, + ), + ); + } } diff --git a/libs/blog/data-access/src/lib/article.ts b/libs/blog/data-access/src/lib/article.ts index 31071da3..aa396072 100644 --- a/libs/blog/data-access/src/lib/article.ts +++ b/libs/blog/data-access/src/lib/article.ts @@ -15,10 +15,11 @@ export class Article { createdAt = ''; updatedAt = ''; tags: Tag[] = []; + content: any; static fromEntry( entry: Entry, - assets?: ContentfulAsset[] + assets?: ContentfulAsset[], ): Article { const article = new Article(); article.title = entry.fields['title'] as string; @@ -45,6 +46,7 @@ export class Article { .join(), }); }); + article.content = entry.fields['content']; return article; } } diff --git a/libs/blog/feature-article/.eslintrc.json b/libs/blog/feature-article/.eslintrc.json new file mode 100644 index 00000000..28b8d978 --- /dev/null +++ b/libs/blog/feature-article/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "blog", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "blog", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/feature-article/README.md b/libs/blog/feature-article/README.md new file mode 100644 index 00000000..f5586ac8 --- /dev/null +++ b/libs/blog/feature-article/README.md @@ -0,0 +1,7 @@ +# blog-article + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-article` to execute the unit tests. diff --git a/libs/blog/feature-article/jest.config.ts b/libs/blog/feature-article/jest.config.ts new file mode 100644 index 00000000..b3e69904 --- /dev/null +++ b/libs/blog/feature-article/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'blog-article', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/blog/feature-article', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/feature-article/project.json b/libs/blog/feature-article/project.json new file mode 100644 index 00000000..1025c204 --- /dev/null +++ b/libs/blog/feature-article/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-article", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/feature-article/src", + "prefix": "blog", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/feature-article/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/feature-article/src/index.ts b/libs/blog/feature-article/src/index.ts new file mode 100644 index 00000000..1e895e63 --- /dev/null +++ b/libs/blog/feature-article/src/index.ts @@ -0,0 +1 @@ +export * from './lib/blog-article.component'; diff --git a/libs/blog/feature-article/src/lib/blog-article.component.html b/libs/blog/feature-article/src/lib/blog-article.component.html new file mode 100644 index 00000000..cb87d81f --- /dev/null +++ b/libs/blog/feature-article/src/lib/blog-article.component.html @@ -0,0 +1,23 @@ +@if (article$ | async; as article) { +
+
+
Date
+
+ +
+
+

+ {{ article.title }} +

+ +

+ {{ article.abstract }} +

+ +
+
+} diff --git a/libs/blog/feature-article/src/lib/blog-article.component.spec.ts b/libs/blog/feature-article/src/lib/blog-article.component.spec.ts new file mode 100644 index 00000000..a9746cf3 --- /dev/null +++ b/libs/blog/feature-article/src/lib/blog-article.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BlogArticleComponent } from './blog-article.component'; + +describe('BlogArticleComponent', () => { + let component: BlogArticleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BlogArticleComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(BlogArticleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/blog/feature-article/src/lib/blog-article.component.ts b/libs/blog/feature-article/src/lib/blog-article.component.ts new file mode 100644 index 00000000..eab46685 --- /dev/null +++ b/libs/blog/feature-article/src/lib/blog-article.component.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { map, Observable } from 'rxjs'; + +import { Article, ArticleService } from '@valerymelou/blog/data-access'; +import { MetadataService } from '@valerymelou/shared/seo'; + +@Component({ + selector: 'blog-article', + standalone: true, + imports: [CommonModule], + templateUrl: './blog-article.component.html', +}) +export class BlogArticleComponent { + article$!: Observable
; + + constructor( + private route: ActivatedRoute, + private articleService: ArticleService, + private metadataService: MetadataService, + ) { + this.route.params.subscribe({ + next: (params: Params) => { + if (params['slug']) this.getArticle(params['slug']); + }, + }); + } + + getArticle(slug: string): void { + slug = slug.split('-').slice(3).join('-'); + this.article$ = this.articleService.getOne(slug).pipe( + map((article: Article) => { + this.metadataService.updateMetadata({ + title: article.title, + description: article.abstract, + image: article.cover?.url, + }); + return article; + }), + ); + } +} diff --git a/libs/blog/feature-article/src/test-setup.ts b/libs/blog/feature-article/src/test-setup.ts new file mode 100644 index 00000000..ab1eeeb3 --- /dev/null +++ b/libs/blog/feature-article/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/libs/blog/feature-article/tsconfig.json b/libs/blog/feature-article/tsconfig.json new file mode 100644 index 00000000..5cf0a165 --- /dev/null +++ b/libs/blog/feature-article/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog/feature-article/tsconfig.lib.json b/libs/blog/feature-article/tsconfig.lib.json new file mode 100644 index 00000000..9b49be75 --- /dev/null +++ b/libs/blog/feature-article/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog/feature-article/tsconfig.spec.json b/libs/blog/feature-article/tsconfig.spec.json new file mode 100644 index 00000000..f858ef78 --- /dev/null +++ b/libs/blog/feature-article/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/feature-home/src/lib/blog-home.component.ts b/libs/blog/feature-home/src/lib/blog-home.component.ts index 557fd4e7..eb8f1291 100644 --- a/libs/blog/feature-home/src/lib/blog-home.component.ts +++ b/libs/blog/feature-home/src/lib/blog-home.component.ts @@ -6,13 +6,13 @@ import { Article, ArticleService, Results, -} from '@valerymelou/blob/data-access'; +} from '@valerymelou/blog/data-access'; import { LinkComponent } from '@valerymelou/shared/ui'; import { RouterModule } from '@angular/router'; import { MetadataService } from '@valerymelou/shared/seo'; @Component({ - selector: 'blog-blog-home', + selector: 'blog-home', standalone: true, imports: [CommonModule, RouterModule, LinkComponent], templateUrl: './blog-home.component.html', diff --git a/libs/shared/layout/src/lib/navbar/navbar.component.ts b/libs/shared/layout/src/lib/navbar/navbar.component.ts index 9567ab98..b4308617 100644 --- a/libs/shared/layout/src/lib/navbar/navbar.component.ts +++ b/libs/shared/layout/src/lib/navbar/navbar.component.ts @@ -60,7 +60,7 @@ export class NavbarComponent { if (event instanceof NavigationEnd) { this.isAbout = event.urlAfterRedirects === '/about'; this.isProject = event.urlAfterRedirects === '/projects'; - this.isBlog = event.urlAfterRedirects === '/blog'; + this.isBlog = event.urlAfterRedirects.startsWith('/blog'); } }); } diff --git a/package.json b/package.json index b0f94690..16547af7 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@angular/platform-browser": "18.0.0", "@angular/platform-browser-dynamic": "18.0.0", "@angular/router": "18.0.0", + "@contentful/rich-text-types": "^16.6.1", "@ng-icons/bootstrap-icons": "^27.3.1", "@ng-icons/core": "^27.3.1", "@ng-icons/material-icons": "^27.5.2", diff --git a/tsconfig.base.json b/tsconfig.base.json index 78f49217..2d63e8ae 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,16 +15,17 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@valerymelou/blob/data-access": ["libs/blog/data-access/src/index.ts"], + "@valerymelou/blog/article": ["libs/blog/feature-article/src/index.ts"], + "@valerymelou/blog/data-access": ["libs/blog/data-access/src/index.ts"], "@valerymelou/blog/home": ["libs/blog/feature-home/src/index.ts"], "@valerymelou/cms/contentful": ["libs/cms/contentful/src/index.ts"], "@valerymelou/pages/about": ["libs/pages/about/src/index.ts"], "@valerymelou/pages/home": ["libs/pages/home/src/index.ts"], "@valerymelou/pages/projects": ["libs/pages/projects/src/index.ts"], "@valerymelou/shared/layout": ["libs/shared/layout/src/index.ts"], - "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"], "@valerymelou/shared/seo": ["libs/shared/seo/src/index.ts"], - "@valerymelou/shared/theming": ["libs/shared/theming/src/index.ts"] + "@valerymelou/shared/theming": ["libs/shared/theming/src/index.ts"], + "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/yarn.lock b/yarn.lock index 263a70a0..b218e225 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1606,6 +1606,11 @@ resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.5.0.tgz#f82f38d2131ebae10458990cbb7574d9b77b77e7" integrity sha512-Z5fls/6Hs+ETq6vx+sfiq+n+z3dwurPVJukgLvThdvzs4VlChoPkRIKr4WrBdjzLYqUspawlf7XvtuUpp32glw== +"@contentful/rich-text-types@^16.6.1": + version "16.6.1" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.6.1.tgz#d7a0890b885ab681028aa7f1085e219d8b2c51be" + integrity sha512-Rb8QusSMSTMUP4dbpBDWgwGJoqKDYRxPcNXySP0URYTQ6pViJ2SXhVc81RJCgJ5Fb02n5ctbvDYGtcnBH8yhDw== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -10382,6 +10387,7 @@ wildcard@^2.0.0: integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==