fix(icons): migrate admin dashboard to Lucide

Replace every PrimeIcons pi-* class (static and data-driven) in the
admin dashboard area with app-icon: dashboard-card, dashboard-shortcut-
card, dashboard-status-row, dashboard-timeline, and the icon data in
admin-dashboard.facade.ts / admin-dashboard-page.component.ts. Icon
fields on AdminDashboardQuickAction/Shortcut/DashboardTimelineEntry
are now typed AppIconName instead of string, so a typo or unmapped
icon name is a compile error instead of a silently blank icon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-20 02:13:41 +04:00
parent f3c8a8cc96
commit 9569b6dc46
13 changed files with 70 additions and 54 deletions

View File

@@ -1,8 +1,8 @@
<app-card class="dashboard-card" padding="lg">
@if (titleKey(); as title) {
<header class="dashboard-card__header">
@if (icon(); as iconClass) {
<span class="pi {{ iconClass }} dashboard-card__icon" aria-hidden="true"></span>
@if (icon(); as iconName) {
<app-icon [name]="iconName" [size]="18" class="dashboard-card__icon" />
}
<h3 class="dashboard-card__title">{{ title | translate }}</h3>
<div class="dashboard-card__header-actions">

View File

@@ -1,11 +1,13 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { CardComponent } from '../../../../shared/ui/card/card.component';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
@Component({
selector: 'app-dashboard-card',
standalone: true,
imports: [TranslatePipe, CardComponent],
imports: [TranslatePipe, CardComponent, IconComponent],
templateUrl: './dashboard-card.component.html',
styleUrl: './dashboard-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -15,7 +17,7 @@ import { CardComponent } from '../../../../shared/ui/card/card.component';
})
export class DashboardCardComponent {
readonly titleKey = input<string | null>(null);
readonly icon = input<string | null>(null);
readonly icon = input<AppIconName | null>(null);
/** 12-column grid span at desktop width, e.g. 'span 12' or 'span 4'. */
readonly span = input<string>('span 4');
}

View File

@@ -1,6 +1,6 @@
@if (comingSoon()) {
<button type="button" class="dashboard-shortcut-card dashboard-shortcut-card--disabled" disabled>
<span class="pi {{ icon() }} dashboard-shortcut-card__icon" aria-hidden="true"></span>
<app-icon [name]="icon()" [size]="22" class="dashboard-shortcut-card__icon" />
<span class="dashboard-shortcut-card__title">{{ labelKey() | translate }}</span>
@if (descriptionKey(); as description) {
<span class="dashboard-shortcut-card__description">{{ description | translate }}</span>
@@ -9,7 +9,7 @@
</button>
} @else {
<a class="dashboard-shortcut-card" [routerLink]="route()">
<span class="pi {{ icon() }} dashboard-shortcut-card__icon" aria-hidden="true"></span>
<app-icon [name]="icon()" [size]="22" class="dashboard-shortcut-card__icon" />
<span class="dashboard-shortcut-card__title">{{ labelKey() | translate }}</span>
@if (descriptionKey(); as description) {
<span class="dashboard-shortcut-card__description">{{ description | translate }}</span>

View File

@@ -1,17 +1,19 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
@Component({
selector: 'app-dashboard-shortcut-card',
standalone: true,
imports: [RouterLink, TranslatePipe],
imports: [RouterLink, TranslatePipe, IconComponent],
templateUrl: './dashboard-shortcut-card.component.html',
styleUrl: './dashboard-shortcut-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardShortcutCardComponent {
readonly icon = input.required<string>();
readonly icon = input.required<AppIconName>();
readonly labelKey = input.required<string>();
readonly descriptionKey = input<string | null>(null);
readonly route = input<string[] | null>(null);

View File

@@ -5,7 +5,7 @@
</div>
} @else {
<div class="dashboard-status-row dashboard-status-row--{{ status() }}">
<span class="pi {{ icon() }} dashboard-status-row__icon" aria-hidden="true"></span>
<app-icon [name]="icon()" [size]="16" class="dashboard-status-row__icon" />
<span class="dashboard-status-row__label">{{ labelKey() | translate }}</span>
@if (displayValue() !== null) {
<span class="dashboard-status-row__value">{{ displayValue() }}</span>

View File

@@ -1,14 +1,16 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
import { AdminDashboardHealthStatus } from '../models/admin-dashboard.model';
const STATUS_ICON: Record<AdminDashboardHealthStatus, string> = {
healthy: 'pi-check-circle',
attention: 'pi-exclamation-triangle',
unhealthy: 'pi-times-circle',
unknown: 'pi-question-circle',
loading: 'pi-spin pi-spinner',
const STATUS_ICON: Record<AdminDashboardHealthStatus, AppIconName> = {
healthy: 'checkCircle',
attention: 'warning',
unhealthy: 'xCircle',
unknown: 'help',
loading: 'spinner',
};
const STATUS_TEXT_KEY: Record<AdminDashboardHealthStatus, string> = {
@@ -22,7 +24,7 @@ const STATUS_TEXT_KEY: Record<AdminDashboardHealthStatus, string> = {
@Component({
selector: 'app-dashboard-status-row',
standalone: true,
imports: [TranslatePipe, SkeletonComponent],
imports: [TranslatePipe, SkeletonComponent, IconComponent],
templateUrl: './dashboard-status-row.component.html',
styleUrl: './dashboard-status-row.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -31,7 +31,7 @@
<ol class="dashboard-timeline">
@for (entry of entries(); track entry.id) {
<li class="dashboard-timeline__item">
<span class="pi {{ entry.icon }} dashboard-timeline__icon" aria-hidden="true"></span>
<app-icon [name]="entry.icon" [size]="18" class="dashboard-timeline__icon" />
<span class="dashboard-timeline__label">{{ entry.labelKey | translate }}</span>
<time class="dashboard-timeline__time">{{ entry.timeText }}</time>
</li>

View File

@@ -2,12 +2,14 @@ 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';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
export interface DashboardTimelineEntry {
id: string;
labelKey: string;
timeText: string;
icon: string;
icon: AppIconName;
}
export type DashboardTimelineStatus = 'loading' | 'ready' | 'unavailable' | 'error';
@@ -15,7 +17,7 @@ export type DashboardTimelineStatus = 'loading' | 'ready' | 'unavailable' | 'err
@Component({
selector: 'app-dashboard-timeline',
standalone: true,
imports: [TranslatePipe, SkeletonComponent, EmptyStateComponent],
imports: [TranslatePipe, SkeletonComponent, EmptyStateComponent, IconComponent],
templateUrl: './dashboard-timeline.component.html',
styleUrl: './dashboard-timeline.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -16,24 +16,24 @@ import {
} from '../models/admin-dashboard.model';
const QUICK_ACTIONS: AdminDashboardQuickAction[] = [
{ 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'] },
{ 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'] },
{ id: 'add-product', icon: 'plusCircle', labelKey: 'dashboard.actionAddProduct', descriptionKey: 'dashboard.actionAddProductDescription', route: ['backoffice', 'products', 'create'] },
{ id: 'create-category', icon: 'network', labelKey: 'dashboard.actionCreateCategory', descriptionKey: 'dashboard.actionCreateCategoryDescription', route: ['backoffice', 'categories', 'create'] },
{ id: 'edit-project', icon: 'layoutGrid', labelKey: 'dashboard.actionEditProject', descriptionKey: 'dashboard.actionEditProjectDescription', route: ['edit'] },
{ id: 'edit-homepage', icon: 'home', labelKey: 'dashboard.actionEditHomepage', descriptionKey: 'dashboard.actionEditHomepageDescription', route: ['edit', 'homepage'] },
{ id: 'media-library', icon: 'images', labelKey: 'dashboard.actionMediaLibrary', descriptionKey: 'dashboard.actionMediaLibraryDescription', route: ['backoffice', 'media'] },
{ id: 'orders', icon: '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'] },
{ 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'] },
{ id: 'products', icon: 'package', labelKey: 'dashboard.actionProducts', route: ['backoffice', 'products'] },
{ id: 'categories', icon: 'tags', labelKey: 'dashboard.actionCategories', route: ['backoffice', 'categories'] },
{ id: 'marketplace-builder', icon: 'network', labelKey: 'dashboard.shortcutMarketplaceBuilder', route: ['edit'] },
{ id: 'static-pages', icon: 'edit', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] },
{ id: 'orders', icon: 'cart', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] },
{ id: 'users', icon: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] },
{ id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: [], comingSoon: true },
{ id: 'media-library', icon: 'images', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] },
{ id: 'content', icon: 'alignLeft', labelKey: 'dashboard.shortcutContent', route: ['edit', 'static-pages'] },
];
/**

View File

@@ -1,3 +1,5 @@
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
export type AdminDashboardCardStatus = 'loading' | 'ready' | 'empty' | 'error' | 'pending-backend';
export interface AdminDashboardCardState<T> {
@@ -28,7 +30,7 @@ export type AdminDashboardQuickActionId =
export interface AdminDashboardQuickAction {
id: AdminDashboardQuickActionId;
icon: string;
icon: AppIconName;
labelKey: string;
descriptionKey: string;
route: string[];
@@ -73,7 +75,7 @@ export interface AdminDashboardDraftField {
export interface AdminDashboardShortcut {
id: string;
icon: string;
icon: AppIconName;
labelKey: string;
route: string[];
comingSoon?: boolean;

View File

@@ -41,7 +41,7 @@
</app-dashboard-section>
<!-- Draft Status -->
<app-dashboard-card [titleKey]="'dashboard.draftStatusTitle'" [icon]="'pi-file-edit'" [span]="'span 4'">
<app-dashboard-card [titleKey]="'dashboard.draftStatusTitle'" [icon]="'edit'" [span]="'span 4'">
@if (facade.dirty()) {
<p class="dashboard-draft__lead">{{ 'dashboard.draftStatusHasChanges' | translate }}</p>
@if (facade.modifiedFieldLabelKeys().length > 0) {
@@ -66,21 +66,21 @@
</div>
} @else {
<p class="dashboard-draft__all-clear">
<span class="pi pi-check-circle" aria-hidden="true"></span>
<app-icon name="checkCircle" [size]="18" />
{{ 'dashboard.draftStatusAllPublished' | translate }}
</p>
}
</app-dashboard-card>
<!-- Marketplace Health -->
<app-dashboard-card [titleKey]="'dashboard.healthTitle'" [icon]="'pi-heart'" [span]="'span 4'">
<app-dashboard-card [titleKey]="'dashboard.healthTitle'" [icon]="'heart'" [span]="'span 4'">
@for (check of facade.homeHealthChecks(); track check.code) {
<app-dashboard-status-row [labelKey]="check.labelKey" [status]="check.status" [displayValue]="check.displayValue ?? null" />
}
</app-dashboard-card>
<!-- Recent Activity -->
<app-dashboard-card [titleKey]="'dashboard.activityTitle'" [icon]="'pi-clock'" [span]="'span 4'">
<app-dashboard-card [titleKey]="'dashboard.activityTitle'" [icon]="'clock'" [span]="'span 4'">
<app-dashboard-timeline [status]="activityStatus()" [entries]="activityTimelineEntries()" />
</app-dashboard-card>
@@ -99,12 +99,12 @@
</app-dashboard-section>
<!-- Documentation -->
<app-dashboard-card [titleKey]="'dashboard.docTitle'" [icon]="'pi-question-circle'" [span]="'span 4'">
<app-dashboard-card [titleKey]="'dashboard.docTitle'" [icon]="'help'" [span]="'span 4'">
<ul class="dashboard-doc-list">
@for (link of documentationLinks; track link.id) {
<li>
<button type="button" class="dashboard-doc-list__item" disabled>
<span class="pi {{ link.icon }}" aria-hidden="true"></span>
<app-icon [name]="link.icon" [size]="18" />
<span>{{ link.labelKey | translate }}</span>
<span class="dashboard-doc-list__badge">{{ 'adminShell.nav.comingSoonBadge' | translate }}</span>
</button>

View File

@@ -11,19 +11,21 @@ import { DashboardStatusRowComponent } from '../components/dashboard-status-row.
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 { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AdminDashboardQuickAction, AdminDashboardShortcut } from '../models/admin-dashboard.model';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
interface LinkedAction {
icon: string;
icon: AppIconName;
labelKey: string;
descriptionKey?: string;
route: string[];
comingSoon: boolean;
}
const ACTIVITY_ICON: Record<string, string> = {
'draft-saved': 'pi-save',
published: 'pi-upload',
const ACTIVITY_ICON: Record<string, AppIconName> = {
'draft-saved': 'save',
published: 'upload',
};
const ACTIVITY_LABEL_KEY: Record<string, string> = {
@@ -31,11 +33,11 @@ const ACTIVITY_LABEL_KEY: Record<string, string> = {
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' },
const DOCUMENTATION_LINKS: Array<{ id: string; icon: AppIconName; labelKey: string }> = [
{ id: 'documentation', icon: 'book', labelKey: 'dashboard.docDocumentation' },
{ id: 'shortcuts', icon: 'zap', labelKey: 'dashboard.docKeyboardShortcuts' },
{ id: 'report-issue', icon: 'flag', labelKey: 'dashboard.docReportIssue' },
{ id: 'release-notes', icon: 'megaphone', labelKey: 'dashboard.docReleaseNotes' },
];
@Component({
@@ -51,6 +53,7 @@ const DOCUMENTATION_LINKS = [
DashboardShortcutCardComponent,
DashboardTimelineComponent,
ButtonComponent,
IconComponent,
],
templateUrl: './admin-dashboard-page.component.html',
styleUrls: ['./admin-dashboard-page.component.scss'],
@@ -98,7 +101,7 @@ export class AdminDashboardPageComponent {
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',
icon: ACTIVITY_ICON[entry.type] ?? 'circle',
})),
);