integration new apis
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
<div class="item-card">
|
||||
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
|
||||
<div class="item-image">
|
||||
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
|
||||
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" decoding="async" width="300" height="300" />
|
||||
@if (item.discount > 0) {
|
||||
<div class="discount-badge">-{{ item.discount }}%</div>
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<div class="item-details">
|
||||
<h3 class="item-name">{{ item.name }}</h3>
|
||||
<h3 class="item-name">{{ itemName(item) }}</h3>
|
||||
|
||||
<div class="item-rating">
|
||||
<span class="rating-stars">⭐ {{ item.rating }}</span>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ApiService, CartService } from '../../services';
|
||||
import { Item } from '../../models';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass } from '../../utils/item.utils';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId, 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';
|
||||
|
||||
@@ -23,7 +24,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
|
||||
hasMore = signal(true);
|
||||
|
||||
private skip = 0;
|
||||
private readonly count = 20;
|
||||
private readonly count = 50;
|
||||
private isLoadingMore = false;
|
||||
private routeSubscription?: Subscription;
|
||||
private scrollTimeout?: ReturnType<typeof setTimeout>;
|
||||
@@ -108,4 +109,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
|
||||
readonly getMainImage = getMainImage;
|
||||
readonly trackByItemId = trackByItemId;
|
||||
readonly getBadgeClass = getBadgeClass;
|
||||
|
||||
private langService = inject(LanguageService);
|
||||
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
|
||||
}
|
||||
|
||||
@@ -18,6 +18,30 @@
|
||||
<h2>{{ parentName() }}</h2>
|
||||
</header>
|
||||
|
||||
<!-- Nested subcategories from API (backOffice format with hasItems) -->
|
||||
@if (nestedSubcategories().length > 0) {
|
||||
<div class="categories-grid">
|
||||
@for (sub of nestedSubcategories(); track trackBySubId($index, sub)) {
|
||||
<a [routerLink]="['/category', sub.id] | langRoute" class="category-card">
|
||||
<div class="category-image">
|
||||
@if (sub.img) {
|
||||
<img [src]="sub.img" [alt]="sub.name" loading="lazy" decoding="async" />
|
||||
} @else {
|
||||
<div class="category-fallback">{{ sub.name.charAt(0) }}</div>
|
||||
}
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<h3 class="category-name">{{ sub.name }}</h3>
|
||||
@if (sub.itemCount) {
|
||||
<span class="category-count">{{ sub.itemCount }}</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Legacy flat subcategories -->
|
||||
@if (subcategories().length > 0) {
|
||||
<div class="categories-grid">
|
||||
@for (cat of subcategories(); track trackByCategoryId($index, cat)) {
|
||||
@@ -35,7 +59,53 @@
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
}
|
||||
|
||||
<!-- Items directly in this category -->
|
||||
@if (categoryItems().length > 0) {
|
||||
<div class="category-items-section">
|
||||
<h3 class="items-section-title">{{ 'subcategories.itemsInCategory' | translate }}</h3>
|
||||
<div class="items-grid">
|
||||
@for (item of categoryItems(); track trackByItemId($index, item)) {
|
||||
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-card">
|
||||
<div class="item-image">
|
||||
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" decoding="async" />
|
||||
@if (item.discount > 0) {
|
||||
<span class="item-discount">-{{ item.discount }}%</span>
|
||||
}
|
||||
@if (item.badges && item.badges.length > 0) {
|
||||
<div class="item-badges">
|
||||
@for (badge of item.badges; track badge) {
|
||||
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-name">{{ itemName(item) }}</h4>
|
||||
<div class="item-price">
|
||||
@if (item.discount > 0) {
|
||||
<span class="old-price">{{ item.price }} {{ item.currency }}</span>
|
||||
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.0-0' }} {{ item.currency }}</span>
|
||||
} @else {
|
||||
<span class="current-price">{{ item.price }} {{ item.currency }}</span>
|
||||
}
|
||||
</div>
|
||||
<button class="item-cart-btn" (click)="addToCart(item.itemID, $event)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasSubcategories() && categoryItems().length === 0) {
|
||||
<div class="no-subcats">
|
||||
<div class="no-subcats-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -235,6 +235,149 @@
|
||||
min-height: calc(2 * 1.3em);
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: 0.8rem;
|
||||
color: #697777;
|
||||
}
|
||||
|
||||
// Items section within subcategories page
|
||||
.category-items-section {
|
||||
margin-top: 40px;
|
||||
|
||||
.items-section-title {
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e3c38;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
border: 1px solid #d3dad9;
|
||||
border-radius: 13px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #fff;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.item-image {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.item-card:hover & img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.item-discount {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.item-badges {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #1e3c38;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.old-price {
|
||||
font-size: 0.8rem;
|
||||
color: #a1b4b5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e3c38;
|
||||
}
|
||||
}
|
||||
|
||||
.item-cart-btn {
|
||||
align-self: flex-end;
|
||||
background: #497671;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #3a5f5b;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
@@ -248,6 +391,11 @@
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@@ -273,6 +421,11 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
@@ -294,6 +447,11 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ApiService, LanguageService } from '../../services';
|
||||
import { Category } from '../../models';
|
||||
import { ApiService, CartService, LanguageService } from '../../services';
|
||||
import { Category, Item, Subcategory } from '../../models';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subcategories',
|
||||
imports: [RouterLink, LangRoutePipe, TranslatePipe],
|
||||
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './subcategories.component.html',
|
||||
styleUrls: ['./subcategories.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -17,6 +19,10 @@ import { TranslateService } from '../../i18n/translate.service';
|
||||
export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
categories = signal<Category[]>([]);
|
||||
subcategories = signal<Category[]>([]);
|
||||
/** Nested subcategories from API with hasItems support */
|
||||
nestedSubcategories = signal<Subcategory[]>([]);
|
||||
/** Items belonging directly to this category (when hasItems is true) */
|
||||
categoryItems = signal<Item[]>([]);
|
||||
loading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
@@ -29,7 +35,8 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private langService: LanguageService
|
||||
private langService: LanguageService,
|
||||
private cartService: CartService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -45,19 +52,40 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
|
||||
private loadForParent(parentID: number): void {
|
||||
this.loading.set(true);
|
||||
this.categoryItems.set([]);
|
||||
this.nestedSubcategories.set([]);
|
||||
|
||||
this.apiService.getCategories().subscribe({
|
||||
next: (cats) => {
|
||||
this.categories.set(cats);
|
||||
const subs = cats.filter(c => c.parentID === parentID);
|
||||
const parent = cats.find(c => c.categoryID === parentID);
|
||||
this.parentName.set(parent ? parent.name : this.i18n.t('home.categoriesTitle'));
|
||||
|
||||
if (!subs || subs.length === 0) {
|
||||
// Check for nested subcategories from API response (backOffice format)
|
||||
const nested = parent?.subcategories || [];
|
||||
const visibleNested = nested.filter(s => s.visible !== false);
|
||||
|
||||
// Also check flat legacy subcategories
|
||||
const flatSubs = cats.filter(c => c.parentID === parentID);
|
||||
|
||||
if (visibleNested.length > 0) {
|
||||
// Use nested subcategories from API
|
||||
this.nestedSubcategories.set(visibleNested);
|
||||
this.subcategories.set([]);
|
||||
|
||||
// If this category itself has items, load them too
|
||||
this.loadCategoryItems(parentID);
|
||||
} else if (flatSubs.length > 0) {
|
||||
// Legacy flat subcategories
|
||||
this.subcategories.set(flatSubs);
|
||||
this.nestedSubcategories.set([]);
|
||||
|
||||
// Also load items for this category in case it has direct items
|
||||
this.loadCategoryItems(parentID);
|
||||
} else {
|
||||
// No subcategories: redirect to items list for this category
|
||||
const lang = this.langService.currentLanguage();
|
||||
this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true });
|
||||
} else {
|
||||
this.subcategories.set(subs);
|
||||
}
|
||||
|
||||
this.loading.set(false);
|
||||
@@ -70,8 +98,41 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/** Load items that belong directly to this category */
|
||||
private loadCategoryItems(categoryID: number): void {
|
||||
this.apiService.getCategoryItems(categoryID, 50, 0).subscribe({
|
||||
next: (items) => {
|
||||
this.categoryItems.set(items);
|
||||
},
|
||||
error: () => {
|
||||
// Not critical — subcategories still work
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasSubcategories(): boolean {
|
||||
return this.subcategories().length > 0 || this.nestedSubcategories().length > 0;
|
||||
}
|
||||
|
||||
addToCart(itemID: number, event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cartService.addItem(itemID);
|
||||
}
|
||||
|
||||
// TrackBy function for performance optimization
|
||||
trackByCategoryId(index: number, category: Category): number {
|
||||
return category.categoryID;
|
||||
}
|
||||
|
||||
trackBySubId(index: number, sub: Subcategory): string {
|
||||
return sub.id;
|
||||
}
|
||||
|
||||
readonly getDiscountedPrice = getDiscountedPrice;
|
||||
readonly getMainImage = getMainImage;
|
||||
readonly trackByItemId = trackByItemId;
|
||||
readonly getBadgeClass = getBadgeClass;
|
||||
|
||||
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user