Optical improvements, don't duplicate initially created entries
This commit is contained in:
parent
ecb9e1f540
commit
114d92c453
4 changed files with 103 additions and 80 deletions
|
|
@ -78,7 +78,7 @@ public class AchievementService {
|
||||||
|
|
||||||
// First activity overall
|
// First activity overall
|
||||||
long totalActivities = activityRepository.countByUserId(userId);
|
long totalActivities = activityRepository.countByUserId(userId);
|
||||||
if (totalActivities == 1) {
|
if (totalActivities == 1 && !hasAchievement(userId, Achievement.AchievementType.FIRST_ACTIVITY)) {
|
||||||
achievements.add(awardAchievement(
|
achievements.add(awardAchievement(
|
||||||
userId,
|
userId,
|
||||||
Achievement.AchievementType.FIRST_ACTIVITY,
|
Achievement.AchievementType.FIRST_ACTIVITY,
|
||||||
|
|
@ -103,7 +103,7 @@ public class AchievementService {
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (achievementType != null) {
|
if (achievementType != null && !hasAchievement(userId, achievementType)) {
|
||||||
achievements.add(awardAchievement(
|
achievements.add(awardAchievement(
|
||||||
userId,
|
userId,
|
||||||
achievementType,
|
achievementType,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
# For dev: Start PostgreSQL with: docker run -d --name fitpub-postgres -p 5432:5432 -e POSTGRES_DB=fitpub -e POSTGRES_USER=fitpub -e POSTGRES_PASSWORD=fitpub postgis/postgis:16-3.4
|
# For dev: Start PostgreSQL with: docker run -d --name fitpub-postgres -p 5432:5432 -e POSTGRES_DB=fitpub -e POSTGRES_USER=fitpub -e POSTGRES_PASSWORD=change_me_in_production postgis/postgis:16-3.4
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/fitpub}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/fitpub}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:fitpub}
|
username: ${SPRING_DATASOURCE_USERNAME:fitpub}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:fitpub}
|
password: ${SPRING_DATASOURCE_PASSWORD:change_me_in_production}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
|
|
|
||||||
|
|
@ -62,95 +62,75 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card border shadow-sm">
|
<div class="card border shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-2">
|
||||||
<!-- Distance -->
|
<!-- Distance -->
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-signpost-2 text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-signpost-2"></i> Distance
|
<div>
|
||||||
|
<div class="text-muted small">Distance</div>
|
||||||
|
<div class="fw-bold" id="metricDistance">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricDistance">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Duration -->
|
<!-- Duration -->
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-clock text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-clock"></i> Duration
|
<div>
|
||||||
|
<div class="text-muted small">Duration</div>
|
||||||
|
<div class="fw-bold" id="metricDuration">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricDuration">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Elevation Gain -->
|
<!-- Elevation Gain -->
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-graph-up-arrow text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-graph-up-arrow"></i> Elevation Gain
|
<div>
|
||||||
|
<div class="text-muted small">Elevation</div>
|
||||||
|
<div class="fw-bold" id="metricElevationGain">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricElevationGain">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Avg Pace -->
|
<!-- Avg Pace -->
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-speedometer text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-speedometer"></i> Avg Pace
|
<div>
|
||||||
|
<div class="text-muted small">Avg Pace</div>
|
||||||
|
<div class="fw-bold" id="metricPace">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricPace">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Avg Speed -->
|
<!-- Avg Speed -->
|
||||||
<div class="col-md-3 col-6" id="metricAvgSpeedContainer" style="display: none;">
|
<div class="col-md-3 col-6" id="metricAvgSpeedContainer" style="display: none;">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-speedometer2 text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-speedometer2"></i> Avg Speed
|
<div>
|
||||||
|
<div class="text-muted small">Avg Speed</div>
|
||||||
|
<div class="fw-bold" id="metricAvgSpeed">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricAvgSpeed">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Max Speed -->
|
|
||||||
<div class="col-md-3 col-6" id="metricMaxSpeedContainer" style="display: none;">
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="text-muted small mb-1">
|
|
||||||
<i class="bi bi-lightning"></i> Max Speed
|
|
||||||
</div>
|
|
||||||
<div class="fw-bold fs-5" id="metricMaxSpeed">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Avg Heart Rate -->
|
<!-- Avg Heart Rate -->
|
||||||
<div class="col-md-3 col-6" id="metricAvgHRContainer" style="display: none;">
|
<div class="col-md-3 col-6" id="metricAvgHRContainer" style="display: none;">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-heart text-danger me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-heart"></i> Avg Heart Rate
|
<div>
|
||||||
|
<div class="text-muted small">Avg HR</div>
|
||||||
|
<div class="fw-bold" id="metricAvgHR">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricAvgHR">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Max Heart Rate -->
|
|
||||||
<div class="col-md-3 col-6" id="metricMaxHRContainer" style="display: none;">
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="text-muted small mb-1">
|
|
||||||
<i class="bi bi-heart-pulse-fill"></i> Max Heart Rate
|
|
||||||
</div>
|
|
||||||
<div class="fw-bold fs-5" id="metricMaxHR">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Calories -->
|
<!-- Calories -->
|
||||||
<div class="col-md-3 col-6" id="metricCaloriesContainer" style="display: none;">
|
<div class="col-md-3 col-6" id="metricCaloriesContainer" style="display: none;">
|
||||||
<div class="metric-card">
|
<div class="d-flex align-items-center py-2">
|
||||||
<div class="text-muted small mb-1">
|
<i class="bi bi-fire text-orange me-2" style="font-size: 1.5rem;"></i>
|
||||||
<i class="bi bi-fire"></i> Calories
|
<div>
|
||||||
|
<div class="text-muted small">Calories</div>
|
||||||
|
<div class="fw-bold" id="metricCalories">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold fs-5" id="metricCalories">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Cadence -->
|
|
||||||
<div class="col-md-3 col-6" id="metricCadenceContainer" style="display: none;">
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="text-muted small mb-1">
|
|
||||||
<i class="bi bi-arrow-repeat"></i> Avg Cadence
|
|
||||||
</div>
|
|
||||||
<div class="fw-bold fs-5" id="metricCadence">--</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -511,26 +491,14 @@
|
||||||
document.getElementById('metricAvgSpeedContainer').style.display = 'block';
|
document.getElementById('metricAvgSpeedContainer').style.display = 'block';
|
||||||
document.getElementById('metricAvgSpeed').textContent = (activity.averageSpeed * 3.6).toFixed(1) + ' km/h';
|
document.getElementById('metricAvgSpeed').textContent = (activity.averageSpeed * 3.6).toFixed(1) + ' km/h';
|
||||||
}
|
}
|
||||||
if (activity.maxSpeed) {
|
|
||||||
document.getElementById('metricMaxSpeedContainer').style.display = 'block';
|
|
||||||
document.getElementById('metricMaxSpeed').textContent = (activity.maxSpeed * 3.6).toFixed(1) + ' km/h';
|
|
||||||
}
|
|
||||||
if (activity.averageHeartRate) {
|
if (activity.averageHeartRate) {
|
||||||
document.getElementById('metricAvgHRContainer').style.display = 'block';
|
document.getElementById('metricAvgHRContainer').style.display = 'block';
|
||||||
document.getElementById('metricAvgHR').textContent = activity.averageHeartRate + ' bpm';
|
document.getElementById('metricAvgHR').textContent = activity.averageHeartRate + ' bpm';
|
||||||
}
|
}
|
||||||
if (activity.maxHeartRate) {
|
|
||||||
document.getElementById('metricMaxHRContainer').style.display = 'block';
|
|
||||||
document.getElementById('metricMaxHR').textContent = activity.maxHeartRate + ' bpm';
|
|
||||||
}
|
|
||||||
if (activity.calories) {
|
if (activity.calories) {
|
||||||
document.getElementById('metricCaloriesContainer').style.display = 'block';
|
document.getElementById('metricCaloriesContainer').style.display = 'block';
|
||||||
document.getElementById('metricCalories').textContent = activity.calories + ' kcal';
|
document.getElementById('metricCalories').textContent = activity.calories + ' kcal';
|
||||||
}
|
}
|
||||||
if (activity.averageCadence) {
|
|
||||||
document.getElementById('metricCadenceContainer').style.display = 'block';
|
|
||||||
document.getElementById('metricCadence').textContent = activity.averageCadence + ' spm';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render map if track data exists
|
// Render map if track data exists
|
||||||
if (activity.simplifiedTrack) {
|
if (activity.simplifiedTrack) {
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,14 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
console.log('Form submitted');
|
console.log('Form submitted');
|
||||||
|
console.log('Uploaded activity ID:', uploadedActivityId);
|
||||||
|
|
||||||
|
// If activity is already uploaded, update metadata instead
|
||||||
|
if (uploadedActivityId) {
|
||||||
|
await updateActivityMetadata();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('File input files:', fitFileInput.files);
|
console.log('File input files:', fitFileInput.files);
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
|
|
@ -257,17 +265,10 @@
|
||||||
uploadBtnText.classList.add('d-none');
|
uploadBtnText.classList.add('d-none');
|
||||||
uploadBtnSpinner.classList.remove('d-none');
|
uploadBtnSpinner.classList.remove('d-none');
|
||||||
|
|
||||||
// Prepare form data
|
// Prepare form data (file only for initial upload)
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', fitFileInput.files[0]);
|
formData.append('file', fitFileInput.files[0]);
|
||||||
|
|
||||||
// If metadata is filled, include it
|
|
||||||
if (!metadataSection.classList.contains('d-none')) {
|
|
||||||
formData.append('title', document.getElementById('title').value);
|
|
||||||
formData.append('description', document.getElementById('description').value);
|
|
||||||
formData.append('visibility', document.getElementById('visibility').value);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload with progress tracking
|
// Upload with progress tracking
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
@ -398,6 +399,60 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update activity metadata (after initial upload)
|
||||||
|
async function updateActivityMetadata() {
|
||||||
|
console.log('Updating metadata for activity:', uploadedActivityId);
|
||||||
|
|
||||||
|
// Disable buttons
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
uploadBtnText.classList.add('d-none');
|
||||||
|
uploadBtnSpinner.classList.remove('d-none');
|
||||||
|
|
||||||
|
// Hide alerts
|
||||||
|
errorAlert.classList.add('d-none');
|
||||||
|
successAlert.classList.add('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateData = {
|
||||||
|
title: document.getElementById('title').value,
|
||||||
|
description: document.getElementById('description').value,
|
||||||
|
visibility: document.getElementById('visibility').value
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${uploadedActivityId}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updateData)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Success - redirect to activity detail page
|
||||||
|
FitPub.showAlert('Activity updated successfully!', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/activities/' + uploadedActivityId;
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to update activity');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update error:', error);
|
||||||
|
errorMessage.textContent = error.message || 'Failed to update activity. Please try again.';
|
||||||
|
errorAlert.classList.remove('d-none');
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
uploadBtnText.classList.remove('d-none');
|
||||||
|
uploadBtnSpinner.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function formatDistance(meters) {
|
function formatDistance(meters) {
|
||||||
if (!meters) return 'N/A';
|
if (!meters) return 'N/A';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue