very first commit
This commit is contained in:
81
src/app/pages/category/category.component.html
Normal file
81
src/app/pages/category/category.component.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<div class="category-container">
|
||||
@if (error()) {
|
||||
<div class="error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="resetAndLoad()">Попробовать снова</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!error()) {
|
||||
<div class="items-grid">
|
||||
@for (item of items(); track trackByItemId($index, item)) {
|
||||
<div class="item-card">
|
||||
<a [routerLink]="['/item', item.itemID]" class="item-link">
|
||||
<div class="item-image">
|
||||
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
|
||||
@if (item.discount > 0) {
|
||||
<div class="discount-badge">-{{ item.discount }}%</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-details">
|
||||
<h3 class="item-name">{{ item.name }}</h3>
|
||||
|
||||
<div class="item-rating">
|
||||
<span class="rating-stars">⭐ {{ item.rating }}</span>
|
||||
<span class="rating-count">({{ item.callbacks?.length || 0 }})</span>
|
||||
</div>
|
||||
|
||||
<div class="item-price">
|
||||
@if (item.discount > 0) {
|
||||
<span class="original-price">{{ item.price }} {{ item.currency }}</span>
|
||||
<span class="discounted-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
|
||||
} @else {
|
||||
<span class="current-price">{{ item.price }} {{ item.currency }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-stock">
|
||||
<div class="stock-bar">
|
||||
<span class="bar-segment" [class.filled]="item.remainings === 'high' || item.remainings === 'medium' || item.remainings === 'low'" [class.high]="item.remainings === 'high'" [class.medium]="item.remainings === 'medium'" [class.low]="item.remainings === 'low'"></span>
|
||||
<span class="bar-segment" [class.filled]="item.remainings === 'high' || item.remainings === 'medium'" [class.high]="item.remainings === 'high'" [class.medium]="item.remainings === 'medium'"></span>
|
||||
<span class="bar-segment" [class.filled]="item.remainings === 'high'" [class.high]="item.remainings === 'high'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
|
||||
В корзину
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (loading() && items().length > 0) {
|
||||
<div class="loading-more">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasMore() && items().length > 0) {
|
||||
<div class="no-more">
|
||||
<p>Все товары загружены</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (items().length === 0 && !loading()) {
|
||||
<div class="no-items">
|
||||
<p>В этой категории пока нет товаров</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading() && items().length === 0) {
|
||||
<div class="loading-initial">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка товаров...</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
233
src/app/pages/category/category.component.scss
Normal file
233
src/app/pages/category/category.component.scss
Normal file
@@ -0,0 +1,233 @@
|
||||
.category-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error,
|
||||
.loading-initial,
|
||||
.no-items,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 24px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.item-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 75%; // 4:3 aspect ratio
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
|
||||
.rating-stars {
|
||||
color: #ffa502;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
text-decoration: line-through;
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.discounted-price,
|
||||
.current-price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.item-stock {
|
||||
.stock-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.bar-segment {
|
||||
width: 20px;
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&.filled.high {
|
||||
background: #2ed573;
|
||||
}
|
||||
|
||||
&.filled.medium {
|
||||
background: #ffa502;
|
||||
}
|
||||
|
||||
&.filled.low {
|
||||
background: #ff4757;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.no-more {
|
||||
color: #555;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.discounted-price,
|
||||
.current-price {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
117
src/app/pages/category/category.component.ts
Normal file
117
src/app/pages/category/category.component.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ApiService, CartService } from '../../services';
|
||||
import { Item } from '../../models';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
templateUrl: './category.component.html',
|
||||
styleUrls: ['./category.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CategoryComponent implements OnInit, OnDestroy {
|
||||
categoryID = signal<number>(0);
|
||||
items = signal<Item[]>([]);
|
||||
loading = signal(false);
|
||||
error = signal<string | null>(null);
|
||||
hasMore = signal(true);
|
||||
|
||||
private skip = 0;
|
||||
private readonly count = 20;
|
||||
private isLoadingMore = false;
|
||||
private routeSubscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private cartService: CartService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.routeSubscription = this.route.params.subscribe(params => {
|
||||
const id = parseInt(params['id'], 10);
|
||||
this.categoryID.set(id);
|
||||
this.resetAndLoad();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.routeSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
resetAndLoad(): void {
|
||||
this.items.set([]);
|
||||
this.skip = 0;
|
||||
this.hasMore.set(true);
|
||||
this.loadItems();
|
||||
}
|
||||
|
||||
loadItems(): void {
|
||||
if (this.isLoadingMore || !this.hasMore()) return;
|
||||
|
||||
this.loading.set(true);
|
||||
this.isLoadingMore = true;
|
||||
|
||||
this.apiService.getCategoryItems(this.categoryID(), this.count, this.skip).subscribe({
|
||||
next: (newItems) => {
|
||||
// Handle null or empty response
|
||||
if (!newItems || newItems.length === 0) {
|
||||
this.hasMore.set(false);
|
||||
} else {
|
||||
if (newItems.length < this.count) {
|
||||
this.hasMore.set(false);
|
||||
}
|
||||
this.items.update(current => [...current, ...newItems]);
|
||||
this.skip += this.count;
|
||||
}
|
||||
this.loading.set(false);
|
||||
this.isLoadingMore = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load items');
|
||||
this.loading.set(false);
|
||||
this.isLoadingMore = false;
|
||||
console.error('Error loading items:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private scrollTimeout: any;
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onScroll(): void {
|
||||
if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
|
||||
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const bottomPosition = document.documentElement.scrollHeight - 500;
|
||||
|
||||
if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore() && !this.isLoadingMore) {
|
||||
this.loadItems();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
addToCart(itemID: number, event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cartService.addItem(itemID);
|
||||
}
|
||||
|
||||
getDiscountedPrice(item: Item): number {
|
||||
return item.price * (1 - item.discount / 100);
|
||||
}
|
||||
|
||||
getMainImage(item: Item): string {
|
||||
return item.photos?.[0]?.url || '';
|
||||
}
|
||||
|
||||
// TrackBy function for performance optimization
|
||||
trackByItemId(index: number, item: Item): number {
|
||||
return item.itemID;
|
||||
}
|
||||
}
|
||||
38
src/app/pages/category/subcategories.component.html
Normal file
38
src/app/pages/category/subcategories.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="subcategories-container">
|
||||
@if (loading()) {
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка подкатегорий...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="ngOnInit()">Попробовать снова</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading() && !error()) {
|
||||
<header class="sub-header">
|
||||
<h2>{{ parentName() }}</h2>
|
||||
</header>
|
||||
|
||||
<div class="categories-grid">
|
||||
@for (cat of subcategories(); track trackByCategoryId($index, cat)) {
|
||||
<div class="category-card">
|
||||
<a [routerLink]="['/category', cat.categoryID]" class="category-link">
|
||||
<div class="category-media">
|
||||
@if (cat.icon) {
|
||||
<img [src]="cat.icon" [alt]="cat.name" loading="lazy" decoding="async" />
|
||||
} @else {
|
||||
<div class="category-fallback">{{ cat.name }}</div>
|
||||
}
|
||||
</div>
|
||||
<h3>{{ cat.name }}</h3>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
300
src/app/pages/category/subcategories.component.scss
Normal file
300
src/app/pages/category/subcategories.component.scss
Normal file
@@ -0,0 +1,300 @@
|
||||
.subcategories-container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
|
||||
// Loading состояние
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #f3f4f6;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Error состояние
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
|
||||
p {
|
||||
color: #dc2626;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 24px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeInUp 0.5s ease backwards;
|
||||
cursor: pointer;
|
||||
|
||||
// Анимация появления с задержкой для каждой карточки
|
||||
@for $i from 1 through 20 {
|
||||
&:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.category-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
|
||||
.category-media {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f6f7fb 0%, #e9ecf5 100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .category-media {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.category-media img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.category-fallback {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
padding: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
h3 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
color: white;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4) 70%, transparent);
|
||||
z-index: 1;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&:hover h3 {
|
||||
padding: 16px 14px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.5) 70%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes для анимаций
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Мобильная версия
|
||||
@media (max-width: 768px) {
|
||||
padding: 16px;
|
||||
|
||||
.sub-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
border-radius: 10px;
|
||||
|
||||
.category-link {
|
||||
min-height: 140px;
|
||||
|
||||
h3 {
|
||||
font-size: 0.85rem;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очень маленькие экраны
|
||||
@media (max-width: 480px) {
|
||||
padding: 12px;
|
||||
|
||||
.sub-header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
.category-link {
|
||||
min-height: 120px;
|
||||
|
||||
h3 {
|
||||
font-size: 0.8rem;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
.category-fallback {
|
||||
font-size: 0.95rem;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Большие экраны
|
||||
@media (min-width: 1200px) {
|
||||
padding: 32px;
|
||||
|
||||
.sub-header {
|
||||
margin-bottom: 28px;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
.category-card {
|
||||
.category-link {
|
||||
min-height: 220px;
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/app/pages/category/subcategories.component.ts
Normal file
65
src/app/pages/category/subcategories.component.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ApiService } from '../../services';
|
||||
import { Category } from '../../models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subcategories',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
templateUrl: './subcategories.component.html',
|
||||
styleUrls: ['./subcategories.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SubcategoriesComponent implements OnInit {
|
||||
categories = signal<Category[]>([]);
|
||||
subcategories = signal<Category[]>([]);
|
||||
loading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
parentName = signal<string>('');
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.subscribe(params => {
|
||||
const id = parseInt(params['id'], 10);
|
||||
this.loadForParent(id);
|
||||
});
|
||||
}
|
||||
|
||||
private loadForParent(parentID: number): void {
|
||||
this.loading.set(true);
|
||||
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 : 'Категория');
|
||||
|
||||
if (!subs || subs.length === 0) {
|
||||
// No subcategories: redirect to items list for this category
|
||||
this.router.navigate(['/category', parentID, 'items'], { replaceUrl: true });
|
||||
} else {
|
||||
this.subcategories.set(subs);
|
||||
}
|
||||
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load subcategories');
|
||||
this.loading.set(false);
|
||||
console.error('Error loading categories:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TrackBy function for performance optimization
|
||||
trackByCategoryId(index: number, category: Category): number {
|
||||
return category.categoryID;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user