From 6a8598ef302dc5126b6828211acba1f77523cdb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Z=C3=B6ller?= Date: Fri, 9 Jan 2026 09:05:51 +0100 Subject: [PATCH] UI fixes & updates --- .../templates/activities/detail.html | 90 +++++++++++++------ src/main/resources/templates/layout.html | 3 + .../templates/profile/followers.html | 11 ++- .../templates/profile/following.html | 11 ++- .../resources/templates/profile/public.html | 11 ++- .../resources/templates/profile/view.html | 11 ++- .../resources/templates/users/discover.html | 13 ++- 7 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/main/resources/templates/activities/detail.html b/src/main/resources/templates/activities/detail.html index 3aebc52..816e03a 100644 --- a/src/main/resources/templates/activities/detail.html +++ b/src/main/resources/templates/activities/detail.html @@ -444,6 +444,23 @@ let hoverMarker = null; let currentTrackPoints = null; + /** + * Throttle function to limit how often a function can be called + * @param {Function} func - Function to throttle + * @param {number} limit - Minimum time between calls in milliseconds + * @returns {Function} Throttled function + */ + function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + // Load activity details loadActivity(); @@ -798,6 +815,18 @@ // Smooth elevation data to remove zero/invalid values const smoothedData = smoothElevationData(elevationData); + // Create throttled hover handler for elevation chart + const elevationHoverHandler = throttle((event, activeElements) => { + if (activeElements && activeElements.length > 0) { + const dataIndex = activeElements[0].index; + if (smoothedData[dataIndex]) { + updateMapMarker(smoothedData[dataIndex].trackPointIndex); + } + } else { + hideMapMarker(); + } + }, 50); // 50ms throttle + // Create elevation chart with hover interaction const ctx = document.getElementById('elevationChart').getContext('2d'); new Chart(ctx, { @@ -813,22 +842,13 @@ fill: true, tension: 0.3, pointRadius: 0, - pointHoverRadius: 5 + pointHoverRadius: 0 // Disable hover radius for better performance }] }, options: { responsive: true, maintainAspectRatio: true, - onHover: (event, activeElements) => { - if (activeElements && activeElements.length > 0) { - const dataIndex = activeElements[0].index; - if (smoothedData[dataIndex]) { - updateMapMarker(smoothedData[dataIndex].trackPointIndex); - } - } else { - hideMapMarker(); - } - }, + onHover: elevationHoverHandler, plugins: { legend: { display: false @@ -1010,6 +1030,18 @@ // Calculate total duration to determine time format const totalMinutes = heartRateData[heartRateData.length - 1].time; + // Create throttled hover handler for heart rate chart + const heartRateHoverHandler = throttle((event, activeElements) => { + if (activeElements && activeElements.length > 0) { + const dataIndex = activeElements[0].index; + if (heartRateData[dataIndex]) { + updateMapMarker(heartRateData[dataIndex].trackPointIndex); + } + } else { + hideMapMarker(); + } + }, 50); // 50ms throttle + // Create heart rate chart using Chart.js const ctx = document.getElementById('heartRateChart').getContext('2d'); new Chart(ctx, { @@ -1025,22 +1057,13 @@ fill: true, tension: 0.3, pointRadius: 0, - pointHoverRadius: 5 + pointHoverRadius: 0 // Disable hover radius for better performance }] }, options: { responsive: true, maintainAspectRatio: true, - onHover: (event, activeElements) => { - if (activeElements && activeElements.length > 0) { - const dataIndex = activeElements[0].index; - if (heartRateData[dataIndex]) { - updateMapMarker(heartRateData[dataIndex].trackPointIndex); - } - } else { - hideMapMarker(); - } - }, + onHover: heartRateHoverHandler, plugins: { legend: { display: false @@ -1114,7 +1137,8 @@ speedData.push({ time: elapsedMinutes, - speed: speedKmh + speed: speedKmh, + trackPointIndex: i // Store the original track point index }); } } @@ -1126,6 +1150,18 @@ // Calculate total duration to determine time format const totalMinutes = smoothedSpeedData[smoothedSpeedData.length - 1].time; + // Create throttled hover handler for speed chart + const speedHoverHandler = throttle((event, activeElements) => { + if (activeElements && activeElements.length > 0) { + const dataIndex = activeElements[0].index; + if (smoothedSpeedData[dataIndex]) { + updateMapMarker(smoothedSpeedData[dataIndex].trackPointIndex); + } + } else { + hideMapMarker(); + } + }, 50); // 50ms throttle + // Create speed chart using Chart.js const ctx = document.getElementById('speedChart').getContext('2d'); new Chart(ctx, { @@ -1141,12 +1177,13 @@ fill: true, tension: 0.3, pointRadius: 0, - pointHoverRadius: 5 + pointHoverRadius: 0 // Disable hover radius for better performance }] }, options: { responsive: true, maintainAspectRatio: true, + onHover: speedHoverHandler, plugins: { legend: { display: false @@ -1199,7 +1236,7 @@ /** * Smooth speed data by applying moving average - * @param {Array} data - Array of {time, speed} objects + * @param {Array} data - Array of {time, speed, trackPointIndex} objects * @returns {Array} Smoothed speed data */ function smoothSpeedData(data) { @@ -1224,7 +1261,8 @@ smoothed.push({ time: data[i].time, - speed: count > 0 ? sum / count : data[i].speed + speed: count > 0 ? sum / count : data[i].speed, + trackPointIndex: data[i].trackPointIndex // Preserve track point index }); } diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 70774f2..ef25951 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -268,6 +268,9 @@ + + + diff --git a/src/main/resources/templates/profile/followers.html b/src/main/resources/templates/profile/followers.html index 4188cd0..4aad9f3 100644 --- a/src/main/resources/templates/profile/followers.html +++ b/src/main/resources/templates/profile/followers.html @@ -118,7 +118,7 @@

@${escapeHtml(follower.handle)}

- ${follower.bio ? `

${escapeHtml(follower.bio)}

` : ''} + ${follower.bio ? `

${sanitizeHtml(follower.bio)}

` : ''} @@ -132,6 +132,15 @@ div.textContent = text; return div.innerHTML; } + + function sanitizeHtml(html) { + if (!html) return ''; + // Use DOMPurify to sanitize HTML, allowing safe tags like p, br, a + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'span'], + ALLOWED_ATTR: ['href', 'class', 'rel', 'target'] + }); + } }); diff --git a/src/main/resources/templates/profile/following.html b/src/main/resources/templates/profile/following.html index 07be08d..96a4af1 100644 --- a/src/main/resources/templates/profile/following.html +++ b/src/main/resources/templates/profile/following.html @@ -118,7 +118,7 @@

@${escapeHtml(user.handle)}

- ${user.bio ? `

${escapeHtml(user.bio)}

` : ''} + ${user.bio ? `

${sanitizeHtml(user.bio)}

` : ''} @@ -132,6 +132,15 @@ div.textContent = text; return div.innerHTML; } + + function sanitizeHtml(html) { + if (!html) return ''; + // Use DOMPurify to sanitize HTML, allowing safe tags like p, br, a + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'span'], + ALLOWED_ATTR: ['href', 'class', 'rel', 'target'] + }); + } }); diff --git a/src/main/resources/templates/profile/public.html b/src/main/resources/templates/profile/public.html index b93aea9..8ccd834 100644 --- a/src/main/resources/templates/profile/public.html +++ b/src/main/resources/templates/profile/public.html @@ -172,7 +172,7 @@ // Bio const bioElement = document.getElementById('bio'); if (user.bio) { - bioElement.textContent = user.bio; + bioElement.innerHTML = sanitizeHtml(user.bio); } else { bioElement.innerHTML = 'No bio'; } @@ -429,6 +429,15 @@ div.textContent = text; return div.innerHTML; } + + function sanitizeHtml(html) { + if (!html) return ''; + // Use DOMPurify to sanitize HTML, allowing safe tags like p, br, a + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'span'], + ALLOWED_ATTR: ['href', 'class', 'rel', 'target'] + }); + } }); diff --git a/src/main/resources/templates/profile/view.html b/src/main/resources/templates/profile/view.html index 9d5e9fc..7a03c04 100644 --- a/src/main/resources/templates/profile/view.html +++ b/src/main/resources/templates/profile/view.html @@ -175,7 +175,7 @@ // Bio const bioElement = document.getElementById('bio'); if (user.bio) { - bioElement.textContent = user.bio; + bioElement.innerHTML = sanitizeHtml(user.bio); } else { bioElement.innerHTML = 'No bio yet. Add one?'; } @@ -290,6 +290,15 @@ div.textContent = text; return div.innerHTML; } + + function sanitizeHtml(html) { + if (!html) return ''; + // Use DOMPurify to sanitize HTML, allowing safe tags like p, br, a + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'span'], + ALLOWED_ATTR: ['href', 'class', 'rel', 'target'] + }); + } }); diff --git a/src/main/resources/templates/users/discover.html b/src/main/resources/templates/users/discover.html index 82430b9..36ec9a4 100644 --- a/src/main/resources/templates/users/discover.html +++ b/src/main/resources/templates/users/discover.html @@ -311,7 +311,7 @@ ${actor.bio - ? `

${escapeHtml(actor.bio)}

` + ? `

${sanitizeHtml(actor.bio)}

` : '

No bio

' } @@ -444,7 +444,7 @@ ${user.bio - ? `

${escapeHtml(user.bio)}

` + ? `

${sanitizeHtml(user.bio)}

` : '

No bio

' } @@ -540,6 +540,15 @@ div.textContent = text; return div.innerHTML; } + + function sanitizeHtml(html) { + if (!html) return ''; + // Use DOMPurify to sanitize HTML, allowing safe tags like p, br, a + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'span'], + ALLOWED_ATTR: ['href', 'class', 'rel', 'target'] + }); + }