From 634fe41e78f02e8c96fc4e6a3f25d338606408b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Tue, 14 May 2024 14:54:01 +0200
Subject: [PATCH] wip: bloc for holidays

---
 lib/api/holidays/getHolidays.dart             |   6 +-
 lib/api/holidays/getHolidaysCache.dart        |   9 +-
 .../basis/dataloader/holiday_data_loader.dart |   9 +
 .../dataloader}/mhsl_data_loader.dart         |   2 +-
 .../dataLoader/data_loader.dart               |   6 +-
 .../loadableState/loadable_state.dart         |  10 +-
 .../view/loadable_state_error_bar.dart        |   1 -
 .../loadable_hydrated_bloc.dart               |  59 ++-
 .../loadable_hydrated_bloc_event.dart         |   8 +-
 .../loadable_save_context.dart                |   1 -
 lib/state/app/modules/app_modules.dart        |   4 +-
 .../modules/holidays/bloc/holidays_bloc.dart  |  36 ++
 .../modules/holidays/bloc/holidays_event.dart |   9 +
 .../modules/holidays/bloc/holidays_state.dart |  30 ++
 .../holidays/bloc/holidays_state.freezed.dart | 463 ++++++++++++++++++
 .../holidays/bloc/holidays_state.g.dart       |  43 ++
 .../dataProvider/holidays_get_holidays.dart   |  13 +
 .../repository/holidays_repository.dart       |   7 +
 .../modules/holidays/view/holidays_view.dart  | 120 +++++
 .../bloc/marianum_message_bloc.dart           |   2 +-
 .../marianum_message_get_messages.dart        |   2 +-
 .../pages/talk/components/chatBubble.dart     |   1 -
 .../talk/components/chatBubbleStyles.dart     |   2 +-
 23 files changed, 803 insertions(+), 40 deletions(-)
 create mode 100644 lib/state/app/basis/dataloader/holiday_data_loader.dart
 rename lib/state/app/{infrastructure/dataLoader => basis/dataloader}/mhsl_data_loader.dart (78%)
 create mode 100644 lib/state/app/modules/holidays/bloc/holidays_bloc.dart
 create mode 100644 lib/state/app/modules/holidays/bloc/holidays_event.dart
 create mode 100644 lib/state/app/modules/holidays/bloc/holidays_state.dart
 create mode 100644 lib/state/app/modules/holidays/bloc/holidays_state.freezed.dart
 create mode 100644 lib/state/app/modules/holidays/bloc/holidays_state.g.dart
 create mode 100644 lib/state/app/modules/holidays/dataProvider/holidays_get_holidays.dart
 create mode 100644 lib/state/app/modules/holidays/repository/holidays_repository.dart
 create mode 100644 lib/state/app/modules/holidays/view/holidays_view.dart

diff --git a/lib/api/holidays/getHolidays.dart b/lib/api/holidays/getHolidays.dart
index 1d7be0b..8ce3325 100644
--- a/lib/api/holidays/getHolidays.dart
+++ b/lib/api/holidays/getHolidays.dart
@@ -1,5 +1,4 @@
 import 'dart:convert';
-
 import 'package:http/http.dart' as http;
 
 import 'getHolidaysResponse.dart';
