Spaces:
Runtime error
Runtime error
Commit Β·
36362ca
1
Parent(s): 24e6b37
UX: drop Exports tab, auto-spin Roulette on first visit
Browse files- Remove the Exports sub-tab entirely β it was a one-shot action poorly
placed as a dedicated tab. Download buttons (KML / GeoJSON / CSV) and
the Google My Maps instructions now live in a #download-bar at the top
of Browse, revealed when showSubTabBar() fires.
- Roulette now auto-spins on first visit: showSubTab('roulette') triggers
spin-btn.click() if #pick-card is still hidden. Subsequent tab switches
don't re-spin (card is already visible).
- Clean up orphaned success-block / success-ring CSS (no longer used).
- Update test_routes.py: assert no exports tab, assert download-bar is
inside the explore panel.
- CLAUDE.md + README.md updated to reflect two-sub-tab nav.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md +10 -5
- README.md +4 -5
- tests/test_routes.py +2 -1
- web/static/app.css +1 -21
- web/static/app.js +12 -8
- web/templates/index.html +21 -31
CLAUDE.md
CHANGED
|
@@ -55,15 +55,16 @@ The app has a **permanent two-level nav** visible from first page load:
|
|
| 55 |
```
|
| 56 |
[π₯ Extract] [πΊ Explore] β #top-nav, always sticky at top
|
| 57 |
βββ import card (shown until data loads)
|
| 58 |
-
βββ [
|
| 59 |
```
|
| 60 |
|
| 61 |
- **Extract tab** (`#top-panel-extract`): upload card, model picker, run button, progress card.
|
| 62 |
- **Explore tab** (`#top-panel-explore`): import card at top; once data loads the import card hides
|
| 63 |
-
and `#sub-tab-bar` appears with
|
| 64 |
-
- After extraction completes β `showTopTab('explore')` β `showSubTabBar(jobId)` β `showSubTab('roulette')` + nudge toast.
|
| 65 |
- After KML/CSV import β same but `showSubTab('browse')`.
|
| 66 |
- `οΌ New` button: `showTopTab('extract')` (no page reload β preserves Extract form state).
|
|
|
|
| 67 |
|
| 68 |
**Food-only filter** β `FOOD_CATEGORIES = new Set(['Restaurant','Bar','Cafe','Bakery','Market'])` in
|
| 69 |
`app.js`; matching `FOOD_CATEGORIES` set in `web/app.py`. `filterState.foodOnly` defaults `true`
|
|
@@ -73,10 +74,14 @@ The app has a **permanent two-level nav** visible from first page load:
|
|
| 73 |
**`POST /import`** β accepts `.kml` or `.csv`, writes job CSV+KML, returns `{job_id, rows, pinned}`
|
| 74 |
with `step:"done"` immediately (no SSE stream).
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
Key JS functions:
|
| 77 |
- `showTopTab(name)` β switches top-level Extract / Explore panels
|
| 78 |
-
- `showSubTabBar(jobId)` β hides import card, reveals `#sub-tab-bar` inside Explore
|
| 79 |
-
- `showSubTab(name)` β switches sub-panels (
|
| 80 |
- `updateTabBadges()` β sets badge counts and stat strip
|
| 81 |
|
| 82 |
KML parser convention: folder nesting = country (outer) β city (inner); `<coordinates>` order is
|
|
|
|
| 55 |
```
|
| 56 |
[π₯ Extract] [πΊ Explore] β #top-nav, always sticky at top
|
| 57 |
βββ import card (shown until data loads)
|
| 58 |
+
βββ [π² Roulette] [π Browse] β #sub-tab-bar, after data loads
|
| 59 |
```
|
| 60 |
|
| 61 |
- **Extract tab** (`#top-panel-extract`): upload card, model picker, run button, progress card.
|
| 62 |
- **Explore tab** (`#top-panel-explore`): import card at top; once data loads the import card hides
|
| 63 |
+
and `#sub-tab-bar` appears with two sub-panels.
|
| 64 |
+
- After extraction completes β `showTopTab('explore')` β `showSubTabBar(jobId)` β `showSubTab('roulette')` (auto-spins) + nudge toast.
|
| 65 |
- After KML/CSV import β same but `showSubTab('browse')`.
|
| 66 |
- `οΌ New` button: `showTopTab('extract')` (no page reload β preserves Extract form state).
|
| 67 |
+
- **Roulette auto-spins** on first visit: `showSubTab('roulette')` triggers `$('spin-btn').click()` if `#pick-card` is still hidden (no previous result). Subsequent tab switches do not re-spin.
|
| 68 |
|
| 69 |
**Food-only filter** β `FOOD_CATEGORIES = new Set(['Restaurant','Bar','Cafe','Bakery','Market'])` in
|
| 70 |
`app.js`; matching `FOOD_CATEGORIES` set in `web/app.py`. `filterState.foodOnly` defaults `true`
|
|
|
|
| 74 |
**`POST /import`** β accepts `.kml` or `.csv`, writes job CSV+KML, returns `{job_id, rows, pinned}`
|
| 75 |
with `step:"done"` immediately (no SSE stream).
|
| 76 |
|
| 77 |
+
**Downloads** live in the Browse sub-panel (`#download-bar`), revealed when `showSubTabBar` is called.
|
| 78 |
+
Buttons: `#download-btn` (KML), `#download-geojson-btn`, `#download-csv-btn` β onclick set in
|
| 79 |
+
both `showDownload` (post-extraction) and `handleImportFile` (post-import).
|
| 80 |
+
|
| 81 |
Key JS functions:
|
| 82 |
- `showTopTab(name)` β switches top-level Extract / Explore panels
|
| 83 |
+
- `showSubTabBar(jobId)` β hides import card, reveals `#sub-tab-bar` + `#download-bar` inside Explore
|
| 84 |
+
- `showSubTab(name)` β switches sub-panels (roulette / browse); auto-spins roulette on first visit
|
| 85 |
- `updateTabBadges()` β sets badge counts and stat strip
|
| 86 |
|
| 87 |
KML parser convention: folder nesting = country (outer) β city (inner); `<coordinates>` order is
|
README.md
CHANGED
|
@@ -304,11 +304,10 @@ The UI lets you:
|
|
| 304 |
- **Watch real-time progress** as posts are extracted, geocoded, and mapped
|
| 305 |
- **Two-tab layout visible from first load** β the app shows two top-level tabs immediately on the landing page:
|
| 306 |
- **π₯ Extract** β upload your Instagram export, pick a model, run the pipeline, watch real-time progress
|
| 307 |
-
- **πΊ Explore** β for returning users; drop your `places_map.kml` or `places_full.csv` here to skip re-extraction entirely. Once data loads,
|
| 308 |
-
- **
|
| 309 |
-
- **
|
| 310 |
-
|
| 311 |
-
- After extraction completes, the app automatically jumps to Explore β Roulette
|
| 312 |
- **Preview pins on a map** β an in-app Leaflet/OpenStreetMap map renders every geocoded place so you can sanity-check for misplaced pins (e.g. a venue geocoded into the wrong country) before exporting. It follows your active filters and pins link back to the original reel
|
| 313 |
- **Filter and search** β text search by name/city, country, category, and cuisine dropdowns, multi-select price range chips, "missing pins" toggle, and a **π΄ Food only** toggle (on by default) that limits Browse and Roulette to food/drink places (Restaurant, Bar, Cafe, Bakery, Market) β all client-side with instant results
|
| 314 |
- **Fix extractions inline** β click any field (name, city, state, category, cuisine, occasion) directly on the card to edit it; changes are saved to the CSV immediately
|
|
|
|
| 304 |
- **Watch real-time progress** as posts are extracted, geocoded, and mapped
|
| 305 |
- **Two-tab layout visible from first load** β the app shows two top-level tabs immediately on the landing page:
|
| 306 |
- **π₯ Extract** β upload your Instagram export, pick a model, run the pipeline, watch real-time progress
|
| 307 |
+
- **πΊ Explore** β for returning users; drop your `places_map.kml` or `places_full.csv` here to skip re-extraction entirely. Once data loads, two sub-tabs appear:
|
| 308 |
+
- **π² Roulette** β random place picker that auto-spins on first visit, filtered by category, city, or distance from your location. Default after extraction
|
| 309 |
+
- **π Browse** β stats bar, interactive Leaflet map, full-text search, filters, and card grid with KML / GeoJSON / CSV download buttons at the top. Default after import
|
| 310 |
+
- After extraction completes, the app automatically jumps to Explore β Roulette (spins immediately)
|
|
|
|
| 311 |
- **Preview pins on a map** β an in-app Leaflet/OpenStreetMap map renders every geocoded place so you can sanity-check for misplaced pins (e.g. a venue geocoded into the wrong country) before exporting. It follows your active filters and pins link back to the original reel
|
| 312 |
- **Filter and search** β text search by name/city, country, category, and cuisine dropdowns, multi-select price range chips, "missing pins" toggle, and a **π΄ Food only** toggle (on by default) that limits Browse and Roulette to food/drink places (Restaurant, Bar, Cafe, Bakery, Market) β all client-side with instant results
|
| 313 |
- **Fix extractions inline** β click any field (name, city, state, category, cuisine, occasion) directly on the card to edit it; changes are saved to the CSV immediately
|
tests/test_routes.py
CHANGED
|
@@ -70,9 +70,10 @@ try:
|
|
| 70 |
html.index('id="sub-tab-bar"') > html.index('id="top-panel-explore"'))
|
| 71 |
check("import-card inside explore panel",
|
| 72 |
html.index('id="import-card"') > html.index('id="top-panel-explore"'))
|
| 73 |
-
check("sub-tab exports present", 'data-sub-tab="exports"' in html)
|
| 74 |
check("sub-tab roulette present", 'data-sub-tab="roulette"' in html)
|
| 75 |
check("sub-tab browse present", 'data-sub-tab="browse"' in html)
|
|
|
|
|
|
|
| 76 |
check("no legacy #tab-bar id", 'id="tab-bar"' not in html)
|
| 77 |
check("no legacy .tab-panel class", 'class="tab-panel"' not in html)
|
| 78 |
|
|
|
|
| 70 |
html.index('id="sub-tab-bar"') > html.index('id="top-panel-explore"'))
|
| 71 |
check("import-card inside explore panel",
|
| 72 |
html.index('id="import-card"') > html.index('id="top-panel-explore"'))
|
|
|
|
| 73 |
check("sub-tab roulette present", 'data-sub-tab="roulette"' in html)
|
| 74 |
check("sub-tab browse present", 'data-sub-tab="browse"' in html)
|
| 75 |
+
check("no exports sub-tab", 'data-sub-tab="exports"' not in html)
|
| 76 |
+
check("download-bar in browse panel", html.index('id="download-bar"') > html.index('id="top-panel-explore"'))
|
| 77 |
check("no legacy #tab-bar id", 'id="tab-bar"' not in html)
|
| 78 |
check("no legacy .tab-panel class", 'class="tab-panel"' not in html)
|
| 79 |
|
web/static/app.css
CHANGED
|
@@ -259,27 +259,7 @@
|
|
| 259 |
}
|
| 260 |
.status-msg.error { color: #ff6b6b; }
|
| 261 |
|
| 262 |
-
/* ββ Download ββββββββββββββββββββββββββ
|
| 263 |
-
#download-section { display: none; }
|
| 264 |
-
|
| 265 |
-
.success-ring {
|
| 266 |
-
display: inline-block;
|
| 267 |
-
padding: 3px;
|
| 268 |
-
background: var(--grad);
|
| 269 |
-
border-radius: 50%;
|
| 270 |
-
margin-bottom: 16px;
|
| 271 |
-
}
|
| 272 |
-
.success-inner {
|
| 273 |
-
background: var(--bg);
|
| 274 |
-
border-radius: 50%;
|
| 275 |
-
width: 64px; height: 64px;
|
| 276 |
-
display: flex; align-items: center; justify-content: center;
|
| 277 |
-
font-size: 28px;
|
| 278 |
-
}
|
| 279 |
-
.success-block { text-align: center; margin-bottom: 20px; }
|
| 280 |
-
.success-title { font-size: 1.3rem; font-weight: 800; margin-bottom: 6px; }
|
| 281 |
-
.success-stats { font-size: 0.85rem; color: var(--muted); }
|
| 282 |
-
.success-stats strong { color: var(--ig3); }
|
| 283 |
|
| 284 |
.download-row {
|
| 285 |
display: flex; gap: 10px; margin-top: 10px;
|
|
|
|
| 259 |
}
|
| 260 |
.status-msg.error { color: #ff6b6b; }
|
| 261 |
|
| 262 |
+
/* ββ Download bar (Browse panel) ββββββββββββββββββββββββββ */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
.download-row {
|
| 265 |
display: flex; gap: 10px; margin-top: 10px;
|
web/static/app.js
CHANGED
|
@@ -33,7 +33,13 @@ function showSubTab(name) {
|
|
| 33 |
);
|
| 34 |
const panel = $('panel-' + name);
|
| 35 |
if (panel) panel.classList.add('active');
|
| 36 |
-
if (name === 'roulette'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
if (name === 'browse' && _map) setTimeout(() => _map.invalidateSize(), 60);
|
| 38 |
}
|
| 39 |
|
|
@@ -41,6 +47,7 @@ function showSubTabBar(jobId) {
|
|
| 41 |
currentJobId = jobId;
|
| 42 |
$('import-card').style.display = 'none';
|
| 43 |
$('sub-tab-bar').style.display = 'block';
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
function updateTabBadges() {
|
|
@@ -145,6 +152,9 @@ async function handleImportFile(f) {
|
|
| 145 |
status.textContent = '';
|
| 146 |
showTopTab('explore');
|
| 147 |
showSubTabBar(data.job_id);
|
|
|
|
|
|
|
|
|
|
| 148 |
showSubTab('browse');
|
| 149 |
loadResults(data.job_id);
|
| 150 |
if (_clientGeocode) geocodeClientSide();
|
|
@@ -583,17 +593,11 @@ function listenProgress(jobId) {
|
|
| 583 |
function showDownload(d) {
|
| 584 |
$('progress-section').style.display = 'none';
|
| 585 |
|
| 586 |
-
const stats = [];
|
| 587 |
-
if (d.extracted) stats.push(`<strong>${d.extracted}</strong> places found`);
|
| 588 |
-
if (d.pinned) stats.push(`<strong>${d.pinned}</strong> geocoded`);
|
| 589 |
-
if (d.cost) stats.push(`~<strong>$${d.cost}</strong> Claude cost`);
|
| 590 |
-
$('success-stats').innerHTML = stats.join(' Β· ');
|
| 591 |
-
|
| 592 |
$('download-btn').onclick = () => { window.location = '/download/' + currentJobId; };
|
| 593 |
$('download-geojson-btn').onclick = () => { window.location = '/download/' + currentJobId + '/geojson'; };
|
| 594 |
$('download-csv-btn').onclick = () => { window.location = '/download/' + currentJobId + '/csv'; };
|
| 595 |
|
| 596 |
-
// Jump to Explore, reveal sub-tab bar, default to Roulette
|
| 597 |
showTopTab('explore');
|
| 598 |
showSubTabBar(currentJobId);
|
| 599 |
showSubTab('roulette');
|
|
|
|
| 33 |
);
|
| 34 |
const panel = $('panel-' + name);
|
| 35 |
if (panel) panel.classList.add('active');
|
| 36 |
+
if (name === 'roulette') {
|
| 37 |
+
if (!_geolocationRequested) _requestGeolocation();
|
| 38 |
+
// Auto-spin on first visit (card not yet shown)
|
| 39 |
+
if (currentJobId && $('pick-card').style.display === 'none') {
|
| 40 |
+
setTimeout(() => $('spin-btn').click(), 120);
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
if (name === 'browse' && _map) setTimeout(() => _map.invalidateSize(), 60);
|
| 44 |
}
|
| 45 |
|
|
|
|
| 47 |
currentJobId = jobId;
|
| 48 |
$('import-card').style.display = 'none';
|
| 49 |
$('sub-tab-bar').style.display = 'block';
|
| 50 |
+
$('download-bar').style.display = 'block';
|
| 51 |
}
|
| 52 |
|
| 53 |
function updateTabBadges() {
|
|
|
|
| 152 |
status.textContent = '';
|
| 153 |
showTopTab('explore');
|
| 154 |
showSubTabBar(data.job_id);
|
| 155 |
+
$('download-btn').onclick = () => { window.location = '/download/' + data.job_id; };
|
| 156 |
+
$('download-geojson-btn').onclick = () => { window.location = '/download/' + data.job_id + '/geojson'; };
|
| 157 |
+
$('download-csv-btn').onclick = () => { window.location = '/download/' + data.job_id + '/csv'; };
|
| 158 |
showSubTab('browse');
|
| 159 |
loadResults(data.job_id);
|
| 160 |
if (_clientGeocode) geocodeClientSide();
|
|
|
|
| 593 |
function showDownload(d) {
|
| 594 |
$('progress-section').style.display = 'none';
|
| 595 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
$('download-btn').onclick = () => { window.location = '/download/' + currentJobId; };
|
| 597 |
$('download-geojson-btn').onclick = () => { window.location = '/download/' + currentJobId + '/geojson'; };
|
| 598 |
$('download-csv-btn').onclick = () => { window.location = '/download/' + currentJobId + '/csv'; };
|
| 599 |
|
| 600 |
+
// Jump to Explore, reveal sub-tab bar, default to Roulette (auto-spins)
|
| 601 |
showTopTab('explore');
|
| 602 |
showSubTabBar(currentJobId);
|
| 603 |
showSubTab('roulette');
|
web/templates/index.html
CHANGED
|
@@ -236,8 +236,7 @@
|
|
| 236 |
<!-- Sub-tab bar (hidden until data loads) -->
|
| 237 |
<div id="sub-tab-bar">
|
| 238 |
<div class="sub-tab-bar-inner">
|
| 239 |
-
<button class="sub-tab-btn active" data-sub-tab="
|
| 240 |
-
<button class="sub-tab-btn" data-sub-tab="roulette">π² Roulette<span class="tab-badge" id="badge-roulette"></span></button>
|
| 241 |
<button class="sub-tab-btn" data-sub-tab="browse">π Browse<span class="tab-badge" id="badge-browse"></span></button>
|
| 242 |
<button class="sub-tab-btn sub-tab-btn-ghost" id="new-extract-btn">οΌ New</button>
|
| 243 |
</div>
|
|
@@ -248,35 +247,8 @@
|
|
| 248 |
<div id="tab-stat-strip"></div>
|
| 249 |
</div>
|
| 250 |
|
| 251 |
-
<!-- ββ Exports sub-panel ββββββββββββββββββββββββββββββββββββ -->
|
| 252 |
-
<div class="sub-panel" id="panel-exports">
|
| 253 |
-
<div class="card" id="download-section">
|
| 254 |
-
<div class="success-block">
|
| 255 |
-
<div class="success-ring"><div class="success-inner">β
</div></div>
|
| 256 |
-
<div class="success-title">Your map is ready!</div>
|
| 257 |
-
<div class="success-stats" id="success-stats"></div>
|
| 258 |
-
</div>
|
| 259 |
-
<button class="btn" id="download-btn">β¬ Download KML β Google My Maps</button>
|
| 260 |
-
<details style="max-width:520px;margin:10px auto 0;text-align:left;font-size:0.82rem;color:var(--muted);">
|
| 261 |
-
<summary style="cursor:pointer;font-weight:600;">How to import into Google My Maps (and use it on your phone)</summary>
|
| 262 |
-
<ol style="line-height:1.8;padding-left:18px;margin-top:8px;">
|
| 263 |
-
<li>Go to <a href="https://mymaps.google.com" target="_blank" rel="noopener">mymaps.google.com</a> β <strong>Create a new map</strong></li>
|
| 264 |
-
<li>Click <strong>Import</strong> on the base layer and upload the KML you just downloaded</li>
|
| 265 |
-
<li>Pins appear organised by country β city, each linking back to the original reel</li>
|
| 266 |
-
<li><strong>On your phone:</strong> open the Google Maps app β <em>Saved β Maps</em> to find your custom map and navigate to any pin</li>
|
| 267 |
-
</ol>
|
| 268 |
-
<p style="margin-top:6px;">Prefer no Google account? The KML also imports into <a href="https://organicmaps.app" target="_blank" rel="noopener">Organic Maps</a> or OsmAnd β fully offline, great for travelling abroad.</p>
|
| 269 |
-
</details>
|
| 270 |
-
<div class="download-row">
|
| 271 |
-
<button class="btn btn-outline" id="download-geojson-btn">β¬ GeoJSON</button>
|
| 272 |
-
<button class="btn btn-outline" id="download-csv-btn">β¬ CSV</button>
|
| 273 |
-
</div>
|
| 274 |
-
<p class="download-note">GeoJSON includes geocoded pins only Β· CSV includes all rows</p>
|
| 275 |
-
</div>
|
| 276 |
-
</div>
|
| 277 |
-
|
| 278 |
<!-- ββ Roulette sub-panel βββββββββββββββββββββββββββββββββββ -->
|
| 279 |
-
<div class="sub-panel" id="panel-roulette">
|
| 280 |
<div class="card" id="roulette-section">
|
| 281 |
<p class="card-title">π² Place Roulette</p>
|
| 282 |
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:18px;">Can't decide where to go? Let the algorithm decide.</p>
|
|
@@ -311,7 +283,7 @@
|
|
| 311 |
<button class="roulette-btn" id="spin-btn">
|
| 312 |
<span class="dice">π²</span><span>Spin the wheel</span>
|
| 313 |
</button>
|
| 314 |
-
<div class="pick-card" id="pick-card">
|
| 315 |
<div class="pick-name" id="pick-name"></div>
|
| 316 |
<div class="pick-loc" id="pick-loc"></div>
|
| 317 |
<div class="pick-meta" id="pick-meta"></div>
|
|
@@ -331,6 +303,24 @@
|
|
| 331 |
<button class="new-banner-dismiss" id="new-banner-dismiss" type="button" title="Dismiss">β</button>
|
| 332 |
</div>
|
| 333 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
<div class="card" id="stats-bar" style="display:none; max-width:960px; margin:0 auto 16px;">
|
| 335 |
<div class="stats-row">
|
| 336 |
<div class="stat-chip"><span class="stat-icon">π</span><div><div class="stat-val" id="stat-total">β</div><div class="stat-label">places</div></div></div>
|
|
|
|
| 236 |
<!-- Sub-tab bar (hidden until data loads) -->
|
| 237 |
<div id="sub-tab-bar">
|
| 238 |
<div class="sub-tab-bar-inner">
|
| 239 |
+
<button class="sub-tab-btn active" data-sub-tab="roulette">π² Roulette<span class="tab-badge" id="badge-roulette"></span></button>
|
|
|
|
| 240 |
<button class="sub-tab-btn" data-sub-tab="browse">π Browse<span class="tab-badge" id="badge-browse"></span></button>
|
| 241 |
<button class="sub-tab-btn sub-tab-btn-ghost" id="new-extract-btn">οΌ New</button>
|
| 242 |
</div>
|
|
|
|
| 247 |
<div id="tab-stat-strip"></div>
|
| 248 |
</div>
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
<!-- ββ Roulette sub-panel βββββββββββββββββββββββββββββββββββ -->
|
| 251 |
+
<div class="sub-panel active" id="panel-roulette">
|
| 252 |
<div class="card" id="roulette-section">
|
| 253 |
<p class="card-title">π² Place Roulette</p>
|
| 254 |
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:18px;">Can't decide where to go? Let the algorithm decide.</p>
|
|
|
|
| 283 |
<button class="roulette-btn" id="spin-btn">
|
| 284 |
<span class="dice">π²</span><span>Spin the wheel</span>
|
| 285 |
</button>
|
| 286 |
+
<div class="pick-card" id="pick-card" style="display:none;">
|
| 287 |
<div class="pick-name" id="pick-name"></div>
|
| 288 |
<div class="pick-loc" id="pick-loc"></div>
|
| 289 |
<div class="pick-meta" id="pick-meta"></div>
|
|
|
|
| 303 |
<button class="new-banner-dismiss" id="new-banner-dismiss" type="button" title="Dismiss">β</button>
|
| 304 |
</div>
|
| 305 |
</div>
|
| 306 |
+
<div id="download-bar" style="display:none; max-width:960px; margin:0 auto 12px; text-align:center;">
|
| 307 |
+
<button class="btn" id="download-btn">β¬ KML β Google My Maps</button>
|
| 308 |
+
<div class="download-row" style="margin-top:8px;">
|
| 309 |
+
<button class="btn btn-outline" id="download-geojson-btn">β¬ GeoJSON</button>
|
| 310 |
+
<button class="btn btn-outline" id="download-csv-btn">β¬ CSV</button>
|
| 311 |
+
</div>
|
| 312 |
+
<details style="max-width:520px;margin:10px auto 0;text-align:left;font-size:0.82rem;color:var(--muted);">
|
| 313 |
+
<summary style="cursor:pointer;font-weight:600;">How to import into Google My Maps</summary>
|
| 314 |
+
<ol style="line-height:1.8;padding-left:18px;margin-top:8px;">
|
| 315 |
+
<li>Go to <a href="https://mymaps.google.com" target="_blank" rel="noopener">mymaps.google.com</a> β <strong>Create a new map</strong></li>
|
| 316 |
+
<li>Click <strong>Import</strong> on the base layer and upload the KML you just downloaded</li>
|
| 317 |
+
<li>Pins appear organised by country β city, each linking back to the original reel</li>
|
| 318 |
+
<li><strong>On your phone:</strong> open the Google Maps app β <em>Saved β Maps</em> to find your custom map</li>
|
| 319 |
+
</ol>
|
| 320 |
+
<p style="margin-top:6px;">Prefer no Google account? The KML also imports into <a href="https://organicmaps.app" target="_blank" rel="noopener">Organic Maps</a> or OsmAnd β fully offline.</p>
|
| 321 |
+
</details>
|
| 322 |
+
<p class="download-note">GeoJSON includes geocoded pins only Β· CSV includes all rows</p>
|
| 323 |
+
</div>
|
| 324 |
<div class="card" id="stats-bar" style="display:none; max-width:960px; margin:0 auto 16px;">
|
| 325 |
<div class="stats-row">
|
| 326 |
<div class="stat-chip"><span class="stat-icon">π</span><div><div class="stat-val" id="stat-total">β</div><div class="stat-label">places</div></div></div>
|