Skip to main content
The 200 Mates application is built using a modular vanilla JavaScript architecture with no build step required. This design choice prioritizes simplicity, accessibility, and ease of contribution.

Core Architecture Principles

Modular Design

Separate modules for distinct features (globe, form, gallery)

No Build Step

Pure vanilla JavaScript - no transpilation or bundling

Static Hosting

Deployed on GitHub Pages as static HTML/CSS/JS

Real-Time Data

Supabase backend for data storage and retrieval

Application Flow

The application follows a straightforward initialization and rendering pipeline:
app.js
// 1. Initialize i18n
applyI18n();

// 2. Start GPS detection
initGPS();

// 3. Load mate data from Supabase
loadMates();

// 4. Refresh data every 30 seconds
setInterval(loadMates, 30_000);
1

Page Load

HTML loads and initializes the DOM structure with the globe container, form, and gallery elements.
2

Script Loading

External libraries load first (Bootstrap, Supabase, Globe.gl, TopoJSON), followed by data files (i18n, constants, ISO tables, countries), then modules, and finally app.js.
3

State Initialization

Global state variables are initialized in modules/state.js including allMates[], countriesColored[], and countriesGeo[].
4

Data Fetching

loadMates() fetches approved mate submissions from Supabase and triggers rendering functions.
5

Rendering

Three parallel render pipelines execute:
  • renderPolygons() - Colors countries on the globe
  • renderMarkers() - Places mate markers with jitter
  • renderArcs() - Animates connections between mates
  • renderGallery() - Displays recent submissions

Module System

The application is divided into eight distinct modules, each with a specific responsibility:

Data Flow Diagram

┌─────────────┐
│  Supabase   │
│  Database   │
└──────┬──────┘


┌─────────────┐      ┌──────────────┐
│   app.js    │──────→│   state.js   │ (Global State)
│ loadMates() │      └──────┬───────┘
└─────────────┘             │
       │                    │
       ↓                    ↓
   ┌───────────────────────────────┐
   │                               │
   ↓                               ↓
┌────────┐  ┌─────────┐  ┌──────────┐  ┌─────────┐
│ globe  │  │ gallery │  │   form   │  │  menu   │
└────────┘  └─────────┘  └──────────┘  └─────────┘
     │           │             │             │
     ↓           ↓             ↓             ↓
  Polygon     Recent        Submit       Dropdown
  Rendering   Mates         New Mate     Navigation
  Markers     Display       with Photo   & Tabs
  Arcs        Lightbox      & GPS data

State Management

All global application state is centralized in modules/state.js:
state.js
let allMates         = [];  // All approved mate submissions
let countriesColored = [];  // ISO3 codes of countries with mates
let countriesGeo     = [];  // GeoJSON features for globe polygons
let arcInterval      = null; // Timer for arc animations
let polyTimer        = null; // Debounce timer for polygon rendering
let lastMarkerIds    = "";   // Cache key for marker rendering
let currentLang      = "es"; // Active language (es/en/pt)
The state module uses let declarations to allow updates from any module. While this creates global mutable state, it keeps the architecture simple and avoids the complexity of a state management library.

Rendering Pipeline

1. Polygon Rendering

Countries are colored based on whether they have mate submissions:
globe.js:74
function renderPolygons() {
  if (!countriesGeo.length) return;
  if (polyTimer) clearTimeout(polyTimer);
  polyTimer = setTimeout(() => {
    const colored = [...countriesColored], sel = selectedIso3;
    globe
      .polygonsData([])
      .polygonCapColor(d =>
        sel && d.iso3 === sel    ? "#c8a46e" :  // Selected: gold
        colored.includes(d.iso3) ? "#6A8F60" :  // Has mates: green
        "rgba(255,255,255,.04)"                  // Empty: transparent
      )
      .polygonAltitude(d =>
        sel && d.iso3 === sel    ? .02 :
        colored.includes(d.iso3) ? .012 :
        0
      )
      .polygonsData(countriesGeo.slice());
  }, 60);
}

2. Marker Rendering with Jitter

Mate markers are dispersed using a jitter algorithm to prevent overlap:
globe.js:103
function applyJitter(mates) {
  const groups = {};
  mates.forEach((m, i) => {
    const k = `${m.lat.toFixed(1)},${m.lng.toFixed(1)}`;
    if (!groups[k]) groups[k] = [];
    groups[k].push({ ...m, _uid: `${m.id}_${i}` });
  });
  const out = [];
  Object.values(groups).forEach(g => {
    if (g.length === 1) { out.push(g[0]); return; }
    g.forEach((m, i) => {
      const a = (i / g.length) * 2 * Math.PI;
      out.push({ 
        ...m, 
        lat: m.lat + 1.2 * Math.cos(a), 
        lng: m.lng + 1.2 * Math.sin(a) 
      });
    });
  });
  return out;
}
The jitter algorithm groups markers by rounded coordinates (0.1° precision) and arranges overlapping markers in a circle with 1.2° radius. This ensures all markers remain visible and clickable.

3. Arc Animation System

Arcs connect consecutive mate submissions in an animated sequence:
globe.js:192
function renderArcs(mates) {
  if (arcInterval) { clearInterval(arcInterval); arcInterval = null; }
  if (mates.length < 2) { globe.arcsData([]); return; }
  let i = 0;
  function next() {
    const idx = i % (mates.length - 1);
    globe
      .arcsData([{ 
        startLat: mates[idx].lat, 
        startLng: mates[idx].lng, 
        endLat: mates[idx+1].lat, 
        endLng: mates[idx+1].lng 
      }])
      .arcColor(() => ["rgba(106,143,96,.6)", "rgba(200,164,110,.6)"])
      .arcAltitudeAutoScale(0.4)
      .arcStroke(0.5)
      .arcDashLength(0.4)
      .arcDashGap(0.2)
      .arcDashAnimateTime(2500);
    i++;
  }
  next();
  arcInterval = setInterval(next, 2500);
}

Performance Considerations

Polygon rendering is debounced with a 60ms timeout to prevent excessive redraws during rapid state changes.
Markers are only re-rendered when the lastMarkerIds cache key changes, preventing unnecessary DOM manipulation.
Only one arc is rendered at a time, cycling through connections every 2.5 seconds to minimize GPU load.
Data refreshes every 30 seconds using setInterval, striking a balance between freshness and server load.

Technology Stack

ComponentTechnologyPurpose
FrontendVanilla JavaScriptCore application logic
3D GlobeGlobe.glWebGL-based globe visualization
UI FrameworkBootstrap 5.3Responsive layout and components
BackendSupabasePostgreSQL database + auth + storage
Geo DataWorld Atlas TopoJSONCountry boundaries
HostingGitHub PagesStatic site deployment
FontsDM Sans (Google Fonts)Typography

Next Steps

Module Details

Explore each module’s functions and responsibilities

Globe Visualization

Deep dive into the Globe.gl integration

Data Structures

Understand the data models and formats

Supabase Integration

Learn about the backend setup

Build docs developers (and LLMs) love