fix local bootstrap startup and catalog widget UX
This commit is contained in:
@@ -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
11
proxy.conf.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "https://api.dexarmarket.ru:445",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
325
public/assets/mock/bootstrap/bootstrap.json
Normal file
325
public/assets/mock/bootstrap/bootstrap.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.product-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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 '/';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user