Skip to content

Commit ea6ddd7

Browse files
ndbroadbentclaude
andcommitted
refactor: modularize map export into separate template files
- Split map-html.ts into map/ directory with separate concerns - Use plain app.js, styles.css, index.html files with Vite ?raw imports - Show map popups on hover instead of click - Keep popup open when mouse moves into it 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6db55e6 commit ea6ddd7

File tree

10 files changed

+786
-490
lines changed

10 files changed

+786
-490
lines changed

src/export/map-html.ts

Lines changed: 2 additions & 490 deletions
Large diffs are not rendered by default.

src/export/map/app.js

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/* global L, mapData */
2+
;(function () {
3+
'use strict'
4+
5+
if (typeof mapData === 'undefined') {
6+
console.error('mapData not found. Make sure data.js is loaded first.')
7+
return
8+
}
9+
10+
var floatingTooltip = document.getElementById('floatingTooltip')
11+
var escapeDiv = document.createElement('div')
12+
13+
function escapeHtml(str) {
14+
escapeDiv.textContent = str
15+
return escapeDiv.innerHTML
16+
}
17+
18+
function formatSenders(messages) {
19+
var uniqueSenders = []
20+
var seen = {}
21+
for (var i = 0; i < messages.length; i++) {
22+
var name = messages[i].sender.split(' ')[0]
23+
if (!seen[name]) {
24+
seen[name] = true
25+
uniqueSenders.push(name)
26+
}
27+
}
28+
var label = uniqueSenders.length === 1 ? 'Sender' : 'Senders'
29+
var display =
30+
uniqueSenders.length <= 2
31+
? uniqueSenders.join(', ')
32+
: uniqueSenders.slice(0, 2).join(', ') + ', and ' + (uniqueSenders.length - 2) + ' more'
33+
return { label: label, display: display }
34+
}
35+
36+
function buildTooltipHtml(messages) {
37+
return messages
38+
.map(function (m) {
39+
var senderName = m.sender.split(' ')[0]
40+
return (
41+
'<div class="msg-tooltip-item">' +
42+
'<div class="msg-tooltip-header">' +
43+
escapeHtml(senderName) +
44+
' · ' +
45+
m.date +
46+
'</div>' +
47+
'<div class="msg-tooltip-text">' +
48+
escapeHtml(m.message) +
49+
'</div></div>'
50+
)
51+
})
52+
.join('')
53+
}
54+
55+
function showTooltip(trigger, messages) {
56+
floatingTooltip.innerHTML = buildTooltipHtml(messages)
57+
var rect = trigger.getBoundingClientRect()
58+
var left = rect.left
59+
if (left + 300 > window.innerWidth) left = window.innerWidth - 310
60+
if (left < 10) left = 10
61+
floatingTooltip.style.left = left + 'px'
62+
floatingTooltip.style.bottom = window.innerHeight - rect.top + 10 + 'px'
63+
floatingTooltip.style.top = 'auto'
64+
floatingTooltip.style.display = 'block'
65+
}
66+
67+
function hideTooltip() {
68+
floatingTooltip.style.display = 'none'
69+
}
70+
71+
// Initialize map
72+
var map = L.map('map').setView([mapData.center.lat, mapData.center.lng], mapData.zoom)
73+
74+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
75+
attribution: '&copy; OpenStreetMap contributors'
76+
}).addTo(map)
77+
78+
var markersLayer = mapData.clusterMarkers
79+
? L.markerClusterGroup({
80+
maxClusterRadius: 50,
81+
spiderfyOnMaxZoom: true,
82+
showCoverageOnHover: false
83+
})
84+
: L.layerGroup()
85+
86+
// Add markers
87+
mapData.points.forEach(function (p) {
88+
var messagesEncoded = encodeURIComponent(JSON.stringify(p.messages)).replace(/'/g, '%27')
89+
var senderDisplay = formatSenders(p.messages)
90+
var mentionCount = p.messages.length
91+
var mentionText = mentionCount > 1 ? ' (' + mentionCount + ' mentions)' : ''
92+
93+
var imageHtml = p.imagePath
94+
? '<img src="' +
95+
escapeHtml(p.imagePath) +
96+
'" style="display:block;width:100%;max-width:200px;border-radius:4px;margin-bottom:8px;" />'
97+
: ''
98+
99+
var mapsUrl = p.placeId
100+
? 'https://www.google.com/maps/search/?api=1&query=' +
101+
encodeURIComponent(p.activity) +
102+
'&query_place_id=' +
103+
p.placeId
104+
: null
105+
106+
var popupContent =
107+
'<div style="max-width:240px;">' +
108+
imageHtml +
109+
'<strong>' +
110+
escapeHtml(p.activity) +
111+
'</strong><br>' +
112+
'<small>' +
113+
p.date +
114+
' · <span class="sender-trigger" data-messages="' +
115+
messagesEncoded +
116+
'">' +
117+
senderDisplay.label +
118+
': ' +
119+
senderDisplay.display +
120+
mentionText +
121+
'</span></small><br>' +
122+
(p.location ? '<em>' + escapeHtml(p.location) + '</em><br>' : '') +
123+
(mapsUrl ? '<a href="' + mapsUrl + '" target="_blank">View on Google Maps</a><br>' : '') +
124+
(p.url ? '<a href="' + escapeHtml(p.url) + '" target="_blank">Source Link</a>' : '') +
125+
'</div>'
126+
127+
var marker = L.marker([p.lat, p.lng], {
128+
icon: L.divIcon({
129+
className: 'custom-marker',
130+
html:
131+
'<div style="background-color:' +
132+
p.color +
133+
';width:12px;height:12px;border-radius:50%;border:2px solid white;"></div>',
134+
iconSize: [16, 16],
135+
iconAnchor: [8, 8]
136+
})
137+
})
138+
.addTo(markersLayer)
139+
.bindPopup(popupContent)
140+
141+
var closeTimeout = null
142+
marker.on('mouseover', function () {
143+
if (closeTimeout) {
144+
clearTimeout(closeTimeout)
145+
closeTimeout = null
146+
}
147+
this.openPopup()
148+
})
149+
marker.on('mouseout', function () {
150+
var self = this
151+
closeTimeout = setTimeout(function () {
152+
self.closePopup()
153+
}, 100)
154+
})
155+
marker.on('popupopen', function () {
156+
var popup = this.getPopup()
157+
var popupEl = popup.getElement()
158+
if (popupEl) {
159+
popupEl.addEventListener('mouseenter', function () {
160+
if (closeTimeout) {
161+
clearTimeout(closeTimeout)
162+
closeTimeout = null
163+
}
164+
})
165+
popupEl.addEventListener('mouseleave', function () {
166+
marker.closePopup()
167+
})
168+
}
169+
})
170+
})
171+
172+
map.addLayer(markersLayer)
173+
174+
if (markersLayer.getLayers().length > 0) {
175+
map.fitBounds(markersLayer.getBounds(), { padding: [50, 50] })
176+
}
177+
178+
// Popup tooltip handlers
179+
map.on('popupopen', function () {
180+
var triggers = document.querySelectorAll('.leaflet-popup .sender-trigger[data-messages]')
181+
triggers.forEach(function (trigger) {
182+
trigger.addEventListener('mouseenter', function () {
183+
var messages = JSON.parse(decodeURIComponent(trigger.getAttribute('data-messages') || ''))
184+
if (!messages.length) return
185+
showTooltip(trigger, messages)
186+
})
187+
trigger.addEventListener('mouseleave', hideTooltip)
188+
})
189+
})
190+
191+
// Render info box
192+
document.getElementById('infoTitle').textContent = mapData.title
193+
document.getElementById('infoCount').textContent = mapData.points.length
194+
195+
// Render legend
196+
var legendHtml = Object.entries(mapData.senderColors)
197+
.sort(function (a, b) {
198+
return a[0].localeCompare(b[0])
199+
})
200+
.map(function (entry) {
201+
var sender = entry[0]
202+
var color = entry[1]
203+
var firstName = sender.split(' ')[0]
204+
return (
205+
'<div class="legend-item"><div class="legend-dot" style="background-color:' +
206+
color +
207+
';"></div>' +
208+
'<span>' +
209+
escapeHtml(firstName) +
210+
"'s suggestions</span></div>"
211+
)
212+
})
213+
.join('')
214+
document.getElementById('legend').innerHTML = legendHtml
215+
216+
// Activity list
217+
var activities = mapData.points.map(function (p) {
218+
return {
219+
id: p.activityId,
220+
activity: p.activity,
221+
sender: p.sender.split(' ')[0],
222+
location: p.location,
223+
date: p.date,
224+
score: p.score,
225+
imagePath: p.imagePath,
226+
placeId: p.placeId,
227+
url: p.url,
228+
messages: p.messages
229+
}
230+
})
231+
232+
function renderActivityList(sorted) {
233+
var html = sorted
234+
.map(function (a, idx) {
235+
var mapsUrl = a.placeId
236+
? 'https://www.google.com/maps/search/?api=1&query=' +
237+
encodeURIComponent(a.activity) +
238+
'&query_place_id=' +
239+
a.placeId
240+
: null
241+
var thumb = a.imagePath
242+
? '<img src="' + a.imagePath + '" class="activity-thumb" alt="" />'
243+
: '<div class="activity-thumb-placeholder"></div>'
244+
var links =
245+
(mapsUrl ? '<a href="' + mapsUrl + '" target="_blank">Google Maps</a>' : '') +
246+
(a.url ? '<a href="' + a.url + '" target="_blank">Source</a>' : '')
247+
var senderInfo = formatSenders(a.messages)
248+
var senderHtml =
249+
'<span class="sender-trigger" data-idx="' +
250+
idx +
251+
'">' +
252+
senderInfo.label +
253+
': ' +
254+
senderInfo.display +
255+
'</span>'
256+
return (
257+
'<div class="activity-row">' +
258+
thumb +
259+
'<div class="activity-content">' +
260+
'<div class="activity-title">' +
261+
escapeHtml(a.activity) +
262+
'</div>' +
263+
'<div class="activity-meta">' +
264+
(a.location ? '<span class="activity-location">' + escapeHtml(a.location) + '</span> · ' : '') +
265+
senderHtml +
266+
' · ' +
267+
a.date +
268+
'</div>' +
269+
'<div class="activity-links">' +
270+
links +
271+
'</div>' +
272+
'</div></div>'
273+
)
274+
})
275+
.join('')
276+
document.getElementById('activityListBody').innerHTML = html
277+
attachTooltipHandlers(sorted)
278+
}
279+
280+
function attachTooltipHandlers(sorted) {
281+
var triggers = document.querySelectorAll('.sender-trigger[data-idx]')
282+
triggers.forEach(function (trigger) {
283+
trigger.addEventListener('mouseenter', function () {
284+
var idx = parseInt(trigger.getAttribute('data-idx'), 10)
285+
var a = sorted[idx]
286+
if (!a) return
287+
showTooltip(trigger, a.messages)
288+
})
289+
trigger.addEventListener('mouseleave', hideTooltip)
290+
})
291+
}
292+
293+
window.sortActivities = function (sortBy) {
294+
var sorted = activities.slice()
295+
if (sortBy === 'score')
296+
sorted.sort(function (a, b) {
297+
return b.score - a.score
298+
})
299+
else if (sortBy === 'oldest')
300+
sorted.sort(function (a, b) {
301+
return a.date.localeCompare(b.date)
302+
})
303+
else if (sortBy === 'newest')
304+
sorted.sort(function (a, b) {
305+
return b.date.localeCompare(a.date)
306+
})
307+
renderActivityList(sorted)
308+
}
309+
310+
window.openModal = function () {
311+
document.getElementById('activityModal').classList.add('open')
312+
document.body.style.overflow = 'hidden'
313+
window.sortActivities(document.getElementById('sortSelect').value)
314+
}
315+
316+
window.closeModal = function (e) {
317+
if (!e || e.target === e.currentTarget) {
318+
document.getElementById('activityModal').classList.remove('open')
319+
document.body.style.overflow = ''
320+
}
321+
}
322+
323+
document.addEventListener('keydown', function (e) {
324+
if (e.key === 'Escape') window.closeModal()
325+
})
326+
})()

0 commit comments

Comments
 (0)