Skip to content

Instantly share code, notes, and snippets.

@simonw
Created October 18, 2025 18:34
Show Gist options
  • Select an option

  • Save simonw/b9f5416b37c4ceec46d8447b52be0ad2 to your computer and use it in GitHub Desktop.

Select an option

Save simonw/b9f5416b37c4ceec46d8447b52be0ad2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Historic Orchestrions Around the World</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 3rem;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.stats {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.stat {
background: rgba(255,255,255,0.2);
padding: 0.5rem 1.5rem;
border-radius: 20px;
backdrop-filter: blur(10px);
}
.locations-container {
display: flex;
flex-direction: column;
gap: 2.5rem;
margin-bottom: 2rem;
}
.location-group {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.location-header {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 3px solid #667eea;
}
.venue-name {
font-size: 1.8rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.5rem;
}
.orchestrion-count {
display: inline-block;
background: #667eea;
color: white;
padding: 0.2rem 0.8rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
margin-left: 0.5rem;
}
.orchestrions-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.card {
background: #f8f9fa;
border-radius: 12px;
padding: 1.5rem;
border-left: 4px solid #667eea;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.card-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e0e0e0;
}
.orchestrion-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
}
.location {
display: flex;
align-items: center;
gap: 0.5rem;
color: #666;
font-size: 0.95rem;
}
.location-icon {
font-size: 1.2rem;
}
.map-link {
display: inline-block;
margin-top: 0.5rem;
padding: 0.4rem 1rem;
background: #4285f4;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 0.9rem;
transition: background 0.3s ease;
}
.map-link:hover {
background: #3367d6;
}
.section {
margin-bottom: 1rem;
}
.section-title {
font-weight: 600;
color: #667eea;
margin-bottom: 0.4rem;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.section-content {
color: #555;
font-size: 0.95rem;
}
.section-content a {
color: #667eea;
text-decoration: none;
}
.section-content a:hover {
text-decoration: underline;
}
.loading {
text-align: center;
color: white;
font-size: 1.5rem;
padding: 3rem;
}
.error {
background: #f44336;
color: white;
padding: 1rem;
border-radius: 8px;
text-align: center;
margin: 2rem auto;
max-width: 600px;
}
footer {
text-align: center;
color: white;
margin-top: 3rem;
opacity: 0.8;
font-size: 0.9rem;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.venue-name {
font-size: 1.4rem;
}
.orchestrions-list {
grid-template-columns: 1fr;
}
.location-group {
padding: 1.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Historic Orchestrions Around the World</h1>
<p class="subtitle">A collection of rare and remarkable mechanical orchestras</p>
<div class="stats" id="stats"></div>
</header>
<div id="loading" class="loading">Loading orchestrions...</div>
<div id="error" style="display: none;"></div>
<div id="orchestrions" class="locations-container"></div>
<footer>
<p>Data compiled from museum archives and historical records</p>
</footer>
</div>
<script>
async function loadOrchestrions() {
try {
const response = await fetch('https://gist.githubusercontent.com/simonw/2a0b26633802149a44e15cf1cd396f86/raw/836dde6b9b9452ac797d98b403872034f7536c9d/orchestrions.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayOrchestrions(data);
} catch (error) {
document.getElementById('loading').style.display = 'none';
const errorDiv = document.getElementById('error');
errorDiv.style.display = 'block';
errorDiv.className = 'error';
errorDiv.textContent = `Error loading data: ${error.message}`;
}
}
function displayOrchestrions(orchestrions) {
document.getElementById('loading').style.display = 'none';
// Group orchestrions by venue
const grouped = {};
orchestrions.forEach(orch => {
const key = `${orch.venue}|${orch.city}|${orch.country}|${orch.latitude}|${orch.longitude}`;
if (!grouped[key]) {
grouped[key] = {
venue: orch.venue,
city: orch.city,
country: orch.country,
latitude: orch.latitude,
longitude: orch.longitude,
orchestrions: []
};
}
grouped[key].orchestrions.push(orch);
});
const locations = Object.values(grouped);
// Display statistics
const countries = new Set(orchestrions.map(o => o.country));
const statsHtml = `
<div class="stat"><strong>${orchestrions.length}</strong> Orchestrions</div>
<div class="stat"><strong>${locations.length}</strong> Locations</div>
<div class="stat"><strong>${countries.size}</strong> Countries</div>
`;
document.getElementById('stats').innerHTML = statsHtml;
// Display locations and orchestrions
const container = document.getElementById('orchestrions');
container.innerHTML = locations.map(location => {
const googleMapsUrl = `https://www.google.com/maps?q=${location.latitude},${location.longitude}`;
const count = location.orchestrions.length;
return `
<div class="location-group">
<div class="location-header">
<div class="venue-name">
${escapeHtml(location.venue)}
${count > 1 ? `<span class="orchestrion-count">${count} orchestrions</span>` : ''}
</div>
<div class="location">
<span class="location-icon">📍</span>
<span>${escapeHtml(location.city)}, ${escapeHtml(location.country)}</span>
</div>
<a href="${googleMapsUrl}" target="_blank" rel="noopener" class="map-link">
View on Google Maps →
</a>
</div>
<div class="orchestrions-list">
${location.orchestrions.map((orch, idx) => `
<div class="card">
${count > 1 ? `
<div class="card-header">
<div class="orchestrion-title">Orchestrion ${idx + 1}</div>
</div>
` : ''}
${orch.description ? `
<div class="section">
<div class="section-title">Description</div>
<div class="section-content">${formatText(orch.description)}</div>
</div>
` : ''}
${orch.history ? `
<div class="section">
<div class="section-title">History</div>
<div class="section-content">${formatText(orch.history)}</div>
</div>
` : ''}
${orch.notes ? `
<div class="section">
<div class="section-title">Notes</div>
<div class="section-content">${formatText(orch.notes)}</div>
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatText(text) {
// Convert markdown-style links [text](url) to HTML links
return escapeHtml(text).replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener">$1</a>'
);
}
// Load data when page loads
loadOrchestrions();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment