Replace entire repo content with code from /root/shahikitchen-google/

This commit is contained in:
root
2026-07-03 02:51:15 +00:00
parent c8b7dc95e4
commit 8ccf033329
79 changed files with 7611 additions and 439 deletions
@@ -0,0 +1,121 @@
import { isGothenburgDeliveryAddress } from '@/domain/delivery/gothenburg-address';
import {
disableGeoapifyAutocomplete,
isGeoapifyApiKeyConfigured,
isGeoapifyAutocompleteDisabled,
mapGeoapifyHttpStatus,
type GeoapifyUnavailableReason,
} from './geoapify-availability';
import {
getCachedGeoapifyFeatures,
getInFlightGeoapifyRequest,
setCachedGeoapifyFeatures,
trackInFlightGeoapifyRequest,
waitForGeoapifyRateLimit,
} from './geoapify-request-cache';
import type { GeoapifyAutocompleteResponse, GeoapifyFeature } from './geoapify-types';
/** Backaplan branch — bias autocomplete toward Gothenburg. */
const GOTHENBURG_BIAS = 'proximity:11.944,57.699';
/** Approximate Göteborg municipality bounding box (minLon,minLat,maxLon,maxLat). */
const GOTHENBURG_RECT_FILTER = 'rect:11.74,57.60,12.15,57.82';
export type GeoapifyAutocompleteResult =
| { status: 'ok'; features: GeoapifyFeature[] }
| { status: 'unavailable'; reason: GeoapifyUnavailableReason; features: [] };
export function isDeliverableGeoapifyFeature(feature: GeoapifyFeature): boolean {
const { properties } = feature;
return isGothenburgDeliveryAddress({
city: properties.city,
municipality: properties.municipality,
postcode: properties.postcode,
countryCode: properties.country_code,
formatted: properties.formatted,
});
}
export function shouldUseGeoapifyAutocomplete(): boolean {
return isGeoapifyApiKeyConfigured() && !isGeoapifyAutocompleteDisabled();
}
function unavailable(reason: GeoapifyUnavailableReason): GeoapifyAutocompleteResult {
disableGeoapifyAutocomplete();
return { status: 'unavailable', reason, features: [] };
}
async function requestGeoapifyAddressSuggestions(
query: string,
lang: 'sv' | 'en',
): Promise<GeoapifyFeature[]> {
const cached = getCachedGeoapifyFeatures(query, lang);
if (cached) return cached;
const inFlight = getInFlightGeoapifyRequest(query, lang);
if (inFlight) return inFlight;
const request = (async () => {
await waitForGeoapifyRateLimit();
const apiKey = process.env.NEXT_PUBLIC_GEOAPIFY_API_KEY!.trim();
const params = new URLSearchParams({
text: query.trim(),
format: 'geojson',
lang,
limit: '8',
filter: `countrycode:se|${GOTHENBURG_RECT_FILTER}`,
bias: GOTHENBURG_BIAS,
apiKey,
});
const response = await fetch(
`https://api.geoapify.com/v1/geocode/autocomplete?${params.toString()}`,
);
if (!response.ok) {
throw new GeoapifyRequestError(mapGeoapifyHttpStatus(response.status));
}
const data = (await response.json()) as GeoapifyAutocompleteResponse;
const features = (data.features ?? []).filter(isDeliverableGeoapifyFeature);
setCachedGeoapifyFeatures(query, lang, features);
return features;
})();
return trackInFlightGeoapifyRequest(query, lang, request);
}
class GeoapifyRequestError extends Error {
constructor(public readonly reason: GeoapifyUnavailableReason) {
super(reason);
this.name = 'GeoapifyRequestError';
}
}
export async function fetchGeoapifyAddressSuggestions(
query: string,
lang: 'sv' | 'en' = 'sv',
): Promise<GeoapifyAutocompleteResult> {
if (!isGeoapifyApiKeyConfigured()) {
return unavailable('missing_key');
}
if (isGeoapifyAutocompleteDisabled()) {
return unavailable('quota_exceeded');
}
if (query.trim().length < 3) {
return { status: 'ok', features: [] };
}
try {
const features = await requestGeoapifyAddressSuggestions(query, lang);
return { status: 'ok', features };
} catch (error) {
if (error instanceof GeoapifyRequestError) {
return unavailable(error.reason);
}
return unavailable('network');
}
}