From 5a7f82fd3e31eacfde77e772337c21903eca3fcd Mon Sep 17 00:00:00 2001 From: Maja Date: Thu, 6 Feb 2025 11:08:24 +0100 Subject: [PATCH 1/3] feat(main): newsletter & top banners --- libs/blog-bff/banners/api/src/lib/dtos.ts | 21 +++++--- libs/blog-bff/banners/api/src/lib/mappers.ts | 49 ++++++++++++----- .../blog-contracts/banners/src/lib/banners.ts | 22 +++++++- .../lib/infrastructure/ad-banner.service.ts | 6 +-- .../src/lib/state/ad-banner.store.ts | 12 ++--- .../ad-image-banner.component.html | 1 + .../article-details.component.html | 4 +- .../article-details.component.ts | 5 +- .../feature-latest-articles.component.html | 52 +++++++++++++++++-- .../feature-latest-articles.component.ts | 26 ++++++++-- .../src/lib/root-shell.component.ts | 44 +++++++++------- 11 files changed, 181 insertions(+), 61 deletions(-) diff --git a/libs/blog-bff/banners/api/src/lib/dtos.ts b/libs/blog-bff/banners/api/src/lib/dtos.ts index 173c86c1..18e1ff23 100644 --- a/libs/blog-bff/banners/api/src/lib/dtos.ts +++ b/libs/blog-bff/banners/api/src/lib/dtos.ts @@ -1,13 +1,22 @@ export interface WPBannerDto { id: number; acf: { + is_slider_banner_displayed: boolean; display_time: string; - slides: - | { - slide_image: number /* slideId */; - slide_url: string /* url to navigate to after click */; - }[] - | null; + slides: { + slide_image_desktop: number /* slideId */; + slide_image_mobile: number /* slideId */; + slide_url: string /* url to navigate to after click */; + }[]; + + is_top_banner_displayed: boolean; + top_banner_image_desktop: number /* mediaId */; + top_banner_image_mobile: number /* mediaId */; + top_banner_image_url: string /* url to navigate to after click */; + + is_card_banner_displayed: boolean; + card_banner_image: number /* mediaId */; + card_banner_url: string /* url to navigate to after click */; }; } diff --git a/libs/blog-bff/banners/api/src/lib/mappers.ts b/libs/blog-bff/banners/api/src/lib/mappers.ts index c3e3ec10..d725af74 100644 --- a/libs/blog-bff/banners/api/src/lib/mappers.ts +++ b/libs/blog-bff/banners/api/src/lib/mappers.ts @@ -1,21 +1,46 @@ -import { Slider } from '@angular-love/blog/contracts/banners'; +import { Banners } from '@angular-love/blog/contracts/banners'; import { WPBannerDto, WPBannerMediaDto } from './dtos'; export const toBanner = ( dto: WPBannerDto, mediaDto: WPBannerMediaDto[], -): Slider => { +): Banners => { return { - slideDisplayTimeMs: +dto.acf.display_time, - slides: - dto.acf.slides?.map((slide) => { - const media = mediaDto.find((media) => media.id === slide.slide_image)!; - return { - url: media.guid.rendered, - alt: media.alt_text, - navigateTo: slide.slide_url, - }; - }) ?? [], + ...(dto.acf.is_slider_banner_displayed && { + slider: { + slideDisplayTimeMs: +dto.acf.display_time, + slides: dto.acf.slides.map((slide) => { + const media = mediaDto.find( + (media) => media.id === slide.slide_image_desktop, + )!; + return { + url: media?.guid.rendered, + alt: media?.alt_text, + navigateTo: slide.slide_url, + }; + }), + }, + }), + ...(dto.acf.is_top_banner_displayed && { + topBanner: { + url: mediaDto.find( + (media) => media.id === dto.acf.top_banner_image_desktop, + )?.guid.rendered, + alt: mediaDto.find( + (media) => media.id === dto.acf.top_banner_image_desktop, + )?.alt_text, + navigateTo: dto.acf.top_banner_image_url, + }, + }), + ...(dto.acf.is_card_banner_displayed && { + cardBanner: { + url: mediaDto.find((media) => media.id === dto.acf.card_banner_image) + ?.guid.rendered, + alt: mediaDto.find((media) => media.id === dto.acf.card_banner_image) + ?.alt_text, + navigateTo: dto.acf.card_banner_url, + }, + }), }; }; diff --git a/libs/blog-contracts/banners/src/lib/banners.ts b/libs/blog-contracts/banners/src/lib/banners.ts index ba9c194e..8e57f085 100644 --- a/libs/blog-contracts/banners/src/lib/banners.ts +++ b/libs/blog-contracts/banners/src/lib/banners.ts @@ -1,8 +1,26 @@ export interface Slider { slideDisplayTimeMs: number; slides: { - url: string; - alt: string; + url?: string; + alt?: string; navigateTo: string; }[]; } + +export interface TopBanner { + url?: string; + alt?: string; + navigateTo: string; +} + +export interface CardBanner { + url?: string; + alt?: string; + navigateTo: string; +} + +export interface Banners { + slider?: Slider; + topBanner?: TopBanner; + cardBanner?: CardBanner; +} diff --git a/libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts b/libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts index d6836f4b..e6e4787d 100644 --- a/libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts +++ b/libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { Slider } from '@angular-love/blog/contracts/banners'; +import { Banners } from '@angular-love/blog/contracts/banners'; import { ConfigService } from '@angular-love/shared/config'; @Injectable({ providedIn: 'root' }) @@ -9,7 +9,7 @@ export class AdBannerService { private readonly _apiBaseUrl = inject(ConfigService).get('apiBaseUrl'); private readonly _http = inject(HttpClient); - getBannerSlider() { - return this._http.get(`${this._apiBaseUrl}/banners`); + getVisibleBanners() { + return this._http.get(`${this._apiBaseUrl}/banners`); } } diff --git a/libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts b/libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts index 30fb615b..6e7edc31 100644 --- a/libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts +++ b/libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts @@ -4,16 +4,16 @@ import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { pipe, switchMap } from 'rxjs'; -import { Slider } from '@angular-love/blog/contracts/banners'; +import { Banners } from '@angular-love/blog/contracts/banners'; import { AdBannerService } from '../infrastructure/ad-banner.service'; type AdBannerState = { - slider: Slider | null; + banners: Banners | null; }; const initialState: AdBannerState = { - slider: null, + banners: null, }; export const AdBannerStore = signalStore( @@ -23,10 +23,10 @@ export const AdBannerStore = signalStore( getData: rxMethod( pipe( switchMap(() => - adBannerService.getBannerSlider().pipe( + adBannerService.getVisibleBanners().pipe( tapResponse({ - next: (slider) => { - patchState(store, { slider }); + next: (banners) => { + patchState(store, { banners }); }, error: () => { patchState(store); diff --git a/libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner.component.html b/libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner.component.html index e2913e7f..95d28440 100644 --- a/libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner.component.html +++ b/libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner.component.html @@ -4,6 +4,7 @@ role="button" class="!relative cursor-pointer" [attr.aria-label]="banner().alt" + [alt]="banner().alt" [ngSrc]="banner().url" (click)="navigateFromBanner()" (keydown.enter)="navigateFromBanner()" diff --git a/libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html b/libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html index bab625bb..0fa0e5c9 100644 --- a/libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html +++ b/libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html @@ -62,8 +62,8 @@

diff --git a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts index 2853667f..15c05b74 100644 --- a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts +++ b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts @@ -1,7 +1,14 @@ -import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { NgClass, NgOptimizedImage } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, +} from '@angular/core'; +import { Router } from '@angular/router'; import { TranslocoDirective } from '@jsverse/transloco'; +import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access'; import { ArticleListStore } from '@angular-love/blog/articles/data-access'; import { ArticleRegularCardSkeletonComponent, @@ -11,7 +18,7 @@ import { UiArticleListTitleComponent } from '@angular-love/blog/articles/ui-arti import { NewsletterComponent } from '@angular-love/blog/newsletter'; import { CardComponent, - GradientCardDirective, + CardContentDirective, } from '@angular-love/blog/shared/ui-card'; import { RepeatDirective } from '@angular-love/utils'; @@ -24,12 +31,12 @@ import { RepeatDirective } from '@angular-love/utils'; UiArticleCardComponent, NewsletterComponent, CardComponent, - GradientCardDirective, NgClass, TranslocoDirective, ArticleRegularCardSkeletonComponent, - CardComponent, RepeatDirective, + CardContentDirective, + NgOptimizedImage, ], host: { 'data-testid': 'latest-articles-container', @@ -38,6 +45,11 @@ import { RepeatDirective } from '@angular-love/utils'; }) export class FeatureLatestArticlesComponent { private readonly _articleListStore = inject(ArticleListStore); + private readonly _router = inject(Router); + private readonly _bannerStore = inject(AdBannerStore); + protected readonly cardBanner = computed( + () => this._bannerStore.banners()?.cardBanner, + ); readonly isFetchArticleListLoading = this._articleListStore.isFetchArticleListLoading; @@ -53,4 +65,8 @@ export class FeatureLatestArticlesComponent { excludeCategory: 'angular-in-depth-en', }); } + + navigateFromBanner() { + this._router.navigate([this.cardBanner()?.url]); + } } diff --git a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts index 65a68224..044ed599 100644 --- a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts +++ b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts @@ -15,13 +15,26 @@ import { import { SearchComponent } from '@angular-love/blog/search/feature-search'; import { AdImageBanner, + AdImageBannerComponent, AlBannerCarouselComponent, } from '@angular-love/blog/shared/ad-banner'; @Component({ selector: 'al-root-shell', template: ` -
+
+ @if (topBanner(); as topBanner) { + + }
- + @if (slides(); as slides) { } @@ -46,14 +59,15 @@ import { SearchComponent, NgClass, AlBannerCarouselComponent, + AdImageBannerComponent, ], }) export class RootShellComponent { - protected readonly sliderStore = inject(AdBannerStore); + protected readonly bannerStore = inject(AdBannerStore); protected readonly slides = computed(() => - this.sliderStore.slider()?.slides.map((slide) => ({ - url: slide.url, - alt: slide.alt, + this.bannerStore.banners()?.slider?.slides.map((slide) => ({ + url: slide.url!, + alt: slide.alt!, action: { type: 'url', url: slide.navigateTo, @@ -61,14 +75,14 @@ export class RootShellComponent { })), ); protected readonly msPerSlide = computed( - () => this.sliderStore.slider()?.slideDisplayTimeMs, + () => this.bannerStore.banners()?.slider?.slideDisplayTimeMs, + ); + protected readonly topBanner = computed( + () => this.bannerStore.banners()?.topBanner, ); readonly translocoService = inject(TranslocoService); - // todo: temporary solution to keep in mind how banner influence the layout - protected readonly adBannerVisible = computed(() => false); - readonly language = toSignal( this.translocoService.langChanges$.pipe( startWith(this.translocoService.getActiveLang()), @@ -87,13 +101,7 @@ export class RootShellComponent { ); } - constructor(viewport: ViewportScroller) { - // todo: temporary solution to keep in mind how banner influence the layout - effect(() => { - this.adBannerVisible() - ? viewport.setOffset([0, 160]) - : viewport.setOffset([0, 80]); - }); - this.sliderStore.getData(); + constructor() { + this.bannerStore.getData(); } } From 87d4b61458d32f0d7f35100323a54b36611ea10b Mon Sep 17 00:00:00 2001 From: DominikKalinowski Date: Wed, 12 Feb 2025 17:59:06 +0100 Subject: [PATCH 2/3] feat: move newsletter banner to separate component, use navigateTo for navigation --- libs/blog/ad-banner/ui/src/index.ts | 1 + .../al-newsletter-banner.component.ts | 44 +++++++++++++++++++ .../feature-latest-articles.component.html | 44 +------------------ .../feature-latest-articles.component.ts | 16 ++----- .../src/lib/root-shell.component.ts | 6 +-- 5 files changed, 53 insertions(+), 58 deletions(-) create mode 100644 libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts diff --git a/libs/blog/ad-banner/ui/src/index.ts b/libs/blog/ad-banner/ui/src/index.ts index 09b611ad..40009e49 100644 --- a/libs/blog/ad-banner/ui/src/index.ts +++ b/libs/blog/ad-banner/ui/src/index.ts @@ -2,3 +2,4 @@ export * from './lib/ad-image-banner/ad-image-banner.component'; export * from './lib/ad-image-banner/ad-image-banner-data.interface'; export * from './lib/instances/al-indepth-banner.component'; export * from './lib/banner-carousel/al-banner-carousel.component'; +export * from './lib/newsletter-banner/al-newsletter-banner.component'; diff --git a/libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts b/libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts new file mode 100644 index 00000000..86541647 --- /dev/null +++ b/libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts @@ -0,0 +1,44 @@ +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { CardBanner } from '@angular-love/blog/contracts/banners'; + +@Component({ + selector: 'al-newsletter-banner', + imports: [NgOptimizedImage], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + `, +}) +export class AlNewsletterBannerComponent { + readonly cardBanner = input.required(); +} diff --git a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html index b2c30754..23742412 100644 --- a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html +++ b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html @@ -21,50 +21,10 @@ } @else { } -
+
@if (cardBanner()) { - - + } @else { } diff --git a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts index 15c05b74..42c9b75d 100644 --- a/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts +++ b/libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts @@ -1,11 +1,10 @@ -import { NgClass, NgOptimizedImage } from '@angular/common'; +import { NgClass } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, } from '@angular/core'; -import { Router } from '@angular/router'; import { TranslocoDirective } from '@jsverse/transloco'; import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access'; @@ -16,6 +15,7 @@ import { } from '@angular-love/blog/articles/ui-article-card'; import { UiArticleListTitleComponent } from '@angular-love/blog/articles/ui-article-list-title'; import { NewsletterComponent } from '@angular-love/blog/newsletter'; +import { AlNewsletterBannerComponent } from '@angular-love/blog/shared/ad-banner'; import { CardComponent, CardContentDirective, @@ -36,7 +36,7 @@ import { RepeatDirective } from '@angular-love/utils'; ArticleRegularCardSkeletonComponent, RepeatDirective, CardContentDirective, - NgOptimizedImage, + AlNewsletterBannerComponent, ], host: { 'data-testid': 'latest-articles-container', @@ -45,18 +45,12 @@ import { RepeatDirective } from '@angular-love/utils'; }) export class FeatureLatestArticlesComponent { private readonly _articleListStore = inject(ArticleListStore); - private readonly _router = inject(Router); private readonly _bannerStore = inject(AdBannerStore); protected readonly cardBanner = computed( () => this._bannerStore.banners()?.cardBanner, ); - readonly isFetchArticleListLoading = this._articleListStore.isFetchArticleListLoading; - - readonly isFetchArticleListError = - this._articleListStore.isFetchArticleListError; - readonly articles = this._articleListStore.articles; constructor() { @@ -65,8 +59,4 @@ export class FeatureLatestArticlesComponent { excludeCategory: 'angular-in-depth-en', }); } - - navigateFromBanner() { - this._router.navigate([this.cardBanner()?.url]); - } } diff --git a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts index 044ed599..f8f3a02f 100644 --- a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts +++ b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts @@ -1,5 +1,5 @@ -import { NgClass, ViewportScroller } from '@angular/common'; -import { Component, computed, effect, inject } from '@angular/core'; +import { NgClass } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { Router, RouterOutlet } from '@angular/router'; import { TranslocoService } from '@jsverse/transloco'; @@ -30,7 +30,7 @@ import { alt: topBanner.alt!, action: { type: 'url', - url: '', + url: topBanner.navigateTo!, }, }" /> From fd83adb8e39bda3bd30e7096ed3fd3d290f724a0 Mon Sep 17 00:00:00 2001 From: Marcin Stelmaszyk Date: Mon, 19 May 2025 14:33:27 +0200 Subject: [PATCH 3/3] feat: changed TopBanner type --- libs/blog-contracts/banners/src/lib/banners.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/blog-contracts/banners/src/lib/banners.ts b/libs/blog-contracts/banners/src/lib/banners.ts index 8e57f085..92646d2b 100644 --- a/libs/blog-contracts/banners/src/lib/banners.ts +++ b/libs/blog-contracts/banners/src/lib/banners.ts @@ -8,9 +8,10 @@ export interface Slider { } export interface TopBanner { - url?: string; - alt?: string; - navigateTo: string; + navigateTo?: string; + buttonText?: string; + text: string; + buttonPosition: 'left' | 'right'; } export interface CardBanner {