Skip to content

Commit 24b5d46

Browse files
SableRafclaude
andcommitted
Simplify and optimize MapView, NodePanel, and popup
Code reuse: - Export getOsmUrl from popup.ts and import it in NodePanel.vue, removing the identical duplicate that existed in both files i18n: - Fix hardcoded English "by" in popup.ts organizingEntityHtml — now uses t('panel.by'), which is already translated in all 9 locales Code quality: - Extract getOrganizerNames() in NodePanel.vue so formatOrganizers() and hasMoreHosts() share the name-extraction logic instead of duplicating it - Hoist isTextInput guard in MapView handleKeydown to a single early-return, removing the repeated !isTextInput && check from every key branch Performance: - Build nodeMap (id → Node) alongside markerMap in MapView; applyMarkerLabels now does O(1) map lookups instead of O(n) Array.find per marker - Replace non-terminating forEach in slidingWindowHandler with a for…of loop that breaks on first match, and use nodeMap for O(1) node lookup - Add descPreview and calLinks computed properties in NodePanel so getDescPreview() and calendarLinks() are not called twice per render - Remove unnecessary document.body.appendChild/removeChild in downloadIcs — the anchor element does not need to be in the DOM to trigger a download Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f2851d7 commit 24b5d46

3 files changed

Lines changed: 34 additions & 26 deletions

File tree

