ArthurZ HF Staff commited on
Commit
32f3fa8
·
verified ·
1 Parent(s): 6466fc5

<!DOCTYPE html>

Browse files

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Europe Night Trains (10h+)</title>
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<style>
:root { --bg:#0b0f14; --panel:#10161f; --muted:#9fb0c3; --accent:#56b0ff; }
* { box-sizing: border-box; }
html, body { height:100%; margin:0; background:var(--bg); color:#e6edf3; font:14px system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji"; }
/* Ensure the map actually gets a concrete height via the grid parent */
#app { display:grid; grid-template-columns: 320px 1fr; grid-template-rows: 56px 1fr; height:100vh; min-height:0; }
header { grid-column:1 / span 2; display:flex; align-items:center; gap:.75rem; padding:12px 16px; background:var(--panel); border-bottom:1px solid #1b2532; }
header h1 { font-size:16px; margin:0; font-weight:600; letter-spacing:.2px; }
header .sub { color:var(--muted); font-weight:400; }
#sidebar { background:var(--panel); border-right:1px solid #1b2532; overflow:auto; min-height:0; }
#map { width:100%; height:100%; min-height:0; }
.group { padding:10px 12px; border-bottom:1px solid #192330; }
.group h3 { margin:0 0 8px 0; font-size:12px; color:#87a3bd; text-transform:uppercase; letter-spacing:.12em; }
.route { display:flex; align-items:center; gap:.5rem; padding:8px; border-radius:10px; cursor:pointer; transition:opacity .15s ease, background .15s ease; }
.route:hover { background:#0f1722; }
.route[aria-pressed="false"] { opacity:0.45; }
.dot { width:10px; height:10px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 2px rgba(86,176,255,.2); flex:0 0 10px; }
.citypair { display:flex; flex-direction:column; line-height:1.15; }
.citypair strong { font-size:13px; }
.meta { font-size:12px; color:var(--muted); }
.controls { display:flex; gap:.5rem; padding:8px 12px; position:sticky; top:0; background:linear-gradient(var(--panel), var(--panel)); border-bottom:1px solid #1b2532; z-index:5; }
.btn { padding:6px 10px; border-radius:10px; background:#0f1722; border:1px solid #1b2532; color:#cfe3f6; cursor:pointer; }
.btn:hover { background:#122033; }
.legend { position:absolute; right:12px; bottom:12px; background:var(--panel); border:1px solid #1b2532; padding:8px 10px; border-radius:12px; color:#cfe3f6; font-size:12px; }
.legend .swatch { display:inline-block; width:12px; height:3px; background:var(--accent); margin:0 6px 0 0; vertical-align:middle; border-radius:2px; }
.leaflet-container { background:#0a0e13; }
a, .leaflet-popup-content a { color:#9bd1ff; }


@media
(max-width: 880px) {
#app { grid-template-columns: 1fr; grid-template-rows: 56px 220px 1fr; }
#sidebar { grid-row: 2; border-right: none; border-bottom:1px solid #1b2532; }
#map { grid-row: 3; }
}
</style>
</head>
<body>
<div id="app">
<header>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18" stroke="#56b0ff" stroke-width="1.6" stroke-linecap="round"/></svg>
<h1>Europe Night Trains <span class="sub">(10h+ start → terminus)</span></h1>
</header>
<aside id="sidebar" aria-label="Routes sidebar">
<div class="controls">
<button class="btn" id="showAll" type="button" title="Show all routes (S)">Show all</button>
<button class="btn" id="hideAll" type="button" title="Hide all routes (H)">Hide all</button>
<button class="btn" id="fitAll" type="button" title="Fit to Europe (F)">Fit to Europe</button>
</div>
<div class="group" id="list"></div>
</aside>
<main id="map" aria-label="Map of Europe with night train routes"></main>
</div>

<div class="legend" role="note"><span class="swatch"></span> Night train route (start → terminus)</div>

<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
// --- City coordinates (approximate) ---
const CITIES = {
Amsterdam:[52.379, 4.9],
Vienna:[48.208, 16.373],
Innsbruck:[47.269, 11.404],
Zürich:[47.376, 8.541],
Brussels:[50.847, 4.357],
Berlin:[52.52, 13.405],
Prague:[50.075, 14.437],
Paris:[48.857, 2.351],
Nice:[43.703, 7.266],
"Latour-de-Carol/Enveitg":[42.453, 1.918],
Briançon:[44.897, 6.643],
Stockholm:[59.33, 18.06],
Narvik:[68.438, 17.427],
Helsinki:[60.171, 24.941],
Rovaniemi:[66.503, 25.728],
Kolari:[67.35, 23.78],
Hamburg:[53.55, 9.993],
Rome:[41.902, 12.496],
Munich:[48.137, 11.575],
Zagreb:[45.815, 15.981],
Budapest:[47.497, 19.04],
Bucharest:[44.426, 26.102],
London:[51.507, -0.128],
Inverness:[57.477, -4.224],
"Fort William":[56.82, -5.105]
};

// --- Routes ≥10h (start → terminus) ---
const ROUTES = [
{from:"Amsterdam", to:"Vienna", op:"Nightjet"},
{from:"Amsterdam", to:"Innsbruck", op:"Nightjet"},
{from:"Amsterdam", to:"Zürich", op:"Nightjet"},
{from:"Brussels", to:"Berlin", op:"European Sleeper"},
{from:"Brussels", to:"Prague", op:"European Sleeper"},
{from:"Paris", to:"Berlin", op:"Nightjet"},
{from:"Paris", to:"Nice", op:"Intercités de Nuit"},
{from:"Paris", to:"Latour-de-Carol/Enveitg", op:"Intercités de Nuit"},
{from:"Paris", to:"Briançon", op:"Intercités de Nuit"},
{from:"Stockholm", to:"Berlin", op:"SJ EuroNight"},
{from:"Stockholm", to:"Narvik", op:"Vy/SJ"},
{from:"Helsinki", to:"Rovaniemi", op:"VR"},
{from:"Helsinki", to:"Kolari", op:"VR"},
{from:"Hamburg", to:"Vienna", op:"Nightjet"},
{from:"Hamburg", to:"Innsbruck", op:"Nightjet"},
{from:"Vienna", to:"Rome", op:"Nightjet"},
{from:"Munich", to:"Rome", op:"Nightjet"},
{from:"Zürich", to:"Prague", op:"EN Canopus"},
{from:"Zürich", to:"Zagreb", op:"EN Lisinski"},
{from:"Budapest", to:"Bucharest", op:"EN Dacia"},
{from:"Vienna", to:"Bucharest", op:"EN Dacia"},
{from:"London", to:"Inverness", op:"Caledonian Sleeper"},
{from:"London", to:"Fort William", op:"Caledonian Sleeper"}
];

// --- Map init ---
const map = L.map('map', { zoomControl: true, scrollWheelZoom: true, preferCanvas:true }).setView([51.2, 10], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 10,
attribution: '© OpenStreetMap contributors'
}).addTo(map);

// --- Draw cities ---
const cityMarkers = {};
Object.entries(CITIES).forEach(([name, [lat, lng]]) => {
const m = L.circleMarker([lat, lng], { radius:5, weight:1, color:'#56b0ff', fillColor:'#56b0ff', fillOpacity:0.85 })
.bindTooltip(name, { permanent:false, direction:'top', offset:[0,-2]})
.addTo(map);
cityMarkers[name] = m;
});

// --- Route layers + rows registry ---
const registry = []; // { layer, data, row }
const makePopup = (r) => `<strong>${r.from} → ${r.to}</strong><br><span style="color:#9fb0c3">${r.op}</span>`;

ROUTES.forEach((r) => {
const a = CITIES[r.from], b = CITIES[r.to];
if(!a || !b) return;
const layer = L.polyline([a, b], { weight:3, opacity:0.9, color:'#56b0ff', lineCap:'round' })
.bindPopup(makePopup(r))
.addTo(map);
registry.push({ layer, data: r, row: null });
});

function fitAllVisible(){
const visible = registry.filter(o => map.hasLayer(o.layer));
if(visible.length === 0){ map.setView([51.2,10], 4); return; }
const group = L.featureGroup(visible.map(o => o.layer));
map.fitBounds(group.getBounds().pad(0.15));
}
fitAllVisible();

// --- Sidebar list ---
const list = document.getElementById('list');

function addRouteRow(entry){
const { data, layer } = entry;
const row = document.createElement('button');
row.type = 'button';
row.className = 'route';
row.setAttribute('aria-pressed', 'true');
row.innerHTML = `<span class="dot" aria-hidden="true"></span><div class="citypair"><strong>${data.from} → ${data.to}</strong><span class="meta">${data.op}</span></div>`;

row.addEventListener('click', () => {
const isVisible = map.hasLayer(layer);
if(isVisible){
layer.closePopup();
map.removeLayer(layer);
row.setAttribute('aria-pressed','false');
}else{
layer.addTo(map);
row.setAttribute('aria-pressed','true');
const bounds = L.latLngBounds([CITIES[data.from], CITIES[data.to]]).pad(0.25);
map.fitBounds(bounds);
layer.openPopup();
}
});

list.appendChild(row);
entry.row = row;
}

registry.forEach(addRouteRow);

// --- Buttons ---
document.getElementById('showAll').onclick = () => {
registry.forEach(o => { if(!map.hasLayer(o.layer)) o.layer.addTo(map); o.row?.setAttribute('aria-pressed','true'); });
fitAllVisible();
};
document.getElementById('hideAll').onclick = () => {
registry.forEach(o => { if(map.hasLayer(o.layer)) { o.layer.closePopup(); map.removeLayer(o.layer); } o.row?.setAttribute('aria-pressed','false'); });
};
document.getElementById('fitAll').onclick = fitAllVisible;

// --- Keyboard UX ---
document.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if(key === 'f') fitAllVisible();
if(key === 'h') document.getElementById('hideAll').click();
if(key === 's') document.getElementById('showAll').click();
});

// Resize observer to keep map sized correctly if layout changes
new ResizeObserver(() => { map.invalidateSize(); }).observe(document.getElementById('app'));
</script>
</body>
</html>

make it fancy and work - Initial Deployment

Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +393 -18
  3. prompts.txt +217 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Night Trains In Europ
3
- emoji: 🐢
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: night-trains-in-europ
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,394 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Europe Night Trains Explorer</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet">
9
+ <script src="https://unpkg.com/[email protected]/dist/aos.js"></script>
10
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
11
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
12
+ <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script>
13
+ <script src="https://unpkg.com/feather-icons"></script>
14
+ <style>
15
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
16
+
17
+ :root {
18
+ --primary: #3b82f6;
19
+ --primary-light: #93c5fd;
20
+ --primary-dark: #1d4ed8;
21
+ --dark: #0f172a;
22
+ --darker: #020617;
23
+ --light: #f8fafc;
24
+ --muted: #94a3b8;
25
+ }
26
+
27
+ body {
28
+ font-family: 'Inter', sans-serif;
29
+ background-color: var(--darker);
30
+ color: var(--light);
31
+ }
32
+
33
+ .route-card {
34
+ transition: all 0.3s ease;
35
+ border-left: 3px solid var(--primary);
36
+ }
37
+
38
+ .route-card:hover {
39
+ transform: translateY(-2px);
40
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
41
+ }
42
+
43
+ .route-card.active {
44
+ background-color: rgba(59, 130, 246, 0.1);
45
+ }
46
+
47
+ .map-container {
48
+ height: 100%;
49
+ border-radius: 0.75rem;
50
+ overflow: hidden;
51
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
52
+ }
53
+
54
+ .leaflet-container {
55
+ background-color: var(--dark) !important;
56
+ }
57
+
58
+ .leaflet-popup-content-wrapper {
59
+ background-color: var(--dark) !important;
60
+ color: var(--light) !important;
61
+ border-radius: 0.5rem !important;
62
+ }
63
+
64
+ .leaflet-popup-tip {
65
+ background-color: var(--dark) !important;
66
+ }
67
+
68
+ .leaflet-control-attribution {
69
+ background-color: rgba(15, 23, 42, 0.7) !important;
70
+ color: var(--muted) !important;
71
+ }
72
+
73
+ .vanta-bg {
74
+ position: fixed;
75
+ top: 0;
76
+ left: 0;
77
+ width: 100%;
78
+ height: 100%;
79
+ z-index: -1;
80
+ opacity: 0.15;
81
+ }
82
+
83
+ .gradient-text {
84
+ background: linear-gradient(90deg, var(--primary), var(--primary-light));
85
+ -webkit-background-clip: text;
86
+ background-clip: text;
87
+ color: transparent;
88
+ }
89
+
90
+ .scrollbar-hide::-webkit-scrollbar {
91
+ display: none;
92
+ }
93
+
94
+ .scrollbar-hide {
95
+ -ms-overflow-style: none;
96
+ scrollbar-width: none;
97
+ }
98
+ </style>
99
+ </head>
100
+ <body class="min-h-screen">
101
+ <div id="vanta-bg" class="vanta-bg"></div>
102
+
103
+ <div class="container mx-auto px-4 py-8">
104
+ <header class="mb-12 text-center" data-aos="fade-down">
105
+ <h1 class="text-4xl md:text-5xl font-bold mb-2 gradient-text">Europe Night Trains</h1>
106
+ <p class="text-lg text-gray-400 max-w-2xl mx-auto">Explore the network of overnight train routes across Europe (10+ hour journeys)</p>
107
+ </header>
108
+
109
+ <div class="flex flex-col lg:flex-row gap-8" data-aos="fade-up">
110
+ <!-- Sidebar -->
111
+ <div class="w-full lg:w-1/3 bg-slate-900/50 backdrop-blur-md rounded-xl border border-slate-800/50 overflow-hidden">
112
+ <div class="p-6 border-b border-slate-800/50">
113
+ <div class="flex items-center gap-4">
114
+ <div class="flex-1">
115
+ <h2 class="text-xl font-semibold text-white">Routes</h2>
116
+ <p class="text-sm text-gray-400">Click to highlight on map</p>
117
+ </div>
118
+ <div class="flex gap-2">
119
+ <button id="showAll" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition flex items-center gap-1">
120
+ <i data-feather="eye" class="w-4 h-4"></i> All
121
+ </button>
122
+ <button id="hideAll" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded-lg transition flex items-center gap-1">
123
+ <i data-feather="eye-off" class="w-4 h-4"></i> None
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="overflow-y-auto h-[500px] lg:h-[calc(100vh-250px)] scrollbar-hide" id="routesList">
130
+ <!-- Routes will be inserted here by JavaScript -->
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Map -->
135
+ <div class="w-full lg:w-2/3">
136
+ <div class="map-container h-[500px] lg:h-[calc(100vh-250px)]">
137
+ <div id="map" class="h-full w-full"></div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <footer class="mt-12 text-center text-gray-500 text-sm" data-aos="fade-up">
143
+ <p>Data sourced from various European rail operators • Last updated: {current_date}</p>
144
+ <p class="mt-2">Made with <i data-feather="heart" class="w-4 h-4 inline text-red-500"></i> for train enthusiasts</p>
145
+ </footer>
146
+ </div>
147
+
148
+ <script>
149
+ // Initialize animations
150
+ AOS.init({
151
+ duration: 800,
152
+ once: true
153
+ });
154
+
155
+ // Initialize Vanta.js background
156
+ VANTA.GLOBE({
157
+ el: "#vanta-bg",
158
+ mouseControls: true,
159
+ touchControls: true,
160
+ gyroControls: false,
161
+ minHeight: 200.00,
162
+ minWidth: 200.00,
163
+ scale: 1.00,
164
+ scaleMobile: 1.00,
165
+ color: "#3b82f6",
166
+ backgroundColor: "#020617",
167
+ size: 0.8
168
+ });
169
+
170
+ // City coordinates
171
+ const CITIES = {
172
+ Amsterdam: [52.379, 4.9],
173
+ Vienna: [48.208, 16.373],
174
+ Innsbruck: [47.269, 11.404],
175
+ Zürich: [47.376, 8.541],
176
+ Brussels: [50.847, 4.357],
177
+ Berlin: [52.52, 13.405],
178
+ Prague: [50.075, 14.437],
179
+ Paris: [48.857, 2.351],
180
+ Nice: [43.703, 7.266],
181
+ "Latour-de-Carol/Enveitg": [42.453, 1.918],
182
+ Briançon: [44.897, 6.643],
183
+ Stockholm: [59.33, 18.06],
184
+ Narvik: [68.438, 17.427],
185
+ Helsinki: [60.171, 24.941],
186
+ Rovaniemi: [66.503, 25.728],
187
+ Kolari: [67.35, 23.78],
188
+ Hamburg: [53.55, 9.993],
189
+ Rome: [41.902, 12.496],
190
+ Munich: [48.137, 11.575],
191
+ Zagreb: [45.815, 15.981],
192
+ Budapest: [47.497, 19.04],
193
+ Bucharest: [44.426, 26.102],
194
+ London: [51.507, -0.128],
195
+ Inverness: [57.477, -4.224],
196
+ "Fort William": [56.82, -5.105]
197
+ };
198
+
199
+ // Routes data
200
+ const ROUTES = [
201
+ { from: "Amsterdam", to: "Vienna", op: "Nightjet", duration: "14h 30m" },
202
+ { from: "Amsterdam", to: "Innsbruck", op: "Nightjet", duration: "12h 45m" },
203
+ { from: "Amsterdam", to: "Zürich", op: "Nightjet", duration: "11h 50m" },
204
+ { from: "Brussels", to: "Berlin", op: "European Sleeper", duration: "11h 30m" },
205
+ { from: "Brussels", to: "Prague", op: "European Sleeper", duration: "14h 20m" },
206
+ { from: "Paris", to: "Berlin", op: "Nightjet", duration: "13h 15m" },
207
+ { from: "Paris", to: "Nice", op: "Intercités de Nuit", duration: "10h 45m" },
208
+ { from: "Paris", to: "Latour-de-Carol/Enveitg", op: "Intercités de Nuit", duration: "11h 10m" },
209
+ { from: "Paris", to: "Briançon", op: "Intercités de Nuit", duration: "10h 30m" },
210
+ { from: "Stockholm", to: "Berlin", op: "SJ EuroNight", duration: "16h 20m" },
211
+ { from: "Stockholm", to: "Narvik", op: "Vy/SJ", duration: "18h 15m" },
212
+ { from: "Helsinki", to: "Rovaniemi", op: "VR", duration: "10h 30m" },
213
+ { from: "Helsinki", to: "Kolari", op: "VR", duration: "12h 45m" },
214
+ { from: "Hamburg", to: "Vienna", op: "Nightjet", duration: "11h 55m" },
215
+ { from: "Hamburg", to: "Innsbruck", op: "Nightjet", duration: "10h 45m" },
216
+ { from: "Vienna", to: "Rome", op: "Nightjet", duration: "13h 40m" },
217
+ { from: "Munich", to: "Rome", op: "Nightjet", duration: "11h 30m" },
218
+ { from: "Zürich", to: "Prague", op: "EN Canopus", duration: "12h 15m" },
219
+ { from: "Zürich", to: "Zagreb", op: "EN Lisinski", duration: "14h 20m" },
220
+ { from: "Budapest", to: "Bucharest", op: "EN Dacia", duration: "16h 10m" },
221
+ { from: "Vienna", to: "Bucharest", op: "EN Dacia", duration: "17h 30m" },
222
+ { from: "London", to: "Inverness", op: "Caledonian Sleeper", duration: "11h 45m" },
223
+ { from: "London", to: "Fort William", op: "Caledonian Sleeper", duration: "13h 30m" }
224
+ ];
225
+
226
+ // Initialize map
227
+ const map = L.map('map', {
228
+ zoomControl: true,
229
+ scrollWheelZoom: true,
230
+ preferCanvas: true
231
+ }).setView([51.2, 10], 5);
232
+
233
+ // Add tile layer
234
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
235
+ maxZoom: 10,
236
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
237
+ }).addTo(map);
238
+
239
+ // Add city markers
240
+ const cityMarkers = {};
241
+ Object.entries(CITIES).forEach(([name, [lat, lng]]) => {
242
+ const marker = L.circleMarker([lat, lng], {
243
+ radius: 6,
244
+ weight: 1,
245
+ color: '#3b82f6',
246
+ fillColor: '#3b82f6',
247
+ fillOpacity: 0.9
248
+ }).bindTooltip(name, {
249
+ permanent: false,
250
+ direction: 'top',
251
+ offset: [0, -6],
252
+ className: 'custom-tooltip'
253
+ }).addTo(map);
254
+
255
+ cityMarkers[name] = marker;
256
+ });
257
+
258
+ // Route layers registry
259
+ const routeRegistry = [];
260
+ const makePopup = (r) => `
261
+ <div class="p-2">
262
+ <h3 class="font-bold text-blue-400">${r.from} → ${r.to}</h3>
263
+ <p class="text-sm text-gray-300">Operator: ${r.op}</p>
264
+ <p class="text-sm text-gray-300">Duration: ${r.duration}</p>
265
+ </div>
266
+ `;
267
+
268
+ // Create route polylines
269
+ ROUTES.forEach((route) => {
270
+ const fromCoords = CITIES[route.from];
271
+ const toCoords = CITIES[route.to];
272
+
273
+ if (!fromCoords || !toCoords) return;
274
+
275
+ const polyline = L.polyline([fromCoords, toCoords], {
276
+ weight: 4,
277
+ opacity: 0.9,
278
+ color: '#3b82f6',
279
+ lineCap: 'round',
280
+ dashArray: route.op.includes('Nightjet') ? null : '10, 10'
281
+ }).bindPopup(makePopup(route));
282
+
283
+ routeRegistry.push({
284
+ layer: polyline,
285
+ data: route,
286
+ element: null
287
+ });
288
+
289
+ polyline.addTo(map);
290
+ });
291
+
292
+ // Fit map to all visible layers
293
+ function fitAllVisible() {
294
+ const visibleLayers = routeRegistry.filter(o => map.hasLayer(o.layer));
295
+ if (visibleLayers.length === 0) {
296
+ map.setView([51.2, 10], 4);
297
+ return;
298
+ }
299
+
300
+ const group = L.featureGroup(visibleLayers.map(o => o.layer));
301
+ map.fitBounds(group.getBounds().pad(0.15));
302
+ }
303
+
304
+ // Create route list in sidebar
305
+ const routesList = document.getElementById('routesList');
306
+
307
+ function createRouteElement(entry) {
308
+ const { data, layer } = entry;
309
+ const element = document.createElement('div');
310
+ element.className = 'route-card p-4 border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer transition';
311
+ element.innerHTML = `
312
+ <div class="flex items-start gap-3">
313
+ <div class="mt-1 w-3 h-3 rounded-full bg-blue-500 flex-shrink-0"></div>
314
+ <div class="flex-1">
315
+ <h3 class="font-medium text-white">${data.from} → ${data.to}</h3>
316
+ <div class="flex items-center justify-between mt-1">
317
+ <span class="text-sm text-gray-400">${data.op}</span>
318
+ <span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-1 rounded-full">${data.duration}</span>
319
+ </div>
320
+ </div>
321
+ <i data-feather="chevron-right" class="text-gray-500 w-5 h-5"></i>
322
+ </div>
323
+ `;
324
+
325
+ element.addEventListener('click', () => {
326
+ // Toggle route visibility
327
+ if (map.hasLayer(layer)) {
328
+ layer.closePopup();
329
+ map.removeLayer(layer);
330
+ element.classList.remove('active');
331
+ } else {
332
+ layer.addTo(map);
333
+ element.classList.add('active');
334
+ const bounds = L.latLngBounds([CITIES[data.from], CITIES[data.to]]).pad(0.25);
335
+ map.fitBounds(bounds);
336
+ layer.openPopup();
337
+ }
338
+ });
339
+
340
+ routesList.appendChild(element);
341
+ entry.element = element;
342
+ }
343
+
344
+ // Initialize route list
345
+ routeRegistry.forEach(createRouteElement);
346
+
347
+ // Button event handlers
348
+ document.getElementById('showAll').addEventListener('click', () => {
349
+ routeRegistry.forEach(entry => {
350
+ if (!map.hasLayer(entry.layer)) {
351
+ entry.layer.addTo(map);
352
+ }
353
+ if (entry.element) {
354
+ entry.element.classList.add('active');
355
+ }
356
+ });
357
+ fitAllVisible();
358
+ });
359
+
360
+ document.getElementById('hideAll').addEventListener('click', () => {
361
+ routeRegistry.forEach(entry => {
362
+ if (map.hasLayer(entry.layer)) {
363
+ entry.layer.closePopup();
364
+ map.removeLayer(entry.layer);
365
+ }
366
+ if (entry.element) {
367
+ entry.element.classList.remove('active');
368
+ }
369
+ });
370
+ });
371
+
372
+ // Keyboard shortcuts
373
+ document.addEventListener('keydown', (e) => {
374
+ const key = e.key.toLowerCase();
375
+ if (key === 'a') document.getElementById('showAll').click();
376
+ if (key === 'n') document.getElementById('hideAll').click();
377
+ if (key === 'f') fitAllVisible();
378
+ });
379
+
380
+ // Handle responsive resizing
381
+ new ResizeObserver(() => {
382
+ map.invalidateSize();
383
+ }).observe(document.getElementById('map'));
384
+
385
+ // Initialize Feather icons
386
+ feather.replace();
387
+
388
+ // Set current date in footer
389
+ const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
390
+ document.querySelector('footer p:first-child').textContent =
391
+ document.querySelector('footer p:first-child').textContent.replace('{current_date}', currentDate);
392
+ </script>
393
+ </body>
394
  </html>
prompts.txt ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Europe Night Trains (10h+)</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="https://unpkg.com/[email protected]/dist/leaflet.css"
10
+ integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
11
+ crossorigin=""
12
+ />
13
+ <style>
14
+ :root { --bg:#0b0f14; --panel:#10161f; --muted:#9fb0c3; --accent:#56b0ff; }
15
+ * { box-sizing: border-box; }
16
+ html, body { height:100%; margin:0; background:var(--bg); color:#e6edf3; font:14px system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji"; }
17
+ /* Ensure the map actually gets a concrete height via the grid parent */
18
+ #app { display:grid; grid-template-columns: 320px 1fr; grid-template-rows: 56px 1fr; height:100vh; min-height:0; }
19
+ header { grid-column:1 / span 2; display:flex; align-items:center; gap:.75rem; padding:12px 16px; background:var(--panel); border-bottom:1px solid #1b2532; }
20
+ header h1 { font-size:16px; margin:0; font-weight:600; letter-spacing:.2px; }
21
+ header .sub { color:var(--muted); font-weight:400; }
22
+ #sidebar { background:var(--panel); border-right:1px solid #1b2532; overflow:auto; min-height:0; }
23
+ #map { width:100%; height:100%; min-height:0; }
24
+ .group { padding:10px 12px; border-bottom:1px solid #192330; }
25
+ .group h3 { margin:0 0 8px 0; font-size:12px; color:#87a3bd; text-transform:uppercase; letter-spacing:.12em; }
26
+ .route { display:flex; align-items:center; gap:.5rem; padding:8px; border-radius:10px; cursor:pointer; transition:opacity .15s ease, background .15s ease; }
27
+ .route:hover { background:#0f1722; }
28
+ .route[aria-pressed="false"] { opacity:0.45; }
29
+ .dot { width:10px; height:10px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 2px rgba(86,176,255,.2); flex:0 0 10px; }
30
+ .citypair { display:flex; flex-direction:column; line-height:1.15; }
31
+ .citypair strong { font-size:13px; }
32
+ .meta { font-size:12px; color:var(--muted); }
33
+ .controls { display:flex; gap:.5rem; padding:8px 12px; position:sticky; top:0; background:linear-gradient(var(--panel), var(--panel)); border-bottom:1px solid #1b2532; z-index:5; }
34
+ .btn { padding:6px 10px; border-radius:10px; background:#0f1722; border:1px solid #1b2532; color:#cfe3f6; cursor:pointer; }
35
+ .btn:hover { background:#122033; }
36
+ .legend { position:absolute; right:12px; bottom:12px; background:var(--panel); border:1px solid #1b2532; padding:8px 10px; border-radius:12px; color:#cfe3f6; font-size:12px; }
37
+ .legend .swatch { display:inline-block; width:12px; height:3px; background:var(--accent); margin:0 6px 0 0; vertical-align:middle; border-radius:2px; }
38
+ .leaflet-container { background:#0a0e13; }
39
+ a, .leaflet-popup-content a { color:#9bd1ff; }
40
+ @media (max-width: 880px) {
41
+ #app { grid-template-columns: 1fr; grid-template-rows: 56px 220px 1fr; }
42
+ #sidebar { grid-row: 2; border-right: none; border-bottom:1px solid #1b2532; }
43
+ #map { grid-row: 3; }
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div id="app">
49
+ <header>
50
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18" stroke="#56b0ff" stroke-width="1.6" stroke-linecap="round"/></svg>
51
+ <h1>Europe Night Trains <span class="sub">(10h+ start → terminus)</span></h1>
52
+ </header>
53
+ <aside id="sidebar" aria-label="Routes sidebar">
54
+ <div class="controls">
55
+ <button class="btn" id="showAll" type="button" title="Show all routes (S)">Show all</button>
56
+ <button class="btn" id="hideAll" type="button" title="Hide all routes (H)">Hide all</button>
57
+ <button class="btn" id="fitAll" type="button" title="Fit to Europe (F)">Fit to Europe</button>
58
+ </div>
59
+ <div class="group" id="list"></div>
60
+ </aside>
61
+ <main id="map" aria-label="Map of Europe with night train routes"></main>
62
+ </div>
63
+
64
+ <div class="legend" role="note"><span class="swatch"></span> Night train route (start → terminus)</div>
65
+
66
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
67
+ <script>
68
+ // --- City coordinates (approximate) ---
69
+ const CITIES = {
70
+ Amsterdam:[52.379, 4.9],
71
+ Vienna:[48.208, 16.373],
72
+ Innsbruck:[47.269, 11.404],
73
+ Zürich:[47.376, 8.541],
74
+ Brussels:[50.847, 4.357],
75
+ Berlin:[52.52, 13.405],
76
+ Prague:[50.075, 14.437],
77
+ Paris:[48.857, 2.351],
78
+ Nice:[43.703, 7.266],
79
+ "Latour-de-Carol/Enveitg":[42.453, 1.918],
80
+ Briançon:[44.897, 6.643],
81
+ Stockholm:[59.33, 18.06],
82
+ Narvik:[68.438, 17.427],
83
+ Helsinki:[60.171, 24.941],
84
+ Rovaniemi:[66.503, 25.728],
85
+ Kolari:[67.35, 23.78],
86
+ Hamburg:[53.55, 9.993],
87
+ Rome:[41.902, 12.496],
88
+ Munich:[48.137, 11.575],
89
+ Zagreb:[45.815, 15.981],
90
+ Budapest:[47.497, 19.04],
91
+ Bucharest:[44.426, 26.102],
92
+ London:[51.507, -0.128],
93
+ Inverness:[57.477, -4.224],
94
+ "Fort William":[56.82, -5.105]
95
+ };
96
+
97
+ // --- Routes ≥10h (start → terminus) ---
98
+ const ROUTES = [
99
+ {from:"Amsterdam", to:"Vienna", op:"Nightjet"},
100
+ {from:"Amsterdam", to:"Innsbruck", op:"Nightjet"},
101
+ {from:"Amsterdam", to:"Zürich", op:"Nightjet"},
102
+ {from:"Brussels", to:"Berlin", op:"European Sleeper"},
103
+ {from:"Brussels", to:"Prague", op:"European Sleeper"},
104
+ {from:"Paris", to:"Berlin", op:"Nightjet"},
105
+ {from:"Paris", to:"Nice", op:"Intercités de Nuit"},
106
+ {from:"Paris", to:"Latour-de-Carol/Enveitg", op:"Intercités de Nuit"},
107
+ {from:"Paris", to:"Briançon", op:"Intercités de Nuit"},
108
+ {from:"Stockholm", to:"Berlin", op:"SJ EuroNight"},
109
+ {from:"Stockholm", to:"Narvik", op:"Vy/SJ"},
110
+ {from:"Helsinki", to:"Rovaniemi", op:"VR"},
111
+ {from:"Helsinki", to:"Kolari", op:"VR"},
112
+ {from:"Hamburg", to:"Vienna", op:"Nightjet"},
113
+ {from:"Hamburg", to:"Innsbruck", op:"Nightjet"},
114
+ {from:"Vienna", to:"Rome", op:"Nightjet"},
115
+ {from:"Munich", to:"Rome", op:"Nightjet"},
116
+ {from:"Zürich", to:"Prague", op:"EN Canopus"},
117
+ {from:"Zürich", to:"Zagreb", op:"EN Lisinski"},
118
+ {from:"Budapest", to:"Bucharest", op:"EN Dacia"},
119
+ {from:"Vienna", to:"Bucharest", op:"EN Dacia"},
120
+ {from:"London", to:"Inverness", op:"Caledonian Sleeper"},
121
+ {from:"London", to:"Fort William", op:"Caledonian Sleeper"}
122
+ ];
123
+
124
+ // --- Map init ---
125
+ const map = L.map('map', { zoomControl: true, scrollWheelZoom: true, preferCanvas:true }).setView([51.2, 10], 5);
126
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
127
+ maxZoom: 10,
128
+ attribution: '&copy; OpenStreetMap contributors'
129
+ }).addTo(map);
130
+
131
+ // --- Draw cities ---
132
+ const cityMarkers = {};
133
+ Object.entries(CITIES).forEach(([name, [lat, lng]]) => {
134
+ const m = L.circleMarker([lat, lng], { radius:5, weight:1, color:'#56b0ff', fillColor:'#56b0ff', fillOpacity:0.85 })
135
+ .bindTooltip(name, { permanent:false, direction:'top', offset:[0,-2]})
136
+ .addTo(map);
137
+ cityMarkers[name] = m;
138
+ });
139
+
140
+ // --- Route layers + rows registry ---
141
+ const registry = []; // { layer, data, row }
142
+ const makePopup = (r) => `<strong>${r.from} → ${r.to}</strong><br><span style="color:#9fb0c3">${r.op}</span>`;
143
+
144
+ ROUTES.forEach((r) => {
145
+ const a = CITIES[r.from], b = CITIES[r.to];
146
+ if(!a || !b) return;
147
+ const layer = L.polyline([a, b], { weight:3, opacity:0.9, color:'#56b0ff', lineCap:'round' })
148
+ .bindPopup(makePopup(r))
149
+ .addTo(map);
150
+ registry.push({ layer, data: r, row: null });
151
+ });
152
+
153
+ function fitAllVisible(){
154
+ const visible = registry.filter(o => map.hasLayer(o.layer));
155
+ if(visible.length === 0){ map.setView([51.2,10], 4); return; }
156
+ const group = L.featureGroup(visible.map(o => o.layer));
157
+ map.fitBounds(group.getBounds().pad(0.15));
158
+ }
159
+ fitAllVisible();
160
+
161
+ // --- Sidebar list ---
162
+ const list = document.getElementById('list');
163
+
164
+ function addRouteRow(entry){
165
+ const { data, layer } = entry;
166
+ const row = document.createElement('button');
167
+ row.type = 'button';
168
+ row.className = 'route';
169
+ row.setAttribute('aria-pressed', 'true');
170
+ row.innerHTML = `<span class="dot" aria-hidden="true"></span><div class="citypair"><strong>${data.from} → ${data.to}</strong><span class="meta">${data.op}</span></div>`;
171
+
172
+ row.addEventListener('click', () => {
173
+ const isVisible = map.hasLayer(layer);
174
+ if(isVisible){
175
+ layer.closePopup();
176
+ map.removeLayer(layer);
177
+ row.setAttribute('aria-pressed','false');
178
+ }else{
179
+ layer.addTo(map);
180
+ row.setAttribute('aria-pressed','true');
181
+ const bounds = L.latLngBounds([CITIES[data.from], CITIES[data.to]]).pad(0.25);
182
+ map.fitBounds(bounds);
183
+ layer.openPopup();
184
+ }
185
+ });
186
+
187
+ list.appendChild(row);
188
+ entry.row = row;
189
+ }
190
+
191
+ registry.forEach(addRouteRow);
192
+
193
+ // --- Buttons ---
194
+ document.getElementById('showAll').onclick = () => {
195
+ registry.forEach(o => { if(!map.hasLayer(o.layer)) o.layer.addTo(map); o.row?.setAttribute('aria-pressed','true'); });
196
+ fitAllVisible();
197
+ };
198
+ document.getElementById('hideAll').onclick = () => {
199
+ registry.forEach(o => { if(map.hasLayer(o.layer)) { o.layer.closePopup(); map.removeLayer(o.layer); } o.row?.setAttribute('aria-pressed','false'); });
200
+ };
201
+ document.getElementById('fitAll').onclick = fitAllVisible;
202
+
203
+ // --- Keyboard UX ---
204
+ document.addEventListener('keydown', (e) => {
205
+ const key = e.key.toLowerCase();
206
+ if(key === 'f') fitAllVisible();
207
+ if(key === 'h') document.getElementById('hideAll').click();
208
+ if(key === 's') document.getElementById('showAll').click();
209
+ });
210
+
211
+ // Resize observer to keep map sized correctly if layout changes
212
+ new ResizeObserver(() => { map.invalidateSize(); }).observe(document.getElementById('app'));
213
+ </script>
214
+ </body>
215
+ </html>
216
+
217
+ make it fancy and work