PageSourceSearch

https://www.vicbug.app/v2/io/route-bookmarks.js?v=284

js vicbug.app collected 2026-09-24 13:55:34 UTC 9,013 bytes, 291 lines download raw bytes

1/**
2 * Route Bookmarks — lightweight bookmark references to library routes.
3 *
4 * Stores minimal data in users/{uid}/bookmarks/{routeId}.
5 * Renders bookmarks inside the "Bookmarked Routes" folder within "My Routes".
6 */
7
8import { store } from '../core/state.js?v=284';
9import { eventBus } from '../core/event-bus.js?v=284';
10import { config } from '../core/config.js?v=284';
11import { escapeHtml } from '../core/utils.js?v=284';
12import { enterViewMode } from './route-view.js?v=284';
13
14function getDb() {
15  try { return firebase.firestore(); } catch (e) { return null; }
16}
17
18let bookmarkedIds = new Set();
19
20export function initBookmarks() {
21  // Load bookmarks when user signs in
22  eventBus.on('auth:changed', ({ user }) => {
23    if (user) {
24      loadBookmarks();
25    } else {
26      bookmarkedIds.clear();
27      renderBookmarkList([]);
28      const folder = document.getElementById('my-bookmarked-folder');
29      if (folder) folder.style.display = 'none';
30    }
31  });
32}
33
34/**
35 * Toggle bookmark for a route. Returns true if bookmarked, false if removed.
36 */
37export async function toggleBookmark(routeId, routeData) {
38  const user = store.get('currentUser');
39  if (!user) {
40    eventBus.emit('auth:loginRequired');
41    return null;
42  }
43
44  const db = getDb();
45  if (!db) return null;
46
47  const ref = db.collection('users').doc(user.uid)
48    .collection('bookmarks').doc(routeId);
49
50  if (bookmarkedIds.has(routeId)) {
51    // Remove bookmark
52    await ref.delete();
53    bookmarkedIds.delete(routeId);
54    await loadBookmarks();
55    return false;
56  } else {
57    // Add bookmark
58    let distKm = routeData?.distanceKm || routeData?.distance || 0;
59    if (distKm > 5000) distKm = distKm / 1000; // likely stored in meters
60    const elevUp = routeData?.elevation?.up || 0;
61
62    // Generate preview from the currently-displayed route coordinates
63    const lastRoute = store.get('_lastRouteData');
64    const previewUrl = lastRoute?.coordinates
65      ? getStaticMapUrl(lastRoute.coordinates)
66      : null;
67
68    const bookmarkData = {
69      routeId,
70      name: routeData?.name || 'Untitled Route',
71      distance: Math.round(distKm * 10) / 10,
72      elevation: Math.round(elevUp),
73      bookmarkedAt: firebase.firestore.FieldValue.serverTimestamp(),
74    };
75    if (previewUrl) bookmarkData.previewUrl = previewUrl;
76
77    await ref.set(bookmarkData);
78    bookmarkedIds.add(routeId);
79    await loadBookmarks();
80    return true;
81  }
82}
83
84/**
85 * Check if a route is currently bookmarked.
86 */
87export function isBookmarked(routeId) {
88  return bookmarkedIds.has(routeId);
89}
90
91/**
92 * Load all bookmarks from Firestore and render the list.
93 */
94export async function loadBookmarks() {
95  const user = store.get('currentUser');
96  if (!user) return;
97
98  const db = getDb();
99  if (!db) return;
100
101  const listEl = document.getElementById('bookmarked-routes-list');
102  const emptyEl = document.getElementById('bookmarked-routes-empty');
103  const loadingEl = document.getElementById('bookmarked-routes-loading');
104  const folder = document.getElementById('my-bookmarked-folder');
105
106  if (loadingEl) loadingEl.classList.remove('hidden');
107  if (emptyEl) emptyEl.classList.add('hidden');
108  if (listEl) listEl.innerHTML = '';
109
110  try {
111    const snapshot = await db.collection('users').doc(user.uid)
112      .collection('bookmarks')
113      .orderBy('bookmarkedAt', 'desc')
114      .limit(50)
115      .get();
116
117    if (loadingEl) loadingEl.classList.add('hidden');
118
119    bookmarkedIds.clear();
120    const bookmarks = [];
121    snapshot.forEach(doc => {
122      const data = doc.data();
123      bookmarkedIds.add(doc.id);
124      bookmarks.push({ id: doc.id, ...data });
125    });
126
127    renderBookmarkList(bookmarks);
128
129    // Show/hide folder based on whether there are bookmarks
130    if (folder) {
131      folder.style.display = bookmarks.length > 0 ? '' : 'none';
132    }
133  } catch (err) {
134    console.error('[Bookmarks] Load failed:', err);
135    if (loadingEl) loadingEl.classList.add('hidden');
136    if (emptyEl) emptyEl.classList.remove('hidden');
137  }
138}
139
140function renderBookmarkList(bookmarks) {
141  const listEl = document.getElementById('bookmarked-routes-list');
142  const emptyEl = document.getElementById('bookmarked-routes-empty');
143  if (!listEl) return;
144
145  if (bookmarks.length === 0) {
146    listEl.innerHTML = '';
147    if (emptyEl) emptyEl.classList.remove('hidden');
148    return;
149  }
150
151  if (emptyEl) emptyEl.classList.add('hidden');
152  listEl.innerHTML = '';
153
154  for (const bm of bookmarks) {
155    const div = document.createElement('div');
156    div.className = 'saved-route-item';
157
158    const distStr = bm.distance ? `${bm.distance} km` : '';
159    const elevStr = bm.elevation ? `↑${bm.elevation}m` : '';
160    let dateStr = '';
161    if (bm.bookmarkedAt) {
162      const date = bm.bookmarkedAt.toDate ? bm.bookmarkedAt.toDate() : new Date(bm.bookmarkedAt);
163      const day = date.getDate();
164      const suffix = [, 'st', 'nd', 'rd'][day % 10 > 3 ? 0 : (day % 100 - day % 10 !== 10) * (day % 10)] || 'th';
165      const month = date.toLocaleDateString('en-AU', { month: 'short' });
166      const year = date.getFullYear();
167      dateStr = `${day}${suffix} ${month}, ${year}`;
168    }
169
170    div.innerHTML = `
171      <div class="saved-route-row">
172        <div class="saved-route-thumb"${bm.previewUrl ? '' : ' style="display:none"'}>
173          ${bm.previewUrl
174            ? `<img src="${escapeHtml(bm.previewUrl)}" alt="" loading="lazy" onerror="this.parentElement.style.display='none'">`
175            : ''}
176        </div>
177        <div class="saved-route-info">
178          <div class="saved-route-top">
179            <div class="saved-route-name">${escapeHtml(bm.name || 'Untitled Route')}</div>
180            <button class="saved-route-delete" title="Remove bookmark">
181              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
182                <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
183              </svg>
184            </button>
185          </div>
186          <div class="saved-route-meta">
187            ${distStr ? `<span>${distStr}</span>` : ''}
188            ${elevStr ? `<span>${elevStr}</span>` : ''}
189          </div>
190          <div class="saved-route-meta">
191            ${dateStr ? `<span>${dateStr}</span>` : ''}
192          </div>
193        </div>
194      </div>
195    `;
196
197    // Click to open route in view mode
198    div.addEventListener('click', (e) => {
199      if (e.target.closest('.saved-route-delete')) return;
200      enterViewMode(bm.routeId || bm.id);
201    });
202
203    // Remove bookmark
204    const deleteBtn = div.querySelector('.saved-route-delete');
205    if (deleteBtn) {
206      deleteBtn.addEventListener('click', async (e) => {
207        e.stopPropagation();
208        await removeBookmark(bm.id);
209      });
210    }
211
212    listEl.appendChild(div);
213  }
214}
215
216async function removeBookmark(routeId) {
217  const user = store.get('currentUser');
218  if (!user) return;
219
220  const db = getDb();
221  if (!db) return;
222
223  try {
224    await db.collection('users').doc(user.uid)
225      .collection('bookmarks').doc(routeId).delete();
226    bookmarkedIds.delete(routeId);
227    await loadBookmarks();
228
229    // If we're currently viewing this route, update the bookmark button
230    if (store.get('viewMode') && store.get('viewRouteId') === routeId) {
231      eventBus.emit('bookmark:changed', { routeId, bookmarked: false });
232    }
233  } catch (err) {
234    console.error('[Bookmarks] Remove failed:', err);
235  }
236}
237
238
239// ─── Static map preview ───
240
241function getStaticMapUrl(coordinates) {
242  if (!coordinates || coordinates.length < 2 || !config.mapboxToken) return null;
243
244  const base = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/`;
245  const suffix = `/auto/150x150@2x?access_token=${config.mapboxToken}&padding=20,20,20,20`;
246
247  for (const pts of [500, 250]) {
248    const sampled = sampleCoords(coordinates, pts);
249    const enc = encodeURIComponent(encodePolyline(sampled));
250    if (!enc) continue;
251    const url = `${base}path-7+60a5fa-0.3(${enc}),path-3+3b82f6-0.9(${enc})${suffix}`;
252    if (url.length <= 8000) return url;
253  }
254  return null;
255}
256
257function sampleCoords(coords, maxPoints) {
258  if (coords.length <= maxPoints) return coords;
259  const step = (coords.length - 1) / (maxPoints - 1);
260  const result = [];
261  for (let i = 0; i < maxPoints - 1; i++) {
262    result.push(coords[Math.round(i * step)]);
263  }
264  result.push(coords[coords.length - 1]);
265  return result;
266}
267
268function encodePolyline(coords) {
269  let encoded = '';
270  let prevLat = 0, prevLng = 0;
271  for (const c of coords) {
272    const lat = Math.round((c[1] || c.lat || 0) * 1e5);
273    const lng = Math.round((c[0] || c.lng || 0) * 1e5);
274    encoded += encodeSignedValue(lat - prevLat);
275    encoded += encodeSignedValue(lng - prevLng);
276    prevLat = lat;
277    prevLng = lng;
278  }
279  return encoded;
280}
281
282function encodeSignedValue(val) {
283  let v = val < 0 ? ~(val << 1) : val << 1;
284  let encoded = '';
285  while (v >= 0x20) {
286    encoded += String.fromCharCode((0x20 | (v & 0x1f)) + 63);
287    v >>= 5;
288  }
289  encoded += String.fromCharCode(v + 63);
290  return encoded;
291}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.