Files
marketplaces/src/app/services/location.service.ts
sdarbinyan 3cf732797c refactor: route localStorage access through shared LocalStorageService
Sprint 2 high-priority cleanup: cart/language/location services called
localStorage directly, bypassing the try/catch safety and core/<domain>
pattern used elsewhere (e.g. ProjectEditorDraftStorageService). New
core/storage/LocalStorageService centralizes get/set/remove and JSON
helpers with private-mode/quota error handling, reused across all three.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 02:58:39 +04:00

134 lines
4.7 KiB
TypeScript

import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Region, GeoIpResponse } from '../models/location.model';
import { ApiConfigService } from '../core/config/api-config.service';
import { LocalStorageService } from '../core/storage/local-storage.service';
const STORAGE_KEY = 'selected_region';
@Injectable({
providedIn: 'root'
})
export class LocationService {
private regionSignal = signal<Region | null>(null);
private regionsSignal = signal<Region[]>([]);
private loadingSignal = signal(false);
private detectedSignal = signal(false);
/** Current selected region (null = global / all regions) */
readonly region = this.regionSignal.asReadonly();
/** All available regions */
readonly regions = this.regionsSignal.asReadonly();
/** Whether geo-detection is in progress */
readonly detecting = this.loadingSignal.asReadonly();
/** Whether region was auto-detected */
readonly autoDetected = this.detectedSignal.asReadonly();
/** Computed region id for API calls — empty string means global */
readonly regionId = computed(() => this.regionSignal()?.id ?? '');
constructor(
private readonly http: HttpClient,
private readonly apiConfig: ApiConfigService,
private readonly storage: LocalStorageService
) {
this.loadRegions();
this.restoreFromStorage();
}
/** Fetch available regions from backend */
loadRegions(): void {
this.http.get<Region[]>(`${this.apiConfig.getBaseUrl()}/regions`).subscribe({
next: (regions) => {
this.regionsSignal.set(regions);
// If we have a stored region, validate it still exists
const stored = this.regionSignal();
if (stored && !regions.find(r => r.id === stored.id)) {
this.clearRegion();
}
},
error: () => {
// Fallback: hardcoded popular regions
this.regionsSignal.set(this.getFallbackRegions());
}
});
}
/** Set region by user choice */
setRegion(region: Region): void {
this.regionSignal.set(region);
this.storage.setJSON(STORAGE_KEY, region);
}
/** Clear region (go global) */
clearRegion(): void {
this.regionSignal.set(null);
this.storage.removeItem(STORAGE_KEY);
}
/** Auto-detect user location via IP geolocation */
detectLocation(): void {
if (this.detectedSignal()) return; // already tried
this.loadingSignal.set(true);
// Using free ip-api.com — no key required, 45 req/min
this.http.get<GeoIpResponse>('http://ip-api.com/json/?fields=city,country,countryCode,region,timezone,lat,lon')
.subscribe({
next: (geo) => {
this.detectedSignal.set(true);
this.loadingSignal.set(false);
// Only auto-set if user hasn't manually chosen a region
if (!this.regionSignal()) {
const matchedRegion = this.findRegionByGeo(geo);
if (matchedRegion) {
this.setRegion(matchedRegion);
}
}
},
error: () => {
this.detectedSignal.set(true);
this.loadingSignal.set(false);
}
});
}
/** Try to match detected geo data to an available region */
private findRegionByGeo(geo: GeoIpResponse): Region | null {
const regions = this.regionsSignal();
if (!regions.length) return null;
// Exact city match
const cityMatch = regions.find(r =>
r.city.toLowerCase() === geo.city?.toLowerCase()
);
if (cityMatch) return cityMatch;
// Country match — pick the first region for that country
const countryMatch = regions.find(r =>
r.countryCode.toLowerCase() === geo.countryCode?.toLowerCase()
);
return countryMatch || null;
}
/** Restore previously selected region from storage */
private restoreFromStorage(): void {
const region = this.storage.getJSON<Region>(STORAGE_KEY);
if (region) {
this.regionSignal.set(region);
}
}
/** Fallback regions if backend /regions endpoint is unavailable */
private getFallbackRegions(): Region[] {
return [
{ id: 'moscow', city: 'Москва', country: 'Россия', countryCode: 'RU', timezone: 'Europe/Moscow' },
{ id: 'spb', city: 'Санкт-Петербург', country: 'Россия', countryCode: 'RU', timezone: 'Europe/Moscow' },
{ id: 'yerevan', city: 'Ереван', country: 'Армения', countryCode: 'AM', timezone: 'Asia/Yerevan' },
{ id: 'minsk', city: 'Минск', country: 'Беларусь', countryCode: 'BY', timezone: 'Europe/Minsk' },
{ id: 'almaty', city: 'Алматы', country: 'Казахстан', countryCode: 'KZ', timezone: 'Asia/Almaty' },
{ id: 'tbilisi', city: 'Тбилиси', country: 'Грузия', countryCode: 'GE', timezone: 'Asia/Tbilisi' },
];
}
}