From 4e5e460b7eb77ce243717827a22a377336de5100 Mon Sep 17 00:00:00 2001 From: danyaZh Date: Thu, 25 Jan 2024 12:29:23 +0200 Subject: [PATCH 1/3] feat: implemented search on blog page --- .../src/assets/styles/styles.scss | 2 + libs/route-pages/blog/src/blog.module.ts | 17 ++- .../pages/blog-page/blog-page.component.html | 111 ++++++++++++------ .../pages/blog-page/blog-page.component.scss | 82 ++++++++++++- .../pages/blog-page/blog-page.component.ts | 61 +++++++++- .../blog/src/pipes/highlight.pipe.ts | 23 ++++ libs/route-pages/blog/src/pipes/index.ts | 2 + libs/route-pages/blog/tsconfig.lib.json | 2 +- package.json | 3 +- tsconfig.base.json | 2 +- yarn.lock | 5 + 11 files changed, 258 insertions(+), 52 deletions(-) create mode 100644 libs/route-pages/blog/src/pipes/highlight.pipe.ts create mode 100644 libs/route-pages/blog/src/pipes/index.ts diff --git a/apps/valor-software-site/src/assets/styles/styles.scss b/apps/valor-software-site/src/assets/styles/styles.scss index bd4264bbe..5cf0ef9f2 100644 --- a/apps/valor-software-site/src/assets/styles/styles.scss +++ b/apps/valor-software-site/src/assets/styles/styles.scss @@ -10,6 +10,8 @@ // swiper core styles @import 'swiper/css'; +@import 'material-icons/iconfont/material-icons.scss'; + body, html { @apply bg-grey_bg h-full; scroll-behavior: smooth; diff --git a/libs/route-pages/blog/src/blog.module.ts b/libs/route-pages/blog/src/blog.module.ts index 2c4ea84cf..4ceebc30f 100644 --- a/libs/route-pages/blog/src/blog.module.ts +++ b/libs/route-pages/blog/src/blog.module.ts @@ -7,7 +7,12 @@ import { CommonDocsModule } from '@valor-software/common-docs'; import { BlogComponent, ArticleComponent, BlogItemComponent } from './components'; import { FeedbackModule } from '@valor-software/feedback'; import { SwiperModule } from 'swiper/angular'; -import { DomainNamePipe } from './pipes/domain-name.pipe'; +import { DomainNamePipe, HighlightMatchingLettersPipe } from './pipes'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; @NgModule({ declarations: [ @@ -15,14 +20,20 @@ import { DomainNamePipe } from './pipes/domain-name.pipe'; BlogComponent, ArticleComponent, BlogItemComponent, - DomainNamePipe + DomainNamePipe, + HighlightMatchingLettersPipe ], imports: [ CommonModule, RouterModule.forChild(routes), CommonDocsModule, FeedbackModule, - SwiperModule + SwiperModule, + MatAutocompleteModule, + MatInputModule, + MatIconModule, + ReactiveFormsModule, + MatButtonModule ] }) export class BlogModule { diff --git a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.html b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.html index 3ceeebd7c..f950338e8 100644 --- a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.html +++ b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.html @@ -1,52 +1,85 @@ -
-
+
+
+ + + + + + + + + - - - - + search + + -
- - - - - -
-
-

Latest Articles

+
+ + + + + - - - + + +
+

Latest Articles

