diff --git a/.github/labeler.yml b/.github/labeler.yml index d49d9b8..dc35649 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,7 +29,7 @@ repositories: - any: ['lib/services/repositories/**/*'] interceptors: - - any: ['lib/services/interceptors/**/*'] + - any: ['lib/services/networking/interceptors/**/*'] api: - any: ['lib/services/networking/**/*api*'] diff --git a/lib/helper/utils/constants.dart b/lib/helper/utils/constants.dart index 39ee20c..ed7ec33 100644 --- a/lib/helper/utils/constants.dart +++ b/lib/helper/utils/constants.dart @@ -87,6 +87,9 @@ class Constants { /// The color value for dark grey [CustomDialog] in the app. static const Color barrierColor = Colors.black87; + /// The color value for light grey [CustomDialog] in the app. + static const Color barrierColorLight = Color(0xBF000000); + /// The TextStyle for Lato font in the app. static TextStyle latoFont = GoogleFonts.lato().copyWith(color: Colors.black); diff --git a/lib/helper/utils/custom_theme.dart b/lib/helper/utils/custom_theme.dart index a6ec97e..8a6d69c 100644 --- a/lib/helper/utils/custom_theme.dart +++ b/lib/helper/utils/custom_theme.dart @@ -18,6 +18,7 @@ class CustomTheme { /// * textButtonTheme: [TextButtonTheme] without the default padding, static late final mainTheme = ThemeData( primaryColor: Constants.primaryColor, + accentColor: Constants.primaryColor, scaffoldBackgroundColor: Constants.scaffoldColor, fontFamily: Constants.poppinsFont.fontFamily, textTheme: TextTheme( diff --git a/lib/models/booking_model.dart b/lib/models/booking_model.dart index 93bd51c..40a4c86 100644 --- a/lib/models/booking_model.dart +++ b/lib/models/booking_model.dart @@ -15,7 +15,7 @@ class BookingModel with _$BookingModel { const factory BookingModel({ @JsonKey(toJson: Constants.toNull, includeIfNull: false) required int? bookingId, @JsonKey(includeIfNull: false) required int? userId, - required int showId, + @JsonKey(includeIfNull: false) required int? showId, @JsonKey(toJson: Constants.toNull, includeIfNull: false) String? seatRow, @JsonKey(toJson: Constants.toNull, includeIfNull: false) int? seatNumber, required double price, diff --git a/lib/models/user_booking_model.dart b/lib/models/user_booking_model.dart index 0cee930..111e847 100644 --- a/lib/models/user_booking_model.dart +++ b/lib/models/user_booking_model.dart @@ -1,6 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'booking_model.dart'; +import 'user_booking_show_model.dart'; part 'user_booking_model.freezed.dart'; part 'user_booking_model.g.dart'; @@ -9,9 +10,9 @@ part 'user_booking_model.g.dart'; class UserBookingModel with _$UserBookingModel { @JsonSerializable() const factory UserBookingModel({ - required int showId, required String title, required String posterUrl, + required UserBookingShowModel show, required List bookings, }) = _UserBookingModel; diff --git a/lib/models/user_booking_show_model.dart b/lib/models/user_booking_show_model.dart new file mode 100644 index 0000000..4a84cba --- /dev/null +++ b/lib/models/user_booking_show_model.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../enums/show_type_enum.dart'; + +part 'user_booking_show_model.freezed.dart'; +part 'user_booking_show_model.g.dart'; + +@freezed +class UserBookingShowModel with _$UserBookingShowModel { + @JsonSerializable(fieldRename: FieldRename.snake) + const factory UserBookingShowModel({ + required int showId, + required ShowType showType, + required DateTime showDatetime, + }) = _UserBookingShowModel; + + factory UserBookingShowModel.fromJson(Map json) => _$UserBookingShowModelFromJson(json); +} diff --git a/lib/providers/bookings_provider.dart b/lib/providers/bookings_provider.dart index a801804..c08ac3d 100644 --- a/lib/providers/bookings_provider.dart +++ b/lib/providers/bookings_provider.dart @@ -18,6 +18,12 @@ import '../services/repositories/bookings_repository.dart'; import 'all_providers.dart'; import 'shows_provider.dart'; +final userBookingsProvider = FutureProvider.autoDispose((ref) async { + final _userId = ref.watch(authProvider.notifier).currentUserId; + final _bookingsProvider = ref.watch(bookingsProvider); + return await _bookingsProvider.getUserBookings(userId: _userId); +}); + class BookingsProvider { final BookingsRepository _bookingsRepository; final Reader _reader; diff --git a/lib/providers/shows_provider.dart b/lib/providers/shows_provider.dart index 2e095bb..338b70e 100644 --- a/lib/providers/shows_provider.dart +++ b/lib/providers/shows_provider.dart @@ -1,22 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -//Services -import '../services/repositories/shows_repository.dart'; - //Enums import '../enums/show_status_enum.dart'; import '../enums/show_type_enum.dart'; +import '../models/show_model.dart'; //Models import '../models/show_time_model.dart'; -import '../models/show_model.dart'; + +//Services +import '../services/repositories/shows_repository.dart'; //Providers import 'all_providers.dart'; import 'movies_provider.dart'; -final showsFutureProvider = FutureProvider>( - (ref) async { +final showsFutureProvider = FutureProvider.autoDispose>( + (ref) async { final _movieId = ref.watch(selectedMovieProvider).state.movieId; final _showsProvider = ref.watch(showsProvider); final _showDates = await _showsProvider.getAllShows(movieId: _movieId!); @@ -24,15 +24,15 @@ final showsFutureProvider = FutureProvider>( }, ); -final selectedShowProvider = StateProvider((ref) { +final selectedShowProvider = StateProvider.autoDispose((ref) { return ref.watch(showsFutureProvider).maybeWhen( - data: (shows) => shows[0], - orElse: () => ShowModel.initial(), - ); + data: (shows) => shows[0], + orElse: () => ShowModel.initial(), + ); }); -final selectedShowTimeProvider = StateProvider( - (ref) { +final selectedShowTimeProvider = StateProvider.autoDispose( + (ref) { final _selectedShow = ref.watch(selectedShowProvider).state; if (_selectedShow.showTimes.isEmpty) return ShowTimeModel.initial(); return _selectedShow.showTimes[0]; @@ -88,11 +88,7 @@ class ShowsProvider { theaterId: theaterId, ); - final show = ShowModel( - date: date, - movieId: movieId, - showTimes: [showTime] - ); + final show = ShowModel(date: date, movieId: movieId, showTimes: [showTime]); return show; } diff --git a/lib/providers/theaters_provider.dart b/lib/providers/theaters_provider.dart index d3832cb..505b148 100644 --- a/lib/providers/theaters_provider.dart +++ b/lib/providers/theaters_provider.dart @@ -19,19 +19,23 @@ import 'shows_provider.dart'; final selectedTheaterNameProvider = StateProvider((_) => ""); -final showSeatingFuture = FutureProvider((ref) async { +/// Does not use `ref.maintainState = true` bcz we wanted to load theater seats +/// everytime because it can receive frequent updates. +final showSeatingFuture = FutureProvider.autoDispose((ref) async { final _selectedShowTime = ref.watch(selectedShowTimeProvider).state; - - final _theatersProvider = ref.read(theatersProvider); final _theaterId = _selectedShowTime.theaterId; + final _showId = _selectedShowTime.showId; + + /// For any provider that can notify listeners, watch it's notifier instead + /// of state to prevent rebuilds upon listener's notifications. + final _theatersProvider = ref.watch(theatersProvider.notifier); final theater = await _theatersProvider.getTheaterById(theaterId: _theaterId); final _bookingsProvider = ref.watch(bookingsProvider); - final _showId = _selectedShowTime.showId; final bookedSeats = await _bookingsProvider.getShowBookedSeats(showId: _showId); - ref.read(selectedTheaterNameProvider).state = theater.theaterName; + ref.watch(selectedTheaterNameProvider.notifier).state = theater.theaterName; return ShowSeatingModel( showTime: _selectedShowTime, @@ -60,7 +64,6 @@ class TheatersProvider extends ChangeNotifier { } else { _selectedSeats.remove(seat); } - print(_selectedSeats); notifyListeners(); } diff --git a/lib/routes/app_router.dart b/lib/routes/app_router.dart index ef3d6be..3b3b74c 100644 --- a/lib/routes/app_router.dart +++ b/lib/routes/app_router.dart @@ -11,6 +11,7 @@ import '../views/screens/theater_screen.dart'; import '../views/screens/ticket_summary_screen.dart'; import '../views/screens/payment_screen.dart'; import '../views/screens/confirmation_screen.dart'; +import '../views/screens/user_bookings_screen.dart'; @MaterialAutoRouter( routes: [ @@ -25,6 +26,7 @@ import '../views/screens/confirmation_screen.dart'; AutoRoute(page: TicketSummaryScreen), AutoRoute(page: PaymentScreen), AutoRoute(page: ConfirmationScreen), + AutoRoute(page: UserBookingsScreen), ], ) class $AppRouter{} diff --git a/lib/services/local_storage/prefs_base.dart b/lib/services/local_storage/prefs_base.dart index 9275350..c61f56e 100644 --- a/lib/services/local_storage/prefs_base.dart +++ b/lib/services/local_storage/prefs_base.dart @@ -1,6 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; -///Internal class for shared preferences methods +///Base class for shared preferences methods ///This class provides low level preferences methods class PrefsBase{ ///Instance of shared preferences diff --git a/lib/services/local_storage/prefs_service.dart b/lib/services/local_storage/prefs_service.dart index 65c4e27..a657384 100644 --- a/lib/services/local_storage/prefs_service.dart +++ b/lib/services/local_storage/prefs_service.dart @@ -9,57 +9,67 @@ import '../../models/user_model.dart'; //states import '../../providers/states/auth_state.dart'; +/// A service class for providing methods to store and retrieve data from +/// shared preferences. class PrefsService { - final authTokenKey = "authToken"; - final authStateKey = "authStateKey"; - final authPasswordKey = "authPasswordKey"; - final authUserKey = "authUserKey"; + + /// The name of auth token key + static const _authTokenKey = "authToken"; + + /// The name of auth state key + static const _authStateKey = "authStateKey"; + + /// The name of user password key + static const _authPasswordKey = "authPasswordKey"; + + /// The name of user model key + static const _authUserKey = "authUserKey"; ///Instance of prefs class final _prefs = PrefsBase.instance; ///Returns logged in user password String getAuthPassword() { - return _prefs.get(authPasswordKey) ?? ''; + return _prefs.get(_authPasswordKey) ?? ''; } ///Returns last authentication status bool getAuthState() { - return _prefs.get(authStateKey) ?? false; + return _prefs.get(_authStateKey) ?? false; } ///Returns last authenticated user UserModel? getAuthUser() { - final user = _prefs.get(authUserKey); + final user = _prefs.get(_authUserKey); if(user == null) return null; return UserModel.fromJson(jsonDecode(user)); } ///Returns last authentication token String getAuthToken() { - return _prefs.get(authTokenKey) ?? ''; + return _prefs.get(_authTokenKey) ?? ''; } ///Sets the authentication password to this value void setAuthPassword(String password) { - _prefs.set(authPasswordKey, password); + _prefs.set(_authPasswordKey, password); } ///Sets the authentication status to this value void setAuthState(AuthState authState) { if(authState is AUTHENTICATED) { - _prefs.set(authStateKey, true); + _prefs.set(_authStateKey, true); } } ///Sets the authenticated user to this value void setAuthUser(UserModel user) { - _prefs.set(authUserKey, jsonEncode(user.toJson())); + _prefs.set(_authUserKey, jsonEncode(user.toJson())); } ///Sets the authentication token to this value void setAuthToken(String token) { - _prefs.set(authTokenKey, token); + _prefs.set(_authTokenKey, token); } ///Resets the authentication diff --git a/lib/services/networking/interceptors/api_interceptor.dart b/lib/services/networking/interceptors/api_interceptor.dart index 0f11961..d7f95bd 100644 --- a/lib/services/networking/interceptors/api_interceptor.dart +++ b/lib/services/networking/interceptors/api_interceptor.dart @@ -3,7 +3,29 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../providers/all_providers.dart'; +/// A class that holds intercepting logic for API related requests. This is +/// the first interceptor in case of both request and response. +/// +/// Since this interceptor isn't responsible for error handling, if an exception +/// occurs it is passed on the next [Interceptor] or to [Dio]. class ApiInterceptor extends Interceptor { + + /// This method intercepts an out-going request before it reaches the + /// destination. + /// + /// [options] contains http request information and configuration. + /// [handler] is used to forward, resolve, or reject requests. + /// + /// This method is used to inject any token/API keys in the request. + /// + /// The [RequestInterceptorHandler] in each method controls the what will + /// happen to the intercepted request. It has 3 possible options: + /// + /// - [handler.next]/[super.onRequest], if you want to forward the request. + /// - [handler.resolve]/[super.onResponse], if you want to resolve the + /// request with your custom [Response]. All ** request ** interceptors are ignored. + /// - [handler.reject]/[super.onError], if you want to fail the request + /// with your custom [DioError]. @override void onRequest( RequestOptions options, @@ -20,6 +42,31 @@ class ApiInterceptor extends Interceptor { return handler.next(options); } + /// This method intercepts an incoming response before it reaches Dio. + /// + /// [response] contains http [Response] info. + /// [handler] is used to forward, resolve, or reject responses. + /// + /// This method is used to check the success of the response by verifying + /// its headers. + /// + /// If response is successful, it is simply passed on. It may again be + /// intercepted if there are any after it. If none, it is passed to [Dio]. + /// + /// Else if response indicates failure, a [DioError] is thrown with the + /// response and original request's options. + /// + /// ** The success criteria is dependant on the API and may not always be + /// the same. It might need changing according to your own API. ** + /// + /// The [RequestInterceptorHandler] in each method controls the what will + /// happen to the intercepted response. It has 3 possible options: + /// + /// - [handler.next]/[super.onRequest], if you want to forward the [Response]. + /// - [handler.resolve]/[super.onResponse], if you want to resolve the + /// [Response] with your custom data. All ** response ** interceptors are ignored. + /// - [handler.reject]/[super.onError], if you want to fail the response + /// with your custom [DioError]. @override void onResponse( Response response, diff --git a/lib/services/networking/interceptors/logging_interceptor.dart b/lib/services/networking/interceptors/logging_interceptor.dart index 0ec5a7d..678cb0e 100644 --- a/lib/services/networking/interceptors/logging_interceptor.dart +++ b/lib/services/networking/interceptors/logging_interceptor.dart @@ -4,8 +4,30 @@ import 'package:flutter/foundation.dart'; import 'package:dio/dio.dart'; +/// A class that intercepts network requests for logging purposes only. This is +/// the second interceptor in case of both request and response. +/// +/// ** This interceptor doesn't modify the request or response in any way. ** class LoggingInterceptor extends Interceptor { + /// This method intercepts an out-going request before it reaches the + /// destination. + /// + /// [options] contains http request information and configuration. + /// [handler] is used to forward, resolve, or reject requests. + /// + /// This method is used to log details of all out going requests, then pass + /// it on after that. It may again be intercepted if there are any + /// after it. If none, it is passed to [Dio]. + /// + /// The [RequestInterceptorHandler] in each method controls the what will + /// happen to the intercepted request. It has 3 possible options: + /// + /// - [handler.next]/[super.onRequest], if you want to forward the request. + /// - [handler.resolve]/[super.onResponse], if you want to resolve the + /// request with your custom [Response]. All ** request ** interceptors are ignored. + /// - [handler.reject]/[super.onError], if you want to fail the request + /// with your custom [DioError]. @override void onRequest( RequestOptions options, @@ -33,6 +55,23 @@ class LoggingInterceptor extends Interceptor { return super.onRequest(options, handler); } + /// This method intercepts an incoming response before it reaches Dio. + /// + /// [response] contains http [Response] info. + /// [handler] is used to forward, resolve, or reject responses. + /// + /// This method is used to log all details of incoming responses, then pass + /// it on after that. It may again be intercepted if there are any + /// after it. If none, it is passed to [Dio]. + /// + /// The [RequestInterceptorHandler] in each method controls the what will + /// happen to the intercepted response. It has 3 possible options: + /// + /// - [handler.next]/[super.onRequest], if you want to forward the [Response]. + /// - [handler.resolve]/[super.onResponse], if you want to resolve the + /// [Response] with your custom data. All ** response ** interceptors are ignored. + /// - [handler.reject]/[super.onError], if you want to fail the response + /// with your custom [DioError]. @override void onResponse( Response response, @@ -50,6 +89,28 @@ class LoggingInterceptor extends Interceptor { return super.onResponse(response, handler); } + /// This method intercepts any exceptions thrown by Dio, or passed from a + /// previous interceptor. + /// + /// [dioError] contains error info when the request failed. + /// [handler] is used to forward, resolve, or reject errors. + /// + /// This method is used to log all details of the error arising due to the + /// failed request, then pass it on after that. It may again be intercepted + /// if there are any after it. If none, it is passed to [Dio]. + /// + /// ** The structure of response in case of errors is dependant on the API and + /// may not always be the same. It might need changing according to your + /// own API. ** + /// + /// The [RequestInterceptorHandler] in each method controls the what will + /// happen to the intercepted error. It has 3 possible options: + /// + /// - [handler.next]/[super.onRequest], if you want to forward the [Response]. + /// - [handler.resolve]/[super.onResponse], if you want to resolve the + /// [Response] with your custom data. All ** error ** interceptors are ignored. + /// - [handler.reject]/[super.onError], if you want to fail the response + /// with your custom [DioError]. @override void onError( DioError dioError, @@ -59,12 +120,12 @@ class LoggingInterceptor extends Interceptor { if(dioError.response != null){ debugPrint("\tStatus code: ${dioError.response!.statusCode}"); if(dioError.response!.data != null){ - final Map headers = dioError.response!.data["headers"]; - String message = headers["message"]; - String error = headers["error"]; + final Map headers = dioError.response!.data["headers"]; //API Dependant + String message = headers["message"]; //API Dependant + String error = headers["error"]; //API Dependant debugPrint("\tException: $error"); debugPrint("\tMessage: $message"); - if(headers.containsKey("data")){ + if(headers.containsKey("data")){ //API Dependant List data = headers["data"]; if(data.isNotEmpty) { debugPrint("\tData: $data"); diff --git a/lib/services/networking/interceptors/refresh_token_interceptor.dart b/lib/services/networking/interceptors/refresh_token_interceptor.dart index 8e55a5d..c6aa86a 100644 --- a/lib/services/networking/interceptors/refresh_token_interceptor.dart +++ b/lib/services/networking/interceptors/refresh_token_interceptor.dart @@ -8,14 +8,33 @@ import '../../../providers/all_providers.dart'; //Endpoints import '../api_endpoint.dart'; +/// A class that holds intercepting logic for refreshing tokens. This is +/// the last interceptor in the queue. class RefreshTokenInterceptor extends Interceptor { + + /// An instance of [Dio] for network requests final Dio _dio; RefreshTokenInterceptor(this._dio); + /// The name of the exception on which this interceptor is triggered. // ignore: non_constant_identifier_names String get TokenExpiredException => "TokenExpiredException"; + /// This method is used to send a refresh token request if the error + /// indicates an expired token. + /// + /// In case of expired token, it creates a new [Dio] instance, replicates + /// its options and locks the current instance to prevent further requests. + /// The new instance retrieves a new token and updates it. The original + /// request is retried with the new token. + /// + /// ** NOTE: ** Any requests from original instance will trigger all attached + /// interceptors as expected. + /// + /// ** The structure of response in case of errors or the refresh request is + /// dependant on the API and may not always be the same. It might need + /// changing according to your own API. ** @override void onError( DioError dioError, @@ -75,6 +94,11 @@ class RefreshTokenInterceptor extends Interceptor { return super.onError(dioError, handler); } + /// This method sends out a request to refresh the token. Since this request + /// uses the new [Dio] instance it needs its own logging and error handling. + /// + /// ** The structure of response is dependant on the API and may not always + /// be the same. It might need changing according to your own API. ** Future _refreshTokenRequest({ required DioError dioError, required ErrorInterceptorHandler handler, diff --git a/lib/views/screens/app_startup_screen.dart b/lib/views/screens/app_startup_screen.dart index 9f1469b..696784f 100644 --- a/lib/views/screens/app_startup_screen.dart +++ b/lib/views/screens/app_startup_screen.dart @@ -13,7 +13,6 @@ class AppStartupScreen extends HookWidget { const AppStartupScreen(); Widget build(BuildContext context) { - print(MediaQuery.of(context).padding.bottom); final authState = useProvider(authProvider); return authState.maybeWhen( authenticated: (fullName) => const WelcomeScreen(), diff --git a/lib/views/screens/movies_screen.dart b/lib/views/screens/movies_screen.dart index cafc59a..3b74fff 100644 --- a/lib/views/screens/movies_screen.dart +++ b/lib/views/screens/movies_screen.dart @@ -4,24 +4,21 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; //Helper -import '../../helper/utils/constants.dart'; import '../../helper/extensions/context_extensions.dart'; +import '../../helper/utils/constants.dart'; //Providers -import '../../providers/movies_provider.dart'; import '../../providers/all_providers.dart'; +import '../../providers/movies_provider.dart'; -//Services -import '../../services/networking/network_exception.dart'; +//Skeletons +import '../skeletons/movies_skeleton_loader.dart'; //Widgets -import '../widgets/common/custom_error_widget.dart'; import '../widgets/movies/movie_backdrop_view.dart'; import '../widgets/movies/movie_carousel.dart'; import '../widgets/movies/movie_icons_row.dart'; - -//Skeletons -import '../skeletons/movies_skeleton_loader.dart'; +import '../widgets/common/error_response_handler.dart'; class MoviesScreen extends HookWidget { const MoviesScreen(); @@ -97,21 +94,15 @@ class MoviesScreen extends HookWidget { ); }, loading: () => const MoviesSkeletonLoader(), - error: (error, st) { - if (error is NetworkException) { - return CustomErrorWidget.dark( - error: error, - retryCallback: () { - context.refresh(moviesFuture); - }, - height: screenHeight * 0.5, - ); - } - context.read(authProvider.notifier).logout(); - context.router.popUntilRoot(); - debugPrint(error.toString()); - debugPrint(st.toString()); - }, + error: (error, st) => ErrorResponseHandler( + error: error, + stackTrace: st, + retryCallback: () => context.refresh(moviesFuture), + onError: () { + context.read(authProvider.notifier).logout(); + context.router.popUntilRoot(); + }, + ), ), ), ); diff --git a/lib/views/screens/shows_screen.dart b/lib/views/screens/shows_screen.dart index df919a1..e8e89c3 100644 --- a/lib/views/screens/shows_screen.dart +++ b/lib/views/screens/shows_screen.dart @@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; //Helper import '../../enums/show_status_enum.dart'; -import '../../helper/utils/constants.dart'; import '../../helper/extensions/context_extensions.dart'; +import '../../helper/utils/constants.dart'; //Providers import '../../providers/movies_provider.dart'; @@ -15,26 +15,22 @@ import '../../providers/shows_provider.dart'; //Routes import '../../routes/app_router.gr.dart'; -//Services -import '../../services/networking/network_exception.dart'; +//Skeletons +import '../skeletons/shows_skeleton_loader.dart'; //Widgets -import '../widgets/common/custom_error_widget.dart'; import '../widgets/common/custom_text_button.dart'; +import '../widgets/common/error_response_handler.dart'; import '../widgets/show_times/show_dates_list.dart'; import '../widgets/show_times/show_details_box.dart'; import '../widgets/show_times/show_times_list.dart'; -//Skeletons -import '../skeletons/shows_skeleton_loader.dart'; - class ShowsScreen extends HookWidget { const ShowsScreen(); @override Widget build(BuildContext context) { final showList = useProvider(showsFutureProvider); - final screenHeight = context.screenHeight; return Scaffold( body: SafeArea( child: Column( @@ -48,7 +44,7 @@ class ShowsScreen extends HookWidget { const SizedBox(width: 15), InkResponse( radius: 25, - child: const Icon(Icons.arrow_back_sharp,size: 26), + child: const Icon(Icons.arrow_back_sharp, size: 26), onTap: () { context.router.pop(); }, @@ -78,6 +74,7 @@ class ShowsScreen extends HookWidget { const SizedBox(height: 42), + //Show details Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 550), @@ -165,7 +162,7 @@ class ShowsScreen extends HookWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Consumer( - builder: (ctx,watch,child) { + builder: (ctx, watch, child) { final showStatus = watch(selectedShowTimeProvider) .state .showStatus; @@ -196,20 +193,11 @@ class ShowsScreen extends HookWidget { ], ), loading: () => const ShowsSkeletonLoader(), - error: (error, st) { - if (error is NetworkException) { - return CustomErrorWidget.dark( - error: error, - retryCallback: () { - context.refresh(showsFutureProvider); - }, - height: screenHeight * 0.5, - ); - } - debugPrint(error.toString()); - debugPrint(st.toString()); - return const SizedBox.shrink(); - }, + error: (error, st) => ErrorResponseHandler( + retryCallback: () => context.refresh(showsFutureProvider), + error: error, + stackTrace: st, + ), ), ), ), diff --git a/lib/views/screens/theater_screen.dart b/lib/views/screens/theater_screen.dart index a3f1845..f77d855 100644 --- a/lib/views/screens/theater_screen.dart +++ b/lib/views/screens/theater_screen.dart @@ -6,27 +6,24 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; //Helpers -import '../../helper/utils/constants.dart'; import '../../helper/extensions/context_extensions.dart'; +import '../../helper/utils/constants.dart'; +import '../../providers/all_providers.dart'; //Providers import '../../providers/theaters_provider.dart'; -import '../../providers/all_providers.dart'; -//Services -import '../../services/networking/network_exception.dart'; +//Skeletons +import '../skeletons/theater_skeleton_loader.dart'; //Widgets import '../widgets/common/custom_chips_list.dart'; -import '../widgets/common/custom_error_widget.dart'; -import '../widgets/theater/purchase_seats_button.dart'; +import '../widgets/common/error_response_handler.dart'; import '../widgets/theater/curved_screen.dart'; +import '../widgets/theater/purchase_seats_button.dart'; import '../widgets/theater/seat_color_indicators.dart'; import '../widgets/theater/seats_area.dart'; -//Skeletons -import '../skeletons/theater_skeleton_loader.dart'; - class TheaterScreen extends HookWidget { const TheaterScreen(); @@ -106,10 +103,12 @@ class TheaterScreen extends HookWidget { Padding( padding: const EdgeInsets.fromLTRB(20, 2, 0, 22), child: Consumer( - builder:(ctx,watch,child) { - final _theatersProvider = watch(theatersProvider); + builder: (ctx, watch, child) { + final _theatersProvider = + watch(theatersProvider); return CustomChipsList( - chipContents: _theatersProvider.selectedSeatNames, + chipContents: + _theatersProvider.selectedSeatNames, chipHeight: 27, chipGap: 10, fontSize: 14, @@ -118,7 +117,8 @@ class TheaterScreen extends HookWidget { contentColor: Constants.orangeColor, borderWidth: 1.5, fontWeight: FontWeight.bold, - backgroundColor: Colors.red.shade700.withOpacity(0.3), + backgroundColor: + Colors.red.shade700.withOpacity(0.3), isScrollable: true, ); }, @@ -133,7 +133,11 @@ class TheaterScreen extends HookWidget { ); }, loading: () => const TheaterSkeletonLoader(), - error: (error, st) => _buildError(error, st, context), + error: (error, st) => ErrorResponseHandler( + error: error, + retryCallback: () => context.refresh(showSeatingFuture), + stackTrace: st, + ), ), ), ), @@ -143,21 +147,6 @@ class TheaterScreen extends HookWidget { ), ); } - - Widget _buildError(error, StackTrace? st, BuildContext context) { - if (error is NetworkException) { - return CustomErrorWidget.dark( - error: error, - retryCallback: () { - context.refresh(showSeatingFuture); - }, - height: context.screenHeight * 0.5, - ); - } - debugPrint(error.toString()); - debugPrint(st.toString()); - return const SizedBox.shrink(); - } } class _BackIcon extends StatelessWidget { diff --git a/lib/views/screens/user_bookings_screen.dart b/lib/views/screens/user_bookings_screen.dart new file mode 100644 index 0000000..fd37dba --- /dev/null +++ b/lib/views/screens/user_bookings_screen.dart @@ -0,0 +1,65 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; + +//Helpers +import '../../helper/extensions/context_extensions.dart'; + +//Widgets +import '../widgets/user_bookings/user_bookings_history.dart'; + +class UserBookingsScreen extends StatelessWidget { + const UserBookingsScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + const SizedBox(height: 20), + + //Back and title + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 15), + InkResponse( + radius: 25, + child: const Icon(Icons.arrow_back_sharp, size: 26), + onTap: () { + context.router.pop(); + }, + ), + + const SizedBox(width: 20), + + //Movie Title + Expanded( + child: Text( + "Your bookings", + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: context.headline3.copyWith(fontSize: 22), + ), + ), + + const SizedBox(width: 50), + ], + ), + + const SizedBox(height: 20), + + //Bookings history + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: UserBookingsHistory(), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/skeletons/user_bookings_skeleton_loader.dart b/lib/views/skeletons/user_bookings_skeleton_loader.dart new file mode 100644 index 0000000..24c178b --- /dev/null +++ b/lib/views/skeletons/user_bookings_skeleton_loader.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; + +//Helpers +import '../../helper/utils/constants.dart'; + +//Widgets +import '../widgets/common/shimmer_loader.dart'; + +class UserBookingsSkeletonLoader extends StatelessWidget { + const UserBookingsSkeletonLoader(); + + @override + Widget build(BuildContext context) { + return ShimmerLoader( + child: ListView.separated( + itemCount: 4, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (ctx,i) => const SizedBox(height: 20), + itemBuilder: (ctx,i) => const _UserBookingSkeleton(), + ), + ); + } +} + +class _UserBookingSkeleton extends StatelessWidget { + const _UserBookingSkeleton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 140, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + //Booking overview + SizedBox( + height: 120, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + //Booking details + _BookingDetailsSkeleton(), + + //No of seats + SizedBox( + height: double.infinity, + width: 45, + child: DecoratedBox( + decoration: BoxDecoration( + color: Constants.darkSkeletonColor, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + ), + ), + ], + ), + ), + + //Movie Image + const Positioned( + bottom: 13, + left: 13, + child: SizedBox( + height: 125, + width: 100, + child: DecoratedBox( + decoration: BoxDecoration( + color: Constants.darkSkeletonColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: Center( + child: Icon( + Icons.movie_creation_rounded, + color: Constants.lightSkeletonColor, + size: 40, + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class _BookingDetailsSkeleton extends StatelessWidget { + const _BookingDetailsSkeleton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + decoration: const BoxDecoration( + color: Constants.lightSkeletonColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + padding: const EdgeInsets.fromLTRB(125, 10, 10, 13), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + //Movie data + SizedBox( + height: 25, + width: double.infinity, + child: DecoratedBox( + decoration: BoxDecoration( + color: Constants.darkSkeletonColor, + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + ), + ), + ), + + SizedBox(height: 10), + + //Show details + Expanded( + child: SizedBox( + width: double.infinity, + child: DecoratedBox( + decoration: BoxDecoration( + color: Constants.darkSkeletonColor, + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/common/custom_error_widget.dart b/lib/views/widgets/common/custom_error_widget.dart index b8ee1d5..f457d4f 100644 --- a/lib/views/widgets/common/custom_error_widget.dart +++ b/lib/views/widgets/common/custom_error_widget.dart @@ -60,6 +60,7 @@ class CustomErrorWidget extends StatelessWidget { const SizedBox(height: 30), Text( error.message, + textAlign: TextAlign.center, style: textTheme.headline5!.copyWith(fontSize: 21), ), const Spacer(), diff --git a/lib/views/widgets/common/error_response_handler.dart b/lib/views/widgets/common/error_response_handler.dart new file mode 100644 index 0000000..e0231e3 --- /dev/null +++ b/lib/views/widgets/common/error_response_handler.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +//Helpers +import '../../../helper/extensions/context_extensions.dart'; + +//Services +import '../../../services/networking/network_exception.dart'; + +//Widgets +import 'custom_error_widget.dart'; + +class ErrorResponseHandler extends StatelessWidget { + final Object error; + final StackTrace? stackTrace; + final VoidCallback? onError; + final VoidCallback? retryCallback; + + const ErrorResponseHandler({ + Key? key, + this.onError, + required this.retryCallback, + required this.error, + required this.stackTrace, + }) : super(key: key); + + const factory ErrorResponseHandler.builder({ + Key? key, + VoidCallback? onError, + required Object error, + required StackTrace? stackTrace, + required Widget Function(NetworkException) builder, + }) = _ErrorResponseHandlerWithBuilder; + + @override + Widget build(BuildContext context) { + if (error is NetworkException) { + return CustomErrorWidget.dark( + error: error as NetworkException, + retryCallback: retryCallback!, + height: context.screenHeight * 0.5, + ); + } + if (onError != null) onError!(); + debugPrint(error.toString()); + debugPrint(stackTrace?.toString()); + return const SizedBox.shrink(); + } +} + +class _ErrorResponseHandlerWithBuilder extends ErrorResponseHandler { + final Widget Function(NetworkException) builder; + + const _ErrorResponseHandlerWithBuilder({ + Key? key, + VoidCallback? onError, + required Object error, + required StackTrace? stackTrace, + required this.builder, + }) : super( + key: key, + error: error, + stackTrace: stackTrace, + onError: onError, + retryCallback: null, + ); + + @override + Widget build(BuildContext context) { + if (error is NetworkException) return builder(error as NetworkException); + if (onError != null) onError!(); + debugPrint(error.toString()); + debugPrint(stackTrace?.toString()); + return const SizedBox.shrink(); + } +} diff --git a/lib/views/widgets/movie_details/movie_actors_list.dart b/lib/views/widgets/movie_details/movie_actors_list.dart index 2273b31..5c08cd1 100644 --- a/lib/views/widgets/movie_details/movie_actors_list.dart +++ b/lib/views/widgets/movie_details/movie_actors_list.dart @@ -16,9 +16,6 @@ import '../../../providers/all_providers.dart'; //Providers import '../../../providers/movies_provider.dart'; -//Services -import '../../../services/networking/network_exception.dart'; - //Placeholders import '../../skeletons/actor_picture_placeholder.dart'; @@ -27,8 +24,9 @@ import '../../skeletons/movie_actors_skeleton_loader.dart'; //Widgets import '../common/custom_network_image.dart'; +import '../common/error_response_handler.dart'; -final movieRolesFuture = FutureProvider.family, int>( +final movieRolesFuture = FutureProvider.family.autoDispose, int>( (ref, movieId) async { final _moviesProvider = ref.watch(moviesProvider); @@ -36,6 +34,7 @@ final movieRolesFuture = FutureProvider.family, int>( movieId: movieId, ); + ref.maintainState = true; //Caches the response only if the future completed. return movieRolesList; }, ); @@ -105,14 +104,11 @@ class MovieActorsList extends HookWidget { ), ), loading: () => const MovieActorsSkeletonLoader(), - error: (error, st) { - if (error is NetworkException) { - return Text(error.message); - } - debugPrint(error.toString()); - debugPrint(st.toString()); - return Text(error.toString()); - }, + error: (error, st) => ErrorResponseHandler.builder( + error: error, + stackTrace: st, + builder: (error) => Text(error.message), + ), ), ), ], diff --git a/lib/views/widgets/show_times/show_times_list.dart b/lib/views/widgets/show_times/show_times_list.dart index 2affcc9..d6d3bb4 100644 --- a/lib/views/widgets/show_times/show_times_list.dart +++ b/lib/views/widgets/show_times/show_times_list.dart @@ -10,16 +10,9 @@ import '../../../providers/shows_provider.dart'; import '../../../helper/utils/constants.dart'; import '../../../helper/extensions/context_extensions.dart'; -class ShowTimesList extends StatefulHookWidget { +class ShowTimesList extends HookWidget { const ShowTimesList(); - @override - _ShowTimesListState createState() => _ShowTimesListState(); -} - -class _ShowTimesListState extends State { - int selectedIndex = 0; - Shader getShader(Rect bounds) { return const LinearGradient( begin: Alignment.centerRight, @@ -35,6 +28,7 @@ class _ShowTimesListState extends State { @override Widget build(BuildContext context) { final showTimes = useProvider(selectedShowProvider).state.showTimes; + final selectedShowTime = useProvider(selectedShowTimeProvider).state; return ShaderMask( shaderCallback: getShader, blendMode: BlendMode.dstOut, @@ -49,12 +43,9 @@ class _ShowTimesListState extends State { ? const EdgeInsets.only(left: 20) : const EdgeInsets.all(0), child: _ShowTimeItem( - isActive: i == selectedIndex, + isActive: showTimes[i] == selectedShowTime, onTap: () { - setState(() { - context.read(selectedShowTimeProvider).state = showTimes[i]; - selectedIndex = i; - }); + context.read(selectedShowTimeProvider).state = showTimes[i]; }, time: DateFormat.jm().format(showTimes[i].startTime), ), diff --git a/lib/views/widgets/ticket_summary/ticket_details_list.dart b/lib/views/widgets/ticket_summary/ticket_details_list.dart index 2997541..97df521 100644 --- a/lib/views/widgets/ticket_summary/ticket_details_list.dart +++ b/lib/views/widgets/ticket_summary/ticket_details_list.dart @@ -47,7 +47,7 @@ class TicketDetailsList extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Seat:", + "Seat", style: TextStyle( fontSize: 13, color: Constants.textGreyColor, diff --git a/lib/views/widgets/user_bookings/booking_details_dialog.dart b/lib/views/widgets/user_bookings/booking_details_dialog.dart new file mode 100644 index 0000000..36665c1 --- /dev/null +++ b/lib/views/widgets/user_bookings/booking_details_dialog.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; + +//Helpers +import '../../../enums/booking_status_enum.dart'; +import '../../../helper/utils/constants.dart'; + +//Models +import '../../../models/booking_model.dart'; + +//Skeletons +import '../../skeletons/movie_poster_placeholder.dart'; + +//Widgets +import '../common/custom_network_image.dart'; + +class BookingDetailsDialog extends StatelessWidget { + final String posterUrl; + final List bookings; + + const BookingDetailsDialog({ + Key? key, + required this.posterUrl, + required this.bookings, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + height: 400, + width: 250, + child: Column( + children: [ + //Movie Image + CustomNetworkImage( + imageUrl: posterUrl, + fit: BoxFit.cover, + height: 120, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20), + ), + placeholder: const MoviePosterPlaceholder(), + errorWidget: const MoviePosterPlaceholder(), + ), + + //Grey Container + Expanded( + child: Material( + color: Constants.scaffoldColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 12, 15, 0), + child: Column( + children: [ + //Column Labels + Row( + children:const [ + //Seat label + SizedBox( + width: 50, + child: Text( + "Seat", + style: TextStyle( + color: Constants.textWhite80Color, + ), + ), + ), + + //Price label + Expanded( + child: Text( + "Price", + style: TextStyle( + color: Constants.textWhite80Color, + ), + ), + ), + + //Status label + SizedBox( + width: 100, + child: Text( + "Seat Status", + style: TextStyle( + color: Constants.textWhite80Color, + ), + ), + ), + ], + ), + + const SizedBox(height: 10), + + //Column data + Expanded( + child: ListView.separated( + itemCount: bookings.length, + padding: const EdgeInsets.all(0), + separatorBuilder: (ctx, i) => const SizedBox(height: 20), + itemBuilder: (ctx, i) => _BookingSeatsListItem( + booking: bookings[i], + ), + ), + ), + ], + ), + ), + ), + ), + + //Expand icon + Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Constants.primaryColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + child: const Icon( + Icons.expand_more_sharp, + color: Colors.white, + ), + ), + ], + ), + ), + ); + } +} + +class _BookingSeatsListItem extends StatelessWidget { + const _BookingSeatsListItem({ + Key? key, + required this.booking, + }) : super(key: key); + + final BookingModel booking; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + //Seat Name + SizedBox( + width: 50, + child: Text( + "${booking.seatRow}-${booking.seatNumber}", + style: const TextStyle( + color: Constants.textGreyColor, + fontSize: 13, + ), + ), + ), + + //Seat Price + Expanded( + child: Text( + "${booking.seatNumber == 3 ? "1000.0": booking.price}", + style: const TextStyle( + color: Constants.textGreyColor, + fontSize: 13, + ), + ), + ), + + //Seat Status + SizedBox( + width: 100, + child: Row( + children: [ + //Booking Status value + Text( + "${booking.bookingStatus.name}", + style: const TextStyle( + color: Constants.textGreyColor, + fontSize: 13, + ), + ), + + const Spacer(), + + //Booking Status icon + if (booking.bookingStatus == BookingStatus.CANCELLED) + const Icon( + Icons.cancel_sharp, + size: 16, + color: Colors.red, + ) + else if (booking.bookingStatus == BookingStatus.RESERVED) + const Icon( + Icons.watch_later_sharp, + size: 16, + color: Colors.amber, + ) + else if (booking.bookingStatus == BookingStatus.CONFIRMED) + const Icon( + Icons.check_circle_sharp, + size: 16, + color: Color(0xFF64DD17), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/views/widgets/user_bookings/booking_summary_row.dart b/lib/views/widgets/user_bookings/booking_summary_row.dart new file mode 100644 index 0000000..837ccd6 --- /dev/null +++ b/lib/views/widgets/user_bookings/booking_summary_row.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +//Helpers +import '../../../enums/show_type_enum.dart'; +import '../../../helper/utils/constants.dart'; + +class BookingSummaryRow extends StatelessWidget { + final String title; + final int noOfSeats; + final double total; + final DateTime showDateTime; + final ShowType showType; + + const BookingSummaryRow({ + Key? key, + required this.total, + required this.title, + required this.noOfSeats, + required this.showDateTime, + required this.showType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 120, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + //Ticket total and movie name + Expanded( + child: Container( + decoration: const BoxDecoration( + color: Constants.scaffoldGreyColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + padding: const EdgeInsets.fromLTRB(125, 10, 5, 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //Movie data + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 15, + color: Constants.textWhite80Color, + fontWeight: FontWeight.bold, + ), + ), + + const Spacer(), + + //Show type row + Row( + children: [ + //Show type icon + const Icon( + Icons.hd_outlined, + size: 19, + color: Colors.blue, + ), + + const SizedBox(width: 10), + + //Show status + Text( + "${showType.inString}", + style: const TextStyle( + fontSize: 14, + color: Constants.textWhite80Color, + ), + ), + ], + ), + + const Spacer(), + + //Show timings row + Row( + children: [ + //Show date icon + const Icon( + Icons.date_range_outlined, + size: 19, + color: Constants.primaryColor, + ), + + const SizedBox(width: 10), + + //Show time data + Text( + "${DateFormat("d MMMM,yy H:m").format(showDateTime)}", + style: const TextStyle( + fontSize: 14, + color: Constants.textWhite80Color, + ), + ), + ], + ), + + const Spacer(), + + //Show payment row + Row( + children: [ + //Total icon + const Icon( + Icons.local_atm_outlined, + size: 19, + color: Colors.green, + ), + + const SizedBox(width: 10), + + //Total data + Text( + "Rs. $total", + style: const TextStyle( + fontSize: 14, + color: Constants.textWhite80Color, + ), + ), + ], + ), + ], + ), + ), + ), + + //No of seats + SizedBox( + height: double.infinity, + width: 45, + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: Constants.buttonGradientRed, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + //Ticket icon + const Icon( + Icons.local_activity_sharp, + color: Constants.textWhite80Color, + ), + + const SizedBox(height: 5), + + //No. of seats + Text( + "$noOfSeats", + style: const TextStyle( + fontSize: 16, + color: Constants.textWhite80Color, + ), + ) + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/widgets/user_bookings/user_bookings_history.dart b/lib/views/widgets/user_bookings/user_bookings_history.dart new file mode 100644 index 0000000..2d737be --- /dev/null +++ b/lib/views/widgets/user_bookings/user_bookings_history.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +//Providers +import '../../../providers/bookings_provider.dart'; + +//Skeletons +import '../../skeletons/user_bookings_skeleton_loader.dart'; + +//Widgets +import '../common/error_response_handler.dart'; +import 'user_bookings_list.dart'; + +class UserBookingsHistory extends HookWidget { + const UserBookingsHistory(); + + @override + Widget build(BuildContext context) { + final userBookingsFuture = useProvider(userBookingsProvider); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + switchOutCurve: Curves.easeInBack, + child: userBookingsFuture.when( + data: (bookings) => UserBookingsList(bookings: bookings), + loading: () => const UserBookingsSkeletonLoader(), + error: (error, st) => ErrorResponseHandler( + error: error, + retryCallback: () => context.refresh(userBookingsProvider), + stackTrace: st, + ), + ), + ); + } +} diff --git a/lib/views/widgets/user_bookings/user_bookings_list.dart b/lib/views/widgets/user_bookings/user_bookings_list.dart new file mode 100644 index 0000000..c91e992 --- /dev/null +++ b/lib/views/widgets/user_bookings/user_bookings_list.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +//Helpers +import '../../../helper/utils/constants.dart'; + +//Models +import '../../../models/user_booking_model.dart'; + +//Skeletons +import '../../skeletons/movie_poster_placeholder.dart'; + +//Widgets +import '../common/custom_network_image.dart'; +import 'booking_details_dialog.dart'; +import 'booking_summary_row.dart'; + +class UserBookingsList extends StatelessWidget { + const UserBookingsList({ + Key? key, + required this.bookings, + }) : super(key: key); + + static const movieSize = 100.0; + final List bookings; + + void onTap(BuildContext context, UserBookingModel booking) { + showGeneralDialog( + barrierColor: Constants.barrierColorLight, + transitionDuration: const Duration(milliseconds: 400), + barrierDismissible: true, + barrierLabel: '', + context: context, + transitionBuilder: (context, a1, a2, dialog) { + final curveValue = + (1 - Curves.linearToEaseOut.transform(a1.value)) * 200; + return Transform( + transform: Matrix4.translationValues(curveValue, 0.0, 0.0), + child: Opacity(opacity: a1.value, child: dialog), + ); + }, + pageBuilder: (_, __, ___) => BookingDetailsDialog( + posterUrl: booking.posterUrl, + bookings: booking.bookings, + ), + ); + } + + @override + Widget build(BuildContext context) { + return ListView.separated( + physics: const BouncingScrollPhysics(), + itemCount: bookings.length, + separatorBuilder: (_, i) => const SizedBox(height: 20), + itemBuilder: (_, i) { + final booking = bookings[i]; + final total = booking.bookings.fold(0.0, (sum, seat) => seat.price); + final noOfSeats = booking.bookings.length; + return SizedBox( + height: 140, + child: GestureDetector( + onTap: () {}, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + //Booking overview + BookingSummaryRow( + total: total, + title: booking.title, + noOfSeats: noOfSeats, + showDateTime: booking.show.showDatetime, + showType: booking.show.showType, + ), + + //Movie Image + Positioned( + bottom: 13, + left: 13, + child: CustomNetworkImage( + imageUrl: booking.posterUrl, + fit: BoxFit.cover, + width: movieSize, + height: movieSize + 25, + borderRadius: const BorderRadius.all(Radius.circular(10)), + placeholder: const MoviePosterPlaceholder(iconSize: 40), + errorWidget: const MoviePosterPlaceholder(iconSize: 40), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/views/widgets/welcome/view_bookings_button.dart b/lib/views/widgets/welcome/view_bookings_button.dart index 84cae96..de8cd27 100644 --- a/lib/views/widgets/welcome/view_bookings_button.dart +++ b/lib/views/widgets/welcome/view_bookings_button.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:auto_route/auto_route.dart'; //Helpers import '../../../helper/utils/constants.dart'; +//Routes +import '../../../routes/app_router.gr.dart'; + //Widgets import '../common/custom_text_button.dart'; @@ -13,7 +17,7 @@ class ViewBookingsButton extends StatelessWidget { Widget build(BuildContext context) { return CustomTextButton.outlined( width: double.infinity, - onPressed: () {}, + onPressed: () => context.router.push(const UserBookingsScreenRoute()), border: Border.all(color: Constants.primaryColor,width: 4), child: const Center( child: Text(