Skip to content

Commit 02a456b

Browse files
committed
Emphasize additive nature of "Mark as Tasted" action
Changes: - Rename _markAsTasted → _addTasting (clearer intent) - Add "Undo" action in snackbar for immediate mistakes - FAB button changes: "Mark as Tasted" → "Add Another" - Show numbered tasting history (1, 2, 3...) - Swipe to delete individual tastings (for mistakes) - Confirmation only if drink has multiple tastings - Edit timestamps via tap (for corrections) - Notes are per-drink, not per-tasting Key UX principle: Adding tastings is primary action (additive), deletion is available but de-emphasized (for mistakes only). Multiple tastings are normal festival behavior.
1 parent 40293f4 commit 02a456b

1 file changed

Lines changed: 226 additions & 41 deletions

File tree

docs/deep-linking-architecture-readonly-urls.md

Lines changed: 226 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)