From 419cb9a32c7406b6a45f1ee6935b058a41f352d1 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sat, 18 Jul 2026 08:59:29 +0400 Subject: [PATCH] feat(admin): redesign dashboard into professional SaaS homepage Replaced the placeholder dashboard with a real business homepage built on 6 new reusable widget components (DashboardSection, DashboardCard, DashboardMetric, DashboardStatusRow, DashboardTimeline, DashboardShortcutCard), all wired through shared app-card/app-skeleton/ app-empty-state. Sections: Welcome (tenant, env badge, current user, last publish/save - all real facade signals, never blank), Quick Actions (large cards: add product, create category, open builder, edit homepage, media, orders), Draft Status (real dirty/modifiedFields/publish/discard from ProjectEditorFacade), Marketplace Health (9 checks - config, product/ category counts, missing translations, draft, static-pages-unpublished, homepage/theme configured, all real; images-without-alt shown as 'not tracked yet' rather than fabricated), Recent Activity (existing localStorage-backed history service, loading/empty states), Useful Shortcuts, Documentation (honest coming-soon list, no dead links). 12-column responsive grid: 3-across cards on desktop, 2-column on tablet, single column on mobile, no horizontal scroll. Keyboard/focus/ ARIA per shortcut card and status row; health status never conveyed by color alone (icon + text every time). AdminDashboardHealthCheck (boolean 'healthy' shape) and its facade signal are untouched - AdminMonitoringPageComponent also consumes them. New richer status data lives in a separate homeHealthChecks signal/ AdminDashboardHomeHealthCheck type instead of widening the shared one. Added ~50 new dashboard.* / kept existing i18n strings across ru/en/hy; no raw keys. Co-Authored-By: Claude Sonnet 5 --- .../admin-dashboard-activity.component.html | 12 - .../admin-dashboard-activity.component.scss | 42 ---- .../admin-dashboard-activity.component.ts | 21 -- .../admin-dashboard-card.component.html | 23 -- .../admin-dashboard-card.component.scss | 94 -------- .../admin-dashboard-card.component.ts | 21 -- .../admin-dashboard-health.component.html | 12 - .../admin-dashboard-health.component.scss | 51 ---- .../admin-dashboard-health.component.ts | 20 -- ...min-dashboard-quick-actions.component.html | 10 - ...min-dashboard-quick-actions.component.scss | 59 ----- ...admin-dashboard-quick-actions.component.ts | 17 -- .../components/dashboard-card.component.html | 16 ++ .../components/dashboard-card.component.scss | 45 ++++ .../components/dashboard-card.component.ts | 21 ++ .../dashboard-metric.component.html | 17 ++ .../dashboard-metric.component.scss | 46 ++++ .../components/dashboard-metric.component.ts | 20 ++ .../dashboard-section.component.html | 14 ++ .../dashboard-section.component.scss | 42 ++++ .../components/dashboard-section.component.ts | 20 ++ .../dashboard-shortcut-card.component.html | 18 ++ .../dashboard-shortcut-card.component.scss | 90 +++++++ .../dashboard-shortcut-card.component.ts | 19 ++ .../dashboard-status-row.component.html | 15 ++ .../dashboard-status-row.component.scss | 48 ++++ .../dashboard-status-row.component.ts | 38 +++ .../dashboard-timeline.component.html | 42 ++++ .../dashboard-timeline.component.scss | 46 ++++ .../dashboard-timeline.component.ts | 26 ++ .../facade/admin-dashboard.facade.ts | 105 ++++++-- .../dashboard/models/admin-dashboard.model.ts | 37 ++- .../pages/admin-dashboard-page.component.html | 128 ++++++++-- .../pages/admin-dashboard-page.component.scss | 228 +++++++++++++++--- .../pages/admin-dashboard-page.component.ts | 219 +++++++---------- src/app/i18n/en.ts | 50 +++- src/app/i18n/hy.ts | 50 +++- src/app/i18n/ru.ts | 50 +++- src/app/i18n/translations.ts | 48 ++++ 39 files changed, 1291 insertions(+), 589 deletions(-) delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-activity.component.html delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-activity.component.scss delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-activity.component.ts delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-card.component.html delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-card.component.scss delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-card.component.ts delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-health.component.html delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-health.component.scss delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-health.component.ts delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.html delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.scss delete mode 100644 src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-card.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-card.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-card.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-metric.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-metric.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-metric.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-section.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-section.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-section.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-status-row.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-status-row.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-status-row.component.ts create mode 100644 src/app/features/admin/dashboard/components/dashboard-timeline.component.html create mode 100644 src/app/features/admin/dashboard/components/dashboard-timeline.component.scss create mode 100644 src/app/features/admin/dashboard/components/dashboard-timeline.component.ts diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.html b/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.html deleted file mode 100644 index 2a0f798..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
-

{{ 'dashboard.activityTitle' | translate }}

- -

{{ 'dashboard.activityEmpty' | translate }}

- -
    -
  • - {{ entry.labelKey | translate }} - {{ entry.timeText }} -
  • -
