Skip to content

Commit f9ad105

Browse files
committed
feat: Create edit headline page
- Implemented EditHeadlinePage - Added EditHeadlineBloc - Added form to edit headline
1 parent e53d688 commit f9ad105

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:go_router/go_router.dart';
4+
import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart';
5+
import 'package:ht_dashboard/content_management/bloc/edit_headline/edit_headline_bloc.dart';
6+
import 'package:ht_dashboard/l10n/l10n.dart';
7+
import 'package:ht_dashboard/shared/shared.dart';
8+
import 'package:ht_data_repository/ht_data_repository.dart';
9+
import 'package:ht_shared/ht_shared.dart';
10+
11+
/// {@template edit_headline_page}
12+
/// A page for editing an existing headline.
13+
/// It uses a [BlocProvider] to create and provide an [EditHeadlineBloc].
14+
/// {@endtemplate}
15+
class EditHeadlinePage extends StatelessWidget {
16+
/// {@macro edit_headline_page}
17+
const EditHeadlinePage({required this.headlineId, super.key});
18+
19+
/// The ID of the headline to be edited.
20+
final String headlineId;
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return BlocProvider(
25+
create: (context) => EditHeadlineBloc(
26+
headlinesRepository: context.read<HtDataRepository<Headline>>(),
27+
sourcesRepository: context.read<HtDataRepository<Source>>(),
28+
categoriesRepository: context.read<HtDataRepository<Category>>(),
29+
headlineId: headlineId,
30+
)..add(const EditHeadlineLoaded()),
31+
child: const _EditHeadlineView(),
32+
);
33+
}
34+
}
35+
36+
class _EditHeadlineView extends StatefulWidget {
37+
const _EditHeadlineView();
38+
39+
@override
40+
State<_EditHeadlineView> createState() => _EditHeadlineViewState();
41+
}
42+
43+
class _EditHeadlineViewState extends State<_EditHeadlineView> {
44+
final _formKey = GlobalKey<FormState>();
45+
late final TextEditingController _titleController;
46+
late final TextEditingController _descriptionController;
47+
late final TextEditingController _urlController;
48+
late final TextEditingController _imageUrlController;
49+
50+
@override
51+
void initState() {
52+
super.initState();
53+
final state = context.read<EditHeadlineBloc>().state;
54+
_titleController = TextEditingController(text: state.title);
55+
_descriptionController = TextEditingController(text: state.description);
56+
_urlController = TextEditingController(text: state.url);
57+
_imageUrlController = TextEditingController(text: state.imageUrl);
58+
}
59+
60+
@override
61+
void dispose() {
62+
_titleController.dispose();
63+
_descriptionController.dispose();
64+
_urlController.dispose();
65+
_imageUrlController.dispose();
66+
super.dispose();
67+
}
68+
69+
@override
70+
Widget build(BuildContext context) {
71+
final l10n = context.l10n;
72+
return Scaffold(
73+
appBar: AppBar(
74+
title: Text(l10n.editHeadline),
75+
actions: [
76+
BlocBuilder<EditHeadlineBloc, EditHeadlineState>(
77+
builder: (context, state) {
78+
if (state.status == EditHeadlineStatus.submitting) {
79+
return const Padding(
80+
padding: EdgeInsets.only(right: AppSpacing.lg),
81+
child: SizedBox(
82+
width: 24,
83+
height: 24,
84+
child: CircularProgressIndicator(strokeWidth: 3),
85+
),
86+
);
87+
}
88+
return IconButton(
89+
icon: const Icon(Icons.save),
90+
tooltip: l10n.saveChanges,
91+
onPressed: state.isFormValid
92+
? () => context.read<EditHeadlineBloc>().add(
93+
const EditHeadlineSubmitted(),
94+
)
95+
: null,
96+
);
97+
},
98+
),
99+
],
100+
),
101+
body: BlocConsumer<EditHeadlineBloc, EditHeadlineState>(
102+
listenWhen: (previous, current) =>
103+
previous.status != current.status ||
104+
previous.initialHeadline != current.initialHeadline,
105+
listener: (context, state) {
106+
if (state.status == EditHeadlineStatus.success &&
107+
state.initialHeadline != null &&
108+
ModalRoute.of(context)!.isCurrent) {
109+
ScaffoldMessenger.of(context)
110+
..hideCurrentSnackBar()
111+
..showSnackBar(
112+
SnackBar(
113+
content: Text(l10n.headlineUpdatedSuccessfully),
114+
),
115+
);
116+
context.read<ContentManagementBloc>().add(
117+
const LoadHeadlinesRequested(),
118+
);
119+
context.pop();
120+
}
121+
if (state.status == EditHeadlineStatus.failure) {
122+
ScaffoldMessenger.of(context)
123+
..hideCurrentSnackBar()
124+
..showSnackBar(
125+
SnackBar(
126+
content: Text(state.errorMessage ?? l10n.unknownError),
127+
backgroundColor: Theme.of(context).colorScheme.error,
128+
),
129+
);
130+
}
131+
if (state.initialHeadline != null) {
132+
_titleController.text = state.title;
133+
_descriptionController.text = state.description;
134+
_urlController.text = state.url;
135+
_imageUrlController.text = state.imageUrl;
136+
}
137+
},
138+
builder: (context, state) {
139+
if (state.status == EditHeadlineStatus.loading) {
140+
return LoadingStateWidget(
141+
icon: Icons.newspaper,
142+
headline: l10n.loadingHeadline,
143+
subheadline: l10n.pleaseWait,
144+
);
145+
}
146+
147+
if (state.status == EditHeadlineStatus.failure &&
148+
state.initialHeadline == null) {
149+
return FailureStateWidget(
150+
message: state.errorMessage ?? l10n.unknownError,
151+
onRetry: () => context.read<EditHeadlineBloc>().add(
152+
const EditHeadlineLoaded(),
153+
),
154+
);
155+
}
156+
157+
// Find the correct instances from the lists to ensure
158+
// the Dropdowns can display the selections correctly.
159+
Source? selectedSource;
160+
if (state.source != null) {
161+
try {
162+
selectedSource = state.sources.firstWhere(
163+
(s) => s.id == state.source!.id,
164+
);
165+
} catch (_) {
166+
selectedSource = null;
167+
}
168+
}
169+
170+
Category? selectedCategory;
171+
if (state.category != null) {
172+
try {
173+
selectedCategory = state.categories.firstWhere(
174+
(c) => c.id == state.category!.id,
175+
);
176+
} catch (_) {
177+
selectedCategory = null;
178+
}
179+
}
180+
181+
return SingleChildScrollView(
182+
child: Padding(
183+
padding: const EdgeInsets.all(AppSpacing.lg),
184+
child: Form(
185+
key: _formKey,
186+
child: Column(
187+
crossAxisAlignment: CrossAxisAlignment.start,
188+
children: [
189+
TextFormField(
190+
controller: _titleController,
191+
decoration: InputDecoration(
192+
labelText: l10n.headlineTitle,
193+
border: const OutlineInputBorder(),
194+
),
195+
onChanged: (value) => context
196+
.read<EditHeadlineBloc>()
197+
.add(EditHeadlineTitleChanged(value)),
198+
),
199+
const SizedBox(height: AppSpacing.lg),
200+
TextFormField(
201+
controller: _descriptionController,
202+
decoration: InputDecoration(
203+
labelText: l10n.description,
204+
border: const OutlineInputBorder(),
205+
),
206+
maxLines: 3,
207+
onChanged: (value) => context
208+
.read<EditHeadlineBloc>()
209+
.add(EditHeadlineDescriptionChanged(value)),
210+
),
211+
const SizedBox(height: AppSpacing.lg),
212+
TextFormField(
213+
controller: _urlController,
214+
decoration: InputDecoration(
215+
labelText: l10n.sourceUrl,
216+
border: const OutlineInputBorder(),
217+
),
218+
onChanged: (value) => context
219+
.read<EditHeadlineBloc>()
220+
.add(EditHeadlineUrlChanged(value)),
221+
),
222+
const SizedBox(height: AppSpacing.lg),
223+
TextFormField(
224+
controller: _imageUrlController,
225+
decoration: InputDecoration(
226+
labelText: l10n.imageUrl,
227+
border: const OutlineInputBorder(),
228+
),
229+
onChanged: (value) => context
230+
.read<EditHeadlineBloc>()
231+
.add(EditHeadlineImageUrlChanged(value)),
232+
),
233+
const SizedBox(height: AppSpacing.lg),
234+
DropdownButtonFormField<Source?>(
235+
value: selectedSource,
236+
decoration: InputDecoration(
237+
labelText: l10n.sourceName,
238+
border: const OutlineInputBorder(),
239+
),
240+
items: [
241+
DropdownMenuItem(value: null, child: Text(l10n.none)),
242+
...state.sources.map(
243+
(source) => DropdownMenuItem(
244+
value: source,
245+
child: Text(source.name),
246+
),
247+
),
248+
],
249+
onChanged: (value) => context
250+
.read<EditHeadlineBloc>()
251+
.add(EditHeadlineSourceChanged(value)),
252+
),
253+
const SizedBox(height: AppSpacing.lg),
254+
DropdownButtonFormField<Category?>(
255+
value: selectedCategory,
256+
decoration: InputDecoration(
257+
labelText: l10n.categoryName,
258+
border: const OutlineInputBorder(),
259+
),
260+
items: [
261+
DropdownMenuItem(value: null, child: Text(l10n.none)),
262+
...state.categories.map(
263+
(category) => DropdownMenuItem(
264+
value: category,
265+
child: Text(category.name),
266+
),
267+
),
268+
],
269+
onChanged: (value) => context
270+
.read<EditHeadlineBloc>()
271+
.add(EditHeadlineCategoryChanged(value)),
272+
),
273+
],
274+
),
275+
),
276+
),
277+
);
278+
},
279+
),
280+
);
281+
}
282+
}

0 commit comments

Comments
 (0)