UI fixes & updates
This commit is contained in:
parent
22f7f7c271
commit
6a8598ef30
7 changed files with 118 additions and 32 deletions
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -268,6 +268,9 @@
|
|||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- DOMPurify for HTML sanitization -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script th:src="@{/js/auth.js}"></script>
|
||||
<script th:src="@{/js/fitpub.js}"></script>
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
<p class="text-muted small mb-0">
|
||||
@${escapeHtml(follower.handle)}
|
||||
</p>
|
||||
${follower.bio ? `<p class="small mt-1 mb-0 text-muted">${escapeHtml(follower.bio)}</p>` : ''}
|
||||
${follower.bio ? `<p class="small mt-1 mb-0 text-muted">${sanitizeHtml(follower.bio)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
<p class="text-muted small mb-0">
|
||||
@${escapeHtml(user.handle)}
|
||||
</p>
|
||||
${user.bio ? `<p class="small mt-1 mb-0 text-muted">${escapeHtml(user.bio)}</p>` : ''}
|
||||
${user.bio ? `<p class="small mt-1 mb-0 text-muted">${sanitizeHtml(user.bio)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
|
|
|||
|
|
@ -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 = '<span class="text-muted">No bio</span>';
|
||||
}
|
||||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
|
|
|||
|
|
@ -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 = '<span class="text-muted">No bio yet. <a href="/profile/edit">Add one?</a></span>';
|
||||
}
|
||||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@
|
|||
</div>
|
||||
|
||||
${actor.bio
|
||||
? `<p class="card-text small mb-3">${escapeHtml(actor.bio)}</p>`
|
||||
? `<p class="card-text small mb-3">${sanitizeHtml(actor.bio)}</p>`
|
||||
: '<p class="card-text small text-muted mb-3 fst-italic">No bio</p>'
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +444,7 @@
|
|||
|
||||
<!-- Bio -->
|
||||
${user.bio
|
||||
? `<p class="card-text small text-muted mb-3 bio-preview">${escapeHtml(user.bio)}</p>`
|
||||
? `<p class="card-text small text-muted mb-3 bio-preview">${sanitizeHtml(user.bio)}</p>`
|
||||
: '<p class="card-text small text-muted mb-3 fst-italic">No bio</p>'
|
||||
}
|
||||
|
||||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue