Skip to content

Commit f59f4bb

Browse files
committed
feat: add create headline page
- Implemented form for new headlines - Added bloc for state management - Handles loading, success, error states
1 parent acc3909 commit f59f4bb

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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/create_headline/create_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 create_headline_page}
12+
/// A page for creating a new headline.
13+
/// It uses a [BlocProvider] to create and provide a [CreateHeadlineBloc].
14+
/// {@endtemplate}
15+
class CreateHeadlinePage extends StatelessWidget {
16+
/// {@macro create_headline_page}
17+
const CreateHeadlinePage({super.key});
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return BlocProvider(
22+
create: (context) => CreateHeadlineBloc(
23+
headlinesRepository: context.read<HtDataRepository<Headline>>(),
24+
sourcesRepository: context.read<HtDataRepository<Source>>(),
25+
categoriesRepository: context.read<HtDataRepository<Category>>(),
26+
)..add(const CreateHeadlineDataLoaded()),
27+
child: const _CreateHeadlineView(),
28+
);
29+
}
30+
}
31+
32+
class _CreateHeadlineView extends StatefulWidget {
33+
const _CreateHeadlineView();
34+
35+
@override
36+
State<_CreateHeadlineView> createState() => _CreateHeadlineViewState();
37+
}
38+
39+
class _CreateHeadlineViewState extends State<_CreateHeadlineView> {
40+
final _formKey = GlobalKey<FormState>();
41+
42+
@override
43+
Widget build(BuildContext context) {
44+
final l10n = context.l10n;
45+
return Scaffold(
46+
appBar: AppBar(
47+
title: Text(l10n.createHeadline),
48+
actions: [
49+
BlocBuilder<CreateHeadlineBloc, CreateHeadlineState>(
50+
builder: (context, state) {
51+
if (state.status == CreateHeadlineStatus.submitting) {
52+
return const Padding(
53+
padding: EdgeInsets.only(right: AppSpacing.lg),
54+
child: SizedBox(
55+
width: 24,
56+
height: 24,
57+
child: CircularProgressIndicator(strokeWidth: 3),
58+
),
59+
);
60+
}
61+
return IconButton(
62+
icon: const Icon(Icons.save),
63+
tooltip: l10n.saveChanges,
64+
onPressed: state.isFormValid
65+
? () => context.read<CreateHeadlineBloc>().add(
66+
const CreateHeadlineSubmitted(),
67+
)
68+
: null,
69+
);
70+
},
71+
),
72+
],
73+
),
74+
body: BlocConsumer<CreateHeadlineBloc, CreateHeadlineState>(
75+
listenWhen: (previous, current) => previous.status != current.status,
76+
listener: (context, state) {
77+
if (state.status == CreateHeadlineStatus.success &&
78+
ModalRoute.of(context)!.isCurrent) {
79+
ScaffoldMessenger.of(context)
80+
..hideCurrentSnackBar()
81+
..showSnackBar(
82+
SnackBar(
83+
content: Text(l10n.headlineCreatedSuccessfully),
84+
),
85+
);
86+
context.read<ContentManagementBloc>().add(
87+
const LoadHeadlinesRequested(),
88+
);
89+
context.pop();
90+
}
91+
if (state.status == CreateHeadlineStatus.failure) {
92+
ScaffoldMessenger.of(context)
93+
..hideCurrentSnackBar()
94+
..showSnackBar(
95+
SnackBar(
96+
content: Text(state.errorMessage ?? l10n.unknownError),
97+
backgroundColor: Theme.of(context).colorScheme.error,
98+
),
99+
);
100+
}
101+
},
102+
builder: (context, state) {
103+
if (state.status == CreateHeadlineStatus.loading) {
104+
return LoadingStateWidget(
105+
icon: Icons.newspaper,
106+
headline: l10n.loadingData,
107+
subheadline: l10n.pleaseWait,
108+
);
109+
}
110+
111+
if (state.status == CreateHeadlineStatus.failure &&
112+
state.sources.isEmpty &&
113+
state.categories.isEmpty) {
114+
return FailureStateWidget(
115+
message: state.errorMessage ?? l10n.unknownError,
116+
onRetry: () => context.read<CreateHeadlineBloc>().add(
117+
const CreateHeadlineDataLoaded(),
118+
),
119+
);
120+
}
121+
122+
return SingleChildScrollView(
123+
child: Padding(
124+
padding: const EdgeInsets.all(AppSpacing.lg),
125+
child: Form(
126+
key: _formKey,
127+
child: Column(
128+
crossAxisAlignment: CrossAxisAlignment.start,
129+
children: [
130+
TextFormField(
131+
initialValue: state.title,
132+
decoration: InputDecoration(
133+
labelText: l10n.headlineTitle,
134+
border: const OutlineInputBorder(),
135+
),
136+
onChanged: (value) => context
137+
.read<CreateHeadlineBloc>()
138+
.add(CreateHeadlineTitleChanged(value)),
139+
),
140+
const SizedBox(height: AppSpacing.lg),
141+
TextFormField(
142+
initialValue: state.description,
143+
decoration: InputDecoration(
144+
labelText: l10n.description,
145+
border: const OutlineInputBorder(),
146+
),
147+
maxLines: 3,
148+
onChanged: (value) => context
149+
.read<CreateHeadlineBloc>()
150+
.add(CreateHeadlineDescriptionChanged(value)),
151+
),
152+
const SizedBox(height: AppSpacing.lg),
153+
TextFormField(
154+
initialValue: state.url,
155+
decoration: InputDecoration(
156+
labelText: l10n.sourceUrl,
157+
border: const OutlineInputBorder(),
158+
),
159+
onChanged: (value) => context
160+
.read<CreateHeadlineBloc>()
161+
.add(CreateHeadlineUrlChanged(value)),
162+
),
163+
const SizedBox(height: AppSpacing.lg),
164+
TextFormField(
165+
initialValue: state.imageUrl,
166+
decoration: InputDecoration(
167+
labelText: l10n.imageUrl,
168+
border: const OutlineInputBorder(),
169+
),
170+
onChanged: (value) => context
171+
.read<CreateHeadlineBloc>()
172+
.add(CreateHeadlineImageUrlChanged(value)),
173+
),
174+
const SizedBox(height: AppSpacing.lg),
175+
DropdownButtonFormField<Source?>(
176+
value: state.source,
177+
decoration: InputDecoration(
178+
labelText: l10n.sourceName,
179+
border: const OutlineInputBorder(),
180+
),
181+
items: [
182+
DropdownMenuItem(value: null, child: Text(l10n.none)),
183+
...state.sources.map(
184+
(source) => DropdownMenuItem(
185+
value: source,
186+
child: Text(source.name),
187+
),
188+
),
189+
],
190+
onChanged: (value) => context
191+
.read<CreateHeadlineBloc>()
192+
.add(CreateHeadlineSourceChanged(value)),
193+
),
194+
const SizedBox(height: AppSpacing.lg),
195+
DropdownButtonFormField<Category?>(
196+
value: state.category,
197+
decoration: InputDecoration(
198+
labelText: l10n.categoryName,
199+
border: const OutlineInputBorder(),
200+
),
201+
items: [
202+
DropdownMenuItem(value: null, child: Text(l10n.none)),
203+
...state.categories.map(
204+
(category) => DropdownMenuItem(
205+
value: category,
206+
child: Text(category.name),
207+
),
208+
),
209+
],
210+
onChanged: (value) => context
211+
.read<CreateHeadlineBloc>()
212+
.add(CreateHeadlineCategoryChanged(value)),
213+
),
214+
],
215+
),
216+
),
217+
),
218+
);
219+
},
220+
),
221+
);
222+
}
223+
}

0 commit comments

Comments
 (0)