very first commit
This commit is contained in:
168
src/app/services/api.service.ts
Normal file
168
src/app/services/api.service.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Category, Item } from '../models';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
private readonly baseUrl = environment.apiUrl;
|
||||
private readonly qrBaseUrl = environment.apiUrl;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
private normalizeItem(item: Item): Item {
|
||||
return {
|
||||
...item,
|
||||
remainings: item.remainings || 'high'
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeItems(items: Item[] | null | undefined): Item[] {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
return items.map(item => this.normalizeItem(item));
|
||||
}
|
||||
|
||||
ping(): Observable<{ message: string }> {
|
||||
return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
|
||||
}
|
||||
|
||||
getCategories(): Observable<Category[]> {
|
||||
return this.http.get<Category[]>(`${this.baseUrl}/category`);
|
||||
}
|
||||
|
||||
getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
|
||||
const params = new HttpParams()
|
||||
.set('count', count.toString())
|
||||
.set('skip', skip.toString());
|
||||
return this.http.get<Item[]>(`${this.baseUrl}/category/${categoryID}`, { params })
|
||||
.pipe(map(items => this.normalizeItems(items)));
|
||||
}
|
||||
|
||||
getItem(itemID: number): Observable<Item> {
|
||||
return this.http.get<Item>(`${this.baseUrl}/item/${itemID}`)
|
||||
.pipe(map(item => this.normalizeItem(item)));
|
||||
}
|
||||
|
||||
searchItems(search: string, count: number = 50, skip: number = 0): Observable<{ items: Item[], total: number }> {
|
||||
const params = new HttpParams()
|
||||
.set('search', search)
|
||||
.set('count', count.toString())
|
||||
.set('skip', skip.toString());
|
||||
return this.http.get<{ items: Item[], total: number, count: number, skip: number }>(`${this.baseUrl}/searchitems`, { params })
|
||||
.pipe(
|
||||
map(response => ({
|
||||
items: this.normalizeItems(response?.items || []),
|
||||
total: response?.total || 0
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
addToCart(itemID: number, quantity: number = 1): Observable<{ message: string }> {
|
||||
return this.http.post<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity });
|
||||
}
|
||||
|
||||
updateCartQuantity(itemID: number, quantity: number): Observable<{ message: string }> {
|
||||
return this.http.patch<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity });
|
||||
}
|
||||
|
||||
removeFromCart(itemIDs: number[]): Observable<{ message: string }> {
|
||||
return this.http.delete<{ message: string }>(`${this.baseUrl}/cart`, { body: itemIDs });
|
||||
}
|
||||
|
||||
getCart(): Observable<Item[]> {
|
||||
return this.http.get<Item[]>(`${this.baseUrl}/cart`)
|
||||
.pipe(map(items => this.normalizeItems(items)));
|
||||
}
|
||||
|
||||
// Review submission
|
||||
submitReview(reviewData: {
|
||||
itemID: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
username: string | null;
|
||||
userId: number | null;
|
||||
timestamp: string;
|
||||
}): Observable<{ message: string }> {
|
||||
return this.http.post<{ message: string }>(`${this.baseUrl}/comment`, reviewData);
|
||||
}
|
||||
|
||||
// Payment - SBP Integration
|
||||
createPayment(paymentData: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
siteuserID: string;
|
||||
siteorderID: string;
|
||||
redirectUrl: string;
|
||||
telegramUsername: string;
|
||||
items: Array<{ itemID: number; price: number; name: string }>;
|
||||
}): Observable<{
|
||||
qrId: string;
|
||||
qrStatus: string;
|
||||
qrExpirationDate: string;
|
||||
payload: string;
|
||||
qrUrl: string;
|
||||
}> {
|
||||
return this.http.post<{
|
||||
qrId: string;
|
||||
qrStatus: string;
|
||||
qrExpirationDate: string;
|
||||
payload: string;
|
||||
qrUrl: string;
|
||||
}>(`${this.baseUrl}/cart`, paymentData);
|
||||
}
|
||||
|
||||
checkPaymentStatus(qrId: string): Observable<{
|
||||
additionalInfo: string;
|
||||
paymentPurpose: string;
|
||||
amount: number;
|
||||
code: string;
|
||||
createDate: string;
|
||||
currency: string;
|
||||
order: string;
|
||||
paymentStatus: string;
|
||||
qrId: string;
|
||||
transactionDate: string;
|
||||
transactionId: number;
|
||||
qrExpirationDate: string;
|
||||
phoneNumber: string;
|
||||
}> {
|
||||
return this.http.get<{
|
||||
additionalInfo: string;
|
||||
paymentPurpose: string;
|
||||
amount: number;
|
||||
code: string;
|
||||
createDate: string;
|
||||
currency: string;
|
||||
order: string;
|
||||
paymentStatus: string;
|
||||
qrId: string;
|
||||
transactionDate: string;
|
||||
transactionId: number;
|
||||
qrExpirationDate: string;
|
||||
phoneNumber: string;
|
||||
}>(`${this.qrBaseUrl}/qr/payment/${qrId}`);
|
||||
}
|
||||
|
||||
submitPurchaseEmail(emailData: {
|
||||
email: string;
|
||||
telegramUserId: string | null;
|
||||
items: Array<{ itemID: number; name: string; price: number; currency: string }>;
|
||||
}): Observable<{ message: string }> {
|
||||
return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData);
|
||||
}
|
||||
|
||||
getRandomItems(count: number = 5, categoryID?: number): Observable<Item[]> {
|
||||
let params = new HttpParams().set('count', count.toString());
|
||||
if (categoryID) {
|
||||
params = params.set('category', categoryID.toString());
|
||||
}
|
||||
return this.http.get<Item[]>(`${this.baseUrl}/randomitems`, { params })
|
||||
.pipe(map(items => this.normalizeItems(items)));
|
||||
}
|
||||
}
|
||||
217
src/app/services/cart.service.ts
Normal file
217
src/app/services/cart.service.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { Injectable, signal, computed, effect } from '@angular/core';
|
||||
import { ApiService } from './api.service';
|
||||
import { Item, CartItem } from '../models';
|
||||
import type { } from '../types/telegram.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CartService {
|
||||
private readonly STORAGE_KEY = 'dexarmarket_cart';
|
||||
private cartItems = signal<CartItem[]>([]);
|
||||
private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp;
|
||||
|
||||
items = this.cartItems.asReadonly();
|
||||
itemCount = computed(() => {
|
||||
const items = this.cartItems();
|
||||
if (!Array.isArray(items)) return 0;
|
||||
return items.reduce((total, item) => total + item.quantity, 0);
|
||||
});
|
||||
totalPrice = computed(() => {
|
||||
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);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
this.loadCart();
|
||||
|
||||
// Auto-save whenever cart changes
|
||||
effect(() => {
|
||||
const items = this.cartItems();
|
||||
this.saveToStorage(items);
|
||||
});
|
||||
}
|
||||
|
||||
private saveToStorage(items: CartItem[]): void {
|
||||
const data = JSON.stringify(items);
|
||||
|
||||
// Always save to sessionStorage
|
||||
sessionStorage.setItem(this.STORAGE_KEY, data);
|
||||
|
||||
// Also save to Telegram CloudStorage if available
|
||||
if (this.isTelegram) {
|
||||
window.Telegram!.WebApp.CloudStorage.setItem(this.STORAGE_KEY, data, (err) => {
|
||||
if (err) {
|
||||
console.error('Error saving to Telegram CloudStorage:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private loadCart(): void {
|
||||
if (this.isTelegram) {
|
||||
// Load from Telegram CloudStorage first
|
||||
window.Telegram!.WebApp.CloudStorage.getItem(this.STORAGE_KEY, (err, value) => {
|
||||
if (err) {
|
||||
console.error('Error loading from Telegram CloudStorage:', err);
|
||||
this.loadFromSessionStorage();
|
||||
} else if (value) {
|
||||
try {
|
||||
const items = JSON.parse(value);
|
||||
if (Array.isArray(items)) {
|
||||
// Migrate old items without quantity field
|
||||
const migratedItems: CartItem[] = items.map(item => ({
|
||||
...item,
|
||||
quantity: item.quantity || 1
|
||||
}));
|
||||
this.cartItems.set(migratedItems);
|
||||
} else {
|
||||
this.cartItems.set([]);
|
||||
}
|
||||
} catch (parseErr) {
|
||||
console.error('Error parsing cart data:', parseErr);
|
||||
this.loadFromSessionStorage();
|
||||
}
|
||||
} else {
|
||||
// No data in CloudStorage, try sessionStorage
|
||||
this.loadFromSessionStorage();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Load from sessionStorage
|
||||
this.loadFromSessionStorage();
|
||||
}
|
||||
|
||||
// API version (commented out)
|
||||
// this.apiService.getCart().subscribe({
|
||||
// next: (items) => {
|
||||
// console.log('Cart response:', items);
|
||||
// if (Array.isArray(items)) {
|
||||
// this.cartItems.set(items);
|
||||
// } else {
|
||||
// console.error('Cart response is not an array:', items);
|
||||
// this.cartItems.set([]);
|
||||
// }
|
||||
// },
|
||||
// error: (err) => console.error('Error loading cart:', err)
|
||||
// });
|
||||
}
|
||||
|
||||
private loadFromSessionStorage(): void {
|
||||
const stored = sessionStorage.getItem(this.STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
const items = JSON.parse(stored);
|
||||
if (Array.isArray(items)) {
|
||||
// Migrate old items without quantity field
|
||||
const migratedItems: CartItem[] = items.map(item => ({
|
||||
...item,
|
||||
quantity: item.quantity || 1
|
||||
}));
|
||||
this.cartItems.set(migratedItems);
|
||||
} else {
|
||||
this.cartItems.set([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing cart from sessionStorage:', err);
|
||||
this.cartItems.set([]);
|
||||
}
|
||||
} else {
|
||||
this.cartItems.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
addItem(itemID: number, quantity: number = 1): void {
|
||||
const currentItems = this.cartItems();
|
||||
const existingItem = currentItems.find(i => i.itemID === itemID);
|
||||
|
||||
if (existingItem) {
|
||||
// Item exists, increase quantity
|
||||
this.updateQuantity(itemID, existingItem.quantity + quantity);
|
||||
} else {
|
||||
// Get item details from API and add to cart
|
||||
this.apiService.getItem(itemID).subscribe({
|
||||
next: (item) => {
|
||||
const cartItem: CartItem = { ...item, quantity };
|
||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error adding to cart:', err);
|
||||
alert('Ошибка добавления в корзину: ' + (err.error?.message || err.message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// API version (commented out)
|
||||
// this.apiService.addToCart(itemID, quantity).subscribe({
|
||||
// next: (response) => {
|
||||
// console.log('Item added to cart:', response);
|
||||
// this.loadCart();
|
||||
// },
|
||||
// error: (err) => {
|
||||
// console.error('Error adding to cart:', err);
|
||||
// alert('Ошибка добавления в корзину: ' + (err.error?.message || err.message));
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
updateQuantity(itemID: number, quantity: number): void {
|
||||
if (quantity <= 0) {
|
||||
this.removeItem(itemID);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentItems = this.cartItems();
|
||||
const updatedItems = currentItems.map(item =>
|
||||
item.itemID === itemID ? { ...item, quantity } : item
|
||||
);
|
||||
this.cartItems.set(updatedItems);
|
||||
|
||||
// API version (commented out)
|
||||
// this.apiService.updateCartQuantity(itemID, quantity).subscribe({
|
||||
// next: (response) => {
|
||||
// console.log('Quantity updated:', response);
|
||||
// this.loadCart();
|
||||
// },
|
||||
// error: (err) => {
|
||||
// console.error('Error updating quantity:', err);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
removeItems(itemIDs: number[]): void {
|
||||
const currentItems = this.cartItems();
|
||||
const updatedItems = currentItems.filter(item => !itemIDs.includes(item.itemID));
|
||||
this.cartItems.set(updatedItems);
|
||||
|
||||
// API version (commented out)
|
||||
// this.apiService.removeFromCart(itemIDs).subscribe({
|
||||
// next: (response) => {
|
||||
// console.log('Items removed from cart:', response);
|
||||
// this.loadCart();
|
||||
// },
|
||||
// error: (err) => {
|
||||
// console.error('Error removing from cart:', err);
|
||||
// alert('Ошибка удаления из корзины: ' + (err.error?.message || err.message));
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
removeItem(itemID: number): void {
|
||||
this.removeItems([itemID]);
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
this.cartItems.set([]);
|
||||
|
||||
// API version (commented out)
|
||||
// const itemIDs = this.cartItems().map(item => item.itemID);
|
||||
// if (itemIDs.length > 0) {
|
||||
// this.removeItems(itemIDs);
|
||||
// }
|
||||
}
|
||||
}
|
||||
4
src/app/services/index.ts
Normal file
4
src/app/services/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './api.service';
|
||||
export * from './cart.service';
|
||||
export * from './telegram.service';
|
||||
export * from './language.service';
|
||||
44
src/app/services/language.service.ts
Normal file
44
src/app/services/language.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
flag: string;
|
||||
flagSvg: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LanguageService {
|
||||
private currentLanguageSignal = signal<string>('ru');
|
||||
|
||||
languages: Language[] = [
|
||||
{ code: 'ru', name: 'Русский', flag: '🇷🇺', flagSvg: '/flags/ru.svg', enabled: true },
|
||||
{ code: 'en', name: 'English', flag: '🇬🇧', flagSvg: '/flags/en.svg', enabled: false },
|
||||
{ code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: false }
|
||||
];
|
||||
|
||||
currentLanguage = this.currentLanguageSignal.asReadonly();
|
||||
|
||||
constructor() {
|
||||
// Load saved language from localStorage
|
||||
const savedLang = localStorage.getItem('selectedLanguage');
|
||||
if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) {
|
||||
this.currentLanguageSignal.set(savedLang);
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage(langCode: string): void {
|
||||
const lang = this.languages.find(l => l.code === langCode);
|
||||
if (lang && lang.enabled) {
|
||||
this.currentLanguageSignal.set(langCode);
|
||||
localStorage.setItem('selectedLanguage', langCode);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentLanguage(): Language | undefined {
|
||||
return this.languages.find(l => l.code === this.currentLanguageSignal());
|
||||
}
|
||||
}
|
||||
39
src/app/services/telegram.service.ts
Normal file
39
src/app/services/telegram.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import type { } from '../types/telegram.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TelegramService {
|
||||
private tg = typeof window !== 'undefined' ? window.Telegram?.WebApp : null;
|
||||
|
||||
isTelegramApp(): boolean {
|
||||
return !!this.tg;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.tg?.initDataUnsafe?.user || null;
|
||||
}
|
||||
|
||||
getUserId(): number | null {
|
||||
return this.tg?.initDataUnsafe?.user?.id || null;
|
||||
}
|
||||
|
||||
getUsername(): string | null {
|
||||
return this.tg?.initDataUnsafe?.user?.username || null;
|
||||
}
|
||||
|
||||
getFirstName(): string | null {
|
||||
return this.tg?.initDataUnsafe?.user?.first_name || null;
|
||||
}
|
||||
|
||||
getFullName(): string | null {
|
||||
const user = this.getUser();
|
||||
if (!user) return null;
|
||||
return `${user.first_name}${user.last_name ? ' ' + user.last_name : ''}`;
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return this.getUsername() || this.getFullName() || 'Пользователь';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user