Skip to content

Commit 386e04b

Browse files
authored
Merge pull request #26 from headlines-toolkit/feature_headlines_details_page_simlilar_headlines
Feature headlines details page simlilar headlines
2 parents 2133f3c + 6497421 commit 386e04b

File tree

7 files changed

+250
-15
lines changed

7 files changed

+250
-15
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:equatable/equatable.dart';
5+
import 'package:ht_data_repository/ht_data_repository.dart';
6+
import 'package:ht_shared/ht_shared.dart' show Headline, HtHttpException;
7+
8+
part 'similar_headlines_event.dart';
9+
part 'similar_headlines_state.dart';
10+
11+
class SimilarHeadlinesBloc
12+
extends Bloc<SimilarHeadlinesEvent, SimilarHeadlinesState> {
13+
SimilarHeadlinesBloc({
14+
required HtDataRepository<Headline> headlinesRepository,
15+
}) : _headlinesRepository = headlinesRepository,
16+
super(SimilarHeadlinesInitial()) {
17+
on<FetchSimilarHeadlines>(_onFetchSimilarHeadlines);
18+
}
19+
20+
final HtDataRepository<Headline> _headlinesRepository;
21+
static const int _similarHeadlinesLimit = 5;
22+
23+
Future<void> _onFetchSimilarHeadlines(
24+
FetchSimilarHeadlines event,
25+
Emitter<SimilarHeadlinesState> emit,
26+
) async {
27+
emit(SimilarHeadlinesLoading());
28+
try {
29+
final currentHeadline = event.currentHeadline;
30+
if (currentHeadline.category == null ||
31+
currentHeadline.category!.id.isEmpty) {
32+
emit(SimilarHeadlinesEmpty());
33+
return;
34+
}
35+
36+
final queryParams = {
37+
'categories': currentHeadline.category!.id,
38+
};
39+
40+
final response = await _headlinesRepository.readAllByQuery(
41+
queryParams,
42+
limit: _similarHeadlinesLimit + 1, // Fetch one extra to check if current is there
43+
);
44+
45+
// Filter out the current headline from the results
46+
final similarHeadlines = response.items
47+
.where((headline) => headline.id != currentHeadline.id)
48+
.toList();
49+
50+
// Take only the required limit after filtering
51+
final finalSimilarHeadlines = similarHeadlines.take(_similarHeadlinesLimit).toList();
52+
53+
if (finalSimilarHeadlines.isEmpty) {
54+
emit(SimilarHeadlinesEmpty());
55+
} else {
56+
emit(SimilarHeadlinesLoaded(similarHeadlines: finalSimilarHeadlines));
57+
}
58+
} on HtHttpException catch (e) {
59+
emit(SimilarHeadlinesError(message: e.message));
60+
} catch (e) {
61+
emit(SimilarHeadlinesError(message: 'An unexpected error occurred: $e'));
62+
}
63+
}
64+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
part of 'similar_headlines_bloc.dart';
2+
3+
abstract class SimilarHeadlinesEvent extends Equatable {
4+
const SimilarHeadlinesEvent();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
class FetchSimilarHeadlines extends SimilarHeadlinesEvent {
11+
const FetchSimilarHeadlines({required this.currentHeadline});
12+
13+
final Headline currentHeadline;
14+
15+
@override
16+
List<Object> get props => [currentHeadline];
17+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
part of 'similar_headlines_bloc.dart';
2+
3+
abstract class SimilarHeadlinesState extends Equatable {
4+
const SimilarHeadlinesState();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
class SimilarHeadlinesInitial extends SimilarHeadlinesState {}
11+
12+
class SimilarHeadlinesLoading extends SimilarHeadlinesState {}
13+
14+
class SimilarHeadlinesLoaded extends SimilarHeadlinesState {
15+
const SimilarHeadlinesLoaded({required this.similarHeadlines});
16+
17+
final List<Headline> similarHeadlines;
18+
19+
@override
20+
List<Object> get props => [similarHeadlines];
21+
}
22+
23+
class SimilarHeadlinesEmpty extends SimilarHeadlinesState {}
24+
25+
class SimilarHeadlinesError extends SimilarHeadlinesState {
26+
const SimilarHeadlinesError({required this.message});
27+
28+
final String message;
29+
30+
@override
31+
List<Object> get props => [message];
32+
}

lib/headline-details/view/headline_details_page.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
66
import 'package:flutter_bloc/flutter_bloc.dart';
77
import 'package:ht_main/account/bloc/account_bloc.dart'; // Import AccountBloc
88
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; // Import BLoC
9+
import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; // Import SimilarHeadlinesBloc
10+
import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart'; // Import HeadlineItemWidget
911
import 'package:ht_main/l10n/l10n.dart';
12+
import 'package:ht_main/router/routes.dart'; // Import Routes
1013
import 'package:ht_main/shared/shared.dart';
1114
import 'package:ht_shared/ht_shared.dart'
1215
show Headline; // Import Headline model
@@ -365,6 +368,17 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
365368
padding: EdgeInsets.only(bottom: AppSpacing.paddingLarge),
366369
sliver: SliverToBoxAdapter(child: SizedBox.shrink()),
367370
),
371+
// --- Similar Headlines Section ---
372+
SliverToBoxAdapter(
373+
child: Padding(
374+
padding: horizontalPadding.copyWith(top: AppSpacing.xl),
375+
child: Text(
376+
l10n.similarHeadlinesSectionTitle, // Add this l10n key
377+
style: textTheme.titleLarge,
378+
),
379+
),
380+
),
381+
_buildSimilarHeadlinesSection(context),
368382
],
369383
);
370384
}
@@ -469,4 +483,65 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
469483