-
diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.scss b/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.scss deleted file mode 100644 index fb0ab89..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -.dashboard-activity__title { - margin: 0 0 12px; - font-size: 16px; - font-weight: 700; - color: var(--text-primary, #1e3c38); -} - -.dashboard-activity__empty { - margin: 0; - font-size: 14px; - color: var(--text-light, #828e8d); -} - -.dashboard-activity__list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 8px; -} - -.dashboard-activity__item { - display: flex; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; - border: 1px solid var(--border-color, #d3dad9); - border-radius: var(--radius-sm, 8px); - background: var(--bg-secondary, #f5f5f5); - font-size: 14px; -} - -.dashboard-activity__label { - color: var(--text-primary, #1e3c38); - font-weight: 600; -} - -.dashboard-activity__time { - color: var(--text-light, #828e8d); - white-space: nowrap; -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.ts b/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.ts deleted file mode 100644 index 8db9362..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-activity.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslatePipe } from '../../../../i18n/translate.pipe'; - -export interface AdminDashboardActivityViewEntry { - id: string; - labelKey: string; - timeText: string; -} - -@Component({ - selector: 'app-admin-dashboard-activity', - standalone: true, - imports: [CommonModule, TranslatePipe], - templateUrl: './admin-dashboard-activity.component.html', - styleUrls: ['./admin-dashboard-activity.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AdminDashboardActivityComponent { - @Input() entries: AdminDashboardActivityViewEntry[] = []; -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.html b/src/app/features/admin/dashboard/components/admin-dashboard-card.component.html deleted file mode 100644 index cd9cd7c..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
- -

{{ title }}

-
- -
- - - -

{{ 'dashboard.stateEmpty' | translate }}

- -

{{ 'dashboard.stateError' | translate }}

- -

{{ 'dashboard.statePendingBackend' | translate }}

- - -

{{ value }}

-

{{ subtitle }}

-
-
-
-
diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.scss b/src/app/features/admin/dashboard/components/admin-dashboard-card.component.scss deleted file mode 100644 index ec49bac..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.scss +++ /dev/null @@ -1,94 +0,0 @@ -.dashboard-card { - display: flex; - flex-direction: column; - gap: 10px; - padding: 18px; - border: 1px solid var(--border-color, #d3dad9); - border-radius: var(--radius-md, 12px); - background: var(--bg-primary, #fff); - box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06)); - min-height: 108px; - transition: box-shadow var(--transition-normal, 180ms ease), border-color var(--transition-normal, 180ms ease), transform var(--transition-normal, 180ms ease); - animation: dashboard-card-in 320ms ease-out both; -} - -.dashboard-card:hover { - border-color: var(--primary-color, #497671); - box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.1)); - transform: translateY(-2px); -} - -@keyframes dashboard-card-in { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } -} - -@media (prefers-reduced-motion: reduce) { - .dashboard-card { - animation: none; - transition: none; - } - - .dashboard-card:hover { - transform: none; - } -} - -.dashboard-card--error { - border-color: var(--error-color, #ef4444); -} - -.dashboard-card--pending { - opacity: 0.75; -} - -.dashboard-card__header { - display: flex; - align-items: center; - gap: 8px; -} - -.dashboard-card__icon { - font-size: 18px; - line-height: 1; -} - -.dashboard-card__title { - margin: 0; - font-size: 13px; - font-weight: 600; - color: var(--text-secondary, #667a77); - text-transform: uppercase; - letter-spacing: 0.02em; -} - -.dashboard-card__body { - display: flex; - flex-direction: column; - gap: 4px; -} - -.dashboard-card__value { - margin: 0; - font-size: 24px; - font-weight: 700; - color: var(--text-primary, #1e3c38); - word-break: break-word; -} - -.dashboard-card__subtitle { - margin: 0; - font-size: 13px; - color: var(--text-light, #828e8d); -} - -.dashboard-card__muted { - margin: 0; - font-size: 14px; - color: var(--text-light, #828e8d); -} - -.dashboard-card__muted--error { - color: var(--error-color, #ef4444); -} - diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.ts b/src/app/features/admin/dashboard/components/admin-dashboard-card.component.ts deleted file mode 100644 index 1f38f56..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-card.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslatePipe } from '../../../../i18n/translate.pipe'; -import { AdminDashboardCardStatus } from '../models/admin-dashboard.model'; -import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; - -@Component({ - selector: 'app-admin-dashboard-card', - standalone: true, - imports: [CommonModule, TranslatePipe, SkeletonComponent], - templateUrl: './admin-dashboard-card.component.html', - styleUrls: ['./admin-dashboard-card.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AdminDashboardCardComponent { - @Input() title = ''; - @Input() status: AdminDashboardCardStatus = 'ready'; - @Input() value: string | null = null; - @Input() subtitle: string | null = null; - @Input() icon: string | null = null; -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.html b/src/app/features/admin/dashboard/components/admin-dashboard-health.component.html deleted file mode 100644 index 4f63c83..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
-

{{ 'dashboard.healthTitle' | translate }}

- -

{{ 'dashboard.healthAllClear' | translate }}

- -
    -
  • - - {{ check.labelKey | translate }} -
  • -
-
diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.scss b/src/app/features/admin/dashboard/components/admin-dashboard-health.component.scss deleted file mode 100644 index ca35a52..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.scss +++ /dev/null @@ -1,51 +0,0 @@ -.dashboard-health__title { - margin: 0 0 12px; - font-size: 16px; - font-weight: 700; - color: var(--text-primary, #1e3c38); -} - -.dashboard-health__all-clear { - margin: 0 0 8px; - font-size: 14px; - color: var(--success-color, #10b981); - font-weight: 600; -} - -.dashboard-health__list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px 16px; -} - -.dashboard-health__item { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - color: var(--text-primary, #1e3c38); -} - -.dashboard-health__item--warning { - color: var(--warning-color, #f59e0b); - font-weight: 600; -} - -.dashboard-health__dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--success-color, #10b981); - flex: none; -} - -.dashboard-health__dot--warning { - background: var(--warning-color, #f59e0b); -} - -@media (max-width: 640px) { - .dashboard-health__list { grid-template-columns: 1fr; } -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.ts b/src/app/features/admin/dashboard/components/admin-dashboard-health.component.ts deleted file mode 100644 index d2a01c9..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-health.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslatePipe } from '../../../../i18n/translate.pipe'; -import { AdminDashboardHealthCheck } from '../models/admin-dashboard.model'; - -@Component({ - selector: 'app-admin-dashboard-health', - standalone: true, - imports: [CommonModule, TranslatePipe], - templateUrl: './admin-dashboard-health.component.html', - styleUrls: ['./admin-dashboard-health.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AdminDashboardHealthComponent { - @Input() checks: AdminDashboardHealthCheck[] = []; - - get hasIssues(): boolean { - return this.checks.some(check => !check.healthy); - } -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.html b/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.html deleted file mode 100644 index 81eb093..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
-

{{ 'dashboard.quickActionsTitle' | translate }}

- -
diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.scss b/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.scss deleted file mode 100644 index acb9f27..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.scss +++ /dev/null @@ -1,59 +0,0 @@ -.dashboard-quick-actions__title { - margin: 0 0 12px; - font-size: 16px; - font-weight: 700; - color: var(--text-primary, #1e3c38); -} - -.dashboard-quick-actions__grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; -} - -.dashboard-quick-actions__item { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - min-height: 56px; - padding: 10px 12px; - border: 1px solid var(--border-color, #d3dad9); - border-radius: var(--radius-md, 12px); - background: var(--bg-primary, #fff); - color: var(--text-primary, #1e3c38); - font-weight: 600; - font-size: 14px; - text-decoration: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; -} - -.dashboard-quick-actions__item:hover, -.dashboard-quick-actions__item:focus-visible { - border-color: var(--primary-color, #497671); - box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.1)); - transform: translateY(-2px); -} - -.dashboard-quick-actions__item:active { - transform: translateY(0); -} - -@media (prefers-reduced-motion: reduce) { - .dashboard-quick-actions__item { - transition: none; - } - - .dashboard-quick-actions__item:hover, - .dashboard-quick-actions__item:focus-visible { - transform: none; - } -} - -@media (max-width: 960px) { - .dashboard-quick-actions__grid { grid-template-columns: repeat(2, 1fr); } -} - -@media (max-width: 560px) { - .dashboard-quick-actions__grid { grid-template-columns: 1fr; } -} diff --git a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.ts b/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.ts deleted file mode 100644 index 534e97a..0000000 --- a/src/app/features/admin/dashboard/components/admin-dashboard-quick-actions.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; -import { TranslatePipe } from '../../../../i18n/translate.pipe'; -import { AdminDashboardQuickAction } from '../models/admin-dashboard.model'; - -@Component({ - selector: 'app-admin-dashboard-quick-actions', - standalone: true, - imports: [CommonModule, RouterLink, TranslatePipe], - templateUrl: './admin-dashboard-quick-actions.component.html', - styleUrls: ['./admin-dashboard-quick-actions.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AdminDashboardQuickActionsComponent { - @Input() actions: AdminDashboardQuickAction[] = []; -} diff --git a/src/app/features/admin/dashboard/components/dashboard-card.component.html b/src/app/features/admin/dashboard/components/dashboard-card.component.html new file mode 100644 index 0000000..0f83958 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-card.component.html @@ -0,0 +1,16 @@ + + @if (titleKey(); as title) { +
+ @if (icon(); as iconClass) { + + } +

{{ title | translate }}

+
+ +
+
+ } +
+ +
+
diff --git a/src/app/features/admin/dashboard/components/dashboard-card.component.scss b/src/app/features/admin/dashboard/components/dashboard-card.component.scss new file mode 100644 index 0000000..4c242d3 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-card.component.scss @@ -0,0 +1,45 @@ +:host { + display: block; + min-width: 0; +} + +.dashboard-card { + height: 100%; +} + +.dashboard-card__header { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-bottom: var(--space-md); +} + +.dashboard-card__icon { + color: var(--primary-color); + font-size: 1.125rem; +} + +.dashboard-card__title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + flex: 1 1 auto; +} + +.dashboard-card__header-actions { + display: flex; + align-items: center; + gap: var(--space-xs); +} + +.dashboard-card__header-actions:empty { + display: none; +} + +.dashboard-card__body { + display: flex; + flex-direction: column; + gap: var(--space-sm); + min-width: 0; +} diff --git a/src/app/features/admin/dashboard/components/dashboard-card.component.ts b/src/app/features/admin/dashboard/components/dashboard-card.component.ts new file mode 100644 index 0000000..4428c1e --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-card.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { CardComponent } from '../../../../shared/ui/card/card.component'; + +@Component({ + selector: 'app-dashboard-card', + standalone: true, + imports: [TranslatePipe, CardComponent], + templateUrl: './dashboard-card.component.html', + styleUrl: './dashboard-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[style.grid-column]': 'span()', + }, +}) +export class DashboardCardComponent { + readonly titleKey = input(null); + readonly icon = input(null); + /** 12-column grid span at desktop width, e.g. 'span 12' or 'span 4'. */ + readonly span = input('span 4'); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-metric.component.html b/src/app/features/admin/dashboard/components/dashboard-metric.component.html new file mode 100644 index 0000000..c57b6b7 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-metric.component.html @@ -0,0 +1,17 @@ +
+ {{ labelKey() | translate }} + @if (loading()) { + + } @else { + + {{ value() ?? '—' }} + @if (badgeLabelKey(); as badgeKey) { + {{ badgeKey | translate }} + } + + } +
diff --git a/src/app/features/admin/dashboard/components/dashboard-metric.component.scss b/src/app/features/admin/dashboard/components/dashboard-metric.component.scss new file mode 100644 index 0000000..fb7adba --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-metric.component.scss @@ -0,0 +1,46 @@ +.dashboard-metric { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.dashboard-metric__label { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + color: var(--text-light); +} + +.dashboard-metric__value { + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-metric__badge { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + padding: 2px 8px; + border-radius: var(--radius-sm); + white-space: nowrap; +} + +.dashboard-metric__value--badge-production .dashboard-metric__badge { + background: color-mix(in srgb, var(--success-color) 12%, transparent); + color: var(--success-color); +} + +.dashboard-metric__value--badge-development .dashboard-metric__badge { + background: color-mix(in srgb, var(--warning-color) 14%, transparent); + color: var(--warning-color); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-metric.component.ts b/src/app/features/admin/dashboard/components/dashboard-metric.component.ts new file mode 100644 index 0000000..f056eef --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-metric.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; + +@Component({ + selector: 'app-dashboard-metric', + standalone: true, + imports: [TranslatePipe, SkeletonComponent], + templateUrl: './dashboard-metric.component.html', + styleUrl: './dashboard-metric.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardMetricComponent { + readonly labelKey = input.required(); + /** null/undefined renders an em dash placeholder - the metric is never left blank. */ + readonly value = input(null); + readonly loading = input(false); + readonly badge = input<'production' | 'development' | null>(null); + readonly badgeLabelKey = input(null); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-section.component.html b/src/app/features/admin/dashboard/components/dashboard-section.component.html new file mode 100644 index 0000000..92ed532 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-section.component.html @@ -0,0 +1,14 @@ +
+
+

{{ titleKey() | translate }}

+ @if (descriptionKey(); as description) { +

{{ description | translate }}

+ } +
+ +
+
+
+ +
+
diff --git a/src/app/features/admin/dashboard/components/dashboard-section.component.scss b/src/app/features/admin/dashboard/components/dashboard-section.component.scss new file mode 100644 index 0000000..43b6368 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-section.component.scss @@ -0,0 +1,42 @@ +:host { + display: block; + min-width: 0; +} + +.dashboard-section__header { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: var(--space-sm); + margin-bottom: var(--space-md); +} + +.dashboard-section__title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + flex: 1 1 auto; +} + +.dashboard-section__description { + flex-basis: 100%; + margin: 0; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.dashboard-section__actions { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-left: auto; +} + +.dashboard-section__actions:empty { + display: none; +} + +.dashboard-section__body { + min-width: 0; +} diff --git a/src/app/features/admin/dashboard/components/dashboard-section.component.ts b/src/app/features/admin/dashboard/components/dashboard-section.component.ts new file mode 100644 index 0000000..512cf47 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-section.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; + +@Component({ + selector: 'app-dashboard-section', + standalone: true, + imports: [TranslatePipe], + templateUrl: './dashboard-section.component.html', + styleUrl: './dashboard-section.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[style.grid-column]': 'span()', + }, +}) +export class DashboardSectionComponent { + readonly titleKey = input.required(); + readonly descriptionKey = input(null); + /** 12-column grid span at desktop width, e.g. 'span 12' or 'span 4'. */ + readonly span = input('span 12'); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.html b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.html new file mode 100644 index 0000000..2bba8ba --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.html @@ -0,0 +1,18 @@ +@if (comingSoon()) { + +} @else { + + + {{ labelKey() | translate }} + @if (descriptionKey(); as description) { + {{ description | translate }} + } + +} diff --git a/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.scss b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.scss new file mode 100644 index 0000000..1e3b97a --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.scss @@ -0,0 +1,90 @@ +:host { + display: block; + height: 100%; +} + +.dashboard-shortcut-card { + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-xs); + width: 100%; + height: 100%; + padding: var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-primary); + box-shadow: var(--shadow-sm); + color: inherit; + text-decoration: none; + text-align: left; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; +} + +.dashboard-shortcut-card:hover { + border-color: var(--primary-color); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.dashboard-shortcut-card:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +.dashboard-shortcut-card--disabled { + cursor: not-allowed; + color: var(--text-light); + opacity: 0.7; +} + +.dashboard-shortcut-card--disabled:hover { + transform: none; + border-color: var(--border-color); + box-shadow: var(--shadow-sm); +} + +.dashboard-shortcut-card__icon { + font-size: 1.125rem; + color: var(--primary-color); +} + +.dashboard-shortcut-card--disabled .dashboard-shortcut-card__icon { + color: var(--text-light); +} + +.dashboard-shortcut-card__title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); +} + +.dashboard-shortcut-card--disabled .dashboard-shortcut-card__title { + color: var(--text-light); +} + +.dashboard-shortcut-card__description { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.dashboard-shortcut-card__badge { + position: absolute; + top: var(--space-sm); + right: var(--space-sm); + font-size: 0.7rem; + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--primary-color) 8%, transparent); + color: var(--primary-color); + border: 1px solid color-mix(in srgb, var(--primary-color) 20%, transparent); +} + +@media (prefers-reduced-motion: reduce) { + .dashboard-shortcut-card { + transition: none; + } +} diff --git a/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.ts b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.ts new file mode 100644 index 0000000..22cd00d --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-shortcut-card.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; + +@Component({ + selector: 'app-dashboard-shortcut-card', + standalone: true, + imports: [RouterLink, TranslatePipe], + templateUrl: './dashboard-shortcut-card.component.html', + styleUrl: './dashboard-shortcut-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardShortcutCardComponent { + readonly icon = input.required(); + readonly labelKey = input.required(); + readonly descriptionKey = input(null); + readonly route = input(null); + readonly comingSoon = input(false); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-status-row.component.html b/src/app/features/admin/dashboard/components/dashboard-status-row.component.html new file mode 100644 index 0000000..8b0f6ec --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-status-row.component.html @@ -0,0 +1,15 @@ +@if (status() === 'loading') { +
+ + +
+} @else { +
+ + {{ labelKey() | translate }} + @if (displayValue() !== null) { + {{ displayValue() }} + } + {{ statusTextKey() | translate }} +
+} diff --git a/src/app/features/admin/dashboard/components/dashboard-status-row.component.scss b/src/app/features/admin/dashboard/components/dashboard-status-row.component.scss new file mode 100644 index 0000000..7fc4738 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-status-row.component.scss @@ -0,0 +1,48 @@ +.dashboard-status-row { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-xs) 0; + font-size: 1rem; +} + +.dashboard-status-row__icon { + font-size: 1rem; + flex-shrink: 0; +} + +.dashboard-status-row__label { + flex: 1 1 auto; + color: var(--text-primary); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-status-row__value { + font-weight: 600; + color: var(--text-primary); +} + +.dashboard-status-row__status-text { + font-size: 0.75rem; + color: var(--text-light); + white-space: nowrap; +} + +.dashboard-status-row--healthy .dashboard-status-row__icon { + color: var(--success-color); +} + +.dashboard-status-row--attention .dashboard-status-row__icon { + color: var(--warning-color); +} + +.dashboard-status-row--unhealthy .dashboard-status-row__icon { + color: var(--error-color); +} + +.dashboard-status-row--unknown .dashboard-status-row__icon { + color: var(--text-light); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-status-row.component.ts b/src/app/features/admin/dashboard/components/dashboard-status-row.component.ts new file mode 100644 index 0000000..73d66e9 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-status-row.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; +import { AdminDashboardHealthStatus } from '../models/admin-dashboard.model'; + +const STATUS_ICON: Record = { + healthy: 'pi-check-circle', + attention: 'pi-exclamation-triangle', + unhealthy: 'pi-times-circle', + unknown: 'pi-question-circle', + loading: 'pi-spin pi-spinner', +}; + +const STATUS_TEXT_KEY: Record = { + healthy: 'dashboard.statusHealthy', + attention: 'dashboard.statusAttention', + unhealthy: 'dashboard.statusUnhealthy', + unknown: 'dashboard.statusUnknown', + loading: 'dashboard.stateLoading', +}; + +@Component({ + selector: 'app-dashboard-status-row', + standalone: true, + imports: [TranslatePipe, SkeletonComponent], + templateUrl: './dashboard-status-row.component.html', + styleUrl: './dashboard-status-row.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'dashboard-status-row-host' }, +}) +export class DashboardStatusRowComponent { + readonly labelKey = input.required(); + readonly status = input.required(); + readonly displayValue = input(null); + + readonly icon = computed(() => STATUS_ICON[this.status()]); + readonly statusTextKey = computed(() => STATUS_TEXT_KEY[this.status()]); +} diff --git a/src/app/features/admin/dashboard/components/dashboard-timeline.component.html b/src/app/features/admin/dashboard/components/dashboard-timeline.component.html new file mode 100644 index 0000000..a39f20a --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-timeline.component.html @@ -0,0 +1,42 @@ +@switch (status()) { + @case ('loading') { + + } + @case ('unavailable') { + + } + @case ('error') { + + } + @default { + @if (entries().length === 0) { + + } @else { +
    + @for (entry of entries(); track entry.id) { +
  1. + + {{ entry.labelKey | translate }} + +
  2. + } +
+ } + } +} diff --git a/src/app/features/admin/dashboard/components/dashboard-timeline.component.scss b/src/app/features/admin/dashboard/components/dashboard-timeline.component.scss new file mode 100644 index 0000000..396f200 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-timeline.component.scss @@ -0,0 +1,46 @@ +.dashboard-timeline { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.dashboard-timeline__skeleton-row { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.dashboard-timeline__item { + display: flex; + align-items: center; + gap: var(--space-sm); + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border-color); +} + +.dashboard-timeline__item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.dashboard-timeline__icon { + color: var(--primary-color); + font-size: 1rem; + flex-shrink: 0; +} + +.dashboard-timeline__label { + flex: 1 1 auto; + color: var(--text-primary); + font-size: 1rem; + min-width: 0; +} + +.dashboard-timeline__time { + font-size: 0.75rem; + color: var(--text-light); + white-space: nowrap; +} diff --git a/src/app/features/admin/dashboard/components/dashboard-timeline.component.ts b/src/app/features/admin/dashboard/components/dashboard-timeline.component.ts new file mode 100644 index 0000000..cf323d1 --- /dev/null +++ b/src/app/features/admin/dashboard/components/dashboard-timeline.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; +import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component'; + +export interface DashboardTimelineEntry { + id: string; + labelKey: string; + timeText: string; + icon: string; +} + +export type DashboardTimelineStatus = 'loading' | 'ready' | 'unavailable' | 'error'; + +@Component({ + selector: 'app-dashboard-timeline', + standalone: true, + imports: [TranslatePipe, SkeletonComponent, EmptyStateComponent], + templateUrl: './dashboard-timeline.component.html', + styleUrl: './dashboard-timeline.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardTimelineComponent { + readonly status = input('ready'); + readonly entries = input([]); +} diff --git a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts index 36bb33f..af2ae70 100644 --- a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts +++ b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts @@ -1,39 +1,53 @@ import { Injectable, computed, effect, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade'; +import { EditorSchemaService } from '../../../project-editor/schema/editor-schema.service'; +import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service'; +import { environment } from '../../../../../environments/environment'; import { ADMIN_DASHBOARD_METRICS_GATEWAY } from '../services/admin-dashboard-metrics-gateway.token'; import { AdminDashboardHistoryService } from '../services/admin-dashboard-history.service'; import { AdminDashboardCardState, AdminDashboardHealthCheck, + AdminDashboardHomeHealthCheck, AdminDashboardMetrics, AdminDashboardQuickAction, + AdminDashboardShortcut, } from '../models/admin-dashboard.model'; const QUICK_ACTIONS: AdminDashboardQuickAction[] = [ - { id: 'edit-project', labelKey: 'dashboard.actionEditProject', route: ['edit', 'general'] }, - { id: 'categories', labelKey: 'dashboard.actionCategories', route: ['backoffice', 'categories'] }, - { id: 'products', labelKey: 'dashboard.actionProducts', route: ['backoffice', 'products'] }, - { id: 'static-pages', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] }, - { id: 'transactions', labelKey: 'dashboard.actionTransactions', route: ['backoffice', 'transactions'] }, - { id: 'orders', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] }, - { id: 'media-library', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] }, - { id: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] }, - { id: 'monitoring', labelKey: 'dashboard.actionMonitoring', route: ['backoffice', 'monitoring'] }, - { id: 'analytics', labelKey: 'dashboard.actionAnalytics', route: ['backoffice', 'analytics'] }, - { id: 'preview-marketplace', labelKey: 'dashboard.actionPreviewMarketplace', route: [''] }, + { id: 'add-product', icon: 'pi-plus-circle', labelKey: 'dashboard.actionAddProduct', descriptionKey: 'dashboard.actionAddProductDescription', route: ['backoffice', 'products', 'create'] }, + { id: 'create-category', icon: 'pi-sitemap', labelKey: 'dashboard.actionCreateCategory', descriptionKey: 'dashboard.actionCreateCategoryDescription', route: ['backoffice', 'categories', 'create'] }, + { id: 'edit-project', icon: 'pi-th-large', labelKey: 'dashboard.actionEditProject', descriptionKey: 'dashboard.actionEditProjectDescription', route: ['edit', 'general'] }, + { id: 'edit-homepage', icon: 'pi-home', labelKey: 'dashboard.actionEditHomepage', descriptionKey: 'dashboard.actionEditHomepageDescription', route: ['edit', 'homepage'] }, + { id: 'media-library', icon: 'pi-images', labelKey: 'dashboard.actionMediaLibrary', descriptionKey: 'dashboard.actionMediaLibraryDescription', route: ['backoffice', 'media'] }, + { id: 'orders', icon: 'pi-shopping-cart', labelKey: 'dashboard.actionOrders', descriptionKey: 'dashboard.actionOrdersDescription', route: ['backoffice', 'orders'] }, +]; + +const SHORTCUTS: AdminDashboardShortcut[] = [ + { id: 'products', icon: 'pi-box', labelKey: 'dashboard.actionProducts', route: ['backoffice', 'products'] }, + { id: 'categories', icon: 'pi-tags', labelKey: 'dashboard.actionCategories', route: ['backoffice', 'categories'] }, + { id: 'marketplace-builder', icon: 'pi-sitemap', labelKey: 'dashboard.shortcutMarketplaceBuilder', route: ['edit', 'general'] }, + { id: 'static-pages', icon: 'pi-file-edit', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] }, + { id: 'orders', icon: 'pi-shopping-cart', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] }, + { id: 'users', icon: 'pi-users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] }, + { id: 'settings', icon: 'pi-cog', labelKey: 'dashboard.shortcutSettings', route: [], comingSoon: true }, + { id: 'media-library', icon: 'pi-images', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] }, + { id: 'content', icon: 'pi-align-left', labelKey: 'dashboard.shortcutContent', route: ['edit', 'static-pages'] }, ]; /** - * Composes ProjectEditorFacade (bootstrap/status/save-publish timestamps/validation) - * with dashboard-only metrics/history so the page component stays presentational. - * Cards read through here, never directly from ConfigService or localStorage - - * swapping local sources for real backend endpoints only touches this facade - * and the gateways it calls, per ADR-006/007. + * Composes ProjectEditorFacade (bootstrap/status/save-publish timestamps/validation/dirty + * state) with dashboard-only metrics/history/auth so the page component stays presentational. + * Cards read through here, never directly from ConfigService or localStorage - swapping local + * sources for real backend endpoints only touches this facade and the gateways it calls, per + * ADR-006/007. */ @Injectable({ providedIn: 'root' }) export class AdminDashboardFacade { private readonly projectEditor = inject(ProjectEditorFacade); + private readonly schema = inject(EditorSchemaService); + private readonly adminAuth = inject(AdminAuthService); private readonly metricsGateway = inject(ADMIN_DASHBOARD_METRICS_GATEWAY); private readonly history = inject(AdminDashboardHistoryService); @@ -42,22 +56,53 @@ export class AdminDashboardFacade { readonly bootstrap = this.projectEditor.bootstrap; readonly status = this.projectEditor.status; + readonly dirty = this.projectEditor.dirty; readonly lastSavedAt = this.projectEditor.lastSavedAt; readonly lastPublishedAt = this.projectEditor.lastPublishedAt; readonly validationIssues = this.projectEditor.validationIssues; readonly homepageWidgets = this.projectEditor.homepageWidgets; + readonly modifiedFields = this.projectEditor.modifiedFields; readonly metrics = this.metricsState.asReadonly(); + private static readonly MAX_MODIFIED_FIELD_CHIPS = 6; + + /** Human field labels (schema labelKey) for the fields the draft actually changed, capped so the widget stays scannable. */ + readonly modifiedFieldLabelKeys = computed(() => { + const keys = [...this.modifiedFields()]; + const fields = this.schema.all(); + return keys + .slice(0, AdminDashboardFacade.MAX_MODIFIED_FIELD_CHIPS) + .map(key => fields.find(field => field.key === key)?.labelKey ?? key); + }); + + readonly modifiedFieldsOverflowCount = computed(() => + Math.max(0, this.modifiedFields().size - AdminDashboardFacade.MAX_MODIFIED_FIELD_CHIPS), + ); + + readonly currentUserName = this.adminAuth.displayName; + readonly isProductionEnvironment = environment.production; + readonly quickActions: AdminDashboardQuickAction[] = QUICK_ACTIONS; + readonly shortcuts: AdminDashboardShortcut[] = SHORTCUTS; readonly enabledWidgetsCount = computed(() => this.homepageWidgets().length); + readonly staticPagesUnpublishedCount = computed(() => { + const staticPages = this.bootstrap()?.staticPages; + if (!staticPages) { + return 0; + } + const pages = Array.isArray(staticPages) ? staticPages : Object.values(staticPages); + return pages.filter(page => (page as { status?: string }).status !== 'published').length; + }); + readonly activityEntries = computed(() => { this.activityTick(); const tenantId = this.bootstrap()?.tenant.id; return tenantId ? this.history.list(tenantId) : []; }); + /** Unchanged shape/logic - AdminMonitoringPageComponent also consumes this signal. */ readonly healthChecks = computed(() => { const current = this.bootstrap(); const issues = new Set(this.validationIssues().map(issue => issue.code)); @@ -71,6 +116,26 @@ export class AdminDashboardFacade { ]; }); + readonly homeHealthChecks = computed(() => { + const current = this.bootstrap(); + const bootstrapLoading = !current; + const issues = new Set(this.validationIssues().map(issue => issue.code)); + const metrics = this.metrics(); + const staticPagesUnpublished = this.staticPagesUnpublishedCount(); + + return [ + { code: 'configuration-valid', labelKey: 'dashboard.healthConfigurationValid', status: bootstrapLoading ? 'loading' : issues.size === 0 ? 'healthy' : 'unhealthy' }, + { code: 'products-count', labelKey: 'dashboard.healthProductsCount', status: metrics.status === 'loading' ? 'loading' : metrics.status === 'error' ? 'unhealthy' : 'healthy', displayValue: metrics.value ? String(metrics.value.productsCount) : null }, + { code: 'categories-count', labelKey: 'dashboard.healthCategoriesCount', status: metrics.status === 'loading' ? 'loading' : metrics.status === 'error' ? 'unhealthy' : 'healthy', displayValue: metrics.value ? String(metrics.value.categoriesCount) : null }, + { code: 'missing-translations', labelKey: 'dashboard.healthMissingTranslations', status: bootstrapLoading ? 'loading' : issues.has('missing-translations') ? 'unhealthy' : 'healthy' }, + { code: 'draft-exists', labelKey: 'dashboard.healthDraftExists', status: bootstrapLoading ? 'loading' : this.dirty() ? 'attention' : 'healthy' }, + { code: 'images-without-alt', labelKey: 'dashboard.healthImagesWithoutAlt', status: 'unknown' }, + { code: 'static-pages-unpublished', labelKey: 'dashboard.healthStaticPagesUnpublished', status: bootstrapLoading ? 'loading' : staticPagesUnpublished > 0 ? 'attention' : 'healthy', displayValue: bootstrapLoading ? null : String(staticPagesUnpublished) }, + { code: 'homepage-configured', labelKey: 'dashboard.healthHomepageConfigured', status: bootstrapLoading ? 'loading' : this.enabledWidgetsCount() > 0 ? 'healthy' : 'attention' }, + { code: 'theme-configured', labelKey: 'dashboard.healthThemeConfigured', status: bootstrapLoading ? 'loading' : current?.theme?.themeId ? 'healthy' : 'attention' }, + ]; + }); + private lastRecordedSavedAt: number | null = null; private lastRecordedPublishedAt: number | null = null; private historyPrimed = false; @@ -125,4 +190,12 @@ export class AdminDashboardFacade { error: () => this.metricsState.set({ status: 'error', value: null }), }); } + + publishDraft(): void { + this.projectEditor.publish(); + } + + discardDraft(): void { + this.projectEditor.resetDraft(); + } } diff --git a/src/app/features/admin/dashboard/models/admin-dashboard.model.ts b/src/app/features/admin/dashboard/models/admin-dashboard.model.ts index 54c29a5..3323cc5 100644 --- a/src/app/features/admin/dashboard/models/admin-dashboard.model.ts +++ b/src/app/features/admin/dashboard/models/admin-dashboard.model.ts @@ -21,11 +21,16 @@ export type AdminDashboardQuickActionId = | 'users' | 'monitoring' | 'analytics' - | 'preview-marketplace'; + | 'preview-marketplace' + | 'add-product' + | 'create-category' + | 'edit-homepage'; export interface AdminDashboardQuickAction { id: AdminDashboardQuickActionId; + icon: string; labelKey: string; + descriptionKey: string; route: string[]; external?: boolean; } @@ -38,8 +43,38 @@ export interface AdminDashboardActivityEntry { timestamp: number; } +/** Kept exactly as-is: AdminMonitoringPageComponent also reads AdminDashboardFacade.healthChecks() and depends on this boolean shape. */ export interface AdminDashboardHealthCheck { code: string; labelKey: string; healthy: boolean; } + +/** + * healthy: green check. attention: amber, informational (e.g. a draft exists) - not a defect. + * unhealthy: red, a real validation problem. unknown: this sprint has no data source for the + * check yet (never fabricated) - shown honestly rather than guessed. + */ +export type AdminDashboardHealthStatus = 'healthy' | 'attention' | 'unhealthy' | 'unknown' | 'loading'; + +/** Richer status rows for the redesigned dashboard homepage (Sprint 2) - separate from AdminDashboardHealthCheck above so Monitoring's existing boolean-badge usage stays untouched. */ +export interface AdminDashboardHomeHealthCheck { + code: string; + labelKey: string; + status: AdminDashboardHealthStatus; + /** Set for metric-style rows (e.g. product count) rendered as a number instead of a pass/fail statement. */ + displayValue?: string | null; +} + +export interface AdminDashboardDraftField { + key: string; + labelKey: string; +} + +export interface AdminDashboardShortcut { + id: string; + icon: string; + labelKey: string; + route: string[]; + comingSoon?: boolean; +} diff --git a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.html b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.html index 0f7c750..91c0889 100644 --- a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.html +++ b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.html @@ -1,23 +1,117 @@
-
-

{{ 'dashboard.title' | translate }}

-

{{ 'dashboard.subtitle' | translate }}

-
+
-
- -
+ +
+
+

{{ 'dashboard.welcomeEyebrow' | translate }}

+

+ {{ (tenantName() ?? ('dashboard.welcomeFallbackTenant' | translate)) }} +

+ + {{ (facade.isProductionEnvironment ? 'dashboard.envProduction' : 'dashboard.envDevelopment') | translate }} + +
- +
+ + + + +
+
+ + + +
+ @for (action of quickActions(); track action.labelKey) { + + } +
+
+ + + + @if (facade.dirty()) { +

{{ 'dashboard.draftStatusHasChanges' | translate }}

+ @if (facade.modifiedFieldLabelKeys().length > 0) { +
    + @for (labelKey of facade.modifiedFieldLabelKeys(); track labelKey) { +
  • {{ labelKey | translate }}
  • + } + @if (facade.modifiedFieldsOverflowCount() > 0) { +
  • + {{ 'dashboard.draftStatusMoreFields' | translate: { count: facade.modifiedFieldsOverflowCount() } }} +
  • + } +
+ } +

{{ 'dashboard.cardLastDraftSave' | translate }}: {{ lastSavedText() ?? ('dashboard.never' | translate) }}

+
+ + {{ 'dashboard.draftOpenBuilder' | translate }} + + {{ 'dashboard.draftPublish' | translate }} + {{ 'dashboard.draftDiscard' | translate }} +
+ } @else { +

+ + {{ 'dashboard.draftStatusAllPublished' | translate }} +

+ } +
+ + + + @for (check of facade.homeHealthChecks(); track check.code) { + + } + + + + + + + + + +
+ @for (shortcut of shortcuts(); track shortcut.labelKey) { + + } +
+
+ + + +
    + @for (link of documentationLinks; track link.id) { +
  • + +
  • + } +
+
-
- -
diff --git a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.scss b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.scss index f448564..8695ade 100644 --- a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.scss +++ b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.scss @@ -1,64 +1,222 @@ .dashboard-page { - display: flex; - flex-direction: column; - gap: 24px; - padding: 20px; - max-width: 1280px; - margin: 0 auto; + display: block; } -.dashboard-page__header { +.dashboard-page__grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: var(--space-lg); + align-items: stretch; +} + +/* ---------- Welcome ---------- */ + +.dashboard-welcome { + grid-column: span 12; + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-lg); + padding: var(--space-lg); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--gradient-primary); + color: #fff; +} + +.dashboard-welcome__intro { display: flex; flex-direction: column; gap: 4px; + min-width: 0; } -.dashboard-page__title { +.dashboard-welcome__eyebrow { margin: 0; - font-size: 26px; - font-weight: 800; - color: var(--text-primary, #1e3c38); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.75); } -.dashboard-page__subtitle { +.dashboard-welcome__title { margin: 0; - font-size: 14px; - color: var(--text-secondary, #667a77); + font-size: clamp(1.5rem, 3vw, 2rem); + font-weight: 700; + line-height: 1.25; + display: flex; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; } -.dashboard-page__cards { +.dashboard-welcome__env-badge { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + padding: 2px 10px; + border-radius: var(--radius-sm); + align-self: center; +} + +.dashboard-welcome__env-badge--production { + background: color-mix(in srgb, #fff 25%, transparent); + color: #fff; +} + +.dashboard-welcome__env-badge--development { + background: color-mix(in srgb, #fff 90%, transparent); + color: var(--warning-color); +} + +.dashboard-welcome__facts { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 16px; + grid-template-columns: repeat(4, minmax(120px, 1fr)); + gap: var(--space-lg); } -.dashboard-page__section { - display: block; - padding: 18px; - border: 1px solid var(--border-color, #d3dad9); - border-radius: var(--radius-md, 12px); - background: var(--bg-primary, #fff); +.dashboard-welcome__facts app-dashboard-metric { + --text-primary: #fff; + --text-light: rgba(255, 255, 255, 0.75); } -.dashboard-page__row { +/* ---------- Shortcut / quick-action grids ---------- */ + +.dashboard-shortcut-grid { display: grid; - grid-template-columns: 1.4fr 1fr; - gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: var(--space-md); } -@media (max-width: 1100px) { - .dashboard-page__cards { grid-template-columns: repeat(3, 1fr); } +.dashboard-shortcut-grid--compact { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } -@media (max-width: 900px) { - .dashboard-page__row { grid-template-columns: 1fr; } +/* ---------- Draft status ---------- */ + +.dashboard-draft__lead { + margin: 0; + font-size: 1rem; + color: var(--text-primary); } -@media (max-width: 720px) { - .dashboard-page__cards { grid-template-columns: repeat(2, 1fr); } +.dashboard-draft__all-clear { + display: flex; + align-items: center; + gap: var(--space-xs); + margin: 0; + font-size: 1rem; + color: var(--success-color); + font-weight: 600; } -@media (max-width: 480px) { - .dashboard-page { padding: 12px; gap: 16px; } - .dashboard-page__cards { grid-template-columns: 1fr; } +.dashboard-draft__fields { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); +} + +.dashboard-draft__field-chip { + font-size: 0.75rem; + padding: 2px 8px; + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--primary-color) 8%, transparent); + color: var(--primary-color); +} + +.dashboard-draft__field-chip--muted { + background: var(--bg-tertiary); + color: var(--text-light); +} + +.dashboard-draft__saved-at { + margin: 0; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.dashboard-draft__actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-top: var(--space-xs); +} + +/* ---------- Documentation ---------- */ + +.dashboard-doc-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.dashboard-doc-list__item { + display: flex; + align-items: center; + gap: var(--space-sm); + width: 100%; + padding: var(--space-xs) 0; + border: none; + background: none; + font-size: 1rem; + color: var(--text-light); + text-align: left; + cursor: not-allowed; + + .pi { + color: var(--text-light); + } +} + +.dashboard-doc-list__badge { + margin-left: auto; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-lg); + background: var(--bg-tertiary); + color: var(--text-light); +} + +/* ---------- Responsive ---------- */ + +/* Tablet: the three-across health/draft/activity/doc row becomes 2 columns instead of 3. */ +@media (max-width: 1023px) and (min-width: 640px) { + .dashboard-page__grid > app-dashboard-card { + grid-column: span 6 !important; + } + + .dashboard-welcome__facts { + grid-template-columns: repeat(2, minmax(120px, 1fr)); + } +} + +/* Mobile: everything is a single column, nothing scrolls sideways. */ +@media (max-width: 639px) { + .dashboard-page__grid > * { + grid-column: span 12 !important; + } + + .dashboard-welcome { + flex-direction: column; + align-items: flex-start; + } + + .dashboard-welcome__facts { + grid-template-columns: 1fr; + } + + .dashboard-shortcut-grid, + .dashboard-shortcut-grid--compact { + grid-template-columns: 1fr; + } } diff --git a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.ts b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.ts index b941293..1b920cd 100644 --- a/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.ts +++ b/src/app/features/admin/dashboard/pages/admin-dashboard-page.component.ts @@ -1,33 +1,56 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslateService } from '../../../../i18n/translate.service'; +import { RouterLink } from '@angular/router'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { TranslateService } from '../../../../i18n/translate.service'; import { LanguageService } from '../../../../services/language.service'; import { AdminDashboardFacade } from '../facade/admin-dashboard.facade'; -import { AdminDashboardCardComponent } from '../components/admin-dashboard-card.component'; -import { AdminDashboardQuickActionsComponent } from '../components/admin-dashboard-quick-actions.component'; -import { AdminDashboardActivityComponent, AdminDashboardActivityViewEntry } from '../components/admin-dashboard-activity.component'; -import { AdminDashboardHealthComponent } from '../components/admin-dashboard-health.component'; -import { AdminDashboardCardStatus, AdminDashboardQuickAction } from '../models/admin-dashboard.model'; +import { DashboardSectionComponent } from '../components/dashboard-section.component'; +import { DashboardCardComponent } from '../components/dashboard-card.component'; +import { DashboardMetricComponent } from '../components/dashboard-metric.component'; +import { DashboardStatusRowComponent } from '../components/dashboard-status-row.component'; +import { DashboardShortcutCardComponent } from '../components/dashboard-shortcut-card.component'; +import { DashboardTimelineComponent, DashboardTimelineEntry, DashboardTimelineStatus } from '../components/dashboard-timeline.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; +import { AdminDashboardQuickAction, AdminDashboardShortcut } from '../models/admin-dashboard.model'; -interface DashboardCardViewModel { - id: string; - titleKey: string; - status: AdminDashboardCardStatus; - value: string | null; - subtitle: string | null; +interface LinkedAction { + icon: string; + labelKey: string; + descriptionKey?: string; + route: string[]; + comingSoon: boolean; } +const ACTIVITY_ICON: Record = { + 'draft-saved': 'pi-save', + published: 'pi-upload', +}; + +const ACTIVITY_LABEL_KEY: Record = { + 'draft-saved': 'dashboard.activityDraftSaved', + published: 'dashboard.activityPublished', +}; + +const DOCUMENTATION_LINKS = [ + { id: 'documentation', icon: 'pi-book', labelKey: 'dashboard.docDocumentation' }, + { id: 'shortcuts', icon: 'pi-bolt', labelKey: 'dashboard.docKeyboardShortcuts' }, + { id: 'report-issue', icon: 'pi-flag', labelKey: 'dashboard.docReportIssue' }, + { id: 'release-notes', icon: 'pi-megaphone', labelKey: 'dashboard.docReleaseNotes' }, +]; + @Component({ selector: 'app-admin-dashboard-page', standalone: true, imports: [ - CommonModule, + RouterLink, TranslatePipe, - AdminDashboardCardComponent, - AdminDashboardQuickActionsComponent, - AdminDashboardActivityComponent, - AdminDashboardHealthComponent, + DashboardSectionComponent, + DashboardCardComponent, + DashboardMetricComponent, + DashboardStatusRowComponent, + DashboardShortcutCardComponent, + DashboardTimelineComponent, + ButtonComponent, ], templateUrl: './admin-dashboard-page.component.html', styleUrls: ['./admin-dashboard-page.component.scss'], @@ -38,134 +61,60 @@ export class AdminDashboardPageComponent { private readonly translate = inject(TranslateService); private readonly languageService = inject(LanguageService); - readonly quickActions = computed(() => { - const lang = this.languageService.currentLanguage(); - return this.facade.quickActions.map(action => ({ - ...action, - route: ['/', lang, ...action.route.filter(segment => segment !== '')], - })); - }); + readonly documentationLinks = DOCUMENTATION_LINKS; - readonly activityEntries = computed(() => - this.facade.activityEntries().map(entry => ({ - id: entry.id, - labelKey: entry.type === 'published' ? 'dashboard.activityPublished' : 'dashboard.activityDraftSaved', - timeText: new Date(entry.timestamp).toLocaleString(), - })), + private toLinkedAction(action: AdminDashboardQuickAction | AdminDashboardShortcut): LinkedAction { + const lang = this.languageService.currentLanguage(); + const comingSoon = 'comingSoon' in action ? !!action.comingSoon : action.route.length === 0; + return { + icon: action.icon, + labelKey: action.labelKey, + descriptionKey: 'descriptionKey' in action ? action.descriptionKey : undefined, + route: comingSoon ? [] : ['/', lang, ...action.route], + comingSoon, + }; + } + + readonly quickActions = computed(() => + this.facade.quickActions.map(action => this.toLinkedAction(action)), ); - readonly healthChecks = this.facade.healthChecks; + readonly shortcuts = computed(() => + this.facade.shortcuts.map(action => this.toLinkedAction(action)), + ); - readonly cards = computed(() => { - const bootstrap = this.facade.bootstrap(); - const bootstrapLoading: AdminDashboardCardStatus = bootstrap ? 'ready' : 'loading'; - const metrics = this.facade.metrics(); - const locales = bootstrap?.localization.supportedLocales ?? []; + readonly previewRoute = computed(() => ['/', this.languageService.currentLanguage()]); - return [ - { - id: 'marketplace-status', - titleKey: 'dashboard.cardMarketplaceStatus', - status: bootstrapLoading, - value: bootstrap ? this.translate.t(this.facade.status() === 'published' ? 'builder.statusPublished' : 'builder.statusDraft') : null, - subtitle: null, - }, - { - id: 'project-name', - titleKey: 'dashboard.cardProjectName', - status: bootstrapLoading, - value: bootstrap?.tenant.name ?? null, - subtitle: null, - }, - { - id: 'current-theme', - titleKey: 'dashboard.cardCurrentTheme', - status: bootstrapLoading, - value: bootstrap?.theme.themeId ?? null, - subtitle: null, - }, - { - id: 'languages', - titleKey: 'dashboard.cardLanguages', - status: bootstrapLoading === 'loading' ? 'loading' : (locales.length === 0 ? 'empty' : 'ready'), - value: locales.length ? locales.join(', ').toUpperCase() : null, - subtitle: null, - }, - { - id: 'categories-count', - titleKey: 'dashboard.cardCategoriesCount', - status: metrics.status, - value: metrics.value ? String(metrics.value.categoriesCount) : null, - subtitle: null, - }, - { - id: 'products-count', - titleKey: 'dashboard.cardProductsCount', - status: metrics.status, - value: metrics.value ? String(metrics.value.productsCount) : null, - subtitle: null, - }, - { - id: 'orders', - titleKey: 'dashboard.cardOrders', - status: 'pending-backend', - value: null, - subtitle: null, - }, - { - id: 'revenue', - titleKey: 'dashboard.cardRevenue', - status: 'pending-backend', - value: null, - subtitle: null, - }, - { - id: 'last-publish', - titleKey: 'dashboard.cardLastPublish', - status: bootstrapLoading, - value: bootstrap ? this.formatTimestamp(this.facade.lastPublishedAt()) : null, - subtitle: null, - }, - { - id: 'last-draft-save', - titleKey: 'dashboard.cardLastDraftSave', - status: bootstrapLoading, - value: bootstrap ? this.formatTimestamp(this.facade.lastSavedAt()) : null, - subtitle: null, - }, - { - id: 'bootstrap-version', - titleKey: 'dashboard.cardBootstrapVersion', - status: bootstrapLoading, - value: bootstrap?.schemaVersion ?? null, - subtitle: null, - }, - { - id: 'active-layout', - titleKey: 'dashboard.cardActiveLayout', - status: bootstrapLoading, - value: bootstrap ? (bootstrap.layout?.type ?? 'default') : null, - subtitle: null, - }, - { - id: 'enabled-widgets', - titleKey: 'dashboard.cardEnabledWidgets', - status: bootstrapLoading, - value: bootstrap ? String(this.facade.enabledWidgetsCount()) : null, - subtitle: null, - }, - ]; - }); + readonly tenantName = computed(() => this.facade.bootstrap()?.tenant.name ?? null); + readonly bootstrapLoading = computed(() => !this.facade.bootstrap()); + + readonly lastPublishedText = computed(() => this.formatTimestamp(this.facade.lastPublishedAt())); + readonly lastSavedText = computed(() => this.formatTimestamp(this.facade.lastSavedAt())); + + readonly activityStatus = computed(() => (this.bootstrapLoading() ? 'loading' : 'ready')); + + readonly activityTimelineEntries = computed(() => + this.facade.activityEntries().map(entry => ({ + id: entry.id, + labelKey: ACTIVITY_LABEL_KEY[entry.type] ?? entry.type, + timeText: new Date(entry.timestamp).toLocaleString(this.languageService.currentLanguage()), + icon: ACTIVITY_ICON[entry.type] ?? 'pi-circle', + })), + ); constructor() { this.facade.ensureLoaded(); } - cardTitle(titleKey: string): string { - return this.translate.t(titleKey); + private formatTimestamp(timestamp: number | null): string | null { + return timestamp ? new Date(timestamp).toLocaleString(this.languageService.currentLanguage()) : null; } - private formatTimestamp(timestamp: number | null): string { - return timestamp ? new Date(timestamp).toLocaleString() : this.translate.t('dashboard.never'); + publishDraft(): void { + this.facade.publishDraft(); + } + + discardDraft(): void { + this.facade.discardDraft(); } } diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index dbc4701..3e3e42f 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -720,7 +720,7 @@ export const en: Translations = { actionAnalytics: 'Analytics', actionPreviewMarketplace: 'Preview Marketplace', activityTitle: 'Recent Activity', - activityEmpty: 'No activity yet. Save or publish a change to see it here.', + activityEmpty: 'No activity yet', activityDraftSaved: 'Draft saved', activityPublished: 'Published', healthTitle: 'System Health', @@ -734,6 +734,54 @@ export const en: Translations = { comingSoonTitle: 'Coming soon', comingSoonDescription: 'This section is not built yet. Check back in a future update.', backToDashboard: 'Back to dashboard', + welcomeEyebrow: 'Welcome back', + welcomeFallbackTenant: 'Your marketplace', + envProduction: 'Production', + envDevelopment: 'Development', + factCurrentUser: 'Current user', + factTenant: 'Store', + quickActionsDescription: 'Your most common tasks - start here', + actionAddProduct: 'Add product', + actionAddProductDescription: 'Create a new product listing', + actionCreateCategory: 'Create category', + actionCreateCategoryDescription: 'Add a category to the catalog', + actionEditProjectDescription: 'Open the marketplace builder', + actionEditHomepage: 'Edit homepage', + actionEditHomepageDescription: 'Configure the homepage widgets', + actionMediaLibraryDescription: 'Upload and organize files', + actionOrdersDescription: 'Review recent orders', + draftStatusTitle: 'Draft status', + draftStatusHasChanges: 'There are unpublished changes waiting to go live.', + draftStatusAllPublished: 'Everything published. No draft in progress.', + draftStatusMoreFields: '+{{count}} more', + draftOpenBuilder: 'Open builder', + draftPublish: 'Publish', + draftDiscard: 'Discard draft', + healthProductsCount: 'Products', + healthCategoriesCount: 'Categories', + healthDraftExists: 'Draft', + healthImagesWithoutAlt: 'Images missing alt text', + healthStaticPagesUnpublished: 'Unpublished static pages', + healthHomepageConfigured: 'Homepage configured', + healthThemeConfigured: 'Theme configured', + statusHealthy: 'Healthy', + statusAttention: 'Needs attention', + statusUnhealthy: 'Issue', + statusUnknown: 'Not tracked yet', + activityEmptyHint: 'Save or publish a change to see it appear here.', + activityUnavailable: 'Activity history unavailable', + activityUnavailableHint: "Couldn't reach the activity data source. Try refreshing later.", + activityErrorHint: "Couldn't load recent activity.", + shortcutsTitle: 'Useful shortcuts', + shortcutsDescription: 'Frequently used destinations across the admin panel', + shortcutMarketplaceBuilder: 'Marketplace Builder', + shortcutSettings: 'Settings', + shortcutContent: 'Content', + docTitle: 'Documentation', + docDocumentation: 'Documentation', + docKeyboardShortcuts: 'Keyboard shortcuts', + docReportIssue: 'Report an issue', + docReleaseNotes: 'Release notes', }, staticPages: { notFound: 'Page not found', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index adf0fe0..5741517 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -720,7 +720,7 @@ export const hy: Translations = { actionAnalytics: 'Վերլուծություն', actionPreviewMarketplace: 'Դիտել մարքեթփլեյսը', activityTitle: 'Վերջին ակտիվություն', - activityEmpty: 'Դեռ ակտիվություն չկա։ Պահպանեք կամ հրապարակեք փոփոխություն՝ այն այստեղ տեսնելու համար։', + activityEmpty: 'Դեռ ակտիվություն չկա', activityDraftSaved: 'Սևագիրը պահպանվեց', activityPublished: 'Հրապարակվեց', healthTitle: 'Համակարգի վիճակ', @@ -734,6 +734,54 @@ export const hy: Translations = { comingSoonTitle: 'Շուտով', comingSoonDescription: 'Այս բաժինը դեռ կառուցված չէ։ Ստուգեք ավելի ուշ։', backToDashboard: 'Վերադառնալ վահանակ', + welcomeEyebrow: 'Բարի վերադարձ', + welcomeFallbackTenant: 'Ձեր մարկետփլեյսը', + envProduction: 'Պրոդակշն', + envDevelopment: 'Մշակում', + factCurrentUser: 'Ընթացիկ օգտատեր', + factTenant: 'Խանութ', + quickActionsDescription: 'Ձեր ամենահաճախակի առաջադրանքները՝ սկսեք այստեղից', + actionAddProduct: 'Ավելացնել ապրանք', + actionAddProductDescription: 'Ստեղծել նոր ապրանքի քարտ', + actionCreateCategory: 'Ստեղծել կատեգորիա', + actionCreateCategoryDescription: 'Ավելացնել կատեգորիա կատալոգում', + actionEditProjectDescription: 'Բացել մարկետփլեյսի կոնստրուկտորը', + actionEditHomepage: 'Խմբագրել գլխավոր էջը', + actionEditHomepageDescription: 'Կարգավորել գլխավոր էջի վիջեթները', + actionMediaLibraryDescription: 'Վերբեռնեք և կազմակերպեք ֆայլերը', + actionOrdersDescription: 'Դիտեք վերջին պատվերները', + draftStatusTitle: 'Սևագրի կարգավիճակ', + draftStatusHasChanges: 'Կան չհրապարակված փոփոխություններ։', + draftStatusAllPublished: 'Ամեն ինչ հրապարակված է։ Սևագիր չկա։', + draftStatusMoreFields: '+{{count}} ևս', + draftOpenBuilder: 'Բացել կոնստրուկտորը', + draftPublish: 'Հրապարակել', + draftDiscard: 'Չեղարկել սևագիրը', + healthProductsCount: 'Ապրանքներ', + healthCategoriesCount: 'Կատեգորիաներ', + healthDraftExists: 'Սևագիր', + healthImagesWithoutAlt: 'Alt տեքստ չունեցող պատկերներ', + healthStaticPagesUnpublished: 'Չհրապարակված ստատիկ էջեր', + healthHomepageConfigured: 'Գլխավոր էջը կարգավորված է', + healthThemeConfigured: 'Թեման կարգավորված է', + statusHealthy: 'Կարգին է', + statusAttention: 'Ուշադրություն է պահանջում', + statusUnhealthy: 'Խնդիր', + statusUnknown: 'Դեռ չի հետևվում', + activityEmptyHint: 'Պահպանեք կամ հրապարակեք փոփոխություն՝ այն այստեղ տեսնելու համար։', + activityUnavailable: 'Ակտիվության պատմությունը հասանելի չէ', + activityUnavailableHint: 'Չհաջողվեց միանալ տվյալների աղբյուրին։ Փորձեք թարմացնել ավելի ուշ։', + activityErrorHint: 'Չհաջողվեց բեռնել վերջին ակտիվությունը։', + shortcutsTitle: 'Օգտակար հղումներ', + shortcutsDescription: 'Ադմին վահանակի հաճախակի օգտագործվող բաժինները', + shortcutMarketplaceBuilder: 'Մարկետփլեյսի կոնստրուկտոր', + shortcutSettings: 'Կարգավորումներ', + shortcutContent: 'Բովանդակություն', + docTitle: 'Փաստաթղթեր', + docDocumentation: 'Փաստաթղթեր', + docKeyboardShortcuts: 'Ստեղնաշարի դյուրանցումներ', + docReportIssue: 'Հայտնել խնդրի մասին', + docReleaseNotes: 'Թողարկման նշումներ', }, staticPages: { notFound: 'Էջը չի գտնվել', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 31239c2..8fe0b14 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -720,7 +720,7 @@ export const ru: Translations = { actionAnalytics: 'Аналитика', actionPreviewMarketplace: 'Просмотр маркетплейса', activityTitle: 'Недавняя активность', - activityEmpty: 'Пока нет активности. Сохраните или опубликуйте изменение, чтобы увидеть его здесь.', + activityEmpty: 'Пока нет активности', activityDraftSaved: 'Черновик сохранён', activityPublished: 'Опубликовано', healthTitle: 'Состояние системы', @@ -734,6 +734,54 @@ export const ru: Translations = { comingSoonTitle: 'Скоро', comingSoonDescription: 'Этот раздел ещё не реализован. Загляните позже.', backToDashboard: 'Вернуться на панель управления', + welcomeEyebrow: 'С возвращением', + welcomeFallbackTenant: 'Ваш маркетплейс', + envProduction: 'Продакшн', + envDevelopment: 'Разработка', + factCurrentUser: 'Текущий пользователь', + factTenant: 'Магазин', + quickActionsDescription: 'Самые частые задачи — начните здесь', + actionAddProduct: 'Добавить товар', + actionAddProductDescription: 'Создать новую карточку товара', + actionCreateCategory: 'Создать категорию', + actionCreateCategoryDescription: 'Добавить категорию в каталог', + actionEditProjectDescription: 'Открыть конструктор маркетплейса', + actionEditHomepage: 'Редактировать главную', + actionEditHomepageDescription: 'Настроить виджеты главной страницы', + actionMediaLibraryDescription: 'Загрузить и организовать файлы', + actionOrdersDescription: 'Просмотреть последние заказы', + draftStatusTitle: 'Статус черновика', + draftStatusHasChanges: 'Есть несохранённые изменения, ожидающие публикации.', + draftStatusAllPublished: 'Всё опубликовано. Черновика нет.', + draftStatusMoreFields: 'ещё {{count}}', + draftOpenBuilder: 'Открыть конструктор', + draftPublish: 'Опубликовать', + draftDiscard: 'Отменить черновик', + healthProductsCount: 'Товары', + healthCategoriesCount: 'Категории', + healthDraftExists: 'Черновик', + healthImagesWithoutAlt: 'Изображения без alt-текста', + healthStaticPagesUnpublished: 'Неопубликованные статические страницы', + healthHomepageConfigured: 'Главная страница настроена', + healthThemeConfigured: 'Тема настроена', + statusHealthy: 'В порядке', + statusAttention: 'Требует внимания', + statusUnhealthy: 'Проблема', + statusUnknown: 'Пока не отслеживается', + activityEmptyHint: 'Сохраните или опубликуйте изменение, чтобы увидеть его здесь.', + activityUnavailable: 'История активности недоступна', + activityUnavailableHint: 'Не удалось подключиться к источнику данных. Попробуйте обновить страницу позже.', + activityErrorHint: 'Не удалось загрузить недавнюю активность.', + shortcutsTitle: 'Полезные ссылки', + shortcutsDescription: 'Часто используемые разделы админ-панели', + shortcutMarketplaceBuilder: 'Конструктор маркетплейса', + shortcutSettings: 'Настройки', + shortcutContent: 'Контент', + docTitle: 'Документация', + docDocumentation: 'Документация', + docKeyboardShortcuts: 'Горячие клавиши', + docReportIssue: 'Сообщить о проблеме', + docReleaseNotes: 'Список изменений', }, staticPages: { notFound: 'Страница не найдена', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index c851308..c074ec8 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -732,6 +732,54 @@ export interface Translations { comingSoonTitle: string; comingSoonDescription: string; backToDashboard: string; + welcomeEyebrow: string; + welcomeFallbackTenant: string; + envProduction: string; + envDevelopment: string; + factCurrentUser: string; + factTenant: string; + quickActionsDescription: string; + actionAddProduct: string; + actionAddProductDescription: string; + actionCreateCategory: string; + actionCreateCategoryDescription: string; + actionEditProjectDescription: string; + actionEditHomepage: string; + actionEditHomepageDescription: string; + actionMediaLibraryDescription: string; + actionOrdersDescription: string; + draftStatusTitle: string; + draftStatusHasChanges: string; + draftStatusAllPublished: string; + draftStatusMoreFields: string; + draftOpenBuilder: string; + draftPublish: string; + draftDiscard: string; + healthProductsCount: string; + healthCategoriesCount: string; + healthDraftExists: string; + healthImagesWithoutAlt: string; + healthStaticPagesUnpublished: string; + healthHomepageConfigured: string; + healthThemeConfigured: string; + statusHealthy: string; + statusAttention: string; + statusUnhealthy: string; + statusUnknown: string; + activityEmptyHint: string; + activityUnavailable: string; + activityUnavailableHint: string; + activityErrorHint: string; + shortcutsTitle: string; + shortcutsDescription: string; + shortcutMarketplaceBuilder: string; + shortcutSettings: string; + shortcutContent: string; + docTitle: string; + docDocumentation: string; + docKeyboardShortcuts: string; + docReportIssue: string; + docReleaseNotes: string; }; staticPages: { notFound: string;