perf(app): lazy-load i18n translation packs, drop dead items-carousel component
RC PERF-01 bundle audit follow-up on 61a5714.
- i18n: ru/en/hy translation packs (346 KB raw combined) were all
statically imported in TranslateService and shipped in the initial
bundle regardless of the visitor's language. Now only 'ru' (platform
default) is bundled eagerly; 'en'/'hy' are dynamic import()s. The
language route guard (languageGuard) awaits preloadLanguage() before
activating the route, so translations are always fully loaded before
any component renders - no flash of untranslated/fallback content.
- widget-host.service.ts: import UnknownWidgetComponent directly instead
of via the widgets/ui barrel (index.ts re-exports 6 widgets).
- Deleted src/app/components/items-carousel/* - confirmed dead (zero
references anywhere, verified via knip and grep), the only consumer
of primeng/primeicons in the app. Removed the now-unused
`@import 'primeicons/primeicons.css'` from styles.scss (no primeicons
CSS classes used elsewhere). primeng/primeicons remain listed in
package.json/package-lock.json - npm CLI in this environment is
blocked by an unrelated, pre-existing broken `barry-cache` devDependency
(ETARGET on `npm install`/`npm uninstall`), so the lockfile could not be
safely regenerated. Flagged, not fixed.
Routes audit (app.routes.ts): all storefront/builder/backoffice feature
routes already use loadComponent/loadChildren; nothing eagerly imported.
No route changes needed.
Lucide icons (icon-registry.ts): already named/tree-shakeable imports
from @lucide/angular, not a full-library import. No change needed.
Before/after (npm run build, production):
- Initial bundle raw: 1.47 MB -> 1.12 MB (-350 KB / -24%)
- Initial bundle transfer (est.): 263.59 kB -> 221.51 kB (-42 kB / -16%)
- Budget overage: 769.22 kB over -> 416.84 kB over (still exceeds the
700 KB budget; project-editor-page-component (320 kB),
catalog-container-component (126 kB), product-details-container
(88 kB), cart-component (61 kB) lazy chunks unchanged - no safe
mechanical split identified within scope, see PERF-01 report for
detail).
Verified: npx tsc --noEmit clean, npm run build green (warning only,
no errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,64 +0,0 @@
|
||||
<div class="carousel-container">
|
||||
@if (loading()) {
|
||||
<div class="carousel-loading">
|
||||
<div class="spinner"></div>
|
||||
<p>{{ 'carousel.loading' | translate }}</p>
|
||||
</div>
|
||||
} @else if (products().length > 0) {
|
||||
<p-carousel
|
||||
[value]="products()"
|
||||
[numVisible]="6"
|
||||
[numScroll]="1"
|
||||
[circular]="true"
|
||||
[responsiveOptions]="responsiveOptions"
|
||||
[autoplayInterval]="3000"
|
||||
[showNavigators]="true"
|
||||
[showIndicators]="true">
|
||||
<ng-template let-product pTemplate="item">
|
||||
<div class="item-card">
|
||||
<a [routerLink]="['/product', product.itemID] | langRoute" class="item-link">
|
||||
<div class="item-image">
|
||||
<img [src]="getItemImage(product)" [alt]="itemName(product)" loading="lazy" />
|
||||
@if (product.discount > 0) {
|
||||
<span class="discount-badge">-{{ product.discount }}%</span>
|
||||
}
|
||||
@if (product.badges && product.badges.length > 0) {
|
||||
<div class="item-badges-overlay">
|
||||
@for (badge of product.badges; track badge) {
|
||||
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-details">
|
||||
<h3 class="item-name">{{ itemName(product) }}</h3>
|
||||
|
||||
@if (product.rating) {
|
||||
<div class="item-rating">
|
||||
<app-icon name="star" [size]="14" />
|
||||
<span class="rating-value">{{ product.rating }}</span>
|
||||
<span class="rating-count">({{ product.callbacks?.length || 0 }})</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="item-price-row">
|
||||
<div class="item-price">
|
||||
@if (product.discount > 0) {
|
||||
<span class="original-price">{{ product.price }} {{ product.currency }}</span>
|
||||
<span class="discounted-price">{{ getDiscountedPrice(product) | number:'1.0-0' }} {{ product.currency }}</span>
|
||||
} @else {
|
||||
<span class="current-price">{{ product.price }} {{ product.currency }}</span>
|
||||
}
|
||||
</div>
|
||||
<button class="cart-icon-btn" (click)="addToCart($event, product)" [attr.aria-label]="'carousel.addToCart' | translate">
|
||||
<app-icon name="cart" [size]="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-carousel>
|
||||
}
|
||||
</div>
|
||||
@@ -1,375 +0,0 @@
|
||||
.carousel-container {
|
||||
width: 100%;
|
||||
padding: 2rem 0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
::ng-deep {
|
||||
// PrimeNG carousel wrapper
|
||||
.p-carousel {
|
||||
.p-carousel-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
.p-carousel-prev,
|
||||
.p-carousel-next {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 2px solid #d3dad9;
|
||||
color: #1e3c38;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
align-self: center;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #d3dad9;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: var(--primary-color, #497671);
|
||||
border-color: var(--primary-color, #497671);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Items container
|
||||
.p-carousel-items-container {
|
||||
.p-carousel-items-content {
|
||||
width: 100%;
|
||||
|
||||
.p-carousel-item {
|
||||
padding: 0 0.5rem !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add gap between items
|
||||
.p-carousel-items-content .p-carousel-item-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.p-carousel-item {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Pagination dots
|
||||
.p-carousel-indicator-list {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
gap: 0.5rem !important;
|
||||
padding: 1.5rem 0 !important;
|
||||
margin: 0 !important;
|
||||
list-style: none !important;
|
||||
|
||||
.p-carousel-indicator {
|
||||
display: inline-block !important;
|
||||
|
||||
.p-carousel-indicator-button {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
border-radius: 50% !important;
|
||||
background-color: #d3dad9 !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.3s ease !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #a1b4b5 !important;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.p-carousel-indicator-active .p-carousel-indicator-button {
|
||||
background-color: var(--primary-color, #497671) !important;
|
||||
width: 32px !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-loading,
|
||||
.carousel-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #697777;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto 1rem;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid var(--primary-color, #497671);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Item card styles
|
||||
.item-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
border: 1px solid #d3dad9;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100% - 1rem) !important;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
.item-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
overflow: hidden;
|
||||
background: #f5f3f9;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding: 12px;
|
||||
transition: transform 0.4s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.item-card:hover & img {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
letter-spacing: 0.02em;
|
||||
z-index: 2;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 12px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: var(--font-size-sm, 0.8125rem);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
margin: 0;
|
||||
line-height: 1.35;
|
||||
min-height: 2.2em;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: #1e3c38;
|
||||
transition: color 0.2s;
|
||||
|
||||
.item-card:hover & {
|
||||
color: #497671;
|
||||
}
|
||||
}
|
||||
|
||||
.item-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rating-value {
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
color: #1e3c38;
|
||||
}
|
||||
|
||||
.rating-count {
|
||||
color: #697777;
|
||||
}
|
||||
}
|
||||
|
||||
.item-price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
|
||||
.current-price,
|
||||
.discounted-price {
|
||||
font-size: var(--font-size-md, 0.9375rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: #1e3c38;
|
||||
}
|
||||
|
||||
.discounted-price {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
color: #a1b4b5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: #497671;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #3d635f;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// alt theme styles
|
||||
.alt-theme {
|
||||
::ng-deep {
|
||||
.p-carousel {
|
||||
.p-carousel-prev,
|
||||
.p-carousel-next {
|
||||
&:not(:disabled):hover {
|
||||
background: var(--primary-color, #497671);
|
||||
border-color: var(--primary-color, #497671);
|
||||
}
|
||||
}
|
||||
|
||||
.p-carousel-indicators {
|
||||
.p-carousel-indicator.p-highlight button {
|
||||
background: var(--primary-color, #497671);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-card {
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.cart-icon-btn {
|
||||
background: var(--primary-color, #497671);
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover, #3d635f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive styles
|
||||
@media (max-width: 968px) {
|
||||
.carousel-container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.item-image {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: var(--font-size-sm, 0.8125rem);
|
||||
}
|
||||
|
||||
.item-price {
|
||||
.current-price,
|
||||
.discounted-price {
|
||||
font-size: var(--font-size-lg, 1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Component, OnInit, signal, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CarouselModule } from 'primeng/carousel';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { CartService } from '../../services';
|
||||
import { Item } from '../../models';
|
||||
import { getDiscountedPrice, getMainImage, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { ProductFacade } from '../../facades/platform/product.facade';
|
||||
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-items-carousel',
|
||||
templateUrl: './items-carousel.component.html',
|
||||
imports: [DecimalPipe, RouterLink, CarouselModule, ButtonModule, TagModule, LangRoutePipe, TranslatePipe, IconComponent],
|
||||
styleUrls: ['./items-carousel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ItemsCarouselComponent implements OnInit {
|
||||
products = signal<Item[]>([]);
|
||||
loading = signal(true);
|
||||
private readonly productFacade = inject(ProductFacade);
|
||||
|
||||
responsiveOptions: { breakpoint: string; numVisible: number; numScroll: number }[] | undefined;
|
||||
|
||||
constructor(
|
||||
private cartService: CartService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.productFacade.getFeaturedProducts({ count: 10 }).subscribe({
|
||||
next: (result) => {
|
||||
this.products.set(result.items);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.responsiveOptions = [
|
||||
{
|
||||
breakpoint: '1400px',
|
||||
numVisible: 5,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '1199px',
|
||||
numVisible: 4,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '991px',
|
||||
numVisible: 3,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '767px',
|
||||
numVisible: 2,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '575px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getSeverity(remainings: string): 'success' | 'info' | 'warn' | 'danger' | 'secondary' | 'contrast' {
|
||||
switch (remainings) {
|
||||
case 'high':
|
||||
return 'success';
|
||||
case 'low':
|
||||
return 'warn';
|
||||
case 'out':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
|
||||
getInventoryStatus(remainings: string): string {
|
||||
switch (remainings) {
|
||||
case 'high':
|
||||
return 'INSTOCK';
|
||||
case 'low':
|
||||
return 'LOWSTOCK';
|
||||
case 'out':
|
||||
return 'OUTOFSTOCK';
|
||||
default:
|
||||
return 'INSTOCK';
|
||||
}
|
||||
}
|
||||
|
||||
readonly getItemImage = getMainImage;
|
||||
readonly getDiscountedPrice = getDiscountedPrice;
|
||||
readonly getBadgeClass = getBadgeClass;
|
||||
|
||||
private langService = inject(LanguageService);
|
||||
itemName(product: Item): string { return getTranslatedField(product, 'name', this.langService.currentLanguage()); }
|
||||
|
||||
addToCart(event: Event, item: Item): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cartService.addItem(item.itemID, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user