diff --git a/website/package.json b/website/package.json index 1bbc427..c7f95cf 100644 --- a/website/package.json +++ b/website/package.json @@ -31,6 +31,7 @@ "@typescript-eslint/eslint-plugin": "7", "@typescript-eslint/parser": "7", "bootstrap": "^5.3.3", + "colorthief": "^2.6.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.8", "express": "^4.18.2", diff --git a/website/public/assets/meshstack-logo-black-white.png b/website/public/assets/meshstack-logo-black-white.png new file mode 100644 index 0000000..68fd693 Binary files /dev/null and b/website/public/assets/meshstack-logo-black-white.png differ diff --git a/website/src/app/app.routes.ts b/website/src/app/app.routes.ts index 2822989..6243426 100644 --- a/website/src/app/app.routes.ts +++ b/website/src/app/app.routes.ts @@ -1,7 +1,25 @@ import { Routes } from '@angular/router'; +const loadTemplateGallery = () => import('./features/template-gallery').then(m => m.TemplateGalleryComponent); +const loadTemplateDetails = () => import('./features/template-details').then(m => m.TemplateDetailsComponent); +const loadPlatformView = () => import('./features/platform-view').then(m => m.PlatformViewComponent); + export const routes: Routes = [ - { path: 'template/:id', loadComponent: () => import('./features/template-details').then(m => m.TemplateDetailsComponent) }, - { path: ':type', loadComponent: () => import('./features/template-gallery').then(m => m.TemplateGalleryComponent) }, - { path: '', redirectTo: '/all', pathMatch: 'full' } + { + path: 'all', + loadComponent: loadTemplateGallery + }, + { + path: 'platforms/:type', + loadComponent: loadPlatformView, + }, + { + path: 'platforms/:type/definitions/:id', + loadComponent: loadTemplateDetails + }, + { + path: 'definitions/:id', + loadComponent: loadTemplateDetails + }, + { path: '', redirectTo: '/all', pathMatch: 'full' }, ]; \ No newline at end of file diff --git a/website/src/app/features/platform-view/index.ts b/website/src/app/features/platform-view/index.ts new file mode 100644 index 0000000..18f82f3 --- /dev/null +++ b/website/src/app/features/platform-view/index.ts @@ -0,0 +1 @@ +export * from './platform-view.component'; \ No newline at end of file diff --git a/website/src/app/features/platform-view/platform-view.component.html b/website/src/app/features/platform-view/platform-view.component.html new file mode 100644 index 0000000..ec8a002 --- /dev/null +++ b/website/src/app/features/platform-view/platform-view.component.html @@ -0,0 +1,28 @@ +
+
+ +
+ +
+ {{ platform.title }} +
+
+
+
+ +

Platform building block definitions

+ These building block definitions provide pre-configured Terraform modules for automating common cloud tasks + +
+ + +
+ +
+
+ +

No building block definitions

+
+
+
+
\ No newline at end of file diff --git a/website/src/app/features/platform-view/platform-view.component.scss b/website/src/app/features/platform-view/platform-view.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/website/src/app/features/platform-view/platform-view.component.spec.ts b/website/src/app/features/platform-view/platform-view.component.spec.ts new file mode 100644 index 0000000..5c6bc63 --- /dev/null +++ b/website/src/app/features/platform-view/platform-view.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PlatformViewComponent } from './platform-view.component'; + +describe('PlatformViewComponent', () => { + let component: PlatformViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PlatformViewComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PlatformViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component) + .toBeTruthy(); + }); +}); diff --git a/website/src/app/features/platform-view/platform-view.component.ts b/website/src/app/features/platform-view/platform-view.component.ts new file mode 100644 index 0000000..6067208 --- /dev/null +++ b/website/src/app/features/platform-view/platform-view.component.ts @@ -0,0 +1,93 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, Subscription, forkJoin, map } from 'rxjs'; + +import { PlatformType } from 'app/core'; +import { CardComponent } from 'app/shared/card'; +import { DefinitionCard } from 'app/shared/definition-card/definition-card'; +import { DefinitionCardComponent } from 'app/shared/definition-card/definition-card.component'; +import { LogoCircleComponent } from 'app/shared/logo-circle/logo-circle.component'; +import { PlatformData, PlatformService } from 'app/shared/platform-logo'; +import { TemplateService } from 'app/shared/template'; + +interface PlatformVM { + logo: string | null; + title: string; +} + +@Component({ + selector: 'mst-platform-view', + imports: [CommonModule, DefinitionCardComponent, CardComponent, LogoCircleComponent], + templateUrl: './platform-view.component.html', + styleUrl: './platform-view.component.scss', + standalone: true +}) +export class PlatformViewComponent implements OnInit, OnDestroy { + public platform$!: Observable; + + public templates$!: Observable; + + private paramSubscription!: Subscription; + + private platformData$!: Observable; + + constructor( + private router: Router, + private route: ActivatedRoute, + private templateService: TemplateService, + private platformLogoService: PlatformService + ) { } + + public ngOnInit(): void { + this.subscribeToRouteParams(); + } + + public ngOnDestroy(): void { + this.paramSubscription.unsubscribe(); + } + + private subscribeToRouteParams(): void { + this.paramSubscription = this.route.paramMap.subscribe(params => { + const type = params.get('type'); + + if (type) { + const templateObs$ = this.templateService.filterTemplatesByPlatformType(type as PlatformType); + this.platformData$ = this.platformLogoService.getAllPlatformData(); + this.platform$ = this.platformData$.pipe( + map(platformData => ({ + logo: platformData[type]?.logo ?? null, + title: platformData[type]?.name ?? '' + }), + + ) + ); + this.templates$ = this.getTemplatesWithLogos(templateObs$, type); + + } else { + this.router.navigate(['/all']); + } + }); + } + + private getTemplatesWithLogos(templateObs$: Observable, type: string): Observable { + return forkJoin({ + templates: templateObs$, + platforms: this.platformData$ + }) + .pipe( + map(({ templates, platforms }) => + templates.map(item => ({ + cardLogo: item.logo, + title: item.name, + description: item.description, + routePath: `/platforms/${type}/definitions/${item.id}`, + supportedPlatforms: item.supportedPlatforms.map(platform => ({ + platformType: platform, + imageUrl: platforms[item.platformType].logo ?? null + })) + })) + ) + ); + } +} diff --git a/website/src/app/features/template-details/import-dialog/import-dialog.component.html b/website/src/app/features/template-details/import-dialog/import-dialog.component.html index 7929fd4..b434062 100644 --- a/website/src/app/features/template-details/import-dialog/import-dialog.component.html +++ b/website/src/app/features/template-details/import-dialog/import-dialog.component.html @@ -50,7 +50,7 @@
to continue using this building block template

