Skip to content

Commit 59e21c9

Browse files
committed
feat: introduce card with dominant border color
* put search into its own field * rework navigation to be a card CU-86c32zqk0
1 parent 8055517 commit 59e21c9

21 files changed

+305
-130
lines changed

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@typescript-eslint/eslint-plugin": "7",
3232
"@typescript-eslint/parser": "7",
3333
"bootstrap": "^5.3.3",
34+
"colorthief": "^2.6.0",
3435
"eslint-plugin-import": "^2.31.0",
3536
"eslint-plugin-jsdoc": "^50.6.8",
3637
"express": "^4.18.2",
6.74 KB
Loading

website/src/app/features/template-details/import-dialog/import-dialog.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ <h5 class="card-text mt-3">to continue using this building block template</h5>
5050
<p>Check it out and how it can help your platform team</p>
5151
<a href="https://www.meshcloud.io/en/book-demo" class="btn btn-outline-primary mt-2">
5252
Discover meshStack
53-
<img src="assets/meshstack-logo.png" alt="meshStack Logo" class="button-image" />
53+
<img src="assets/meshstack-logo-black-white.png" alt="meshStack Logo" class="button-image" />
5454
</a>
5555
</div>
5656
</div>

website/src/app/features/template-details/import-dialog/import-dialog.component.scss

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,3 @@
22
width: 5rem;
33
object-fit: contain;
44
}
5-
6-
.button-image {
7-
width: 2rem;
8-
object-fit: contain;
9-
}

website/src/app/features/template-gallery/template-gallery.component.html

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
1-
<div class="header pt-0 ps-5 pe-5 pb-2">
2-
<div class="row w-25">
3-
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
4-
<div class="input-group mt-3 me-auto">
5-
<input
6-
type="text"
7-
class="form-control"
8-
aria-label="Search"
9-
formControlName="searchTerm"
10-
(keyup.enter)="onSearch()"
11-
placeholder="Search"
12-
/>
13-
<button class="btn btn-primary" type="submit" (click)="onSearch()">
14-
<i class="fa-solid fa-magnifying-glass"></i>
15-
</button>
16-
</div>
17-
</form>
18-
</div>
19-
<div class="pt-5">
1+
<div class="pt-5 ps-5 pe-5">
2+
<ng-container>
203
<mst-navigation></mst-navigation>
21-
</div>
22-
</div>
4+
<div *ngIf="!(isSearch$ | async)" class="pt-5 pb-5">
5+
<h2>All building block definitions</h2>
6+
These are pre-configured Terraform modules for automating common cloud tasks across AWS, Azure, GCP, and custom
7+
cloud platforms, enabling rapid and consistent infrastructure provisioning and management
8+
</div>
9+
</ng-container>
2310

24-
<div class="pt-4 ps-5 pe-5">
2511
<div class="row">
2612
<ng-container *ngIf="templates$ | async as templates">
27-
<p class="pb-2" *ngIf="isSearch && templates.length">{{ templates.length }} templates found</p>
13+
<p class="pb-2" *ngIf="(isSearch$ | async) && templates.length">{{ templates.length }} templates found</p>
2814
<ng-container *ngIf="templates.length > 0; else noTemplates">
2915
<div class="col-sm-3 mb-3" *ngFor="let template of templates">
3016
<mst-card [card]="template"></mst-card>
Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, OnDestroy, OnInit } from '@angular/core';
3-
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
3+
import { ReactiveFormsModule } from '@angular/forms';
44
import { ActivatedRoute, Router } from '@angular/router';
5-
import { Observable, Subscription, forkJoin, map, tap } from 'rxjs';
5+
import { Observable, Subscription, forkJoin, map } from 'rxjs';
66

77
import { PlatformType } from 'app/core';
88
import { CardComponent } from 'app/shared/card';
99
import { Card } from 'app/shared/card/card';
1010
import { NavigationComponent } from 'app/shared/navigation';
1111
import { PlatformLogoData, PlatformLogoService } from 'app/shared/platform-logo';
12+
import { SearchService } from 'app/shared/search-bar/search.service';
1213
import { TemplateService } from 'app/shared/template';
1314