@@ -7,11 +6,10 @@ import 'getHolidaysResponse.dart';
 class GetHolidays {
   Future<GetHolidaysResponse> query() async {
     var response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body;
+    var data = jsonDecode(response) as List<dynamic>;
     return GetHolidaysResponse(
         List<GetHolidaysResponseObject>.from(
-            jsonDecode(response).map<GetHolidaysResponseObject>(
-                    GetHolidaysResponseObject.fromJson
-            )
+            data.map<GetHolidaysResponseObject>((e) => GetHolidaysResponseObject.fromJson(e as Map<String, dynamic>))
         )
     );
   }
diff --git a/lib/api/holidays/getHolidaysCache.dart b/lib/api/holidays/getHolidaysCache.dart
index d27a9ed..49e04e1 100644
--- a/lib/api/holidays/getHolidaysCache.dart
+++ b/lib/api/holidays/getHolidaysCache.dart
@@ -13,12 +13,11 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
   GetHolidaysResponse onLocalData(String json) {
     List<dynamic> parsedListJson = jsonDecode(json)['data'];
     return GetHolidaysResponse(
-        List<GetHolidaysResponseObject>.from(
-            parsedListJson.map<GetHolidaysResponseObject>(
-                    // ignore: unnecessary_lambdas
-                    (dynamic i) => GetHolidaysResponseObject.fromJson(i)
-            )
+      List<GetHolidaysResponseObject>.from(
+        parsedListJson.map<GetHolidaysResponseObject>(
+          (i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>)
         )
+      )
     );
   }
 
diff --git a/lib/state/app/basis/dataloader/holiday_data_loader.dart b/lib/state/app/basis/dataloader/holiday_data_loader.dart
new file mode 100644
index 0000000..19c345f
--- /dev/null
+++ b/lib/state/app/basis/dataloader/holiday_data_loader.dart
@@ -0,0 +1,9 @@
+import 'package:dio/dio.dart';
+
+import '../../infrastructure/dataLoader/data_loader.dart';
+
+abstract class HolidayDataLoader<TResult> extends DataLoader<TResult> {
+  HolidayDataLoader() : super(Dio(BaseOptions(
+    baseUrl: 'https://ferien-api.de/api/v1/',
+  )));
+}
diff --git a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart b/lib/state/app/basis/dataloader/mhsl_data_loader.dart
similarity index 78%
rename from lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart
rename to lib/state/app/basis/dataloader/mhsl_data_loader.dart
index 0825587..6b4baab 100644
--- a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart
+++ b/lib/state/app/basis/dataloader/mhsl_data_loader.dart
@@ -1,6 +1,6 @@
 import 'package:dio/dio.dart';
 
-import 'data_loader.dart';
+import '../../infrastructure/dataLoader/data_loader.dart';
 
 abstract class MhslDataLoader<TResult> extends DataLoader<TResult> {
   MhslDataLoader() : super(Dio(BaseOptions(
diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart
index cb4a3fe..64c1aa7 100644
--- a/lib/state/app/infrastructure/dataLoader/data_loader.dart
+++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart
@@ -35,8 +35,12 @@ abstract class DataLoader<TResult> {
 }
 
 class DataLoaderResult {
-  final Map<String, dynamic> json;
+  final dynamic json;
   final Map<String, String> headers;
 
+  Map<String, dynamic> asMap() => json as Map<String, dynamic>;
+  List<dynamic> asList() => json as List<dynamic>;
+  List<Map<String, dynamic>> asListOfMaps() => asList().map((e) => e as Map<String, dynamic>).toList();
+
   DataLoaderResult({required this.json, required this.headers});
 }
diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart
index ac1626a..b163e41 100644
--- a/lib/state/app/infrastructure/loadableState/loadable_state.dart
+++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart
@@ -9,11 +9,11 @@ class LoadableState<TState> with _$LoadableState {
   const LoadableState._();
 
   const factory LoadableState({
-    @Default(true) bool isLoading,
-    @Default(null) TState? data,
-    @Default(null) int? lastFetch,
-    @Default(null) void Function()? reFetch,
-    @Default(null) LoadingError? error,
+    required bool isLoading,
+    required TState? data,
+    required int? lastFetch,
+    required void Function()? reFetch,
+    required LoadingError? error,
   }) = _LoadableState;
 
   bool _hasError() => error != null;
diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart
index bc5353c..fd27b2b 100644
--- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart
+++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart
@@ -89,4 +89,3 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
     super.dispose();
   }
 }
-
diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart
index 116bad2..cdcedf7 100644
--- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart
+++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart
@@ -17,23 +17,40 @@ abstract class LoadableHydratedBloc<
     LoadableState<TState>
 > {
   late TRepository _repository;
-  LoadableHydratedBloc() : super(const LoadableState()) {
+  LoadableHydratedBloc() : super(const LoadableState(
+    error: null,
+    data: null,
+    isLoading: true,
+    lastFetch: null,
+    reFetch: null,
+  )) {
 
-    on<Emit<TState>>((event, emit) => emit(LoadableState(
-        isLoading: event.loading,
+    on<Emit<TState>>((event, emit) {
+      emit(LoadableState(
+        isLoading: state.isLoading,
         data: event.state(innerState ?? fromNothing()),
-        lastFetch: DateTime.now().millisecondsSinceEpoch,
-        reFetch: retry
+        lastFetch: state.lastFetch,
+        reFetch: retry,
+        error: state.error,
+      ));
+    });
+
+    on<DataGathered<TState>>((event, emit) => emit(LoadableState(
+      isLoading: false,
+      data: event.state(innerState ?? fromNothing()),
+      lastFetch: DateTime.now().millisecondsSinceEpoch,
+      reFetch: retry,
+      error: null,
     )));
 
     on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
         isLoading: true,
         data: innerState,
-        lastFetch: state.lastFetch
+        lastFetch: state.lastFetch,
+        reFetch: null,
+        error: null,
     )));
 
-    on<ClearState<TState>>((event, emit) => emit(const LoadableState()));
-
     on<Error<TState>>((event, emit) => emit(LoadableState(
         isLoading: false,
         data: innerState,
@@ -61,7 +78,7 @@ abstract class LoadableHydratedBloc<
       (e) {
         log('Error while fetching ${TState.toString()}: ${e.toString()}');
         add(Error(LoadingError(
-          message: e.message,
+          message: e.message ?? e.toString(),
           allowRetry: true,
         )));
       },
@@ -73,14 +90,30 @@ abstract class LoadableHydratedBloc<
   @override
   fromJson(Map<String, dynamic> json) {
     var rawData = LoadableSaveContext.unwrap(json);
-    return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data));
+    return LoadableState(
+      isLoading: true,
+      data: fromStorage(rawData.data),
+      lastFetch: rawData.meta.timestamp,
+      reFetch: null,
+      error: null,
+    );
   }
 
   @override
-  Map<String, dynamic>? toJson(LoadableState<TState> state) => LoadableSaveContext.wrap(
-      toStorage(state.data),
+  Map<String, dynamic>? toJson(LoadableState<TState> state) {
+    Map<String, dynamic>? data;
+    try {
+      data = toStorage(state.data);
+    } catch(e) {
+      log('Failed to save state ${TState.toString()}: ${e.toString()}');
+      data = null;
+    }
+
+    return LoadableSaveContext.wrap(
+      data,
       state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
-  );
+    );
+  }
 
   Future<void> gatherData();
   TRepository repository();
diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart
index 06462de..55b8e6a 100644
--- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart
+++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart
@@ -3,10 +3,12 @@ import '../../loadableState/loading_error.dart';
 class LoadableHydratedBlocEvent<TState> {}
 class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
   final TState Function(TState state) state;
-  final bool loading;
-  Emit(this.state, {this.loading = false});
+  Emit(this.state);
+}
+class DataGathered<TState> extends LoadableHydratedBlocEvent<TState> {
+  final TState Function(TState state) state;
+  DataGathered(this.state);
 }
-class ClearState<TState> extends LoadableHydratedBlocEvent<TState> {}
 class Error<TState> extends LoadableHydratedBlocEvent<TState> {
   final LoadingError error;
   Error(this.error);
diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart
index e4d68d5..0dea5cd 100644
--- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart
+++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart
@@ -21,4 +21,3 @@ class LoadableSaveContext with _$LoadableSaveContext {
   static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
       (data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey]));
 }
-
diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart
index 83b93b5..5e53b32 100644
--- a/lib/state/app/modules/app_modules.dart
+++ b/lib/state/app/modules/app_modules.dart
@@ -4,12 +4,12 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
 import '../../../model/breakers/Breaker.dart';
 import '../../../view/pages/files/files.dart';
-import '../../../view/pages/more/holidays/holidays.dart';
 import '../../../view/pages/more/roomplan/roomplan.dart';
 import '../../../view/pages/talk/chatList.dart';
 import '../../../view/pages/timetable/timetable.dart';
 import '../../../widget/centeredLeading.dart';
 import 'gradeAverages/view/grade_averages_view.dart';
+import 'holidays/view/holidays_view.dart';
 import 'marianumMessage/view/marianum_message_list_view.dart';
 
 class AppModule {
@@ -26,7 +26,7 @@ class AppModule {
     Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new),
     Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new),
     Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new),
-    Modules.holidays: AppModule('Schulferien', Icons.holiday_village, Holidays.new),
+    Modules.holidays: AppModule('Schulferien', Icons.holiday_village, HolidaysView.new),
   };
 
   static AppModule getModule(Modules module) => modules()[module]!;
diff --git a/lib/state/app/modules/holidays/bloc/holidays_bloc.dart b/lib/state/app/modules/holidays/bloc/holidays_bloc.dart
new file mode 100644
index 0000000..cd369f0
--- /dev/null
+++ b/lib/state/app/modules/holidays/bloc/holidays_bloc.dart
@@ -0,0 +1,36 @@
+import 'dart:developer';
+
+import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
+import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
+import '../repository/holidays_repository.dart';
+import 'holidays_event.dart';
+import 'holidays_state.dart';
+
+class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
+  HolidaysBloc() {
+    on<SetPastHolidaysVisible>((event, emit) {
+      log('set pastholidays: ${event.shouldBeVisible.toString()}');
+      add(Emit((state) => state.copyWith(showPastHolidays: state.showPastHolidays)));
+    });
+
+    on<DisclaimerDismissed>((event, emit) => add(
+      Emit((state) => state.copyWith(showDisclaimer: false))
+    ));
+  }
+
+  bool showPastHolidays() => innerState?.showPastHolidays ?? false;
+
+  @override
+  fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true);
+  @override
+  fromStorage(Map<String, dynamic> json) => HolidaysState.fromJson(json);
+  @override
+  Future<void> gatherData() async {
+    var holidays = await repo.getHolidays();
+    add(DataGathered((state) => state.copyWith(holidays: holidays)));
+  }
+  @override
+  repository() => HolidaysRepository();
+  @override
+  Map<String, dynamic>? toStorage(state) => state.toJson();
+}
diff --git a/lib/state/app/modules/holidays/bloc/holidays_event.dart b/lib/state/app/modules/holidays/bloc/holidays_event.dart
new file mode 100644
index 0000000..8565250
--- /dev/null
+++ b/lib/state/app/modules/holidays/bloc/holidays_event.dart
@@ -0,0 +1,9 @@
+import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
+import 'holidays_state.dart';
+
+sealed class HolidaysEvent extends LoadableHydratedBlocEvent<HolidaysState> {}
+class SetPastHolidaysVisible extends HolidaysEvent {
+  final bool shouldBeVisible;
+  SetPastHolidaysVisible(this.shouldBeVisible);
+}
+class DisclaimerDismissed extends HolidaysEvent {}
diff --git a/lib/state/app/modules/holidays/bloc/holidays_state.dart b/lib/state/app/modules/holidays/bloc/holidays_state.dart
new file mode 100644
index 0000000..05c0cd2
--- /dev/null
+++ b/lib/state/app/modules/holidays/bloc/holidays_state.dart
@@ -0,0 +1,30 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter/foundation.dart';
+
+part 'holidays_state.freezed.dart';
+part 'holidays_state.g.dart';
+
+@freezed
+class HolidaysState with _$HolidaysState {
+  const factory HolidaysState({
+    required bool showPastHolidays,
+    required bool showDisclaimer,
+    required List<Holiday> holidays,
+  }) = _HolidaysState;
+
+  factory HolidaysState.fromJson(Map<String, Object?> json) => _$HolidaysStateFromJson(json);
+}
+
+@freezed
+class Holiday with _$Holiday {
+  const factory Holiday({
+    required String start,
+    required String end,
+    required int year,
+    required String stateCode,
+    required String name,
+    required String slug,
+  }) = _Holiday;
+
+  factory Holiday.fromJson(Map<String, Object?> json) => _$HolidayFromJson(json);
+}
diff --git a/lib/state/app/modules/holidays/bloc/holidays_state.freezed.dart b/lib/state/app/modules/holidays/bloc/holidays_state.freezed.dart
new file mode 100644
index 0000000..6659f45
--- /dev/null
+++ b/lib/state/app/modules/holidays/bloc/holidays_state.freezed.dart
@@ -0,0 +1,463 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'holidays_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+HolidaysState _$HolidaysStateFromJson(Map<String, dynamic> json) {
+  return _HolidaysState.fromJson(json);
+}
+
+/// @nodoc
+mixin _$HolidaysState {
+  bool get showPastHolidays => throw _privateConstructorUsedError;
+  bool get showDisclaimer => throw _privateConstructorUsedError;
+  List<Holiday> get holidays => throw _privateConstructorUsedError;
+
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $HolidaysStateCopyWith<HolidaysState> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $HolidaysStateCopyWith<$Res> {
+  factory $HolidaysStateCopyWith(
+          HolidaysState value, $Res Function(HolidaysState) then) =
+      _$HolidaysStateCopyWithImpl<$Res, HolidaysState>;
+  @useResult
+  $Res call(
+      {bool showPastHolidays, bool showDisclaimer, List<Holiday> holidays});
+}
+
+/// @nodoc
+class _$HolidaysStateCopyWithImpl<$Res, $Val extends HolidaysState>
+    implements $HolidaysStateCopyWith<$Res> {
+  _$HolidaysStateCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? showPastHolidays = null,
+    Object? showDisclaimer = null,
+    Object? holidays = null,
+  }) {
+    return _then(_value.copyWith(
+      showPastHolidays: null == showPastHolidays
+          ? _value.showPastHolidays
+          : showPastHolidays // ignore: cast_nullable_to_non_nullable
+              as bool,
+      showDisclaimer: null == showDisclaimer
+          ? _value.showDisclaimer
+          : showDisclaimer // ignore: cast_nullable_to_non_nullable
+              as bool,
+      holidays: null == holidays
+          ? _value.holidays
+          : holidays // ignore: cast_nullable_to_non_nullable
+              as List<Holiday>,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$HolidaysStateImplCopyWith<$Res>
+    implements $HolidaysStateCopyWith<$Res> {
+  factory _$$HolidaysStateImplCopyWith(
+          _$HolidaysStateImpl value, $Res Function(_$HolidaysStateImpl) then) =
+      __$$HolidaysStateImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {bool showPastHolidays, bool showDisclaimer, List<Holiday> holidays});
+}
+
+/// @nodoc
+class __$$HolidaysStateImplCopyWithImpl<$Res>
+    extends _$HolidaysStateCopyWithImpl<$Res, _$HolidaysStateImpl>
+    implements _$$HolidaysStateImplCopyWith<$Res> {
+  __$$HolidaysStateImplCopyWithImpl(
+      _$HolidaysStateImpl _value, $Res Function(_$HolidaysStateImpl) _then)
+      : super(_value, _then);
+
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? showPastHolidays = null,
+    Object? showDisclaimer = null,
+    Object? holidays = null,
+  }) {
+    return _then(_$HolidaysStateImpl(
+      showPastHolidays: null == showPastHolidays
+          ? _value.showPastHolidays
+          : showPastHolidays // ignore: cast_nullable_to_non_nullable
+              as bool,
+      showDisclaimer: null == showDisclaimer
+          ? _value.showDisclaimer
+          : showDisclaimer // ignore: cast_nullable_to_non_nullable
+              as bool,
+      holidays: null == holidays
+          ? _value._holidays
+          : holidays // ignore: cast_nullable_to_non_nullable
+              as List<Holiday>,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$HolidaysStateImpl
+    with DiagnosticableTreeMixin
+    implements _HolidaysState {
+  const _$HolidaysStateImpl(
+      {required this.showPastHolidays,
+      required this.showDisclaimer,
+      required final List<Holiday> holidays})
+      : _holidays = holidays;
+
+  factory _$HolidaysStateImpl.fromJson(Map<String, dynamic> json) =>
+      _$$HolidaysStateImplFromJson(json);
+
+  @override
+  final bool showPastHolidays;
+  @override
+  final bool showDisclaimer;
+  final List<Holiday> _holidays;
+  @override
+  List<Holiday> get holidays {
+    if (_holidays is EqualUnmodifiableListView) return _holidays;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(_holidays);
+  }
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'HolidaysState(showPastHolidays: $showPastHolidays, showDisclaimer: $showDisclaimer, holidays: $holidays)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'HolidaysState'))
+      ..add(DiagnosticsProperty('showPastHolidays', showPastHolidays))
+      ..add(DiagnosticsProperty('showDisclaimer', showDisclaimer))
+      ..add(DiagnosticsProperty('holidays', holidays));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$HolidaysStateImpl &&
+            (identical(other.showPastHolidays, showPastHolidays) ||
+                other.showPastHolidays == showPastHolidays) &&
+            (identical(other.showDisclaimer, showDisclaimer) ||
+                other.showDisclaimer == showDisclaimer) &&
+            const DeepCollectionEquality().equals(other._holidays, _holidays));
+  }
+
+  @JsonKey(ignore: true)
+  @override
+  int get hashCode => Object.hash(runtimeType, showPastHolidays, showDisclaimer,
+      const DeepCollectionEquality().hash(_holidays));
+
+  @JsonKey(ignore: true)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
+      __$$HolidaysStateImplCopyWithImpl<_$HolidaysStateImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$HolidaysStateImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _HolidaysState implements HolidaysState {
+  const factory _HolidaysState(
+      {required final bool showPastHolidays,
+      required final bool showDisclaimer,
+      required final List<Holiday> holidays}) = _$HolidaysStateImpl;
+
+  factory _HolidaysState.fromJson(Map<String, dynamic> json) =
+      _$HolidaysStateImpl.fromJson;
+
+  @override
+  bool get showPastHolidays;
+  @override
+  bool get showDisclaimer;
+  @override
+  List<Holiday> get holidays;
+  @override
+  @JsonKey(ignore: true)
+  _$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+Holiday _$HolidayFromJson(Map<String, dynamic> json) {
+  return _Holiday.fromJson(json);
+}
+
+/// @nodoc
+mixin _$Holiday {
+  String get start => throw _privateConstructorUsedError;
+  String get end => throw _privateConstructorUsedError;
+  int get year => throw _privateConstructorUsedError;
+  String get stateCode => throw _privateConstructorUsedError;
+  String get name => throw _privateConstructorUsedError;
+  String get slug => throw _privateConstructorUsedError;
+
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $HolidayCopyWith<Holiday> get copyWith => throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $HolidayCopyWith<$Res> {
+  factory $HolidayCopyWith(Holiday value, $Res Function(Holiday) then) =
+      _$HolidayCopyWithImpl<$Res, Holiday>;
+  @useResult
+  $Res call(
+      {String start,
+      String end,
+      int year,
+      String stateCode,
+      String name,
+      String slug});
+}
+
+/// @nodoc
+class _$HolidayCopyWithImpl<$Res, $Val extends Holiday>
+    implements $HolidayCopyWith<$Res> {
+  _$HolidayCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? start = null,
+    Object? end = null,
+    Object? year = null,
+    Object? stateCode = null,
+    Object? name = null,
+    Object? slug = null,
+  }) {
+    return _then(_value.copyWith(
+      start: null == start
+          ? _value.start
+          : start // ignore: cast_nullable_to_non_nullable
+              as String,
+      end: null == end
+          ? _value.end
+          : end // ignore: cast_nullable_to_non_nullable
+              as String,
+      year: null == year
+          ? _value.year
+          : year // ignore: cast_nullable_to_non_nullable
+              as int,
+      stateCode: null == stateCode
+          ? _value.stateCode
+          : stateCode // ignore: cast_nullable_to_non_nullable
+              as String,
+      name: null == name
+          ? _value.name
+          : name // ignore: cast_nullable_to_non_nullable
+              as String,
+      slug: null == slug
+          ? _value.slug
+          : slug // ignore: cast_nullable_to_non_nullable
+              as String,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$HolidayImplCopyWith<$Res> implements $HolidayCopyWith<$Res> {
+  factory _$$HolidayImplCopyWith(
+          _$HolidayImpl value, $Res Function(_$HolidayImpl) then) =
+      __$$HolidayImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {String start,
+      String end,
+      int year,
+      String stateCode,
+      String name,
+      String slug});
+}
+
+/// @nodoc
+class __$$HolidayImplCopyWithImpl<$Res>
+    extends _$HolidayCopyWithImpl<$Res, _$HolidayImpl>
+    implements _$$HolidayImplCopyWith<$Res> {
+  __$$HolidayImplCopyWithImpl(
+      _$HolidayImpl _value, $Res Function(_$HolidayImpl) _then)
+      : super(_value, _then);
+
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? start = null,
+    Object? end = null,
+    Object? year = null,
+    Object? stateCode = null,
+    Object? name = null,
+    Object? slug = null,
+  }) {
+    return _then(_$HolidayImpl(
+      start: null == start
+          ? _value.start
+          : start // ignore: cast_nullable_to_non_nullable
+              as String,
+      end: null == end
+          ? _value.end
+          : end // ignore: cast_nullable_to_non_nullable
+              as String,
+      year: null == year
+          ? _value.year
+          : year // ignore: cast_nullable_to_non_nullable
+              as int,
+      stateCode: null == stateCode
+          ? _value.stateCode
+          : stateCode // ignore: cast_nullable_to_non_nullable
+              as String,
+      name: null == name
+          ? _value.name
+          : name // ignore: cast_nullable_to_non_nullable
+              as String,
+      slug: null == slug
+          ? _value.slug
+          : slug // ignore: cast_nullable_to_non_nullable
+              as String,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$HolidayImpl with DiagnosticableTreeMixin implements _Holiday {
+  const _$HolidayImpl(
+      {required this.start,
+      required this.end,
+      required this.year,
+      required this.stateCode,
+      required this.name,
+      required this.slug});
+
+  factory _$HolidayImpl.fromJson(Map<String, dynamic> json) =>
+      _$$HolidayImplFromJson(json);
+
+  @override
+  final String start;
+  @override
+  final String end;
+  @override
+  final int year;
+  @override
+  final String stateCode;
+  @override
+  final String name;
+  @override
+  final String slug;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'Holiday(start: $start, end: $end, year: $year, stateCode: $stateCode, name: $name, slug: $slug)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'Holiday'))
+      ..add(DiagnosticsProperty('start', start))
+      ..add(DiagnosticsProperty('end', end))
+      ..add(DiagnosticsProperty('year', year))
+      ..add(DiagnosticsProperty('stateCode', stateCode))
+      ..add(DiagnosticsProperty('name', name))
+      ..add(DiagnosticsProperty('slug', slug));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$HolidayImpl &&
+            (identical(other.start, start) || other.start == start) &&
+            (identical(other.end, end) || other.end == end) &&
+            (identical(other.year, year) || other.year == year) &&
+            (identical(other.stateCode, stateCode) ||
+                other.stateCode == stateCode) &&
+            (identical(other.name, name) || other.name == name) &&
+            (identical(other.slug, slug) || other.slug == slug));
+  }
+
+  @JsonKey(ignore: true)
+  @override
+  int get hashCode =>
+      Object.hash(runtimeType, start, end, year, stateCode, name, slug);
+
+  @JsonKey(ignore: true)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
+      __$$HolidayImplCopyWithImpl<_$HolidayImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$HolidayImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _Holiday implements Holiday {
+  const factory _Holiday(
+      {required final String start,
+      required final String end,
+      required final int year,
+      required final String stateCode,
+      required final String name,
+      required final String slug}) = _$HolidayImpl;
+
+  factory _Holiday.fromJson(Map<String, dynamic> json) = _$HolidayImpl.fromJson;
+
+  @override
+  String get start;
+  @override
+  String get end;
+  @override
+  int get year;
+  @override
+  String get stateCode;
+  @override
+  String get name;
+  @override
+  String get slug;
+  @override
+  @JsonKey(ignore: true)
+  _$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
diff --git a/lib/state/app/modules/holidays/bloc/holidays_state.g.dart b/lib/state/app/modules/holidays/bloc/holidays_state.g.dart
new file mode 100644
index 0000000..1d0f3f0
--- /dev/null
+++ b/lib/state/app/modules/holidays/bloc/holidays_state.g.dart
@@ -0,0 +1,43 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'holidays_state.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$HolidaysStateImpl _$$HolidaysStateImplFromJson(Map<String, dynamic> json) =>
+    _$HolidaysStateImpl(
+      showPastHolidays: json['showPastHolidays'] as bool,
+      showDisclaimer: json['showDisclaimer'] as bool,
+      holidays: (json['holidays'] as List<dynamic>)
+          .map((e) => Holiday.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$$HolidaysStateImplToJson(_$HolidaysStateImpl instance) =>
+    <String, dynamic>{
+      'showPastHolidays': instance.showPastHolidays,
+      'showDisclaimer': instance.showDisclaimer,
+      'holidays': instance.holidays,
+    };
+
+_$HolidayImpl _$$HolidayImplFromJson(Map<String, dynamic> json) =>
+    _$HolidayImpl(
+      start: json['start'] as String,
+      end: json['end'] as String,
+      year: json['year'] as int,
+      stateCode: json['stateCode'] as String,
+      name: json['name'] as String,
+      slug: json['slug'] as String,
+    );
+
+Map<String, dynamic> _$$HolidayImplToJson(_$HolidayImpl instance) =>
+    <String, dynamic>{
+      'start': instance.start,
+      'end': instance.end,
+      'year': instance.year,
+      'stateCode': instance.stateCode,
+      'name': instance.name,
+      'slug': instance.slug,
+    };
diff --git a/lib/state/app/modules/holidays/dataProvider/holidays_get_holidays.dart b/lib/state/app/modules/holidays/dataProvider/holidays_get_holidays.dart
new file mode 100644
index 0000000..cd52128
--- /dev/null
+++ b/lib/state/app/modules/holidays/dataProvider/holidays_get_holidays.dart
@@ -0,0 +1,13 @@
+import 'package:dio/dio.dart';
+
+import '../../../basis/dataloader/holiday_data_loader.dart';
+import '../../../infrastructure/dataLoader/data_loader.dart';
+import '../bloc/holidays_state.dart';
+
+class HolidaysGetHolidays extends HolidayDataLoader<List<Holiday>> {
+  @override
+  List<Holiday> assemble(DataLoaderResult data) => data.asListOfMaps().map(Holiday.fromJson).toList();
+
+  @override
+  Future<Response<String>> fetch() => dio.get('/holidays/HE');
+}
diff --git a/lib/state/app/modules/holidays/repository/holidays_repository.dart b/lib/state/app/modules/holidays/repository/holidays_repository.dart
new file mode 100644
index 0000000..72ec949
--- /dev/null
+++ b/lib/state/app/modules/holidays/repository/holidays_repository.dart
@@ -0,0 +1,7 @@
+import '../../../infrastructure/repository/repository.dart';
+import '../bloc/holidays_state.dart';
+import '../dataProvider/holidays_get_holidays.dart';
+
+class HolidaysRepository extends Repository<HolidaysState> {
+  Future<List<Holiday>> getHolidays() => HolidaysGetHolidays().run();
+}
diff --git a/lib/state/app/modules/holidays/view/holidays_view.dart b/lib/state/app/modules/holidays/view/holidays_view.dart
new file mode 100644
index 0000000..3b40f4e
--- /dev/null
+++ b/lib/state/app/modules/holidays/view/holidays_view.dart
@@ -0,0 +1,120 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:jiffy/jiffy.dart';
+
+import '../../../../../view/pages/more/holidays/holidays.dart';
+import '../../../../../widget/animatedTime.dart';
+import '../../../../../widget/centeredLeading.dart';
+import '../../../../../widget/debug/debugTile.dart';
+import '../../../infrastructure/loadableState/loadable_state.dart';
+import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
+import '../../../infrastructure/utilityWidgets/bloc_module.dart';
+import '../bloc/holidays_bloc.dart';
+import '../bloc/holidays_event.dart';
+import '../bloc/holidays_state.dart';
+
+class HolidaysView extends StatelessWidget {
+  const HolidaysView({super.key});
+
+  @override
+  Widget build(BuildContext context) => BlocModule<HolidaysBloc, LoadableState<HolidaysState>>(
+    create: (contet) => HolidaysBloc(),
+    autoRebuild: true,
+    child: (context, bloc, state) {
+      log(state.toString());
+      void showDisclaimer() {
+        showDialog(context: context, builder: (context) => AlertDialog(
+          title: const Text('Richtigkeit und Bereitstellung der Daten'),
+          content: const Text(''
+            'Sämtliche Datumsangaben sind ohne Gewähr.\n'
+            'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
+            'Die Daten stammen von https://ferien-api.de/'),
+          actions: [
+            TextButton(child: const Text('Okay'), onPressed: () {
+              bloc.add(DisclaimerDismissed());
+              Navigator.of(context).pop();
+            }),
+          ],
+        ));
+      }
+      
+      return Scaffold(
+        appBar: AppBar(
+          title: const Text('Schulferien in Hessen'),
+          actions: [
+            IconButton(
+              icon: const Icon(Icons.warning_amber_outlined),
+              onPressed: showDisclaimer,
+            ),
+            PopupMenuButton<bool>(
+              initialValue: bloc.showPastHolidays(),
+              icon: const Icon(Icons.manage_history_outlined),
+              itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
+                  value: e,
+                  enabled: e != bloc.showPastHolidays(),
+                  child: Row(
+                    children: [
+                      Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
+                      const SizedBox(width: 15),
+                      Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
+                    ],
+                  )
+              )).toList(),
+              onSelected: (e) => bloc.add(SetPastHolidaysVisible(e)),
+            ),
+          ],
+        ),
+        body: LoadableStateConsumer<HolidaysBloc, HolidaysState>(
+          child: (state, loading) => ListView.builder(
+            itemCount: state.holidays.length,
+            itemBuilder: (context, index) {
+              var holiday = state.holidays[index];
+              var holidayType = holiday.name.split(' ').first.capitalize();
+
+              String formatDate(String enDate) => Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
+              
+              return ListTile(
+                leading: const CenteredLeading(Icon(Icons.calendar_month)),
+                title: Text('$holidayType ab ${formatDate(holiday.start)}'),
+                subtitle: Text('bis ${formatDate(holiday.end)}'),
+                onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
+                  title: Text('$holidayType ${holiday.year} in Hessen'),
+                  children: [
+                    ListTile(
+                      leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
+                      title: Text(holiday.name),
+                      subtitle: Text(holiday.slug),
+                    ),
+                    ListTile(
+                      leading: const Icon(Icons.arrow_forward),
+                      title: Text('vom ${formatDate(holiday.start)}'),
+                    ),
+                    ListTile(
+                      leading: const Icon(Icons.arrow_back),
+                      title: Text('bis zum ${formatDate(holiday.end)}'),
+                    ),
+                    Visibility(
+                      visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
+                      replacement: ListTile(
+                        leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
+                        title: Text(Jiffy.parse(holiday.start).fromNow()),
+                      ),
+                      child: ListTile(
+                        leading: const CenteredLeading(Icon(Icons.timer_outlined)),
+                        title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())),
+                        subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
+                      ),
+                    ),
+                    DebugTile(context).jsonData(holiday.toJson()),
+                  ],
+                )),
+                trailing: const Icon(Icons.arrow_right),
+              );
+            },
+          ),
+        ),
+      );
+    },
+  );
+}
diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart
index 7876351..97c3b95 100644
--- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart
+++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart
@@ -8,7 +8,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, Mar
   @override
   Future<void> gatherData() async {
     var messages = await repo.getMessages();
-    add(Emit((state) => state.copyWith(messageList: messages)));
+    add(DataGathered((state) => state.copyWith(messageList: messages)));
   }
 
   @override
diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart
index cb7f9db..f8c4b24 100644
--- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart
+++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart
@@ -1,7 +1,7 @@
 import 'package:dio/dio.dart';
 
 import '../../../infrastructure/dataLoader/data_loader.dart';
-import '../../../infrastructure/dataLoader/mhsl_data_loader.dart';
+import '../../../basis/dataloader/mhsl_data_loader.dart';
 import '../bloc/marianum_message_state.dart';
 
 class MarianumMessageGetMessages extends MhslDataLoader<MarianumMessageList> {
diff --git a/lib/view/pages/talk/components/chatBubble.dart b/lib/view/pages/talk/components/chatBubble.dart
index 8110c1c..0539f19 100644
--- a/lib/view/pages/talk/components/chatBubble.dart
+++ b/lib/view/pages/talk/components/chatBubble.dart
@@ -2,7 +2,6 @@ import 'package:better_open_file/better_open_file.dart';
 import 'package:bubble/bubble.dart';
 import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
 import 'package:flowder/flowder.dart';
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
diff --git a/lib/view/pages/talk/components/chatBubbleStyles.dart b/lib/view/pages/talk/components/chatBubbleStyles.dart
index c640ca2..5cf527a 100644
--- a/lib/view/pages/talk/components/chatBubbleStyles.dart
+++ b/lib/view/pages/talk/components/chatBubbleStyles.dart
@@ -51,4 +51,4 @@ class ChatBubbleStyles {
       alignment: Alignment.topRight,
     );
   }
-}
\ No newline at end of file
+}