-
Notifications
You must be signed in to change notification settings - Fork 0
Feature content management create category #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3c7af92
0b99b93
8edfa8e
db11d3f
031367e
5696d0e
b4835d6
24db001
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import 'package:bloc/bloc.dart'; | ||
import 'package:equatable/equatable.dart'; | ||
import 'package:ht_data_repository/ht_data_repository.dart'; | ||
import 'package:ht_shared/ht_shared.dart'; | ||
|
||
part 'create_category_event.dart'; | ||
part 'create_category_state.dart'; | ||
|
||
/// A BLoC to manage the state of creating a new category. | ||
class CreateCategoryBloc | ||
extends Bloc<CreateCategoryEvent, CreateCategoryState> { | ||
/// {@macro create_category_bloc} | ||
CreateCategoryBloc({ | ||
required HtDataRepository<Category> categoriesRepository, | ||
}) : _categoriesRepository = categoriesRepository, | ||
super(const CreateCategoryState()) { | ||
on<CreateCategoryNameChanged>(_onNameChanged); | ||
on<CreateCategoryDescriptionChanged>(_onDescriptionChanged); | ||
on<CreateCategoryIconUrlChanged>(_onIconUrlChanged); | ||
on<CreateCategorySubmitted>(_onSubmitted); | ||
} | ||
|
||
final HtDataRepository<Category> _categoriesRepository; | ||
|
||
void _onNameChanged( | ||
CreateCategoryNameChanged event, | ||
Emitter<CreateCategoryState> emit, | ||
) { | ||
emit( | ||
state.copyWith( | ||
name: event.name, | ||
status: CreateCategoryStatus.initial, | ||
), | ||
); | ||
} | ||
|
||
void _onDescriptionChanged( | ||
CreateCategoryDescriptionChanged event, | ||
Emitter<CreateCategoryState> emit, | ||
) { | ||
emit( | ||
state.copyWith( | ||
description: event.description, | ||
status: CreateCategoryStatus.initial, | ||
), | ||
); | ||
} | ||
|
||
void _onIconUrlChanged( | ||
CreateCategoryIconUrlChanged event, | ||
Emitter<CreateCategoryState> emit, | ||
) { | ||
emit( | ||
state.copyWith( | ||
iconUrl: event.iconUrl, | ||
status: CreateCategoryStatus.initial, | ||
), | ||
); | ||
} | ||
|
||
Future<void> _onSubmitted( | ||
CreateCategorySubmitted event, | ||
Emitter<CreateCategoryState> emit, | ||
) async { | ||
if (!state.isFormValid) return; | ||
|
||
emit(state.copyWith(status: CreateCategoryStatus.submitting)); | ||
try { | ||
final newCategory = Category( | ||
name: state.name, | ||
description: state.description.isNotEmpty ? state.description : null, | ||
iconUrl: state.iconUrl.isNotEmpty ? state.iconUrl : null, | ||
); | ||
|
||
await _categoriesRepository.create(item: newCategory); | ||
emit(state.copyWith(status: CreateCategoryStatus.success)); | ||
} on HtHttpException catch (e) { | ||
emit( | ||
state.copyWith( | ||
status: CreateCategoryStatus.failure, | ||
errorMessage: e.message, | ||
), | ||
); | ||
} catch (e) { | ||
emit( | ||
state.copyWith( | ||
status: CreateCategoryStatus.failure, | ||
errorMessage: e.toString(), | ||
), | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
part of 'create_category_bloc.dart'; | ||
|
||
/// Base class for all events related to the [CreateCategoryBloc]. | ||
sealed class CreateCategoryEvent extends Equatable { | ||
const CreateCategoryEvent(); | ||
|
||
@override | ||
List<Object> get props => []; | ||
} | ||
|
||
/// Event for when the category's name is changed. | ||
final class CreateCategoryNameChanged extends CreateCategoryEvent { | ||
const CreateCategoryNameChanged(this.name); | ||
final String name; | ||
@override | ||
List<Object> get props => [name]; | ||
} | ||
|
||
/// Event for when the category's description is changed. | ||
final class CreateCategoryDescriptionChanged extends CreateCategoryEvent { | ||
const CreateCategoryDescriptionChanged(this.description); | ||
final String description; | ||
@override | ||
List<Object> get props => [description]; | ||
} | ||
|
||
/// Event for when the category's icon URL is changed. | ||
final class CreateCategoryIconUrlChanged extends CreateCategoryEvent { | ||
const CreateCategoryIconUrlChanged(this.iconUrl); | ||
final String iconUrl; | ||
@override | ||
List<Object> get props => [iconUrl]; | ||
} | ||
|
||
/// Event to signal that the form should be submitted. | ||
final class CreateCategorySubmitted extends CreateCategoryEvent { | ||
const CreateCategorySubmitted(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
part of 'create_category_bloc.dart'; | ||
|
||
/// Represents the status of the create category operation. | ||
enum CreateCategoryStatus { | ||
/// Initial state. | ||
initial, | ||
|
||
/// The form is being submitted. | ||
submitting, | ||
|
||
/// The operation completed successfully. | ||
success, | ||
|
||
/// An error occurred. | ||
failure, | ||
} | ||
|
||
/// The state for the [CreateCategoryBloc]. | ||
final class CreateCategoryState extends Equatable { | ||
/// {@macro create_category_state} | ||
const CreateCategoryState({ | ||
this.status = CreateCategoryStatus.initial, | ||
this.name = '', | ||
this.description = '', | ||
this.iconUrl = '', | ||
this.errorMessage, | ||
}); | ||
|
||
final CreateCategoryStatus status; | ||
final String name; | ||
final String description; | ||
final String iconUrl; | ||
final String? errorMessage; | ||
|
||
/// Returns true if the form is valid and can be submitted. | ||
/// Based on the Category model, only the name is required. | ||
bool get isFormValid => name.isNotEmpty; | ||
|
||
CreateCategoryState copyWith({ | ||
CreateCategoryStatus? status, | ||
String? name, | ||
String? description, | ||
String? iconUrl, | ||
String? errorMessage, | ||
}) { | ||
return CreateCategoryState( | ||
status: status ?? this.status, | ||
name: name ?? this.name, | ||
description: description ?? this.description, | ||
iconUrl: iconUrl ?? this.iconUrl, | ||
errorMessage: errorMessage, | ||
); | ||
} | ||
Comment on lines
+39
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of
|
||
|
||
@override | ||
List<Object?> get props => [status, name, description, iconUrl, errorMessage]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_bloc/flutter_bloc.dart'; | ||
import 'package:go_router/go_router.dart'; | ||
import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; | ||
import 'package:ht_dashboard/content_management/bloc/create_category/create_category_bloc.dart'; | ||
import 'package:ht_dashboard/l10n/l10n.dart'; | ||
import 'package:ht_dashboard/shared/shared.dart'; | ||
import 'package:ht_data_repository/ht_data_repository.dart'; | ||
import 'package:ht_shared/ht_shared.dart'; | ||
|
||
/// {@template create_category_page} | ||
/// A page for creating a new category. | ||
/// It uses a [BlocProvider] to create and provide a [CreateCategoryBloc]. | ||
/// {@endtemplate} | ||
class CreateCategoryPage extends StatelessWidget { | ||
/// {@macro create_category_page} | ||
const CreateCategoryPage({super.key}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return BlocProvider( | ||
create: (context) => CreateCategoryBloc( | ||
categoriesRepository: context.read<HtDataRepository<Category>>(), | ||
), | ||
child: const _CreateCategoryView(), | ||
); | ||
} | ||
} | ||
|
||
class _CreateCategoryView extends StatefulWidget { | ||
const _CreateCategoryView(); | ||
|
||
@override | ||
State<_CreateCategoryView> createState() => _CreateCategoryViewState(); | ||
} | ||
|
||
class _CreateCategoryViewState extends State<_CreateCategoryView> { | ||
final _formKey = GlobalKey<FormState>(); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final l10n = context.l10n; | ||
return Scaffold( | ||
appBar: AppBar( | ||
title: Text(l10n.createCategory), | ||
actions: [ | ||
BlocBuilder<CreateCategoryBloc, CreateCategoryState>( | ||
builder: (context, state) { | ||
if (state.status == CreateCategoryStatus.submitting) { | ||
return const Padding( | ||
padding: EdgeInsets.only(right: AppSpacing.lg), | ||
child: SizedBox( | ||
width: 24, | ||
height: 24, | ||
child: CircularProgressIndicator(strokeWidth: 3), | ||
), | ||
); | ||
} | ||
return IconButton( | ||
icon: const Icon(Icons.save), | ||
tooltip: l10n.saveChanges, | ||
onPressed: state.isFormValid | ||
? () => context.read<CreateCategoryBloc>().add( | ||
const CreateCategorySubmitted(), | ||
) | ||
: null, | ||
); | ||
}, | ||
), | ||
], | ||
), | ||
body: BlocConsumer<CreateCategoryBloc, CreateCategoryState>( | ||
listenWhen: (previous, current) => previous.status != current.status, | ||
listener: (context, state) { | ||
if (state.status == CreateCategoryStatus.success && | ||
ModalRoute.of(context)!.isCurrent) { | ||
ScaffoldMessenger.of(context) | ||
..hideCurrentSnackBar() | ||
..showSnackBar( | ||
SnackBar( | ||
content: Text(l10n.categoryCreatedSuccessfully), | ||
), | ||
); | ||
context.read<ContentManagementBloc>().add( | ||
const LoadCategoriesRequested(), | ||
); | ||
context.pop(); | ||
} | ||
if (state.status == CreateCategoryStatus.failure) { | ||
Comment on lines
+88
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
ScaffoldMessenger.of(context) | ||
..hideCurrentSnackBar() | ||
..showSnackBar( | ||
SnackBar( | ||
content: Text(state.errorMessage ?? l10n.unknownError), | ||
backgroundColor: Theme.of(context).colorScheme.error, | ||
), | ||
); | ||
} | ||
}, | ||
builder: (context, state) { | ||
return SingleChildScrollView( | ||
child: Padding( | ||
padding: const EdgeInsets.all(AppSpacing.lg), | ||
child: Form( | ||
key: _formKey, | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
TextFormField( | ||
initialValue: state.name, | ||
decoration: InputDecoration( | ||
labelText: l10n.categoryName, | ||
border: const OutlineInputBorder(), | ||
), | ||
onChanged: (value) => context | ||
.read<CreateCategoryBloc>() | ||
.add(CreateCategoryNameChanged(value)), | ||
), | ||
const SizedBox(height: AppSpacing.lg), | ||
TextFormField( | ||
initialValue: state.description, | ||
decoration: InputDecoration( | ||
labelText: l10n.description, | ||
border: const OutlineInputBorder(), | ||
), | ||
maxLines: 3, | ||
onChanged: (value) => context | ||
.read<CreateCategoryBloc>() | ||
.add(CreateCategoryDescriptionChanged(value)), | ||
), | ||
const SizedBox(height: AppSpacing.lg), | ||
TextFormField( | ||
initialValue: state.iconUrl, | ||
decoration: InputDecoration( | ||
labelText: l10n.iconUrl, | ||
border: const OutlineInputBorder(), | ||
), | ||
onChanged: (value) => context | ||
.read<CreateCategoryBloc>() | ||
.add(CreateCategoryIconUrlChanged(value)), | ||
), | ||
], | ||
), | ||
), | ||
), | ||
); | ||
}, | ||
), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
e.toString()
for user-facing error messages can expose sensitive information. Return a generic, user-friendly message instead, and log the original error and stack trace for debugging.