From 145a13857d6fcf9ead8d57542d91022b9261008c Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sun, 5 Jul 2026 02:44:04 +0400 Subject: [PATCH] fix local bootstrap startup and catalog widget UX --- angular.json | 6 + proxy.conf.json | 11 + public/assets/mock/bootstrap/bootstrap.json | 325 ++++++++++++++++++ .../product-card/product-card.component.scss | 1 + src/app/core/config/api-config.service.ts | 18 +- .../runtime-provider-strategy.service.ts | 14 + .../product-grid/product-grid.component.scss | 8 + .../widgets/ui/categories-widget.component.ts | 17 +- src/environments/environment.production.ts | 2 + src/environments/environment.ts | 4 +- 10 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 proxy.conf.json create mode 100644 public/assets/mock/bootstrap/bootstrap.json diff --git a/angular.json b/angular.json index 97e9fdf..34faa9e 100644 --- a/angular.json +++ b/angular.json @@ -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" } }, diff --git a/proxy.conf.json b/proxy.conf.json new file mode 100644 index 0000000..bc8db23 --- /dev/null +++ b/proxy.conf.json @@ -0,0 +1,11 @@ +{ + "/api": { + "target": "https://api.dexarmarket.ru:445", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "pathRewrite": { + "^/api": "" + } + } +} \ No newline at end of file diff --git a/public/assets/mock/bootstrap/bootstrap.json b/public/assets/mock/bootstrap/bootstrap.json new file mode 100644 index 0000000..db0758d --- /dev/null +++ b/public/assets/mock/bootstrap/bootstrap.json @@ -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" + } + } + ] + } + ] + } + ] +} diff --git a/src/app/components/product-card/product-card.component.scss b/src/app/components/product-card/product-card.component.scss index 8e0c6b4..7b95542 100644 --- a/src/app/components/product-card/product-card.component.scss +++ b/src/app/components/product-card/product-card.component.scss @@ -1,5 +1,6 @@ .product-card { width: 100%; + height: 100%; min-width: 0; display: flex; flex-direction: column; diff --git a/src/app/core/config/api-config.service.ts b/src/app/core/config/api-config.service.ts index 40bdc6a..1bc3646 100644 --- a/src/app/core/config/api-config.service.ts +++ b/src/app/core/config/api-config.service.ts @@ -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 '/'; diff --git a/src/app/core/providers/runtime-provider-strategy.service.ts b/src/app/core/providers/runtime-provider-strategy.service.ts index 1e4281d..4660f5e 100644 --- a/src/app/core/providers/runtime-provider-strategy.service.ts +++ b/src/app/core/providers/runtime-provider-strategy.service.ts @@ -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'; } diff --git a/src/app/features/website/catalog/components/product-grid/product-grid.component.scss b/src/app/features/website/catalog/components/product-grid/product-grid.component.scss index 47361da..6a7e59a 100644 --- a/src/app/features/website/catalog/components/product-grid/product-grid.component.scss +++ b/src/app/features/website/catalog/components/product-grid/product-grid.component.scss @@ -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) { diff --git a/src/app/widgets/ui/categories-widget.component.ts b/src/app/widgets/ui/categories-widget.component.ts index 63c103e..ecf5bd2 100644 --- a/src/app/widgets/ui/categories-widget.component.ts +++ b/src/app/widgets/ui/categories-widget.component.ts @@ -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) { - + } @else if (widgetData.emptyMessage) {

{{ widgetData.emptyMessage }}

} @@ -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]); + } } \ No newline at end of file diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 0475399..37c62b0 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -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: { diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b1ac189..72c0611 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -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',