feat(admin): sprint 19 admin dashboard, routing, i18n
- Add admin dashboard feature (models/gateway/facade/components/page) - Wire admin/products routes and backoffice coming-soon placeholders - Add lastPublishedAt to ProjectEditorFacade/state - Add dashboard i18n keys (en/ru/hy) and docs/ADMIN.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<section class="dashboard-activity">
|
||||
<h2 class="dashboard-activity__title">{{ 'dashboard.activityTitle' | translate }}</h2>
|
||||
|
||||
<p class="dashboard-activity__empty" *ngIf="entries.length === 0">{{ 'dashboard.activityEmpty' | translate }}</p>
|
||||
|
||||
<ul class="dashboard-activity__list" *ngIf="entries.length > 0">
|
||||
<li class="dashboard-activity__item" *ngFor="let entry of entries">
|
||||
<span class="dashboard-activity__label">{{ entry.labelKey | translate }}</span>
|
||||
<span class="dashboard-activity__time">{{ entry.timeText }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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[] = [];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<article class="dashboard-card" [class.dashboard-card--error]="status === 'error'" [class.dashboard-card--pending]="status === 'pending-backend'">
|
||||
<header class="dashboard-card__header">
|
||||
<span class="dashboard-card__icon" *ngIf="icon" aria-hidden="true">{{ icon }}</span>
|
||||
<h3 class="dashboard-card__title">{{ title }}</h3>
|
||||
</header>
|
||||
|
||||
<div class="dashboard-card__body">
|
||||
<ng-container [ngSwitch]="status">
|
||||
<div class="dashboard-card__skeleton" *ngSwitchCase="'loading'"></div>
|
||||
|
||||
<p class="dashboard-card__muted" *ngSwitchCase="'empty'">{{ 'dashboard.stateEmpty' | translate }}</p>
|
||||
|
||||
<p class="dashboard-card__muted dashboard-card__muted--error" *ngSwitchCase="'error'">{{ 'dashboard.stateError' | translate }}</p>
|
||||
|
||||
<p class="dashboard-card__muted" *ngSwitchCase="'pending-backend'">{{ 'dashboard.statePendingBackend' | translate }}</p>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<p class="dashboard-card__value">{{ value }}</p>
|
||||
<p class="dashboard-card__subtitle" *ngIf="subtitle">{{ subtitle }}</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,83 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.dashboard-card__skeleton {
|
||||
height: 24px;
|
||||
width: 60%;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
background: linear-gradient(90deg, var(--bg-secondary, #f5f5f5) 25%, var(--bg-tertiary, #f0f0f0) 37%, var(--bg-secondary, #f5f5f5) 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: dashboard-card-shimmer 1.4s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes dashboard-card-shimmer {
|
||||
0% { background-position: 100% 50%; }
|
||||
100% { background-position: 0 50%; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-dashboard-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslatePipe],
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<section class="dashboard-health" [class.dashboard-health--warning]="hasIssues">
|
||||
<h2 class="dashboard-health__title">{{ 'dashboard.healthTitle' | translate }}</h2>
|
||||
|
||||
<p class="dashboard-health__all-clear" *ngIf="!hasIssues">{{ 'dashboard.healthAllClear' | translate }}</p>
|
||||
|
||||
<ul class="dashboard-health__list">
|
||||
<li class="dashboard-health__item" *ngFor="let check of checks" [class.dashboard-health__item--warning]="!check.healthy">
|
||||
<span class="dashboard-health__dot" [class.dashboard-health__dot--warning]="!check.healthy" aria-hidden="true"></span>
|
||||
<span>{{ check.labelKey | translate }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,51 @@
|
||||
.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; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<section class="dashboard-quick-actions">
|
||||
<h2 class="dashboard-quick-actions__title">{{ 'dashboard.quickActionsTitle' | translate }}</h2>
|
||||
<div class="dashboard-quick-actions__grid">
|
||||
<a
|
||||
*ngFor="let action of actions"
|
||||
class="dashboard-quick-actions__item"
|
||||
[routerLink]="action.route"
|
||||
>{{ action.labelKey | translate }}</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,43 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
@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; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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[] = [];
|
||||
}
|
||||
Reference in New Issue
Block a user