1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-09 23:25:18 +02:00

feat: add activity statistics to user profile; include distance, moving time, elevation, and total activities

This commit is contained in:
Sean Morley 2025-08-05 18:53:03 -04:00
parent 2459e7d736
commit a173ff5471
2 changed files with 154 additions and 4 deletions

View file

@ -2,8 +2,9 @@ from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db import models
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
from adventures.models import Location, Collection from adventures.models import Location, Collection, Activity
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
@ -37,6 +38,19 @@ class StatsViewSet(viewsets.ViewSet):
visited_country_count = VisitedRegion.objects.filter( visited_country_count = VisitedRegion.objects.filter(
user=user.id).values('region__country').distinct().count() user=user.id).values('region__country').distinct().count()
total_countries = Country.objects.count() total_countries = Country.objects.count()
# get activity counts
user_activities = Activity.objects.filter(
user=user.id)
activity_count = user_activities.count()
activity_distance = user_activities.aggregate(
total_distance=models.Sum('distance'))['total_distance'] or 0
activity_moving_time = user_activities.aggregate(
total_duration=models.Sum('moving_time'))['total_duration'] or 0
activity_elevation = user_activities.aggregate(
total_elevation=models.Sum('elevation_gain'))['total_elevation'] or 0
return Response({ return Response({
'location_count': location_count, 'location_count': location_count,
'trips_count': trips_count, 'trips_count': trips_count,
@ -45,5 +59,9 @@ class StatsViewSet(viewsets.ViewSet):
'visited_region_count': visited_region_count, 'visited_region_count': visited_region_count,
'total_regions': total_regions, 'total_regions': total_regions,
'visited_country_count': visited_country_count, 'visited_country_count': visited_country_count,
'total_countries': total_countries 'total_countries': total_countries,
'activity_distance': activity_distance, # measured in meters
'activity_moving_time': activity_moving_time, # measured in seconds
'activity_elevation': activity_elevation, # measured in meters
'activity_count': activity_count,
}) })

View file

@ -18,6 +18,12 @@
import TrendingUp from '~icons/mdi/trending-up'; import TrendingUp from '~icons/mdi/trending-up';
import Share from '~icons/mdi/share-variant'; import Share from '~icons/mdi/share-variant';
import Award from '~icons/mdi/award'; import Award from '~icons/mdi/award';
import Run from '~icons/mdi/run';
import Timer from '~icons/mdi/timer-outline';
import TrendingUpOutline from '~icons/mdi/trending-up';
import Mountain from '~icons/mdi/mountain';
let measurementSystem = data.user?.measurement_system || 'metric';
let stats: { let stats: {
visited_country_count: number; visited_country_count: number;
@ -28,6 +34,10 @@
total_countries: number; total_countries: number;
visited_city_count: number; visited_city_count: number;
total_cities: number; total_cities: number;
activity_count: number;
activity_distance: number;
activity_moving_time: number;
activity_elevation: number;
} | null; } | null;
const user: User = data.user; const user: User = data.user;
@ -35,6 +45,30 @@
const collections: Collection[] = data.collections; const collections: Collection[] = data.collections;
stats = data.stats || null; stats = data.stats || null;
// function to take in meters for distance and return it in either kilometers or miles
function getDistance(meters: number): string {
return measurementSystem === 'imperial'
? `${(meters * 0.000621371).toFixed(2)} mi`
: `${(meters / 1000).toFixed(2)} km`;
}
function getElevation(meters: number): string {
return measurementSystem === 'imperial'
? `${(meters * 3.28084).toFixed(1)} ft`
: `${meters.toFixed(1)} m`;
}
// Function to format time from seconds to readable format
function formatTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
// Calculate achievements // Calculate achievements
$: worldExplorationPercentage = stats $: worldExplorationPercentage = stats
? Math.round((stats.visited_country_count / stats.total_countries) * 100) ? Math.round((stats.visited_country_count / stats.total_countries) * 100)
@ -258,7 +292,7 @@
</div> </div>
<!-- Secondary Stats --> <!-- Secondary Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<!-- Regions --> <!-- Regions -->
<div <div
class="stat-card card bg-gradient-to-br from-info/10 to-info/5 shadow-xl border border-info/20 hover:shadow-2xl transition-all duration-300" class="stat-card card bg-gradient-to-br from-info/10 to-info/5 shadow-xl border border-info/20 hover:shadow-2xl transition-all duration-300"
@ -320,6 +354,104 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Activity Stats Section -->
{#if stats.activity_count > 0}
<div class="mb-8">
<div class="text-center mb-6">
<h3 class="text-2xl font-bold mb-2">Activity Statistics</h3>
<p class="text-base-content/60">Your fitness and activity achievements</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Activities -->
<div
class="stat-card card bg-gradient-to-br from-accent/10 to-accent/5 shadow-xl border border-accent/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-accent/70 font-medium text-sm uppercase tracking-wide">
Activities
</div>
<div class="text-3xl font-bold text-accent">{stats.activity_count}</div>
<div class="text-accent/60 mt-2">Total recorded</div>
</div>
<div class="p-3 bg-accent/20 rounded-2xl">
<Run class="w-6 h-6 text-accent" />
</div>
</div>
</div>
</div>
<!-- Total Distance -->
<div
class="stat-card card bg-gradient-to-br from-error/10 to-error/5 shadow-xl border border-error/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-error/70 font-medium text-sm uppercase tracking-wide">
Distance
</div>
<div class="text-3xl font-bold text-error">
{getDistance(stats.activity_distance)}
</div>
<div class="text-error/60 mt-2">Total covered</div>
</div>
<div class="p-3 bg-error/20 rounded-2xl">
<TrendingUpOutline class="w-6 h-6 text-error" />
</div>
</div>
</div>
</div>
<!-- Moving Time -->
<div
class="stat-card card bg-gradient-to-br from-purple-500/10 to-purple-500/5 shadow-xl border border-purple-500/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-purple-500/70 font-medium text-sm uppercase tracking-wide">
Moving Time
</div>
<div class="text-3xl font-bold text-purple-500">
{formatTime(stats.activity_moving_time)}
</div>
<div class="text-purple-500/60 mt-2">Active duration</div>
</div>
<div class="p-3 bg-purple-500/20 rounded-2xl">
<Timer class="w-6 h-6 text-purple-500" />
</div>
</div>
</div>
</div>
<!-- Elevation Gain -->
<div
class="stat-card card bg-gradient-to-br from-orange-500/10 to-orange-500/5 shadow-xl border border-orange-500/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-orange-500/70 font-medium text-sm uppercase tracking-wide">
Elevation
</div>
<div class="text-3xl font-bold text-orange-500">
{getElevation(stats.activity_elevation)}
</div>
<div class="text-orange-500/60 mt-2">Total gained</div>
</div>
<div class="p-3 bg-orange-500/20 rounded-2xl">
<Mountain class="w-6 h-6 text-orange-500" />
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
</div> </div>
{/if} {/if}