Bubbly Maps API

TypeScript Functions Reference

Documentation for user and waypoint utility functions

TypeScript Functions Reference

This section documents the TypeScript utility functions used in the Bubbly Maps codebase for handling users and waypoints (wp).

User Functions

calculateUserLevel(xp: number): number

Calculates the user's level based on their total XP (experience points).

Parameters:

  • xp (number) - The user's total experience points

Returns:

  • (number) - The calculated user level

Algorithm:

  • Level 1: 0-99 XP
  • Level 2: 100-249 XP
  • Level 3: 250-499 XP
  • Level 4: 500-999 XP
  • Level 5+: Every 200 XP = 1 level

Example:

import { calculateUserLevel } from '@/lib/user';

const xp = 450;
const level = calculateUserLevel(xp);
console.log(`Level: ${level}`); // Output: Level: 5

const highXp = 1500;
const highLevel = calculateUserLevel(highXp);
console.log(`Level: ${highLevel}`); // Output: Level: 9

Implementation:

export function calculateUserLevel(xp: number): number {
  if (xp < 100) return 1;
  if (xp < 250) return 2;
  if (xp < 500) return 3;
  if (xp < 1000) return 4;
  return Math.floor((xp - 1000) / 200) + 5;
}

canUserCreateWaypoint(xp: number): boolean

Checks if a user has sufficient XP to create a new waypoint.

Parameters:

  • xp (number) - The user's total experience points

Returns:

  • (boolean) - true if user can create waypoints, false otherwise

XP Requirement: 20 XP minimum

Example:

import { canUserCreateWaypoint } from '@/lib/user';

const userXp = 25;
if (canUserCreateWaypoint(userXp)) {
  console.log('User can create waypoints');
} else {
  console.log('User needs more XP');
}

Implementation:

export function canUserCreateWaypoint(xp: number): boolean {
  return xp >= 20;
}

canUserEditWaypoint(xp: number): boolean

Checks if a user has sufficient XP to edit waypoints.

Parameters:

  • xp (number) - The user's total experience points

Returns:

  • (boolean) - true if user can edit waypoints, false otherwise

XP Requirement: 10 XP minimum

Example:

import { canUserEditWaypoint } from '@/lib/user';

const userXp = 15;
if (canUserEditWaypoint(userXp)) {
  console.log('User can edit waypoints');
} else {
  console.log('User needs more XP');
}

Implementation:

export function canUserEditWaypoint(xp: number): boolean {
  return xp >= 10;
}

getUserStats(userId: string): Promise<UserStats>

Fetches comprehensive statistics for a specific user.

Parameters:

  • userId (string) - The user's unique identifier

Returns:

  • Promise<UserStats> - A promise that resolves to user statistics

UserStats Type:

interface UserStats {
  waypointsCreated: number;
  waypointsEdited: number;
  reviewsPosted: number;
  totalXpEarned: number;
  lastActivity: string;
}

Example:

import { getUserStats } from '@/lib/user';

async function displayUserStats(userId: string) {
  try {
    const stats = await getUserStats(userId);
    console.log(`Waypoints Created: ${stats.waypointsCreated}`);
    console.log(`Waypoints Edited: ${stats.waypointsEdited}`);
    console.log(`Reviews Posted: ${stats.reviewsPosted}`);
    console.log(`Total XP: ${stats.totalXpEarned}`);
  } catch (error) {
    console.error('Failed to fetch user stats:', error);
  }
}

awardXP(userId: string, action: XPAction): Promise<void>

Awards XP to a user for performing an action on the platform.

Parameters:

  • userId (string) - The user's unique identifier
  • action (XPAction) - The type of action performed

XPAction Enum:

enum XPAction {
  CREATE_WAYPOINT = 'create_waypoint',    // 50 XP
  EDIT_WAYPOINT = 'edit_waypoint',        // 10 XP
  POST_REVIEW = 'post_review',            // 5 XP
  VERIFIED_BONUS = 'verified_bonus',      // 25 XP
}

Returns:

  • Promise<void> - A promise that resolves when XP is awarded

Example:

import { awardXP, XPAction } from '@/lib/user';

async function onWaypointCreated(userId: string) {
  try {
    await awardXP(userId, XPAction.CREATE_WAYPOINT);
    console.log('User awarded 50 XP for creating a waypoint');
  } catch (error) {
    console.error('Failed to award XP:', error);
  }
}

Waypoint (WP) Functions

validateCoordinates(lat: number, lng: number): boolean

Validates that latitude and longitude coordinates are within valid ranges.

Parameters:

  • lat (number) - Latitude coordinate
  • lng (number) - Longitude coordinate

Returns:

  • (boolean) - true if coordinates are valid, false otherwise

Valid Ranges:

  • Latitude: -90 to 90
  • Longitude: -180 to 180

Example:

import { validateCoordinates } from '@/lib/wp';

const lat = 40.7128;
const lng = -74.0060;

if (validateCoordinates(lat, lng)) {
  console.log('Valid coordinates');
} else {
  console.log('Invalid coordinates');
}

Implementation:

export function validateCoordinates(lat: number, lng: number): boolean {
  return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}

calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number

Calculates the distance between two geographic coordinates using the Haversine formula.

Parameters:

  • lat1 (number) - Latitude of first point
  • lng1 (number) - Longitude of first point
  • lat2 (number) - Latitude of second point
  • lng2 (number) - Longitude of second point

Returns:

  • (number) - Distance in kilometers

Example:

import { calculateDistance } from '@/lib/wp';

const centralPark = { lat: 40.7829, lng: -73.9654 };
const timesSquare = { lat: 40.7580, lng: -73.9855 };

const distance = calculateDistance(
  centralPark.lat,
  centralPark.lng,
  timesSquare.lat,
  timesSquare.lng
);

console.log(`Distance: ${distance.toFixed(2)} km`);

Implementation:

export function calculateDistance(
  lat1: number,
  lng1: number,
  lat2: number,
  lng2: number
): number {
  const R = 6371; // Earth's radius in km
  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(lat1)) *
    Math.cos(toRad(lat2)) *
    Math.sin(dLng / 2) *
    Math.sin(dLng / 2);
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

function toRad(degrees: number): number {
  return degrees * (Math.PI / 180);
}

findNearbyWaypoints(userLat: number, userLng: number, radius: number): Promise<Waypoint[]>

Finds all waypoints within a specified radius of a location.

Parameters:

  • userLat (number) - User's latitude
  • userLng (number) - User's longitude
  • radius (number) - Search radius in kilometers

Returns:

  • Promise<Waypoint[]> - Array of nearby waypoints with distance information

Example:

import { findNearbyWaypoints } from '@/lib/wp';

async function showNearbyFountains(lat: number, lng: number) {
  try {
    const waypoints = await findNearbyWaypoints(lat, lng, 5); // 5km radius
    console.log(`Found ${waypoints.length} fountains within 5km`);
    
    waypoints.forEach(wp => {
      console.log(`${wp.name} - ${wp.distance.toFixed(2)}km away`);
    });
  } catch (error) {
    console.error('Failed to find nearby waypoints:', error);
  }
}

sanitizeWaypointInput(input: WaypointInput): WaypointInput

Sanitizes and validates waypoint input data before saving to the database.

Parameters:

  • input (WaypointInput) - Raw waypoint input data

Returns:

  • (WaypointInput) - Sanitized waypoint input

Sanitization Steps:

  1. Trims whitespace from string fields
  2. Validates coordinate ranges
  3. Removes any script tags or dangerous HTML
  4. Ensures amenities is an array
  5. Validates URL format for image field

Example:

import { sanitizeWaypointInput } from '@/lib/wp';

const rawInput = {
  name: '  Central Fountain  ',
  latitude: 40.7829,
  longitude: -73.9654,
  description: '<script>alert("xss")</script>A great fountain',
  amenities: ['accessible', 'pet-friendly'],
  image: 'https://example.com/image.jpg'
};

const sanitized = sanitizeWaypointInput(rawInput);
console.log(sanitized.name); // "Central Fountain" (trimmed)
console.log(sanitized.description); // "A great fountain" (script removed)

formatWaypointForAPI(waypoint: WaypointDB): WaypointAPI

Formats a waypoint from database format to API response format.

Parameters:

  • waypoint (WaypointDB) - Waypoint data from database

Returns:

  • (WaypointAPI) - Formatted waypoint for API response

Transformations:

  • Converts database column names to camelCase
  • Formats dates to ISO 8601 strings
  • Excludes internal fields
  • Parses JSON fields (amenities)

Example:

import { formatWaypointForAPI } from '@/lib/wp';

const dbWaypoint = {
  id: 42,
  name: 'Central Fountain',
  latitude: 40.7829,
  longitude: -73.9654,
  created_at: new Date('2024-01-15T10:30:00Z'),
  updated_at: new Date('2024-01-15T10:30:00Z'),
  amenities: '["accessible","pet-friendly"]'
};

const apiWaypoint = formatWaypointForAPI(dbWaypoint);
// Returns properly formatted object with:
// - camelCase fields
// - ISO 8601 date strings
// - parsed amenities array

validateAmenities(amenities: string[]): boolean

Validates that amenities are from the allowed list.

Parameters:

  • amenities (string[]) - Array of amenity strings

Returns:

  • (boolean) - true if all amenities are valid, false otherwise