470484
return chips;
471485
}
486+
487+
Widget _buildSimilarHeadlinesSection(BuildContext context) {
488+
final l10n = context.l10n;
489+
return BlocBuilder<SimilarHeadlinesBloc, SimilarHeadlinesState>(
490+
builder: (context, state) {
491+
return switch (state) {
492+
SimilarHeadlinesInitial() ||
493+
SimilarHeadlinesLoading() =>
494+
const SliverToBoxAdapter(
495+
child: Padding(
496+
padding: EdgeInsets.all(AppSpacing.lg),
497+
child: Center(child: CircularProgressIndicator()),
498+
),
499+
),
500+
final SimilarHeadlinesError errorState => SliverToBoxAdapter(
501+
child: Padding(
502+
padding: const EdgeInsets.all(AppSpacing.lg),
503+
child: Text(
504+
errorState.message,
505+
textAlign: TextAlign.center,
506+
style: TextStyle(color: Theme.of(context).colorScheme.error),
507+
),
508+
),
509+
),
510+
SimilarHeadlinesEmpty() => SliverToBoxAdapter(
511+
child: Padding(
512+
padding: const EdgeInsets.all(AppSpacing.lg),
513+
child: Text(
514+
l10n.similarHeadlinesEmpty, // Add this l10n key
515+
textAlign: TextAlign.center,
516+
),
517+
),
518+
),
519+
final SimilarHeadlinesLoaded loadedState => SliverList(
520+
delegate: SliverChildBuilderDelegate(
521+
(context, index) {
522+
final similarHeadline = loadedState.similarHeadlines[index];
523+
// Use a more compact item or reuse HeadlineItemWidget
524+
// For now, reusing HeadlineItemWidget for simplicity
525+
return Padding(
526+
padding: const EdgeInsets.symmetric(
527+
horizontal: AppSpacing.paddingMedium,
528+
vertical: AppSpacing.sm,
529+
),
530+
// Navigate to a new HeadlineDetailsPage instance
531+
// Ensure the targetRouteName is appropriate or handle navigation differently
532+
child: HeadlineItemWidget(
533+
headline: similarHeadline,
534+
targetRouteName: Routes.articleDetailsName,
535+
),
536+
);
537+
},
538+
childCount: loadedState.similarHeadlines.length,
539+
),
540+
),
541+
// Add a default case to satisfy exhaustiveness for the switch statement
542+
_ => const SliverToBoxAdapter(child: SizedBox.shrink()),
543+
};
544+
},
545+
);
546+
}
472547
}

lib/l10n/arb/app_ar.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,5 +787,13 @@
787787
"sharingUnavailableSnackbar": "المشاركة غير متاحة على هذا الجهاز أو المنصة.",
788788
"@sharingUnavailableSnackbar": {
789789
"description": "Snackbar message shown when sharing is unavailable"
790+
},
791+
"similarHeadlinesSectionTitle": "قد يعجبك ايضا",
792+
"@similarHeadlinesSectionTitle": {
793+
"description": "Title for the similar headlines section on the details page"
794+
},
795+
"similarHeadlinesEmpty": "لم يتم العثور على عناوين مشابهة.",
796+
"@similarHeadlinesEmpty": {
797+
"description": "Message shown when no similar headlines are found"
790798
}
791799
}

