-
Notifications
You must be signed in to change notification settings - Fork 3
Implement My Festival favourites feature #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ee613bb
feat: implement My Festival data model and service layer (Phase 3.1-3.2)
claude 6a55f26
feat: complete My Festival data layer migration (Phase 3.3)
claude 5c8624a
test: add BeerProvider tests for My Festival methods
claude ec480b3
Initial plan
Copilot d182620
feat: add type-safe FavoriteStatus enum and improve DateTime comparison
Copilot 91ba27f
style: use const for Duration constructors in tests
Copilot db8a05b
Merge pull request #201 from richardthe3rd/copilot/sub-pr-200
richardthe3rd a476cf6
Initial plan
Copilot 39d7fe4
feat: add status badges to DrinkCard (Task 4.1)
Copilot 2b33128
feat: add tasting history UI to DrinkDetailScreen (Task 4.2)
Copilot 0bcdf54
feat: create FavoritesScreen as Festival Log (Task 4.3)
Copilot 706bc44
Merge pull request #202 from richardthe3rd/copilot/sub-pr-200
richardthe3rd 9ff3b44
Initial plan
Copilot a74d841
Fix test failures: handle missing GoRouter in tests and fix bottom ac…
Copilot bfad100
Merge pull request #203 from richardthe3rd/copilot/sub-pr-200
richardthe3rd 6ea9e49
Initial plan
Copilot 8bbc70c
feat: improve Festival Log UI with bookmark icons and diary layout
Copilot 42af7a3
Merge pull request #204 from richardthe3rd/copilot/sub-pr-200
richardthe3rd 20580ca
Initial plan
Copilot 9305945
fix: reposition tasted badge to bottom-right and hide want_to_try badge
Copilot f726d39
Merge pull request #205 from richardthe3rd/copilot/sub-pr-200
richardthe3rd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| /// Status values for favorite items in the festival log. | ||
| enum FavoriteStatus { | ||
| /// Drink is on the 'want to try' list. | ||
| wantToTry('want_to_try'), | ||
|
|
||
| /// Drink has been tasted at least once. | ||
| tasted('tasted'); | ||
|
|
||
| const FavoriteStatus(this.value); | ||
|
|
||
| /// The string value used for JSON serialization. | ||
| final String value; | ||
|
|
||
| /// Creates a FavoriteStatus from a string value. | ||
| static FavoriteStatus fromString(String value) { | ||
| return values.firstWhere( | ||
| (status) => status.value == value, | ||
| orElse: () => FavoriteStatus.wantToTry, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Represents a drink in the user's festival log. | ||
| /// | ||
| /// Tracks whether a drink is on the 'want to try' list or has been tasted, | ||
| /// along with timestamps of tastings and optional notes. | ||
| class FavoriteItem { | ||
| /// Creates a favorite item. | ||
| const FavoriteItem({ | ||
| required this.id, | ||
| required this.status, | ||
| required this.tries, | ||
| this.notes, | ||
| required this.createdAt, | ||
| required this.updatedAt, | ||
| }); | ||
|
|
||
| /// Drink ID. | ||
| final String id; | ||
|
|
||
| /// Current status of this drink in the festival log. | ||
| final FavoriteStatus status; | ||
|
|
||
| /// List of tasting timestamps (empty if want_to_try). | ||
| final List<DateTime> tries; | ||
|
|
||
| /// Optional user notes. | ||
| final String? notes; | ||
|
|
||
| /// When this item was added to the log. | ||
| final DateTime createdAt; | ||
|
|
||
| /// When this item was last updated. | ||
| final DateTime updatedAt; | ||
|
|
||
| /// Creates a FavoriteItem from JSON. | ||
| factory FavoriteItem.fromJson(Map<String, dynamic> json) { | ||
| return FavoriteItem( | ||
| id: json['id'] as String, | ||
| status: FavoriteStatus.fromString( | ||
| json['status'] as String? ?? 'want_to_try', | ||
| ), | ||
| tries: (json['tries'] as List?) | ||
| ?.map((e) => DateTime.parse(e as String)) | ||
| .toList() ?? | ||
| [], | ||
| notes: json['notes'] as String?, | ||
| createdAt: DateTime.parse(json['createdAt'] as String), | ||
| updatedAt: DateTime.parse(json['updatedAt'] as String), | ||
| ); | ||
| } | ||
|
|
||
| /// Converts this item to JSON. | ||
| Map<String, dynamic> toJson() { | ||
| return { | ||
| 'id': id, | ||
| 'status': status.value, | ||
| 'tries': tries.map((t) => t.toIso8601String()).toList(), | ||
| if (notes != null) 'notes': notes, | ||
| 'createdAt': createdAt.toIso8601String(), | ||
| 'updatedAt': updatedAt.toIso8601String(), | ||
| }; | ||
| } | ||
|
|
||
| /// Creates a copy with updated fields. | ||
| /// | ||
| /// To explicitly clear notes, pass an empty Optional: `notes: Optional.value(null)`. | ||
| /// To keep existing notes, omit the parameter: `copyWith(status: FavoriteStatus.tasted)`. | ||
| FavoriteItem copyWith({ | ||
| String? id, | ||
| FavoriteStatus? status, | ||
| List<DateTime>? tries, | ||
| Optional<String?>? notes, | ||
| DateTime? createdAt, | ||
| DateTime? updatedAt, | ||
| }) { | ||
| return FavoriteItem( | ||
| id: id ?? this.id, | ||
| status: status ?? this.status, | ||
| tries: tries ?? this.tries, | ||
| notes: notes != null ? notes.value : this.notes, | ||
| createdAt: createdAt ?? this.createdAt, | ||
| updatedAt: updatedAt ?? this.updatedAt, | ||
| ); | ||
| } | ||
|
|
||
| /// Equality comparison based on drink ID only. | ||
| /// | ||
| /// Two FavoriteItems are considered equal if they have the same id, | ||
| /// regardless of status, tries, notes, or timestamps. This design | ||
| /// allows FavoriteItem to be used in Sets and as Map keys where | ||
| /// uniqueness is determined by the drink being tracked, not its | ||
| /// specific state. | ||
| @override | ||
| bool operator ==(Object other) => | ||
| identical(this, other) || | ||
| other is FavoriteItem && | ||
| runtimeType == other.runtimeType && | ||
| id == other.id; | ||
|
|
||
| @override | ||
| int get hashCode => id.hashCode; | ||
| } | ||
|
|
||
| /// Wrapper class for explicitly passing null values in copyWith methods. | ||
| /// | ||
| /// Used to distinguish between omitting a parameter (keep existing value) | ||
| /// and explicitly passing null (clear the value). This is particularly | ||
| /// useful for optional fields like notes where both "no change" and | ||
| /// "set to null" are valid operations. | ||
| /// | ||
| /// Example usage: | ||
| /// ```dart | ||
| /// // Keep existing notes | ||
| /// item.copyWith(status: FavoriteStatus.tasted); | ||
| /// | ||
| /// // Clear notes (set to null) | ||
| /// item.copyWith(notes: Optional.value(null)); | ||
| /// | ||
| /// // Set new notes value | ||
| /// item.copyWith(notes: Optional.value('Great beer!')); | ||
| /// ``` | ||
| class Optional<T> { | ||
| const Optional.value(this.value); | ||
|
|
||
| final T value; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export 'drink.dart'; | ||
| export 'favorite_item.dart'; | ||
| export 'festival.dart'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The equality operator only compares the id field, ignoring all other fields (status, tries, notes, createdAt, updatedAt). This means two FavoriteItem instances with the same id but completely different data are considered equal. This could lead to unexpected behavior when using FavoriteItem in collections like Sets or as Map keys. Consider whether this is the intended behavior, or if equality should compare all fields. If id-only equality is intentional, add a comment explaining this design decision.