Examples & Recipes
Real-world implementation patterns and code examples for common Atlas use cases.
Data Adapter Examples
Static JSON Adapter
The simplest setup using a local JSON file.
import type { MapConfig } from './src/core/config';
const config: Partial<MapConfig> = {
dataSources: [
{
id: 'local-events',
name: 'Local Data',
description: 'Events from static JSON file',
adapter: 'static',
adapterConfig: {
path: '/data/events.json',
},
enabled: true,
color: '#3b82f6',
},
],
};
export default config;{
"events": [
{
"id": "inc-001",
"status": "published",
"createdAt": "2024-01-15T00:00:00Z",
"updatedAt": "2024-01-15T00:00:00Z",
"summary": "Bright lights observed over downtown area",
"temporal": {
"date": "2024-01-15",
"dateCertainty": "exact",
"time": "21:30:00",
"timeCertainty": "approximate"
},
"location": {
"name": "Phoenix, Arizona",
"country": "United States",
"latitude": 33.4484,
"longitude": -112.074,
"siteType": "urban",
"locationSensitivity": "standard"
},
"classification": {
"eventType": "sighting"
}
}
]
}Supabase Adapter
Connect to a Supabase database for production data.
const config: Partial<MapConfig> = {
dataSources: [
{
id: 'supabase-main',
name: 'Production Database',
adapter: 'supabase',
adapterConfig: {
// Uses NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY
},
enabled: true,
color: '#10b981',
},
],
};NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...Supabase table schema:
create table events (
id uuid primary key default gen_random_uuid(),
status text not null default 'draft',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
summary text,
temporal jsonb not null,
location jsonb not null,
classification jsonb,
witnesses jsonb,
sensor_evidence jsonb,
media jsonb
);
-- Enable RLS
alter table events enable row level security;
-- Allow anonymous reads for published events
create policy "Public can read published events"
on events for select
using (status = 'published');REST API Adapter
Connect to an external API.
const config: Partial<MapConfig> = {
dataSources: [
{
id: 'external-api',
name: 'External Data Source',
adapter: 'api',
adapterConfig: {
endpoint: 'https://api.example.com/events',
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
},
params: {
status: 'published',
limit: '1000',
},
},
enabled: true,
color: '#f59e0b',
},
],
};The API adapter expects responses in the Atlas event format. For custom APIs, you may need to transform the response—see Custom Adapter below.
Multiple Data Sources
Combine multiple sources with distinct styling.
const config: Partial<MapConfig> = {
dataSources: [
{
id: 'verified',
name: 'Verified Reports',
adapter: 'supabase',
adapterConfig: {}, // Uses default env vars
enabled: true,
color: '#10b981', // Green
},
{
id: 'community',
name: 'Community Reports',
adapter: 'static',
adapterConfig: { path: '/data/community.json' },
enabled: true,
color: '#6366f1', // Purple
},
{
id: 'historical',
name: 'Historical Archive',
adapter: 'api',
adapterConfig: { endpoint: 'https://archive.example.com/api' },
enabled: false, // Disabled by default
color: '#64748b', // Gray
},
],
};Custom Adapter
Create a custom adapter for non-standard data sources.
import type { DataAdapter, MapEvent, DataSourceConfig, MapEventFilters, FilterOptions } from './types';
interface CustomApiResponse {
data: Array<{
uuid: string;
event_date: string;
lat: number;
lng: number;
place_name: string;
description: string;
category: string;
}>;
total: number;
}
export class CustomAdapter implements DataAdapter {
readonly id: string;
readonly name: string;
private endpoint: string;
constructor(config: DataSourceConfig) {
this.id = config.id;
this.name = config.name;
this.endpoint = config.adapterConfig.endpoint as string;
}
async getMapEvents(filters?: MapEventFilters): Promise<MapEvent[]> {
const response = await fetch(this.endpoint);
const data: CustomApiResponse = await response.json();
// Transform to Atlas format
return data.data.map((item) => ({
id: item.uuid,
status: 'published' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
summary: item.description,
temporal: {
date: item.event_date,
dateCertainty: 'exact' as const,
},
location: {
name: item.place_name,
country: 'Unknown',
latitude: item.lat,
longitude: item.lng,
siteType: 'urban' as const,
locationSensitivity: 'standard' as const,
},
classification: {
eventType: this.mapCategory(item.category),
},
}));
}
async getMapEventById(id: string): Promise<MapEvent | null> {
const mapEvents = await this.getMapEvents();
return mapEvents.find((i) => i.id === id) || null;
}
async getFilterOptions(): Promise<FilterOptions> {
const mapEvents = await this.getMapEvents();
return {
countries: [...new Set(mapEvents.map((i) => i.location.country))],
siteTypes: [...new Set(mapEvents.map((i) => i.location.siteType))],
eventTypes: [...new Set(mapEvents.map((i) => i.classification?.eventType).filter(Boolean))],
};
}
private mapCategory(category: string): string {
const mapping: Record<string, string> = {
'visual': 'sighting',
'contact': 'encounter',
'trace': 'physical_trace',
};
return mapping[category] || 'other';
}
}Register the adapter:
import { CustomAdapter } from './custom-adapter';
export function createAdapter(config: DataSourceConfig): DataAdapter {
switch (config.adapter) {
case 'custom':
return new CustomAdapter(config);
// ... other adapters
}
}Custom Renderers
Severity-Based Markers
Color markers based on event severity.
'use client';
import type { MarkerRendererProps } from '@disclosureos/mapping';
const severityColors = {
critical: 'bg-red-500 border-red-700',
high: 'bg-orange-500 border-orange-700',
moderate: 'bg-yellow-500 border-yellow-700',
low: 'bg-blue-500 border-blue-700',
};
export function SeverityMarker({ mapEvents, isSelected, onClick }: MarkerRendererProps) {
const mapEvent = mapEvents[0]; // Use primary mapEvent
const sensitivity = mapEvent.location?.locationSensitivity || 'standard';
const colorClass = severityColors[sensitivity as keyof typeof severityColors] || severityColors.low;
return (
<button
onClick={onClick}
className={`
w-4 h-4 rounded-full border-2 shadow-lg
transition-all duration-200
${colorClass}
${isSelected ? 'scale-150 ring-2 ring-white ring-offset-2' : 'hover:scale-125'}
`}
aria-label={`Event at ${mapEvent.location?.name}`}
/>
);
}Popup with Media Gallery
Rich popup with image/video previews.
'use client';
import { useState } from 'react';
import { X, ChevronLeft, ChevronRight, MapPin, Calendar } from 'lucide-react';
import type { PopupRendererProps } from '@disclosureos/mapping';
export function MediaPopup({ mapEvent, onClose, onViewDetails }: PopupRendererProps) {
const [activeMedia, setActiveMedia] = useState(0);
const media = mapEvent.media || [];
return (
<div className="bg-card rounded-lg shadow-xl max-w-md overflow-hidden">
{/* Media Carousel */}
{media.length > 0 && (
<div className="relative aspect-video bg-muted">
{media[activeMedia].type === 'video' ? (
<video
src={media[activeMedia].url}
controls
className="w-full h-full object-cover"
/>
) : (
<img
src={media[activeMedia].thumbnailUrl || media[activeMedia].url}
alt={media[activeMedia].caption || 'Event media'}
className="w-full h-full object-cover"
/>
)}
{/* Navigation */}
{media.length > 1 && (
<>
<button
onClick={() => setActiveMedia((i) => (i - 1 + media.length) % media.length)}
className="absolute left-2 top-1/2 -translate-y-1/2 p-1 bg-black/50 rounded-full"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<button
onClick={() => setActiveMedia((i) => (i + 1) % media.length)}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 bg-black/50 rounded-full"
>
<ChevronRight className="w-5 h-5 text-white" />
</button>
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex gap-1">
{media.map((_, i) => (
<button
key={i}
onClick={() => setActiveMedia(i)}
className={`w-2 h-2 rounded-full ${i === activeMedia ? 'bg-white' : 'bg-white/50'}`}
/>
))}
</div>
</>
)}
</div>
)}
{/* Content */}
<div className="p-4">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">{mapEvent.location?.name}</h3>
<div className="flex items-center gap-4 text-sm text-muted-foreground mt-1">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{mapEvent.temporal?.date}
</span>
<span className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{mapEvent.location?.country}
</span>
</div>
</div>
<button onClick={onClose} className="p-1 hover:bg-muted rounded">
<X className="w-5 h-5" />
</button>
</div>
{mapEvent.summary && (
<p className="mt-3 text-sm line-clamp-3">{mapEvent.summary}</p>
)}
{onViewDetails && (
<button
onClick={onViewDetails}
className="mt-4 w-full py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:bg-primary/90"
>
View Full Details
</button>
)}
</div>
</div>
);
}Compact Feed Item with Badges
Minimal feed item with classification badges.
'use client';
import { MapPin } from 'lucide-react';
import type { FeedItemRendererProps } from '@disclosureos/mapping';
const classificationLabels: Record<string, string> = {
ce1: 'CE-I',
ce2: 'CE-II',
ce3: 'CE-III',
nl: 'NL',
dd: 'DD',
rv: 'RV',
};
export function BadgeFeedItem({ mapEvent, isSelected, onClick, dataSource }: FeedItemRendererProps) {
const hynek = mapEvent.classification?.hynekClassification;
const hasVideo = mapEvent.media?.some((m) => m.type === 'video');
const hasPhoto = mapEvent.media?.some((m) => m.type === 'image');
return (
<button
onClick={onClick}
className={`
w-full text-left p-3 rounded-lg border transition-all
${isSelected
? 'bg-primary/10 border-primary shadow-sm'
: 'border-border hover:bg-muted/50'
}
`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
{dataSource?.color && (
<div
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: dataSource.color }}
/>
)}
<span className="font-medium truncate">{mapEvent.location?.name}</span>
</div>
{/* Badges */}
<div className="flex gap-1 flex-shrink-0">
{hynek && (
<span className="px-1.5 py-0.5 text-xs bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded">
{classificationLabels[hynek] || hynek.toUpperCase()}
</span>
)}
{hasVideo && (
<span className="px-1.5 py-0.5 text-xs bg-purple-500/20 text-purple-600 dark:text-purple-400 rounded">
Video
</span>
)}
{hasPhoto && !hasVideo && (
<span className="px-1.5 py-0.5 text-xs bg-green-500/20 text-green-600 dark:text-green-400 rounded">
Photo
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground">
<span>{mapEvent.temporal?.date}</span>
<span>•</span>
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
{mapEvent.location?.country}
</span>
</div>
</button>
);
}Complete Page Layout Example
Custom page layout with all controls.
'use client';
import { useState } from 'react';
import { Menu, X } from 'lucide-react';
import { DataProvider, MapEngine, useData, useTheme } from '@/core';
import { FilterSidebar, TimelineControl, MapEventFeed, ThemeToggle, SourceToggle, StatsPanel } from '@/controls';
import { PulseMarker, MediaPopup, BadgeFeedItem } from '@/renderers';
function MapPage() {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isTimelineOpen, setIsTimelineOpen] = useState(true);
const { mapEvents, filteredMapEvents, isLoading, error } = useData();
const { resolvedTheme } = useTheme();
// Track map ready state for unified initialization
const [mapReady, setMapReady] = useState(false);
const appReady = !isLoading && mapReady;
if (error) {
return (
<div className="h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-xl font-semibold">Error loading data</h2>
<p className="text-muted-foreground mt-2">{error.message}</p>
</div>
</div>
);
}
return (
<div className="h-screen flex flex-col">
{/* Header */}
<header className="h-14 border-b flex items-center justify-between px-4">
<div className="flex items-center gap-4">
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="p-2 hover:bg-muted rounded-lg lg:hidden"
>
{isSidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<h1 className="font-semibold">My Atlas Map</h1>
</div>
<div className="flex items-center gap-2">
<SourceToggle />
<ThemeToggle />
</div>
</header>
{/* Main Content */}
<div className="flex-1 flex overflow-hidden">
{/* Left Sidebar - Filters */}
<aside
className={`
w-80 border-r overflow-y-auto
${isSidebarOpen ? 'block' : 'hidden lg:block'}
`}
>
<FilterSidebar />
</aside>
{/* Map */}
<main className="flex-1 relative">
<MapEngine
mapEvents={filteredMapEvents}
renderMarker={(props) => <PulseMarker {...props} />}
renderPopup={(props) => <MediaPopup {...props} />}
onMapReady={() => setMapReady(true)}
/>
{/* Stats Overlay */}
<div className="absolute top-4 right-4">
<StatsPanel mapEvents={filteredMapEvents} totalMapEvents={mapEvents.length} />
</div>
{/* Timeline */}
{isTimelineOpen && (
<div className="absolute bottom-0 left-0 right-0">
<TimelineControl />
</div>
)}
</main>
{/* Right Sidebar - Feed */}
<aside className="w-96 border-l overflow-hidden hidden xl:flex flex-col">
<MapEventFeed
renderFeedItem={(props) => <BadgeFeedItem {...props} />}
/>
</aside>
</div>
</div>
);
}
export default function Page() {
return (
<DataProvider>
<MapPage />
</DataProvider>
);
}Filter Presets
Create quick filter presets for common queries.
'use client';
import { useData } from '@/core';
const presets = [
{
id: 'recent-verified',
name: 'Recent Verified',
filters: {
dateRange: {
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
end: new Date().toISOString().split('T')[0],
},
multipleWitnesses: true,
},
},
{
id: 'close-encounters',
name: 'Close Encounters',
filters: {
hynekClassifications: ['ce1', 'ce2', 'ce3'],
},
},
{
id: 'military-sites',
name: 'Military Sites',
filters: {
siteTypes: ['military_base', 'airport_military'],
locationSensitivities: ['critical', 'high'],
},
},
{
id: 'with-evidence',
name: 'With Evidence',
filters: {
radarConfirmed: true,
physicalEvidence: true,
},
},
];
export function FilterPresets() {
const { updateFilter, resetFilters } = useData();
const applyPreset = (preset: typeof presets[0]) => {
resetFilters();
Object.entries(preset.filters).forEach(([key, value]) => {
updateFilter(key as any, value);
});
};
return (
<div className="flex flex-wrap gap-2 p-4">
<button
onClick={resetFilters}
className="px-3 py-1.5 text-sm border rounded-full hover:bg-muted"
>
Clear All
</button>
{presets.map((preset) => (
<button
key={preset.id}
onClick={() => applyPreset(preset)}
className="px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-full hover:bg-primary/20"
>
{preset.name}
</button>
))}
</div>
);
}