|
| 1 | +import { NextRequest, NextResponse } from "next/server"; |
| 2 | + |
| 3 | +export const runtime = "edge"; |
| 4 | + |
| 5 | +type MapboxFeature = { |
| 6 | + place_name?: string; |
| 7 | + text?: string; |
| 8 | + center?: [number, number]; |
| 9 | + context?: Array<{ id?: string; text?: string }>; |
| 10 | + properties?: Record<string, unknown>; |
| 11 | +}; |
| 12 | + |
| 13 | +function toText(value: unknown): string { |
| 14 | + return typeof value === "string" ? value.trim() : ""; |
| 15 | +} |
| 16 | + |
| 17 | +function extractContextText(feature: MapboxFeature, prefix: string): string { |
| 18 | + const ctx = Array.isArray(feature.context) ? feature.context : []; |
| 19 | + const match = ctx.find((item) => String(item?.id || "").startsWith(prefix)); |
| 20 | + return toText(match?.text); |
| 21 | +} |
| 22 | + |
| 23 | +export async function GET(req: NextRequest) { |
| 24 | + const { searchParams } = new URL(req.url); |
| 25 | + const query = toText(searchParams.get("q")); |
| 26 | + |
| 27 | + if (query.length < 3) { |
| 28 | + return NextResponse.json({ ok: true, configured: true, provider: "mapbox", suggestions: [] }); |
| 29 | + } |
| 30 | + |
| 31 | + const provider = toText(process.env.GARDEN_ADDRESS_AUTOCOMPLETE_PROVIDER || "mapbox").toLowerCase(); |
| 32 | + const mapboxToken = toText(process.env.GARDEN_MAPBOX_ACCESS_TOKEN || process.env.NEXT_PUBLIC_GARDEN_MAPBOX_TOKEN); |
| 33 | + |
| 34 | + if (provider !== "mapbox" || !mapboxToken) { |
| 35 | + return NextResponse.json({ |
| 36 | + ok: true, |
| 37 | + configured: false, |
| 38 | + provider: provider || null, |
| 39 | + suggestions: [], |
| 40 | + reason: "Address autocomplete provider is not configured. Manual address entry is still available." |
| 41 | + }); |
| 42 | + } |
| 43 | + |
| 44 | + try { |
| 45 | + const url = new URL(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json`); |
| 46 | + url.searchParams.set("access_token", mapboxToken); |
| 47 | + url.searchParams.set("autocomplete", "true"); |
| 48 | + url.searchParams.set("limit", "6"); |
| 49 | + url.searchParams.set("country", "ca"); |
| 50 | + url.searchParams.set("types", "address,place,postcode,neighborhood,locality"); |
| 51 | + |
| 52 | + const response = await fetch(url.toString(), { |
| 53 | + headers: { |
| 54 | + "User-Agent": "GardenCleanersAddressAutocomplete/1.0" |
| 55 | + } |
| 56 | + }); |
| 57 | + |
| 58 | + if (!response.ok) { |
| 59 | + return NextResponse.json({ ok: false, configured: true, provider: "mapbox", suggestions: [] }, { status: 502 }); |
| 60 | + } |
| 61 | + |
| 62 | + const body = (await response.json()) as { features?: MapboxFeature[] }; |
| 63 | + const features = Array.isArray(body.features) ? body.features : []; |
| 64 | + |
| 65 | + const suggestions = features.map((feature, index) => { |
| 66 | + const city = extractContextText(feature, "place") || extractContextText(feature, "locality"); |
| 67 | + const region = extractContextText(feature, "region"); |
| 68 | + const postalCode = extractContextText(feature, "postcode"); |
| 69 | + const center = Array.isArray(feature.center) ? feature.center : []; |
| 70 | + const longitude = Number(center[0]); |
| 71 | + const latitude = Number(center[1]); |
| 72 | + |
| 73 | + return { |
| 74 | + id: `${index}-${toText(feature.place_name || feature.text || "address")}`, |
| 75 | + label: toText(feature.place_name || feature.text || "Address"), |
| 76 | + address: toText(feature.text || feature.place_name || ""), |
| 77 | + city, |
| 78 | + region, |
| 79 | + postalCode, |
| 80 | + latitude: Number.isFinite(latitude) ? latitude : null, |
| 81 | + longitude: Number.isFinite(longitude) ? longitude : null |
| 82 | + }; |
| 83 | + }); |
| 84 | + |
| 85 | + return NextResponse.json({ ok: true, configured: true, provider: "mapbox", suggestions }); |
| 86 | + } catch { |
| 87 | + return NextResponse.json({ ok: true, configured: true, provider: "mapbox", suggestions: [] }); |
| 88 | + } |
| 89 | +} |
0 commit comments