← back to Dear Bubbe Nextjs

lib/local-content-handler.ts

389 lines

/**
 * Local Content Handler for Super Max Tier
 * 
 * Integrates with existing news, weather, and sports APIs
 * but adds hyper-local personalization and Bubbe commentary
 */

import { FeatureKey } from './bubbe-pricing';
import { canUserAccessFeature, getUserSession } from './user-session';

// City to team mapping for local sports
const CITY_SPORTS_TEAMS: Record<string, {
  nba?: string[];
  nfl?: string[];
  mlb?: string[];
  nhl?: string[];
}> = {
  'new york': {
    nba: ['knicks', 'nets'],
    nfl: ['giants', 'jets'],
    mlb: ['yankees', 'mets'],
    nhl: ['rangers', 'islanders', 'devils']
  },
  'los angeles': {
    nba: ['lakers', 'clippers'],
    nfl: ['rams', 'chargers'],
    mlb: ['dodgers', 'angels'],
    nhl: ['kings', 'ducks']
  },
  'chicago': {
    nba: ['bulls'],
    nfl: ['bears'],
    mlb: ['cubs', 'white sox'],
    nhl: ['blackhawks']
  },
  'boston': {
    nba: ['celtics'],
    nfl: ['patriots'],
    mlb: ['red sox'],
    nhl: ['bruins']
  },
  'philadelphia': {
    nba: ['76ers'],
    nfl: ['eagles'],
    mlb: ['phillies'],
    nhl: ['flyers']
  },
  'san francisco': {
    nba: ['warriors'],
    nfl: ['49ers'],
    mlb: ['giants'],
    nhl: ['sharks']
  },
  'miami': {
    nba: ['heat'],
    nfl: ['dolphins'],
    mlb: ['marlins'],
    nhl: ['panthers']
  },
  'dallas': {
    nba: ['mavericks'],
    nfl: ['cowboys'],
    mlb: ['rangers'],
    nhl: ['stars']
  },
  'denver': {
    nba: ['nuggets'],
    nfl: ['broncos'],
    mlb: ['rockies'],
    nhl: ['avalanche']
  },
  'atlanta': {
    nba: ['hawks'],
    nfl: ['falcons'],
    mlb: ['braves'],
    nhl: []
  }
};

// Get user's location from IP or headers
export async function getUserLocation(req: any): Promise<{
  city?: string;
  state?: string;
  country?: string;
  latitude?: number;
  longitude?: number;
}> {
  // Try to get from existing geolocation in your system
  // This assumes you already have IP-based location detection
  // as mentioned in your existing code
  
  const ip = req.headers?.['x-forwarded-for'] || 
             req.connection?.remoteAddress || 
             '45.61.58.125'; // Default to server IP
  
  try {
    // Use existing geolocation API (Open-Meteo compatible)
    const response = await fetch(`https://ipapi.co/${ip}/json/`);
    const data = await response.json();
    
    return {
      city: data.city?.toLowerCase(),
      state: data.region,
      country: data.country_name,
      latitude: data.latitude,
      longitude: data.longitude
    };
  } catch (error) {
    console.error('Failed to get location:', error);
    // Default to Los Angeles (your existing default)
    return {
      city: 'los angeles',
      state: 'CA',
      country: 'United States',
      latitude: 34.0522,
      longitude: -118.2437
    };
  }
}

/**
 * Get local news with Bubbe commentary
 */
export async function getLocalNews(userId: string, city: string): Promise<{
  allowed: boolean;
  data?: any;
  bubbeResponse?: string;
}> {
  // Check if user has access to local news
  const access = canUserAccessFeature(userId, 'local_news');
  if (!access.allowed) {
    return { allowed: false, bubbeResponse: access.bubbeResponse };
  }
  
  try {
    // Call your existing news endpoint but filter for local
    const response = await fetch('http://localhost:3011/api/news', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        source: 'local',
        city: city 
      })
    });
    
    const newsData = await response.json();
    
    // Add Bubbe's hot takes on local news
    const bubbeCommentary = addBubbeNewsCommentary(newsData, city);
    
    return {
      allowed: true,
      data: {
        ...newsData,
        bubbeCommentary,
        localizedFor: city
      }
    };
  } catch (error) {
    return {
      allowed: true,
      data: null,
      bubbeResponse: `Oy vey, the news from ${city}? All bad! Trust me, you don't want to know. But since you're paying $54 a month, I'll find out for you!`
    };
  }
}

