@@ -1281,12 +1281,12 @@ class BeerProvider extends ChangeNotifier {
12811281}
12821282```
12831283
1284- #### UI Flow: Mark as Tasted with Notes
1284+ #### UI Flow: Add Tasting Record
12851285
1286- When user taps "Mark as Tasted", show a dialog prompting for optional tasting notes:
1286+ ** "Mark as Tasted" is an additive action ** - each tap adds a new tasting record with timestamp. Users can taste the same drink multiple times (normal behavior at multi-day festivals).
12871287
12881288``` dart
1289- Future<void> _markAsTasted (BuildContext context, String festivalId, String drinkId) async {
1289+ Future<void> _addTasting (BuildContext context, String festivalId, String drinkId) async {
12901290 final provider = context.read<BeerProvider>();
12911291
12921292 // Show dialog to optionally add notes
@@ -1295,25 +1295,51 @@ Future<void> _markAsTasted(BuildContext context, String festivalId, String drink
12951295 builder: (context) => _TastingNoteDialog(
12961296 festivalId: festivalId,
12971297 drinkId: drinkId,
1298+ isNewTasting: true,
12981299 ),
12991300 );
13001301
1301- // Mark as tasted (always happens, even if no note)
1302- provider.markAsTried(festivalId, drinkId);
1302+ // Add tasting record (always happens, even if dialog dismissed)
1303+ // Note: Dialog can be dismissed without entering notes (Skip button)
1304+ if (note != null) {
1305+ // User clicked Save (note may be empty string if they cleared it)
1306+ provider.markAsTried(festivalId, drinkId);
13031307
1304- // Save note if provided
1305- if (note != null && note.trim().isNotEmpty) {
1306- provider.setTastingNote(festivalId, drinkId, note.trim());
1308+ if (note.trim().isNotEmpty) {
1309+ provider.setTastingNote(festivalId, drinkId, note.trim());
1310+ }
13071311 }
13081312
13091313 if (context.mounted) {
13101314 ScaffoldMessenger.of(context).showSnackBar(
1311- SnackBar(content: Text('Marked as tasted!')),
1315+ SnackBar(
1316+ content: Text('Added to tasting log'),
1317+ action: SnackBarAction(
1318+ label: 'Undo',
1319+ onPressed: () {
1320+ // Remove the most recent tasting
1321+ final favorite = provider.getFavorite(festivalId, drinkId);
1322+ if (favorite != null && favorite.triedDates.isNotEmpty) {
1323+ provider.removeTriedDate(
1324+ festivalId,
1325+ drinkId,
1326+ favorite.triedDates.last,
1327+ );
1328+ }
1329+ },
1330+ ),
1331+ ),
13121332 );
13131333 }
13141334}
13151335```
13161336
1337+ ** Key UX principles:**
1338+ - ** Additive action** : Each tap adds a new tasting (doesn't toggle)
1339+ - ** Undo available** : Snackbar with "Undo" for immediate mistakes
1340+ - ** Notes optional** : Dialog can be skipped quickly if user is busy
1341+ - ** No confirmation needed** : Direct action, no "Are you sure?" dialogs
1342+
13171343#### Tasting Note Dialog
13181344
13191345``` dart
@@ -1379,49 +1405,208 @@ class _TastingNoteDialogState extends State<_TastingNoteDialog> {
13791405}
13801406```
13811407
1382- #### Editing Notes Later
1408+ #### Viewing Tasting History
13831409
1384- Notes can be edited from :
1410+ ** Drink Detail Screen ** shows all tasting records with ability to delete mistakes :
13851411
1386- ** 1. Drink Detail Screen:**
13871412``` dart
1388- // In drink detail screen
1389- final note = provider.getTastingNote(festivalId, drinkId);
1390-
1391- // Show note if exists
1392- if (note != null)
1393- Card(
1394- child: ListTile(
1395- leading: Icon(Icons.edit_note),
1396- title: Text('Your Notes'),
1397- subtitle: Text(note.note),
1398- trailing: IconButton(
1399- icon: Icon(Icons.edit),
1400- onPressed: () => _editNote(context, festivalId, drinkId),
1413+ class DrinkDetailScreen extends StatelessWidget {
1414+ final String festivalId;
1415+ final String drinkId;
1416+
1417+ @override
1418+ Widget build(BuildContext context) {
1419+ final provider = context.watch<BeerProvider>();
1420+ final favorite = provider.getFavorite(festivalId, drinkId);
1421+ final note = provider.getTastingNote(festivalId, drinkId);
1422+
1423+ return Scaffold(
1424+ body: Column(
1425+ children: [
1426+ // ... drink details ...
1427+
1428+ // Tasting history section
1429+ if (favorite?.hasTried ?? false)
1430+ _TastingHistorySection(
1431+ festivalId: festivalId,
1432+ drinkId: drinkId,
1433+ triedDates: favorite!.triedDates,
1434+ note: note,
1435+ ),
1436+ ],
14011437 ),
1402- ),
1403- ),
1438+ floatingActionButton: FloatingActionButton.extended(
1439+ onPressed: () => _addTasting(context, festivalId, drinkId),
1440+ icon: Icon(Icons.add),
1441+ label: Text(favorite?.hasTried ?? false ? 'Add Another' : 'Mark as Tasted'),
1442+ ),
1443+ );
1444+ }
1445+ }
14041446
1405- // Edit note
1406- void _editNote(BuildContext context, String festivalId, String drinkId) async {
1407- final note = await showDialog<String>(
1408- context: context,
1409- builder: (context) => _TastingNoteDialog(
1410- festivalId: festivalId,
1411- drinkId: drinkId,
1412- ),
1413- );
1447+ class _TastingHistorySection extends StatelessWidget {
1448+ final String festivalId;
1449+ final String drinkId;
1450+ final List<DateTime> triedDates;
1451+ final TastingNote? note;
14141452
1415- if (note != null) {
1416- if (note.trim().isEmpty) {
1417- context.read<BeerProvider>().removeTastingNote(festivalId, drinkId);
1418- } else {
1419- context.read<BeerProvider>().setTastingNote(festivalId, drinkId, note.trim());
1453+ const _TastingHistorySection({
1454+ required this.festivalId,
1455+ required this.drinkId,
1456+ required this.triedDates,
1457+ this.note,
1458+ });
1459+
1460+ @override
1461+ Widget build(BuildContext context) {
1462+ return Card(
1463+ margin: EdgeInsets.all(16),
1464+ child: Column(
1465+ crossAxisAlignment: CrossAxisAlignment.start,
1466+ children: [
1467+ ListTile(
1468+ leading: Icon(Icons.history),
1469+ title: Text('Tasting History'),
1470+ subtitle: Text('${triedDates.length} time${triedDates.length == 1 ? '' : 's'}'),
1471+ ),
1472+
1473+ // List of all tasting records
1474+ ...triedDates.asMap().entries.map((entry) {
1475+ final index = entry.key;
1476+ final date = entry.value;
1477+
1478+ return Dismissible(
1479+ key: Key('tasting-$drinkId-${date.millisecondsSinceEpoch}'),
1480+ direction: DismissDirection.endToStart,
1481+ background: Container(
1482+ color: Colors.red,
1483+ alignment: Alignment.centerRight,
1484+ padding: EdgeInsets.only(right: 16),
1485+ child: Icon(Icons.delete, color: Colors.white),
1486+ ),
1487+ confirmDismiss: (direction) async {
1488+ // Only show confirmation if more than one tasting
1489+ // (last tasting can be undone via snackbar)
1490+ if (triedDates.length > 1) {
1491+ return await showDialog<bool>(
1492+ context: context,
1493+ builder: (context) => AlertDialog(
1494+ title: Text('Delete tasting record?'),
1495+ content: Text('This was a mistake?'),
1496+ actions: [
1497+ TextButton(
1498+ onPressed: () => Navigator.pop(context, false),
1499+ child: Text('Cancel'),
1500+ ),
1501+ TextButton(
1502+ onPressed: () => Navigator.pop(context, true),
1503+ child: Text('Delete'),
1504+ ),
1505+ ],
1506+ ),
1507+ );
1508+ }
1509+ return true;
1510+ },
1511+ onDismissed: (direction) {
1512+ context.read<BeerProvider>().removeTriedDate(
1513+ festivalId,
1514+ drinkId,
1515+ date,
1516+ );
1517+ },
1518+ child: ListTile(
1519+ leading: CircleAvatar(
1520+ child: Text('${index + 1}'),
1521+ backgroundColor: Colors.green.withOpacity(0.2),
1522+ foregroundColor: Colors.green,
1523+ ),
1524+ title: Text(DateFormat.yMMMd().add_jm().format(date)),
1525+ trailing: Icon(Icons.chevron_right),
1526+ onTap: () => _editTastingDate(context, date),
1527+ ),
1528+ );
1529+ }),
1530+
1531+ // Tasting notes section
1532+ Divider(),
1533+ ListTile(
1534+ leading: Icon(Icons.edit_note),
1535+ title: Text('Your Notes'),
1536+ subtitle: note != null
1537+ ? Text(note.note)
1538+ : Text('No notes yet', style: TextStyle(fontStyle: FontStyle.italic)),
1539+ trailing: Icon(Icons.edit),
1540+ onTap: () => _editNote(context, festivalId, drinkId),
1541+ ),
1542+ ],
1543+ ),
1544+ );
1545+ }
1546+
1547+ void _editTastingDate(BuildContext context, DateTime currentDate) async {
1548+ // Allow editing the timestamp (for corrections)
1549+ final newDate = await showDatePicker(
1550+ context: context,
1551+ initialDate: currentDate,
1552+ firstDate: DateTime(2020),
1553+ lastDate: DateTime(2030),
1554+ );
1555+
1556+ if (newDate != null) {
1557+ final newTime = await showTimePicker(
1558+ context: context,
1559+ initialTime: TimeOfDay.fromDateTime(currentDate),
1560+ );
1561+
1562+ if (newTime != null) {
1563+ final updatedDate = DateTime(
1564+ newDate.year,
1565+ newDate.month,
1566+ newDate.day,
1567+ newTime.hour,
1568+ newTime.minute,
1569+ );
1570+
1571+ context.read<BeerProvider>().updateTriedDate(
1572+ festivalId,
1573+ drinkId,
1574+ currentDate,
1575+ updatedDate,
1576+ );
1577+ }
1578+ }
1579+ }
1580+
1581+ void _editNote(BuildContext context, String festivalId, String drinkId) async {
1582+ final newNote = await showDialog<String>(
1583+ context: context,
1584+ builder: (context) => _TastingNoteDialog(
1585+ festivalId: festivalId,
1586+ drinkId: drinkId,
1587+ isNewTasting: false,
1588+ ),
1589+ );
1590+
1591+ if (newNote != null) {
1592+ if (newNote.trim().isEmpty) {
1593+ context.read<BeerProvider>().removeTastingNote(festivalId, drinkId);
1594+ } else {
1595+ context.read<BeerProvider>().setTastingNote(festivalId, drinkId, newNote.trim());
1596+ }
14201597 }
14211598 }
14221599}
14231600```
14241601
1602+ ** Key UX features:**
1603+ - ** Numbered list** : Shows all tastings chronologically (1, 2, 3...)
1604+ - ** Swipe to delete** : Swipe left on any tasting record to delete (for mistakes)
1605+ - ** Confirmation for multiple** : Only confirms deletion if drink has multiple tastings
1606+ - ** Edit timestamp** : Tap on tasting to edit date/time (for corrections)
1607+ - ** Separate notes** : Notes are per-drink, not per-tasting (simpler UX)
1608+ - ** FAB button label** : Changes from "Mark as Tasted" to "Add Another" after first tasting
1609+
14251610** 2. Festival Log Screen:**
14261611``` dart
14271612// Show note preview in subtitle
0 commit comments