From 30e52a6eeea4e2391ef032d51860ccc8fb4602be Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 11:17:49 +0100 Subject: [PATCH 01/10] chore: Update api base url --- lib/features/todo/data/datasources/todo_remote_datasource.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index 8c3ef0b..d8c7252 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -10,8 +10,7 @@ import '../models/todo_dto.dart'; @injectable class TodoRemoteDataSource { final http.Client _client; - // TODO: Replace this with the URL we provide you - final String baseUrl = 'https://6825aa0f0f0188d7e72ddd9a.mockapi.io/api/v1'; + final String baseUrl = 'https://68346e5b464b49963602ccb5.mockapi.io'; TodoRemoteDataSource(this._client); From a89c9052e69081318381f963988b436768163960 Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 13:27:38 +0100 Subject: [PATCH 02/10] chore: Add description and imageUrl fields to TodoDTO and TodoModel --- lib/features/todo/data/models/todo_dto.dart | 2 + .../todo/data/models/todo_dto.freezed.dart | 69 ++++++++++++++++-- lib/features/todo/data/models/todo_dto.g.dart | 4 ++ .../data/repositories/todo_repository.dart | 8 ++- .../todo/domain/models/todo_model.dart | 10 ++- .../domain/models/todo_model.freezed.dart | 70 +++++++++++++++++-- .../todo/domain/models/todo_model.g.dart | 4 ++ 7 files changed, 154 insertions(+), 13 deletions(-) diff --git a/lib/features/todo/data/models/todo_dto.dart b/lib/features/todo/data/models/todo_dto.dart index 8d9d649..7a89933 100644 --- a/lib/features/todo/data/models/todo_dto.dart +++ b/lib/features/todo/data/models/todo_dto.dart @@ -10,6 +10,8 @@ class TodoDTO with _$TodoDTO { const factory TodoDTO({ required String id, required String title, + String? description, + String? imageUrl, int? createdAtSeconds, }) = _TodoDTO; diff --git a/lib/features/todo/data/models/todo_dto.freezed.dart b/lib/features/todo/data/models/todo_dto.freezed.dart index 752b03e..d235abf 100644 --- a/lib/features/todo/data/models/todo_dto.freezed.dart +++ b/lib/features/todo/data/models/todo_dto.freezed.dart @@ -23,6 +23,8 @@ TodoDTO _$TodoDTOFromJson(Map json) { mixin _$TodoDTO { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get imageUrl => throw _privateConstructorUsedError; int? get createdAtSeconds => throw _privateConstructorUsedError; /// Serializes this TodoDTO to a JSON map. @@ -39,7 +41,13 @@ abstract class $TodoDTOCopyWith<$Res> { factory $TodoDTOCopyWith(TodoDTO value, $Res Function(TodoDTO) then) = _$TodoDTOCopyWithImpl<$Res, TodoDTO>; @useResult - $Res call({String id, String title, int? createdAtSeconds}); + $Res call({ + String id, + String title, + String? description, + String? imageUrl, + int? createdAtSeconds, + }); } /// @nodoc @@ -59,6 +67,8 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> $Res call({ Object? id = null, Object? title = null, + Object? description = freezed, + Object? imageUrl = freezed, Object? createdAtSeconds = freezed, }) { return _then( @@ -73,6 +83,16 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, createdAtSeconds: freezed == createdAtSeconds ? _value.createdAtSeconds @@ -92,7 +112,13 @@ abstract class _$$TodoDTOImplCopyWith<$Res> implements $TodoDTOCopyWith<$Res> { ) = __$$TodoDTOImplCopyWithImpl<$Res>; @override @useResult - $Res call({String id, String title, int? createdAtSeconds}); + $Res call({ + String id, + String title, + String? description, + String? imageUrl, + int? createdAtSeconds, + }); } /// @nodoc @@ -111,6 +137,8 @@ class __$$TodoDTOImplCopyWithImpl<$Res> $Res call({ Object? id = null, Object? title = null, + Object? description = freezed, + Object? imageUrl = freezed, Object? createdAtSeconds = freezed, }) { return _then( @@ -125,6 +153,16 @@ class __$$TodoDTOImplCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, createdAtSeconds: freezed == createdAtSeconds ? _value.createdAtSeconds @@ -141,6 +179,8 @@ class _$TodoDTOImpl extends _TodoDTO { const _$TodoDTOImpl({ required this.id, required this.title, + this.description, + this.imageUrl, this.createdAtSeconds, }) : super._(); @@ -152,11 +192,15 @@ class _$TodoDTOImpl extends _TodoDTO { @override final String title; @override + final String? description; + @override + final String? imageUrl; + @override final int? createdAtSeconds; @override String toString() { - return 'TodoDTO(id: $id, title: $title, createdAtSeconds: $createdAtSeconds)'; + return 'TodoDTO(id: $id, title: $title, description: $description, imageUrl: $imageUrl, createdAtSeconds: $createdAtSeconds)'; } @override @@ -166,13 +210,24 @@ class _$TodoDTOImpl extends _TodoDTO { other is _$TodoDTOImpl && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && + (identical(other.description, description) || + other.description == description) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl) && (identical(other.createdAtSeconds, createdAtSeconds) || other.createdAtSeconds == createdAtSeconds)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, title, createdAtSeconds); + int get hashCode => Object.hash( + runtimeType, + id, + title, + description, + imageUrl, + createdAtSeconds, + ); /// Create a copy of TodoDTO /// with the given fields replaced by the non-null parameter values. @@ -192,6 +247,8 @@ abstract class _TodoDTO extends TodoDTO { const factory _TodoDTO({ required final String id, required final String title, + final String? description, + final String? imageUrl, final int? createdAtSeconds, }) = _$TodoDTOImpl; const _TodoDTO._() : super._(); @@ -203,6 +260,10 @@ abstract class _TodoDTO extends TodoDTO { @override String get title; @override + String? get description; + @override + String? get imageUrl; + @override int? get createdAtSeconds; /// Create a copy of TodoDTO diff --git a/lib/features/todo/data/models/todo_dto.g.dart b/lib/features/todo/data/models/todo_dto.g.dart index bada3c6..b04069b 100644 --- a/lib/features/todo/data/models/todo_dto.g.dart +++ b/lib/features/todo/data/models/todo_dto.g.dart @@ -10,6 +10,8 @@ _$TodoDTOImpl _$$TodoDTOImplFromJson(Map json) => _$TodoDTOImpl( id: json['id'] as String, title: json['title'] as String, + description: json['description'] as String?, + imageUrl: json['imageUrl'] as String?, createdAtSeconds: (json['createdAtSeconds'] as num?)?.toInt(), ); @@ -17,5 +19,7 @@ Map _$$TodoDTOImplToJson(_$TodoDTOImpl instance) => { 'id': instance.id, 'title': instance.title, + 'description': instance.description, + 'imageUrl': instance.imageUrl, 'createdAtSeconds': instance.createdAtSeconds, }; diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index 49e63b0..df9bec1 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -51,7 +51,13 @@ class TodoRepository { createdAt = DateTime.fromMillisecondsSinceEpoch(dto.createdAtSeconds! * 1000); } - return TodoModel(id: dto.id, title: dto.title, createdAt: createdAt); + return TodoModel( + id: dto.id, + title: dto.title, + description: dto.description, + imageUrl: dto.imageUrl, + createdAt: createdAt, + ); } catch (e) { log('Error mapping DTO to model: $e'); return TodoModel(id: const Uuid().v4(), title: 'Unknown title', createdAt: DateTime.now()); diff --git a/lib/features/todo/domain/models/todo_model.dart b/lib/features/todo/domain/models/todo_model.dart index 606a711..6979b3e 100644 --- a/lib/features/todo/domain/models/todo_model.dart +++ b/lib/features/todo/domain/models/todo_model.dart @@ -2,14 +2,20 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:intl/intl.dart'; part 'todo_model.freezed.dart'; + part 'todo_model.g.dart'; @freezed class TodoModel with _$TodoModel { const TodoModel._(); - const factory TodoModel({required String id, required String title, DateTime? createdAt}) = - _TodoModel; + const factory TodoModel({ + required String id, + required String title, + String? description, + String? imageUrl, + DateTime? createdAt, + }) = _TodoModel; DateTime get effectiveCreatedAt => createdAt ?? DateTime.now(); diff --git a/lib/features/todo/domain/models/todo_model.freezed.dart b/lib/features/todo/domain/models/todo_model.freezed.dart index 6052b7a..359be31 100644 --- a/lib/features/todo/domain/models/todo_model.freezed.dart +++ b/lib/features/todo/domain/models/todo_model.freezed.dart @@ -23,6 +23,8 @@ TodoModel _$TodoModelFromJson(Map json) { mixin _$TodoModel { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get imageUrl => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; /// Serializes this TodoModel to a JSON map. @@ -40,7 +42,13 @@ abstract class $TodoModelCopyWith<$Res> { factory $TodoModelCopyWith(TodoModel value, $Res Function(TodoModel) then) = _$TodoModelCopyWithImpl<$Res, TodoModel>; @useResult - $Res call({String id, String title, DateTime? createdAt}); + $Res call({ + String id, + String title, + String? description, + String? imageUrl, + DateTime? createdAt, + }); } /// @nodoc @@ -60,6 +68,8 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> $Res call({ Object? id = null, Object? title = null, + Object? description = freezed, + Object? imageUrl = freezed, Object? createdAt = freezed, }) { return _then( @@ -74,6 +84,16 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -94,7 +114,13 @@ abstract class _$$TodoModelImplCopyWith<$Res> ) = __$$TodoModelImplCopyWithImpl<$Res>; @override @useResult - $Res call({String id, String title, DateTime? createdAt}); + $Res call({ + String id, + String title, + String? description, + String? imageUrl, + DateTime? createdAt, + }); } /// @nodoc @@ -113,6 +139,8 @@ class __$$TodoModelImplCopyWithImpl<$Res> $Res call({ Object? id = null, Object? title = null, + Object? description = freezed, + Object? imageUrl = freezed, Object? createdAt = freezed, }) { return _then( @@ -127,6 +155,16 @@ class __$$TodoModelImplCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -140,8 +178,13 @@ class __$$TodoModelImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$TodoModelImpl extends _TodoModel { - const _$TodoModelImpl({required this.id, required this.title, this.createdAt}) - : super._(); + const _$TodoModelImpl({ + required this.id, + required this.title, + this.description, + this.imageUrl, + this.createdAt, + }) : super._(); factory _$TodoModelImpl.fromJson(Map json) => _$$TodoModelImplFromJson(json); @@ -151,11 +194,15 @@ class _$TodoModelImpl extends _TodoModel { @override final String title; @override + final String? description; + @override + final String? imageUrl; + @override final DateTime? createdAt; @override String toString() { - return 'TodoModel(id: $id, title: $title, createdAt: $createdAt)'; + return 'TodoModel(id: $id, title: $title, description: $description, imageUrl: $imageUrl, createdAt: $createdAt)'; } @override @@ -165,13 +212,18 @@ class _$TodoModelImpl extends _TodoModel { other is _$TodoModelImpl && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && + (identical(other.description, description) || + other.description == description) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, title, createdAt); + int get hashCode => + Object.hash(runtimeType, id, title, description, imageUrl, createdAt); /// Create a copy of TodoModel /// with the given fields replaced by the non-null parameter values. @@ -191,6 +243,8 @@ abstract class _TodoModel extends TodoModel { const factory _TodoModel({ required final String id, required final String title, + final String? description, + final String? imageUrl, final DateTime? createdAt, }) = _$TodoModelImpl; const _TodoModel._() : super._(); @@ -203,6 +257,10 @@ abstract class _TodoModel extends TodoModel { @override String get title; @override + String? get description; + @override + String? get imageUrl; + @override DateTime? get createdAt; /// Create a copy of TodoModel diff --git a/lib/features/todo/domain/models/todo_model.g.dart b/lib/features/todo/domain/models/todo_model.g.dart index 6512f96..1af3b76 100644 --- a/lib/features/todo/domain/models/todo_model.g.dart +++ b/lib/features/todo/domain/models/todo_model.g.dart @@ -10,6 +10,8 @@ _$TodoModelImpl _$$TodoModelImplFromJson(Map json) => _$TodoModelImpl( id: json['id'] as String, title: json['title'] as String, + description: json['description'] as String?, + imageUrl: json['imageUrl'] as String?, createdAt: json['createdAt'] == null ? null @@ -20,5 +22,7 @@ Map _$$TodoModelImplToJson(_$TodoModelImpl instance) => { 'id': instance.id, 'title': instance.title, + 'description': instance.description, + 'imageUrl': instance.imageUrl, 'createdAt': instance.createdAt?.toIso8601String(), }; From 444cc2bbadf67726c628b0f6888340b82efdabbd Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 13:44:46 +0100 Subject: [PATCH 03/10] feat: Show image and description in Todo card --- assets/placeholder.webp | Bin 0 -> 7484 bytes .../todo/presentation/widgets/todo_item.dart | 42 +++++++++++++++--- pubspec.yaml | 2 + 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 assets/placeholder.webp diff --git a/assets/placeholder.webp b/assets/placeholder.webp new file mode 100644 index 0000000000000000000000000000000000000000..bec5be235c4fe15630e2268d4a8f478f0052c935 GIT binary patch literal 7484 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 Date: Tue, 27 May 2025 13:49:58 +0100 Subject: [PATCH 04/10] chore: Add isDone field to TodoDTO and TodoModel --- lib/features/todo/data/models/todo_dto.dart | 1 + .../todo/data/models/todo_dto.freezed.dart | 25 ++++++++++++- lib/features/todo/data/models/todo_dto.g.dart | 2 ++ .../data/repositories/todo_repository.dart | 1 + .../todo/domain/models/todo_model.dart | 1 + .../domain/models/todo_model.freezed.dart | 35 +++++++++++++++++-- .../todo/domain/models/todo_model.g.dart | 2 ++ 7 files changed, 63 insertions(+), 4 deletions(-) diff --git a/lib/features/todo/data/models/todo_dto.dart b/lib/features/todo/data/models/todo_dto.dart index 7a89933..4839b0a 100644 --- a/lib/features/todo/data/models/todo_dto.dart +++ b/lib/features/todo/data/models/todo_dto.dart @@ -12,6 +12,7 @@ class TodoDTO with _$TodoDTO { required String title, String? description, String? imageUrl, + bool? isDone, int? createdAtSeconds, }) = _TodoDTO; diff --git a/lib/features/todo/data/models/todo_dto.freezed.dart b/lib/features/todo/data/models/todo_dto.freezed.dart index d235abf..6aede62 100644 --- a/lib/features/todo/data/models/todo_dto.freezed.dart +++ b/lib/features/todo/data/models/todo_dto.freezed.dart @@ -25,6 +25,7 @@ mixin _$TodoDTO { String get title => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; String? get imageUrl => throw _privateConstructorUsedError; + bool? get isDone => throw _privateConstructorUsedError; int? get createdAtSeconds => throw _privateConstructorUsedError; /// Serializes this TodoDTO to a JSON map. @@ -46,6 +47,7 @@ abstract class $TodoDTOCopyWith<$Res> { String title, String? description, String? imageUrl, + bool? isDone, int? createdAtSeconds, }); } @@ -69,6 +71,7 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> Object? title = null, Object? description = freezed, Object? imageUrl = freezed, + Object? isDone = freezed, Object? createdAtSeconds = freezed, }) { return _then( @@ -93,6 +96,11 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?, + isDone: + freezed == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool?, createdAtSeconds: freezed == createdAtSeconds ? _value.createdAtSeconds @@ -117,6 +125,7 @@ abstract class _$$TodoDTOImplCopyWith<$Res> implements $TodoDTOCopyWith<$Res> { String title, String? description, String? imageUrl, + bool? isDone, int? createdAtSeconds, }); } @@ -139,6 +148,7 @@ class __$$TodoDTOImplCopyWithImpl<$Res> Object? title = null, Object? description = freezed, Object? imageUrl = freezed, + Object? isDone = freezed, Object? createdAtSeconds = freezed, }) { return _then( @@ -163,6 +173,11 @@ class __$$TodoDTOImplCopyWithImpl<$Res> ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?, + isDone: + freezed == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool?, createdAtSeconds: freezed == createdAtSeconds ? _value.createdAtSeconds @@ -181,6 +196,7 @@ class _$TodoDTOImpl extends _TodoDTO { required this.title, this.description, this.imageUrl, + this.isDone, this.createdAtSeconds, }) : super._(); @@ -196,11 +212,13 @@ class _$TodoDTOImpl extends _TodoDTO { @override final String? imageUrl; @override + final bool? isDone; + @override final int? createdAtSeconds; @override String toString() { - return 'TodoDTO(id: $id, title: $title, description: $description, imageUrl: $imageUrl, createdAtSeconds: $createdAtSeconds)'; + return 'TodoDTO(id: $id, title: $title, description: $description, imageUrl: $imageUrl, isDone: $isDone, createdAtSeconds: $createdAtSeconds)'; } @override @@ -214,6 +232,7 @@ class _$TodoDTOImpl extends _TodoDTO { other.description == description) && (identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl) && + (identical(other.isDone, isDone) || other.isDone == isDone) && (identical(other.createdAtSeconds, createdAtSeconds) || other.createdAtSeconds == createdAtSeconds)); } @@ -226,6 +245,7 @@ class _$TodoDTOImpl extends _TodoDTO { title, description, imageUrl, + isDone, createdAtSeconds, ); @@ -249,6 +269,7 @@ abstract class _TodoDTO extends TodoDTO { required final String title, final String? description, final String? imageUrl, + final bool? isDone, final int? createdAtSeconds, }) = _$TodoDTOImpl; const _TodoDTO._() : super._(); @@ -264,6 +285,8 @@ abstract class _TodoDTO extends TodoDTO { @override String? get imageUrl; @override + bool? get isDone; + @override int? get createdAtSeconds; /// Create a copy of TodoDTO diff --git a/lib/features/todo/data/models/todo_dto.g.dart b/lib/features/todo/data/models/todo_dto.g.dart index b04069b..d724020 100644 --- a/lib/features/todo/data/models/todo_dto.g.dart +++ b/lib/features/todo/data/models/todo_dto.g.dart @@ -12,6 +12,7 @@ _$TodoDTOImpl _$$TodoDTOImplFromJson(Map json) => title: json['title'] as String, description: json['description'] as String?, imageUrl: json['imageUrl'] as String?, + isDone: json['isDone'] as bool?, createdAtSeconds: (json['createdAtSeconds'] as num?)?.toInt(), ); @@ -21,5 +22,6 @@ Map _$$TodoDTOImplToJson(_$TodoDTOImpl instance) => 'title': instance.title, 'description': instance.description, 'imageUrl': instance.imageUrl, + 'isDone': instance.isDone, 'createdAtSeconds': instance.createdAtSeconds, }; diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index df9bec1..c3a430e 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -56,6 +56,7 @@ class TodoRepository { title: dto.title, description: dto.description, imageUrl: dto.imageUrl, + isDone: dto.isDone, createdAt: createdAt, ); } catch (e) { diff --git a/lib/features/todo/domain/models/todo_model.dart b/lib/features/todo/domain/models/todo_model.dart index 6979b3e..65ae1e5 100644 --- a/lib/features/todo/domain/models/todo_model.dart +++ b/lib/features/todo/domain/models/todo_model.dart @@ -14,6 +14,7 @@ class TodoModel with _$TodoModel { required String title, String? description, String? imageUrl, + bool? isDone, DateTime? createdAt, }) = _TodoModel; diff --git a/lib/features/todo/domain/models/todo_model.freezed.dart b/lib/features/todo/domain/models/todo_model.freezed.dart index 359be31..4db14a8 100644 --- a/lib/features/todo/domain/models/todo_model.freezed.dart +++ b/lib/features/todo/domain/models/todo_model.freezed.dart @@ -25,6 +25,7 @@ mixin _$TodoModel { String get title => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; String? get imageUrl => throw _privateConstructorUsedError; + bool? get isDone => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; /// Serializes this TodoModel to a JSON map. @@ -47,6 +48,7 @@ abstract class $TodoModelCopyWith<$Res> { String title, String? description, String? imageUrl, + bool? isDone, DateTime? createdAt, }); } @@ -70,6 +72,7 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> Object? title = null, Object? description = freezed, Object? imageUrl = freezed, + Object? isDone = freezed, Object? createdAt = freezed, }) { return _then( @@ -94,6 +97,11 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?, + isDone: + freezed == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool?, createdAt: freezed == createdAt ? _value.createdAt @@ -119,6 +127,7 @@ abstract class _$$TodoModelImplCopyWith<$Res> String title, String? description, String? imageUrl, + bool? isDone, DateTime? createdAt, }); } @@ -141,6 +150,7 @@ class __$$TodoModelImplCopyWithImpl<$Res> Object? title = null, Object? description = freezed, Object? imageUrl = freezed, + Object? isDone = freezed, Object? createdAt = freezed, }) { return _then( @@ -165,6 +175,11 @@ class __$$TodoModelImplCopyWithImpl<$Res> ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?, + isDone: + freezed == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool?, createdAt: freezed == createdAt ? _value.createdAt @@ -183,6 +198,7 @@ class _$TodoModelImpl extends _TodoModel { required this.title, this.description, this.imageUrl, + this.isDone, this.createdAt, }) : super._(); @@ -198,11 +214,13 @@ class _$TodoModelImpl extends _TodoModel { @override final String? imageUrl; @override + final bool? isDone; + @override final DateTime? createdAt; @override String toString() { - return 'TodoModel(id: $id, title: $title, description: $description, imageUrl: $imageUrl, createdAt: $createdAt)'; + return 'TodoModel(id: $id, title: $title, description: $description, imageUrl: $imageUrl, isDone: $isDone, createdAt: $createdAt)'; } @override @@ -216,14 +234,22 @@ class _$TodoModelImpl extends _TodoModel { other.description == description) && (identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl) && + (identical(other.isDone, isDone) || other.isDone == isDone) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, id, title, description, imageUrl, createdAt); + int get hashCode => Object.hash( + runtimeType, + id, + title, + description, + imageUrl, + isDone, + createdAt, + ); /// Create a copy of TodoModel /// with the given fields replaced by the non-null parameter values. @@ -245,6 +271,7 @@ abstract class _TodoModel extends TodoModel { required final String title, final String? description, final String? imageUrl, + final bool? isDone, final DateTime? createdAt, }) = _$TodoModelImpl; const _TodoModel._() : super._(); @@ -261,6 +288,8 @@ abstract class _TodoModel extends TodoModel { @override String? get imageUrl; @override + bool? get isDone; + @override DateTime? get createdAt; /// Create a copy of TodoModel diff --git a/lib/features/todo/domain/models/todo_model.g.dart b/lib/features/todo/domain/models/todo_model.g.dart index 1af3b76..b2a0162 100644 --- a/lib/features/todo/domain/models/todo_model.g.dart +++ b/lib/features/todo/domain/models/todo_model.g.dart @@ -12,6 +12,7 @@ _$TodoModelImpl _$$TodoModelImplFromJson(Map json) => title: json['title'] as String, description: json['description'] as String?, imageUrl: json['imageUrl'] as String?, + isDone: json['isDone'] as bool?, createdAt: json['createdAt'] == null ? null @@ -24,5 +25,6 @@ Map _$$TodoModelImplToJson(_$TodoModelImpl instance) => 'title': instance.title, 'description': instance.description, 'imageUrl': instance.imageUrl, + 'isDone': instance.isDone, 'createdAt': instance.createdAt?.toIso8601String(), }; From b7ee9de5dcf53da27509967524b284c9559a908d Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 12:30:33 +0100 Subject: [PATCH 05/10] feat: Add completion toggle to todo --- .../datasources/todo_remote_datasource.dart | 29 +++++++++++++++++++ .../data/repositories/todo_repository.dart | 10 +++++++ .../todo/presentation/cubits/todo_cubit.dart | 14 +++++++++ .../todo/presentation/widgets/todo_item.dart | 10 +++++++ 4 files changed, 63 insertions(+) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index d8c7252..ff2168a 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -60,6 +60,35 @@ class TodoRemoteDataSource { } } + Future updateTodoStatus(String id, bool isDone) async { + try { + final newTodo = {'isDone': isDone}; + + log('Updating todo status: ${json.encode(newTodo)}'); + + final response = await _client.patch( + Uri.parse('$baseUrl/todo/$id'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(newTodo), + ); + + log('Response status: ${response.statusCode}'); + log('Response body: ${response.body}'); + + if (response.statusCode == 201 || response.statusCode == 200) { + final Map responseData = json.decode(response.body); + + // If the API response is not in the expected format, transform it + return TodoDTO.fromJson(responseData); + } else { + throw Exception('Failed to update todo: ${response.statusCode}'); + } + } catch (e) { + log('Error updating todo: $e'); + throw Exception('Failed to update todo: $e'); + } + } + // For local client-side ID generation String generateId() { return const Uuid().v4(); diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index c3a430e..ce443ab 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -43,6 +43,16 @@ class TodoRepository { } } + Future updateTodoStatus(String id, bool isDone) async { + try { + // Update todo status remotely + return _mapDtoToModel(await remoteDataSource.updateTodoStatus(id, isDone)); + } catch (e) { + log('Error updating remote todo status: $e'); + return null; + } + } + // Helper method to map DTOs to domain models TodoModel _mapDtoToModel(TodoDTO dto) { try { diff --git a/lib/features/todo/presentation/cubits/todo_cubit.dart b/lib/features/todo/presentation/cubits/todo_cubit.dart index f1805f4..e1a4bb4 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -9,6 +9,8 @@ part 'todo_state.dart'; class TodoCubit extends Cubit { final TodoRepository _repository; + TodosLoaded get loaded => state as TodosLoaded; + TodoCubit(this._repository) : super(TodosLoading()); Future loadTodos() async { @@ -30,4 +32,16 @@ class TodoCubit extends Cubit { emit(TodosError(message: e.toString())); } } + + Future updateTodoStatus(String id, bool isDone) async { + try { + final updatedTodo = await _repository.updateTodoStatus(id, isDone); + + if (updatedTodo != null) { + emit(TodosLoaded(todos: loaded.todos.map((todo) => todo.id == id ? updatedTodo : todo).toList())); + } + } catch (e) { + emit(TodosError(message: e.toString())); + } + } } diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index c1de6b7..bfdf70c 100644 --- a/lib/features/todo/presentation/widgets/todo_item.dart +++ b/lib/features/todo/presentation/widgets/todo_item.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/models/todo_model.dart'; +import '../cubits/todo_cubit.dart'; class TodoItem extends StatelessWidget { final TodoModel todo; @@ -9,6 +11,8 @@ class TodoItem extends StatelessWidget { @override Widget build(BuildContext context) { + final todoCubit = context.read(); + return Card( elevation: 2, margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -51,6 +55,12 @@ class TodoItem extends StatelessWidget { ], ), ), + Checkbox( + value: todo.isDone, + onChanged: (value) { + todoCubit.updateTodoStatus(todo.id, value ?? false); + }, + ), ], ), ), From 472338defb60640b59e31673f77a463261439a76 Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 13:56:33 +0100 Subject: [PATCH 06/10] feat: Add description and imageUrl to add todo form --- .../datasources/todo_remote_datasource.dart | 9 ++- .../data/repositories/todo_repository.dart | 4 +- .../todo/presentation/cubits/todo_cubit.dart | 4 +- .../screens/todo_form_screen.dart | 67 ++++++++++++------- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index ff2168a..6ab4a83 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -31,9 +31,14 @@ class TodoRemoteDataSource { } } - Future addTodo(String title) async { + Future addTodo(String title, String? description, String? imageUrl) async { try { - final newTodo = {'title': title, 'createdAtSeconds': DateTime.now().millisecondsSinceEpoch ~/ 1000}; + final newTodo = { + 'title': title, + 'description': description, + 'imageUrl': imageUrl, + 'createdAtSeconds': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; log('Sending todo: ${json.encode(newTodo)}'); diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index ce443ab..6cee511 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -34,10 +34,10 @@ class TodoRepository { } // Add a new todo remotely - Future addTodo(String title) async { + Future addTodo(String title, String? description, String? imageUrl) async { try { // Add todo remotely - await remoteDataSource.addTodo(title); + await remoteDataSource.addTodo(title, description, imageUrl); } catch (e) { log('Error adding remote todo: $e'); } diff --git a/lib/features/todo/presentation/cubits/todo_cubit.dart b/lib/features/todo/presentation/cubits/todo_cubit.dart index e1a4bb4..48cc738 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -23,10 +23,10 @@ class TodoCubit extends Cubit { } } - Future addTodo(String title) async { + Future addTodo(String title, String? description, String? imageUrl) async { try { emit(TodosLoading()); - await _repository.addTodo(title); + await _repository.addTodo(title, description, imageUrl); await loadTodos(); } catch (e) { emit(TodosError(message: e.toString())); diff --git a/lib/features/todo/presentation/screens/todo_form_screen.dart b/lib/features/todo/presentation/screens/todo_form_screen.dart index cf54e93..cd4beaa 100644 --- a/lib/features/todo/presentation/screens/todo_form_screen.dart +++ b/lib/features/todo/presentation/screens/todo_form_screen.dart @@ -13,6 +13,8 @@ class TodoFormScreen extends StatefulWidget { class _TodoFormScreenState extends State { final _formKey = GlobalKey(); final _titleController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _imageUrlController = TextEditingController(); @override void dispose() { @@ -22,7 +24,7 @@ class _TodoFormScreenState extends State { void _submitForm() { if (_formKey.currentState?.validate() ?? false) { - context.read().addTodo(_titleController.text); + context.read().addTodo(_titleController.text, _descriptionController.text, _imageUrlController.text); Navigator.pop(context); } } @@ -31,31 +33,44 @@ class _TodoFormScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Add Todo')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 16), - TextFormField( - controller: _titleController, - decoration: const InputDecoration(labelText: 'Title', border: OutlineInputBorder()), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a title'; - } - return null; - }, - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: _submitForm, - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), - child: const Text('Add Todo'), - ), - ], + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + TextFormField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Title', border: OutlineInputBorder()), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + ), + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration(labelText: 'Description', border: OutlineInputBorder()), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 10, + ), + TextFormField( + controller: _imageUrlController, + decoration: const InputDecoration(labelText: 'Image url', border: OutlineInputBorder()), + ), + Expanded(child: const SizedBox()), + ElevatedButton( + onPressed: _submitForm, + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), + child: const Text('Add Todo'), + ), + ], + ), ), ), ), From 911a51ff259e8bc1b91ab6930264a7e8b145d420 Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 14:27:05 +0100 Subject: [PATCH 07/10] feat: Add todo detail screen --- .../screens/todo_detail_screen.dart | 70 +++++++++++++++++++ .../screens/todo_list_screen.dart | 15 +++- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lib/features/todo/presentation/screens/todo_detail_screen.dart diff --git a/lib/features/todo/presentation/screens/todo_detail_screen.dart b/lib/features/todo/presentation/screens/todo_detail_screen.dart new file mode 100644 index 0000000..ca2b379 --- /dev/null +++ b/lib/features/todo/presentation/screens/todo_detail_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_todo_workshop/features/todo/domain/models/todo_model.dart'; + +import '../cubits/todo_cubit.dart'; + +class TodoDetailScreen extends StatelessWidget { + final String todoId; + + const TodoDetailScreen({super.key, required this.todoId}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + TodoModel? todo; + try { + todo = state is TodosLoaded ? state.todos.firstWhere((todo) => todo.id == todoId) : null; + } catch (e) { + todo = null; + } + + if (todo != null) { + return Scaffold( + appBar: AppBar(title: const Text('Todo Detail')), + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: 200, + width: double.infinity, + child: + todo.imageUrl != null && todo.imageUrl!.isNotEmpty + ? Image.network(todo.imageUrl!, fit: BoxFit.cover) + : Image.asset('assets/placeholder.webp'), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + Text(todo.title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + todo.description != null && todo.description!.isNotEmpty + ? Text(todo.description!, style: const TextStyle(fontSize: 14)) + : const SizedBox(), + Expanded(child: const SizedBox()), + Text( + todo.formattedDate, + style: TextStyle(fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } else { + return Scaffold( + appBar: AppBar(title: const Text('Todo Detail')), + body: const Center(child: Text('Todo not found')), + ); + } + }, + ); + } +} diff --git a/lib/features/todo/presentation/screens/todo_list_screen.dart b/lib/features/todo/presentation/screens/todo_list_screen.dart index a366ba1..ccd2bb7 100644 --- a/lib/features/todo/presentation/screens/todo_list_screen.dart +++ b/lib/features/todo/presentation/screens/todo_list_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../cubits/todo_cubit.dart'; import '../widgets/todo_item.dart'; +import 'todo_detail_screen.dart'; import 'todo_form_screen.dart'; class TodoListScreen extends StatelessWidget { @@ -35,7 +36,19 @@ class TodoListScreen extends StatelessWidget { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { - return TodoItem(todo: todos[index]); + return GestureDetector( + child: TodoItem(todo: todos[index]), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => + BlocProvider.value(value: todoCubit, child: TodoDetailScreen(todoId: todos[index].id)), + ), + ); + }, + ); }, ); } else if (state is TodosError) { From 533be25bdfe491f7bf25a8b5481f05b05ed6050a Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 15:28:52 +0100 Subject: [PATCH 08/10] chore: Add isFavorite to TodoModel --- lib/core/di/injection.config.dart | 9 ++++--- .../datasources/todo_local_datasource.dart | 20 +++++++++++--- .../data/repositories/todo_repository.dart | 17 ++++++++---- .../todo/domain/models/todo_model.dart | 1 + .../domain/models/todo_model.freezed.dart | 26 ++++++++++++++++++- .../todo/domain/models/todo_model.g.dart | 2 ++ 6 files changed, 63 insertions(+), 12 deletions(-) diff --git a/lib/core/di/injection.config.dart b/lib/core/di/injection.config.dart index 2f12433..b930b7d 100644 --- a/lib/core/di/injection.config.dart +++ b/lib/core/di/injection.config.dart @@ -37,12 +37,15 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i484.TodoRemoteDataSource>( () => _i484.TodoRemoteDataSource(gh<_i519.Client>()), ); - gh.factory<_i131.TodoRepository>( - () => _i131.TodoRepository(gh<_i484.TodoRemoteDataSource>()), - ); gh.factory<_i137.TodoLocalDataSource>( () => _i137.TodoLocalDataSource(gh<_i460.SharedPreferences>()), ); + gh.factory<_i131.TodoRepository>( + () => _i131.TodoRepository( + gh<_i484.TodoRemoteDataSource>(), + gh<_i137.TodoLocalDataSource>(), + ), + ); return this; } } diff --git a/lib/features/todo/data/datasources/todo_local_datasource.dart b/lib/features/todo/data/datasources/todo_local_datasource.dart index 78a80e3..09ea009 100644 --- a/lib/features/todo/data/datasources/todo_local_datasource.dart +++ b/lib/features/todo/data/datasources/todo_local_datasource.dart @@ -1,12 +1,26 @@ import 'package:injectable/injectable.dart'; import 'package:shared_preferences/shared_preferences.dart'; -// This class is a stub for workshop participants to implement @injectable class TodoLocalDataSource { final SharedPreferences sharedPreferences; - // ignore: unused_field - final String _todoKey = 'todos'; + final String _todoFavouriteKey = 'favourite_todos'; TodoLocalDataSource(this.sharedPreferences); + + Future> getFavouriteTodos() async { + try { + return sharedPreferences.getStringList(_todoFavouriteKey) ?? []; + } catch (e) { + return []; + } + } + + Future setFavouriteTodos(List todoIds) async { + try { + await sharedPreferences.setStringList(_todoFavouriteKey, todoIds); + } catch (e) { + return; + } + } } diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index 6cee511..135f942 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -4,23 +4,26 @@ import 'package:injectable/injectable.dart'; import 'package:uuid/uuid.dart'; import '../../domain/models/todo_model.dart'; +import '../datasources/todo_local_datasource.dart'; import '../datasources/todo_remote_datasource.dart'; import '../models/todo_dto.dart'; @injectable class TodoRepository { final TodoRemoteDataSource remoteDataSource; + final TodoLocalDataSource localDataSource; - TodoRepository(this.remoteDataSource); + TodoRepository(this.remoteDataSource, this.localDataSource); // Get todos from remote data source only Future> getTodos() async { try { // Get todos from the API final remoteDtos = await remoteDataSource.getTodos(); + final favouriteIds = await localDataSource.getFavouriteTodos(); // Map DTOs to domain models - final todos = remoteDtos.map(_mapDtoToModel).toList(); + final todos = remoteDtos.map((dto) => _mapDtoToModel(dto, favouriteIds)).toList(); // Sort todos by creation date (newest first) todos.sort((a, b) => b.effectiveCreatedAt.compareTo(a.effectiveCreatedAt)); @@ -46,7 +49,9 @@ class TodoRepository { Future updateTodoStatus(String id, bool isDone) async { try { // Update todo status remotely - return _mapDtoToModel(await remoteDataSource.updateTodoStatus(id, isDone)); + final favouriteIds = await localDataSource.getFavouriteTodos(); + + return _mapDtoToModel(await remoteDataSource.updateTodoStatus(id, isDone), favouriteIds); } catch (e) { log('Error updating remote todo status: $e'); return null; @@ -54,12 +59,13 @@ class TodoRepository { } // Helper method to map DTOs to domain models - TodoModel _mapDtoToModel(TodoDTO dto) { + TodoModel _mapDtoToModel(TodoDTO dto, List favouriteIds) { try { DateTime? createdAt; if (dto.createdAtSeconds != null) { createdAt = DateTime.fromMillisecondsSinceEpoch(dto.createdAtSeconds! * 1000); } + final isFavourite = favouriteIds.contains(dto.id); return TodoModel( id: dto.id, @@ -67,11 +73,12 @@ class TodoRepository { description: dto.description, imageUrl: dto.imageUrl, isDone: dto.isDone, + isFavourite: isFavourite, createdAt: createdAt, ); } catch (e) { log('Error mapping DTO to model: $e'); - return TodoModel(id: const Uuid().v4(), title: 'Unknown title', createdAt: DateTime.now()); + return TodoModel(id: const Uuid().v4(), title: 'Unknown title', isFavourite: false, createdAt: DateTime.now()); } } } diff --git a/lib/features/todo/domain/models/todo_model.dart b/lib/features/todo/domain/models/todo_model.dart index 65ae1e5..7c72c1f 100644 --- a/lib/features/todo/domain/models/todo_model.dart +++ b/lib/features/todo/domain/models/todo_model.dart @@ -12,6 +12,7 @@ class TodoModel with _$TodoModel { const factory TodoModel({ required String id, required String title, + required bool isFavourite, String? description, String? imageUrl, bool? isDone, diff --git a/lib/features/todo/domain/models/todo_model.freezed.dart b/lib/features/todo/domain/models/todo_model.freezed.dart index 4db14a8..c6c2ae1 100644 --- a/lib/features/todo/domain/models/todo_model.freezed.dart +++ b/lib/features/todo/domain/models/todo_model.freezed.dart @@ -23,6 +23,7 @@ TodoModel _$TodoModelFromJson(Map json) { mixin _$TodoModel { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; + bool get isFavourite => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; String? get imageUrl => throw _privateConstructorUsedError; bool? get isDone => throw _privateConstructorUsedError; @@ -46,6 +47,7 @@ abstract class $TodoModelCopyWith<$Res> { $Res call({ String id, String title, + bool isFavourite, String? description, String? imageUrl, bool? isDone, @@ -70,6 +72,7 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> $Res call({ Object? id = null, Object? title = null, + Object? isFavourite = null, Object? description = freezed, Object? imageUrl = freezed, Object? isDone = freezed, @@ -87,6 +90,11 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + isFavourite: + null == isFavourite + ? _value.isFavourite + : isFavourite // ignore: cast_nullable_to_non_nullable + as bool, description: freezed == description ? _value.description @@ -125,6 +133,7 @@ abstract class _$$TodoModelImplCopyWith<$Res> $Res call({ String id, String title, + bool isFavourite, String? description, String? imageUrl, bool? isDone, @@ -148,6 +157,7 @@ class __$$TodoModelImplCopyWithImpl<$Res> $Res call({ Object? id = null, Object? title = null, + Object? isFavourite = null, Object? description = freezed, Object? imageUrl = freezed, Object? isDone = freezed, @@ -165,6 +175,11 @@ class __$$TodoModelImplCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + isFavourite: + null == isFavourite + ? _value.isFavourite + : isFavourite // ignore: cast_nullable_to_non_nullable + as bool, description: freezed == description ? _value.description @@ -196,6 +211,7 @@ class _$TodoModelImpl extends _TodoModel { const _$TodoModelImpl({ required this.id, required this.title, + required this.isFavourite, this.description, this.imageUrl, this.isDone, @@ -210,6 +226,8 @@ class _$TodoModelImpl extends _TodoModel { @override final String title; @override + final bool isFavourite; + @override final String? description; @override final String? imageUrl; @@ -220,7 +238,7 @@ class _$TodoModelImpl extends _TodoModel { @override String toString() { - return 'TodoModel(id: $id, title: $title, description: $description, imageUrl: $imageUrl, isDone: $isDone, createdAt: $createdAt)'; + return 'TodoModel(id: $id, title: $title, isFavourite: $isFavourite, description: $description, imageUrl: $imageUrl, isDone: $isDone, createdAt: $createdAt)'; } @override @@ -230,6 +248,8 @@ class _$TodoModelImpl extends _TodoModel { other is _$TodoModelImpl && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && + (identical(other.isFavourite, isFavourite) || + other.isFavourite == isFavourite) && (identical(other.description, description) || other.description == description) && (identical(other.imageUrl, imageUrl) || @@ -245,6 +265,7 @@ class _$TodoModelImpl extends _TodoModel { runtimeType, id, title, + isFavourite, description, imageUrl, isDone, @@ -269,6 +290,7 @@ abstract class _TodoModel extends TodoModel { const factory _TodoModel({ required final String id, required final String title, + required final bool isFavourite, final String? description, final String? imageUrl, final bool? isDone, @@ -284,6 +306,8 @@ abstract class _TodoModel extends TodoModel { @override String get title; @override + bool get isFavourite; + @override String? get description; @override String? get imageUrl; diff --git a/lib/features/todo/domain/models/todo_model.g.dart b/lib/features/todo/domain/models/todo_model.g.dart index b2a0162..64ca82e 100644 --- a/lib/features/todo/domain/models/todo_model.g.dart +++ b/lib/features/todo/domain/models/todo_model.g.dart @@ -10,6 +10,7 @@ _$TodoModelImpl _$$TodoModelImplFromJson(Map json) => _$TodoModelImpl( id: json['id'] as String, title: json['title'] as String, + isFavourite: json['isFavourite'] as bool, description: json['description'] as String?, imageUrl: json['imageUrl'] as String?, isDone: json['isDone'] as bool?, @@ -23,6 +24,7 @@ Map _$$TodoModelImplToJson(_$TodoModelImpl instance) => { 'id': instance.id, 'title': instance.title, + 'isFavourite': instance.isFavourite, 'description': instance.description, 'imageUrl': instance.imageUrl, 'isDone': instance.isDone, From 3c003cfb1d22a93d95d17c7e623e365ecd8f08ac Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 16:31:15 +0100 Subject: [PATCH 09/10] feat: Add favourite toggle to todo detail screen --- .../data/repositories/todo_repository.dart | 21 +++++++++++++++++++ .../todo/presentation/cubits/todo_cubit.dart | 16 ++++++++++++++ .../screens/todo_detail_screen.dart | 16 +++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index 135f942..a8c7fa4 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -58,6 +58,27 @@ class TodoRepository { } } + Future toggleTodoFavourite(String id) async { + try { + final favouriteIds = await localDataSource.getFavouriteTodos(); + + final isFavourite = favouriteIds.contains(id); + + if (isFavourite) { + favouriteIds.remove(id); + } else { + favouriteIds.add(id); + } + + await localDataSource.setFavouriteTodos(favouriteIds); + + return !isFavourite; + } catch (e) { + log('Error toggling todo favourite: $e'); + return null; + } + } + // Helper method to map DTOs to domain models TodoModel _mapDtoToModel(TodoDTO dto, List favouriteIds) { try { diff --git a/lib/features/todo/presentation/cubits/todo_cubit.dart b/lib/features/todo/presentation/cubits/todo_cubit.dart index 48cc738..2e8fbde 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -44,4 +44,20 @@ class TodoCubit extends Cubit { emit(TodosError(message: e.toString())); } } + + Future toggleTodoFavourite(String id) async { + try { + TodoModel? currentTodo = loaded.todos.firstWhere((todo) => todo.id == id); + + final updatedFavorite = await _repository.toggleTodoFavourite(id); + + if (updatedFavorite == null) return; + + TodoModel updatedTodo = currentTodo.copyWith(isFavourite: updatedFavorite); + + emit(TodosLoaded(todos: loaded.todos.map((todo) => todo.id == id ? updatedTodo : todo).toList())); + } catch (e) { + emit(TodosError(message: e.toString())); + } + } } diff --git a/lib/features/todo/presentation/screens/todo_detail_screen.dart b/lib/features/todo/presentation/screens/todo_detail_screen.dart index ca2b379..ee205d1 100644 --- a/lib/features/todo/presentation/screens/todo_detail_screen.dart +++ b/lib/features/todo/presentation/screens/todo_detail_screen.dart @@ -11,6 +11,8 @@ class TodoDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final todoCubit = context.read(); + return BlocBuilder( builder: (context, state) { TodoModel? todo; @@ -22,7 +24,19 @@ class TodoDetailScreen extends StatelessWidget { if (todo != null) { return Scaffold( - appBar: AppBar(title: const Text('Todo Detail')), + appBar: AppBar( + title: Text("Detail"), + actions: [ + IconButton( + icon: Icon( + todo.isFavourite ? Icons.star : Icons.star_border, + color: todo.isFavourite ? Colors.amber : null, + ), + onPressed: () => todoCubit.toggleTodoFavourite(todo!.id), + tooltip: 'Favorite', + ), + ], + ), body: SafeArea( child: Column( children: [ From c399f83af0d1ba172ff21286f26cf12739e43965 Mon Sep 17 00:00:00 2001 From: Paulo Bernardes Date: Tue, 27 May 2025 16:49:49 +0100 Subject: [PATCH 10/10] feat: Add delete button to todo detail screen --- .../data/datasources/todo_remote_datasource.dart | 16 ++++++++++++++++ .../todo/data/repositories/todo_repository.dart | 9 +++++++++ .../todo/presentation/cubits/todo_cubit.dart | 10 ++++++++++ .../presentation/screens/todo_detail_screen.dart | 8 ++++++++ 4 files changed, 43 insertions(+) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index 6ab4a83..62333f0 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -65,6 +65,22 @@ class TodoRemoteDataSource { } } + Future deleteTodo(String id) async { + try { + final response = await _client.delete(Uri.parse('$baseUrl/todo/$id')); + + log('Response status: ${response.statusCode}'); + log('Response body: ${response.body}'); + + if (response.statusCode != 200) { + throw Exception('Failed to delete todo: ${response.statusCode}'); + } + } catch (e) { + log('Error deleting todo: $e'); + throw Exception('Failed to delete todo: $e'); + } + } + Future updateTodoStatus(String id, bool isDone) async { try { final newTodo = {'isDone': isDone}; diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index a8c7fa4..96a7e01 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -46,6 +46,15 @@ class TodoRepository { } } + Future deleteTodo(String id) async { + try { + // Delete todo remotely + await remoteDataSource.deleteTodo(id); + } catch (e) { + log('Error deleting remote todo: $e'); + } + } + Future updateTodoStatus(String id, bool isDone) async { try { // Update todo status remotely diff --git a/lib/features/todo/presentation/cubits/todo_cubit.dart b/lib/features/todo/presentation/cubits/todo_cubit.dart index 2e8fbde..069cecc 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -60,4 +60,14 @@ class TodoCubit extends Cubit { emit(TodosError(message: e.toString())); } } + + Future deleteTodo(String id) async { + try { + emit(TodosLoading()); + await _repository.deleteTodo(id); + await loadTodos(); + } catch (e) { + emit(TodosError(message: e.toString())); + } + } } diff --git a/lib/features/todo/presentation/screens/todo_detail_screen.dart b/lib/features/todo/presentation/screens/todo_detail_screen.dart index ee205d1..8d24ee6 100644 --- a/lib/features/todo/presentation/screens/todo_detail_screen.dart +++ b/lib/features/todo/presentation/screens/todo_detail_screen.dart @@ -64,6 +64,14 @@ class TodoDetailScreen extends StatelessWidget { todo.formattedDate, style: TextStyle(fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic), ), + ElevatedButton( + onPressed: () { + todoCubit.deleteTodo(todo!.id); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), + child: const Text('Delete', style: TextStyle(color: Colors.redAccent)), + ), ], ), ),