← back to Dear Bubbe Nextjs

app/api/location/route.ts

195 lines

import { NextRequest, NextResponse } from 'next/server'

// Free IP geolocation API
async function getLocationFromIP(ip: string) {
  try {
    const response = await fetch(`http://ip-api.com/json/${ip}`)
    const data = await response.json()
    
    if (data.status === 'success') {
      return {
        city: data.city,
        state: data.regionName,
        stateCode: data.region,
        country: data.country,
        countryCode: data.countryCode,
        zip: data.zip,
        lat: data.lat,
        lon: data.lon
      }
    }
  } catch (error) {
    console.error('[LOCATION] IP lookup failed:', error)
  }
  return null
}

// Free reverse geocoding using Nominatim (OpenStreetMap)
async function getLocationFromCoords(lat: number, lon: number) {
  try {
    const response = await fetch(
      `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`,
      {
        headers: {
          'User-Agent': 'BubbeAI/1.0'
        }
      }
    )
    const data = await response.json()
    
    if (data && data.address) {
      return {
        city: data.address.city || data.address.town || data.address.village || data.address.suburb,
        state: data.address.state,
        stateCode: data.address.state_code,
        country: data.address.country,
        countryCode: data.address.country_code?.toUpperCase(),
        zip: data.address.postcode,
        lat: parseFloat(data.lat),
        lon: parseFloat(data.lon)
      }
    }
  } catch (error) {
    console.error('[LOCATION] Reverse geocoding failed:', error)
  }
  return null
}

// ZIP code lookup using Zippopotam.us (free, no API key required)
async function getLocationFromZip(zip: string, countryCode: string = 'us') {
  try {
    // Clean the ZIP code
    const cleanZip = zip.replace(/\D/g, '')
    
    const response = await fetch(`http://api.zippopotam.us/${countryCode}/${cleanZip}`)
    if (response.ok) {
      const data = await response.json()
      
      if (data && data.places && data.places.length > 0) {
        const place = data.places[0]
        return {
          city: place['place name'],
          state: place['state'],
          stateCode: place['state abbreviation'],
          country: data['country'],
          countryCode: data['country abbreviation'],
          zip: data['post code'],
          lat: parseFloat(place['latitude']),
          lon: parseFloat(place['longitude'])
        }
      }
    }
  } catch (error) {
    console.error('[LOCATION] ZIP lookup failed:', error)
  }
  return null
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { type, lat, lon, zip, countryCode } = body
    
    let location = null
    
    switch (type) {
      case 'gps':
        // User provided GPS coordinates
        if (lat && lon) {
          location = await getLocationFromCoords(lat, lon)
        }
        break
        
      case 'zip':
        // User provided ZIP code
        if (zip) {
          location = await getLocationFromZip(zip, countryCode || 'us')
        }
        break
        
      case 'ip':
      default:
        // Fallback to IP-based location
        const forwardedFor = request.headers.get('x-forwarded-for')
        const realIp = request.headers.get('x-real-ip')
        const ip = forwardedFor?.split(',')[0] || realIp || ''
        
        if (ip && ip !== 'unknown') {
          location = await getLocationFromIP(ip)
        }
        break
    }
    
    // If we got a location, format it nicely
    if (location) {
      // Add Bubbe's commentary about the location
      let bubbeComment = ''
      
      if (location.city) {
        const city = location.city.toLowerCase()
        if (city.includes('new york')) {
          bubbeComment = "Finally, someone who lives in a real city!"
        } else if (city.includes('los angeles') || city.includes('beverly hills')) {
          bubbeComment = "LA? All that sunshine and still no spouse?"
        } else if (city.includes('miami') || location.state?.toLowerCase().includes('florida')) {
          bubbeComment = "Florida? That's where people go to retire, not to live!"
        } else if (city.includes('chicago')) {
          bubbeComment = "Chicago? It's so cold there, no wonder you're miserable!"
        } else if (city.includes('boston')) {
          bubbeComment = "Boston? At least they have good schools there."
        } else if (city.includes('san francisco')) {
          bubbeComment = "San Francisco? With those prices, you better be making good money!"
        } else {
          bubbeComment = `${location.city}? Never heard of it. Move to New York!`
        }
      }
      
      return NextResponse.json({
        success: true,
        location: {
          ...location,
          bubbeComment
        }
      })
    }
    
    return NextResponse.json({
      success: false,
      error: 'Could not determine location',
      location: null
    })
    
  } catch (error) {
    console.error('[LOCATION] Error:', error)
    return NextResponse.json(
      { 
        success: false,
        error: 'Failed to get location' 
      },
      { status: 500 }
    )
  }
}

// GET endpoint for testing
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const zip = searchParams.get('zip')
  
  if (zip) {
    const location = await getLocationFromZip(zip)
    return NextResponse.json({ location })
  }
  
  // Get location from IP
  const forwardedFor = request.headers.get('x-forwarded-for')
  const realIp = request.headers.get('x-real-ip')
  const ip = forwardedFor?.split(',')[0] || realIp || ''
  
  if (ip) {
    const location = await getLocationFromIP(ip)
    return NextResponse.json({ location })
  }
  
  return NextResponse.json({ error: 'No location data available' })
}