Skip to content

Commit 927dee7

Browse files
feat(main): newsletter & top banners
1 parent 9b892fc commit 927dee7

File tree

11 files changed

+177
-54
lines changed

11 files changed

+177
-54
lines changed

libs/blog-bff/banners/api/src/lib/dtos.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
export interface WPBannerDto {
22
id: number;
33
acf: {
4+
is_slider_banner_displayed: boolean;
45
display_time: string;
56
slides: {
6-
slide_image: number /* slideId */;
7+
slide_image_desktop: number /* slideId */;
8+
slide_image_mobile: number /* slideId */;
79
slide_url: string /* url to navigate to after click */;
810
}[];
11+
12+
is_top_banner_displayed: boolean;
13+
top_banner_image_desktop: number /* mediaId */;
14+
top_banner_image_mobile: number /* mediaId */;
15+
top_banner_image_url: string /* url to navigate to after click */;
16+
17+
is_card_banner_displayed: boolean;
18+
card_banner_image: number /* mediaId */;
19+
card_banner_url: string /* url to navigate to after click */;
920
};
1021
}
1122

+36-10
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
1-
import { Slider } from '@angular-love/blog/contracts/banners';
1+
import { Banners } from '@angular-love/blog/contracts/banners';
22

33
import { WPBannerDto, WPBannerMediaDto } from './dtos';
44

55
export const toBanner = (
66
dto: WPBannerDto,
77
mediaDto: WPBannerMediaDto[],
8-
): Slider => {
8+
): Banners => {
99
return {
10-
slideDisplayTimeMs: +dto.acf.display_time,
11-
slides: dto.acf.slides.map((slide) => {
12-
const media = mediaDto.find((media) => media.id === slide.slide_image)!;
13-
return {
14-
url: media.guid.rendered,
15-
alt: media.alt_text,
16-
navigateTo: slide.slide_url,
17-
};
10+
...(dto.acf.is_slider_banner_displayed && {
11+
slider: {
12+
slideDisplayTimeMs: +dto.acf.display_time,
13+
slides: dto.acf.slides.map((slide) => {
14+
const media = mediaDto.find(
15+
(media) => media.id === slide.slide_image_desktop,
16+
)!;
17+
return {
18+
url: media?.guid.rendered,
19+
alt: media?.alt_text,
20+
navigateTo: slide.slide_url,
21+
};
22+
}),
23+
},
24+
}),
25+
...(dto.acf.is_top_banner_displayed && {
26+
topBanner: {
27+
url: mediaDto.find(
28+
(media) => media.id === dto.acf.top_banner_image_desktop,
29+
)?.guid.rendered,
30+
alt: mediaDto.find(
31+
(media) => media.id === dto.acf.top_banner_image_desktop,
32+
)?.alt_text,
33+
navigateTo: dto.acf.top_banner_image_url,
34+
},
35+
}),
36+
...(dto.acf.is_card_banner_displayed && {
37+
cardBanner: {
38+
url: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
39+
?.guid.rendered,
40+
alt: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
41+
?.alt_text,
42+
navigateTo: dto.acf.card_banner_url,
43+
},
1844
}),
1945
};
2046
};
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
export interface Slider {
22
slideDisplayTimeMs: number;
33
slides: {
4-
url: string;
5-
alt: string;
4+
url?: string;
5+
alt?: string;
66
navigateTo: string;
77
}[];
88
}
9+
10+
export interface TopBanner {
11+
url?: string;
12+
alt?: string;
13+
navigateTo: string;
14+
}
15+
16+
export interface CardBanner {
17+
url?: string;
18+
alt?: string;
19+
navigateTo: string;
20+
}
21+
22+
export interface Banners {
23+
slider?: Slider;
24+
topBanner?: TopBanner;
25+
cardBanner?: CardBanner;
26+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { HttpClient } from '@angular/common/http';
22
import { inject, Injectable } from '@angular/core';
33

4-
import { Slider } from '@angular-love/blog/contracts/banners';
4+
import { Banners } from '@angular-love/blog/contracts/banners';
55
import { ConfigService } from '@angular-love/shared/config';
66

77
@Injectable({ providedIn: 'root' })
88
export class AdBannerService {
99
private readonly _apiBaseUrl = inject(ConfigService).get('apiBaseUrl');
1010
private readonly _http = inject(HttpClient);
1111

12-
getBannerSlider() {
13-
return this._http.get<Slider>(`${this._apiBaseUrl}/banners`);
12+
getVisibleBanners() {
13+
return this._http.get<Banners>(`${this._apiBaseUrl}/banners`);
1414
}
1515
}

libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
44
import { rxMethod } from '@ngrx/signals/rxjs-interop';
55
import { pipe, switchMap } from 'rxjs';
66

7-
import { Slider } from '@angular-love/blog/contracts/banners';
7+
import { Banners } from '@angular-love/blog/contracts/banners';
88

99
import { AdBannerService } from '../infrastructure/ad-banner.service';
1010

1111
type AdBannerState = {
12-
slider: Slider | null;
12+
banners: Banners | null;
1313
};
1414

1515
const initialState: AdBannerState = {
16-
slider: null,
16+
banners: null,
1717
};
1818

1919
export const AdBannerStore = signalStore(
@@ -23,10 +23,10 @@ export const AdBannerStore = signalStore(
2323
getData: rxMethod<void>(
2424
pipe(
2525
switchMap(() =>
26-
adBannerService.getBannerSlider().pipe(
26+
adBannerService.getVisibleBanners().pipe(
2727
tapResponse({
28-
next: (slider) => {
29-
patchState(store, { slider });
28+
next: (banners) => {
29+
patchState(store, { banners });
3030
},
3131
error: () => {
3232
patchState(store);

libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner.component.html

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
role="button"
55
class="!relative cursor-pointer"
66
[attr.aria-label]="banner().alt"
7+
[alt]="banner().alt"
78
[ngSrc]="banner().url"
89
(click)="navigateFromBanner()"
910
(keydown.enter)="navigateFromBanner()"

libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ <h2 id="article-title" class="flex text-[40px] font-bold">
6262
<div
6363
class="sticky top-24 mt-5 hidden flex-col gap-4 lg:flex"
6464
[ngClass]="{
65-
'top-24': !adBannerStoreVisible(),
66-
'top-48': adBannerStoreVisible()
65+
'top-24': !bannerStore.banners()?.topBanner,
66+
'top-48': bannerStore.banners()?.topBanner,
6767
}"
6868
>
6969
@if (articleDetails().anchors.length) {

libs/blog/articles/feature-article/src/lib/article-details/article-details.component.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { DatePipe, NgClass } from '@angular/common';
22
import {
33
ChangeDetectionStrategy,
44
Component,
5+
inject,
56
input,
6-
signal,
77
} from '@angular/core';
88
import { FastSvgComponent } from '@push-based/ngx-fast-svg';
99

10+
import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access';
1011
import { GiscusCommentsComponent } from '@angular-love/blog/articles/feature-comments';
1112
import { RelatedArticlesComponent } from '@angular-love/blog/articles/feature-related-articles';
1213
import { ArticleCompactCardSkeletonComponent } from '@angular-love/blog/articles/ui-article-card';
@@ -57,5 +58,5 @@ import { ArticleShareIconsComponent } from '../article-share-icons/article-share
5758
})
5859
export class ArticleDetailsComponent {
5960
readonly articleDetails = input.required<Article>();
60-
protected readonly adBannerStoreVisible = signal(false);
61+
protected readonly bannerStore = inject(AdBannerStore);
6162
}

libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html

+47-5
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,52 @@
2121
} @else {
2222
<al-article-regular-card-skeleton *alRepeat="4" />
2323
}
24-
<al-card
25-
alGradientCard
26-
class="md:max-lg:col-span-2 lg:col-start-3 lg:row-start-1"
24+
<div
25+
class="relative flex items-center md:max-lg:col-span-2 lg:col-start-3 lg:row-start-1"
2726
>
28-
<al-newsletter alCardContent />
29-
</al-card>
27+
<!-- when "newsletter banner" exist put it in place of the real newsletter -->
28+
@if (cardBanner()) {
29+
<aside
30+
style="
31+
width: 100%;
32+
height: 100%;
33+
position: absolute;
34+
overflow: hidden;
35+
border-radius: 8px;
36+
"
37+
>
38+
<img
39+
tabindex="0"
40+
role="button"
41+
class="!relative cursor-pointer shadow-inner blur-xl"
42+
[attr.aria-label]="cardBanner()!.alt!"
43+
[alt]="cardBanner()!.alt!"
44+
[ngSrc]="cardBanner()!.url!"
45+
(click)="navigateFromBanner()"
46+
(keydown.enter)="navigateFromBanner()"
47+
style="object-fit: cover"
48+
fill
49+
priority
50+
/>
51+
</aside>
52+
<aside>
53+
<div style="box-shadow: 0 0 8px 8px blue inset">
54+
<img
55+
tabindex="0"
56+
role="button"
57+
class="!relative cursor-pointer"
58+
[attr.aria-label]="cardBanner()!.alt!"
59+
[alt]="cardBanner()!.alt!"
60+
[ngSrc]="cardBanner()!.url!"
61+
(click)="navigateFromBanner()"
62+
(keydown.enter)="navigateFromBanner()"
63+
fill
64+
priority
65+
/>
66+
</div>
67+
</aside>
68+
} @else {
69+
<al-newsletter alCardContent />
70+
}
71+
</div>
3072
</section>

libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { NgClass } from '@angular/common';
2-
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
1+
import { NgClass, NgOptimizedImage } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
inject,
7+
} from '@angular/core';
8+
import { Router } from '@angular/router';
39
import { TranslocoDirective } from '@jsverse/transloco';
410

11+
import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access';
512
import { ArticleListStore } from '@angular-love/blog/articles/data-access';
613
import {
714
ArticleRegularCardSkeletonComponent,
@@ -11,7 +18,7 @@ import { UiArticleListTitleComponent } from '@angular-love/blog/articles/ui-arti
1118
import { NewsletterComponent } from '@angular-love/blog/newsletter';
1219
import {
1320
CardComponent,
14-
GradientCardDirective,
21+
CardContentDirective,
1522
} from '@angular-love/blog/shared/ui-card';
1623
import { RepeatDirective } from '@angular-love/utils';
1724

@@ -24,12 +31,12 @@ import { RepeatDirective } from '@angular-love/utils';
2431
UiArticleCardComponent,
2532
NewsletterComponent,
2633
CardComponent,
27-
GradientCardDirective,
2834
NgClass,
2935
TranslocoDirective,
3036
ArticleRegularCardSkeletonComponent,
31-
CardComponent,
3237
RepeatDirective,
38+
CardContentDirective,
39+
NgOptimizedImage,
3340
],
3441
host: {
3542
'data-testid': 'latest-articles-container',
@@ -38,6 +45,11 @@ import { RepeatDirective } from '@angular-love/utils';
3845
})
3946
export class FeatureLatestArticlesComponent {
4047
private readonly _articleListStore = inject(ArticleListStore);
48+
private readonly _router = inject(Router);
49+
private readonly _bannerStore = inject(AdBannerStore);
50+
protected readonly cardBanner = computed(
51+
() => this._bannerStore.banners()?.cardBanner,
52+
);
4153

4254
readonly isFetchArticleListLoading =
4355
this._articleListStore.isFetchArticleListLoading;
@@ -53,4 +65,8 @@ export class FeatureLatestArticlesComponent {
5365
excludeCategory: 'angular-in-depth-en',
5466
});
5567
}
68+
69+
navigateFromBanner() {
70+
this._router.navigate([this.cardBanner()?.url]);
71+
}
5672
}

0 commit comments

Comments
 (0)