/**
 * Get local sports with Bubbe commentary
 */
export async function getLocalSports(userId: string, city: string): Promise<{
  allowed: boolean;
  data?: any;
  bubbeResponse?: string;
}> {
  const access = canUserAccessFeature(userId, 'local_sports');
  if (!access.allowed) {
    return { allowed: false, bubbeResponse: access.bubbeResponse };
  }
  
  // Get local teams for this city
  const teams = CITY_SPORTS_TEAMS[city.toLowerCase()] || {};
  const allTeams = [
    ...(teams.nba || []),
    ...(teams.nfl || []),
    ...(teams.mlb || []),
    ...(teams.nhl || [])
  ];
  
  if (allTeams.length === 0) {
    return {
      allowed: true,
      bubbeResponse: `${city}? What teams? You live in a sports desert! Move to New York like a normal person!`
    };
  }
  
  // Generate Bubbe commentary on local teams
  const teamCommentary: Record<string, string> = {};
  for (const team of allTeams) {
    teamCommentary[team] = getBubbeTeamCommentary(team);
  }
  
  return {
    allowed: true,
    data: {
      city,
      teams,
      commentary: teamCommentary,
      bubbesSummary: `Your teams? Feh! The ${allTeams[0]} should be ashamed! For $54 a month I watch them lose so you don't have to!`
    }
  };
}

/**
 * Get hyper-local weather with Bubbe guilt
 */
export async function getLocalWeather(userId: string, lat: number, lon: number, city: string): Promise<{
  allowed: boolean;
  data?: any;
  bubbeResponse?: string;
}> {
  const access = canUserAccessFeature(userId, 'local_weather');
  if (!access.allowed) {
    return { allowed: false, bubbeResponse: access.bubbeResponse };
  }
  
  try {
    // Use your existing weather API but with exact coordinates
    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m,relative_humidity_2m&temperature_unit=fahrenheit&timezone=auto`;
    
    const response = await fetch(weatherUrl);
    const weatherData = await response.json();
    
    // Add Bubbe's weather guilt
    const temp = weatherData.current.temperature_2m;
    const feelsLike = weatherData.current.apparent_temperature;
    
    let bubbeWeatherGuilt = '';
    if (temp < 50) {
      bubbeWeatherGuilt = `It's ${temp}°F in ${city}! FREEZING! Where's your coat? You'll catch pneumonia! And then who'll take care of you? Not me!`;
    } else if (temp < 65) {
      bubbeWeatherGuilt = `${temp}°F in ${city}? That's jacket weather! Don't come crying to me when you get sick!`;
    } else if (temp > 85) {
      bubbeWeatherGuilt = `Oy gevalt, ${temp}°F in ${city}! You're sweating like a schmuck! Drink water before you pass out!`;
    } else {
      bubbeWeatherGuilt = `${temp}°F in ${city}, feels like ${feelsLike}°F. Finally, decent weather! But you're probably inside playing video games anyway!`;
    }
    
    return {
      allowed: true,
      data: {
        ...weatherData,
        city,
        bubbeWeatherGuilt,
        clothingAdvice: getClothingAdvice(temp),
        localizedFor: `${city} (your exact neighborhood!)`
      }
    };
  } catch (error) {
    return {
      allowed: true,
      bubbeResponse: `The weather in ${city}? Look outside, genius! For $54 a month I shouldn't have to tell you it's raining!`
    };
  }
}

/**
 * Add Bubbe commentary to news items
 */
function addBubbeNewsCommentary(newsData: any, city: string): string[] {
  const commentary = [
    `In ${city}? Everything's meshuga! The mayor's a putz, the traffic's terrible, and don't get me started on the rent!`,
    `You want LOCAL news? Your neighbor's dog won't stop barking, the deli raised prices AGAIN, and that new restaurant? Terrible!`,
    `${city} news? Oy vey! Construction everywhere, potholes like craters, and the schools? Don't make me laugh!`,
    `Breaking news from ${city}: Everything costs too much, nobody knows how to drive, and the weather's always wrong!`
  ];
  
  return commentary;
}