Check it out and how it can help your platform team

Discover meshStack - meshStack Logo + meshStack Logo diff --git a/website/src/app/features/template-details/import-dialog/import-dialog.component.scss b/website/src/app/features/template-details/import-dialog/import-dialog.component.scss index 393cf64..90730cc 100644 --- a/website/src/app/features/template-details/import-dialog/import-dialog.component.scss +++ b/website/src/app/features/template-details/import-dialog/import-dialog.component.scss @@ -2,8 +2,3 @@ width: 5rem; object-fit: contain; } - -.button-image { - width: 2rem; - object-fit: contain; -} diff --git a/website/src/app/features/template-details/template-details.component.html b/website/src/app/features/template-details/template-details.component.html index 630d113..26d324b 100644 --- a/website/src/app/features/template-details/template-details.component.html +++ b/website/src/app/features/template-details/template-details.component.html @@ -1,12 +1,9 @@
- + + +
diff --git a/website/src/app/features/template-details/template-details.component.scss b/website/src/app/features/template-details/template-details.component.scss index e9c6da1..db93c37 100644 --- a/website/src/app/features/template-details/template-details.component.scss +++ b/website/src/app/features/template-details/template-details.component.scss @@ -1,22 +1,3 @@ -.breadcrumb-item a { - color: gray; - text-decoration: none; - - &:hover { - text-decoration: underline; - } -} - -.breadcrumb-item.active { - color: black; - font-weight: bold; -} - -.breadcrumb-item + .breadcrumb-item::before { - content: ' > '; - padding: 0 5rem; -} - .logo { width: 5rem; object-fit: contain; @@ -26,10 +7,3 @@ white-space: pre-wrap; line-height: 2rem; } - -.breadcrumb-item + .breadcrumb-item::before { - content: '\f054'; - font-family: 'Font Awesome 6 Free'; - font-weight: 900; - color: gray; -} diff --git a/website/src/app/features/template-details/template-details.component.ts b/website/src/app/features/template-details/template-details.component.ts index e04f504..c113ba2 100644 --- a/website/src/app/features/template-details/template-details.component.ts +++ b/website/src/app/features/template-details/template-details.component.ts @@ -2,9 +2,12 @@ import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Observable, Subscription, map } from 'rxjs'; +import { Observable, Subscription, map, of, switchMap } from 'rxjs'; import { PlatformType } from 'app/core'; +import { BreadcrumbItem } from 'app/shared/breadcrumb/breadcrumb'; +import { BreadcrumbComponent } from 'app/shared/breadcrumb/breadcrumb.component'; +import { PlatformService } from 'app/shared/platform-logo'; import { TemplateService } from 'app/shared/template'; import { ImportDialogComponent } from './import-dialog/import-dialog.component'; @@ -20,15 +23,16 @@ interface TemplateDetailsVm { @Component({ selector: 'mst-template-details', - imports: [CommonModule], + imports: [CommonModule, BreadcrumbComponent], templateUrl: './template-details.component.html', styleUrl: './template-details.component.scss', standalone: true - }) export class TemplateDetailsComponent implements OnInit, OnDestroy { public template$!: Observable; + public breadcrumbs$!: Observable; + public copyLabel = 'Copy'; private routeSubscription!: Subscription; @@ -36,10 +40,97 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { constructor( private route: ActivatedRoute, private templateService: TemplateService, + private platformService: PlatformService, private modalService: NgbModal ) { } public ngOnInit(): void { + this.initializeBreadcrumbs(); + this.initializeTemplate(); + } + + public ngOnDestroy(): void { + this.routeSubscription.unsubscribe(); + } + + public copyToClipboard(value: string): void { + navigator.clipboard.writeText(value) + .then(() => this.updateCopyLabel()); + } + + public open(template: TemplateDetailsVm): void { + const modulePath = this.extractModulePath(template.source); + + if (!modulePath) { + // eslint-disable-next-line no-console + console.error('Module path not found in source URL'); + + return; + } + + const component = this.modalService.open(ImportDialogComponent, { size: 'lg', centered: true }).componentInstance; + component.name = template.name; + component.modulePath = modulePath; + } + + private extractModulePath(source: string): string { + const regex = /modules\/[^/]+\/[^/]+/; + const match = source.match(regex); + + return match ? match[0] : ''; + } + + private initializeBreadcrumbs(): void { + this.breadcrumbs$ = this.route.paramMap.pipe( + switchMap(params => { + const id = params.get('id'); + const type = params.get('type'); + + if (!id) { + throw new Error('Template ID is required'); + } + + return this.getPlatformData(type) + .pipe( + switchMap((platformName) => + this.templateService.getTemplateById(id) + .pipe( + map(template => this.buildBreadcrumbs(template.name, platformName, type)) + ) + ) + ); + }) + ); + } + + private getPlatformData(type: string | null): Observable { + if (!type) { + return of(null); + } + + return this.platformService.getPlatformData(type) + .pipe( + map(x => x.name) + ); + } + + private buildBreadcrumbs( + templateName: string, + platformName: string | null, + type: string | null, + ): BreadcrumbItem[] { + const breadcrumbs: BreadcrumbItem[] = [{ label: 'Overview', routePath: '/' }]; + + if (platformName) { + breadcrumbs.push({ label: platformName, routePath: `/platforms/${type}` }); + } + + breadcrumbs.push({ label: templateName, routePath: '' }); + + return breadcrumbs; + } + + private initializeTemplate(): void { this.routeSubscription = this.route.paramMap.subscribe(params => { const id = params.get('id'); @@ -49,7 +140,7 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { this.template$ = this.templateService.getTemplateById(id) .pipe( - map((template) => ({ + map(template => ({ ...template, imageUrl: template.logo, source: template.githubUrls.https, @@ -59,37 +150,11 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { }); } - public ngOnDestroy(): void { - this.routeSubscription.unsubscribe(); - } - - public copyToClipboard(value: string) { - navigator.clipboard.writeText(value) - .then(() => { - this.copyLabel = 'Copied'; - setTimeout(() => { - this.copyLabel = 'Copy'; - }, 1000); - }) - .catch(e => - /* eslint-disable-next-line */ - console.log(e) - ); - } - - public open(template: TemplateDetailsVm) { - const regex = /modules\/[^/]+\/[^/]+/; - const match = template.source.match(regex); - const modulePath = match ? match[0] : ''; - - if (!modulePath) { - /* eslint-disable-next-line */ - console.error('Module path not found in source URL'); - } else { - const component = this.modalService.open(ImportDialogComponent, { size: 'lg', centered: true }).componentInstance; - component.name = template.name; - component.modulePath = modulePath; - } + private updateCopyLabel(): void { + this.copyLabel = 'Copied'; + setTimeout(() => { + this.copyLabel = 'Copy'; + }, 1000); } } diff --git a/website/src/app/features/template-gallery/template-gallery.component.html b/website/src/app/features/template-gallery/template-gallery.component.html index 300d142..de71d99 100644 --- a/website/src/app/features/template-gallery/template-gallery.component.html +++ b/website/src/app/features/template-gallery/template-gallery.component.html @@ -1,37 +1,23 @@ -
-
-
-
- - -
-
-
-
+
+ -
-
+
+

All building block definitions

+ These are pre-configured Terraform modules for automating common cloud tasks across AWS, Azure, GCP, and custom + cloud platforms, enabling rapid and consistent infrastructure provisioning and management +
+ -
-

{{ templates.length }} templates found

+

{{ templates.length }} building block definitions found

- +
-

No templates found

+

No building block definitions found

diff --git a/website/src/app/features/template-gallery/template-gallery.component.ts b/website/src/app/features/template-gallery/template-gallery.component.ts index 38567e2..59f26d4 100644 --- a/website/src/app/features/template-gallery/template-gallery.component.ts +++ b/website/src/app/features/template-gallery/template-gallery.component.ts @@ -1,94 +1,81 @@ import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, Subscription, forkJoin, map, tap } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, Subscription, forkJoin, map } from 'rxjs'; -import { PlatformType } from 'app/core'; -import { CardComponent } from 'app/shared/card'; -import { Card } from 'app/shared/card/card'; +import { DefinitionCard } from 'app/shared/definition-card/definition-card'; +import { DefinitionCardComponent } from 'app/shared/definition-card/definition-card.component'; import { NavigationComponent } from 'app/shared/navigation'; -import { PlatformLogoData, PlatformLogoService } from 'app/shared/platform-logo'; +import { PlatformData, PlatformService } from 'app/shared/platform-logo'; import { TemplateService } from 'app/shared/template'; @Component({ selector: 'mst-template-gallery', - imports: [CommonModule, ReactiveFormsModule, CardComponent, NavigationComponent], + imports: [CommonModule, DefinitionCardComponent, NavigationComponent], templateUrl: './template-gallery.component.html', styleUrl: './template-gallery.component.scss', standalone: true }) export class TemplateGalleryComponent implements OnInit, OnDestroy { - public templates$!: Observable; - - public searchForm!: FormGroup; + public templates$!: Observable; public isSearch = false; - private paramSubscription!: Subscription; + private searchSubscription!: Subscription; - private logos$!: Observable; + private platformData$!: Observable; constructor( - private router: Router, private route: ActivatedRoute, - private fb: FormBuilder, private templateService: TemplateService, - private platformLogoService: PlatformLogoService + private platformLogoService: PlatformService ) {} public ngOnInit(): void { - this.initializeSearchForm(); - this.subscribeToRouteParams(); + this.platformData$ = this.platformLogoService.getAllPlatformData(); + this.subscribeToSearchTerm(); } public ngOnDestroy(): void { - this.paramSubscription.unsubscribe(); + this.searchSubscription?.unsubscribe(); } - public onSearch(): void { - const searchTerm = this.searchForm.value.searchTerm; - this.templates$ = this.getTemplatesWithLogos(this.templateService.search(searchTerm)); - this.isSearch = !!searchTerm; - this.router.navigate(['/all']); - } + private subscribeToSearchTerm(): void { + this.searchSubscription = this.route.queryParams.subscribe(params => { + const searchTerm = params['searchTerm']; + const templateObs$ = searchTerm + ? this.handleSearch(searchTerm) + : this.templateService.filterTemplatesByPlatformType('all'); - private initializeSearchForm(): void { - this.searchForm = this.fb.group({ - searchTerm: [''] + this.templates$ = this.getTemplatesWithLogos(templateObs$); }); } - private subscribeToRouteParams(): void { - this.paramSubscription = this.route.paramMap.subscribe(params => { - const type = params.get('type') ?? 'all'; - const templateObs$ = this.templateService.filterTemplatesByPlatformType(type as PlatformType); + private handleSearch(searchTerm: string): Observable { + this.isSearch = true; - this.logos$ = this.platformLogoService.getLogoUrls(); - this.templates$ = this.getTemplatesWithLogos(templateObs$); - }); + return this.templateService.search(searchTerm); } - private getTemplatesWithLogos(templateObs$: Observable): Observable { - return forkJoin({ - templates: templateObs$, - logos: this.logos$ - }) + private getTemplatesWithLogos(templateObs$: Observable): Observable { + return forkJoin({ templates: templateObs$, platforms: this.platformData$ }) .pipe( - tap(() => (this.isSearch = false)), - map(({ templates, logos }) => - templates.map(item => ({ - cardLogo: item.logo, - title: item.name, - description: item.description, - detailsRoute: `/template/${item.id}`, - supportedPlatforms: item.supportedPlatforms.map(platform => ({ - platformType: platform, - imageUrl: logos[item.platformType] ?? null - })) - })) + map(({ templates, platforms }) => + templates.map(template => this.mapToDefinitionCard(template, platforms)) ) ); } + private mapToDefinitionCard(template: any, platformData: PlatformData): DefinitionCard { + return { + cardLogo: template.logo, + title: template.name, + description: template.description, + routePath: `/definitions/${template.id}`, + supportedPlatforms: template.supportedPlatforms.map(platform => ({ + platformType: platform, + imageUrl: platformData[platform].logo ?? null + })) + }; + } } diff --git a/website/src/app/shared/breadcrumb/breadcrumb.component.html b/website/src/app/shared/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000..2c973c9 --- /dev/null +++ b/website/src/app/shared/breadcrumb/breadcrumb.component.html @@ -0,0 +1,15 @@ + diff --git a/website/src/app/shared/breadcrumb/breadcrumb.component.scss b/website/src/app/shared/breadcrumb/breadcrumb.component.scss new file mode 100644 index 0000000..5433863 --- /dev/null +++ b/website/src/app/shared/breadcrumb/breadcrumb.component.scss @@ -0,0 +1,25 @@ +.breadcrumb-item a { + color: gray; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +.breadcrumb-item.active { + color: black; + font-weight: bold; +} + +.breadcrumb-item + .breadcrumb-item::before { + content: ' > '; + padding: 0 5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + content: '\f054'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; + color: gray; +} diff --git a/website/src/app/shared/breadcrumb/breadcrumb.component.ts b/website/src/app/shared/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..de1774c --- /dev/null +++ b/website/src/app/shared/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { BreadcrumbItem } from './breadcrumb'; + +@Component({ + selector: 'mst-breadcrumb', + imports: [CommonModule, RouterModule], + templateUrl: './breadcrumb.component.html', + styleUrl: './breadcrumb.component.scss', + standalone: true +}) +export class BreadcrumbComponent { + @Input() + public breadcrumbs: BreadcrumbItem[] = []; +} diff --git a/website/src/app/shared/breadcrumb/breadcrumb.ts b/website/src/app/shared/breadcrumb/breadcrumb.ts new file mode 100644 index 0000000..be0bccc --- /dev/null +++ b/website/src/app/shared/breadcrumb/breadcrumb.ts @@ -0,0 +1,4 @@ +export interface BreadcrumbItem { + label: string; + routePath?: string; // optional, in case some breadcrumbs are not links +} \ No newline at end of file diff --git a/website/src/app/shared/card/card.component.html b/website/src/app/shared/card/card.component.html index 1a0e554..ed5a964 100644 --- a/website/src/app/shared/card/card.component.html +++ b/website/src/app/shared/card/card.component.html @@ -1,35 +1,13 @@
- Building Block - -
-
-
- - - Unknown Logo - -
-
-
-
-
{{ card.title }}
-

{{ card.description }}

-
-
-
- - + +
diff --git a/website/src/app/shared/card/card.component.scss b/website/src/app/shared/card/card.component.scss index a02d731..6bcec06 100644 --- a/website/src/app/shared/card/card.component.scss +++ b/website/src/app/shared/card/card.component.scss @@ -4,50 +4,15 @@ height: 100%; transition: transform 0.3s; cursor: pointer; + border-radius: 0.5rem; // Added to make the card border rounder box-shadow: 0 0.5rem 1rem rgba(black, 0.15); - &:hover { + &.clickable:hover { transform: scale(1.05); } - &:focus { + &.clickable:focus { outline: none; box-shadow: 0 0 1rem rgba(0, 120, 255, 0.8); } } - -.card-body { - flex-grow: 1; -} - -.card-label { - position: absolute; - top: 0px; - left: 0px; - background-color: gray; - color: white; - padding: 0.25rem 0.5rem; - font-size: 1rem; - border-radius: var(--bs-card-border-radius) 0 var(--bs-card-border-radius) 0; -} - -.circle { - width: 5rem; - height: 5rem; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - - img { - width: 75%; - height: 75%; - object-fit: contain; - } -} - -.footer-icon { - width: 1rem; - height: 1rem; - object-fit: contain; -} diff --git a/website/src/app/shared/card/card.component.ts b/website/src/app/shared/card/card.component.ts index 457da2c..b92c775 100644 --- a/website/src/app/shared/card/card.component.ts +++ b/website/src/app/shared/card/card.component.ts @@ -1,8 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { Router, RouterModule } from '@angular/router'; - -import { Card } from './card'; +import ColorThief from 'colorthief'; @Component({ selector: 'mst-card', @@ -13,12 +12,48 @@ import { Card } from './card'; }) export class CardComponent { @Input() - public card!: Card; + public label = ''; - constructor(private router: Router) {} + @Input() + public routePath = ''; - public goToDetails() { - this.router.navigate([this.card.detailsRoute]); + @Input() + public set borderColorSourceImage(value: string | null) { + this._borderColorSourceImage = value; + this.updateBorderColor(); } -} + public get borderColorSourceImage(): string | null { + return this._borderColorSourceImage; + } + + public borderColor = ''; + + private _borderColorSourceImage: string| null = ''; + + constructor(private router: Router) { } + + public navigateToRoutePath(): void { + if (this.routePath) { + this.router.navigate([this.routePath]); + } + } + + private updateBorderColor(): void { + if (!this.borderColorSourceImage) { + this.borderColor = ''; + + return; + } + + if (typeof window !== 'undefined') { + const img = new Image(); + img.src = this.borderColorSourceImage; + img.onload = () => { + const colorThief = new ColorThief(); + const dominantColor = colorThief.getColor(img); + this.borderColor = `rgb(${dominantColor.join(',')})`; + }; + } + } +} diff --git a/website/src/app/shared/definition-card/definition-card.component.html b/website/src/app/shared/definition-card/definition-card.component.html new file mode 100644 index 0000000..ec1effd --- /dev/null +++ b/website/src/app/shared/definition-card/definition-card.component.html @@ -0,0 +1,20 @@ + +
+
+ +
+ {{ card.title }} +
+
+
+

{{ card.description }}

+
+
+ +
diff --git a/website/src/app/shared/definition-card/definition-card.component.scss b/website/src/app/shared/definition-card/definition-card.component.scss new file mode 100644 index 0000000..3bf26e8 --- /dev/null +++ b/website/src/app/shared/definition-card/definition-card.component.scss @@ -0,0 +1,5 @@ +.footer-icon { + width: 1rem; + height: 1rem; + object-fit: contain; +} diff --git a/website/src/app/shared/definition-card/definition-card.component.spec.ts b/website/src/app/shared/definition-card/definition-card.component.spec.ts new file mode 100644 index 0000000..2df4d4e --- /dev/null +++ b/website/src/app/shared/definition-card/definition-card.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DefinitionCardComponent } from './definition-card.component'; + +describe('DefinitionCardComponent', () => { + let component: DefinitionCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DefinitionCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DefinitionCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component) + .toBeTruthy(); + }); +}); diff --git a/website/src/app/shared/definition-card/definition-card.component.ts b/website/src/app/shared/definition-card/definition-card.component.ts new file mode 100644 index 0000000..703c7b3 --- /dev/null +++ b/website/src/app/shared/definition-card/definition-card.component.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +import { CardComponent } from '../card'; +import { DefinitionCard } from './definition-card'; +import { LogoCircleComponent } from '../logo-circle/logo-circle.component'; + +@Component({ + selector: 'mst-definition-card', + imports: [CommonModule, CardComponent, LogoCircleComponent], + templateUrl: './definition-card.component.html', + styleUrl: './definition-card.component.scss', + standalone: true +}) +export class DefinitionCardComponent { + @Input() + public card!: DefinitionCard; + +} diff --git a/website/src/app/shared/card/card.ts b/website/src/app/shared/definition-card/definition-card.ts similarity index 65% rename from website/src/app/shared/card/card.ts rename to website/src/app/shared/definition-card/definition-card.ts index 9f86dea..e2bfce3 100644 --- a/website/src/app/shared/card/card.ts +++ b/website/src/app/shared/definition-card/definition-card.ts @@ -1,9 +1,9 @@ import { PlatformType } from 'app/core'; -export interface Card { +export interface DefinitionCard { cardLogo: string | null; title: string; - description: string; - detailsRoute: string; + description: string | null; + routePath: string; supportedPlatforms: { platformType: PlatformType; imageUrl: string }[]; -} +} \ No newline at end of file diff --git a/website/src/app/shared/header/header.component.html b/website/src/app/shared/header/header.component.html index a292964..6ef52c3 100644 --- a/website/src/app/shared/header/header.component.html +++ b/website/src/app/shared/header/header.component.html @@ -1,28 +1,44 @@ -
-
- - - -

Welcome to meshStack Hub!

-
- -
-
-

- Find ready-to-use Terraform modules to integrate with meshStack. Automate cloud governance or roll out commonly - used cloud resources. Built for scalability and security, these modules help you build your platform faster! - Explore the collection below and get started! -

+
+
+
+ + + +
+

Welcome to meshStack Hub!

+ + + Discover meshStack + meshStack Logo + +
+
+
- - Eggcelent! 🥚
Now copy & paste me!
- You've cracked the clue wide open LOL - And this my friend, is an example of *what not to do* when hiding content. - #Accessibility Talk - You've discovered what was meant to hide, - a div transparent just like we tried. - Now for your next, look up hiiiigh - where muscles pull and ceiling tiles lie. -
+
+ + Eggcelent! 🥚 +
+ Now copy & paste me! +
+ You've cracked the clue wide open LOL And this my friend, is an example of *what not to do* when hiding content. + #Accessibility Talk You've discovered what was meant to hide, a div transparent just like we tried. Now for your + next, look up hiiiigh where muscles pull and ceiling tiles lie. +
+
+ + Discover meshStack + meshStack Logo +
-
\ No newline at end of file +
diff --git a/website/src/app/shared/header/header.component.scss b/website/src/app/shared/header/header.component.scss index 94e22d2..c022a4f 100644 --- a/website/src/app/shared/header/header.component.scss +++ b/website/src/app/shared/header/header.component.scss @@ -4,16 +4,14 @@ background-color: $mesh-blue; .logo { - height: 10rem; + height: 5rem; object-fit: contain; position: relative; - left: -2rem; /* Moves the logo 2rem to the left so its edge aligns with the edge of the header */ } .title { color: $mesh-black; font-weight: bold; align-self: center; - margin-top: 2.5rem; } } diff --git a/website/src/app/shared/header/header.component.ts b/website/src/app/shared/header/header.component.ts index 40fe5e8..fa835ee 100644 --- a/website/src/app/shared/header/header.component.ts +++ b/website/src/app/shared/header/header.component.ts @@ -1,13 +1,14 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; +import { SearchBarComponent } from '../search-bar/search-bar.component'; + @Component({ selector: 'mst-header', - imports: [CommonModule], + imports: [CommonModule, SearchBarComponent], templateUrl: './header.component.html', styleUrl: './header.component.scss', standalone: true }) export class HeaderComponent { - } diff --git a/website/src/app/shared/logo-circle/logo-circle.component.html b/website/src/app/shared/logo-circle/logo-circle.component.html new file mode 100644 index 0000000..0f42509 --- /dev/null +++ b/website/src/app/shared/logo-circle/logo-circle.component.html @@ -0,0 +1,6 @@ +
+ + + Unknown Logo + +
diff --git a/website/src/app/shared/logo-circle/logo-circle.component.scss b/website/src/app/shared/logo-circle/logo-circle.component.scss new file mode 100644 index 0000000..ad95136 --- /dev/null +++ b/website/src/app/shared/logo-circle/logo-circle.component.scss @@ -0,0 +1,19 @@ +.circle { + width: 5.5rem; + height: 5.5rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + + &.circle-sm { + width: 3rem; + height: 3rem; + } + + img { + width: 75%; + height: 75%; + object-fit: contain; + } +} diff --git a/website/src/app/shared/logo-circle/logo-circle.component.spec.ts b/website/src/app/shared/logo-circle/logo-circle.component.spec.ts new file mode 100644 index 0000000..ae2e9db --- /dev/null +++ b/website/src/app/shared/logo-circle/logo-circle.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogoCircleComponent } from './logo-circle.component'; + +describe('LogoCircleComponent', () => { + let component: LogoCircleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LogoCircleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LogoCircleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component) + .toBeTruthy(); + }); +}); diff --git a/website/src/app/shared/logo-circle/logo-circle.component.ts b/website/src/app/shared/logo-circle/logo-circle.component.ts new file mode 100644 index 0000000..4109217 --- /dev/null +++ b/website/src/app/shared/logo-circle/logo-circle.component.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'mst-logo-circle', + imports: [CommonModule], + templateUrl: './logo-circle.component.html', + styleUrl: './logo-circle.component.scss' +}) +export class LogoCircleComponent { + @Input() + public size: 'sm' | 'md' = 'md'; + + @Input() + public sourceImage: string | null = null; + + @Input() + public title = ''; +} diff --git a/website/src/app/shared/navigation/navigation.component.html b/website/src/app/shared/navigation/navigation.component.html index 02d2912..3361a73 100644 --- a/website/src/app/shared/navigation/navigation.component.html +++ b/website/src/app/shared/navigation/navigation.component.html @@ -1,17 +1,16 @@ - +
+
+ +
+ +
+ {{ card.title }} +
+
+
+
+
diff --git a/website/src/app/shared/navigation/navigation.component.ts b/website/src/app/shared/navigation/navigation.component.ts index 1ea84f9..7108499 100644 --- a/website/src/app/shared/navigation/navigation.component.ts +++ b/website/src/app/shared/navigation/navigation.component.ts @@ -1,14 +1,44 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { Observable, map } from 'rxjs'; + +import { CardComponent } from '../card'; +import { LogoCircleComponent } from '../logo-circle/logo-circle.component'; +import { PlatformService } from '../platform-logo'; + +interface PlatformCard { + cardLogo: string | null; + title: string; + routePath: string; +} @Component({ selector: 'mst-navigation', - imports: [CommonModule, RouterModule], + imports: [CommonModule, CardComponent, LogoCircleComponent], templateUrl: './navigation.component.html', styleUrl: './navigation.component.scss', standalone: true }) -export class NavigationComponent { - public activeTab = 'all'; +export class NavigationComponent implements OnInit { + public cards$!: Observable; + + constructor(private readonly platformService: PlatformService) {} + + public ngOnInit(): void { + this.cards$ = this.platformService.getAllPlatformData() + .pipe( + map((logos) => this.mapLogosToPlatformCards(logos)) + ); + } + + private mapLogosToPlatformCards(logos: Record): PlatformCard[] { + return Object.entries(logos) + .map(([key, platform]) => + this.createPlatformCard(platform.name, platform.logo, `/platforms/${key}`) + ); + } + + private createPlatformCard(title: string, logoUrl: string, routePath: string): PlatformCard { + return { cardLogo: logoUrl, title, routePath }; + } } diff --git a/website/src/app/shared/platform-logo/platform-logo-data.ts b/website/src/app/shared/platform-logo/platform-logo-data.ts index e6ea463..85bdb3e 100644 --- a/website/src/app/shared/platform-logo/platform-logo-data.ts +++ b/website/src/app/shared/platform-logo/platform-logo-data.ts @@ -1,3 +1,12 @@ export interface PlatformLogoData { - [key: string]: string; + [key: string]: string; +} + +export interface PlatformData { + [key: string]: Platform; +} + +export interface Platform { + name: string; + logo: string; } diff --git a/website/src/app/shared/platform-logo/platform-logo.service.spec.ts b/website/src/app/shared/platform-logo/platform-logo.service.spec.ts index b7cc478..d4d64b7 100644 --- a/website/src/app/shared/platform-logo/platform-logo.service.spec.ts +++ b/website/src/app/shared/platform-logo/platform-logo.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { PlatformLogoService } from './platform-logo.service'; +import { PlatformService } from './platform-logo.service'; describe('PlatformLogoService', () => { - let service: PlatformLogoService; + let service: PlatformService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(PlatformLogoService); + service = TestBed.inject(PlatformService); }); it('should be created', () => { diff --git a/website/src/app/shared/platform-logo/platform-logo.service.ts b/website/src/app/shared/platform-logo/platform-logo.service.ts index 1f8e139..83ea2f3 100644 --- a/website/src/app/shared/platform-logo/platform-logo.service.ts +++ b/website/src/app/shared/platform-logo/platform-logo.service.ts @@ -1,27 +1,68 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, shareReplay, take } from 'rxjs'; +import { Observable, map, shareReplay, take } from 'rxjs'; -import { PlatformLogoData } from './platform-logo-data'; +import { Platform, PlatformData, PlatformLogoData } from './platform-logo-data'; @Injectable({ providedIn: 'root' }) -export class PlatformLogoService { - private logoDataCache$: Observable | null = null; +export class PlatformService { + private logoDataCache$: Observable | null = null; constructor(private http: HttpClient) { } - - public getLogoUrls(): Observable { + public getAllPlatformData(): Observable { if (!this.logoDataCache$) { this.logoDataCache$ = this.http.get('/assets/platform-logos.json') .pipe( take(1), - shareReplay(1) + shareReplay(1), + map((data: PlatformLogoData) => + Object.entries(data) + .reduce((acc, [key, logoUrl]) => { + acc[key] = this.getPlatform(key, logoUrl); + + return acc; + }, {} as PlatformData) + ) ); } return this.logoDataCache$; } + + public getPlatformData(platform: string): Observable { + return this.getAllPlatformData() + .pipe( + map((data: PlatformData) => { + const platformData = data[platform]; + + if (!platformData) { + throw new Error(`Platform ${platform} not found`); + } + + return platformData; + } + ) + ); + } + + private getPlatform(key: string, logoUrl: string): Platform { + switch (key) { + case 'azure': + return { name: 'Azure', logo: logoUrl }; + case 'aws': + return { name: 'AWS', logo: logoUrl }; + case 'gcp': + return { name: 'GCP', logo: logoUrl }; + case 'github': + return { name: 'GitHub', logo: logoUrl }; + case 'aks': + return { name: 'Azure Kubernetes Service', logo: logoUrl }; + default: + return { name: key, logo: logoUrl }; + } + } + } diff --git a/website/src/app/shared/search-bar/search-bar.component.html b/website/src/app/shared/search-bar/search-bar.component.html new file mode 100644 index 0000000..08cdaf2 --- /dev/null +++ b/website/src/app/shared/search-bar/search-bar.component.html @@ -0,0 +1,15 @@ +
+
+ + +
+
diff --git a/website/src/app/shared/search-bar/search-bar.component.ts b/website/src/app/shared/search-bar/search-bar.component.ts new file mode 100644 index 0000000..46888f2 --- /dev/null +++ b/website/src/app/shared/search-bar/search-bar.component.ts @@ -0,0 +1,36 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'mst-search-bar', + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './search-bar.component.html', + standalone: true +}) +export class SearchBarComponent implements OnInit { + public searchForm!: FormGroup; + + constructor( + private router: Router, + private fb: FormBuilder + ) { } + + public ngOnInit(): void { + this.initializeSearchForm(); + } + + public onSearch(): void { + const searchTerm = this.searchForm.value.searchTerm; + this.router.navigate(['/all'], { + queryParams: { searchTerm } + }); + } + + private initializeSearchForm(): void { + this.searchForm = this.fb.group({ + searchTerm: [''] + }); + } +} diff --git a/website/src/styles.scss b/website/src/styles.scss index 5e78b1b..57b50fd 100644 --- a/website/src/styles.scss +++ b/website/src/styles.scss @@ -21,3 +21,8 @@ h6 { body { font-family: 'Roboto', sans-serif; } + +.button-image { + width: 2rem; + object-fit: contain; +} diff --git a/website/src/test.ts b/website/src/test.ts new file mode 100644 index 0000000..78aa623 --- /dev/null +++ b/website/src/test.ts @@ -0,0 +1,12 @@ +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; +import 'zone.js/testing'; + +getTestBed() + .initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() + ); \ No newline at end of file