diff --git a/package-lock.json b/package-lock.json index 1c91b97..1dbf849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,6 @@ "@angular/router": "21.1.5", "@angular/service-worker": "21.1.5", "@lucide/angular": "^1.25.0", - "primeicons": "^7.0.0", - "primeng": "^21.0.3", "rxjs": "~7.8.0", "tslib": "^2.8.0", "zone.js": "~0.16.0" @@ -29,6 +27,7 @@ "@angular/cli": "21.1.5", "@angular/compiler-cli": "21.1.5", "@types/jasmine": "~5.1.0", + "barry-cache": "^0.9.3", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -3085,48 +3084,6 @@ "license": "MIT", "optional": true }, - "node_modules/@primeuix/motion": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@primeuix/motion/-/motion-0.0.10.tgz", - "integrity": "sha512-PsZwOPq79Scp7/ionshRcQ5xKVf9+zuLcyY5mf6onK8chHT5C9JGphmcIZ4CzcqxuGEpsm8AIbTGy+zS3RtzLA==", - "license": "MIT", - "dependencies": { - "@primeuix/utils": "^0.6.3" - }, - "engines": { - "node": ">=12.11.0" - } - }, - "node_modules/@primeuix/styled": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", - "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", - "license": "MIT", - "dependencies": { - "@primeuix/utils": "^0.6.1" - }, - "engines": { - "node": ">=12.11.0" - } - }, - "node_modules/@primeuix/styles": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.2.tgz", - "integrity": "sha512-LNtkJsTonNHF5ag+9s3+zQzm00+LRmffw68QRIHy6S/dam1JpdrrAnUzNYlWbaY7aE2EkZvQmx7Np7+PyHn+ow==", - "license": "MIT", - "dependencies": { - "@primeuix/styled": "^0.7.4" - } - }, - "node_modules/@primeuix/utils": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.3.tgz", - "integrity": "sha512-/SLNQSKQ73WbBIsflKVqbpVjCfFYvQO3Sf1LMheXyxh8JqxO4M63dzP56wwm9OPGuCQ6MYOd2AHgZXz+g7PZcg==", - "license": "MIT", - "engines": { - "node": ">=12.11.0" - } - }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.58", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", @@ -4091,6 +4048,21 @@ "dev": true, "license": "MIT" }, + "node_modules/barry-cache": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/barry-cache/-/barry-cache-0.9.3.tgz", + "integrity": "sha512-HQmOPyifjXnV/99zOQ7oYsxOjOu4zwj1djC6BgkDvNEVMZftwXDYHEFQyaNXzbl6Zhk7VfxT6YXqBDkWkGPRxw==", + "dev": true, + "dependencies": { + "serve-handler": "^6.1.7" + }, + "bin": { + "barry-cache": "dist/cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -6936,9 +6908,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7678,6 +7650,13 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7812,34 +7791,6 @@ "dev": true, "license": "MIT" }, - "node_modules/primeicons": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", - "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", - "license": "MIT" - }, - "node_modules/primeng": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.0.3.tgz", - "integrity": "sha512-B5jrDU2vXZ5+ZNtOIxH2pLuLr8C/pwTsvEkppdq9ehgzwRj0eGbh2eoy8EbH8mWkcSXUuH7xneWi9veIrnwZ+Q==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@primeuix/motion": "^0.0.10", - "@primeuix/styled": "^0.7.4", - "@primeuix/styles": "^2.0.2", - "@primeuix/utils": "^0.6.3", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/cdk": "^21.0.0", - "@angular/common": "^21.0.0", - "@angular/core": "^21.0.7", - "@angular/forms": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/router": "^21.0.0", - "rxjs": "^6.0.0 || ^7.8.1" - } - }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -8276,6 +8227,82 @@ "url": "https://opencollective.com/express" } }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", diff --git a/package.json b/package.json index d25036d..0380fa3 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,6 @@ "@angular/router": "21.1.5", "@angular/service-worker": "21.1.5", "@lucide/angular": "^1.25.0", - "primeicons": "^7.0.0", - "primeng": "^21.0.3", "rxjs": "~7.8.0", "tslib": "^2.8.0", "zone.js": "~0.16.0" @@ -42,12 +40,12 @@ "@angular/cli": "21.1.5", "@angular/compiler-cli": "21.1.5", "@types/jasmine": "~5.1.0", + "barry-cache": "^0.9.3", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.9.3", - "barry-cache": "^0.1.0" + "typescript": "~5.9.3" } } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index bc4a975..f069a66 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { projectEditorDirtyGuard } from './features/project-editor/guards/projec import { adminAuthGuard } from './core/admin-auth/admin-auth.guard'; import { authRoutes } from './core/auth/auth.routes'; import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard'; +import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard'; import { AdminLayoutComponent } from './features/admin/shell/admin-layout.component'; import { environment } from '../environments/environment'; @@ -82,6 +83,7 @@ const coreRoutes: Routes = [ { path: 'products/create', loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent), + canDeactivate: [adminProductDirtyGuard], data: { titleKey: 'adminShell.pages.productCreate.title', descriptionKey: 'adminShell.pages.productCreate.description', @@ -91,6 +93,7 @@ const coreRoutes: Routes = [ { path: 'products/:id/edit', loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent), + canDeactivate: [adminProductDirtyGuard], data: { titleKey: 'adminShell.pages.productEdit.title', descriptionKey: 'adminShell.pages.productEdit.description', @@ -100,6 +103,7 @@ const coreRoutes: Routes = [ { path: 'products/:id/duplicate', loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent), + canDeactivate: [adminProductDirtyGuard], data: { titleKey: 'adminShell.pages.productDuplicate.title', descriptionKey: 'adminShell.pages.productDuplicate.description', diff --git a/src/app/core/search/services/search-history.service.ts b/src/app/core/search/services/search-history.service.ts deleted file mode 100644 index 1dc8cae..0000000 --- a/src/app/core/search/services/search-history.service.ts +++ /dev/null @@ -1 +0,0 @@ -export { SearchHistoryService } from '../../../features/search/services/search-history.service'; diff --git a/src/app/features/admin/products/facade/admin-products.facade.ts b/src/app/features/admin/products/facade/admin-products.facade.ts index bd036fa..3998260 100644 --- a/src/app/features/admin/products/facade/admin-products.facade.ts +++ b/src/app/features/admin/products/facade/admin-products.facade.ts @@ -93,6 +93,7 @@ export class AdminProductsFacade { readonly loading = signal(false); readonly selectedIds = signal([]); readonly draft = signal(null); + readonly dirty = signal(false); readonly editorMode = signal('create'); readonly hasSelection = computed(() => this.selectedIds().length > 0); @@ -283,20 +284,22 @@ export class AdminProductsFacade { startCreate(): void { this.editorMode.set('create'); this.draft.set(this.formFactory.createEmpty()); + this.dirty.set(false); } loadForEdit(id: string, mode: AdminProductEditorMode = 'edit'): void { this.editorMode.set(mode); if (mode === 'duplicate') { - this.gateway.duplicateProduct(id).pipe(take(1)).subscribe({ next: product => this.draft.set(product) }); + this.gateway.duplicateProduct(id).pipe(take(1)).subscribe({ next: product => { this.draft.set(product); this.dirty.set(false); } }); return; } - this.gateway.loadProduct(id).pipe(take(1)).subscribe({ next: product => this.draft.set(product ? { ...product } : null) }); + this.gateway.loadProduct(id).pipe(take(1)).subscribe({ next: product => { this.draft.set(product ? { ...product } : null); this.dirty.set(false); } }); } updateDraft(patch: Partial): void { this.draft.update(current => current ? ({ ...current, ...patch, updatedAt: new Date().toISOString() }) : current); + this.dirty.set(true); } saveDraft(): void { @@ -307,7 +310,7 @@ export class AdminProductsFacade { ? this.gateway.createProduct(draft) : this.gateway.updateProduct(draft); - request.pipe(take(1)).subscribe({ next: () => this.loadList() }); + request.pipe(take(1)).subscribe({ next: () => { this.dirty.set(false); this.loadList(); } }); } deleteOne(id: string): void { diff --git a/src/app/features/admin/products/guards/admin-product-dirty.guard.ts b/src/app/features/admin/products/guards/admin-product-dirty.guard.ts new file mode 100644 index 0000000..b71f8ec --- /dev/null +++ b/src/app/features/admin/products/guards/admin-product-dirty.guard.ts @@ -0,0 +1,14 @@ +import { inject } from '@angular/core'; +import { CanDeactivateFn } from '@angular/router'; +import { AdminProductsFacade } from '../facade/admin-products.facade'; +import { AdminProductEditorPageComponent } from '../pages/admin-product-editor-page.component'; +import { TranslateService } from '../../../../i18n/translate.service'; + +export const adminProductDirtyGuard: CanDeactivateFn = () => { + const facade = inject(AdminProductsFacade); + if (!facade.dirty()) { + return true; + } + const translate = inject(TranslateService); + return window.confirm(translate.t('adminProducts.confirmLeaveUnsaved')); +}; diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 7e6ceb9..aac4058 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -1543,6 +1543,7 @@ export const en: Translations = { recommendNone: 'Nice work — your category structure is in good shape.', }, adminProducts: { + confirmLeaveUnsaved: 'You have unsaved changes. Leave without saving?', emptyTitle: 'No products found', emptyDescription: 'Try adjusting your filters, or create a new product.', emptyGuide: 'Good products have a clear title, at least one photo, a price, and a short description — that\'s enough to publish. You can always add more detail later.', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 6496ed7..7fa0606 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -1538,6 +1538,7 @@ export const hy: Translations = { recommendNone: 'Հիանալի է․ կատեգորիաների կառուցվածքը լավ վիճակում է։', }, adminProducts: { + confirmLeaveUnsaved: 'Դուք ունեք չպահպանված փոփոխություններ։ Դո՞ւրս գալ առանց պահպանելու։', emptyTitle: 'Ապրանքներ չեն գտնվել', emptyDescription: 'Փոխեք ֆիլտրերը կամ ստեղծեք նոր ապրանք։', emptyGuide: 'Լավ ապրանքին բավական է հստակ վերնագիր, առնվազն մեկ լուսանկար, գին և կարճ նկարագրություն՝ հրապարակելու համար։ Մնացածը կարող եք ավելացնել հետո։', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index f0396a9..c98d6de 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -1538,6 +1538,7 @@ export const ru: Translations = { recommendNone: 'Отлично — структура категорий в хорошем состоянии.', }, adminProducts: { + confirmLeaveUnsaved: 'У вас есть несохранённые изменения. Выйти без сохранения?', emptyTitle: 'Товары не найдены', emptyDescription: 'Измените фильтры или создайте новый товар.', emptyGuide: 'Хорошему товару нужны понятное название, минимум одно фото, цена и короткое описание — этого достаточно для публикации. Остальное можно добавить позже.', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 52be344..3b4f37c 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -1551,6 +1551,7 @@ export interface Translations { recommendNone: string; }; adminProducts: { + confirmLeaveUnsaved: string; emptyTitle: string; emptyDescription: string; emptyGuide: string;