pcd-website/src/components/MapView.vue

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const listOpen = ref(false);
2222
let mapInstance: import('leaflet').Map | null = null;
2323
let leafletRef: typeof import('leaflet') | null = null;
2424
const markerMap = new Map<string, import('leaflet').Marker>();
25+
const nodeMap = new Map<string, Node>(); // id → Node, for O(1) lookups
2526
let openPopupNodeId: string | null = null;
2627
let slidingWindowHandler: ((e: FocusEvent) => void) | null = null;
2728
@@ -205,27 +206,32 @@ function handleKeydown(e: KeyboardEvent) {
205206
document.getElementById('burger-btn')?.focus();
206207
}
207208
}
208-
} else if (!isTextInput && /^[0-9]$/.test(e.key)) {
209+
return;
210+
}
211+
212+
if (isTextInput) return;
213+
214+
if (/^[0-9]$/.test(e.key)) {
209215
e.preventDefault();
210216
e.stopPropagation();
211217
if (e.key === '0') {
212218
mapInstance?.setView([20, 10], 3);
213219
} else {
214220
mapInstance?.setZoom(parseInt(e.key));
215221
}
216-
} else if (!isTextInput && (e.key === '+' || e.key === '=' || e.key === 'Add')) {
222+
} else if (e.key === '+' || e.key === '=' || e.key === 'Add') {
217223
e.preventDefault();
218224
e.stopPropagation();
219225
mapInstance?.zoomIn();
220-
} else if (!isTextInput && (e.key === '-' || e.key === 'Subtract')) {
226+
} else if (e.key === '-' || e.key === 'Subtract') {
221227
e.preventDefault();
222228
e.stopPropagation();
223229
mapInstance?.zoomOut();
224-
} else if (!isTextInput && (e.key === 'M' || e.key === 'm')) {
230+
} else if (e.key === 'M' || e.key === 'm') {
225231
e.preventDefault();
226232
listOpen.value = !listOpen.value;
227233
if (listOpen.value) selectedNode.value = null;
228-
} else if (!isTextInput && (e.key === 'L' || e.key === 'l')) {
234+
} else if (e.key === 'L' || e.key === 'l') {
229235
e.preventDefault();
230236
document.getElementById('map')?.focus();
231237
}
@@ -354,6 +360,7 @@ onMounted(async () => {
354360
355361
// Add markers
356362
props.nodes.forEach((node) => {
363+
nodeMap.set(node.id, node);
357364
const icon = node.online_event ? onlineMarkerIcon : markerIcon;
358365
const marker = L.marker([node.lat, node.lng], { icon });
359366
marker.bindPopup(() => makePopupContent(node), { maxWidth: 340 });
@@ -368,7 +375,7 @@ onMounted(async () => {
368375
// animates (which shows/hides individual markers as clusters form or dissolve).
369376
function applyMarkerLabels() {
370377
markerMap.forEach((marker, id) => {
371-
const node = props.nodes.find(n => n.id === id);
378+
const node = nodeMap.get(id);
372379
if (node) {
373380
marker.getElement()?.setAttribute('aria-label', node.event_name);
374381
}
@@ -430,11 +437,12 @@ onMounted(async () => {
430437
if (!target.classList.contains('marker-node')) return;
431438
432439
let foundNode: Node | undefined;
433-
markerMap.forEach((marker, id) => {
440+
for (const [id, marker] of markerMap) {
434441
if (marker.getElement() === target) {
435-
foundNode = props.nodes.find(n => n.id === id);
442+
foundNode = nodeMap.get(id);
443+
break;
436444
}
437-
});
445+
}
438446
439447
if (foundNode) {
440448
const lat = foundNode.lat;

pcd-website/src/components/NodePanel.vue

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script setup lang="ts">
2-
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
2+
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
33
import { useI18n } from 'vue-i18n';
44
import { createFocusTrap, type FocusTrap } from 'focus-trap';
55
import { Icon } from '@iconify/vue';
66
import type { Node } from '../lib/nodes';
77
import { formatDateRange, formatTimeRange, calendarLinks, onlinePlatformName } from '../lib/format';
8+
import { getOsmUrl } from '../lib/popup';
89
import { PCD_EMAIL } from '../config';
910
const props = defineProps<{
1011
node: Node | null;
@@ -135,31 +136,27 @@ function downloadIcs(node: Node) {
135136
const a = document.createElement('a');
136137
a.href = url;
137138
a.download = `${node.id}.ics`;
138-
document.body.appendChild(a);
139139
a.click();
140-
document.body.removeChild(a);
141140
setTimeout(() => URL.revokeObjectURL(url), 1000);
142141
}
143142
144-
function getOsmUrl(node: Node): string {
145-
const query = node.location_name
146-
? [node.location_name, node.address].filter(Boolean).join(', ')
147-
: node.address ?? '';
148-
return `https://www.openstreetmap.org/search?query=${encodeURIComponent(query)}`;
149-
}
150143
151144
function getParagraphs(text: string): string[] {
152145
return text.split(/\n\n+/).filter(Boolean);
153146
}
154147
148+
function getOrganizerNames(organizers: { name: string }[]): string[] {
149+
return organizers.map(o => o.name).filter(Boolean);
150+
}
151+
155152
function formatOrganizers(organizers: { name: string }[], expanded = false): string {
156-
const names = organizers.map(o => o.name).filter(Boolean);
153+
const names = getOrganizerNames(organizers);
157154
if (expanded || names.length <= HOSTS_VISIBLE) return names.join(', ');
158155
return names.slice(0, HOSTS_VISIBLE).join(', ');
159156
}
160157
161158
function hasMoreHosts(organizers: { name: string }[]): boolean {
162-
return organizers.map(o => o.name).filter(Boolean).length > HOSTS_VISIBLE;
159+
return getOrganizerNames(organizers).length > HOSTS_VISIBLE;
163160
}
164161
165162
function getDescPreview(node: Node): { text: string; hasMore: boolean } {
@@ -189,6 +186,9 @@ function getReportIssueHref(node: Node): string {
189186
const body = t('panel.email_body', { name: node.event_name, link: getShareUrl(node) });
190187
return `mailto:${PCD_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
191188
}
189+
190+
const descPreview = computed(() => props.node ? getDescPreview(props.node) : null);
191+
const calLinks = computed(() => props.node && !props.node.date_tbd ? calendarLinks(props.node) : null);
192192
</script>
193193

194194
<template>
@@ -365,15 +365,15 @@ function getReportIssueHref(node: Node): string {
365365
</button>
366366
<div v-show="calDropdownOpen" class="quick-action-menu" role="menu">
367367
<a
368-
:href="calendarLinks(node).googleCalUrl"
368+
:href="calLinks!.googleCalUrl"
369369
target="_blank"
370370
rel="noopener noreferrer"
371371
role="menuitem"
372372
:aria-label="t('panel.google_calendar_new_tab')"
373373
@click="calDropdownOpen = false"
374374
>{{ t('panel.google_calendar') }}</a>
375375
<a
376-
:href="calendarLinks(node).outlookCalUrl"
376+
:href="calLinks!.outlookCalUrl"
377377
target="_blank"
378378
rel="noopener noreferrer"
379379
role="menuitem"
@@ -417,10 +417,10 @@ function getReportIssueHref(node: Node): string {
417417
>{{ para }}</p>
418418
</template>
419419
<template v-else>
420-
<p>{{ getDescPreview(node).text }}</p>
420+
<p>{{ descPreview!.text }}</p>
421421
</template>
422422
<button
423-
v-if="getDescPreview(node).hasMore"
423+
v-if="descPreview!.hasMore"
424424
class="panel-read-more"
425425
:aria-expanded="descExpanded"
426426
@click="descExpanded = !descExpanded"

pcd-website/src/lib/popup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function t(key: string, params?: Record<string, string>): string {
1818

1919
const POPUP_PREVIEW_LENGTH = 120;
2020

21-
function getOsmUrl(node: Node): string {
21+
export function getOsmUrl(node: Node): string {
2222
const query = node.location_name
2323
? [node.location_name, node.address].filter(Boolean).join(', ')
2424
: node.address ?? '';
@@ -43,7 +43,7 @@ export function makePopupContent(node: Node): string {
4343
: '';
4444

4545
const organizingEntityHtml = node.organization_name
46-
? `<p class="popup-organizing-entity">by ${escapeHtml(node.organization_name)}</p>`
46+
? `<p class="popup-organizing-entity">${t('panel.by')} ${escapeHtml(node.organization_name)}</p>`
4747
: '';
4848

4949
const onlineBadgeHtml = node.online_event ? `<span class="popup-online-badge">${t('popup.online_event')}</span>` : '';

0 commit comments

Comments
 (0)