feat: implement history screen with calendar heatmap and day detail
Adds a month grid heatmap (colour-coded by daily mood average), prev/next month navigation, and a scrollable entry list below for the selected day showing mood, time, note, and metric chips (sleep, energy, positivity, self-worth).
This commit is contained in:
@@ -1,12 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/providers/mood_provider.dart';
|
||||
import 'widgets/calendar_heatmap.dart';
|
||||
import 'widgets/day_entry_card.dart';
|
||||
|
||||
class CalendarScreen extends StatelessWidget {
|
||||
const _months = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
|
||||
class CalendarScreen extends ConsumerStatefulWidget {
|
||||
const CalendarScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
|
||||
}
|
||||
|
||||
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
late DateTime _month;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_month = DateTime(now.year, now.month);
|
||||
}
|
||||
|
||||
void _prevMonth() => setState(() => _month = DateTime(_month.year, _month.month - 1));
|
||||
|
||||
void _nextMonth() {
|
||||
final now = DateTime.now();
|
||||
final next = DateTime(_month.year, _month.month + 1);
|
||||
if (!next.isAfter(DateTime(now.year, now.month))) {
|
||||
setState(() => _month = next);
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canGoForward {
|
||||
final now = DateTime.now();
|
||||
return _month.year < now.year ||
|
||||
(_month.year == now.year && _month.month < now.month);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('History coming soon')),
|
||||
final selectedDay = ref.watch(selectedDayProvider);
|
||||
final entriesAsync = ref.watch(moodEntriesForDayProvider(selectedDay));
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('History'),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_MonthNav(
|
||||
label: '${_months[_month.month - 1]} ${_month.year}',
|
||||
onPrev: _prevMonth,
|
||||
onNext: _canGoForward ? _nextMonth : null,
|
||||
),
|
||||
CalendarHeatmap(
|
||||
month: _month,
|
||||
selectedDay: selectedDay,
|
||||
onDayTap: (day) {
|
||||
ref.read(selectedDayProvider.notifier).select(day);
|
||||
// Follow the selected day's month
|
||||
final m = DateTime(day.year, day.month);
|
||||
if (m != _month) setState(() => _month = m);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: entriesAsync.when(
|
||||
data: (entries) => entries.isEmpty
|
||||
? _EmptyDay(day: selectedDay)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: entries.length,
|
||||
itemBuilder: (context, i) =>
|
||||
DayEntryCard(entry: entries[i]),
|
||||
),
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MonthNav extends StatelessWidget {
|
||||
const _MonthNav({
|
||||
required this.label,
|
||||
required this.onPrev,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPrev;
|
||||
final VoidCallback? onNext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(icon: const Icon(Icons.chevron_left), onPressed: onPrev),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: onNext,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyDay extends StatelessWidget {
|
||||
const _EmptyDay({required this.day});
|
||||
|
||||
final DateTime day;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final label = day == today ? 'today' : '${day.day}/${day.month}/${day.year}';
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
'No entries for $label',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user