Allowed Amenities:

  • 'accessible' - Wheelchair accessible
  • 'pet-friendly' - Pets allowed
  • 'filtered' - Has water filtration
  • 'indoor' - Indoor location
  • 'outdoor' - Outdoor location
  • 'bottle-filler' - Has bottle filling station
  • 'cold-water' - Provides cold water
  • '24-7' - Available 24/7

Example:

import { validateAmenities } from '@/lib/wp';

const validAmenities = ['accessible', 'filtered', 'cold-water'];
const invalidAmenities = ['accessible', 'invalid-amenity'];

console.log(validateAmenities(validAmenities)); // true
console.log(validateAmenities(invalidAmenities)); // false

searchWaypoints(query: string): Promise<Waypoint[]>

Searches for waypoints by name, description, or region.

Parameters:

  • query (string) - Search query string

Returns:

  • Promise<Waypoint[]> - Array of matching waypoints

Search Features:

  • Case-insensitive
  • Searches name, description, and region fields
  • Returns results sorted by relevance

Example:

import { searchWaypoints } from '@/lib/wp';

async function search(term: string) {
  try {
    const results = await searchWaypoints(term);
    console.log(`Found ${results.length} waypoints matching "${term}"`);
    
    results.forEach(wp => {
      console.log(`- ${wp.name} (${wp.region})`);
    });
  } catch (error) {
    console.error('Search failed:', error);
  }
}

search('park');

Type Definitions

User Types

interface User {
  id: string;
  name: string;
  email: string;
  xp: number;
  level: number;
  isModerator: boolean;
  createdAt: string;
  updatedAt: string;
  stats: UserStats;
}

interface UserStats {
  waypointsCreated: number;
  waypointsEdited: number;
  reviewsPosted: number;
  totalXpEarned: number;
  lastActivity: string;
}

enum XPAction {
  CREATE_WAYPOINT = 'create_waypoint',
  EDIT_WAYPOINT = 'edit_waypoint',
  POST_REVIEW = 'post_review',
  VERIFIED_BONUS = 'verified_bonus',
}

Waypoint Types

interface Waypoint {
  id: number;
  name: string;
  latitude: number;
  longitude: number;
  description?: string;
  amenities: string[];
  image?: string;
  maintainer?: string;
  region?: string;
  approved: boolean;
  verified: boolean;
  addedByUserId: string;
  createdAt: string;
  updatedAt: string;
  distance?: number; // Optional, added by proximity searches
}

interface WaypointInput {
  name: string;
  latitude: number;
  longitude: number;
  description?: string;
  amenities?: string[];
  image?: string;
  maintainer?: string;
  region?: string;
}

interface WaypointDB {
  id: number;
  name: string;
  latitude: number;
  longitude: number;
  description: string | null;
  amenities: string; // JSON string
  image: string | null;
  maintainer: string | null;
  region: string | null;
  approved: boolean;
  verified: boolean;
  added_by_user_id: string;
  created_at: Date;
  updated_at: Date;
}

interface WaypointAPI {
  id: number;
  name: string;
  latitude: number;
  longitude: number;
  description?: string;
  amenities: string[];
  image?: string;
  maintainer?: string;
  region?: string;
  approved: boolean;
  verified: boolean;
  addedByUserId: string;
  createdAt: string;
  updatedAt: string;
}

Best Practices

Using User Functions

  1. Always check XP requirements before allowing actions:
if (canUserCreateWaypoint(user.xp)) {
  // Allow waypoint creation
} else {
  // Show error message about XP requirement
}
  1. Award XP immediately after successful actions:
await createWaypoint(data);
await awardXP(userId, XPAction.CREATE_WAYPOINT);
  1. Calculate level dynamically to avoid stale data:
const level = calculateUserLevel(user.xp);

Using Waypoint Functions

  1. Always validate coordinates before database operations:
if (!validateCoordinates(lat, lng)) {
  throw new Error('Invalid coordinates');
}
  1. Sanitize all user input before saving:
const sanitized = sanitizeWaypointInput(userInput);
await saveWaypoint(sanitized);
  1. Validate amenities to ensure data consistency:
if (!validateAmenities(input.amenities)) {
  throw new Error('Invalid amenities provided');
}
  1. Use distance calculations for proximity features:
const distance = calculateDistance(userLat, userLng, wpLat, wpLng);
if (distance <= 5) {
  // Waypoint is within 5km
}

Error Handling

All functions that interact with external resources (database, API) should be wrapped in try-catch blocks:

try {
  const stats = await getUserStats(userId);
  // Use stats
} catch (error) {
  console.error('Failed to fetch user stats:', error);
  // Handle error appropriately
}

For validation functions, check return values:

if (!validateCoordinates(lat, lng)) {
  return res.status(400).json({ error: 'Invalid coordinates' });
}

On this page