/**
 * Get Bubbe's take on specific teams
 */
function getBubbeTeamCommentary(team: string): string {
  const commentary: Record<string, string> = {
    // New York
    'yankees': "27 championships and they STILL disappoint me! For what they pay those players?",
    'mets': "The Mets? Oy vey, why do you do this to yourself? Such suffering!",
    'knicks': "The Knicks? Every year 'this is our year.' Every year I'm disappointed!",
    'nets': "The Nets? They had everyone and won NOTHING! Such a waste!",
    'giants': "The Giants? Living in the past! Those Super Bowls were a lifetime ago!",
    'jets': "The Jets? Don't make me laugh! Or cry. Actually, both!",
    
    // Los Angeles  
    'lakers': "The Lakers? They buy everyone and still can't win! LeBron's older than ME!",
    'clippers': "The Clippers? Always second best in their own city! Such a shanda!",
    'dodgers': "The Dodgers spend like drunken sailors! For what? One championship?",
    
    // Chicago
    'bulls': "The Bulls? Michael Jordan retired 30 years ago! Move on already!",
    'bears': "The Bears? Still living off 1985! That was before you were born!",
    'cubs': "The Cubs? One championship and they think they're special!",
    
    // Boston
    'celtics': "The Celtics? So many banners they're running out of room! Show-offs!",
    'red sox': "The Red Sox? Fenway's falling apart like my knees!",
    'patriots': "The Patriots? Brady left and took the magic with him!",
    
    // Default
    'default': "That team? Feh! Overpaid schmucks who can't win when it matters!"
  };
  
  return commentary[team.toLowerCase()] || commentary['default'];
}

/**
 * Get clothing advice based on temperature
 */
function getClothingAdvice(temp: number): string {
  if (temp < 32) return "Bundle up like an Eskimo! Coat, scarf, gloves, hat - EVERYTHING!";
  if (temp < 50) return "Wear a proper coat! Not that thin shmate you call a jacket!";
  if (temp < 65) return "At least bring a sweater! You'll thank me later!";
  if (temp < 75) return "Perfect weather and you'll probably wear shorts like a meshuggeneh!";
  if (temp < 85) return "Dress light but bring sunscreen! Skin cancer is no joke!";
  return "It's HOT! Wear nothing if you want, but stay hydrated!";
}

/**
 * Check if user has Super Max and can access all local features
 */
export function hasSuperMaxAccess(userId: string): boolean {
  const session = getUserSession(userId);
  return session.plan === 'supermax';
}

/**
 * Generate a localized response based on user's area
 */
export async function getPersonalizedLocalResponse(
  userId: string,
  message: string,
  req: any
): Promise<{
  allowed: boolean;
  response?: string;
  localData?: any;
}> {
  // Check Super Max access
  if (!hasSuperMaxAccess(userId)) {
    const access = canUserAccessFeature(userId, 'personalized_insights');
    if (!access.allowed) {
      return { allowed: false, response: access.bubbeResponse };
    }
  }
  
  // Get user's location
  const location = await getUserLocation(req);
  const city = location.city || 'your city';
  
  // Determine what local content they're asking for
  const lowerMessage = message.toLowerCase();
  
  if (lowerMessage.includes('news')) {
    return getLocalNews(userId, city);
  }
  
  if (lowerMessage.includes('sport') || lowerMessage.includes('team')) {
    return getLocalSports(userId, city);
  }
  
  if (lowerMessage.includes('weather') || lowerMessage.includes('temperature')) {
    return getLocalWeather(userId, location.latitude!, location.longitude!, city);
  }
  
  // General local insight
  return {
    allowed: true,
    response: `You want to know about ${city}? Oy, where do I start! The traffic's terrible, everything's expensive, the people are rude - but at least it's not Cleveland! For $54 a month I know EVERYTHING about your neighborhood. The best deli? Goldstein's on 3rd. The gossip? Mrs. Schwartz is dating the mailman! Want more? Just ask!`,
    localData: {
      city,
      state: location.state,
      country: location.country,
      coordinates: {
        latitude: location.latitude,
        longitude: location.longitude
      }
    }
  };
}