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:
parent
2459e7d736
commit
a173ff5471
2 changed files with 154 additions and 4 deletions
|
@ -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,
|
||||||
})
|
})
|
|
@ -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}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue