refactor: marketplace release polish

Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).

- a11y: aria-label added to every bare <select> not already inside a
  <label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
  previously rendered blank during the initial fetch (categories, orders,
  transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
  SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
  present at Sprint 20's first build before any admin/* code existed,
  and new admin pages are all lazy-loaded — documented as out of scope
  for this pass rather than chased

docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-16 00:14:46 +04:00
parent 1db63ac99d
commit 576f2600a5
27 changed files with 223 additions and 127 deletions

View File

@@ -8,7 +8,13 @@
<app-button variant="secondary" (click)="exportCsv()">{{ 'adminOrders.export' | translate }}</app-button>
</div>
@if (facade.summary(); as summary) {
@if (facade.loading()) {
<div class="summary-grid">
@for (i of [1,2,3,4,5,6]; track i) {
<app-skeleton shape="rect" height="64px" />
}
</div>
} @else if (facade.summary(); as summary) {
<div class="summary-grid">
<div class="summary-card"><span>{{ 'adminAnalytics.revenue' | translate }}</span><strong>{{ summary.revenueTotal }} {{ summary.currency }}</strong></div>
<div class="summary-card"><span>{{ 'adminAnalytics.orders' | translate }}</span><strong>{{ summary.ordersCount }}</strong></div>
@@ -30,24 +36,34 @@
<div class="card">
<h2>{{ 'adminAnalytics.topProducts' | translate }}</h2>
<app-table>
<thead>
<tr>
<th>{{ 'adminProducts.name' | translate }}</th>
<th>{{ 'adminAnalytics.quantitySold' | translate }}</th>
<th>{{ 'adminAnalytics.revenue' | translate }}</th>
</tr>
</thead>
<tbody>
@for (product of facade.topProducts(); track product.productId) {
<tr>
<td>{{ product.name }}</td>
<td>{{ product.quantity }}</td>
<td>{{ product.revenue }}</td>
</tr>
@if (facade.loading()) {
<div class="skeleton-rows">
@for (i of [1,2,3]; track i) {
<app-skeleton shape="rect" height="36px" />
}
</tbody>
</app-table>
</div>
} @else if (facade.topProducts().length === 0) {
<app-empty-state [title]="'adminAnalytics.topProductsEmptyTitle' | translate" [description]="'adminAnalytics.topProductsEmptyDescription' | translate" />
} @else {
<app-table>
<thead>
<tr>
<th>{{ 'adminProducts.name' | translate }}</th>
<th>{{ 'adminAnalytics.quantitySold' | translate }}</th>
<th>{{ 'adminAnalytics.revenue' | translate }}</th>
</tr>
</thead>
<tbody>
@for (product of facade.topProducts(); track product.productId) {
<tr>
<td>{{ product.name }}</td>
<td>{{ product.quantity }}</td>
<td>{{ product.revenue }}</td>
</tr>
}
</tbody>
</app-table>
}
</div>
<div class="card pending-section">

View File

@@ -11,3 +11,4 @@
.chart .bar { flex: 1; background: var(--color-primary, #2f8f5b); border-radius: 3px 3px 0 0; min-height: 2px; }
.pending-section p { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text-secondary, #6b7280); }
@media (max-width: 900px) { .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.skeleton-rows { display: grid; gap: 8px; }

View File

@@ -6,11 +6,13 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
import { TableComponent } from '../../../../shared/ui/table/table.component';
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component';
@Component({
selector: 'app-admin-analytics-page',
standalone: true,
imports: [CommonModule, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent],
imports: [CommonModule, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent, SkeletonComponent, EmptyStateComponent],
templateUrl: './admin-analytics-page.component.html',
styleUrls: ['./admin-analytics-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush