Skip to content

Feature dashboard #21

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

Merged
merged 11 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:ht_dashboard/app/config/app_environment.dart';
import 'package:ht_dashboard/app_configuration/bloc/app_configuration_bloc.dart';
import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart';
import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart';
import 'package:ht_dashboard/dashboard/bloc/dashboard_bloc.dart';
import 'package:ht_dashboard/l10n/app_localizations.dart';
import 'package:ht_dashboard/router/router.dart';
// Import for app_theme.dart
Expand All @@ -30,6 +31,7 @@ class App extends StatelessWidget {
required HtDataRepository<UserContentPreferences>
htUserContentPreferencesRepository,
required HtDataRepository<AppConfig> htAppConfigRepository,
required HtDataRepository<DashboardSummary> htDashboardSummaryRepository,
required HtKVStorageService kvStorageService,
required AppEnvironment environment,
super.key,
Expand All @@ -42,6 +44,7 @@ class App extends StatelessWidget {
_htUserContentPreferencesRepository = htUserContentPreferencesRepository,
_htAppConfigRepository = htAppConfigRepository,
_kvStorageService = kvStorageService,
_htDashboardSummaryRepository = htDashboardSummaryRepository,
_environment = environment;

final HtAuthRepository _htAuthenticationRepository;
Expand All @@ -53,6 +56,7 @@ class App extends StatelessWidget {
final HtDataRepository<UserContentPreferences>
_htUserContentPreferencesRepository;
final HtDataRepository<AppConfig> _htAppConfigRepository;
final HtDataRepository<DashboardSummary> _htDashboardSummaryRepository;
final HtKVStorageService _kvStorageService;
final AppEnvironment _environment;

Expand All @@ -68,6 +72,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _htUserAppSettingsRepository),
RepositoryProvider.value(value: _htUserContentPreferencesRepository),
RepositoryProvider.value(value: _htAppConfigRepository),
RepositoryProvider.value(value: _htDashboardSummaryRepository),
RepositoryProvider.value(value: _kvStorageService),
],
child: MultiBlocProvider(
Expand Down Expand Up @@ -98,6 +103,14 @@ class App extends StatelessWidget {
sourcesRepository: context.read<HtDataRepository<Source>>(),
),
),
BlocProvider(
create: (context) => DashboardBloc(
dashboardSummaryRepository: context
.read<HtDataRepository<DashboardSummary>>(),
appConfigRepository: context.read<HtDataRepository<AppConfig>>(),
headlinesRepository: context.read<HtDataRepository<Headline>>(),
),
),
],
child: _AppView(
htAuthenticationRepository: _htAuthenticationRepository,
Expand Down
24 changes: 24 additions & 0 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Future<Widget> bootstrap(
HtDataClient<UserContentPreferences> userContentPreferencesClient;
HtDataClient<UserAppSettings> userAppSettingsClient;
HtDataClient<AppConfig> appConfigClient;
HtDataClient<DashboardSummary> dashboardSummaryClient;

if (appConfig.environment == app_config.AppEnvironment.demo) {
headlinesClient = HtDataInMemory<Headline>(
Expand Down Expand Up @@ -96,6 +97,13 @@ Future<Widget> bootstrap(
getId: (i) => i.id,
initialData: [AppConfig.fromJson(appConfigFixtureData)],
);
dashboardSummaryClient = HtDataInMemory<DashboardSummary>(
toJson: (i) => i.toJson(),
getId: (i) => i.id,
initialData: [
DashboardSummary.fromJson(dashboardSummaryFixtureData),
],
);
} else if (appConfig.environment == app_config.AppEnvironment.development) {
headlinesClient = HtDataApi<Headline>(
httpClient: httpClient!,
Expand Down Expand Up @@ -139,6 +147,12 @@ Future<Widget> bootstrap(
fromJson: AppConfig.fromJson,
toJson: (config) => config.toJson(),
);
dashboardSummaryClient = HtDataApi<DashboardSummary>(
httpClient: httpClient,
modelName: 'dashboard_summary',
fromJson: DashboardSummary.fromJson,
toJson: (summary) => summary.toJson(),
);
Comment on lines 147 to +155

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for initializing dashboardSummaryClient is duplicated in the production environment block (lines 199-204). Consider refactoring to avoid this duplication to improve maintainability.

} else {
headlinesClient = HtDataApi<Headline>(
httpClient: httpClient!,
Expand Down Expand Up @@ -182,6 +196,12 @@ Future<Widget> bootstrap(
fromJson: AppConfig.fromJson,
toJson: (config) => config.toJson(),
);
dashboardSummaryClient = HtDataApi<DashboardSummary>(
httpClient: httpClient,
modelName: 'dashboard_summary',
fromJson: DashboardSummary.fromJson,
toJson: (summary) => summary.toJson(),
);
}

final headlinesRepository = HtDataRepository<Headline>(
Expand All @@ -204,6 +224,9 @@ Future<Widget> bootstrap(
final appConfigRepository = HtDataRepository<AppConfig>(
dataClient: appConfigClient,
);
final dashboardSummaryRepository = HtDataRepository<DashboardSummary>(
dataClient: dashboardSummaryClient,
);

return App(
htAuthenticationRepository: authenticationRepository,
Expand All @@ -214,6 +237,7 @@ Future<Widget> bootstrap(
htUserAppSettingsRepository: userAppSettingsRepository,
htUserContentPreferencesRepository: userContentPreferencesRepository,
htAppConfigRepository: appConfigRepository,
htDashboardSummaryRepository: dashboardSummaryRepository,
kvStorageService: kvStorage,
environment: environment,
);
Expand Down
76 changes: 76 additions & 0 deletions lib/dashboard/bloc/dashboard_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 'dashboard_event.dart';
part 'dashboard_state.dart';

/// A BLoC to manage the state of the dashboard.
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
/// {@macro dashboard_bloc}
DashboardBloc({
required HtDataRepository<DashboardSummary> dashboardSummaryRepository,
required HtDataRepository<AppConfig> appConfigRepository,
required HtDataRepository<Headline> headlinesRepository,
}) : _dashboardSummaryRepository = dashboardSummaryRepository,
_appConfigRepository = appConfigRepository,
_headlinesRepository = headlinesRepository,
super(const DashboardState()) {
on<DashboardSummaryLoaded>(_onDashboardSummaryLoaded);
}

final HtDataRepository<DashboardSummary> _dashboardSummaryRepository;
final HtDataRepository<AppConfig> _appConfigRepository;
final HtDataRepository<Headline> _headlinesRepository;

Future<void> _onDashboardSummaryLoaded(
DashboardSummaryLoaded event,
Emitter<DashboardState> emit,
) async {
emit(state.copyWith(status: DashboardStatus.loading));
try {
// Fetch summary, app config, and recent headlines concurrently
final [
summaryResponse,
appConfigResponse,
recentHeadlinesResponse,
] = await Future.wait([
_dashboardSummaryRepository.read(id: 'dashboard_summary'),
_appConfigRepository.read(id: 'app_config'),
_headlinesRepository.readAll(
sortBy: 'createdAt',
sortOrder: SortOrder.desc,
limit: 5,
),
]);

final summary = summaryResponse as DashboardSummary;
final appConfig = appConfigResponse as AppConfig;
final recentHeadlines =
(recentHeadlinesResponse as PaginatedResponse<Headline>).items;
emit(
state.copyWith(
status: DashboardStatus.success,
summary: summary,
appConfig: appConfig,
recentHeadlines: recentHeadlines,
),
);
} on HtHttpException catch (e) {
emit(
state.copyWith(
status: DashboardStatus.failure,
errorMessage: e.message,
),
);
} catch (e) {
emit(
state.copyWith(
status: DashboardStatus.failure,
errorMessage: 'An unknown error occurred: $e',
),
);
}
Comment on lines +67 to +74

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The generic catch (e) block catches all throwables and exposes the error details ($e) directly into the state's errorMessage. It's better to catch specific Exception types, log the full error for debugging, and provide a generic, user-friendly error message in the state to avoid potential security issues and improve user experience.

    } on HtHttpException catch (e) {
      emit(
        state.copyWith(
          status: DashboardStatus.failure,
          errorMessage: e.message,
        ),
      );
    } catch (e) {
      // Log the full error `e` for debugging purposes.
      emit(
        state.copyWith(
          status: DashboardStatus.failure,
          errorMessage: 'An unknown error occurred.',
        ),
      );
    }

}
}
12 changes: 12 additions & 0 deletions lib/dashboard/bloc/dashboard_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
part of 'dashboard_bloc.dart';

/// Base class for dashboard events.
sealed class DashboardEvent extends Equatable {
const DashboardEvent();

@override
List<Object> get props => [];
}

/// Event to load the dashboard summary data.
final class DashboardSummaryLoaded extends DashboardEvent {}
58 changes: 58 additions & 0 deletions lib/dashboard/bloc/dashboard_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
part of 'dashboard_bloc.dart';

/// Represents the status of the dashboard data loading.
enum DashboardStatus {
/// Initial state.
initial,

/// Data is being loaded.
loading,

/// Data has been successfully loaded.
success,

/// An error occurred while loading data.
failure,
}

/// The state for the [DashboardBloc].
final class DashboardState extends Equatable {
const DashboardState({
this.status = DashboardStatus.initial,
this.summary,
this.appConfig,
this.recentHeadlines = const [],
this.errorMessage,
});

final DashboardStatus status;
final DashboardSummary? summary;
final AppConfig? appConfig;
final List<Headline> recentHeadlines;
final String? errorMessage;

DashboardState copyWith({
DashboardStatus? status,
DashboardSummary? summary,
AppConfig? appConfig,
List<Headline>? recentHeadlines,
String? errorMessage,
}) {
return DashboardState(
status: status ?? this.status,
summary: summary ?? this.summary,
appConfig: appConfig ?? this.appConfig,
recentHeadlines: recentHeadlines ?? this.recentHeadlines,
errorMessage: errorMessage ?? this.errorMessage,
);
}

@override
List<Object?> get props => [
status,
summary,
appConfig,
recentHeadlines,
errorMessage,
];
}
Loading
Loading