optimising and making it better
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable, signal, computed, effect } from '@angular/core';
|
||||
import { ApiService } from './api.service';
|
||||
import { Item, CartItem } from '../models';
|
||||
import { getDiscountedPrice } from '../utils/item.utils';
|
||||
import { environment } from '../../environments/environment';
|
||||
import type { } from '../types/telegram.types';
|
||||
|
||||
@@ -11,6 +12,7 @@ export class CartService {
|
||||
private readonly STORAGE_KEY = `${environment.brandName.toLowerCase().replace(/\s+/g, '_')}_cart`;
|
||||
private cartItems = signal<CartItem[]>([]);
|
||||
private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp;
|
||||
private addingItems = new Set<number>();
|
||||
|
||||
items = this.cartItems.asReadonly();
|
||||
itemCount = computed(() => {
|
||||
@@ -22,8 +24,7 @@ export class CartService {
|
||||
const items = this.cartItems();
|
||||
if (!Array.isArray(items)) return 0;
|
||||
return items.reduce((total, item) => {
|
||||
const price = item.price * (1 - item.discount / 100);
|
||||
return total + (price * item.quantity);
|
||||
return total + (getDiscountedPrice(item) * item.quantity);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
@@ -40,8 +41,8 @@ export class CartService {
|
||||
private saveToStorage(items: CartItem[]): void {
|
||||
const data = JSON.stringify(items);
|
||||
|
||||
// Always save to sessionStorage
|
||||
sessionStorage.setItem(this.STORAGE_KEY, data);
|
||||
// Always save to localStorage
|
||||
localStorage.setItem(this.STORAGE_KEY, data);
|
||||
|
||||
// Also save to Telegram CloudStorage if available
|
||||
if (this.isTelegram) {
|
||||
@@ -59,21 +60,21 @@ export class CartService {
|
||||
window.Telegram!.WebApp.CloudStorage.getItem(this.STORAGE_KEY, (err, value) => {
|
||||
if (err) {
|
||||
console.error('Error loading from Telegram CloudStorage:', err);
|
||||
this.loadFromSessionStorage();
|
||||
this.loadFromLocalStorage();
|
||||
} else if (value) {
|
||||
this.parseAndSetCart(value) || this.loadFromSessionStorage();
|
||||
this.parseAndSetCart(value) || this.loadFromLocalStorage();
|
||||
} else {
|
||||
// No data in CloudStorage, try sessionStorage
|
||||
this.loadFromSessionStorage();
|
||||
// No data in CloudStorage, try localStorage
|
||||
this.loadFromLocalStorage();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.loadFromSessionStorage();
|
||||
this.loadFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
private loadFromSessionStorage(): void {
|
||||
const stored = sessionStorage.getItem(this.STORAGE_KEY);
|
||||
private loadFromLocalStorage(): void {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (stored) {
|
||||
this.parseAndSetCart(stored);
|
||||
}
|
||||
@@ -98,6 +99,9 @@ export class CartService {
|
||||
}
|
||||
|
||||
addItem(itemID: number, quantity: number = 1): void {
|
||||
// Prevent duplicate API calls for same item
|
||||
if (this.addingItems.has(itemID)) return;
|
||||
|
||||
const currentItems = this.cartItems();
|
||||
const existingItem = currentItems.find(i => i.itemID === itemID);
|
||||
|
||||
@@ -106,14 +110,16 @@ export class CartService {
|
||||
this.updateQuantity(itemID, existingItem.quantity + quantity);
|
||||
} else {
|
||||
// Get item details from API and add to cart
|
||||
this.addingItems.add(itemID);
|
||||
this.apiService.getItem(itemID).subscribe({
|
||||
next: (item) => {
|
||||
const cartItem: CartItem = { ...item, quantity };
|
||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||
this.addingItems.delete(itemID);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error adding to cart:', err);
|
||||
alert('Ошибка добавления в корзину: ' + (err.error?.message || err.message));
|
||||
this.addingItems.delete(itemID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './api.service';
|
||||
export * from './cart.service';
|
||||
export * from './telegram.service';
|
||||
export * from './language.service';
|
||||
export * from './seo.service';
|
||||
|
||||
117
src/app/services/seo.service.ts
Normal file
117
src/app/services/seo.service.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Item } from '../models';
|
||||
import { getDiscountedPrice, getMainImage } from '../utils/item.utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SeoService {
|
||||
private meta = inject(Meta);
|
||||
private title = inject(Title);
|
||||
|
||||
private readonly siteUrl = `https://${environment.domain}`;
|
||||
private readonly siteName = environment.brandFullName;
|
||||
|
||||
/**
|
||||
* Set Open Graph & Twitter Card meta tags for a product/item page.
|
||||
*/
|
||||
setItemMeta(item: Item): void {
|
||||
const price = item.discount > 0 ? getDiscountedPrice(item) : item.price;
|
||||
const imageUrl = this.resolveUrl(getMainImage(item));
|
||||
const itemUrl = `${this.siteUrl}/item/${item.itemID}`;
|
||||
const description = this.truncate(this.stripHtml(item.description), 160);
|
||||
const titleText = `${item.name} — ${this.siteName}`;
|
||||
|
||||
this.title.setTitle(titleText);
|
||||
|
||||
this.setOrUpdate([
|
||||
// Open Graph
|
||||
{ property: 'og:type', content: 'product' },
|
||||
{ property: 'og:title', content: item.name },
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:image', content: imageUrl },
|
||||
{ property: 'og:url', content: itemUrl },
|
||||
{ property: 'og:site_name', content: this.siteName },
|
||||
{ property: 'og:locale', content: 'ru_RU' },
|
||||
|
||||
// Product-specific OG tags
|
||||
{ property: 'product:price:amount', content: price.toFixed(2) },
|
||||
{ property: 'product:price:currency', content: item.currency || 'RUB' },
|
||||
|
||||
// Twitter Card
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: item.name },
|
||||
{ name: 'twitter:description', content: description },
|
||||
{ name: 'twitter:image', content: imageUrl },
|
||||
|
||||
// Standard meta
|
||||
{ name: 'description', content: description },
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset meta tags back to defaults (call on navigation away from item page).
|
||||
*/
|
||||
resetToDefaults(): void {
|
||||
const defaultTitle = `${this.siteName} — Маркетплейс товаров и услуг`;
|
||||
const defaultDescription = 'Современный маркетплейс для покупки цифровых товаров. Широкий выбор товаров, удобный поиск, быстрая доставка.';
|
||||
const defaultImage = `${this.siteUrl}/og-image.jpg`;
|
||||
|
||||
this.title.setTitle(defaultTitle);
|
||||
|
||||
this.setOrUpdate([
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:title', content: defaultTitle },
|
||||
{ property: 'og:description', content: defaultDescription },
|
||||
{ property: 'og:image', content: defaultImage },
|
||||
{ property: 'og:url', content: this.siteUrl },
|
||||
{ property: 'og:site_name', content: this.siteName },
|
||||
{ property: 'og:locale', content: 'ru_RU' },
|
||||
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: defaultTitle },
|
||||
{ name: 'twitter:description', content: defaultDescription },
|
||||
{ name: 'twitter:image', content: defaultImage },
|
||||
|
||||
{ name: 'description', content: defaultDescription },
|
||||
]);
|
||||
|
||||
// Remove product-specific tags
|
||||
this.meta.removeTag("property='product:price:amount'");
|
||||
this.meta.removeTag("property='product:price:currency'");
|
||||
}
|
||||
|
||||
private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void {
|
||||
for (const tag of tags) {
|
||||
const selector = tag.property
|
||||
? `property='${tag.property}'`
|
||||
: `name='${tag.name}'`;
|
||||
|
||||
const existing = this.meta.getTag(selector);
|
||||
if (existing) {
|
||||
this.meta.updateTag(tag as any, selector);
|
||||
} else {
|
||||
this.meta.addTag(tag as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert relative URLs to absolute */
|
||||
private resolveUrl(url: string): string {
|
||||
if (!url || url.startsWith('http')) return url;
|
||||
return `${this.siteUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
|
||||
/** Strip HTML tags from a string */
|
||||
private stripHtml(html: string): string {
|
||||
return html?.replace(/<[^>]*>/g, '') || '';
|
||||
}
|
||||
|
||||
/** Truncate text to maxLength, adding ellipsis */
|
||||
private truncate(text: string, maxLength: number): string {
|
||||
if (!text || text.length <= maxLength) return text || '';
|
||||
return text.substring(0, maxLength - 1) + '…';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user