harshith99 Claude Sonnet 4.6 commited on
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 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
- └── [⬇ Exports] [🎲 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 three sub-panels.
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 (exports / roulette / browse)
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, three sub-tabs appear:
308
- - **⬇ Exports** β€” download KML / GeoJSON / CSV + step-by-step Google My Maps import guide
309
- - **🎲 Roulette** β€” random place picker, filtered by category, city, or distance from your location. Default after extraction
310
- - **πŸ“‹ Browse** β€” stats bar, interactive Leaflet map, full-text search, filters, and card grid. Default after import
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' && !_geolocationRequested) _requestGeolocation();
 
 
 
 
 
 
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="exports">⬇ Exports</button>
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>