diff --git a/lib/features/todo/data/datasources/todo_remote_datasource.dart b/lib/features/todo/data/datasources/todo_remote_datasource.dart index 8c3ef0b..46ba747 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://6835745dcd78db2058c1928b.mockapi.io'; TodoRemoteDataSource(this._client); @@ -24,7 +23,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 +31,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 +59,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'); @@ -61,6 +67,56 @@ 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'); + } + } + + 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/models/todo_dto.dart b/lib/features/todo/data/models/todo_dto.dart index 8d9d649..4e328f6 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, + 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 752b03e..f25466f 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 = null, + Object? imageUrl = null, }) { 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: + null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + imageUrl: + null == 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 = null, + Object? imageUrl = null, }) { 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: + null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + imageUrl: + null == 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, + required this.description, + required 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, + required final String description, + required 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..5706820 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..02cd9d6 100644 --- a/lib/features/todo/data/repositories/todo_repository.dart +++ b/lib/features/todo/data/repositories/todo_repository.dart @@ -34,15 +34,32 @@ 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'); } } + 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'); + } + } + + 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 { @@ -51,10 +68,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, 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()); + 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 606a711..e3df8a8 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, 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 6052b7a..9581f78 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 = null, + Object? description = null, + 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: + null == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String, + description: + null == 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 = null, + Object? description = null, + 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: + null == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String, + description: + null == 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, + required this.imageUrl, + required 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, + required final String imageUrl, + required 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..f54a9bd 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..960e033 100644 --- a/lib/features/todo/presentation/cubits/todo_cubit.dart +++ b/lib/features/todo/presentation/cubits/todo_cubit.dart @@ -21,13 +21,51 @@ 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())); } } + + 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'); + } + } + + 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/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'), ), ], diff --git a/lib/features/todo/presentation/widgets/todo_item.dart b/lib/features/todo/presentation/widgets/todo_item.dart index 0930dea..20962e4 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'; @@ -9,23 +11,173 @@ 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: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(todo.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 4), - Text( - todo.formattedDate, - style: TextStyle(fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic), + 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: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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.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: (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( + 'Created:', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), + SizedBox(height: 4), + Text( + todo.formattedDate, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('Close'), + ), + ], + ); + }, + ); + } }