mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-05 04:22:55 +00:00
feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features
Version: 1.1.0 Major changes: - Implemented complete Flutter app structure with all core features - Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features - Integrated Supabase backend with authentication and data repositories - Added offline support with Hive caching and local storage - Implemented comprehensive routing with go_router - Added location services with Google Maps integration - Implemented notifications and home widget support - Added voice recording capabilities and AI chat features - Created comprehensive test suite and documentation - Added Android and iOS platform configurations - Implemented achievements system and social features - Added calendar integration and bucket list functionality This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../data/models/goal_model.dart';
|
||||
import '../../../data/repositories/goals_repository.dart';
|
||||
import '../../../data/repositories/countdown_repository.dart';
|
||||
import '../../../bootstrap/supabase_client.dart';
|
||||
import '../../auth/application/auth_controller.dart';
|
||||
|
||||
class InsightsState {
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final List<Goal> goals;
|
||||
final int totalGoals;
|
||||
final int completedGoals;
|
||||
final int activeGoals;
|
||||
final double overallProgress;
|
||||
final int currentStreak;
|
||||
final int longestStreak;
|
||||
final DateTime? countdownStartDate;
|
||||
final DateTime? countdownEndDate;
|
||||
final int daysRemaining;
|
||||
final double timeElapsedPercentage;
|
||||
|
||||
const InsightsState({
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.goals = const [],
|
||||
this.totalGoals = 0,
|
||||
this.completedGoals = 0,
|
||||
this.activeGoals = 0,
|
||||
this.overallProgress = 0.0,
|
||||
this.currentStreak = 0,
|
||||
this.longestStreak = 0,
|
||||
this.countdownStartDate,
|
||||
this.countdownEndDate,
|
||||
this.daysRemaining = 0,
|
||||
this.timeElapsedPercentage = 0.0,
|
||||
});
|
||||
|
||||
InsightsState copyWith({
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
List<Goal>? goals,
|
||||
int? totalGoals,
|
||||
int? completedGoals,
|
||||
int? activeGoals,
|
||||
double? overallProgress,
|
||||
int? currentStreak,
|
||||
int? longestStreak,
|
||||
DateTime? countdownStartDate,
|
||||
DateTime? countdownEndDate,
|
||||
int? daysRemaining,
|
||||
double? timeElapsedPercentage,
|
||||
}) {
|
||||
return InsightsState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
goals: goals ?? this.goals,
|
||||
totalGoals: totalGoals ?? this.totalGoals,
|
||||
completedGoals: completedGoals ?? this.completedGoals,
|
||||
activeGoals: activeGoals ?? this.activeGoals,
|
||||
overallProgress: overallProgress ?? this.overallProgress,
|
||||
currentStreak: currentStreak ?? this.currentStreak,
|
||||
longestStreak: longestStreak ?? this.longestStreak,
|
||||
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
|
||||
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
|
||||
daysRemaining: daysRemaining ?? this.daysRemaining,
|
||||
timeElapsedPercentage: timeElapsedPercentage ?? this.timeElapsedPercentage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InsightsController extends StateNotifier<InsightsState> {
|
||||
final GoalsRepository _goalsRepository;
|
||||
final CountdownRepository _countdownRepository;
|
||||
final AuthController _authController;
|
||||
|
||||
InsightsController(
|
||||
this._goalsRepository,
|
||||
this._countdownRepository,
|
||||
this._authController,
|
||||
) : super(const InsightsState()) {
|
||||
_loadInsights();
|
||||
}
|
||||
|
||||
Future<void> _loadInsights() async {
|
||||
final userId = _authController.currentUserId;
|
||||
if (userId == null) return;
|
||||
|
||||
state = state.copyWith(isLoading: true);
|
||||
|
||||
try {
|
||||
final goals = await _goalsRepository.getGoals(userId);
|
||||
final countdown = await _countdownRepository.getCountdownInfo(userId);
|
||||
|
||||
final totalGoals = goals.length;
|
||||
final completedGoals = goals.where((g) => g.completed).length;
|
||||
final activeGoals = totalGoals - completedGoals;
|
||||
final overallProgress = totalGoals > 0
|
||||
? (completedGoals / totalGoals) * 100
|
||||
: 0.0;
|
||||
|
||||
final currentStreak = _calculateCurrentStreak(goals);
|
||||
final longestStreak = _calculateLongestStreak(goals);
|
||||
|
||||
final daysRemaining = countdown.daysRemaining ?? 0;
|
||||
final totalDays = countdown.countdownEndDate != null && countdown.countdownStartDate != null
|
||||
? countdown.countdownEndDate!.difference(countdown.countdownStartDate!).inDays
|
||||
: 0;
|
||||
final elapsedDays = countdown.countdownStartDate != null
|
||||
? DateTime.now().difference(countdown.countdownStartDate!).inDays.clamp(0, totalDays)
|
||||
: 0;
|
||||
final timeElapsedPercentage = totalDays > 0
|
||||
? (elapsedDays / totalDays) * 100
|
||||
: 0.0;
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
goals: goals,
|
||||
totalGoals: totalGoals,
|
||||
completedGoals: completedGoals,
|
||||
activeGoals: activeGoals,
|
||||
overallProgress: overallProgress,
|
||||
currentStreak: currentStreak,
|
||||
longestStreak: longestStreak,
|
||||
countdownStartDate: countdown.countdownStartDate,
|
||||
countdownEndDate: countdown.countdownEndDate,
|
||||
daysRemaining: daysRemaining,
|
||||
timeElapsedPercentage: timeElapsedPercentage,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateCurrentStreak(List<Goal> goals) {
|
||||
if (goals.isEmpty) return 0;
|
||||
|
||||
final now = DateTime.now();
|
||||
int streak = 0;
|
||||
DateTime currentDate = now;
|
||||
|
||||
for (int i = 0; i < 365; i++) {
|
||||
final hasActivityOnDay = goals.any((goal) {
|
||||
final updatedDate = goal.updatedAt;
|
||||
return updatedDate.year == currentDate.year &&
|
||||
updatedDate.month == currentDate.month &&
|
||||
updatedDate.day == currentDate.day;
|
||||
});
|
||||
|
||||
if (hasActivityOnDay) {
|
||||
streak++;
|
||||
currentDate = currentDate.subtract(const Duration(days: 1));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
int _calculateLongestStreak(List<Goal> goals) {
|
||||
if (goals.isEmpty) return 0;
|
||||
|
||||
final allDates = goals
|
||||
.map((g) => g.updatedAt)
|
||||
.whereType<DateTime>()
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
if (allDates.isEmpty) return 0;
|
||||
|
||||
int longestStreak = 1;
|
||||
int currentStreak = 1;
|
||||
|
||||
for (int i = 1; i < allDates.length; i++) {
|
||||
final difference = allDates[i].difference(allDates[i - 1]).inDays;
|
||||
if (difference == 1) {
|
||||
currentStreak++;
|
||||
} else if (difference > 1) {
|
||||
longestStreak = longestStreak > currentStreak ? longestStreak : currentStreak;
|
||||
currentStreak = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return longestStreak > currentStreak ? longestStreak : currentStreak;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> getGoalCompletionTrends() {
|
||||
final goals = state.goals;
|
||||
if (goals.isEmpty) return [];
|
||||
|
||||
final now = DateTime.now();
|
||||
final trends = <Map<String, dynamic>>[];
|
||||
|
||||
for (int i = 6; i >= 0; i--) {
|
||||
final weekStart = now.subtract(Duration(days: i * 7));
|
||||
final weekEnd = weekStart.add(const Duration(days: 7));
|
||||
|
||||
final completedInWeek = goals.where((goal) {
|
||||
final updated = goal.updatedAt;
|
||||
return goal.completed &&
|
||||
updated.isAfter(weekStart) &&
|
||||
updated.isBefore(weekEnd);
|
||||
}).length;
|
||||
|
||||
trends.add({
|
||||
'week': 'Week ${7 - i}',
|
||||
'completed': completedInWeek,
|
||||
});
|
||||
}
|
||||
|
||||
return trends;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> getProgressVsTimeData() {
|
||||
if (state.countdownStartDate == null || state.countdownEndDate == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final start = state.countdownStartDate!;
|
||||
final end = state.countdownEndDate!;
|
||||
final totalDays = end.difference(start).inDays;
|
||||
final elapsedDays = DateTime.now().difference(start).inDays.clamp(0, totalDays);
|
||||
|
||||
final data = <Map<String, dynamic>>[];
|
||||
const int intervals = 10;
|
||||
|
||||
for (int i = 0; i <= intervals; i++) {
|
||||
final day = (totalDays * i / intervals).round();
|
||||
final date = start.add(Duration(days: day));
|
||||
final expectedProgress = (i / intervals) * 100;
|
||||
final actualProgress = i <= (elapsedDays / totalDays * intervals).round()
|
||||
? state.overallProgress
|
||||
: 0.0;
|
||||
|
||||
data.add({
|
||||
'day': day,
|
||||
'date': date,
|
||||
'expected': expectedProgress,
|
||||
'actual': actualProgress,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await _loadInsights();
|
||||
}
|
||||
}
|
||||
|
||||
final insightsControllerProvider =
|
||||
StateNotifierProvider<InsightsController, InsightsState>((ref) {
|
||||
final goalsRepository = ref.watch(goalsRepositoryProvider);
|
||||
final countdownRepository = ref.watch(countdownRepositoryProvider);
|
||||
final authController = ref.watch(authControllerProvider.notifier);
|
||||
|
||||
return InsightsController(
|
||||
goalsRepository,
|
||||
countdownRepository,
|
||||
authController,
|
||||
);
|
||||
});
|
||||
|
||||
final goalsRepositoryProvider = Provider<GoalsRepository>((ref) {
|
||||
return GoalsRepository(supabaseClient);
|
||||
});
|
||||
|
||||
final countdownRepositoryProvider = Provider<CountdownRepository>((ref) {
|
||||
return CountdownRepository(supabaseClient);
|
||||
});
|
||||
@@ -0,0 +1,421 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../core/widgets/app_scaffold.dart';
|
||||
import '../../../core/widgets/loading_indicator.dart';
|
||||
import '../../../core/widgets/empty_state.dart';
|
||||
import '../application/insights_controller.dart';
|
||||
|
||||
class InsightsScreen extends ConsumerStatefulWidget {
|
||||
const InsightsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<InsightsScreen> createState() => _InsightsScreenState();
|
||||
}
|
||||
|
||||
class _InsightsScreenState extends ConsumerState<InsightsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(insightsControllerProvider.notifier).refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(insightsControllerProvider);
|
||||
|
||||
return AppScaffold(
|
||||
title: 'Insights',
|
||||
body: state.isLoading
|
||||
? const LoadingIndicator()
|
||||
: state.error != null
|
||||
? _buildError(state.error!)
|
||||
: _buildContent(state),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(String error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Error loading insights',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(error),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(insightsControllerProvider.notifier).refresh();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(InsightsState state) {
|
||||
if (state.totalGoals == 0) {
|
||||
return const EmptyState(
|
||||
icon: Icons.insights_outlined,
|
||||
title: 'No data yet',
|
||||
subtitle: 'Start creating goals to see your insights',
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(insightsControllerProvider.notifier).refresh();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildOverviewCards(state),
|
||||
const SizedBox(height: 24),
|
||||
_buildProgressChart(state),
|
||||
const SizedBox(height: 24),
|
||||
_buildGoalCompletionTrends(state),
|
||||
const SizedBox(height: 24),
|
||||
_buildStreakCard(state),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewCards(InsightsState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Overview',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Total Goals',
|
||||
state.totalGoals.toString(),
|
||||
Icons.flag_outlined,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Completed',
|
||||
state.completedGoals.toString(),
|
||||
Icons.check_circle_outline,
|
||||
Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Active',
|
||||
state.activeGoals.toString(),
|
||||
Icons.pending_outlined,
|
||||
Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Progress',
|
||||
'${state.overallProgress.toStringAsFixed(0)}%',
|
||||
Icons.trending_up,
|
||||
Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressChart(InsightsState state) {
|
||||
final trends = ref.read(insightsControllerProvider.notifier).getProgressVsTimeData();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Progress vs Time',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: trends.isEmpty
|
||||
? const Center(child: Text('No countdown data available'))
|
||||
: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: true),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value % 20 == 0) {
|
||||
return Text(
|
||||
'${value.toInt()}%',
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value.toInt() % 2 == 0) {
|
||||
return Text(
|
||||
'Day ${value.toInt()}',
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: true),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: trends
|
||||
.map((t) => FlSpot(t['day'].toDouble(), t['expected'].toDouble()))
|
||||
.toList(),
|
||||
isCurved: true,
|
||||
color: Colors.grey[400],
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: false),
|
||||
),
|
||||
LineChartBarData(
|
||||
spots: trends
|
||||
.map((t) => FlSpot(t['day'].toDouble(), t['actual'].toDouble()))
|
||||
.toList(),
|
||||
isCurved: true,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegendItem('Expected', Colors.grey[400]!),
|
||||
const SizedBox(width: 16),
|
||||
_buildLegendItem('Actual', Theme.of(context).colorScheme.primary),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoalCompletionTrends(InsightsState state) {
|
||||
final trends = ref.read(insightsControllerProvider.notifier).getGoalCompletionTrends();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Weekly Completion Trends',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: trends.isEmpty
|
||||
? const Center(child: Text('No data available'))
|
||||
: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: trends.map((t) => t['completed'] as int).reduce((a, b) => a > b ? a : b).toDouble() + 1,
|
||||
barGroups: trends.asMap().entries.map((entry) {
|
||||
return BarChartGroupData(
|
||||
x: entry.key,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: entry.value['completed'].toDouble(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 20,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value == value.toInt()) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value >= 0 && value < trends.length) {
|
||||
return Text(
|
||||
trends[value.toInt()]['week'] as String,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStreakCard(InsightsState state) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.local_fire_department,
|
||||
size: 48,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Current Streak',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${state.currentStreak} days',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Longest: ${state.longestStreak} days',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user