Sprint 8: add widget manifest and data source engine

This commit is contained in:
sdarbinyan
2026-07-05 02:08:15 +04:00
parent 91d9444875
commit c3d1153f0e
20 changed files with 867 additions and 375 deletions

View File

@@ -0,0 +1,37 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { SectionConfig } from '../../shared/models/config';
import { CatalogCategoryGridComponent } from '../../features/website/catalog/components/category-grid/category-grid.component';
import { CategoriesWidgetData } from '../contracts/widget-data.contract';
@Component({
selector: 'app-categories-widget',
standalone: true,
imports: [CommonModule, CatalogCategoryGridComponent],
template: `
<section class="categories-widget">
@if (data; as widgetData) {
@if (widgetData.title) {
<h2>{{ widgetData.title }}</h2>
}
@if (widgetData.categories.length) {
<app-catalog-category-grid [categories]="widgetData.categories" />
} @else if (widgetData.emptyMessage) {
<p class="categories-widget__empty">{{ widgetData.emptyMessage }}</p>
}
}
</section>
`,
styles: [
`
.categories-widget { padding: 1rem 0; display: grid; gap: 1rem; }
.categories-widget__empty { margin: 0; color: var(--text-secondary, #667a77); }
`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CategoriesWidgetComponent {
@Input() section: SectionConfig | null = null;
@Input() data: CategoriesWidgetData | null = null;
}

View File

@@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { SectionConfig } from '../../shared/models/config';
import { FooterWidgetData } from '../contracts/widget-data.contract';
export interface FooterNavigationLink {
id: string;
@@ -15,7 +17,7 @@ export interface FooterNavigationLink {
<footer class="footer-nav-widget">
<nav>
<ul>
@for (link of links; track link.id) {
@for (link of data?.links ?? []; track link.id) {
<li>
<button type="button" (click)="onLinkClick(link)">{{ link.label }}</button>
</li>
@@ -34,7 +36,8 @@ export interface FooterNavigationLink {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FooterNavigationWidgetComponent {
@Input() links: FooterNavigationLink[] = [];
@Input() section: SectionConfig | null = null;
@Input() data: FooterWidgetData | null = null;
@Output() linkSelected = new EventEmitter<FooterNavigationLink>();

View File

@@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { SectionConfig } from '../../shared/models/config';
import { HeroWidgetData } from '../contracts/widget-data.contract';
@Component({
selector: 'app-hero-widget',
@@ -7,12 +9,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
imports: [CommonModule],
template: `
<section class="hero-widget">
<h1 class="hero-widget__title">{{ title }}</h1>
@if (subtitle) {
<p class="hero-widget__subtitle">{{ subtitle }}</p>
}
@if (ctaLabel) {
<button type="button" class="hero-widget__cta" (click)="onCtaClick()">{{ ctaLabel }}</button>
@if (data; as widgetData) {
<h1 class="hero-widget__title">{{ widgetData.title }}</h1>
@if (widgetData.subtitle) {
<p class="hero-widget__subtitle">{{ widgetData.subtitle }}</p>
}
@if (widgetData.ctaLabel) {
<button type="button" class="hero-widget__cta" (click)="onCtaClick()">{{ widgetData.ctaLabel }}</button>
}
}
</section>
`,
@@ -27,9 +31,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeroWidgetComponent {
@Input() title: string = '';
@Input() subtitle: string = '';
@Input() ctaLabel: string = '';
@Input() section: SectionConfig | null = null;
@Input() data: HeroWidgetData | null = null;
@Output() ctaClicked = new EventEmitter<void>();

View File

@@ -1,3 +1,4 @@
export * from './categories-widget.component';
export * from './footer-navigation-widget.component';
export * from './hero-widget.component';
export * from './product-carousel-widget.component';

View File

@@ -1,53 +1,37 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ProductCardConfig } from '../../shared/models/ui';
import { SectionConfig } from '../../shared/models/config';
import { CatalogProductGridComponent } from '../../features/website/catalog/components/product-grid/product-grid.component';
import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
@Component({
selector: 'app-product-carousel-widget',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, CatalogProductGridComponent],
template: `
<section class="product-carousel-widget">
@if (title) {
<h2>{{ title }}</h2>
@if (data?.title) {
<h2>{{ data?.title }}</h2>
}
<div class="product-carousel-widget__grid">
@for (item of items; track item.id) {
<article class="product-card">
<img [src]="item.imageUrl" [alt]="item.title" loading="lazy" />
<h3>{{ item.title }}</h3>
@if (item.subtitle) {
<p>{{ item.subtitle }}</p>
}
<strong>{{ item.price.amount }} {{ item.price.currency }}</strong>
<button type="button" (click)="onProductClick(item)">{{ actionLabel }}</button>
</article>
@if (data?.products?.length) {
<app-catalog-product-grid [products]="data?.products ?? []" />
} @else if (data?.emptyMessage) {
<p class="product-carousel-widget__empty">{{ data?.emptyMessage }}</p>
}
</div>
</section>
`,
styles: [
`
.product-carousel-widget { padding: 1rem; }
.product-carousel-widget__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
.product-card { padding: 0.75rem; border: 1px solid var(--border-color, #d3dad9); border-radius: var(--radius-md, 12px); }
img { width: 100%; height: 140px; object-fit: cover; border-radius: 8px; margin-bottom: 0.5rem; }
h3 { margin: 0 0 0.25rem; font-size: 1rem; }
p { margin: 0 0 0.5rem; color: var(--text-secondary, #667a77); }
button { margin-top: 0.5rem; border: none; border-radius: 8px; padding: 0.5rem 0.75rem; cursor: pointer; background: var(--primary-color, #497671); color: #fff; }
.product-carousel-widget__empty { margin: 0; color: var(--text-secondary, #667a77); }
`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCarouselWidgetComponent {
@Input() title: string = '';
@Input() actionLabel: string = 'Select';
@Input() items: ProductCardConfig[] = [];
@Input() section: SectionConfig | null = null;
@Input() data: ProductCollectionWidgetData | null = null;
@Output() productSelected = new EventEmitter<ProductCardConfig>();
onProductClick(item: ProductCardConfig): void {
this.productSelected.emit(item);
}
@Output() productSelected = new EventEmitter<unknown>();
}