diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 4a404ef..5c4e273 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -116,6 +116,19 @@ export const TRANSLATIONS: Record> = { NO_TRANSLATIONS: 'No Russian translation yet', TRANSLATION_SAVED: 'Translation saved', + // --- Attributes tab --- + ATTRIBUTES: 'Attributes', + ATTRIBUTES_HINT: 'Key-value attributes for product specifications.', + ATTR_KEY: 'Attribute key', + ATTR_VALUE: 'Attribute value', + ATTR_KEY_PLACEHOLDER: 'e.g. Material', + ATTR_VALUE_PLACEHOLDER: 'e.g. Cotton 100%', + NO_ATTRIBUTES: 'No attributes yet', + + // --- New item fields --- + COLOUR: 'Colour', + SIZE: 'Size', + // --- CRUD / Toast messages --- CONFIRM_DELETE: 'Are you sure you want to delete', VALIDATION_ERROR: 'Validation error', @@ -261,6 +274,19 @@ export const TRANSLATIONS: Record> = { NO_TRANSLATIONS: 'Русский перевод не заполнен', TRANSLATION_SAVED: 'Перевод сохранён', + // --- Attributes tab --- + ATTRIBUTES: 'Атрибуты', + ATTRIBUTES_HINT: 'Ключ-значение атрибуты для характеристик товара.', + ATTR_KEY: 'Ключ атрибута', + ATTR_VALUE: 'Значение атрибута', + ATTR_KEY_PLACEHOLDER: 'напр. Материал', + ATTR_VALUE_PLACEHOLDER: 'напр. Хлопок 100%', + NO_ATTRIBUTES: 'Атрибутов пока нет', + + // --- New item fields --- + COLOUR: 'Цвет', + SIZE: 'Размер', + // --- CRUD / Toast messages --- CONFIRM_DELETE: 'Вы уверены, что хотите удалить', VALIDATION_ERROR: 'Ошибка валидации', diff --git a/src/app/models/item.model.ts b/src/app/models/item.model.ts index 525291c..ed57a4b 100644 --- a/src/app/models/item.model.ts +++ b/src/app/models/item.model.ts @@ -7,6 +7,24 @@ export interface ItemTranslation { description?: ItemDescriptionField[]; } +/** Localized name entry */ +export interface ItemName { + language: string; + value: string; +} + +/** Localized description entry */ +export interface ItemDescription { + language: string; + value: string; +} + +/** Key-value attribute pair */ +export interface ItemAttribute { + key: string; + value: string; +} + export interface Item { id: string; name: string; @@ -19,9 +37,14 @@ export interface Item { imgs: string[]; tags: string[]; badges?: string[]; + colour?: string; + size?: string; simpleDescription: string; description: ItemDescriptionField[]; subcategoryId: string; + names?: ItemName[]; + descriptions?: ItemDescription[]; + attributes?: ItemAttribute[]; comments?: Comment[]; /** Optional translations keyed by language code: { ru: { name: '...', simpleDescription: '...', description: [...] } } */ translations?: { [lang: string]: ItemTranslation }; diff --git a/src/app/pages/item-editor/item-editor.component.html b/src/app/pages/item-editor/item-editor.component.html index cd2a91c..b7e55b4 100644 --- a/src/app/pages/item-editor/item-editor.component.html +++ b/src/app/pages/item-editor/item-editor.component.html @@ -135,6 +135,26 @@ (blur)="onFieldChange('simpleDescription', item()!.simpleDescription)"> + +
+ + {{ 'COLOUR' | translate }} + + + + + {{ 'SIZE' | translate }} + + +
@@ -395,7 +415,7 @@ -
@@ -444,7 +464,78 @@
- --> + + + +
+
+

{{ 'ATTRIBUTES' | translate }}

+

{{ 'ATTRIBUTES_HINT' | translate }}

+ +
+ + {{ 'ATTR_KEY' | translate }} + + + + + {{ 'ATTR_VALUE' | translate }} + + + + +
+ +
+ @for (attr of item()!.attributes || []; track $index) { +
+ + {{ 'ATTR_KEY' | translate }} + + + + + {{ 'ATTR_VALUE' | translate }} + + + + +
+ } +
+ + @if (!(item()!.attributes?.length)) { +
+ tune +

{{ 'NO_ATTRIBUTES' | translate }}

+
+ } +
+
+
} diff --git a/src/app/pages/item-editor/item-editor.component.ts b/src/app/pages/item-editor/item-editor.component.ts index 1c8b7a4..0f1d2fa 100644 --- a/src/app/pages/item-editor/item-editor.component.ts +++ b/src/app/pages/item-editor/item-editor.component.ts @@ -18,7 +18,7 @@ import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag- import { ApiService } from '../../services'; import { ValidationService } from '../../services/validation.service'; import { ToastService } from '../../services/toast.service'; -import { Item, ItemDescriptionField, Subcategory } from '../../models'; +import { Item, ItemDescriptionField, ItemAttribute, Subcategory } from '../../models'; import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'; import { LoadingSkeletonComponent } from '../../components/loading-skeleton/loading-skeleton.component'; import { LanguageService } from '../../services/language.service'; @@ -82,6 +82,8 @@ export class ItemEditorComponent implements OnInit { ]; newBadge = ''; + newAttrKey = ''; + newAttrValue = ''; private destroyRef = inject(DestroyRef); @@ -337,6 +339,36 @@ export class ItemEditorComponent implements OnInit { } } + // Attributes handling + addAttribute() { + if (!this.newAttrKey.trim() || !this.newAttrValue.trim()) return; + const currentItem = this.item(); + if (!currentItem) return; + const attrs = [...(currentItem.attributes || []), { key: this.newAttrKey.trim(), value: this.newAttrValue.trim() }]; + currentItem.attributes = attrs; + this.onFieldChange('attributes' as any, attrs); + this.newAttrKey = ''; + this.newAttrValue = ''; + } + + updateAttribute(index: number, field: 'key' | 'value', value: string) { + const currentItem = this.item(); + if (!currentItem) return; + const attrs = [...(currentItem.attributes || [])]; + attrs[index] = { ...attrs[index], [field]: value }; + currentItem.attributes = attrs; + this.onFieldChange('attributes' as any, attrs); + } + + removeAttribute(index: number) { + const currentItem = this.item(); + if (!currentItem) return; + const attrs = [...(currentItem.attributes || [])]; + attrs.splice(index, 1); + currentItem.attributes = attrs; + this.onFieldChange('attributes' as any, attrs); + } + goBack() { const item = this.item(); if (item && item.subcategoryId) { diff --git a/src/app/services/mock-data.service.ts b/src/app/services/mock-data.service.ts index 1cf1415..540ec64 100644 --- a/src/app/services/mock-data.service.ts +++ b/src/app/services/mock-data.service.ts @@ -98,6 +98,8 @@ export class MockDataService { ], tags: ['new', 'featured', 'bestseller'], badges: ['new', 'featured'], + colour: 'Natural Titanium', + size: '', simpleDescription: 'Latest iPhone with titanium design and A17 Pro chip', description: [ { key: 'Color', value: 'Natural Titanium' }, @@ -105,6 +107,19 @@ export class MockDataService { { key: 'Display', value: '6.7 inch Super Retina XDR' }, { key: 'Chip', value: 'A17 Pro' } ], + attributes: [ + { key: 'Color', value: 'Natural Titanium' }, + { key: 'Storage', value: '256GB' }, + { key: 'Chip', value: 'A17 Pro' } + ], + names: [ + { language: 'ru', value: 'iPhone 15 Pro Max' }, + { language: 'en', value: 'iPhone 15 Pro Max' } + ], + descriptions: [ + { language: 'ru', value: 'Новейший iPhone с титановым корпусом и чипом A17 Pro' }, + { language: 'en', value: 'Latest iPhone with titanium design and A17 Pro chip' } + ], subcategoryId: 'sub1', comments: [ { @@ -215,8 +230,13 @@ export class MockDataService { currency: 'USD', imgs: [`https://placehold.co/600x400?text=Product+${i}`], tags: ['test'], + colour: '', + size: i % 2 === 0 ? 'M' : 'L', simpleDescription: `This is test product number ${i}`, description: [{ key: 'Size', value: 'Medium' }], + attributes: [{ key: 'Size', value: i % 2 === 0 ? 'M' : 'L' }], + names: [], + descriptions: [], subcategoryId }); } @@ -434,8 +454,13 @@ export class MockDataService { imgs: data.imgs || [], tags: data.tags || [], badges: data.badges || [], + colour: data.colour || '', + size: data.size || '', simpleDescription: data.simpleDescription || '', description: data.description || [], + attributes: data.attributes || [], + names: data.names || [], + descriptions: data.descriptions || [], subcategoryId }; this.items.push(newItem);