lib/l10n/arb/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,5 +787,13 @@
787787
"sharingUnavailableSnackbar": "Sharing is not available on this device or platform.",
788788
"@sharingUnavailableSnackbar": {
789789
"description": "Snackbar message shown when sharing is unavailable"
790+
},
791+
"similarHeadlinesSectionTitle": "You Might Also Like",
792+
"@similarHeadlinesSectionTitle": {
793+
"description": "Title for the similar headlines section on the details page"
794+
},
795+
"similarHeadlinesEmpty": "No similar headlines found.",
796+
"@similarHeadlinesEmpty": {
797+
"description": "Message shown when no similar headlines are found"
790798
}
791799
}

lib/router/router.dart

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:ht_main/authentication/view/authentication_page.dart';
2020
import 'package:ht_main/authentication/view/email_code_verification_page.dart';
2121
import 'package:ht_main/authentication/view/request_code_page.dart';
2222
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; // Re-added
23+
import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; // Import SimilarHeadlinesBloc
2324
import 'package:ht_main/headline-details/view/headline_details_page.dart';
2425
import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import new BLoC
2526
// import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import new BLoC - REMOVED
@@ -344,11 +345,21 @@ GoRouter createRouter({
344345
final headlineFromExtra = state.extra as Headline?;
345346
final headlineIdFromPath = state.pathParameters['id'];
346347

347-
return BlocProvider(
348-
create: (context) => HeadlineDetailsBloc(
349-
headlinesRepository:
350-
context.read<HtDataRepository<Headline>>(),
351-
),
348+
return MultiBlocProvider(
349+
providers: [
350+
BlocProvider(
351+
create: (context) => HeadlineDetailsBloc(
352+
headlinesRepository:
353+
context.read<HtDataRepository<Headline>>(),
354+
),
355+
),
356+
BlocProvider(
357+
create: (context) => SimilarHeadlinesBloc(
358+
headlinesRepository:
359+
context.read<HtDataRepository<Headline>>(),
360+
),
361+
),
362+
],
352363
child: HeadlineDetailsPage(
353364
initialHeadline: headlineFromExtra,
354365
// Ensure headlineId is non-null if initialHeadline is null
@@ -471,11 +482,21 @@ GoRouter createRouter({
471482
builder: (context, state) {
472483
final headlineFromExtra = state.extra as Headline?;
473484
final headlineIdFromPath = state.pathParameters['id'];
474-
return BlocProvider(
475-
create: (context) => HeadlineDetailsBloc(
476-
headlinesRepository:
477-
context.read<HtDataRepository<Headline>>(),
478-
),
485+
return MultiBlocProvider(
486+
providers: [
487+
BlocProvider(
488+
create: (context) => HeadlineDetailsBloc(
489+
headlinesRepository:
490+
context.read<HtDataRepository<Headline>>(),
491+
),
492+
),
493+
BlocProvider(
494+
create: (context) => SimilarHeadlinesBloc(
495+
headlinesRepository:
496+
context.read<HtDataRepository<Headline>>(),
497+
),
498+
),
499+
],
479500
child: HeadlineDetailsPage(
480501
initialHeadline: headlineFromExtra,
481502
headlineId: headlineFromExtra?.id ?? headlineIdFromPath,
@@ -625,11 +646,21 @@ GoRouter createRouter({
625646
builder: (context, state) {
626647
final headlineFromExtra = state.extra as Headline?;
627648
final headlineIdFromPath = state.pathParameters['id'];
628-
return BlocProvider(
629-
create: (context) => HeadlineDetailsBloc(
630-
headlinesRepository:
631-
context.read<HtDataRepository<Headline>>(),
632-
),
649+
return MultiBlocProvider(
650+
providers: [
651+
BlocProvider(
652+
create: (context) => HeadlineDetailsBloc(
653+
headlinesRepository:
654+
context.read<HtDataRepository<Headline>>(),
655+
),
656+
),
657+
BlocProvider(
658+
create: (context) => SimilarHeadlinesBloc(
659+
headlinesRepository:
660+
context.read<HtDataRepository<Headline>>(),
661+
),
662+
),
663+
],
633664
child: HeadlineDetailsPage(
634665
initialHeadline: headlineFromExtra,
635666
headlineId: headlineFromExtra?.id ?? headlineIdFromPath,

0 commit comments

Comments
 (0)