fix local bootstrap startup and catalog widget UX

This commit is contained in:
sdarbinyan
2026-07-05 02:44:04 +04:00
parent 487a3fb913
commit 145a13857d
10 changed files with 399 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
.product-card {
width: 100%;
height: 100%;
min-width: 0;
display: flex;
flex-direction: column;

View File

@@ -19,12 +19,13 @@ export class ApiConfigService {
if (this.tenantResolver.isLocalhost() && localhostUrl) {
url = localhostUrl;
} else if (bootstrapUrl) {
url = bootstrapUrl;
} else if (tenantMap?.[tenantKey]) {
url = tenantMap[tenantKey];
} else if (apiTemplate) {
url = apiTemplate.replace('{tenant}', tenantKey);
} else if (bootstrapUrl) {
// Bootstrap API override is opt-in and only for absolute URLs.
url = bootstrapUrl;
}
return this.normalizeBaseUrl(url);
@@ -58,24 +59,33 @@ export class ApiConfigService {
}
private resolveBootstrapApiBaseUrl(): string | null {
const allowBootstrapApiOverride = (environment as any).allowBootstrapApiOverride === true;
if (!allowBootstrapApiOverride) {
return null;
}
const bootstrap = this.configService.getBootstrapSnapshot() as any;
if (!bootstrap) {
return null;
}
const endpointBase = bootstrap?.apiEndpoints?.website?.baseUrl;
if (typeof endpointBase === 'string' && endpointBase.trim().length > 0) {
if (typeof endpointBase === 'string' && this.isAbsoluteHttpUrl(endpointBase)) {
return endpointBase;
}
const tenantBase = bootstrap?.tenant?.apiBaseUrl;
if (typeof tenantBase === 'string' && tenantBase.trim().length > 0) {
if (typeof tenantBase === 'string' && this.isAbsoluteHttpUrl(tenantBase)) {
return tenantBase;
}
return null;
}
private isAbsoluteHttpUrl(url: string): boolean {
return /^https?:\/\//i.test(url.trim());
}
private normalizeBaseUrl(url: string): string {
if (!url || url === '/') {
return '/';

View File

@@ -5,11 +5,25 @@ export type RuntimeProviderMode = 'mock' | 'api' | 'remote-config';
@Injectable({ providedIn: 'root' })
export class RuntimeProviderStrategyService {
private isLocalhost(): boolean {
if (typeof window === 'undefined') {
return false;
}
const host = window.location.hostname.toLowerCase();
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
}
getBootstrapProviderMode(): RuntimeProviderMode {
if (environment.useMockData) {
return 'mock';
}
// Local dev must use mocked bootstrap while API data can stay production-based.
if ((environment as any).useMockBootstrapOnLocal === true && this.isLocalhost()) {
return 'mock';
}
return 'api';
}

View File

@@ -7,6 +7,14 @@
.catalog-product-shell {
min-width: 0;
display: flex;
height: 100%;
}
.catalog-product-shell app-product-card {
display: block;
width: 100%;
height: 100%;
}
@media (max-width: 640px) {

View File

@@ -1,8 +1,11 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
import { Router } from '@angular/router';
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';
import { Category } from '../../core/categories/models/category-domain.model';
import { LanguageService } from '../../services/language.service';
@Component({
selector: 'app-categories-widget',
@@ -16,7 +19,10 @@ import { CategoriesWidgetData } from '../contracts/widget-data.contract';
}
@if (widgetData.categories.length) {
<app-catalog-category-grid [categories]="widgetData.categories" />
<app-catalog-category-grid
[categories]="widgetData.categories"
(categorySelected)="onCategorySelected($event)"
/>
} @else if (widgetData.emptyMessage) {
<p class="categories-widget__empty">{{ widgetData.emptyMessage }}</p>
}
@@ -32,6 +38,13 @@ import { CategoriesWidgetData } from '../contracts/widget-data.contract';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CategoriesWidgetComponent {
private readonly router = inject(Router);
private readonly languageService = inject(LanguageService);
@Input() section: SectionConfig | null = null;
@Input() data: CategoriesWidgetData | null = null;
onCategorySelected(category: Category): void {
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]);
}
}