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.

    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..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 @@ -15,7 +15,7 @@ $bp-large: 1024px; } .first-articles { - width: 30%; + width: 32%; display: none; flex-direction: column; max-height: 100%; @@ -35,4 +35,86 @@ $bp-large: 1024px; ::ng-deep .pink_swiper .swiper-slide { padding-top: 0; +} + +.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; +} + +::ng-deep .mat-mdc-input-element::placeholder{ + color: #8C8C8C !important; } \ No newline at end of file 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..5555e7ef1 --- /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"