From ba999b7dbbe2ae50dc6b80c4ab0688ea09d9743c Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sun, 19 Jul 2026 08:35:18 +0400 Subject: [PATCH] feat(admin): category editor auto-slug, SEO fill, breadcrumb and icon explainers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 P0 user feedback: category icon input and breadcrumbs ('Хлебные крошки') were unexplained, slug and SEO were manual. - Title edits derive the slug and SEO title with the same never-overwrite-manual-input rule as products (verified in browser) - SEO group gains the same 'Fill from product details' one-click action - Icon field now explains itself (emoji/symbol shown in menus) and shows a live preview of the entered symbol - Breadcrumb block explains in merchant language what customers see (Home › Electronics › Phones) and where it comes from; separator changed to › to match the storefront - New adminCategories keys (navBreadcrumbHint, iconHint) in en/ru/hy --- .../admin-category-form.component.html | 20 +++++++++--- .../admin-category-form.component.scss | 5 +++ .../admin-category-form.component.ts | 32 +++++++++++++++++++ src/app/i18n/en.ts | 2 ++ src/app/i18n/hy.ts | 2 ++ src/app/i18n/ru.ts | 2 ++ src/app/i18n/translations.ts | 2 ++ 7 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/app/features/admin/categories/components/admin-category-form.component.html b/src/app/features/admin/categories/components/admin-category-form.component.html index 28b0760..05f6c59 100644 --- a/src/app/features/admin/categories/components/admin-category-form.component.html +++ b/src/app/features/admin/categories/components/admin-category-form.component.html @@ -27,9 +27,9 @@ }
- + - + - - + +
+ + @if (category.icon) { + + } +
@@ -73,6 +78,10 @@ @if (activeGroup() === 'seo') {

{{ 'adminProducts.seoExplain' | translate }}

+
+ {{ 'adminProducts.seoGenerate' | translate }} + {{ 'adminProducts.seoGenerateHint' | translate }} +
@@ -97,7 +106,8 @@ @if (activeGroup() === 'navigation') {

{{ 'adminCategories.navBreadcrumb' | translate }}

-

{{ breadcrumb.length > 0 ? breadcrumb.join(' / ') : ('adminCategories.navRoot' | translate) }}

+ +

{{ breadcrumb.length > 0 ? breadcrumb.join(' › ') : ('adminCategories.navRoot' | translate) }}

{{ 'adminCategories.navChildren' | translate }}

@if (children.length === 0) { diff --git a/src/app/features/admin/categories/components/admin-category-form.component.scss b/src/app/features/admin/categories/components/admin-category-form.component.scss index b55ba4d..3c86045 100644 --- a/src/app/features/admin/categories/components/admin-category-form.component.scss +++ b/src/app/features/admin/categories/components/admin-category-form.component.scss @@ -42,3 +42,8 @@ input[type='checkbox'] { width: auto; } .seo-preview__title { margin: 0; color: #1a0dab; font-size: 1rem; } .seo-preview__url { margin: 0; color: #006621; font-size: 0.8rem; } .seo-preview__description { margin: 0; color: var(--text-secondary, #5f6e6a); font-size: 0.85rem; } +.icon-field { display: flex; align-items: center; gap: 8px; app-input { flex: 1; } } +.icon-field__preview { font-size: 1.4rem; line-height: 1; } +.seo-generate-row { display: flex; align-items: center; gap: 10px; } +.seo-generate-row__hint { color: var(--text-secondary, #6b7280); font-size: 0.8rem; } +.breadcrumb-hint { margin: 0 0 4px; color: var(--text-secondary, #6b7280); font-size: 0.85rem; } diff --git a/src/app/features/admin/categories/components/admin-category-form.component.ts b/src/app/features/admin/categories/components/admin-category-form.component.ts index f1ad1fa..754f3f8 100644 --- a/src/app/features/admin/categories/components/admin-category-form.component.ts +++ b/src/app/features/admin/categories/components/admin-category-form.component.ts @@ -78,6 +78,38 @@ export class AdminCategoryFormComponent { this.categoryChange.emit({ [key]: value } as Partial); } + /** + * Title edits keep the slug and SEO title in sync only while they still hold + * their auto-derived value (or are empty). A manually edited slug/SEO title + * no longer matches the derived value, so it is never overwritten. + */ + updateTitle(value: string): void { + const patch: Partial = { title: value }; + const previousAutoSlug = this.slugify(this.category.title); + if (!this.category.slug || this.category.slug === previousAutoSlug) { + patch.slug = this.slugify(value); + } + if (!this.category.seo.metaTitle || this.category.seo.metaTitle === this.category.title) { + patch.seo = { ...this.category.seo, metaTitle: value }; + } + this.categoryChange.emit(patch); + } + + /** Fills empty SEO fields from title/description; existing values stay untouched. */ + generateSeo(): void { + this.categoryChange.emit({ + seo: { + metaTitle: this.category.seo.metaTitle || this.category.title, + metaDescription: this.category.seo.metaDescription || this.category.description || this.category.title, + keywords: this.category.seo.keywords || this.slugify(this.category.title).split('-').filter(Boolean).join(', '), + }, + }); + } + + private slugify(value: string): string { + return value.toLowerCase().trim().replace(/[^a-z0-9а-яёա-ֆ]+/gi, '-').replace(/(^-|-$)/g, ''); + } + updateTranslation(locale: string, field: 'title' | 'description', value: string): void { this.categoryChange.emit({ translations: { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index fd39939..a6f7d62 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -1417,6 +1417,8 @@ export const en: Translations = { groupAdvanced: 'Advanced', visibilityExplain: 'A hidden category and its products stay out of the storefront navigation and search until you make it visible again.', navBreadcrumb: 'Breadcrumb', + navBreadcrumbHint: 'This is the path customers see above this category, e.g. Home › Electronics › Phones. It comes from the parent category you choose.', + iconHint: 'A small emoji or symbol shown next to the category name in menus, e.g. 📱. Leave empty for none.', navRoot: 'This is a top-level category.', navChildren: 'Subcategories', navNoChildren: 'No subcategories yet.', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 147344e..e3e2128 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -1412,6 +1412,8 @@ export const hy: Translations = { groupAdvanced: 'Լրացուցիչ', visibilityExplain: 'Թաքցված կատեգորիան և դրա ապրանքները չեն երևում խանութի նավիգացիայում և որոնման մեջ, մինչև այն նորից տեսանելի դարձնեք։', navBreadcrumb: 'Ուղենիշ', + navBreadcrumbHint: 'Սա այն ուղին է, որը գնորդները տեսնում են կատեգորիայի վերևում, օր.՝ Գլխավոր › Էլեկտրոնիկա › Հեռախոսներ։ Այն կառուցվում է ընտրված ծնող կատեգորիայից։', + iconHint: 'Փոքր էմոջի կամ նշան կատեգորիայի անվան կողքին մենյուում, օր.՝ 📱։ Թողեք դատարկ, եթե պետք չէ։', navRoot: 'Սա վերին մակարդակի կատեգորիա է։', navChildren: 'Ենթակատեգորիաներ', navNoChildren: 'Ենթակատեգորիաներ դեռ չկան։', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 6f9b254..c2a8540 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -1412,6 +1412,8 @@ export const ru: Translations = { groupAdvanced: 'Дополнительно', visibilityExplain: 'Скрытая категория и её товары не отображаются в навигации и поиске магазина, пока вы снова не сделаете её видимой.', navBreadcrumb: 'Хлебные крошки', + navBreadcrumbHint: 'Это путь, который покупатели видят над категорией, напр. Главная › Электроника › Телефоны. Он строится из выбранной родительской категории.', + iconHint: 'Небольшой эмодзи или символ рядом с названием категории в меню, напр. 📱. Оставьте пустым, если не нужен.', navRoot: 'Это категория верхнего уровня.', navChildren: 'Подкатегории', navNoChildren: 'Подкатегорий пока нет.', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index bb3fb4f..ea6c6f0 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -1424,6 +1424,8 @@ export interface Translations { groupAdvanced: string; visibilityExplain: string; navBreadcrumb: string; + navBreadcrumbHint: string; + iconHint: string; navRoot: string; navChildren: string; navNoChildren: string;