GEO DB - INTERACTIVE APP SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>City Data Explorer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
color-scheme: light dark;
--bg-color: transparent;
--text-main: #ffffff;
--text-muted: #aaaaaa;
--text-subtle: #888888;
--surface: transparent;
--surface-raised: #111111;
--surface-overlay: #222222;
--surface-hover: #2a2a2a;
--surface-active: #1a3a5f;
--border: #333333;
--border-light: #444444;
--primary: #4a90e2;
--primary-hover: #357ae8;
--danger-bg: rgba(239, 68, 68, 0.1);
--danger-border: rgba(239, 68, 68, 0.3);
--danger-text: #fca5a5;
}
html:not(.dark) {
--bg-color: transparent;
--text-main: #111111;
--text-muted: #555555;
--text-subtle: #666666;
--surface: transparent;
--surface-raised: #f9f9f9;
--surface-overlay: #ffffff;
--surface-hover: #f0f0f0;
--surface-active: #e6f0fa;
--border: #cccccc;
--border-light: #bbbbbb;
--primary: #4a90e2;
--primary-hover: #357ae8;
--danger-bg: rgba(239, 68, 68, 0.1);
--danger-border: rgba(239, 68, 68, 0.3);
--danger-text: #dc2626;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
color: var(--text-main);
line-height: 1.5;
padding: 0;
margin: 0;
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
.container {
width: 100%;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
h1 {
font-size: 22px;
font-weight: 600;
margin-bottom: 4px;
}
.subtitle {
color: var(--text-muted);
font-size: 14px;
}
.search-area {
background: transparent;
padding: 20px;
margin-bottom: 0;
border-bottom: 1px solid var(--border);
}
.search-row {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
input {
flex: 1;
padding: 12px 15px;
background: var(--surface-overlay);
border: 1px solid var(--border-light);
border-radius: 6px;
color: var(--text-main);
font-size: 16px; /* Better for mobile */
-webkit-appearance: none;
}
input:focus {
outline: none;
border-color: var(--primary);
}
button {
padding: 12px 20px;
background: var(--primary);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
}
button:hover {
background: var(--primary-hover);
transform: translateY(-2px);
}
button:disabled {
background: var(--border-light);
cursor: not-allowed;
}
.quick-search {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-search button {
padding: 8px 12px;
background: var(--surface-overlay);
font-size: 14px;
flex: 1;
min-width: calc(50% - 4px); /* 2 buttons per row on mobile */
color: var(--text-main);
border: 1px solid var(--border-light);
border-radius: 6px;
transition: all 0.2s;
}
.quick-search button:hover {
background: var(--surface-hover);
border-color: var(--primary);
transform: translateY(-2px);
}
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--surface-raised);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--border-light);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--border-light) var(--surface-raised);
}
.alert {
padding: 12px 15px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 14px;
display: none;
}
.alert.error {
background: var(--danger-bg);
border: 1px solid var(--danger-border);
color: var(--danger-text);
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-subtle);
display: none;
}
.results-container {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 20px;
flex: 1;
min-height: 0;
}
.results-side {
background: var(--surface-raised);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.results-count {
font-size: 14px;
color: var(--text-subtle);
}
.city-list-container {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.city-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.details-side {
background: var(--surface-raised);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.details-content-container {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--text-main);
}
.city-item {
padding: 12px;
background: var(--surface-overlay);
border-radius: 6px;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
}
.city-item:hover {
background: var(--surface-hover);
border-color: var(--border-light);
}
.city-item.active {
background: var(--surface-active);
border-color: var(--primary);
}
.city-name {
font-weight: 500;
margin-bottom: 4px;
color: var(--text-main);
font-size: 15px;
}
.city-location {
font-size: 13px;
color: var(--text-muted);
margin-bottom: 6px;
}
.city-stats {
display: flex;
gap: 15px;
font-size: 12px;
color: var(--text-subtle);
}
.no-results {
text-align: center;
padding: 30px;
color: var(--text-subtle);
}
.details-content {
min-height: 400px;
}
.details-header {
margin-bottom: 20px;
}
.details-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
color: var(--text-main);
}
.details-subtitle {
font-size: 14px;
color: var(--text-muted);
margin-bottom: 10px;
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.detail-box {
background: var(--surface-overlay);
border-radius: 6px;
padding: 15px;
border: 1px solid var(--border);
}
.detail-label {
font-size: 12px;
color: var(--text-subtle);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 18px;
font-weight: 500;
color: var(--text-main);
}
.detail-sub {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
}
.empty-details {
text-align: center;
padding: 60px 20px;
color: var(--text-subtle);
}
.info-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
font-size: 13px;
}
.info-item {
color: var(--text-subtle);
}
.info-value {
color: var(--text-main);
font-weight: 500;
}
/* Mobile layout - tabs instead of side-by-side */
.mobile-tabs {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
margin-top: 20px;
}
.tab-buttons {
display: flex;
background: var(--surface-raised);
border-radius: 8px 8px 0 0;
overflow: hidden;
border-bottom: 1px solid var(--border);
}
.tab-button {
flex: 1;
padding: 14px;
background: transparent;
color: var(--text-subtle);
border: none;
text-align: center;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.tab-button:hover {
color: var(--text-main);
background: rgba(128, 128, 128, 0.1);
}
.tab-button.active {
background: var(--surface-overlay);
color: var(--text-main);
border-bottom: 2px solid var(--primary);
}
.tab-content {
background: var(--surface-raised);
border-radius: 0 0 8px 8px;
padding: 20px;
border: 1px solid var(--border);
border-top: none;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.desktop-layout {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
margin-top: 20px;
}
/* Desktop styles */
@media (min-width: 1025px) {
.mobile-tabs {
display: none !important;
}
}
/* Tablet and mobile styles */
@media (max-width: 1024px) {
.desktop-layout {
display: none;
}
.mobile-tabs {
display: block;
}
.results-container {
grid-template-columns: 1fr;
}
.city-list-container,
.details-content-container {
max-height: none;
overflow-y: visible;
}
.quick-search button {
min-width: calc(25% - 6px); /* 4 buttons per row on tablet */
}
.details-grid {
grid-template-columns: 1fr;
}
}
/* Small mobile styles */
@media (max-width: 640px) {
.search-row {
flex-direction: column;
}
button, input {
width: 100%;
}
.quick-search button {
min-width: calc(50% - 4px); /* 2 buttons per row on mobile */
font-size: 13px;
padding: 10px 8px;
}
.tab-content {
padding: 16px;
min-height: 400px;
}
.city-item {
padding: 10px;
}
.city-name {
font-size: 14px;
}
.detail-box {
padding: 12px;
}
.detail-value {
font-size: 16px;
}
}
/* Very small screens */
@media (max-width: 375px) {
h1 {
font-size: 20px;
}
.quick-search button {
min-width: 100%; /* 1 button per row */
}
.tab-button {
font-size: 13px;
padding: 12px 8px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="search-area">
<div class="search-row">
<input type="text" id="cityInput" placeholder="Enter city name (e.g., Paris, Tokyo)" autocomplete="off">
<button id="searchBtn">Search</button>
</div>
<div class="quick-search">
<button data-city="Paris">Paris</button>
<button data-city="London">London</button>
<button data-city="Tokyo">Tokyo</button>
<button data-city="New York">New York</button>
<button data-city="Berlin">Berlin</button>
<button data-city="Sydney">Sydney</button>
<button data-city="Dubai">Dubai</button>
<button data-city="Singapore">Singapore</button>
</div>
</div>
<div id="alert" class="alert"></div>
<div id="loading" class="loading">
Loading city data...
</div>
<!-- Desktop Layout (for larger screens) -->
<div id="desktopResults" class="desktop-layout" style="display: none;">
<div class="results-container">
<div class="results-side">
<div class="results-header">
<div class="section-title">Search Results</div>
<div id="resultsCount" class="results-count"></div>
</div>
<div class="city-list-container">
<div id="cityList" class="city-list"></div>
</div>
</div>
<div class="details-side">
<div class="section-title">City Details</div>
<div class="details-content-container">
<div id="detailsContent" class="details-content">
<div class="empty-details">
Select a city to view detailed information
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile Layout (for smaller screens) -->
<div id="mobileTabs" class="mobile-tabs" style="display: none;">
<div class="tab-buttons">
<button class="tab-button active" data-tab="cities">Cities</button>
<button class="tab-button" data-tab="details">Details</button>
</div>
<div class="tab-content" id="citiesTab">
<div class="results-header" style="margin-bottom: 15px;">
<div class="section-title">Search Results</div>
<div id="mobileResultsCount" class="results-count"></div>
</div>
<div id="mobileCityList" class="city-list"></div>
</div>
<div class="tab-content" id="detailsTab" style="display: none;">
<div class="section-title" style="margin-bottom: 15px;">City Details</div>
<div id="mobileDetailsContent">
<div class="empty-details">
Select a city to view detailed information
</div>
</div>
</div>
</div>
</div>
<script>
const PROXY_ENDPOINT = '/.netlify/functions/proxy-request';
const GEO_DB_NOTE_ID = '53d3b57b-137e-483c-b546-b0193f446d7c';
const DEFAULT_LIMIT = 10;
// DOM elements
const cityInput = document.getElementById('cityInput');
const searchBtn = document.getElementById('searchBtn');
const alertDiv = document.getElementById('alert');
const loadingDiv = document.getElementById('loading');
const desktopResults = document.getElementById('desktopResults');
const mobileTabs = document.getElementById('mobileTabs');
const cityList = document.getElementById('cityList');
const mobileCityList = document.getElementById('mobileCityList');
const detailsContent = document.getElementById('detailsContent');
const mobileDetailsContent = document.getElementById('mobileDetailsContent');
const resultsCount = document.getElementById('resultsCount');
const mobileResultsCount = document.getElementById('mobileResultsCount');
const quickButtons = document.querySelectorAll('.quick-search button');
const tabButtons = document.querySelectorAll('.tab-button');
let isSearching = false;
let currentCityId = null;
let currentCities = [];
function showAlert(message, type = 'error') {
alertDiv.textContent = message;
alertDiv.className = `alert ${type}`;
alertDiv.style.display = 'block';
if (type !== 'error') {
setTimeout(() => {
alertDiv.style.display = 'none';
}, 3000);
}
}
function hideAlert() {
alertDiv.style.display = 'none';
}
function switchTab(tabName) {
// Update tab buttons
tabButtons.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
}
});
// Show selected tab content
document.getElementById('citiesTab').style.display = tabName === 'cities' ? 'block' : 'none';
document.getElementById('detailsTab').style.display = tabName === 'details' ? 'block' : 'none';
}
async function makeRequest(url, noteId = GEO_DB_NOTE_ID) {
try {
const response = await fetch(PROXY_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
noteId: noteId,
targetUrl: url,
method: 'GET',
options: {
headers: {
'x-rapidapi-host': 'wft-geo-db.p.rapidapi.com',
'x-rapidapi-key': '{{RAPIDAPI_KEY}}'
}
},
debug: false
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Proxy response:', response.status, errorText);
let errorMessage = `API Error (${response.status})`;
try {
const errorData = JSON.parse(errorText);
if (errorData.errors && errorData.errors.length > 0) {
errorMessage = errorData.errors[0].message || errorMessage;
} else if (errorData.message) {
errorMessage = errorData.message;
}
} catch {
// If not JSON, keep original message
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
async function searchCity(cityName = null) {
if (isSearching) return;
const city = cityName || cityInput.value.trim();
if (!city) {
showAlert('Please enter a city name');
return;
}
if (cityName && !cityInput.value) {
cityInput.value = cityName;
}
isSearching = true;
searchBtn.disabled = true;
quickButtons.forEach(btn => btn.disabled = true);
hideAlert();
loadingDiv.style.display = 'block';
desktopResults.style.display = 'none';
mobileTabs.style.display = 'none';
try {
const searchUrl = `https://wft-geo-db.p.rapidapi.com/v1/geo/cities?namePrefix=${encodeURIComponent(city)}&limit=${DEFAULT_LIMIT}&sort=-population`;
const data = await makeRequest(searchUrl);
currentCities = data.data || [];
// Show appropriate layout based on screen size
if (window.innerWidth >= 1025) {
desktopResults.style.display = 'flex';
mobileTabs.style.display = 'none';
displaySearchResults(currentCities, city, cityList);
resultsCount.textContent = `${currentCities.length} cities`;
} else {
desktopResults.style.display = 'none';
mobileTabs.style.display = 'flex';
displaySearchResults(currentCities, city, mobileCityList);
mobileResultsCount.textContent = `${currentCities.length} cities`;
switchTab('cities'); // Always show cities tab first on mobile
}
if (currentCities.length > 0) {
currentCityId = currentCities[0].id;
fetchCityDetails(currentCityId);
}
} catch (error) {
console.error('Search error:', error);
showAlert(`Search failed: ${error.message}`);
} finally {
isSearching = false;
searchBtn.disabled = false;
quickButtons.forEach(btn => btn.disabled = false);
loadingDiv.style.display = 'none';
}
}
async function fetchCityDetails(cityId) {
if (!cityId) return;
// Update active city in both desktop and mobile lists
updateActiveCity(cityId);
try {
const detailUrl = `https://wft-geo-db.p.rapidapi.com/v1/geo/cities/${cityId}`;
const data = await makeRequest(detailUrl);
const city = data.data || data;
// Update both desktop and mobile details
displayCityDetails(city, detailsContent);
displayCityDetails(city, mobileDetailsContent);
currentCityId = cityId;
// On mobile, switch to details tab after selection
if (window.innerWidth < 1025) {
switchTab('details');
}
} catch (error) {
console.error('Details error:', error);
const errorHtml = `
<div class="empty-details">
Error loading city details
</div>
`;
detailsContent.innerHTML = errorHtml;
mobileDetailsContent.innerHTML = errorHtml;
}
}
function updateActiveCity(cityId) {
// Update desktop city list
document.querySelectorAll('#cityList .city-item').forEach(item => {
item.classList.remove('active');
if (parseInt(item.dataset.cityId) === cityId) {
item.classList.add('active');
}
});
// Update mobile city list
document.querySelectorAll('#mobileCityList .city-item').forEach(item => {
item.classList.remove('active');
if (parseInt(item.dataset.cityId) === cityId) {
item.classList.add('active');
}
});
}
function displaySearchResults(cities, searchTerm, targetElement) {
targetElement.innerHTML = '';
if (!cities || cities.length === 0) {
targetElement.innerHTML = '<div class="no-results">No cities found</div>';
return;
}
cities.forEach((city, index) => {
const item = document.createElement('div');
item.className = 'city-item';
item.dataset.cityId = city.id;
if (index === 0) {
item.classList.add('active');
}
item.innerHTML = `
<div class="city-name">${city.name}, ${city.countryCode}</div>
<div class="city-location">${city.region || 'Unknown region'}</div>
<div class="city-stats">
<span>👥 ${formatNumber(city.population)}</span>
<span>📍 ${city.latitude?.toFixed(4)}, ${city.longitude?.toFixed(4)}</span>
</div>
`;
item.addEventListener('click', () => fetchCityDetails(city.id));
targetElement.appendChild(item);
});
}
function displayCityDetails(city, targetElement) {
const elevation = city.elevationMeters ?
`${formatNumber(city.elevationMeters)}m (${formatNumber(Math.round(city.elevationMeters * 3.28084))}ft)` :
'N/A';
targetElement.innerHTML = `
<div class="details-header">
<div class="details-title">${city.name}, ${city.countryCode}</div>
<div class="details-subtitle">${city.region || ''} • ${city.country || ''}</div>
</div>
<div class="details-grid">
<div class="detail-box">
<div class="detail-label">Population</div>
<div class="detail-value">${formatNumber(city.population)}</div>
<div class="detail-sub">Total residents</div>
</div>
<div class="detail-box">
<div class="detail-label">Elevation</div>
<div class="detail-value">${elevation.split(' ')[0]}</div>
<div class="detail-sub">Above sea level</div>
</div>
<div class="detail-box">
<div class="detail-label">Timezone</div>
<div class="detail-value">${city.timezone || 'N/A'}</div>
<div class="detail-sub">Local time zone</div>
</div>
<div class="detail-box">
<div class="detail-label">Coordinates</div>
<div class="detail-value">${city.latitude?.toFixed(4)}, ${city.longitude?.toFixed(4)}</div>
<div class="detail-sub">Latitude, Longitude</div>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">Country Code: </span>
<span class="info-value">${city.countryCode || 'N/A'}</span>
</div>
<div class="info-item">
<span class="info-label">Region Code: </span>
<span class="info-value">${city.regionCode || 'N/A'}</span>
</div>
${city.wikiDataId ? `
<div class="info-item">
<span class="info-label">WikiData ID: </span>
<span class="info-value">${city.wikiDataId}</span>
</div>
` : ''}
</div>
`;
}
function formatNumber(num) {
if (!num) return 'N/A';
return num.toLocaleString();
}
// Event listeners
searchBtn.addEventListener('click', () => searchCity());
cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchCity();
});
quickButtons.forEach(button => {
button.addEventListener('click', () => {
const city = button.getAttribute('data-city');
searchCity(city);
});
});
tabButtons.forEach(button => {
button.addEventListener('click', () => {
switchTab(button.dataset.tab);
});
});
// Handle window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (currentCities.length > 0) {
if (window.innerWidth >= 1025) {
desktopResults.style.display = 'flex';
mobileTabs.style.display = 'none';
displaySearchResults(currentCities, '', cityList);
resultsCount.textContent = `${currentCities.length} cities`;
} else {
desktopResults.style.display = 'none';
mobileTabs.style.display = 'flex';
displaySearchResults(currentCities, '', mobileCityList);
mobileResultsCount.textContent = `${currentCities.length} cities`;
}
}
}, 250);
});
// Focus input on load
cityInput.focus();
// Initialize with a tip (only once)
if (!localStorage.getItem('cityExplorerTipShown')) {
setTimeout(() => {
showAlert('💡 Tip: Search for any city or use the quick buttons to get started', '');
localStorage.setItem('cityExplorerTipShown', 'true');
}, 1500);
}
</script>
<script>
(function syncTheme() {
try {
if (window.parent && window.parent !== window) {
const parentHtml = window.parent.document.documentElement;
const localHtml = document.documentElement;
const updateTheme = () => {
if (parentHtml.classList.contains('dark')) {
localHtml.classList.add('dark');
} else {
localHtml.classList.remove('dark');
}
};
updateTheme();
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updateTheme();
}
});
});
observer.observe(parentHtml, { attributes: true });
return;
}
} catch (e) {
console.warn('Could not sync theme with parent window.', e);
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
})();
</script>
</body>
</html>