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 }} + } +
+ }