Skip to content

feat(main): newsletter & top banners #380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions libs/blog-bff/banners/api/src/lib/dtos.ts
Original file line number Diff line number Diff line change
@@ -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 */;
};
}

Expand Down
49 changes: 37 additions & 12 deletions libs/blog-bff/banners/api/src/lib/mappers.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}),
};
};
22 changes: 20 additions & 2 deletions libs/blog-contracts/banners/src/lib/banners.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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' })
export class AdBannerService {
private readonly _apiBaseUrl = inject(ConfigService).get('apiBaseUrl');
private readonly _http = inject(HttpClient);

getBannerSlider() {
return this._http.get<Slider>(`${this._apiBaseUrl}/banners`);
getVisibleBanners() {
return this._http.get<Banners>(`${this._apiBaseUrl}/banners`);
}
Comment on lines +12 to 14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we host this data as a json asset? it would be created in the build time as a part of the process in apps/blog/scripts/build-routes.mjs. This resource can be received via http client, similarly to libs/blog/articles/data-access/src/lib/guards/article-exists.guard.ts.

With current implementation we fetch banners from BFF for every available page. Basically it adds an extra request during prerendering. If we prerender 500 pages, it ends up with BFF 1000 request, where 50% is the same banners request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, we can do that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pr resolves the issue #415

}
12 changes: 6 additions & 6 deletions libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -23,10 +23,10 @@ export const AdBannerStore = signalStore(
getData: rxMethod<void>(
pipe(
switchMap(() =>
adBannerService.getBannerSlider().pipe(
adBannerService.getVisibleBanners().pipe(
tapResponse({
next: (slider) => {
patchState(store, { slider });
next: (banners) => {
patchState(store, { banners });
},
error: () => {
patchState(store);
Expand Down
1 change: 1 addition & 0 deletions libs/blog/ad-banner/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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()"
Expand Down
Original file line number Diff line number Diff line change
@@ -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: `
<a
class="relative flex h-full items-center"
[href]="cardBanner().navigateTo!"
>
<aside class="absolute h-full w-full overflow-hidden rounded-lg">
<img
tabindex="0"
role="button"
class="!relative object-cover shadow-inner blur-xl"
[attr.aria-label]="cardBanner()!.alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
<aside>
<img
tabindex="0"
role="button"
class="!relative"
[attr.aria-label]="cardBanner().alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
</a>
`,
})
export class AlNewsletterBannerComponent {
readonly cardBanner = input.required<CardBanner>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ <h2 id="article-title" class="flex text-[40px] font-bold">
<div
class="sticky top-24 mt-5 hidden flex-col gap-4 lg:flex"
[ngClass]="{
'top-24': !adBannerStoreVisible(),
'top-48': adBannerStoreVisible()
'top-24': !bannerStore.banners()?.topBanner,
'top-48': bannerStore.banners()?.topBanner,
}"
>
@if (articleDetails().anchors.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { DatePipe, NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
input,
signal,
} from '@angular/core';
import { FastSvgComponent } from '@push-based/ngx-fast-svg';

import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access';
import { GiscusCommentsComponent } from '@angular-love/blog/articles/feature-comments';
import { RelatedArticlesComponent } from '@angular-love/blog/articles/feature-related-articles';
import { ArticleCompactCardSkeletonComponent } from '@angular-love/blog/articles/ui-article-card';
Expand Down Expand Up @@ -57,5 +58,5 @@ import { ArticleShareIconsComponent } from '../article-share-icons/article-share
})
export class ArticleDetailsComponent {
readonly articleDetails = input.required<Article>();
protected readonly adBannerStoreVisible = signal(false);
protected readonly bannerStore = inject(AdBannerStore);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
} @else {
<al-article-regular-card-skeleton *alRepeat="4" />
}
<al-card
alGradientCard
class="md:max-lg:col-span-2 lg:col-start-3 lg:row-start-1"
>
<al-newsletter alCardContent />
</al-card>
<div class="md:max-lg:col-span-2 lg:col-start-3 lg:row-start-1">
<!-- when "newsletter banner" exist put it in place of the real newsletter -->
@if (cardBanner()) {
<al-newsletter-banner [cardBanner]="cardBanner()!" />
} @else {
<al-newsletter alCardContent />
}
</div>
</section>
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { NgClass } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core';
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,
UiArticleCardComponent,
} 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,
GradientCardDirective,
CardContentDirective,
} from '@angular-love/blog/shared/ui-card';
import { RepeatDirective } from '@angular-love/utils';

Expand All @@ -24,12 +31,12 @@ import { RepeatDirective } from '@angular-love/utils';
UiArticleCardComponent,
NewsletterComponent,
CardComponent,
GradientCardDirective,
NgClass,
TranslocoDirective,
ArticleRegularCardSkeletonComponent,
CardComponent,
RepeatDirective,
CardContentDirective,
AlNewsletterBannerComponent,
],
host: {
'data-testid': 'latest-articles-container',
Expand All @@ -38,13 +45,12 @@ import { RepeatDirective } from '@angular-love/utils';
})
export class FeatureLatestArticlesComponent {
private readonly _articleListStore = inject(ArticleListStore);

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() {
Expand Down
Loading