Mark indoor activities to exclude them from the heatmap

This commit is contained in:
Tim Zöller 2026-01-11 11:56:48 +01:00
parent 851ba87ef2
commit 22c4ca0964
34 changed files with 1626 additions and 58 deletions

View file

@ -117,6 +117,31 @@
#zipFileInput {
display: none;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.status-SUCCESS {
background: rgba(57, 255, 20, 0.25);
color: #39ff14;
border: 1px solid #39ff14;
}
.status-FAILED {
background: rgba(220, 53, 69, 0.25);
color: #ff6b7a;
border: 1px solid #dc3545;
}
.status-PENDING, .status-PROCESSING {
background: rgba(255, 193, 7, 0.25);
color: #ffc107;
border: 1px solid #ffc107;
}
.job-card:hover {
box-shadow: 0 2px 8px rgba(255, 20, 147, 0.3);
}
}
</style>
</head>
<body>

View file

@ -34,6 +34,9 @@
<h2 id="activityTitle">Activity Title</h2>
<p class="text-muted mb-2">
<span id="activityType" class="activity-type-badge"></span>
<span id="indoorBadge" class="badge bg-warning text-dark ms-2" style="display: none;">
<i class="bi bi-house-door"></i> Indoor
</span>
<span class="ms-2">
<i class="bi bi-calendar"></i>
<span id="activityDate"></span>
@ -507,6 +510,17 @@
document.querySelector('#visibilityBadge i').className = `bi bi-${visIcon}`;
document.getElementById('visibilityBadge').className = `ms-2 visibility-${activity.visibility.toLowerCase()}`;
// Indoor badge
const indoorBadge = document.getElementById('indoorBadge');
if (activity.indoor === true) {
indoorBadge.style.display = 'inline-block';
if (activity.indoorDetectionMethod) {
indoorBadge.title = `Detected via: ${activity.indoorDetectionMethod}`;
}
} else {
indoorBadge.style.display = 'none';
}
// Description
if (activity.description) {
document.getElementById('activityDescription').textContent = activity.description;

View file

@ -160,6 +160,12 @@
<span class="activity-type-badge activity-type-${activity.activityType.toLowerCase()}">
${activity.activityType}
</span>
${activity.indoor
? `<span class="badge bg-warning text-dark ms-2" title="${activity.indoorDetectionMethod ? 'Detected via: ' + activity.indoorDetectionMethod : 'Indoor Activity'}">
<i class="bi bi-house-door"></i> Indoor
</span>`
: ''
}
<span class="ms-2">
<i class="bi bi-calendar"></i>
${FitPub.formatDateWithTimezone(activity.startedAt, activity.timezone || 'UTC')}

View file

@ -117,7 +117,7 @@
</div>
<!-- Confirm Password -->
<div class="mb-4">
<div class="mb-3">
<label for="confirmPassword" class="form-label">
Confirm Password <span class="text-danger">*</span>
</label>
@ -133,6 +133,25 @@
</div>
</div>
<!-- Registration Password (Invite Code) -->
<div class="mb-4" id="registrationPasswordField" style="display: none;">
<label for="registrationPassword" class="form-label">
Registration Password <span class="text-danger">*</span>
</label>
<input type="password"
class="form-control"
id="registrationPassword"
name="registrationPassword"
placeholder="Enter the registration password"
autocomplete="off">
<div class="form-text">
<i class="bi bi-key"></i> This instance requires a registration password to create new accounts
</div>
<div class="invalid-feedback">
Registration password is required.
</div>
</div>
<!-- Terms and Conditions -->
<div class="mb-3 form-check">
<input type="checkbox"
@ -197,7 +216,7 @@
<!-- Custom Scripts -->
<th:block layout:fragment="scripts">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', async function() {
const form = document.getElementById('registerForm');
const registerBtn = document.getElementById('registerBtn');
const registerBtnText = document.getElementById('registerBtnText');
@ -209,6 +228,18 @@
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
const confirmPasswordFeedback = document.getElementById('confirmPasswordFeedback');
const registrationPasswordField = document.getElementById('registrationPasswordField');
const registrationPasswordInput = document.getElementById('registrationPassword');
// Check if registration password is required by checking URL parameters
// If ?invite=true or REGISTRATION_PASSWORD is set, show the field
const urlParams = new URLSearchParams(window.location.search);
const showRegistrationPassword = urlParams.has('invite') || urlParams.has('code');
// Always show the field and let backend validate
// This simplifies the logic - if not required, backend will ignore it
registrationPasswordField.style.display = 'block';
registrationPasswordInput.required = true;
// Password confirmation validation
confirmPassword.addEventListener('input', function() {
@ -252,7 +283,8 @@
username: document.getElementById('username').value,
email: document.getElementById('email').value,
displayName: document.getElementById('displayName').value,
password: password.value
password: password.value,
registrationPassword: registrationPasswordInput.value || null
};
try {

View file

@ -125,6 +125,58 @@
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
color: white;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.error-container {
background: linear-gradient(135deg, #2d0052 0%, #1a0033 100%);
}
.error-card {
background: #251040;
color: #e8e8f0;
border: 3px solid #ff1493;
}
.error-title {
color: #e8e8f0;
}
.error-subtitle {
color: #a8a8c0;
}
.error-message {
color: #a8a8c0;
}
.error-actions {
background: #1a0a30;
border: 2px solid #00ffff;
}
.error-actions p {
color: #a8a8c0;
}
.btn-home {
background: linear-gradient(135deg, #ff1493 0%, #9d00ff 100%);
border-color: #ff1493;
}
.btn-home:hover {
box-shadow: 0 10px 25px rgba(255, 20, 147, 0.4);
}
.btn-login {
background: linear-gradient(135deg, #00ffff 0%, #39ff14 100%);
color: #0f0520;
}
.btn-login:hover {
box-shadow: 0 10px 25px rgba(0, 255, 255, 0.4);
}
}
</style>
</head>
<body>

View file

@ -114,6 +114,61 @@
.btn-back:hover {
color: #764ba2;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.error-container {
background: linear-gradient(135deg, #2d0052 0%, #1a0033 100%);
}
.error-card {
background: #251040;
color: #e8e8f0;
border: 3px solid #ff1493;
}
.error-title {
color: #e8e8f0;
}
.error-subtitle {
color: #a8a8c0;
}
.error-message {
color: #a8a8c0;
}
.error-suggestions {
background: #1a0a30;
border: 2px solid #00ffff;
}
.error-suggestions h5 {
color: #e8e8f0;
}
.error-suggestions li {
color: #a8a8c0;
}
.btn-home {
background: linear-gradient(135deg, #ff1493 0%, #9d00ff 100%);
border-color: #ff1493;
}
.btn-home:hover {
box-shadow: 0 10px 25px rgba(255, 20, 147, 0.4);
}
.btn-back {
color: #00ffff;
}
.btn-back:hover {
color: #ff1493;
}
}
</style>
</head>
<body>

View file

@ -114,6 +114,61 @@
.btn-back:hover {
color: #fee140;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.error-container {
background: linear-gradient(135deg, #2d0052 0%, #1a0033 100%);
}
.error-card {
background: #251040;
color: #e8e8f0;
border: 3px solid #ff1493;
}
.error-title {
color: #e8e8f0;
}
.error-subtitle {
color: #a8a8c0;
}
.error-message {
color: #a8a8c0;
}
.error-suggestions {
background: #1a0a30;
border: 2px solid #00ffff;
}
.error-suggestions h5 {
color: #e8e8f0;
}
.error-suggestions li {
color: #a8a8c0;
}
.btn-home {
background: linear-gradient(135deg, #ff1493 0%, #9d00ff 100%);
border-color: #ff1493;
}
.btn-home:hover {
box-shadow: 0 10px 25px rgba(255, 20, 147, 0.4);
}
.btn-back {
color: #00ffff;
}
.btn-back:hover {
color: #ff1493;
}
}
</style>
</head>
<body>

View file

@ -81,6 +81,61 @@
.btn-back:hover {
color: #f093fb;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.error-container {
background: linear-gradient(135deg, #2d0052 0%, #1a0033 100%);
}
.error-card {
background: #251040;
color: #e8e8f0;
border: 3px solid #ff1493;
}
.error-title {
color: #e8e8f0;
}
.error-subtitle {
color: #a8a8c0;
}
.error-message {
color: #a8a8c0;
}
.error-code {
background: #1a0a30;
border: 2px solid #00ffff;
}
.error-code strong {
color: #ff1493;
}
.error-code div {
color: #a8a8c0;
}
.btn-home {
background: linear-gradient(135deg, #ff1493 0%, #9d00ff 100%);
border-color: #ff1493;
}
.btn-home:hover {
box-shadow: 0 10px 25px rgba(255, 20, 147, 0.4);
}
.btn-back {
color: #00ffff;
}
.btn-back:hover {
color: #ff1493;
}
}
</style>
</head>
<body>

View file

@ -62,11 +62,16 @@
<script th:inline="javascript">
const targetUsername = /*[[${username}]]*/ '';
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById('usernameDisplay').textContent = targetUsername;
loadFollowing();
async function loadFollowing() {
// Check if viewing own following list
const currentUser = FitPubAuth.getCurrentUser();
const isOwnProfile = currentUser && currentUser.username === targetUsername;
loadFollowing(isOwnProfile);
async function loadFollowing(isOwnProfile) {
try {
const response = await fetch(`/api/users/${targetUsername}/following`);
@ -81,7 +86,7 @@
document.getElementById('followingContent').classList.remove('d-none');
if (following.length > 0) {
renderFollowing(following);
renderFollowing(following, isOwnProfile);
} else {
document.getElementById('followingEmpty').classList.remove('d-none');
}
@ -93,7 +98,44 @@
}
}
function renderFollowing(following) {
async function handleUnfollow(username, button) {
if (!confirm(`Are you sure you want to unfollow @${username}?`)) {
return;
}
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Unfollowing...';
try {
const response = await FitPubAuth.authenticatedFetch(`/api/users/${username}/follow`, {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to unfollow');
}
// Remove the user from the list
button.closest('.border-bottom').remove();
// Check if list is now empty
const list = document.getElementById('followingList');
if (list.children.length === 0) {
document.getElementById('followingEmpty').classList.remove('d-none');
}
FitPub.showAlert('success', `Successfully unfollowed @${username}`);
} catch (error) {
console.error('Unfollow error:', error);
FitPub.showAlert('danger', error.message || 'Failed to unfollow user');
button.disabled = false;
button.innerHTML = originalText;
}
}
function renderFollowing(following, isOwnProfile) {
const list = document.getElementById('followingList');
list.innerHTML = following.map(user => `
<div class="d-flex align-items-center py-3 border-bottom">
@ -107,7 +149,7 @@
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="flex-grow-1">
<h6 class="mb-0">
${user.local ?
`<a href="/profile/${escapeHtml(user.username)}" class="text-decoration-none">${escapeHtml(user.displayName || user.username)}</a>` :
@ -120,6 +162,14 @@
</p>
${user.bio ? `<p class="small mt-1 mb-0 text-muted">${sanitizeHtml(user.bio)}</p>` : ''}
</div>
${isOwnProfile ? `
<div class="ms-3">
<button class="btn btn-sm btn-outline-danger"
onclick="handleUnfollow('${escapeHtml(user.username || user.handle)}', this)">
<i class="bi bi-person-dash"></i> Unfollow
</button>
</div>
` : ''}
</div>
</div>
</div>