diff --git a/package-lock.json b/package-lock.json
index 476b2b7..f02d078 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,11 +8,15 @@
"name": "dexarmarket",
"version": "0.0.0",
"dependencies": {
+ "@angular/animations": "^21.1.5",
+ "@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6",
+ "@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6",
+ "@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0",
@@ -324,6 +328,21 @@
"yarn": ">= 1.13.0"
}
},
+ "node_modules/@angular/animations": {
+ "version": "21.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.5.tgz",
+ "integrity": "sha512-gsqHX8lCYV8cgVtHs0iLwrX8SVlmcjUF44l/xCc/jBC/TeKWRl2e6Jqrn1Wcd0NDlGiNsm+mYNyqMyy5/I7kjw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "21.1.5"
+ }
+ },
"node_modules/@angular/build": {
"version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.0.tgz",
@@ -472,6 +491,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@angular/cdk": {
+ "version": "21.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.5.tgz",
+ "integrity": "sha512-AlQPgqe3LLwXCyrDwYSX3m/WKnl2ppCMW7Gb+7bJpIcpMdWYEpSOSQF318jXGYIysKg43YbdJ1tWhJWY/cbn3w==",
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^8.0.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^21.0.0 || ^22.0.0",
+ "@angular/core": "^21.0.0 || ^22.0.0",
+ "@angular/platform-browser": "^21.0.0 || ^22.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/@angular/cli": {
"version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.0.tgz",
@@ -613,6 +648,23 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
+ "node_modules/@angular/material": {
+ "version": "21.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.5.tgz",
+ "integrity": "sha512-D6JvFulPvIKhPJ52prMV7DxwYMzcUpHar11ZcMb7r9WQzUfCS3FDPXfMAce5n3h+3kFccfmmGpnyBwqTlLPSig==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/cdk": "21.1.5",
+ "@angular/common": "^21.0.0 || ^22.0.0",
+ "@angular/core": "^21.0.0 || ^22.0.0",
+ "@angular/forms": "^21.0.0 || ^22.0.0",
+ "@angular/platform-browser": "^21.0.0 || ^22.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/@angular/platform-browser": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz",
@@ -635,6 +687,24 @@
}
}
},
+ "node_modules/@angular/platform-browser-dynamic": {
+ "version": "21.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.1.5.tgz",
+ "integrity": "sha512-Pd8nPbJSIONnze1WS9wLBAtaFw4TYIH+ZGjKHS9G1E9l09tDWtHWyB7dY82Sc//Nc8iR4V7dcsbUmFjOJHThww==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "21.1.5",
+ "@angular/compiler": "21.1.5",
+ "@angular/core": "21.1.5",
+ "@angular/platform-browser": "21.1.5"
+ }
+ },
"node_modules/@angular/router": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.6.tgz",
@@ -7687,7 +7757,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@@ -7741,7 +7810,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
diff --git a/package.json b/package.json
index 13980de..fb051d4 100644
--- a/package.json
+++ b/package.json
@@ -16,11 +16,15 @@
},
"private": true,
"dependencies": {
+ "@angular/animations": "^21.1.5",
+ "@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6",
+ "@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6",
+ "@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0",
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 35bef9f..21b372d 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -3,6 +3,7 @@ import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
+import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { provideServiceWorker } from '@angular/service-worker';
@@ -15,7 +16,7 @@ export const appConfig: ApplicationConfig = {
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
),
provideHttpClient(
- withInterceptors([cacheInterceptor])
+ withInterceptors([mockDataInterceptor, cacheInterceptor])
), provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
diff --git a/src/app/components/items-carousel/items-carousel.component.html b/src/app/components/items-carousel/items-carousel.component.html
index 834cebb..003bff8 100644
--- a/src/app/components/items-carousel/items-carousel.component.html
+++ b/src/app/components/items-carousel/items-carousel.component.html
@@ -22,6 +22,13 @@
@if (product.discount > 0) {
-{{ product.discount }}%
}
+ @if (product.badges && product.badges.length > 0) {
+
+ @for (badge of product.badges; track badge) {
+ {{ badge }}
+ }
+
+ }
diff --git a/src/app/components/items-carousel/items-carousel.component.ts b/src/app/components/items-carousel/items-carousel.component.ts
index 6773297..d237bb8 100644
--- a/src/app/components/items-carousel/items-carousel.component.ts
+++ b/src/app/components/items-carousel/items-carousel.component.ts
@@ -7,7 +7,7 @@ import { TagModule } from 'primeng/tag';
import { ApiService, CartService } from '../../services';
import { Item } from '../../models';
import { environment } from '../../../environments/environment';
-import { getDiscountedPrice, getMainImage } from '../../utils/item.utils';
+import { getDiscountedPrice, getMainImage, getBadgeClass } from '../../utils/item.utils';
@Component({
selector: 'app-items-carousel',
@@ -96,6 +96,7 @@ export class ItemsCarouselComponent implements OnInit {
readonly getItemImage = getMainImage;
readonly getDiscountedPrice = getDiscountedPrice;
+ readonly getBadgeClass = getBadgeClass;
addToCart(event: Event, item: Item): void {
event.preventDefault();
diff --git a/src/app/interceptors/mock-data.interceptor.ts b/src/app/interceptors/mock-data.interceptor.ts
new file mode 100644
index 0000000..6bfd703
--- /dev/null
+++ b/src/app/interceptors/mock-data.interceptor.ts
@@ -0,0 +1,765 @@
+import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
+import { of, delay } from 'rxjs';
+import { environment } from '../../environments/environment';
+
+// ─── Mock Categories (backOffice format: string IDs, img, subcategories, visible) ───
+
+const MOCK_CATEGORIES = [
+ {
+ id: 'electronics',
+ categoryID: 1,
+ name: 'Электроника',
+ parentID: 0,
+ visible: true,
+ priority: 1,
+ img: 'https://images.unsplash.com/photo-1498049794561-7780e7231661?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1498049794561-7780e7231661?w=400&h=300&fit=crop',
+ projectId: 'dexar',
+ itemCount: 15,
+ subcategories: [
+ {
+ id: 'smartphones',
+ name: 'Смартфоны',
+ visible: true,
+ priority: 1,
+ img: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
+ categoryId: 'electronics',
+ parentId: 'electronics',
+ itemCount: 8,
+ hasItems: true,
+ subcategories: []
+ },
+ {
+ id: 'laptops',
+ name: 'Ноутбуки',
+ visible: true,
+ priority: 2,
+ img: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
+ categoryId: 'electronics',
+ parentId: 'electronics',
+ itemCount: 6,
+ hasItems: true,
+ subcategories: []
+ }
+ ]
+ },
+ {
+ id: 'clothing',
+ categoryID: 2,
+ name: 'Одежда',
+ parentID: 0,
+ visible: true,
+ priority: 2,
+ img: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop',
+ projectId: 'dexar',
+ itemCount: 25,
+ subcategories: [
+ {
+ id: 'mens',
+ name: 'Мужская',
+ visible: true,
+ priority: 1,
+ img: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
+ categoryId: 'clothing',
+ parentId: 'clothing',
+ itemCount: 12,
+ hasItems: true,
+ subcategories: []
+ },
+ {
+ id: 'womens',
+ name: 'Женская',
+ visible: true,
+ priority: 2,
+ img: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
+ categoryId: 'clothing',
+ parentId: 'clothing',
+ itemCount: 13,
+ hasItems: true,
+ subcategories: []
+ }
+ ]
+ },
+ {
+ id: 'home',
+ categoryID: 3,
+ name: 'Дом и сад',
+ parentID: 0,
+ visible: true,
+ priority: 3,
+ img: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop',
+ projectId: 'dexar',
+ itemCount: 8,
+ subcategories: []
+ },
+ // Subcategories as flat entries (for the legacy flat category list)
+ {
+ id: 'smartphones',
+ categoryID: 11,
+ name: 'Смартфоны',
+ parentID: 1,
+ visible: true,
+ priority: 1,
+ img: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
+ itemCount: 8
+ },
+ {
+ id: 'laptops',
+ categoryID: 12,
+ name: 'Ноутбуки',
+ parentID: 1,
+ visible: true,
+ priority: 2,
+ img: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
+ itemCount: 6
+ },
+ {
+ id: 'mens',
+ categoryID: 21,
+ name: 'Мужская одежда',
+ parentID: 2,
+ visible: true,
+ priority: 1,
+ img: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
+ itemCount: 12
+ },
+ {
+ id: 'womens',
+ categoryID: 22,
+ name: 'Женская одежда',
+ parentID: 2,
+ visible: true,
+ priority: 2,
+ img: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
+ icon: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
+ itemCount: 13
+ }
+];
+
+// ─── Mock Items (backOffice format with ALL fields) ───
+
+const MOCK_ITEMS: any[] = [
+ {
+ id: 'iphone15',
+ itemID: 101,
+ name: 'iPhone 15 Pro Max',
+ visible: true,
+ priority: 1,
+ quantity: 50,
+ price: 149990,
+ discount: 0,
+ currency: 'RUB',
+ rating: 4.8,
+ remainings: 'high',
+ categoryID: 11,
+ imgs: [
+ 'https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=600&h=400&fit=crop',
+ 'https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=600&h=400&fit=crop' },
+ { url: 'https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=600&h=400&fit=crop' }
+ ],
+ tags: ['new', 'featured', 'apple'],
+ badges: ['new', 'bestseller'],
+ simpleDescription: 'Новейший iPhone с титановым корпусом и чипом A17 Pro',
+ description: [
+ { key: 'Цвет', value: 'Натуральный титан' },
+ { key: 'Память', value: '256 ГБ' },
+ { key: 'Дисплей', value: '6.7" Super Retina XDR' },
+ { key: 'Процессор', value: 'A17 Pro' },
+ { key: 'Камера', value: '48 Мп основная' },
+ { key: 'Аккумулятор', value: '4441 мАч' }
+ ],
+ descriptionFields: [
+ { key: 'Цвет', value: 'Натуральный титан' },
+ { key: 'Память', value: '256 ГБ' },
+ { key: 'Дисплей', value: '6.7" Super Retina XDR' },
+ { key: 'Процессор', value: 'A17 Pro' },
+ { key: 'Камера', value: '48 Мп основная' },
+ { key: 'Аккумулятор', value: '4441 мАч' }
+ ],
+ subcategoryId: 'smartphones',
+ translations: {
+ en: {
+ name: 'iPhone 15 Pro Max',
+ simpleDescription: 'Latest iPhone with titanium body and A17 Pro chip',
+ description: [
+ { key: 'Color', value: 'Natural Titanium' },
+ { key: 'Storage', value: '256GB' },
+ { key: 'Display', value: '6.7" Super Retina XDR' },
+ { key: 'Chip', value: 'A17 Pro' },
+ { key: 'Camera', value: '48MP main' },
+ { key: 'Battery', value: '4441 mAh' }
+ ]
+ }
+ },
+ comments: [
+ { id: 'c1', text: 'Отличный телефон! Камера просто огонь 🔥', author: 'Иван Петров', stars: 5, createdAt: '2025-12-15T10:30:00Z' },
+ { id: 'c2', text: 'Батарея держит весь день, очень доволен.', author: 'Мария Козлова', stars: 4, createdAt: '2026-01-05T14:20:00Z' }
+ ],
+ callbacks: [
+ { rating: 5, content: 'Отличный телефон! Камера просто огонь 🔥', userID: 'Иван Петров', timestamp: '2025-12-15T10:30:00Z' },
+ { rating: 4, content: 'Батарея держит весь день, очень доволен.', userID: 'Мария Козлова', timestamp: '2026-01-05T14:20:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'samsung-s24',
+ itemID: 102,
+ name: 'Samsung Galaxy S24 Ultra',
+ visible: true,
+ priority: 2,
+ quantity: 35,
+ price: 129990,
+ discount: 10,
+ currency: 'RUB',
+ rating: 4.6,
+ remainings: 'high',
+ categoryID: 11,
+ imgs: [
+ 'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=600&h=400&fit=crop' }
+ ],
+ tags: ['new', 'android', 'samsung'],
+ badges: ['new', 'sale'],
+ simpleDescription: 'Премиальный флагман Samsung с S Pen',
+ description: [
+ { key: 'Цвет', value: 'Титановый серый' },
+ { key: 'Память', value: '512 ГБ' },
+ { key: 'ОЗУ', value: '12 ГБ' },
+ { key: 'Дисплей', value: '6.8" Dynamic AMOLED 2X' }
+ ],
+ descriptionFields: [
+ { key: 'Цвет', value: 'Титановый серый' },
+ { key: 'Память', value: '512 ГБ' },
+ { key: 'ОЗУ', value: '12 ГБ' },
+ { key: 'Дисплей', value: '6.8" Dynamic AMOLED 2X' }
+ ],
+ subcategoryId: 'smartphones',
+ translations: {
+ en: {
+ name: 'Samsung Galaxy S24 Ultra',
+ simpleDescription: 'Premium Samsung flagship with S Pen',
+ description: [
+ { key: 'Color', value: 'Titanium Gray' },
+ { key: 'Storage', value: '512GB' },
+ { key: 'RAM', value: '12GB' },
+ { key: 'Display', value: '6.8" Dynamic AMOLED 2X' }
+ ]
+ }
+ },
+ comments: [
+ { id: 'c3', text: 'S Pen — топ, использую каждый день.', author: 'Алексей', stars: 5, createdAt: '2026-01-20T08:10:00Z' }
+ ],
+ callbacks: [
+ { rating: 5, content: 'S Pen — топ, использую каждый день.', userID: 'Алексей', timestamp: '2026-01-20T08:10:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'pixel-8',
+ itemID: 103,
+ name: 'Google Pixel 8 Pro',
+ visible: true,
+ priority: 3,
+ quantity: 20,
+ price: 89990,
+ discount: 15,
+ currency: 'RUB',
+ rating: 4.5,
+ remainings: 'medium',
+ categoryID: 11,
+ imgs: [
+ 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=600&h=400&fit=crop' }
+ ],
+ tags: ['sale', 'android', 'ai', 'google'],
+ badges: ['sale', 'hot'],
+ simpleDescription: 'Лучший смартфон для ИИ-фотографии',
+ description: [
+ { key: 'Цвет', value: 'Bay Blue' },
+ { key: 'Память', value: '256 ГБ' },
+ { key: 'Процессор', value: 'Tensor G3' }
+ ],
+ descriptionFields: [
+ { key: 'Цвет', value: 'Bay Blue' },
+ { key: 'Память', value: '256 ГБ' },
+ { key: 'Процессор', value: 'Tensor G3' }
+ ],
+ subcategoryId: 'smartphones',
+ translations: {},
+ comments: [],
+ callbacks: [],
+ questions: [
+ { question: 'Поддерживает eSIM?', answer: 'Да, поддерживает dual eSIM.', upvotes: 12, downvotes: 0 }
+ ]
+ },
+ {
+ id: 'macbook-pro',
+ itemID: 104,
+ name: 'MacBook Pro 16" M3 Max',
+ visible: true,
+ priority: 1,
+ quantity: 15,
+ price: 299990,
+ discount: 0,
+ currency: 'RUB',
+ rating: 4.9,
+ remainings: 'low',
+ categoryID: 12,
+ imgs: [
+ 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=600&h=400&fit=crop',
+ 'https://images.unsplash.com/photo-1541807084-5c52b6b3adef?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=600&h=400&fit=crop' },
+ { url: 'https://images.unsplash.com/photo-1541807084-5c52b6b3adef?w=600&h=400&fit=crop' }
+ ],
+ tags: ['featured', 'professional', 'apple'],
+ badges: ['exclusive', 'limited'],
+ simpleDescription: 'Мощный ноутбук для профессионалов',
+ description: [
+ { key: 'Процессор', value: 'Apple M3 Max' },
+ { key: 'ОЗУ', value: '36 ГБ' },
+ { key: 'Память', value: '1 ТБ SSD' },
+ { key: 'Дисплей', value: '16.2" Liquid Retina XDR' },
+ { key: 'Батарея', value: 'До 22 ч' }
+ ],
+ descriptionFields: [
+ { key: 'Процессор', value: 'Apple M3 Max' },
+ { key: 'ОЗУ', value: '36 ГБ' },
+ { key: 'Память', value: '1 ТБ SSD' },
+ { key: 'Дисплей', value: '16.2" Liquid Retina XDR' },
+ { key: 'Батарея', value: 'До 22 ч' }
+ ],
+ subcategoryId: 'laptops',
+ translations: {
+ en: {
+ name: 'MacBook Pro 16" M3 Max',
+ simpleDescription: 'Powerful laptop for professionals',
+ description: [
+ { key: 'Chip', value: 'Apple M3 Max' },
+ { key: 'RAM', value: '36GB' },
+ { key: 'Storage', value: '1TB SSD' },
+ { key: 'Display', value: '16.2" Liquid Retina XDR' },
+ { key: 'Battery', value: 'Up to 22h' }
+ ]
+ }
+ },
+ comments: [
+ { id: 'c4', text: 'Невероятная производительность. Рендер в 3 раза быстрее.', author: 'Дизайнер Про', stars: 5, createdAt: '2025-11-15T12:00:00Z' },
+ { id: 'c5', text: 'Стоит каждого рубля. Экран — сказка.', author: 'Видеоредактор', stars: 5, createdAt: '2026-02-01T09:00:00Z' }
+ ],
+ callbacks: [
+ { rating: 5, content: 'Невероятная производительность. Рендер в 3 раза быстрее.', userID: 'Дизайнер Про', timestamp: '2025-11-15T12:00:00Z' },
+ { rating: 5, content: 'Стоит каждого рубля. Экран — сказка.', userID: 'Видеоредактор', timestamp: '2026-02-01T09:00:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'dell-xps',
+ itemID: 105,
+ name: 'Dell XPS 15',
+ visible: true,
+ priority: 2,
+ quantity: 3,
+ price: 179990,
+ discount: 5,
+ currency: 'RUB',
+ rating: 4.3,
+ remainings: 'low',
+ categoryID: 12,
+ imgs: [
+ 'https://images.unsplash.com/photo-1593642702749-b7d2a804c22e?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1593642702749-b7d2a804c22e?w=600&h=400&fit=crop' }
+ ],
+ tags: ['windows', 'professional'],
+ badges: ['limited'],
+ simpleDescription: 'Тонкий и мощный Windows ноутбук',
+ description: [
+ { key: 'Процессор', value: 'Intel Core i9-13900H' },
+ { key: 'ОЗУ', value: '32 ГБ' },
+ { key: 'Дисплей', value: '15.6" OLED 3.5K' }
+ ],
+ descriptionFields: [
+ { key: 'Процессор', value: 'Intel Core i9-13900H' },
+ { key: 'ОЗУ', value: '32 ГБ' },
+ { key: 'Дисплей', value: '15.6" OLED 3.5K' }
+ ],
+ subcategoryId: 'laptops',
+ translations: {},
+ comments: [],
+ callbacks: [],
+ questions: []
+ },
+ {
+ id: 'jacket-leather',
+ itemID: 201,
+ name: 'Кожаная куртка Premium',
+ visible: true,
+ priority: 1,
+ quantity: 8,
+ price: 34990,
+ discount: 20,
+ currency: 'RUB',
+ rating: 4.7,
+ remainings: 'medium',
+ categoryID: 21,
+ imgs: [
+ 'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=600&h=400&fit=crop' }
+ ],
+ tags: ['leather', 'premium', 'winter'],
+ badges: ['sale', 'bestseller'],
+ simpleDescription: 'Стильная мужская кожаная куртка из натуральной кожи',
+ description: [
+ { key: 'Материал', value: 'Натуральная кожа' },
+ { key: 'Размеры', value: 'S, M, L, XL, XXL' },
+ { key: 'Цвет', value: 'Чёрный' },
+ { key: 'Подкладка', value: 'Полиэстер 100%' }
+ ],
+ descriptionFields: [
+ { key: 'Материал', value: 'Натуральная кожа' },
+ { key: 'Размеры', value: 'S, M, L, XL, XXL' },
+ { key: 'Цвет', value: 'Чёрный' },
+ { key: 'Подкладка', value: 'Полиэстер 100%' }
+ ],
+ subcategoryId: 'mens',
+ translations: {
+ en: {
+ name: 'Premium Leather Jacket',
+ simpleDescription: 'Stylish men\'s genuine leather jacket',
+ description: [
+ { key: 'Material', value: 'Genuine Leather' },
+ { key: 'Sizes', value: 'S, M, L, XL, XXL' },
+ { key: 'Color', value: 'Black' },
+ { key: 'Lining', value: '100% Polyester' }
+ ]
+ }
+ },
+ comments: [
+ { id: 'c6', text: 'Качество кожи отличное, сидит идеально.', author: 'Антон', stars: 5, createdAt: '2026-01-10T16:30:00Z' }
+ ],
+ callbacks: [
+ { rating: 5, content: 'Качество кожи отличное, сидит идеально.', userID: 'Антон', timestamp: '2026-01-10T16:30:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'dress-silk',
+ itemID: 202,
+ name: 'Шёлковое платье Elegance',
+ visible: true,
+ priority: 1,
+ quantity: 12,
+ price: 18990,
+ discount: 0,
+ currency: 'RUB',
+ rating: 4.9,
+ remainings: 'high',
+ categoryID: 22,
+ imgs: [
+ 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=600&h=400&fit=crop' }
+ ],
+ tags: ['silk', 'elegant', 'new'],
+ badges: ['new', 'featured'],
+ simpleDescription: 'Элегантное шёлковое платье для особых случаев',
+ description: [
+ { key: 'Материал', value: '100% Шёлк' },
+ { key: 'Размеры', value: 'XS, S, M, L' },
+ { key: 'Цвет', value: 'Бордовый' },
+ { key: 'Длина', value: 'Миди' }
+ ],
+ descriptionFields: [
+ { key: 'Материал', value: '100% Шёлк' },
+ { key: 'Размеры', value: 'XS, S, M, L' },
+ { key: 'Цвет', value: 'Бордовый' },
+ { key: 'Длина', value: 'Миди' }
+ ],
+ subcategoryId: 'womens',
+ translations: {},
+ comments: [
+ { id: 'c7', text: 'Восхитительное платье! Ткань потрясающая.', author: 'Елена', stars: 5, createdAt: '2026-02-14T20:00:00Z' },
+ { id: 'c8', text: 'Идеально на вечер. Рекомендую!', author: 'Наталья', stars: 5, createdAt: '2026-02-10T11:00:00Z' }
+ ],
+ callbacks: [
+ { rating: 5, content: 'Восхитительное платье! Ткань потрясающая.', userID: 'Елена', timestamp: '2026-02-14T20:00:00Z' },
+ { rating: 5, content: 'Идеально на вечер. Рекомендую!', userID: 'Наталья', timestamp: '2026-02-10T11:00:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'hoodie-basic',
+ itemID: 203,
+ name: 'Худи Oversize Basic',
+ visible: true,
+ priority: 3,
+ quantity: 45,
+ price: 5990,
+ discount: 0,
+ currency: 'RUB',
+ rating: 4.2,
+ remainings: 'high',
+ categoryID: 21,
+ imgs: [
+ 'https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=600&h=400&fit=crop' }
+ ],
+ tags: ['casual', 'basic'],
+ badges: [],
+ simpleDescription: 'Удобное худи свободного кроя на каждый день',
+ description: [
+ { key: 'Материал', value: 'Хлопок 80%, Полиэстер 20%' },
+ { key: 'Размеры', value: 'S, M, L, XL' },
+ { key: 'Цвет', value: 'Серый меланж' }
+ ],
+ descriptionFields: [
+ { key: 'Материал', value: 'Хлопок 80%, Полиэстер 20%' },
+ { key: 'Размеры', value: 'S, M, L, XL' },
+ { key: 'Цвет', value: 'Серый меланж' }
+ ],
+ subcategoryId: 'mens',
+ translations: {},
+ comments: [],
+ callbacks: [],
+ questions: []
+ },
+ {
+ id: 'sneakers-run',
+ itemID: 204,
+ name: 'Кроссовки AirPulse Run',
+ visible: true,
+ priority: 2,
+ quantity: 0,
+ price: 12990,
+ discount: 30,
+ currency: 'RUB',
+ rating: 4.4,
+ remainings: 'out',
+ categoryID: 21,
+ imgs: [
+ 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&h=400&fit=crop' }
+ ],
+ tags: ['sport', 'running'],
+ badges: ['sale', 'hot'],
+ simpleDescription: 'Лёгкие беговые кроссовки с пенной амортизацией',
+ description: [
+ { key: 'Верх', value: 'Текстильная сетка' },
+ { key: 'Подошва', value: 'Пена EVA' },
+ { key: 'Вес', value: '260 г' }
+ ],
+ descriptionFields: [
+ { key: 'Верх', value: 'Текстильная сетка' },
+ { key: 'Подошва', value: 'Пена EVA' },
+ { key: 'Вес', value: '260 г' }
+ ],
+ subcategoryId: 'mens',
+ translations: {},
+ comments: [
+ { id: 'c9', text: 'Нет в наличии уже месяц... Верните!', author: 'Бегун42', stars: 3, createdAt: '2026-02-05T07:00:00Z' }
+ ],
+ callbacks: [
+ { rating: 3, content: 'Нет в наличии уже месяц... Верните!', userID: 'Бегун42', timestamp: '2026-02-05T07:00:00Z' }
+ ],
+ questions: []
+ },
+ {
+ id: 'lamp-smart',
+ itemID: 301,
+ name: 'Умная лампа Homelight Pro',
+ visible: true,
+ priority: 1,
+ quantity: 100,
+ price: 3990,
+ discount: 0,
+ currency: 'RUB',
+ rating: 4.1,
+ remainings: 'high',
+ categoryID: 3,
+ imgs: [
+ 'https://images.unsplash.com/photo-1507473885765-e6ed057ab6fe?w=600&h=400&fit=crop'
+ ],
+ photos: [
+ { url: 'https://images.unsplash.com/photo-1507473885765-e6ed057ab6fe?w=600&h=400&fit=crop' }
+ ],
+ tags: ['smart-home', 'lighting'],
+ badges: ['featured'],
+ simpleDescription: 'Wi-Fi лампа с управлением через приложение и голосом',
+ description: [
+ { key: 'Яркость', value: '1100 лм' },
+ { key: 'Цветовая t°', value: '2700K–6500K' },
+ { key: 'Совместимость', value: 'Алиса, Google Home, Alexa' }
+ ],
+ descriptionFields: [
+ { key: 'Яркость', value: '1100 лм' },
+ { key: 'Цветовая t°', value: '2700K–6500K' },
+ { key: 'Совместимость', value: 'Алиса, Google Home, Alexa' }
+ ],
+ subcategoryId: 'home',
+ translations: {},
+ comments: [],
+ callbacks: [],
+ questions: []
+ }
+];
+
+// ─── Helper ───
+
+function getAllVisibleItems(): any[] {
+ return MOCK_ITEMS.filter(i => i.visible !== false);
+}
+
+function getItemsByCategoryId(categoryID: number): any[] {
+ return getAllVisibleItems().filter(i => i.categoryID === categoryID);
+}
+
+function respond(body: T, delayMs = 150) {
+ return of(new HttpResponse({ status: 200, body })).pipe(delay(delayMs));
+}
+
+// ─── The Interceptor ───
+
+export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
+ if (!(environment as any).useMockData) {
+ return next(req);
+ }
+
+ const url = req.url;
+
+ // ── GET /ping
+ if (url.endsWith('/ping') && req.method === 'GET') {
+ return respond({ message: 'pong (mock)' });
+ }
+
+ // ── GET /category (all categories flat list)
+ if (url.endsWith('/category') && req.method === 'GET') {
+ return respond(MOCK_CATEGORIES);
+ }
+
+ // ── GET /category/:id (items for a category)
+ const catItemsMatch = url.match(/\/category\/(\d+)$/);
+ if (catItemsMatch && req.method === 'GET') {
+ const catId = parseInt(catItemsMatch[1], 10);
+ const items = getItemsByCategoryId(catId);
+ return respond(items);
+ }
+
+ // ── GET /item/:id
+ const itemMatch = url.match(/\/item\/(\d+)$/);
+ if (itemMatch && req.method === 'GET') {
+ const itemId = parseInt(itemMatch[1], 10);
+ const item = MOCK_ITEMS.find(i => i.itemID === itemId);
+ if (item) {
+ return respond(item);
+ }
+ return of(new HttpResponse({ status: 404, body: { error: 'Item not found' } })).pipe(delay(100));
+ }
+
+ // ── GET /searchitems?search=...
+ if (url.includes('/searchitems') && req.method === 'GET') {
+ const search = req.params.get('search')?.toLowerCase() || '';
+ const items = getAllVisibleItems().filter(i =>
+ i.name.toLowerCase().includes(search) ||
+ i.simpleDescription?.toLowerCase().includes(search) ||
+ i.tags?.some((t: string) => t.toLowerCase().includes(search))
+ );
+ return respond({
+ items,
+ total: items.length,
+ count: items.length,
+ skip: 0
+ });
+ }
+
+ // ── GET /randomitems
+ if (url.includes('/randomitems') && req.method === 'GET') {
+ const count = parseInt(req.params.get('count') || '5', 10);
+ const shuffled = [...getAllVisibleItems()].sort(() => Math.random() - 0.5);
+ return respond(shuffled.slice(0, count));
+ }
+
+ // ── GET /cart (return empty)
+ if (url.endsWith('/cart') && req.method === 'GET') {
+ return respond([]);
+ }
+
+ // ── POST /cart (add to cart / create payment)
+ if (url.endsWith('/cart') && req.method === 'POST') {
+ const body = req.body as any;
+ if (body?.amount) {
+ // Payment mock
+ return respond({
+ qrId: 'mock-qr-' + Date.now(),
+ qrStatus: 'CREATED',
+ qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
+ payload: 'https://example.com/pay/mock',
+ qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
+ }, 300);
+ }
+ return respond({ message: 'Added (mock)' });
+ }
+
+ // ── PATCH /cart
+ if (url.endsWith('/cart') && req.method === 'PATCH') {
+ return respond({ message: 'Updated (mock)' });
+ }
+
+ // ── DELETE /cart
+ if (url.endsWith('/cart') && req.method === 'DELETE') {
+ return respond({ message: 'Removed (mock)' });
+ }
+
+ // ── POST /comment
+ if (url.endsWith('/comment') && req.method === 'POST') {
+ return respond({ message: 'Review submitted (mock)' }, 200);
+ }
+
+ // ── POST /purchase-email
+ if (url.endsWith('/purchase-email') && req.method === 'POST') {
+ return respond({ message: 'Email sent (mock)' }, 200);
+ }
+
+ // ── GET /qr/payment/:id (always return success for testing)
+ if (url.includes('/qr/payment/') && req.method === 'GET') {
+ return respond({
+ paymentStatus: 'SUCCESS',
+ code: 'SUCCESS',
+ amount: 0,
+ currency: 'RUB',
+ qrId: 'mock',
+ transactionId: 999,
+ transactionDate: new Date().toISOString(),
+ additionalInfo: '',
+ paymentPurpose: '',
+ createDate: new Date().toISOString(),
+ order: 'mock-order',
+ qrExpirationDate: new Date().toISOString(),
+ phoneNumber: ''
+ }, 500);
+ }
+
+ // Fallback — pass through
+ return next(req);
+};
diff --git a/src/app/models/category.model.ts b/src/app/models/category.model.ts
index 9b4501a..7575455 100644
--- a/src/app/models/category.model.ts
+++ b/src/app/models/category.model.ts
@@ -6,4 +6,28 @@ export interface Category {
wideBanner?: string | boolean;
itemCount?: number;
priority?: number;
+
+ // BackOffice API fields
+ id?: string;
+ visible?: boolean;
+ img?: string;
+ projectId?: string;
+ subcategories?: Subcategory[];
+}
+
+export interface Subcategory {
+ id: string;
+ name: string;
+ visible?: boolean;
+ priority?: number;
+ img?: string;
+ categoryId: string;
+ parentId: string;
+ itemCount?: number;
+ hasItems?: boolean;
+ subcategories?: Subcategory[];
+}
+
+export interface CategoryTranslation {
+ name?: string;
}
diff --git a/src/app/models/index.ts b/src/app/models/index.ts
index 491f438..d034c53 100644
--- a/src/app/models/index.ts
+++ b/src/app/models/index.ts
@@ -1,2 +1,3 @@
export * from './category.model';
export * from './item.model';
+
diff --git a/src/app/models/item.model.ts b/src/app/models/item.model.ts
index 6320ed0..abdcf34 100644
--- a/src/app/models/item.model.ts
+++ b/src/app/models/item.model.ts
@@ -5,6 +5,25 @@ export interface Photo {
type?: string;
}
+export interface DescriptionField {
+ key: string;
+ value: string;
+}
+
+export interface Comment {
+ id?: string;
+ text: string;
+ author?: string;
+ stars?: number;
+ createdAt?: string;
+}
+
+export interface ItemTranslation {
+ name?: string;
+ simpleDescription?: string;
+ description?: DescriptionField[];
+}
+
export interface Callback {
rating?: number;
content?: string;
@@ -34,7 +53,20 @@ export interface Item {
callbacks: Callback[] | null;
questions: Question[] | null;
partnerID?: string;
- quantity?: number; // For cart items
+ quantity?: number;
+
+ // BackOffice API fields
+ id?: string;
+ visible?: boolean;
+ priority?: number;
+ imgs?: string[];
+ tags?: string[];
+ badges?: string[];
+ simpleDescription?: string;
+ descriptionFields?: DescriptionField[];
+ subcategoryId?: string;
+ translations?: Record;
+ comments?: Comment[];
}
export interface CartItem extends Item {
diff --git a/src/app/pages/cart/cart.component.html b/src/app/pages/cart/cart.component.html
index 445c122..945aa13 100644
--- a/src/app/pages/cart/cart.component.html
+++ b/src/app/pages/cart/cart.component.html
@@ -44,7 +44,15 @@
- {{ item.description.substring(0, 100) }}...
+ {{ item.simpleDescription || item?.description?.substring?.(0, 100) || '' }}...
+
+ @if (item.badges && item.badges.length > 0) {
+
+ @for (badge of item.badges; track badge) {
+ {{ badge }}
+ }
+
+ }