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

@@ -28,6 +28,11 @@
{
"glob": "**/*",
"input": "public"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
}
],
"styles": [
@@ -104,6 +109,7 @@
"buildTarget": "Dexarmarket:build:production"
},
"development": {
"proxyConfig": "proxy.conf.json",
"buildTarget": "Dexarmarket:build:development"
}
},

11
proxy.conf.json Normal file
View File

@@ -0,0 +1,11 @@
{
"/api": {
"target": "https://api.dexarmarket.ru:445",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": ""
}
}
}

View File

@@ -0,0 +1,325 @@
{
"schemaVersion": "1.0.0",
"generatedAt": "2026-07-03T00:00:00Z",
"tenant": {
"id": "tenant-default-001",
"slug": "default",
"code": "DEFAULT",
"host": "default.local",
"name": "Marketplace",
"websiteBaseUrl": "https://marketplace.local",
"builderBaseUrl": "https://builder.marketplace.local",
"backofficeBaseUrl": "https://backoffice.marketplace.local",
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB",
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
"timezone": "Europe/Moscow"
},
"branding": {
"brandName": "Marketplace",
"legalName": "Marketplace LLC",
"slogan": "Digital commerce marketplace",
"logoUrl": "/icons/icon-192x192.png",
"logoCompactUrl": "/icons/icon-192x192.png",
"faviconUrl": "/favicon.ico",
"appIconUrl": "/icons/icon-192x192.png",
"supportEmail": "support@marketplace.local",
"supportPhone": "+7-900-000-00-00"
},
"theme": {
"themeId": "default-light",
"mode": "light",
"palette": {
"primary": "#497671",
"secondary": "#a1b4b5",
"accent": "#a7ceca",
"success": "#10b981",
"warning": "#f59e0b",
"danger": "#ef4444",
"info": "#3b82f6",
"textPrimary": "#1e3c38",
"textSecondary": "#667a77",
"backgroundPrimary": "#ffffff",
"backgroundSecondary": "#f5f5f5",
"border": "#d3dad9"
},
"typography": {
"primaryFontFamily": "DM Sans, sans-serif",
"headingFontFamily": "DM Sans, sans-serif",
"baseFontSize": 16
},
"spacing": {
"unit": 4,
"scale": [0, 4, 8, 12, 16, 24, 32, 48]
},
"borderRadiusScale": {
"sm": "8px",
"md": "12px",
"lg": "16px",
"xl": "22px"
},
"shadows": {
"sm": "0 2px 8px rgba(0,0,0,0.1)",
"md": "0 4px 12px rgba(0,0,0,0.15)",
"lg": "0 12px 32px rgba(73,118,113,0.2)"
},
"iconSet": "default"
},
"company": {
"companyName": "Marketplace LLC",
"registrationNumber": "1027700000000",
"taxId": "7700000000",
"address": {
"country": "Russia",
"region": "Moscow",
"city": "Moscow",
"street": "Tverskaya 1",
"postalCode": "125009"
},
"contacts": {
"email": "support@marketplace.local",
"phone": "+7-900-000-00-00",
"telegram": "@marketplace_support",
"website": "https://marketplace.local"
}
},
"featureFlags": {
"wishlist": true,
"compare": true,
"reviews": true,
"blog": false,
"chat": false,
"analytics": true,
"notifications": true,
"coupons": true,
"loyalty": false,
"giftCards": false,
"invoices": true
},
"apiEndpoints": {
"bootstrap": {
"path": "/bootstrap",
"method": "GET",
"timeoutMs": 10000
},
"website": {},
"builder": {},
"backoffice": {}
},
"localization": {
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"currencyByLocale": {
"ru": "RUB",
"en": "USD",
"hy": "AMD"
},
"dictionaries": [
{
"locale": "ru",
"dictionaryUrl": "/assets/i18n/ru.json",
"version": "1.0.0"
},
{
"locale": "en",
"dictionaryUrl": "/assets/i18n/en.json",
"version": "1.0.0"
},
{
"locale": "hy",
"dictionaryUrl": "/assets/i18n/hy.json",
"version": "1.0.0"
}
]
},
"seo": {
"default": {
"title": "Marketplace",
"description": "Digital commerce marketplace",
"robots": "index,follow"
},
"byPageKey": {
"home": {
"title": "Marketplace - Home",
"description": "Digital commerce marketplace",
"canonicalUrl": "https://marketplace.local/",
"robots": "index,follow"
}
}
},
"permissions": {
"definitions": [
{
"key": "builder.pages.edit",
"description": "Edit pages in builder"
},
{
"key": "backoffice.products.read",
"description": "Read products in backoffice"
}
],
"roles": [
{
"role": "builder_admin",
"permissions": ["builder.pages.edit"]
},
{
"role": "backoffice_manager",
"permissions": ["backoffice.products.read"]
}
]
},
"navigation": {
"header": [
{
"id": "nav-home",
"labelKey": "nav.home",
"route": "/",
"icon": "home",
"order": 1
},
{
"id": "nav-search",
"labelKey": "nav.search",
"route": "/search",
"icon": "search",
"order": 2
},
{
"id": "nav-cart",
"labelKey": "nav.cart",
"route": "/cart",
"icon": "cart",
"order": 3
}
],
"footer": [
{
"id": "footer-about",
"labelKey": "nav.about",
"route": "/about",
"order": 1
},
{
"id": "footer-contacts",
"labelKey": "nav.contacts",
"route": "/contacts",
"order": 2
},
{
"id": "footer-privacy",
"labelKey": "nav.privacy",
"route": "/privacy-policy",
"order": 3
}
]
},
"pages": [
{
"id": "page-home",
"key": "home",
"title": "Home",
"route": {
"path": "/",
"exact": true
},
"layout": "default-public",
"seoKey": "home",
"visible": true,
"sections": [
{
"id": "section-hero",
"type": "hero",
"order": 1,
"layout": {
"strategy": "hero",
"columns": 1,
"gap": "1.5rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-hero-main",
"type": "hero",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Welcome to Marketplace Platform",
"subtitle": "Configuration-driven multi-tenant commerce",
"ctaLabel": "Start Shopping"
}
}
]
},
{
"id": "section-categories",
"type": "categories",
"order": 2,
"layout": {
"strategy": "grid",
"columns": 1,
"gap": "1.5rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-categories-root",
"type": "categories",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Categories",
"source": "root",
"emptyMessage": "No categories available"
}
}
]
},
{
"id": "section-featured-products",
"type": "product-collection",
"order": 3,
"layout": {
"strategy": "carousel",
"columns": 1,
"gap": "1rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-featured-products",
"type": "product-collection",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Featured Products",
"source": "featured",
"count": 8,
"actionLabel": "Select"
}
}
]
}
]
}
]
}

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]);
}
}

View File

@@ -1,7 +1,9 @@
// Marketplace Production Configuration
export const environment = {
production: true,
useMockBootstrapOnLocal: false,
fallbackTenantKey: 'default',
allowBootstrapApiOverride: false,
localhostApiUrl: 'https://api.dexarmarket.ru:445',
tenantApiTemplate: 'https://{tenant}.api.dexarmarket.ru:445',
tenantApiBaseUrls: {

View File

@@ -2,8 +2,10 @@
export const environment = {
production: false,
useMockData: false, // Toggle to test with backOffice mock data
useMockBootstrapOnLocal: true,
fallbackTenantKey: 'default',
localhostApiUrl: 'https://api.dexarmarket.ru:445',
allowBootstrapApiOverride: false,
localhostApiUrl: '/api',
tenantApiTemplate: 'https://{tenant}.api.dexarmarket.ru:445',
tenantApiBaseUrls: {
default: 'https://api.dexarmarket.ru:445',