- + + + + + + + + + +
+
diff --git a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss index 13faf2e6a..711466b7b 100644 --- a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss +++ b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss @@ -15,7 +15,7 @@ $bp-large: 1024px; } .first-articles { - width: 30%; + width: 32%; display: none; flex-direction: column; max-height: 100%; @@ -35,4 +35,82 @@ $bp-large: 1024px; ::ng-deep .pink_swiper .swiper-slide { padding-top: 0; -} \ No newline at end of file +} + +.search { + width: 32%; +} + +::ng-deep .cdk-overlay-pane { + .mat-mdc-autocomplete-panel { + background-color: #515151; + padding-top: 0; + padding-bottom: 16px; + max-height: 306px + } + + .mat-mdc-optgroup { + color: #E3E3E3; + } + + .mat-mdc-optgroup-label { + min-height: auto; + margin-bottom: 8px; + margin-top: 16px; + } + + .mat-mdc-option { + background-color: #343434; + margin: 0 16px 4px; + color: #E3E3E3; + border-radius: 4px; + } + + .mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple) { + padding: 10px 12px; + } + + .mat-mdc-option:hover:not(.mdc-list-item--disabled) { + background-color: rgba(226, 78, 99, 1) !important; + color: rgba(255, 255, 255, 1) !important; + } + + .mat-mdc-option:focus:not(.mdc-list-item--disabled) { + background-color: rgba(226, 78, 99, 1) !important; + color: rgba(255, 255, 255, 1) !important; + } + + .mat-mdc-option:active:not(.mdc-list-item--disabled) { + background-color: rgba(226, 78, 99, 1) !important; + color: rgba(255, 255, 255, 1) !important; + } + + .mat-mdc-option:focus.mdc-list-item, .mat-mdc-option.mat-mdc-option-active.mdc-list-item { + background-color: rgba(226, 78, 99, 1) !important; + color: rgba(255, 255, 255, 1) !important; + } +} + +::ng-deep .mat-icon { + color: #8C8C8C; +} + +::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) { + background-color: #515151 !important; +} + +::ng-deep .mdc-text-field__input:focus { + box-shadow: none; +} + +::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after { + border-bottom-color: transparent !important; +} + +::ng-deep .mat-mdc-autocomplete-trigger { + color: rgba(255, 255, 255, 1) !important; +} + +::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input { + caret-color: white !important; +} diff --git a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.ts b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.ts index d03b7ae92..658aa1352 100644 --- a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.ts +++ b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.ts @@ -1,12 +1,19 @@ import { Component, OnDestroy } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { forkJoin, Subscription } from 'rxjs'; import { GetArticlesService, IArticle, titleRefactoring } from '@valor-software/common-docs'; import SwiperCore, { Pagination, SwiperOptions } from 'swiper'; +import { FormControl } from '@angular/forms'; +import { Domains } from '../../models'; SwiperCore.use([Pagination]); +interface GroupedArticles { + tag: string; + articles: IArticle[]; +} + @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'blog-page', @@ -30,6 +37,9 @@ export class BlogPageComponent implements OnDestroy { } }; + groupedAndFilteredArticles: GroupedArticles[] = []; + searchTermControl = new FormControl(''); + constructor( private readonly router: Router, private readonly getArticlesServ: GetArticlesService, @@ -43,14 +53,14 @@ export class BlogPageComponent implements OnDestroy { })); } - getFirstProjects(value: Type[]): Type[] { - return value.slice(0, 4); - } - getRouteLink(link: string): any { return titleRefactoring(link); } + navigateToArticle(title: string): void { + this.router.navigate(['..', 'articles', this.getRouteLink(title)]); + } + ngOnDestroy() { this.$generalSubscription.unsubscribe(); } @@ -65,14 +75,55 @@ export class BlogPageComponent implements OnDestroy { this.firstArticles = this.getFirstProjects(res); this.activeArticle = this.firstArticles[0]; this.filterFirstItems(); + this._observeSearchValueChanges(); } }) ); } + private getFirstProjects(value: Type[]): Type[] { + return value.slice(0, 4); + } + private filterFirstItems() { if (this.activeArticle) { this.firstArticles = this.firstArticles?.filter(item => item !== this.activeArticle); } } + + private _filterAndGroupArticles(articles: IArticle[], searchTerm: string): GroupedArticles[] { + if (!searchTerm || searchTerm.length === 0) { + return []; + } + + const filteredArticles = articles.filter(article => + article.title.toLowerCase().includes(searchTerm?.toLowerCase()) + ); + const groupedByTag: Map = new Map(); + + filteredArticles.forEach(article => { + article.domains.forEach(domain => { + const existingArticles = groupedByTag.get(domain) || []; + groupedByTag.set(domain, [...existingArticles, article]); + }); + }); + + return Array.from(groupedByTag.entries()).map( + ([tag, articles]) => ({ + tag: Domains[tag] || tag, + articles + }) + ); + } + + private _observeSearchValueChanges(): void { + this.$generalSubscription.add( + this.searchTermControl.valueChanges.pipe( + map(term => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.groupedAndFilteredArticles = this._filterAndGroupArticles(this.articles, term!); + }) + ).subscribe() + ); + } } \ No newline at end of file diff --git a/libs/route-pages/blog/src/pipes/highlight.pipe.ts b/libs/route-pages/blog/src/pipes/highlight.pipe.ts new file mode 100644 index 000000000..84739c089 --- /dev/null +++ b/libs/route-pages/blog/src/pipes/highlight.pipe.ts @@ -0,0 +1,23 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +@Pipe({ + name: 'highlightMatchingLetters', +}) +export class HighlightMatchingLettersPipe implements PipeTransform { + constructor(private readonly domSanitizer: DomSanitizer) { + } + + transform(value: string, searchTerm: string | null): SafeHtml { + if (!searchTerm || !value) { + return this.domSanitizer.bypassSecurityTrustHtml(value); + } + + const regex = new RegExp(searchTerm, 'gi'); + const highlightedValue = value.replace(regex, + match => `${match}` + ); + + return this.domSanitizer.bypassSecurityTrustHtml(highlightedValue); + } +} \ No newline at end of file diff --git a/libs/route-pages/blog/src/pipes/index.ts b/libs/route-pages/blog/src/pipes/index.ts new file mode 100644 index 000000000..05c679a62 --- /dev/null +++ b/libs/route-pages/blog/src/pipes/index.ts @@ -0,0 +1,2 @@ +export * from './highlight.pipe'; +export * from './domain-name.pipe'; diff --git a/libs/route-pages/blog/tsconfig.lib.json b/libs/route-pages/blog/tsconfig.lib.json index 5aebe84f5..754fdf6e7 100644 --- a/libs/route-pages/blog/tsconfig.lib.json +++ b/libs/route-pages/blog/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "ES2019"], "useDefineForClassFields": false }, "exclude": [ diff --git a/package.json b/package.json index 8729b076a..4fd6618d6 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "asciidoctor": "^2.2.6", "glob": "^8.0.3", "img-comparison-slider": "^7.6.0", + "material-icons": "^1.13.12", "ng-recaptcha": "^9.0.0", "ngx-cookie-service": "^15.0.0", "rxjs": "~6.6.3", @@ -95,4 +96,4 @@ "typescript": "5.0.4", "xmlbuilder": "^15.1.1" } -} \ No newline at end of file +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 2dfbea4d7..784c2dcfe 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -11,7 +11,7 @@ "target": "es2015", "module": "esnext", "lib": [ - "es2017", + "ES2019", "dom" ], "types": [ diff --git a/yarn.lock b/yarn.lock index 5130c2faf..8d7bdf842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9259,6 +9259,11 @@ marked@^4.0.12: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== +material-icons@^1.13.12: + version "1.13.12" + resolved "https://registry.yarnpkg.com/material-icons/-/material-icons-1.13.12.tgz#eed4082bf0426642edeb027e75397e3064adc536" + integrity sha512-/2YoaB79IjUK2B2JB+vIXXYGtBfHb/XG66LvoKVM5ykHW7yfrV5SP6d7KLX6iijY6/G9GqwgtPQ/sbhFnOURVA== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" From 2d05c01780408c70c2084f40fa825c568072dec0 Mon Sep 17 00:00:00 2001 From: danyaZh Date: Thu, 25 Jan 2024 12:46:00 +0200 Subject: [PATCH 2/3] feat: fixed build --- apps/valor-software-site/project.json | 8 ++++---- .../painless-cli-integration-testing.html | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/valor-software-site/project.json b/apps/valor-software-site/project.json index acb6e166a..b54889a30 100644 --- a/apps/valor-software-site/project.json +++ b/apps/valor-software-site/project.json @@ -57,8 +57,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "5mb" }, { "type": "anyComponentStyle", @@ -98,8 +98,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "5mb" }, { "type": "anyComponentStyle", diff --git a/apps/valor-software-site/src/assets/articles/painless-cli-integration-testing/painless-cli-integration-testing.html b/apps/valor-software-site/src/assets/articles/painless-cli-integration-testing/painless-cli-integration-testing.html index 2f0eb7f32..b97cea171 100644 --- a/apps/valor-software-site/src/assets/articles/painless-cli-integration-testing/painless-cli-integration-testing.html +++ b/apps/valor-software-site/src/assets/articles/painless-cli-integration-testing/painless-cli-integration-testing.html @@ -261,12 +261,14 @@

Make the solution test-friendly

Run the CLI as a separate process and intercommunicate with them.

  • -

    Run the CLI functionality inside the tests. -I chose the second one because it’s more suitable for integration testing. The first is mostly regarding e2e or Big (Google definitions) tests. Also, the second approach is much more straightforward in implementation. But there is one critical answer here. We must pass the input data (key presses) to the CLI.

    +

    Run the CLI functionality inside the tests.

  • +

    I chose the second one because it’s more suitable for integration testing. The first is mostly regarding e2e or Big (Google definitions) tests. Also, the second approach is much more straightforward in implementation. But there is one critical answer here. We must pass the input data (key presses) to the CLI.

    +
    +

    If we talk about the first approach, the following approaches will be useful: Nodejs Child Process: write to stdin from an already initialised process. It makes sense to note here that the approaches above are rather for e2e testing than integration.

    From d6d2cd3216b16c9cda54338bfb43986429c2a910 Mon Sep 17 00:00:00 2001 From: danyaZh Date: Fri, 2 Feb 2024 15:34:05 +0200 Subject: [PATCH 3/3] feat: fixed comments --- .../blog/src/pages/blog-page/blog-page.component.scss | 4 ++++ libs/route-pages/blog/src/pipes/highlight.pipe.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss index 711466b7b..44bd795fa 100644 --- a/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss +++ b/libs/route-pages/blog/src/pages/blog-page/blog-page.component.scss @@ -114,3 +114,7 @@ $bp-large: 1024px; ::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input { caret-color: white !important; } + +::ng-deep .mat-mdc-input-element::placeholder{ + color: #8C8C8C !important; +} \ No newline at end of file diff --git a/libs/route-pages/blog/src/pipes/highlight.pipe.ts b/libs/route-pages/blog/src/pipes/highlight.pipe.ts index 84739c089..5555e7ef1 100644 --- a/libs/route-pages/blog/src/pipes/highlight.pipe.ts +++ b/libs/route-pages/blog/src/pipes/highlight.pipe.ts @@ -15,7 +15,7 @@ export class HighlightMatchingLettersPipe implements PipeTransform { const regex = new RegExp(searchTerm, 'gi'); const highlightedValue = value.replace(regex, - match => `${match}` + match => `${match}` ); return this.domSanitizer.bypassSecurityTrustHtml(highlightedValue);