From d1b18c5296dee0c01eb64b92d85af043ff57c998 Mon Sep 17 00:00:00 2001 From: Stefan Willoughby Date: Sun, 26 Apr 2026 14:29:00 +1000 Subject: [PATCH] feat: add day detail screen with swipe-to-delete and undo Adds DayDetailScreen showing all entries for a selected day with swipe-to-delete and an undo snackbar. CalendarScreen gains a tappable day header (Today / Yesterday / Month Day) that pushes the detail screen. --- lib/features/calendar/calendar_screen.dart | 32 ++++++ lib/features/calendar/day_detail_screen.dart | 111 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 lib/features/calendar/day_detail_screen.dart diff --git a/lib/features/calendar/calendar_screen.dart b/lib/features/calendar/calendar_screen.dart index f7d4a2b..90553fa 100644 --- a/lib/features/calendar/calendar_screen.dart +++ b/lib/features/calendar/calendar_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/providers/mood_provider.dart'; +import 'day_detail_screen.dart'; import 'widgets/calendar_heatmap.dart'; import 'widgets/day_entry_card.dart'; @@ -36,6 +37,15 @@ class _CalendarScreenState extends ConsumerState { } } + static String _dayLabel(DateTime day) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + if (day == today) return 'Today'; + if (day == yesterday) return 'Yesterday'; + return '${_months[day.month - 1]} ${day.day}'; + } + bool get _canGoForward { final now = DateTime.now(); return _month.year < now.year || @@ -70,6 +80,28 @@ class _CalendarScreenState extends ConsumerState { }, ), const Divider(height: 1), + InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DayDetailScreen(day: selectedDay), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Text( + _dayLabel(selectedDay), + style: Theme.of(context).textTheme.titleSmall, + ), + const Spacer(), + Icon(Icons.chevron_right, + color: Theme.of(context).colorScheme.outline), + ], + ), + ), + ), + const Divider(height: 1), Expanded( child: entriesAsync.when( data: (entries) => entries.isEmpty diff --git a/lib/features/calendar/day_detail_screen.dart b/lib/features/calendar/day_detail_screen.dart new file mode 100644 index 0000000..184ccaf --- /dev/null +++ b/lib/features/calendar/day_detail_screen.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/database/app_database.dart'; +import '../../core/providers/mood_provider.dart'; +import 'widgets/day_entry_card.dart'; + +const _weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; +const _months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December', +]; + +class DayDetailScreen extends ConsumerWidget { + const DayDetailScreen({super.key, required this.day}); + + final DateTime day; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final entriesAsync = ref.watch(moodEntriesForDayProvider(day)); + + return Scaffold( + appBar: AppBar( + title: Text(_appBarTitle(day)), + centerTitle: false, + ), + body: entriesAsync.when( + data: (entries) => entries.isEmpty + ? Center( + child: Text( + 'No entries', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.outline), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: entries.length, + itemBuilder: (context, i) => _SwipeToDeleteCard( + entry: entries[i], + onDelete: () => _deleteWithUndo(context, ref, entries[i]), + ), + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => const SizedBox.shrink(), + ), + ); + } + + void _deleteWithUndo(BuildContext context, WidgetRef ref, MoodEntry entry) { + ref.read(moodEntryNotifierProvider.notifier).deleteEntry(entry.id); + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + SnackBar( + content: const Text('Entry deleted'), + action: SnackBarAction( + label: 'Undo', + onPressed: () => ref.read(moodEntryNotifierProvider.notifier).addEntry( + moodLevel: entry.moodLevel, + note: entry.note, + timestamp: entry.timestamp, + sleepMinutes: entry.sleepMinutes, + energyLevel: entry.energyLevel, + positivityLevel: entry.positivityLevel, + selfWorthLevel: entry.selfWorthLevel, + ), + ), + ), + ); + } + + static String _appBarTitle(DateTime day) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + if (day == today) return 'Today'; + if (day == yesterday) return 'Yesterday'; + return '${_weekdays[day.weekday - 1]}, ${day.day} ${_months[day.month - 1]}'; + } +} + +class _SwipeToDeleteCard extends StatelessWidget { + const _SwipeToDeleteCard({required this.entry, required this.onDelete}); + + final MoodEntry entry; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Dismissible( + key: ValueKey(entry.id), + direction: DismissDirection.endToStart, + onDismissed: (_) => onDelete(), + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 24), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: cs.errorContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(Icons.delete_outline, color: cs.onErrorContainer), + ), + child: DayEntryCard(entry: entry), + ); + } +}