1415
@Component({
@@ -21,42 +22,35 @@ import { TemplateService } from 'app/shared/template';
2122
export class TemplateGalleryComponent implements OnInit, OnDestroy {
2223
public templates$!: Observable<Card[]>;
2324

24-
public searchForm!: FormGroup;
25-
26-
public isSearch = false;
25+
public isSearch$!: Observable<boolean>;
2726

2827
private paramSubscription!: Subscription;
2928

29+
private searchSubscription!: Subscription;
30+
3031
private logos$!: Observable<PlatformLogoData>;
3132

3233
constructor(
3334
private router: Router,
3435
private route: ActivatedRoute,
35-
private fb: FormBuilder,
3636
private templateService: TemplateService,
37+
private searchService: SearchService,
3738
private platformLogoService: PlatformLogoService
38-
) {}
39+
) { }
3940

4041
public ngOnInit(): void {
41-
this.initializeSearchForm();
4242
this.subscribeToRouteParams();
43+
this.subscribeToSearchTerm();
44+
this.isSearch$ = this.searchService.getSearchTerm$()
45+
.pipe(
46+
map(searchTerm =>
47+
searchTerm === '' ? false : true)
48+
);
4349
}
4450

4551
public ngOnDestroy(): void {
4652
this.paramSubscription.unsubscribe();
47-
}
48-
49-
public onSearch(): void {
50-
const searchTerm = this.searchForm.value.searchTerm;
51-
this.templates$ = this.getTemplatesWithLogos(this.templateService.search(searchTerm));
52-
this.isSearch = !!searchTerm;
53-
this.router.navigate(['/all']);
54-
}
55-
56-
private initializeSearchForm(): void {
57-
this.searchForm = this.fb.group({
58-
searchTerm: ['']
59-
});
53+
this.searchSubscription.unsubscribe();
6054
}
6155

6256
private subscribeToRouteParams(): void {
@@ -69,19 +63,25 @@ export class TemplateGalleryComponent implements OnInit, OnDestroy {
6963
});
7064
}
7165

66+
private subscribeToSearchTerm(): void {
67+
this.searchSubscription = this.searchService.searchTerm$.subscribe(searchTerm => {
68+
this.templates$ = this.getTemplatesWithLogos(this.templateService.search(searchTerm));
69+
this.router.navigate(['/all']);
70+
});
71+
}
72+
7273
private getTemplatesWithLogos(templateObs$: Observable<any>): Observable<Card[]> {
7374
return forkJoin({
7475
templates: templateObs$,
7576
logos: this.logos$
7677
})
7778
.pipe(
78-
tap(() => (this.isSearch = false)),
7979
map(({ templates, logos }) =>
8080
templates.map(item => ({
8181
cardLogo: item.logo,
8282
title: item.name,
8383
description: item.description,
84-
detailsRoute: `/template/${item.id}`,
84+
routePath: `/template/${item.id}`,
8585
supportedPlatforms: item.supportedPlatforms.map(platform => ({
8686
platformType: platform,
8787
imageUrl: logos[item.platformType] ?? null
@@ -90,5 +90,4 @@ export class TemplateGalleryComponent implements OnInit, OnDestroy {
9090
)
9191
);
9292
}
93-
9493
}

website/src/app/shared/card/card.component.html

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
<div
2-
class="card text-center"
2+
class="card"
33
tabindex="0"
44
role="card"
55
[attr.aria-label]="card.title"
6-
(click)="goToDetails()"
7-
(keyup.enter)="goToDetails()"
6+
(click)="navigateToRoutePath()"
7+
(keyup.enter)="navigateToRoutePath()"
8+
[ngStyle]="config.borderDominantColorOfLogo ? { 'border-color': borderColor, 'border-width': '0.15rem' } : {}"
89
>
9-
<span class="card-label">Building Block</span>
10-
1110
<div class="card-body">
12-
<div class="d-flex justify-content-center">
13-
<div class="circle">
11+
<div
12+
class="d-flex justify-content-center mt-2"
13+
[ngClass]="config.titleNextToLogo ? 'flex-row align-items-stretch' : 'flex-column align-items-center'"
14+
>
15+
<div class="circle shadow" [ngClass]="{ 'circle-sm': config.titleNextToLogo }">
1416
<img *ngIf="card.cardLogo; else unknownLogo" [src]="card.cardLogo" [alt]="'Logo for ' + card.title" />
1517
<ng-template #unknownLogo>
1618
<img src="assets/meshstack-logo.png" alt="Unknown Logo" />
1719
</ng-template>
1820
</div>
19-
</div>
20-
<div class="d-flex justify-content-center mt-3">
21-
<div class="container">
22-
<h5 class="card-title fw-bold">{{ card.title }}</h5>
23-
<p class="card-text text-start mt-3">{{ card.description }}</p>
21+
<div class="d-flex">
22+
<h5
23+
class="card-title align-self-center text-center fw-bold m-0"
24+
[ngClass]="config.titleNextToLogo ? 'ms-3 h6' : 'mt-3'"
25+
>
26+
{{ card.title }}
27+
</h5>
2428
</div>
2529
</div>
30+
<div class="d-flex mt-3 justify-content-center">
31+
<p class="card-text ps-3 pe-3">{{ card.description }}</p>
32+
</div>
2633
</div>
2734

28-
<div class="card-footer">
35+
<div class="card-footer" *ngIf="config.showFooter">
2936
<div class="d-flex flex-row-reverse justify-content-between">
3037
<ng-container *ngFor="let platform of card.supportedPlatforms">
3138
<img [src]="platform.imageUrl" [alt]="platform.platformType" class="footer-icon" />

website/src/app/shared/card/card.component.scss

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
transition: transform 0.3s;
66
cursor: pointer;
77
box-shadow: 0 0.5rem 1rem rgba(black, 0.15);
8+
border-radius: 0.5rem; // Added to make the card border rounder
89

910
&:hover {
1011
transform: scale(1.05);
@@ -32,13 +33,18 @@
3233
}
3334

3435
.circle {
35-
width: 5rem;
36-
height: 5rem;
36+
width: 5.5rem;
37+
height: 5.5rem;
3738
border-radius: 50%;
3839
display: flex;
3940
justify-content: center;
4041
align-items: center;
4142

43+
&.circle-sm {
44+
width: 3rem;
45+
height: 3rem;
46+
}
47+
4248
img {
4349
width: 75%;
4450
height: 75%;
Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, Input } from '@angular/core';
33
import { Router, RouterModule } from '@angular/router';
4+
import ColorThief from 'colorthief';
45

5-
import { Card } from './card';
6+
import { Card, CardConfig } from './card';
67

78
@Component({
89
selector: 'mst-card',
@@ -13,12 +14,58 @@ import { Card } from './card';
1314
})
1415
export class CardComponent {
1516
@Input()
16-
public card!: Card;
17+
public set card(value: Card) {
18+
this._card = value;
19+
this.updateBorderColor();
20+
}
1721

18-
constructor(private router: Router) {}
22+
public get card(): Card {
23+
return this._card;
24+
}
1925

20-
public goToDetails() {
21-
this.router.navigate([this.card.detailsRoute]);
26+
@Input()
27+
public set config(value: CardConfig) {
28+
this._config = value;
29+
this.updateBorderColor();
2230
}
23-
}
2431

32+
public get config(): CardConfig {
33+
return this._config;
34+
}
35+
36+
public borderColor = '';
37+
38+
private _card!: Card;
39+
40+
private _config: CardConfig = {
41+
titleNextToLogo: false,
42+
showFooter: true,
43+
borderDominantColorOfLogo: false
44+
};
45+
46+
constructor(private router: Router) { }
47+
48+
public navigateToRoutePath(): void {
49+
if (this.card.routePath) {
50+
this.router.navigate([this.card.routePath]);
51+
}
52+
}
53+
54+
private updateBorderColor(): void {
55+
if (!this.card?.cardLogo || !this.config.borderDominantColorOfLogo) {
56+
this.borderColor = '';
57+
58+
return;
59+
}
60+
61+
if (typeof window !== 'undefined') {
62+
const img = new Image();
63+
img.src = this.card.cardLogo;
64+
img.onload = () => {
65+
const colorThief = new ColorThief();
66+
const dominantColor = colorThief.getColor(img);
67+
this.borderColor = `rgb(${dominantColor.join(',')})`;
68+
};
69+
}
70+
}
71+
}

website/src/app/shared/card/card.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { PlatformType } from 'app/core';
33
export interface Card {
44
cardLogo: string | null;
55
title: string;
6-
description: string;
7-
detailsRoute: string;
6+
description: string | null;
7+
routePath: string;
88
supportedPlatforms: { platformType: PlatformType; imageUrl: string }[];
99
}
10+
11+
export interface CardConfig {
12+
titleNextToLogo: boolean;
13+
showFooter: boolean;
14+
borderDominantColorOfLogo: boolean;
15+
}

0 commit comments

Comments
 (0)