@africanies/angular-web-sdk 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.angular/cache/22.0.9/playground/.tsbuildinfo +1 -1
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/README.md +85 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +4639 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/package.json +31 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +2748 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19500 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5521 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/README.md +46 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +243 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/package.json +35 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/tailwind-preset.cjs +330 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +176 -0
- package/.nx/cache/run.json +25 -25
- package/.nx/cache/terminalOutputs/12565224028063674529 +37 -0
- package/.nx/cache/terminalOutputs/13478445034325959094 +176 -0
- package/.nx/cache/terminalOutputs/15835338512221847842 +184 -182
- package/.nx/cache/terminalOutputs/17289624717336768573 +21 -0
- package/.nx/cache/terminalOutputs/17571479482482478257 +35 -0
- package/.nx/cache/terminalOutputs/3088264223473143358 +8 -0
- package/.nx/cache/terminalOutputs/3953970051755345887 +21 -0
- package/.nx/cache/terminalOutputs/4225324930345532464 +21 -0
- package/.nx/cache/terminalOutputs/4363587767476846758 +176 -0
- package/.nx/cache/terminalOutputs/8193462679279018087 +39 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
- package/.nx/workspace-data/d/daemon.log +4460 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +2998 -3006
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +2 -2
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +6 -1
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -1
- package/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +6 -1
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +30 -55
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
- package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +14 -16
- package/libs/africanies-core/src/index.ts +1 -1
- package/libs/africanies-core/src/lib/notification/notification-link.ts +6 -1
- package/libs/africanies-ui/src/lib/layout/app-shell-header.component.ts +9 -9
- package/libs/africanies-ui/src/lib/layout/header-greeting.util.ts +0 -2
- package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +11 -40
- package/libs/africanies-ui/src/lib/layout/header-weather.ts +22 -63
- package/package.json +1 -1
|
@@ -210,9 +210,9 @@ export {
|
|
|
210
210
|
|
|
211
211
|
// User notifications (auth required)
|
|
212
212
|
export {
|
|
213
|
+
isNotificationExternalLink,
|
|
213
214
|
mapNotification,
|
|
214
215
|
mapNotificationInboxItem,
|
|
215
|
-
isNotificationExternalLink,
|
|
216
216
|
mapNotificationList,
|
|
217
217
|
mapNotificationPayload,
|
|
218
218
|
NOTIFICATION_READ_PATH,
|
|
@@ -12,7 +12,10 @@ const QUERY_SHIPMENT_APP = 'shipment_app';
|
|
|
12
12
|
const LEGACY_PORTAL_HOST =
|
|
13
13
|
/^(?:test-)?(?:admin|customer)-(?:export|import)\.africaniestest\.com$/i;
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Signed export downloads and other hosts that must not be rewritten in-app.
|
|
17
|
+
* @param link
|
|
18
|
+
*/
|
|
16
19
|
export function isNotificationExternalLink(link: string): boolean {
|
|
17
20
|
const trimmed = decodeNotificationLink(link).trim();
|
|
18
21
|
if (!trimmed) {
|
|
@@ -39,6 +42,8 @@ export function isNotificationExternalLink(link: string): boolean {
|
|
|
39
42
|
* - Legacy portal hosts become relative `/portal/...` paths with query preserved.
|
|
40
43
|
* - When the notification already includes `shipment_app`, it is kept as-is.
|
|
41
44
|
* - Otherwise classic host / `shipment_app` rewriting applies for portal URLs.
|
|
45
|
+
* @param link
|
|
46
|
+
* @param mode
|
|
42
47
|
*/
|
|
43
48
|
export function resolveNotificationLinkForMode(
|
|
44
49
|
link: string | null | undefined,
|
|
@@ -113,13 +113,13 @@ export class AppShellHeaderEndDirective {}
|
|
|
113
113
|
<span
|
|
114
114
|
class="text-body-sm font-medium tabular-nums tracking-tight text-ink dark:text-white"
|
|
115
115
|
>
|
|
116
|
-
{{ temp }}°
|
|
116
|
+
{{ temp }}°C
|
|
117
117
|
</span>
|
|
118
118
|
}
|
|
119
119
|
<span
|
|
120
120
|
class="hidden text-caption text-neutral-600 md:inline dark:text-neutral-400"
|
|
121
121
|
>
|
|
122
|
-
{{
|
|
122
|
+
{{ weatherCondition() }}
|
|
123
123
|
</span>
|
|
124
124
|
</div>
|
|
125
125
|
}
|
|
@@ -274,15 +274,15 @@ export class AppShellHeaderComponent {
|
|
|
274
274
|
if (temp === undefined || !Number.isFinite(temp)) {
|
|
275
275
|
return null;
|
|
276
276
|
}
|
|
277
|
-
return
|
|
277
|
+
return String(Math.round(temp));
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
protected readonly
|
|
280
|
+
protected readonly weatherCondition = computed(() => {
|
|
281
281
|
const forecast = this.weather();
|
|
282
282
|
if (!forecast) {
|
|
283
283
|
return '';
|
|
284
284
|
}
|
|
285
|
-
return
|
|
285
|
+
return headerWeatherLabel(forecast.kind, this.now().getHours());
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
protected readonly weatherAriaLabel = computed(() => {
|
|
@@ -290,11 +290,11 @@ export class AppShellHeaderComponent {
|
|
|
290
290
|
if (!forecast) {
|
|
291
291
|
return 'Weather';
|
|
292
292
|
}
|
|
293
|
-
const condition =
|
|
293
|
+
const condition = this.weatherCondition().toLowerCase();
|
|
294
294
|
const temp = this.weatherTemp();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
return temp === null
|
|
296
|
+
? `Weather: ${condition}`
|
|
297
|
+
: `Weather: ${temp} degrees Celsius, ${condition}`;
|
|
298
298
|
});
|
|
299
299
|
|
|
300
300
|
protected readonly unreadCount = computed(
|
|
@@ -33,8 +33,6 @@ export interface HeaderWeather {
|
|
|
33
33
|
kind: HeaderWeatherKind;
|
|
34
34
|
/** Celsius, when the forecast includes it. */
|
|
35
35
|
temperatureC?: number;
|
|
36
|
-
/** City from IP geolocation, when known. */
|
|
37
|
-
city?: string;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
const PERIOD_KICKERS: Record<HeaderGreetingPeriod, readonly string[]> = {
|
|
@@ -66,7 +66,7 @@ describe('loadHeaderWeather', () => {
|
|
|
66
66
|
).resolves.toBeNull();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('reads Open-Meteo after IP
|
|
69
|
+
it('reads Open-Meteo after IP coordinates', async () => {
|
|
70
70
|
const fetchFn = jest.fn(async (url: string) => {
|
|
71
71
|
if (String(url).includes('geojs')) {
|
|
72
72
|
return jsonResponse({
|
|
@@ -75,9 +75,6 @@ describe('loadHeaderWeather', () => {
|
|
|
75
75
|
city: 'Lagos',
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
if (String(url).includes('bigdatacloud')) {
|
|
79
|
-
return jsonResponse({ city: 'Abuja', locality: 'Abuja' });
|
|
80
|
-
}
|
|
81
78
|
return jsonResponse({
|
|
82
79
|
current: { weather_code: 61, temperature_2m: 27.4 },
|
|
83
80
|
});
|
|
@@ -88,29 +85,8 @@ describe('loadHeaderWeather', () => {
|
|
|
88
85
|
).resolves.toEqual({
|
|
89
86
|
kind: 'rain',
|
|
90
87
|
temperatureC: 27.4,
|
|
91
|
-
city: 'Abuja',
|
|
92
|
-
});
|
|
93
|
-
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('falls back to the IP city when reverse geocode is empty', async () => {
|
|
97
|
-
const fetchFn = jest.fn(async (url: string) => {
|
|
98
|
-
if (String(url).includes('geojs')) {
|
|
99
|
-
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
100
|
-
}
|
|
101
|
-
if (String(url).includes('bigdatacloud')) {
|
|
102
|
-
return jsonResponse({});
|
|
103
|
-
}
|
|
104
|
-
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await expect(
|
|
108
|
-
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
109
|
-
).resolves.toEqual({
|
|
110
|
-
kind: 'clear',
|
|
111
|
-
temperatureC: 30,
|
|
112
|
-
city: 'Lagos',
|
|
113
88
|
});
|
|
89
|
+
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
114
90
|
});
|
|
115
91
|
|
|
116
92
|
it('reuses the same-hour session cache', async () => {
|
|
@@ -118,9 +94,6 @@ describe('loadHeaderWeather', () => {
|
|
|
118
94
|
if (String(url).includes('geojs')) {
|
|
119
95
|
return jsonResponse({ latitude: 1, longitude: 2 });
|
|
120
96
|
}
|
|
121
|
-
if (String(url).includes('bigdatacloud')) {
|
|
122
|
-
return jsonResponse({ city: 'Accra' });
|
|
123
|
-
}
|
|
124
97
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
125
98
|
});
|
|
126
99
|
|
|
@@ -128,16 +101,13 @@ describe('loadHeaderWeather', () => {
|
|
|
128
101
|
const second = await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
129
102
|
|
|
130
103
|
expect(first).toEqual(second);
|
|
131
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
104
|
+
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
132
105
|
});
|
|
133
106
|
|
|
134
107
|
it('fetches again when the cached hour no longer matches', async () => {
|
|
135
108
|
const fetchFn = jest.fn(async (url: string) => {
|
|
136
109
|
if (String(url).includes('geojs')) {
|
|
137
|
-
return jsonResponse({ latitude: 1, longitude: 2
|
|
138
|
-
}
|
|
139
|
-
if (String(url).includes('bigdatacloud')) {
|
|
140
|
-
return jsonResponse({ city: 'Lagos' });
|
|
110
|
+
return jsonResponse({ latitude: 1, longitude: 2 });
|
|
141
111
|
}
|
|
142
112
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30.2 } });
|
|
143
113
|
});
|
|
@@ -154,7 +124,7 @@ describe('loadHeaderWeather', () => {
|
|
|
154
124
|
sessionStorage.setItem(cacheKey!, JSON.stringify(stored));
|
|
155
125
|
|
|
156
126
|
await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
157
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
127
|
+
expect(fetchFn).toHaveBeenCalledTimes(4);
|
|
158
128
|
});
|
|
159
129
|
|
|
160
130
|
it('uses browser coordinates when geolocation succeeds', async () => {
|
|
@@ -181,9 +151,6 @@ describe('loadHeaderWeather', () => {
|
|
|
181
151
|
});
|
|
182
152
|
|
|
183
153
|
const fetchFn = jest.fn(async (url: string) => {
|
|
184
|
-
if (String(url).includes('bigdatacloud')) {
|
|
185
|
-
return jsonResponse({ city: 'Abuja' });
|
|
186
|
-
}
|
|
187
154
|
if (String(url).includes('geojs')) {
|
|
188
155
|
throw new Error('should not call IP geo');
|
|
189
156
|
}
|
|
@@ -195,7 +162,6 @@ describe('loadHeaderWeather', () => {
|
|
|
195
162
|
).resolves.toEqual({
|
|
196
163
|
kind: 'clear',
|
|
197
164
|
temperatureC: 31,
|
|
198
|
-
city: 'Abuja',
|
|
199
165
|
});
|
|
200
166
|
expect(
|
|
201
167
|
fetchFn.mock.calls.some((call) => String(call[0]).includes('geojs')),
|
|
@@ -206,7 +172,12 @@ describe('loadHeaderWeather', () => {
|
|
|
206
172
|
describe('headerWeatherLabel', () => {
|
|
207
173
|
it('returns a short condition', () => {
|
|
208
174
|
expect(headerWeatherLabel('rain')).toBe('Rain');
|
|
209
|
-
expect(headerWeatherLabel('
|
|
175
|
+
expect(headerWeatherLabel('cloudy')).toBe('Cloudy');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('uses Sunny by day and Clear at night', () => {
|
|
179
|
+
expect(headerWeatherLabel('clear', 10)).toBe('Sunny');
|
|
180
|
+
expect(headerWeatherLabel('clear', 22)).toBe('Clear');
|
|
210
181
|
});
|
|
211
182
|
});
|
|
212
183
|
|
|
@@ -2,15 +2,12 @@ import type { IconName } from '@africanies/africanies-icons';
|
|
|
2
2
|
|
|
3
3
|
import type { HeaderWeather, HeaderWeatherKind } from './header-greeting.util';
|
|
4
4
|
|
|
5
|
-
/** Bump when cache shape or
|
|
6
|
-
const CACHE_KEY = 'africanies-header-weather-
|
|
5
|
+
/** Bump when cache shape or lookup strategy changes. */
|
|
6
|
+
const CACHE_KEY = 'africanies-header-weather-v5';
|
|
7
7
|
const FETCH_MS = 4_000;
|
|
8
8
|
const BROWSER_GEO_MS = 3_500;
|
|
9
|
-
/** IP fallback — city
|
|
9
|
+
/** IP fallback for coordinates only — city strings are often wrong. */
|
|
10
10
|
const GEO_URL = 'https://get.geojs.io/v1/ip/geo.json';
|
|
11
|
-
/** Client reverse-geocode so the label matches the forecast coordinates. */
|
|
12
|
-
const REVERSE_GEO_URL =
|
|
13
|
-
'https://api.bigdatacloud.net/data/reverse-geocode-client';
|
|
14
11
|
|
|
15
12
|
const WEATHER_LABELS: Record<HeaderWeatherKind, string> = {
|
|
16
13
|
clear: 'Clear',
|
|
@@ -26,21 +23,13 @@ interface CachedWeather {
|
|
|
26
23
|
hour: string;
|
|
27
24
|
kind: HeaderWeatherKind;
|
|
28
25
|
temperatureC?: number;
|
|
29
|
-
city?: string;
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
interface GeoJsPayload {
|
|
33
|
-
city?: string;
|
|
34
29
|
latitude?: string | number;
|
|
35
30
|
longitude?: string | number;
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
interface ReverseGeoPayload {
|
|
39
|
-
city?: string;
|
|
40
|
-
locality?: string;
|
|
41
|
-
principalSubdivision?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
interface OpenMeteoPayload {
|
|
45
34
|
current?: {
|
|
46
35
|
weather_code?: number;
|
|
@@ -88,9 +77,16 @@ export function mapWmoWeatherCode(code: number): HeaderWeatherKind | null {
|
|
|
88
77
|
* Short condition label for the header weather chip.
|
|
89
78
|
*
|
|
90
79
|
* @param kind - Coarse forecast kind.
|
|
91
|
-
* @
|
|
80
|
+
* @param hour - Local hour; clear days show as Sunny, nights as Clear.
|
|
81
|
+
* @returns Display label such as `Rain` or `Sunny`.
|
|
92
82
|
*/
|
|
93
|
-
export function headerWeatherLabel(
|
|
83
|
+
export function headerWeatherLabel(
|
|
84
|
+
kind: HeaderWeatherKind,
|
|
85
|
+
hour = new Date().getHours(),
|
|
86
|
+
): string {
|
|
87
|
+
if (kind === 'clear') {
|
|
88
|
+
return hour >= 19 || hour < 6 ? 'Clear' : 'Sunny';
|
|
89
|
+
}
|
|
94
90
|
return WEATHER_LABELS[kind];
|
|
95
91
|
}
|
|
96
92
|
|
|
@@ -112,12 +108,12 @@ export function headerWeatherIcon(kind: HeaderWeatherKind, hour: number): IconNa
|
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
/**
|
|
115
|
-
*
|
|
111
|
+
* Local forecast via browser / IP coordinates + Open-Meteo (no API key).
|
|
116
112
|
*
|
|
117
|
-
* Coordinates prefer the browser Geolocation API (
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
113
|
+
* Coordinates prefer the browser Geolocation API (may prompt once).
|
|
114
|
+
* Otherwise IP geo is used for lat/lon only — city labels are not shown
|
|
115
|
+
* because IP and reverse-geocode cities are often wrong (e.g. NG IPs
|
|
116
|
+
* labelled Lagos).
|
|
121
117
|
*
|
|
122
118
|
* Fails closed: missing browser APIs, timeouts, and HTTP errors all return
|
|
123
119
|
* `null` so the greeting can stay time-of-day only. Cached per local hour
|
|
@@ -162,11 +158,8 @@ export async function loadHeaderWeather(
|
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
const temperatureC = asNumber(forecast?.current?.temperature_2m) ?? undefined;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
coords.fallbackCity;
|
|
168
|
-
const weather: HeaderWeather = { kind, temperatureC, city };
|
|
169
|
-
writeCache({ hour, kind, temperatureC, city });
|
|
161
|
+
const weather: HeaderWeather = { kind, temperatureC };
|
|
162
|
+
writeCache({ hour, kind, temperatureC });
|
|
170
163
|
return weather;
|
|
171
164
|
} catch {
|
|
172
165
|
return null;
|
|
@@ -194,7 +187,6 @@ function readCache(hour: string): HeaderWeather | null {
|
|
|
194
187
|
return {
|
|
195
188
|
kind: parsed.kind,
|
|
196
189
|
temperatureC: parsed.temperatureC,
|
|
197
|
-
city: parsed.city,
|
|
198
190
|
};
|
|
199
191
|
} catch {
|
|
200
192
|
return null;
|
|
@@ -211,12 +203,11 @@ function writeCache(value: CachedWeather): void {
|
|
|
211
203
|
|
|
212
204
|
/**
|
|
213
205
|
* Prefer device coordinates when available (may prompt once); otherwise fall
|
|
214
|
-
* back to IP
|
|
215
|
-
* geocoding from these coordinates still depends on accurate lat/lon.
|
|
206
|
+
* back to IP lat/lon for the forecast only.
|
|
216
207
|
*/
|
|
217
208
|
async function resolveCoordinates(
|
|
218
209
|
fetchFn: typeof fetch,
|
|
219
|
-
): Promise<
|
|
210
|
+
): Promise<Coordinates | null> {
|
|
220
211
|
const browser = await tryBrowserGeolocation();
|
|
221
212
|
if (browser) {
|
|
222
213
|
return browser;
|
|
@@ -228,11 +219,7 @@ async function resolveCoordinates(
|
|
|
228
219
|
if (latitude === null || longitude === null) {
|
|
229
220
|
return null;
|
|
230
221
|
}
|
|
231
|
-
return {
|
|
232
|
-
latitude,
|
|
233
|
-
longitude,
|
|
234
|
-
fallbackCity: asCity(geo?.city),
|
|
235
|
-
};
|
|
222
|
+
return { latitude, longitude };
|
|
236
223
|
}
|
|
237
224
|
|
|
238
225
|
async function tryBrowserGeolocation(): Promise<Coordinates | null> {
|
|
@@ -266,23 +253,6 @@ async function tryBrowserGeolocation(): Promise<Coordinates | null> {
|
|
|
266
253
|
});
|
|
267
254
|
}
|
|
268
255
|
|
|
269
|
-
async function reverseGeocodeCity(
|
|
270
|
-
fetchFn: typeof fetch,
|
|
271
|
-
latitude: number,
|
|
272
|
-
longitude: number,
|
|
273
|
-
): Promise<string | undefined> {
|
|
274
|
-
const url =
|
|
275
|
-
`${REVERSE_GEO_URL}?latitude=${encodeURIComponent(String(latitude))}` +
|
|
276
|
-
`&longitude=${encodeURIComponent(String(longitude))}` +
|
|
277
|
-
`&localityLanguage=en`;
|
|
278
|
-
const payload = (await fetchJson(fetchFn, url)) as ReverseGeoPayload | null;
|
|
279
|
-
return (
|
|
280
|
-
asCity(payload?.city) ??
|
|
281
|
-
asCity(payload?.locality) ??
|
|
282
|
-
asCity(payload?.principalSubdivision)
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
256
|
function asNumber(value: unknown): number | null {
|
|
287
257
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
288
258
|
return value;
|
|
@@ -294,17 +264,6 @@ function asNumber(value: unknown): number | null {
|
|
|
294
264
|
return null;
|
|
295
265
|
}
|
|
296
266
|
|
|
297
|
-
function asCity(value: unknown): string | undefined {
|
|
298
|
-
if (typeof value !== 'string') {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
const trimmed = value.trim();
|
|
302
|
-
if (!trimmed || trimmed.toLowerCase() === 'n/a') {
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
return trimmed;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
267
|
async function fetchJson(
|
|
309
268
|
fetchFn: typeof fetch,
|
|
310
269
|
url: string,
|