From ae019c9e3990a07b65a7db7103ab16d1cb6bedb4 Mon Sep 17 00:00:00 2001 From: Jakub Ostwald Date: Tue, 27 May 2025 13:47:37 +0200 Subject: [PATCH 1/5] extend DTOs and internal data structures with new fields together with new fields in task creation widget --- .../datasources/todo_remote_datasource.dart | 19 ++-- lib/features/todo/data/models/todo_dto.dart | 3 + .../todo/data/models/todo_dto.freezed.dart | 94 +++++++++++++++- lib/features/todo/data/models/todo_dto.g.dart | 6 ++ .../data/repositories/todo_repository.dart | 8 +- .../todo/domain/models/todo_model.dart | 4 +- .../domain/models/todo_model.freezed.dart | 101 ++++++++++++++++-- .../todo/domain/models/todo_model.g.dart | 6 ++ .../todo/presentation/cubits/todo_cubit.dart | 4 +- .../screens/todo_form_screen.dart | 47 +++++++- 10 files changed, 264 insertions(+), 28 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..f866045 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -4,14 +4,14 @@ import 'dart:developer'; import 'package:http/http.dart' as http; import 'package:injectable/injectable.dart'; import 'package:uuid/uuid.dart'; +import 'package:uuid/v4.dart'; 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://6835745dcd78db2058c1928b.mockapi.io'; TodoRemoteDataSource(this._client); @@ -24,7 +24,7 @@ class TodoRemoteDataSource { log('API Response: ${response.body}'); return jsonList.map((json) => TodoDTO.fromJson(json)).toList(); } else { - throw Exception('Failed to load todos: ${response.statusCode}'); + throw Exception('Failed to load todos: ${response.statusCode} \n ${response.body}'); } } catch (e) { log('Error fetching todos: $e'); @@ -32,9 +32,16 @@ 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 = { + 'id': generateId(), + 'title': title, + 'createdAtSeconds': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'isDone': false, + 'imageUrl': imageUrl, + 'description': description, + }; log('Sending todo: ${json.encode(newTodo)}'); @@ -53,7 +60,7 @@ class TodoRemoteDataSource { // If the API response is not in the expected format, transform it return TodoDTO.fromJson(responseData); } else { - throw Exception('Failed to create todo: ${response.statusCode}'); + throw Exception('Failed to create todo: ${response.statusCode} \n ${response.body}'); } } catch (e) { log('Error adding todo: $e'); diff --git a/lib/features/todo/data/models/todo_dto.dart b/lib/features/todo/data/models/todo_dto.dart index 8d9d649..f0c8743 100644 --- a/lib/features/todo/data/models/todo_dto.dart +++ b/lib/features/todo/data/models/todo_dto.dart @@ -11,6 +11,9 @@ class TodoDTO with _$TodoDTO { required String id, required String title, int? createdAtSeconds, + required bool isDone, + String? description, + String? imageUrl, }) = _TodoDTO; factory TodoDTO.fromJson(Map json) => _$TodoDTOFromJson(json); diff --git a/lib/features/todo/data/models/todo_dto.freezed.dart b/lib/features/todo/data/models/todo_dto.freezed.dart index 752b03e..a4bf105 100644 --- a/lib/features/todo/data/models/todo_dto.freezed.dart +++ b/lib/features/todo/data/models/todo_dto.freezed.dart @@ -24,6 +24,9 @@ mixin _$TodoDTO { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; int? get createdAtSeconds => throw _privateConstructorUsedError; + bool get isDone => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get imageUrl => throw _privateConstructorUsedError; /// Serializes this TodoDTO to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -39,7 +42,14 @@ 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, + int? createdAtSeconds, + bool isDone, + String? description, + String? imageUrl, + }); } /// @nodoc @@ -60,6 +70,9 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> Object? id = null, Object? title = null, Object? createdAtSeconds = freezed, + Object? isDone = null, + Object? description = freezed, + Object? imageUrl = freezed, }) { return _then( _value.copyWith( @@ -78,6 +91,21 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> ? _value.createdAtSeconds : createdAtSeconds // ignore: cast_nullable_to_non_nullable as int?, + isDone: + null == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool, + 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?, ) as $Val, ); @@ -92,7 +120,14 @@ 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, + int? createdAtSeconds, + bool isDone, + String? description, + String? imageUrl, + }); } /// @nodoc @@ -112,6 +147,9 @@ class __$$TodoDTOImplCopyWithImpl<$Res> Object? id = null, Object? title = null, Object? createdAtSeconds = freezed, + Object? isDone = null, + Object? description = freezed, + Object? imageUrl = freezed, }) { return _then( _$TodoDTOImpl( @@ -130,6 +168,21 @@ class __$$TodoDTOImplCopyWithImpl<$Res> ? _value.createdAtSeconds : createdAtSeconds // ignore: cast_nullable_to_non_nullable as int?, + isDone: + null == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool, + 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?, ), ); } @@ -142,6 +195,9 @@ class _$TodoDTOImpl extends _TodoDTO { required this.id, required this.title, this.createdAtSeconds, + required this.isDone, + this.description, + this.imageUrl, }) : super._(); factory _$TodoDTOImpl.fromJson(Map json) => @@ -153,10 +209,16 @@ class _$TodoDTOImpl extends _TodoDTO { final String title; @override final int? createdAtSeconds; + @override + final bool isDone; + @override + final String? description; + @override + final String? imageUrl; @override String toString() { - return 'TodoDTO(id: $id, title: $title, createdAtSeconds: $createdAtSeconds)'; + return 'TodoDTO(id: $id, title: $title, createdAtSeconds: $createdAtSeconds, isDone: $isDone, description: $description, imageUrl: $imageUrl)'; } @override @@ -167,12 +229,25 @@ class _$TodoDTOImpl extends _TodoDTO { (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && (identical(other.createdAtSeconds, createdAtSeconds) || - other.createdAtSeconds == createdAtSeconds)); + other.createdAtSeconds == createdAtSeconds) && + (identical(other.isDone, isDone) || other.isDone == isDone) && + (identical(other.description, description) || + other.description == description) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, title, createdAtSeconds); + int get hashCode => Object.hash( + runtimeType, + id, + title, + createdAtSeconds, + isDone, + description, + imageUrl, + ); /// Create a copy of TodoDTO /// with the given fields replaced by the non-null parameter values. @@ -193,6 +268,9 @@ abstract class _TodoDTO extends TodoDTO { required final String id, required final String title, final int? createdAtSeconds, + required final bool isDone, + final String? description, + final String? imageUrl, }) = _$TodoDTOImpl; const _TodoDTO._() : super._(); @@ -204,6 +282,12 @@ abstract class _TodoDTO extends TodoDTO { String get title; @override int? get createdAtSeconds; + @override + bool get isDone; + @override + String? get description; + @override + String? get imageUrl; /// Create a copy of TodoDTO /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/todo/data/models/todo_dto.g.dart b/lib/features/todo/data/models/todo_dto.g.dart index bada3c6..3901cbc 100644 --- a/lib/features/todo/data/models/todo_dto.g.dart +++ b/lib/features/todo/data/models/todo_dto.g.dart @@ -11,6 +11,9 @@ _$TodoDTOImpl _$$TodoDTOImplFromJson(Map json) => id: json['id'] as String, title: json['title'] as String, createdAtSeconds: (json['createdAtSeconds'] as num?)?.toInt(), + isDone: json['isDone'] as bool, + description: json['description'] as String?, + imageUrl: json['imageUrl'] as String?, ); Map _$$TodoDTOImplToJson(_$TodoDTOImpl instance) => @@ -18,4 +21,7 @@ Map _$$TodoDTOImplToJson(_$TodoDTOImpl instance) => 'id': instance.id, 'title': instance.title, 'createdAtSeconds': instance.createdAtSeconds, + 'isDone': instance.isDone, + 'description': instance.description, + 'imageUrl': instance.imageUrl, }; diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index 49e63b0..14341e3 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'); } @@ -51,10 +51,10 @@ 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, createdAt: createdAt, isDone: dto.isDone); } 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', createdAt: DateTime.now(), isDone: false); } } } diff --git a/lib/features/todo/domain/models/todo_model.dart b/lib/features/todo/domain/models/todo_model.dart index 606a711..22537b8 100644 --- a/lib/features/todo/domain/models/todo_model.dart +++ b/lib/features/todo/domain/models/todo_model.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:intl/intl.dart'; @@ -8,7 +10,7 @@ part 'todo_model.g.dart'; class TodoModel with _$TodoModel { const TodoModel._(); - const factory TodoModel({required String id, required String title, DateTime? createdAt}) = + const factory TodoModel({required String id, required String title, DateTime? createdAt, String? imageUrl, String? description, required bool isDone}) = _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..07fc556 100644 --- a/lib/features/todo/domain/models/todo_model.freezed.dart +++ b/lib/features/todo/domain/models/todo_model.freezed.dart @@ -24,6 +24,9 @@ mixin _$TodoModel { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; + String? get imageUrl => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + bool get isDone => throw _privateConstructorUsedError; /// Serializes this TodoModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -40,7 +43,14 @@ 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, + DateTime? createdAt, + String? imageUrl, + String? description, + bool isDone, + }); } /// @nodoc @@ -61,6 +71,9 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> Object? id = null, Object? title = null, Object? createdAt = freezed, + Object? imageUrl = freezed, + Object? description = freezed, + Object? isDone = null, }) { return _then( _value.copyWith( @@ -79,6 +92,21 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isDone: + null == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val, ); @@ -94,7 +122,14 @@ abstract class _$$TodoModelImplCopyWith<$Res> ) = __$$TodoModelImplCopyWithImpl<$Res>; @override @useResult - $Res call({String id, String title, DateTime? createdAt}); + $Res call({ + String id, + String title, + DateTime? createdAt, + String? imageUrl, + String? description, + bool isDone, + }); } /// @nodoc @@ -114,6 +149,9 @@ class __$$TodoModelImplCopyWithImpl<$Res> Object? id = null, Object? title = null, Object? createdAt = freezed, + Object? imageUrl = freezed, + Object? description = freezed, + Object? isDone = null, }) { return _then( _$TodoModelImpl( @@ -132,6 +170,21 @@ class __$$TodoModelImplCopyWithImpl<$Res> ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?, + imageUrl: + freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isDone: + null == isDone + ? _value.isDone + : isDone // ignore: cast_nullable_to_non_nullable + as bool, ), ); } @@ -140,8 +193,14 @@ 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.createdAt, + this.imageUrl, + this.description, + required this.isDone, + }) : super._(); factory _$TodoModelImpl.fromJson(Map json) => _$$TodoModelImplFromJson(json); @@ -152,10 +211,16 @@ class _$TodoModelImpl extends _TodoModel { final String title; @override final DateTime? createdAt; + @override + final String? imageUrl; + @override + final String? description; + @override + final bool isDone; @override String toString() { - return 'TodoModel(id: $id, title: $title, createdAt: $createdAt)'; + return 'TodoModel(id: $id, title: $title, createdAt: $createdAt, imageUrl: $imageUrl, description: $description, isDone: $isDone)'; } @override @@ -166,12 +231,25 @@ class _$TodoModelImpl extends _TodoModel { (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && (identical(other.createdAt, createdAt) || - other.createdAt == createdAt)); + other.createdAt == createdAt) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl) && + (identical(other.description, description) || + other.description == description) && + (identical(other.isDone, isDone) || other.isDone == isDone)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, title, createdAt); + int get hashCode => Object.hash( + runtimeType, + id, + title, + createdAt, + imageUrl, + description, + isDone, + ); /// Create a copy of TodoModel /// with the given fields replaced by the non-null parameter values. @@ -192,6 +270,9 @@ abstract class _TodoModel extends TodoModel { required final String id, required final String title, final DateTime? createdAt, + final String? imageUrl, + final String? description, + required final bool isDone, }) = _$TodoModelImpl; const _TodoModel._() : super._(); @@ -204,6 +285,12 @@ abstract class _TodoModel extends TodoModel { String get title; @override DateTime? get createdAt; + @override + String? get imageUrl; + @override + String? get description; + @override + bool get isDone; /// Create a copy of TodoModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/todo/domain/models/todo_model.g.dart b/lib/features/todo/domain/models/todo_model.g.dart index 6512f96..394a86b 100644 --- a/lib/features/todo/domain/models/todo_model.g.dart +++ b/lib/features/todo/domain/models/todo_model.g.dart @@ -14,6 +14,9 @@ _$TodoModelImpl _$$TodoModelImplFromJson(Map json) => json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), + imageUrl: json['imageUrl'] as String?, + description: json['description'] as String?, + isDone: json['isDone'] as bool, ); Map _$$TodoModelImplToJson(_$TodoModelImpl instance) => @@ -21,4 +24,7 @@ Map _$$TodoModelImplToJson(_$TodoModelImpl instance) => 'id': instance.id, 'title': instance.title, 'createdAt': instance.createdAt?.toIso8601String(), + 'imageUrl': instance.imageUrl, + 'description': instance.description, + 'isDone': instance.isDone, }; diff --git a/lib/features/todo/presentation/cubits/todo_cubit.dart b/lib/features/todo/presentation/cubits/todo_cubit.dart index f1805f4..1ba3f0a 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -21,10 +21,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..5817d8f 100644 --- a/lib/features/todo/presentation/screens/todo_form_screen.dart +++ b/lib/features/todo/presentation/screens/todo_form_screen.dart @@ -13,16 +13,24 @@ class TodoFormScreen extends StatefulWidget { class _TodoFormScreenState extends State { final _formKey = GlobalKey(); final _titleController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _imageUrlController = TextEditingController(); @override void dispose() { _titleController.dispose(); + _descriptionController.dispose(); + _imageUrlController.dispose(); super.dispose(); } void _submitForm() { if (_formKey.currentState?.validate() ?? false) { - context.read().addTodo(_titleController.text); + context.read().addTodo( + _titleController.text, + _descriptionController.text, + _imageUrlController.text, + ); Navigator.pop(context); } } @@ -41,7 +49,10 @@ class _TodoFormScreenState extends State { const SizedBox(height: 16), TextFormField( controller: _titleController, - decoration: const InputDecoration(labelText: 'Title', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Title', + border: OutlineInputBorder(), + ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a title'; @@ -49,10 +60,40 @@ class _TodoFormScreenState extends State { return null; }, ), + const SizedBox(height: 16), + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a description'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _imageUrlController, + decoration: const InputDecoration( + labelText: 'image URL', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an image URL'; + } + return null; + }, + ), const SizedBox(height: 24), ElevatedButton( onPressed: _submitForm, - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), child: const Text('Add Todo'), ), ], From f5a77be4fb29e161417fd150abb741f5b8e2d0b0 Mon Sep 17 00:00:00 2001 From: Jakub Ostwald Date: Tue, 27 May 2025 15:32:58 +0200 Subject: [PATCH 2/5] edit UI to include images and descriptions --- lib/features/todo/data/models/todo_dto.dart | 4 +- .../todo/data/models/todo_dto.freezed.dart | 52 +++++++++---------- lib/features/todo/data/models/todo_dto.g.dart | 4 +- .../data/repositories/todo_repository.dart | 4 +- .../todo/domain/models/todo_model.dart | 2 +- .../domain/models/todo_model.freezed.dart | 52 +++++++++---------- .../todo/domain/models/todo_model.g.dart | 4 +- .../todo/presentation/widgets/todo_item.dart | 15 +++++- 8 files changed, 75 insertions(+), 62 deletions(-) diff --git a/lib/features/todo/data/models/todo_dto.dart b/lib/features/todo/data/models/todo_dto.dart index f0c8743..4e328f6 100644 --- a/lib/features/todo/data/models/todo_dto.dart +++ b/lib/features/todo/data/models/todo_dto.dart @@ -12,8 +12,8 @@ class TodoDTO with _$TodoDTO { required String title, int? createdAtSeconds, required bool isDone, - String? description, - String? imageUrl, + required String description, + required String imageUrl, }) = _TodoDTO; factory TodoDTO.fromJson(Map json) => _$TodoDTOFromJson(json); diff --git a/lib/features/todo/data/models/todo_dto.freezed.dart b/lib/features/todo/data/models/todo_dto.freezed.dart index a4bf105..f25466f 100644 --- a/lib/features/todo/data/models/todo_dto.freezed.dart +++ b/lib/features/todo/data/models/todo_dto.freezed.dart @@ -25,8 +25,8 @@ mixin _$TodoDTO { String get title => throw _privateConstructorUsedError; int? get createdAtSeconds => throw _privateConstructorUsedError; bool get isDone => throw _privateConstructorUsedError; - String? get description => throw _privateConstructorUsedError; - String? get imageUrl => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + String get imageUrl => throw _privateConstructorUsedError; /// Serializes this TodoDTO to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -47,8 +47,8 @@ abstract class $TodoDTOCopyWith<$Res> { String title, int? createdAtSeconds, bool isDone, - String? description, - String? imageUrl, + String description, + String imageUrl, }); } @@ -71,8 +71,8 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> Object? title = null, Object? createdAtSeconds = freezed, Object? isDone = null, - Object? description = freezed, - Object? imageUrl = freezed, + Object? description = null, + Object? imageUrl = null, }) { return _then( _value.copyWith( @@ -97,15 +97,15 @@ class _$TodoDTOCopyWithImpl<$Res, $Val extends TodoDTO> : isDone // ignore: cast_nullable_to_non_nullable as bool, description: - freezed == description + null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String?, + as String, imageUrl: - freezed == imageUrl + null == imageUrl ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable - as String?, + as String, ) as $Val, ); @@ -125,8 +125,8 @@ abstract class _$$TodoDTOImplCopyWith<$Res> implements $TodoDTOCopyWith<$Res> { String title, int? createdAtSeconds, bool isDone, - String? description, - String? imageUrl, + String description, + String imageUrl, }); } @@ -148,8 +148,8 @@ class __$$TodoDTOImplCopyWithImpl<$Res> Object? title = null, Object? createdAtSeconds = freezed, Object? isDone = null, - Object? description = freezed, - Object? imageUrl = freezed, + Object? description = null, + Object? imageUrl = null, }) { return _then( _$TodoDTOImpl( @@ -174,15 +174,15 @@ class __$$TodoDTOImplCopyWithImpl<$Res> : isDone // ignore: cast_nullable_to_non_nullable as bool, description: - freezed == description + null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String?, + as String, imageUrl: - freezed == imageUrl + null == imageUrl ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable - as String?, + as String, ), ); } @@ -196,8 +196,8 @@ class _$TodoDTOImpl extends _TodoDTO { required this.title, this.createdAtSeconds, required this.isDone, - this.description, - this.imageUrl, + required this.description, + required this.imageUrl, }) : super._(); factory _$TodoDTOImpl.fromJson(Map json) => @@ -212,9 +212,9 @@ class _$TodoDTOImpl extends _TodoDTO { @override final bool isDone; @override - final String? description; + final String description; @override - final String? imageUrl; + final String imageUrl; @override String toString() { @@ -269,8 +269,8 @@ abstract class _TodoDTO extends TodoDTO { required final String title, final int? createdAtSeconds, required final bool isDone, - final String? description, - final String? imageUrl, + required final String description, + required final String imageUrl, }) = _$TodoDTOImpl; const _TodoDTO._() : super._(); @@ -285,9 +285,9 @@ abstract class _TodoDTO extends TodoDTO { @override bool get isDone; @override - String? get description; + String get description; @override - String? get imageUrl; + String get imageUrl; /// Create a copy of TodoDTO /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/todo/data/models/todo_dto.g.dart b/lib/features/todo/data/models/todo_dto.g.dart index 3901cbc..5706820 100644 --- a/lib/features/todo/data/models/todo_dto.g.dart +++ b/lib/features/todo/data/models/todo_dto.g.dart @@ -12,8 +12,8 @@ _$TodoDTOImpl _$$TodoDTOImplFromJson(Map json) => title: json['title'] as String, createdAtSeconds: (json['createdAtSeconds'] as num?)?.toInt(), isDone: json['isDone'] as bool, - description: json['description'] as String?, - imageUrl: json['imageUrl'] as String?, + description: json['description'] as String, + imageUrl: json['imageUrl'] as String, ); Map _$$TodoDTOImplToJson(_$TodoDTOImpl instance) => diff --git a/lib/features/todo/data/repositories/todo_repository.dart b/lib/features/todo/data/repositories/todo_repository.dart index 14341e3..5ba1872 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -51,10 +51,10 @@ class TodoRepository { createdAt = DateTime.fromMillisecondsSinceEpoch(dto.createdAtSeconds! * 1000); } - return TodoModel(id: dto.id, title: dto.title, createdAt: createdAt, isDone: dto.isDone); + return TodoModel(id: dto.id, title: dto.title, createdAt: createdAt, isDone: dto.isDone, imageUrl: dto.imageUrl, description: dto.description); } catch (e) { log('Error mapping DTO to model: $e'); - return TodoModel(id: const Uuid().v4(), title: 'Unknown title', createdAt: DateTime.now(), isDone: false); + return TodoModel(id: const Uuid().v4(), title: 'Unknown title', createdAt: DateTime.now(), isDone: false, imageUrl: '', description: ''); } } } diff --git a/lib/features/todo/domain/models/todo_model.dart b/lib/features/todo/domain/models/todo_model.dart index 22537b8..e3df8a8 100644 --- a/lib/features/todo/domain/models/todo_model.dart +++ b/lib/features/todo/domain/models/todo_model.dart @@ -10,7 +10,7 @@ part 'todo_model.g.dart'; class TodoModel with _$TodoModel { const TodoModel._(); - const factory TodoModel({required String id, required String title, DateTime? createdAt, String? imageUrl, String? description, required bool isDone}) = + const factory TodoModel({required String id, required String title, DateTime? createdAt, required String imageUrl, required String description, required bool isDone}) = _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 07fc556..9581f78 100644 --- a/lib/features/todo/domain/models/todo_model.freezed.dart +++ b/lib/features/todo/domain/models/todo_model.freezed.dart @@ -24,8 +24,8 @@ mixin _$TodoModel { String get id => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; - String? get imageUrl => throw _privateConstructorUsedError; - String? get description => throw _privateConstructorUsedError; + String get imageUrl => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; bool get isDone => throw _privateConstructorUsedError; /// Serializes this TodoModel to a JSON map. @@ -47,8 +47,8 @@ abstract class $TodoModelCopyWith<$Res> { String id, String title, DateTime? createdAt, - String? imageUrl, - String? description, + String imageUrl, + String description, bool isDone, }); } @@ -71,8 +71,8 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> Object? id = null, Object? title = null, Object? createdAt = freezed, - Object? imageUrl = freezed, - Object? description = freezed, + Object? imageUrl = null, + Object? description = null, Object? isDone = null, }) { return _then( @@ -93,15 +93,15 @@ class _$TodoModelCopyWithImpl<$Res, $Val extends TodoModel> : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?, imageUrl: - freezed == imageUrl + null == imageUrl ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable - as String?, + as String, description: - freezed == description + null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String?, + as String, isDone: null == isDone ? _value.isDone @@ -126,8 +126,8 @@ abstract class _$$TodoModelImplCopyWith<$Res> String id, String title, DateTime? createdAt, - String? imageUrl, - String? description, + String imageUrl, + String description, bool isDone, }); } @@ -149,8 +149,8 @@ class __$$TodoModelImplCopyWithImpl<$Res> Object? id = null, Object? title = null, Object? createdAt = freezed, - Object? imageUrl = freezed, - Object? description = freezed, + Object? imageUrl = null, + Object? description = null, Object? isDone = null, }) { return _then( @@ -171,15 +171,15 @@ class __$$TodoModelImplCopyWithImpl<$Res> : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?, imageUrl: - freezed == imageUrl + null == imageUrl ? _value.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable - as String?, + as String, description: - freezed == description + null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String?, + as String, isDone: null == isDone ? _value.isDone @@ -197,8 +197,8 @@ class _$TodoModelImpl extends _TodoModel { required this.id, required this.title, this.createdAt, - this.imageUrl, - this.description, + required this.imageUrl, + required this.description, required this.isDone, }) : super._(); @@ -212,9 +212,9 @@ class _$TodoModelImpl extends _TodoModel { @override final DateTime? createdAt; @override - final String? imageUrl; + final String imageUrl; @override - final String? description; + final String description; @override final bool isDone; @@ -270,8 +270,8 @@ abstract class _TodoModel extends TodoModel { required final String id, required final String title, final DateTime? createdAt, - final String? imageUrl, - final String? description, + required final String imageUrl, + required final String description, required final bool isDone, }) = _$TodoModelImpl; const _TodoModel._() : super._(); @@ -286,9 +286,9 @@ abstract class _TodoModel extends TodoModel { @override DateTime? get createdAt; @override - String? get imageUrl; + String get imageUrl; @override - String? get description; + String get description; @override bool get isDone; diff --git a/lib/features/todo/domain/models/todo_model.g.dart b/lib/features/todo/domain/models/todo_model.g.dart index 394a86b..f54a9bd 100644 --- a/lib/features/todo/domain/models/todo_model.g.dart +++ b/lib/features/todo/domain/models/todo_model.g.dart @@ -14,8 +14,8 @@ _$TodoModelImpl _$$TodoModelImplFromJson(Map json) => json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), - imageUrl: json['imageUrl'] as String?, - description: json['description'] as String?, + imageUrl: json['imageUrl'] as String, + description: json['description'] as String, isDone: json['isDone'] as bool, ); diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index 0930dea..510ff82 100644 --- a/lib/features/todo/presentation/widgets/todo_item.dart +++ b/lib/features/todo/presentation/widgets/todo_item.dart @@ -17,7 +17,20 @@ class TodoItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(todo.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + CircleAvatar(backgroundImage: NetworkImage(todo.imageUrl)), + const SizedBox(width: 12), + Text(todo.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 4), + Text( + todo.description, + maxLines: 2, + style: TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), const SizedBox(height: 4), Text( todo.formattedDate, From eca5a0d46b310e22299b06aad77847f5e018d851 Mon Sep 17 00:00:00 2001 From: Jakub Ostwald Date: Tue, 27 May 2025 16:35:36 +0200 Subject: [PATCH 3/5] edit UI to include checkbox for the todo --- .../todo/presentation/widgets/todo_item.dart | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index 510ff82..5fc14d2 100644 --- a/lib/features/todo/presentation/widgets/todo_item.dart +++ b/lib/features/todo/presentation/widgets/todo_item.dart @@ -7,6 +7,10 @@ class TodoItem extends StatelessWidget { const TodoItem({super.key, required this.todo}); + void setDone(bool value) { + + } + @override Widget build(BuildContext context) { return Card( @@ -14,27 +18,50 @@ class TodoItem extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Padding( padding: const EdgeInsets.all(12.0), - child: Column( + child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CircleAvatar(backgroundImage: NetworkImage(todo.imageUrl)), - const SizedBox(width: 12), - Text(todo.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + CircleAvatar(backgroundImage: NetworkImage(todo.imageUrl)), + const SizedBox(width: 12), + Text( + todo.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + todo.description, + maxLines: 2, + style: TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + todo.formattedDate, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), ], ), - const SizedBox(height: 4), - Text( - todo.description, - maxLines: 2, - style: TextStyle(fontSize: 14), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - todo.formattedDate, - style: TextStyle(fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic), + Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Checkbox(value: todo.isDone, onChanged: null) + ], ), ], ), From 94bc79cd49784aefdfa386134bf9b9b2b934463c Mon Sep 17 00:00:00 2001 From: Jakub Ostwald Date: Tue, 27 May 2025 17:37:08 +0200 Subject: [PATCH 4/5] add updating done state --- .../datasources/todo_remote_datasource.dart | 27 +++++++++++++++++++ .../data/repositories/todo_repository.dart | 9 +++++++ .../todo/presentation/cubits/todo_cubit.dart | 19 +++++++++++++ .../todo/presentation/widgets/todo_item.dart | 10 +++---- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index f866045..a18db45 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -68,6 +68,33 @@ class TodoRemoteDataSource { } } + Future updateDoneState(String id, bool isDone) async { + try { + log('Updating done in a todo with id: $id'); + + final response = await _client.patch( + Uri.parse('$baseUrl/todo/$id'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({'isDone': isDone}), + ); + + 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 create todo: ${response.statusCode} \n ${response.body}'); + } + } catch (e) { + log('error while updating done state: $e'); + throw Exception('Failed to update done state'); + } + } + // 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 5ba1872..e6fe2ca 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -43,6 +43,15 @@ class TodoRepository { } } + Future updateDoneState(String id, bool isDone) async { + try { + return _mapDtoToModel(await remoteDataSource.updateDoneState(id, isDone)); + } catch (e) { + log('Error updating done state: $e'); + throw Exception('Error updating done state'); + } + } + // 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 1ba3f0a..85c958f 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -30,4 +30,23 @@ class TodoCubit extends Cubit { emit(TodosError(message: e.toString())); } } + + Future updateTodo(bool isDone, String id) async { + try { + await _repository.updateDoneState(id, isDone); + if (state is TodosLoaded) { + final updatedItems = + (state as TodosLoaded).todos.map((item) { + return item.id == id ? item.copyWith(isDone: isDone) : item; + }).toList(); + + emit(TodosLoaded(todos: updatedItems)); + } else { + loadTodos(); + } + } catch (e) { + emit(TodosError(message: e.toString())); + throw Exception('error while updating done state'); + } + } } diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index 5fc14d2..272aafc 100644 --- a/lib/features/todo/presentation/widgets/todo_item.dart +++ b/lib/features/todo/presentation/widgets/todo_item.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_todo_workshop/features/todo/presentation/cubits/todo_cubit.dart'; import '../../domain/models/todo_model.dart'; @@ -7,10 +9,6 @@ class TodoItem extends StatelessWidget { const TodoItem({super.key, required this.todo}); - void setDone(bool value) { - - } - @override Widget build(BuildContext context) { return Card( @@ -60,7 +58,9 @@ class TodoItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Checkbox(value: todo.isDone, onChanged: null) + Checkbox(value: todo.isDone, onChanged: (bool? value) { + context.read().updateTodo(!todo.isDone, todo.id); + }) ], ), ], From 705c0e164079578c850c12ad392429f1cf4e7bab Mon Sep 17 00:00:00 2001 From: Jakub Ostwald Date: Tue, 27 May 2025 21:27:31 +0200 Subject: [PATCH 5/5] add deletion and fix limiting of the description --- .../datasources/todo_remote_datasource.dart | 24 ++- .../data/repositories/todo_repository.dart | 8 + .../todo/presentation/cubits/todo_cubit.dart | 19 ++ .../todo/presentation/widgets/todo_item.dart | 178 ++++++++++++++---- 4 files changed, 195 insertions(+), 34 deletions(-) diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index a18db45..46ba747 100644 --- a/lib/features/todo/data/datasources/todo_remote_datasource.dart +++ b/lib/features/todo/data/datasources/todo_remote_datasource.dart @@ -4,7 +4,6 @@ import 'dart:developer'; import 'package:http/http.dart' as http; import 'package:injectable/injectable.dart'; import 'package:uuid/uuid.dart'; -import 'package:uuid/v4.dart'; import '../models/todo_dto.dart'; @@ -95,6 +94,29 @@ class TodoRemoteDataSource { } } + Future deleteTodo(String id) async { + try { + log('Deleting a todo with id: $id'); + + final response = await _client.delete( + Uri.parse('$baseUrl/todo/$id'), + ); + + log('Response status: ${response.statusCode}'); + log('Response body: ${response.body}'); + + if (response.statusCode == 201 || response.statusCode == 200) { + final Map responseData = json.decode(response.body); + log('response: $responseData'); + } else { + throw Exception('Failed to create todo: ${response.statusCode} \n ${response.body}'); + } + } catch (e) { + log('error while updating done state: $e'); + throw Exception('Failed to update done state'); + } + } + // 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 e6fe2ca..02cd9d6 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -52,6 +52,14 @@ class TodoRepository { } } + Future deleteTodo(String id) async { + try { + await remoteDataSource.deleteTodo(id); + } catch (e) { + log('Error while deleting the todo: $e'); + } + } + // 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 85c958f..960e033 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -49,4 +49,23 @@ class TodoCubit extends Cubit { throw Exception('error while updating done state'); } } + + Future deleteTodo(String id) async { + try { + await _repository.deleteTodo(id); + if (state is TodosLoaded) { + final updatedItems = + (state as TodosLoaded).todos.map((item) { + return item.id == id ? null : item; + }).nonNulls.toList(); + + emit(TodosLoaded(todos: updatedItems)); + } else { + loadTodos(); + } + } catch (e) { + emit(TodosError(message: e.toString())); + throw Exception('error while deleting todo'); + } + } } diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index 272aafc..20962e4 100644 --- a/lib/features/todo/presentation/widgets/todo_item.dart +++ b/lib/features/todo/presentation/widgets/todo_item.dart @@ -11,39 +11,152 @@ class TodoItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - elevation: 2, - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( + return Dismissible( + key: Key(todo.id.toString()), + direction: DismissDirection.endToStart, + background: Container( + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 20), + color: Colors.red, + child: Icon(Icons.delete, color: Colors.white), + ), + confirmDismiss: (direction) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Delete Todo'), + content: Text('Are you sure you want to delete "${todo.title}"?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ); + }, + ); + }, + onDismissed: (direction) { + context.read().deleteTodo(todo.id); + }, + child: GestureDetector( + onTap: () => _showTodoDialog(context), + child: Card( + elevation: 2, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CircleAvatar(backgroundImage: NetworkImage(todo.imageUrl)), - const SizedBox(width: 12), + Row( + children: [ + CircleAvatar( + backgroundImage: NetworkImage(todo.imageUrl), + ), + const SizedBox(width: 12), + Text( + todo.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + SizedBox( + width: 200, + child: Text( + maxLines: 2, + todo.description, + style: TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ), + const SizedBox(height: 4), Text( - todo.title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + todo.formattedDate, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, ), ), ], ), - const SizedBox(height: 4), + Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Checkbox( + value: todo.isDone, + onChanged: (bool? value) { + context.read().updateTodo( + !todo.isDone, + todo.id, + ); + }, + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + void _showTodoDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + todo.title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (todo.imageUrl.isNotEmpty) + Container( + height: 150, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: NetworkImage(todo.imageUrl), + fit: BoxFit.cover, + ), + ), + ), + SizedBox(height: 16), + Text( + 'Description:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 4), + Text(todo.description), + SizedBox(height: 24), Text( - todo.description, - maxLines: 2, - style: TextStyle(fontSize: 14), - overflow: TextOverflow.ellipsis, + 'Created:', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), - const SizedBox(height: 4), + SizedBox(height: 4), Text( todo.formattedDate, style: TextStyle( @@ -54,18 +167,17 @@ class TodoItem extends StatelessWidget { ), ], ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Checkbox(value: todo.isDone, onChanged: (bool? value) { - context.read().updateTodo(!todo.isDone, todo.id); - }) - ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('Close'), ), ], - ), - ), + ); + }, ); } }