Files
dailyou/lib/features/calendar/widgets/day_entry_card.dart
T
stefwill 10c15e5c90 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).
2026-04-26 14:25:20 +10:00

138 lines
4.2 KiB
Dart

import 'package:flutter/material.dart';
import '../../../core/database/app_database.dart';
const _moodEmojis = ['😞', '😔', '😐', '🙂', '😄'];
const _moodLabels = ['Awful', 'Bad', 'Okay', 'Good', 'Great'];
class DayEntryCard extends StatelessWidget {
const DayEntryCard({super.key, required this.entry});
final MoodEntry entry;
@override
Widget build(BuildContext context) {
final idx = (entry.moodLevel - 1).clamp(0, 4);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_moodEmojis[idx], style: const TextStyle(fontSize: 28)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(_moodLabels[idx],
style: Theme.of(context).textTheme.titleSmall),
const Spacer(),
Text(
_formatTime(entry.timestamp),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
],
),
if (entry.note != null && entry.note!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
entry.note!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
if (_hasMetrics) ...[
const SizedBox(height: 8),
_MetricsRow(entry: entry),
],
],
),
),
],
),
),
);
}
bool get _hasMetrics =>
entry.sleepMinutes != null ||
entry.energyLevel != null ||
entry.positivityLevel != null ||
entry.selfWorthLevel != null;
static String _formatTime(DateTime dt) {
final h = dt.hour.toString().padLeft(2, '0');
final m = dt.minute.toString().padLeft(2, '0');
return '$h:$m';
}
}
class _MetricsRow extends StatelessWidget {
const _MetricsRow({required this.entry});
final MoodEntry entry;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 6,
runSpacing: 4,
children: [
if (entry.sleepMinutes != null)
_Chip(
icon: Icons.bedtime_outlined,
label: _sleepLabel(entry.sleepMinutes!),
),
if (entry.energyLevel != null)
_Chip(label: ['💤', '🥱', '⚡', '🔥', '🚀'][(entry.energyLevel! - 1).clamp(0, 4)]),
if (entry.positivityLevel != null)
_Chip(label: ['🌧', '☁️', '🌤', '☀️', '🌈'][(entry.positivityLevel! - 1).clamp(0, 4)]),
if (entry.selfWorthLevel != null)
_Chip(label: ['💔', '😕', '😐', '💪', '⭐'][(entry.selfWorthLevel! - 1).clamp(0, 4)]),
],
);
}
static String _sleepLabel(int minutes) {
final h = minutes ~/ 60;
final m = (minutes % 60).toString().padLeft(2, '0');
return '$h:$m';
}
}
class _Chip extends StatelessWidget {
const _Chip({this.icon, required this.label});
final IconData? icon;
final String label;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 12, color: cs.outline),
const SizedBox(width: 3),
],
Text(label, style: Theme.of(context).textTheme.labelSmall),
],
),
);
}
}