From 9b80891a4f15b9fa728fb3514cf9eaec6085d56d Mon Sep 17 00:00:00 2001 From: amaralkaff Date: Thu, 7 Nov 2024 14:07:30 +0800 Subject: [PATCH] feat: add progress --- lib/widgets/progress_tracker.dart | 869 ++++++++++++++++-------------- pubspec.lock | 30 +- 2 files changed, 481 insertions(+), 418 deletions(-) diff --git a/lib/widgets/progress_tracker.dart b/lib/widgets/progress_tracker.dart index 09b1c2f..de4289f 100644 --- a/lib/widgets/progress_tracker.dart +++ b/lib/widgets/progress_tracker.dart @@ -1,9 +1,12 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math' show min, max; + import 'package:fl_chart/fl_chart.dart'; -import 'package:workout_ai/services/progress_service.dart'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:workout_ai/models/progress_data.dart' as progress; +import 'package:workout_ai/services/progress_service.dart'; import 'package:workout_ai/services/workout_info_service.dart'; -import 'dart:math' show min, max; class ProgressTracker extends StatefulWidget { final List workouts; @@ -26,31 +29,200 @@ class ProgressTracker extends StatefulWidget { class _ProgressTrackerState extends State { progress.ProgressResponse? _progressData; bool _isLoadingProgress = true; - String? _progressError; + Timer? _refreshTimer; @override void initState() { super.initState(); _loadProgressData(); + _refreshTimer = Timer.periodic( + const Duration(seconds: 30), + (_) => _loadProgressData(), + ); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } + + Widget _buildShimmerContent() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildShimmerWeightCard(), + const SizedBox(height: 20), + ...List.generate( + 3, + (index) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildShimmerWorkoutItem(), + )), + const SizedBox(height: 20), + ], + ); + } + + Widget _buildShimmerWorkoutItem() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 16, + color: Colors.white, + ), + const SizedBox(height: 8), + Row( + children: [ + Container( + width: 60, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 8), + Container( + width: 60, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildShimmerWeightCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 120, + height: 16, + color: Colors.white, + ), + const SizedBox(height: 8), + Container( + width: 80, + height: 32, + color: Colors.white, + ), + const SizedBox(height: 24), + AspectRatio( + aspectRatio: 1.70, + child: Container( + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Center( + child: Container( + width: 60, + height: 20, + color: Colors.white, + ), + ), + const SizedBox(height: 24), + Container( + width: 180, + height: 16, + color: Colors.white, + ), + const SizedBox(height: 8), + Container( + width: 100, + height: 32, + color: Colors.white, + ), + ], + ), + ), + ); } Future _loadProgressData() async { + if (!mounted) return; + try { - setState(() { - _isLoadingProgress = true; - _progressError = null; - }); + if (_progressData == null) { + setState(() { + _isLoadingProgress = true; + }); + } final progressService = ProgressService(); final data = await progressService.getProgress(); + if (!mounted) return; + setState(() { _progressData = data; _isLoadingProgress = false; }); } catch (e) { + if (!mounted) return; setState(() { - _progressError = e.toString(); _isLoadingProgress = false; }); } @@ -58,39 +230,43 @@ class _ProgressTrackerState extends State { @override Widget build(BuildContext context) { - if (widget.isLoading || _isLoadingProgress) { - return const Padding( - padding: EdgeInsets.all(20.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - if (widget.error != null) { - return _buildErrorWidget(); - } - - if (_progressError != null) { - return _buildProgressErrorWidget(); - } - - return RefreshIndicator( - onRefresh: _loadProgressData, - child: ListView( - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - _buildWeightTracker(), - const SizedBox(height: 20), - if (widget.workouts.isEmpty) - _buildEmptyState() - else - ...widget.workouts.map((workout) { - final color = - workout.woName.contains('Push') ? Colors.blue : Colors.green; - return _buildWorkoutItem(workout, color); - }), - ], + final bool isLoading = widget.isLoading || _isLoadingProgress; + + return SizedBox( + height: MediaQuery.of(context).size.height, + child: RefreshIndicator( + onRefresh: () async { + await Future.wait([ + _loadProgressData(), + Future.delayed(const Duration(milliseconds: 500)), + ]); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: isLoading + ? _buildShimmerContent() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWeightTracker(), + const SizedBox(height: 20), + if (widget.workouts.isEmpty) + _buildEmptyState() + else + ...widget.workouts.map((workout) { + final color = workout.woName.contains('Push') + ? Colors.blue + : Colors.green; + return _buildWorkoutItem(workout, color); + }), + // Add bottom padding + const SizedBox(height: 20), + ], + ), + ), + ), ), ); } @@ -103,11 +279,15 @@ class _ProgressTrackerState extends State { color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), - child: const Text('No progress data available'), + child: const SizedBox( + height: 100, + child: Center( + child: Text('No progress data available'), + ), + ), ); } - // Sort data by month final sortedData = List.from(_progressData!.data) ..sort((a, b) => a.month.compareTo(b.month)); @@ -132,6 +312,7 @@ class _ProgressTrackerState extends State { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ _buildHeaderSection(isGain, weightChange, percentChange), const SizedBox(height: 24), @@ -154,7 +335,7 @@ class _ProgressTrackerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Current Weight', // Changed from 'Average Weight' + 'Current Weight', style: TextStyle( color: Colors.grey.shade600, fontSize: 14, @@ -164,8 +345,7 @@ class _ProgressTrackerState extends State { Row( children: [ Text( - // Use current weight from the latest data point - _progressData!.data.last.currentWeight.toString(), + _progressData!.data.last.currentWeight.toStringAsFixed(1), style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, @@ -184,7 +364,6 @@ class _ProgressTrackerState extends State { ), ], ), - // Only show weight change if we have more than one month of data if (_progressData!.data.length > 1) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -201,7 +380,7 @@ class _ProgressTrackerState extends State { ), const SizedBox(width: 4), Text( - '${weightChange.abs().toStringAsFixed(2)} (${isGain ? '+' : '-'}${percentChange.toStringAsFixed(1)}%)', + '${weightChange.abs().toStringAsFixed(1)} (${isGain ? '+' : '-'}${percentChange.toStringAsFixed(1)}%)', style: TextStyle( color: isGain ? Colors.red.shade600 : Colors.green.shade600, fontSize: 14, @@ -215,193 +394,6 @@ class _ProgressTrackerState extends State { ); } - Widget _buildChart(List sortedData) { - // Calculate ranges - final minWeight = sortedData.map((d) => d.currentWeight).reduce(min); - final maxWeight = sortedData.map((d) => d.currentWeight).reduce(max); - final minCalories = sortedData.map((d) => d.totalCalories).reduce(min); - final maxCalories = sortedData.map((d) => d.totalCalories).reduce(max); - - // Calculate scale factors - final weightRange = maxWeight - minWeight; - final caloriesRange = maxCalories - minCalories; - - // Adjust padding and range for single month - final chartPadding = sortedData.length == 1 - ? maxWeight * 0.1 // 10% of the weight value - : weightRange * 0.1; - - // For single month, create a wider display range - final effectiveMinWeight = sortedData.length == 1 - ? minWeight * 0.9 // Show 90% of the weight value as minimum - : minWeight - chartPadding; - final effectiveMaxWeight = sortedData.length == 1 - ? maxWeight * 1.1 // Show 110% of the weight value as maximum - : maxWeight + chartPadding; - - return AspectRatio( - aspectRatio: 1.70, - child: Padding( - padding: - const EdgeInsets.only(right: 18, left: 12, top: 24, bottom: 12), - child: LineChart( - LineChartData( - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: (effectiveMaxWeight - effectiveMinWeight) / 5, - getDrawingHorizontalLine: (value) { - return FlLine( - color: Colors.grey.shade200, - strokeWidth: 1, - ); - }, - ), - titlesData: FlTitlesData( - show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - interval: 1, - getTitlesWidget: (value, meta) { - if (value.toInt() >= sortedData.length) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _getMonthName(sortedData[value.toInt()].month), - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: sortedData.length == 1 - ? (effectiveMaxWeight - effectiveMinWeight) / 5 - : weightRange / 5, - reservedSize: 45, - getTitlesWidget: (value, meta) { - return Text( - value.toStringAsFixed(1), - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ); - }, - ), - ), - ), - borderData: FlBorderData(show: false), - minX: 0, - maxX: - sortedData.length == 1 ? 1 : (sortedData.length - 1).toDouble(), - minY: effectiveMinWeight, - maxY: effectiveMaxWeight, - lineBarsData: [ - // Weight Line - LineChartBarData( - spots: sortedData.asMap().entries.map((entry) { - return FlSpot( - entry.key.toDouble(), - entry.value.currentWeight, - ); - }).toList(), - isCurved: - sortedData.length > 1, // Only curve if multiple points - color: Colors.blue, - barWidth: 3, - isStrokeCapRound: true, - dotData: FlDotData( - show: true, - getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 4, - color: Colors.white, - strokeWidth: 2, - strokeColor: Colors.blue, - ); - }, - ), - belowBarData: BarAreaData( - show: true, - color: Colors.blue.withOpacity(0.1), - ), - ), - // Calories Line (scaled to weight range) - if (sortedData.length > - 1) // Only show calories line for multiple months - LineChartBarData( - spots: sortedData.asMap().entries.map((entry) { - final scaledCalories = - (entry.value.totalCalories - minCalories) * - (weightRange / caloriesRange) + - minWeight; - return FlSpot(entry.key.toDouble(), scaledCalories); - }).toList(), - isCurved: true, - color: Colors.green, - barWidth: 2, - isStrokeCapRound: true, - dotData: const FlDotData(show: false), - dashArray: [5, 5], - ), - ], - lineTouchData: LineTouchData( - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (spot) => Colors.blue, - tooltipRoundedRadius: 8, - getTooltipItems: (touchedSpots) { - return touchedSpots.map((spot) { - final data = sortedData[spot.x.toInt()]; - final isWeight = spot.barIndex == 0; - return LineTooltipItem( - isWeight - ? 'Weight: ${data.currentWeight.toStringAsFixed(1)} kg' - : 'Month Total: ${data.totalCalories.toStringAsFixed(1)} cal', - const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - children: [ - TextSpan( - text: '\n${_getMonthName(data.month)}', - style: TextStyle( - color: Colors.grey.shade300, - fontSize: 10, - fontWeight: FontWeight.normal, - ), - ), - ], - ); - }).toList(); - }, - ), - ), - ), - ), - ), - ); - } - Widget _buildChartLegend() { return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -438,157 +430,167 @@ class _ProgressTrackerState extends State { ); } - Widget _buildCaloriesSection(List sortedData) { - // Just use the calories from the current month - final currentMonthCalories = sortedData.last.totalCalories; + Widget _buildChart(List sortedData) { + if (sortedData.isEmpty) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Calories Burned This Month', // Changed text to be more specific - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, + try { + final weights = sortedData.map((d) => d.currentWeight).toList(); + if (weights.isEmpty) return const SizedBox.shrink(); + + final minWeight = weights.reduce(min); + final maxWeight = weights.reduce(max); + + if (maxWeight - minWeight < 0.01) { + return Container( + height: 200, + alignment: Alignment.center, + child: Text( + 'Weight: ${weights.first.toStringAsFixed(1)} kg', + style: const TextStyle(fontSize: 16), ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - currentMonthCalories.toStringAsFixed(1), - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, - ), - ), - const SizedBox(width: 4), - Text( - 'Kcal', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, + ); + } + + return RepaintBoundary( + child: AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: + const EdgeInsets.only(right: 18, left: 12, top: 24, bottom: 12), + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: (maxWeight - minWeight) / 4, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + ), + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 1, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index < 0 || index >= sortedData.length) { + return const SizedBox.shrink(); + } + return Text( + _getMonthName(sortedData[index].month), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: (maxWeight - minWeight) / 4, + getTitlesWidget: (value, meta) { + return Text( + value.toStringAsFixed(1), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ); + }, + ), + ), + ), + borderData: FlBorderData(show: false), + minX: 0, + maxX: (sortedData.length - 1).toDouble(), + minY: minWeight - 1, + maxY: maxWeight + 1, + lineBarsData: [ + LineChartBarData( + spots: sortedData.asMap().entries.map((entry) { + return FlSpot( + entry.key.toDouble(), + entry.value.currentWeight, + ); + }).toList(), + isCurved: false, + color: Colors.blue, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.white, + strokeWidth: 2, + strokeColor: Colors.blue, + ); + }, + ), + ), + ], ), ), - ], + ), ), - ], - ); + ); + } catch (e) { + debugPrint('Chart error: $e'); + return Container( + height: 200, + alignment: Alignment.center, + child: const Text('Unable to display chart'), + ); + } } - Widget _buildProgressErrorWidget() { + Widget _buildEmptyState() { return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), + width: double.infinity, + padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: Colors.red.shade50, + color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.shade200), + border: Border.all(color: Colors.grey.shade200), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: Colors.red.shade700), - const SizedBox(height: 16), - Text( - 'Error loading progress data:\n$_progressError', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.red.shade700, - fontSize: 14, - ), + Icon( + Icons.fitness_center, + size: 48, + color: Colors.grey.shade400, ), const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _loadProgressData, - icon: const Icon(Icons.refresh), - label: const Text('Try Again'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.red.shade400, - ), - ), - ], - ), - ); - } - - Widget _buildErrorWidget() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.error_outline, color: Colors.red.shade700), - const SizedBox(width: 8), - Expanded( - child: Text( - widget.error!.contains('Authentication') - ? 'Please log in again to view your progress' - : 'Unable to load workout progress', - style: TextStyle( - color: Colors.red.shade700, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - widget.error!, - style: TextStyle(color: Colors.red.shade700), - ), - const SizedBox(height: 12), - Center( - child: ElevatedButton.icon( - onPressed: widget.onRetry, - icon: const Icon(Icons.refresh), - label: const Text('Try Again'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.red.shade400, - ), - ), - ), - ], - ), - ); - } - - Widget _buildEmptyState() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: const Column( - children: [ - Icon(Icons.fitness_center, size: 48, color: Colors.grey), - SizedBox(height: 16), Text( 'No workouts yet', style: TextStyle( - fontSize: 16, + fontSize: 18, fontWeight: FontWeight.bold, + color: Colors.grey.shade800, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text( 'Complete your first workout to see your progress here', textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), ), ], ), @@ -596,54 +598,78 @@ class _ProgressTrackerState extends State { } Widget _buildWorkoutItem(WorkoutInfo workout, Color color) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: color.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.1)), - ), - child: Row( - children: [ - Icon( - workout.woName.contains('Push') - ? Icons.fitness_center - : Icons.accessibility_new, - color: color, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Container( + decoration: BoxDecoration( + color: color.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.1)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( children: [ - Text( - workout.woName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + workout.woName.contains('Push') + ? Icons.fitness_center + : Icons.accessibility_new, + color: color, + size: 20, ), ), - const SizedBox(height: 8), - Row( - children: [ - _buildStatChip( - '${workout.sumWo} reps', - color, - color.withOpacity(0.1), - ), - const SizedBox(width: 8), - _buildStatChip( - '${workout.totalCalories.toStringAsFixed(1)} cal', - Colors.green, - Colors.green.withOpacity(0.1), - ), - ], + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + workout.woName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildStatChip( + '${workout.sumWo} reps', + color, + color.withOpacity(0.1), + ), + const SizedBox(width: 8), + _buildStatChip( + '${workout.totalCalories.toStringAsFixed(1)} cal', + Colors.green, + Colors.green.withOpacity(0.1), + ), + ], + ), + ], + ), ), ], ), ), - ], + ), ), ); } @@ -665,6 +691,43 @@ class _ProgressTrackerState extends State { ); } + Widget _buildCaloriesSection(List sortedData) { + final lastEntry = sortedData.last; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Daily Calories', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + lastEntry.totalCalories.toStringAsFixed(0), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(width: 4), + Text( + 'kcal', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ], + ); + } + String _getMonthName(int month) { const monthNames = [ 'Jan', diff --git a/pubspec.lock b/pubspec.lock index eb86ffc..9cb8c7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" cross_file: dependency: transitive description: @@ -492,18 +492,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -716,7 +716,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -729,10 +729,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -753,10 +753,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -769,10 +769,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" typed_data: dependency: transitive description: @@ -793,10 +793,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" web: dependency: transitive description: