Mark indoor activities to exclude them from the heatmap
This commit is contained in:
parent
851ba87ef2
commit
22c4ca0964
34 changed files with 1626 additions and 58 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue