mirror of https://github.com/flutter/samples.git
[Compass App] Booking screen (#2380)
This PR adds the Booking screen at the end of the main app flow. After the user selects `Activity`s, these get stored in the `ItineraryConfig` and then the user navigates to the `BookingScreen`. In the `BookingScreen`, a `Booking` is generated with the `BookingCreateComponent`. This component communicates with multiple repositories, and it is a bit more complex than the average view model, something that we want to show as discussed in the previous PRs. <details> <summary>Screenshots</summary>   </details> In the `BookingScreen`, the user can tap on "share trip" which displays the OS share sheet functionality. This uses the plugin `share_plus`, but the functionality is also wrapped in the `BookingShareComponent`, which takes a `Booking` object and creates a shareable string, then calls to the `Share.share()` method from `share_plus`. But the `share_plus` dependency is also injected into the `BookingShareComponent`, allowing us to unit test without plugin dependencies. This is an example of a shared booking to instant messaging:  **TODO** - I want to take a look at the whole experience on mobile, as I noticed some inconsistent UI and navigation issues that I couldn't see on Desktop. I will submit those in a new PR. - We also talked about user authentication in the design document. I will work on that once we are happy with the main app flow. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.mdpull/2389/head
parent
0305894b0e
commit
0c88289339
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../../data/repositories/activity/activity_repository.dart';
|
||||||
|
import '../../../data/repositories/destination/destination_repository.dart';
|
||||||
|
import '../../../utils/result.dart';
|
||||||
|
|
||||||
|
/// Component for creating [Booking] objects from [ItineraryConfig].
|
||||||
|
///
|
||||||
|
/// Fetches [Destination] and [Activity] objects from repositories,
|
||||||
|
/// checks if dates are set and creates a [Booking] object.
|
||||||
|
class BookingCreateComponent {
|
||||||
|
BookingCreateComponent({
|
||||||
|
required DestinationRepository destinationRepository,
|
||||||
|
required ActivityRepository activityRepository,
|
||||||
|
}) : _destinationRepository = destinationRepository,
|
||||||
|
_activityRepository = activityRepository;
|
||||||
|
|
||||||
|
final DestinationRepository _destinationRepository;
|
||||||
|
final ActivityRepository _activityRepository;
|
||||||
|
final _log = Logger('BookingComponent');
|
||||||
|
|
||||||
|
/// Create [Booking] from a stored [ItineraryConfig]
|
||||||
|
Future<Result<Booking>> createFrom(ItineraryConfig itineraryConfig) async {
|
||||||
|
// Get Destination object from repository
|
||||||
|
if (itineraryConfig.destination == null) {
|
||||||
|
_log.warning('Destination is not set');
|
||||||
|
return Result.error(Exception('Destination is not set'));
|
||||||
|
}
|
||||||
|
final destinationResult =
|
||||||
|
await _fetchDestination(itineraryConfig.destination!);
|
||||||
|
if (destinationResult is Error<Destination>) {
|
||||||
|
_log.warning('Error fetching destination: ${destinationResult.error}');
|
||||||
|
return Result.error(destinationResult.error);
|
||||||
|
}
|
||||||
|
_log.fine('Destination loaded: ${destinationResult.asOk.value.ref}');
|
||||||
|
|
||||||
|
// Get Activity objects from repository
|
||||||
|
if (itineraryConfig.activities.isEmpty) {
|
||||||
|
_log.warning('Activities are not set');
|
||||||
|
return Result.error(Exception('Activities are not set'));
|
||||||
|
}
|
||||||
|
final activitiesResult = await _activityRepository.getByDestination(
|
||||||
|
itineraryConfig.destination!,
|
||||||
|
);
|
||||||
|
if (activitiesResult is Error<List<Activity>>) {
|
||||||
|
_log.warning('Error fetching activities: ${activitiesResult.error}');
|
||||||
|
return Result.error(activitiesResult.error);
|
||||||
|
}
|
||||||
|
final activities = activitiesResult.asOk.value
|
||||||
|
.where(
|
||||||
|
(activity) => itineraryConfig.activities.contains(activity.ref),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
_log.fine('Activities loaded (${activities.length})');
|
||||||
|
|
||||||
|
// Check if dates are set
|
||||||
|
if (itineraryConfig.startDate == null || itineraryConfig.endDate == null) {
|
||||||
|
_log.warning('Dates are not set');
|
||||||
|
return Result.error(Exception('Dates are not set'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Booking object
|
||||||
|
return Result.ok(
|
||||||
|
Booking(
|
||||||
|
startDate: itineraryConfig.startDate!,
|
||||||
|
endDate: itineraryConfig.endDate!,
|
||||||
|
destination: destinationResult.asOk.value,
|
||||||
|
activity: activities,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<Destination>> _fetchDestination(String destinationRef) async {
|
||||||
|
final result = await _destinationRepository.getDestinations();
|
||||||
|
switch (result) {
|
||||||
|
case Ok<List<Destination>>():
|
||||||
|
final destination = result.value
|
||||||
|
.firstWhere((destination) => destination.ref == destinationRef);
|
||||||
|
return Ok(destination);
|
||||||
|
case Error<List<Destination>>():
|
||||||
|
return Result.error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import '../../../utils/result.dart';
|
||||||
|
import '../../core/ui/date_format_start_end.dart';
|
||||||
|
|
||||||
|
typedef ShareFunction = Future<void> Function(String text);
|
||||||
|
|
||||||
|
/// Component for sharing a booking.
|
||||||
|
class BookingShareComponent {
|
||||||
|
BookingShareComponent._(this._share);
|
||||||
|
|
||||||
|
/// Create a [BookingShareComponent] that uses `share_plus` package.
|
||||||
|
factory BookingShareComponent.withSharePlus() =>
|
||||||
|
BookingShareComponent._(Share.share);
|
||||||
|
|
||||||
|
/// Create a [BookingShareComponent] with a custom share function.
|
||||||
|
factory BookingShareComponent.custom(ShareFunction share) =>
|
||||||
|
BookingShareComponent._(share);
|
||||||
|
|
||||||
|
final ShareFunction _share;
|
||||||
|
final _log = Logger('BookingShareComponent');
|
||||||
|
|
||||||
|
Future<Result<void>> shareBooking(Booking booking) async {
|
||||||
|
final text = 'Trip to ${booking.destination.name}\n'
|
||||||
|
'on ${dateFormatStartEnd(DateTimeRange(start: booking.startDate, end: booking.endDate))}\n'
|
||||||
|
'Activities:\n'
|
||||||
|
'${booking.activity.map((a) => ' - ${a.name}').join('\n')}.';
|
||||||
|
|
||||||
|
_log.info('Sharing booking: $text');
|
||||||
|
try {
|
||||||
|
await _share(text);
|
||||||
|
_log.fine('Shared booking');
|
||||||
|
return Result.ok(null);
|
||||||
|
} on Exception catch (error) {
|
||||||
|
_log.severe('Failed to share booking', error);
|
||||||
|
return Result.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||||
|
import '../../../utils/command.dart';
|
||||||
|
import '../../../utils/result.dart';
|
||||||
|
import '../components/booking_create_component.dart';
|
||||||
|
import '../components/booking_share_component.dart';
|
||||||
|
|
||||||
|
class BookingViewModel extends ChangeNotifier {
|
||||||
|
BookingViewModel({
|
||||||
|
required BookingCreateComponent bookingComponent,
|
||||||
|
required BookingShareComponent shareComponent,
|
||||||
|
required ItineraryConfigRepository itineraryConfigRepository,
|
||||||
|
}) : _createComponent = bookingComponent,
|
||||||
|
_shareComponent = shareComponent,
|
||||||
|
_itineraryConfigRepository = itineraryConfigRepository {
|
||||||
|
loadBooking = Command0(_loadBooking)..execute();
|
||||||
|
shareBooking = Command0(() => _shareComponent.shareBooking(_booking!));
|
||||||
|
}
|
||||||
|
|
||||||
|
final BookingCreateComponent _createComponent;
|
||||||
|
final BookingShareComponent _shareComponent;
|
||||||
|
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||||
|
final _log = Logger('BookingViewModel');
|
||||||
|
Booking? _booking;
|
||||||
|
|
||||||
|
Booking? get booking => _booking;
|
||||||
|
|
||||||
|
late final Command0 loadBooking;
|
||||||
|
|
||||||
|
/// Share the current booking using the OS share dialog.
|
||||||
|
late final Command0 shareBooking;
|
||||||
|
|
||||||
|
Future<Result<void>> _loadBooking() async {
|
||||||
|
_log.fine('Loading booking');
|
||||||
|
final itineraryConfig =
|
||||||
|
await _itineraryConfigRepository.getItineraryConfig();
|
||||||
|
switch (itineraryConfig) {
|
||||||
|
case Ok<ItineraryConfig>():
|
||||||
|
_log.fine('Loaded stored ItineraryConfig');
|
||||||
|
final result = await _createComponent.createFrom(itineraryConfig.value);
|
||||||
|
switch (result) {
|
||||||
|
case Ok<Booking>():
|
||||||
|
_log.fine('Created Booking');
|
||||||
|
_booking = result.value;
|
||||||
|
notifyListeners();
|
||||||
|
return Result.ok(null);
|
||||||
|
case Error<Booking>():
|
||||||
|
_log.warning('Booking error: ${result.error}');
|
||||||
|
notifyListeners();
|
||||||
|
return Result.error(result.asError.error);
|
||||||
|
}
|
||||||
|
case Error<ItineraryConfig>():
|
||||||
|
_log.warning('ItineraryConfig error: ${itineraryConfig.error}');
|
||||||
|
notifyListeners();
|
||||||
|
return Result.error(itineraryConfig.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../view_models/booking_viewmodel.dart';
|
||||||
|
import 'booking_header.dart';
|
||||||
|
|
||||||
|
class BookingBody extends StatelessWidget {
|
||||||
|
const BookingBody({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BookingViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
builder: (context, _) {
|
||||||
|
final booking = viewModel.booking;
|
||||||
|
if (booking == null) return const SizedBox();
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(child: BookingHeader(booking: booking)),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final activity = booking.activity[index];
|
||||||
|
return _Activity(activity: activity);
|
||||||
|
},
|
||||||
|
childCount: booking.activity.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: 200)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Activity extends StatelessWidget {
|
||||||
|
const _Activity({
|
||||||
|
required this.activity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Activity activity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: Dimens.paddingVertical,
|
||||||
|
left: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
right: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: activity.imageUrl,
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
activity.timeOfDay.name.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
activity.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
activity.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../core/localization/applocalization.dart';
|
||||||
|
import '../../core/themes/colors.dart';
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../../core/ui/back_button.dart';
|
||||||
|
import '../../core/ui/date_format_start_end.dart';
|
||||||
|
import '../../core/ui/home_button.dart';
|
||||||
|
import '../../core/ui/tag_chip.dart';
|
||||||
|
|
||||||
|
class BookingHeader extends StatelessWidget {
|
||||||
|
const BookingHeader({
|
||||||
|
super.key,
|
||||||
|
required this.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Booking booking;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_Top(booking: booking),
|
||||||
|
Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||||
|
child: Text(
|
||||||
|
booking.destination.knownFor,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Dimens.paddingVertical),
|
||||||
|
_Tags(booking: booking),
|
||||||
|
const SizedBox(height: Dimens.paddingVertical),
|
||||||
|
Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||||
|
child: Text(
|
||||||
|
AppLocalization.of(context).yourChosenActivities,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Top extends StatelessWidget {
|
||||||
|
const _Top({
|
||||||
|
required this.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Booking booking;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 260,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
_HeaderImage(booking: booking),
|
||||||
|
const _Gradient(),
|
||||||
|
_Headline(booking: booking),
|
||||||
|
Positioned(
|
||||||
|
left: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
top: Dimens.of(context).paddingScreenVertical,
|
||||||
|
child: SafeArea(
|
||||||
|
top: true,
|
||||||
|
child: CustomBackButton(
|
||||||
|
onTap: () => context.go('/activities'),
|
||||||
|
blur: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
top: Dimens.of(context).paddingScreenVertical,
|
||||||
|
child: const SafeArea(
|
||||||
|
top: true,
|
||||||
|
child: HomeButton(blur: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tags extends StatelessWidget {
|
||||||
|
const _Tags({
|
||||||
|
required this.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Booking booking;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
final chipColor = switch (brightness) {
|
||||||
|
Brightness.dark => AppColors.whiteTransparent,
|
||||||
|
Brightness.light => AppColors.blackTransparent,
|
||||||
|
};
|
||||||
|
return Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: booking.destination.tags
|
||||||
|
.map(
|
||||||
|
(tag) => TagChip(
|
||||||
|
tag: tag,
|
||||||
|
fontSize: 16,
|
||||||
|
height: 32,
|
||||||
|
chipColor: chipColor,
|
||||||
|
onChipColor: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Headline extends StatelessWidget {
|
||||||
|
const _Headline({
|
||||||
|
required this.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Booking booking;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
|
child: Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
booking.destination.name,
|
||||||
|
style: Theme.of(context).textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
dateFormatStartEnd(
|
||||||
|
DateTimeRange(
|
||||||
|
start: booking.startDate,
|
||||||
|
end: booking.endDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeaderImage extends StatelessWidget {
|
||||||
|
const _HeaderImage({
|
||||||
|
required this.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Booking booking;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
imageUrl: booking.destination.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Gradient extends StatelessWidget {
|
||||||
|
const _Gradient();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../core/localization/applocalization.dart';
|
||||||
|
import '../../core/ui/error_indicator.dart';
|
||||||
|
import '../view_models/booking_viewmodel.dart';
|
||||||
|
import 'booking_body.dart';
|
||||||
|
import 'booking_share_button.dart';
|
||||||
|
|
||||||
|
class BookingScreen extends StatelessWidget {
|
||||||
|
const BookingScreen({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BookingViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (d, r) => context.go('/activities'),
|
||||||
|
child: Scaffold(
|
||||||
|
body: ListenableBuilder(
|
||||||
|
listenable: viewModel.loadBooking,
|
||||||
|
builder: (context, child) {
|
||||||
|
if (viewModel.loadBooking.running) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (viewModel.loadBooking.error) {
|
||||||
|
return Center(
|
||||||
|
child: ErrorIndicator(
|
||||||
|
title: AppLocalization.of(context).errorWhileLoadingBooking,
|
||||||
|
label: AppLocalization.of(context).tryAgain,
|
||||||
|
onPressed: viewModel.loadBooking.execute,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
BookingBody(viewModel: viewModel),
|
||||||
|
BookingShareButton(viewModel: viewModel),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/localization/applocalization.dart';
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../../core/ui/blur_filter.dart';
|
||||||
|
import '../view_models/booking_viewmodel.dart';
|
||||||
|
|
||||||
|
class BookingShareButton extends StatelessWidget {
|
||||||
|
const BookingShareButton({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BookingViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: true,
|
||||||
|
top: false,
|
||||||
|
child: ClipRect(
|
||||||
|
child: SizedBox(
|
||||||
|
height: (Dimens.of(context).paddingScreenVertical * 2) + 64,
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: kBlurFilter,
|
||||||
|
child: Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
builder: (context, _) {
|
||||||
|
return FilledButton(
|
||||||
|
key: const Key('share-button'),
|
||||||
|
onPressed: viewModel.booking != null
|
||||||
|
? viewModel.shareBooking.execute
|
||||||
|
: null,
|
||||||
|
child: Text(AppLocalization.of(context).shareTrip),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
final kBlurFilter = ImageFilter.blur(sigmaX: 2, sigmaY: 2);
|
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:compass_app/ui/booking/components/booking_create_component.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||||
|
import '../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||||
|
import '../../../testing/models/activity.dart';
|
||||||
|
import '../../../testing/models/booking.dart';
|
||||||
|
import '../../../testing/models/destination.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('BookingCreateComponent tests', () {
|
||||||
|
test('Create booking', () async {
|
||||||
|
final component = BookingCreateComponent(
|
||||||
|
activityRepository: FakeActivityRepository(),
|
||||||
|
destinationRepository: FakeDestinationRepository(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final booking = await component.createFrom(
|
||||||
|
ItineraryConfig(
|
||||||
|
startDate: DateTime(2024, 01, 01),
|
||||||
|
endDate: DateTime(2024, 02, 12),
|
||||||
|
destination: kDestination1.ref,
|
||||||
|
activities: [kActivity.ref],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(booking.asOk.value, kBooking);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:compass_app/ui/booking/components/booking_create_component.dart';
|
||||||
|
import 'package:compass_app/ui/booking/components/booking_share_component.dart';
|
||||||
|
import 'package:compass_app/ui/booking/view_models/booking_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/booking/widgets/booking_screen.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../testing/app.dart';
|
||||||
|
import '../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||||
|
import '../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||||
|
import '../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||||
|
import '../../../testing/mocks.dart';
|
||||||
|
import '../../../testing/models/activity.dart';
|
||||||
|
import '../../../testing/models/destination.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('BookingScreen widget tests', () {
|
||||||
|
late MockGoRouter goRouter;
|
||||||
|
late BookingViewModel viewModel;
|
||||||
|
late bool shared;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
shared = false;
|
||||||
|
viewModel = BookingViewModel(
|
||||||
|
itineraryConfigRepository: FakeItineraryConfigRepository(
|
||||||
|
itineraryConfig: ItineraryConfig(
|
||||||
|
continent: 'Europe',
|
||||||
|
startDate: DateTime(2024, 01, 01),
|
||||||
|
endDate: DateTime(2024, 01, 31),
|
||||||
|
guests: 2,
|
||||||
|
destination: kDestination1.ref,
|
||||||
|
activities: [kActivity.ref],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bookingComponent: BookingCreateComponent(
|
||||||
|
activityRepository: FakeActivityRepository(),
|
||||||
|
destinationRepository: FakeDestinationRepository(),
|
||||||
|
),
|
||||||
|
shareComponent: BookingShareComponent.custom((text) async {
|
||||||
|
shared = true;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
goRouter = MockGoRouter();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> loadScreen(WidgetTester tester) async {
|
||||||
|
await testApp(
|
||||||
|
tester,
|
||||||
|
BookingScreen(viewModel: viewModel),
|
||||||
|
goRouter: goRouter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should load screen', (WidgetTester tester) async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
expect(find.byType(BookingScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display booking', (WidgetTester tester) async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
|
||||||
|
// Wait for list to load
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('name1'), findsOneWidget);
|
||||||
|
expect(find.text('tags1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should share booking', (WidgetTester tester) async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('share-button')));
|
||||||
|
expect(shared, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:compass_app/ui/booking/components/booking_share_component.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../testing/models/activity.dart';
|
||||||
|
import '../../../testing/models/destination.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('BookingShareComponent tests', () {
|
||||||
|
test('Share booking', () async {
|
||||||
|
String? sharedText;
|
||||||
|
final component = BookingShareComponent.custom((text) async {
|
||||||
|
sharedText = text;
|
||||||
|
});
|
||||||
|
final booking = Booking(
|
||||||
|
startDate: DateTime(2024, 01, 01),
|
||||||
|
endDate: DateTime(2024, 02, 12),
|
||||||
|
destination: kDestination1,
|
||||||
|
activity: [kActivity],
|
||||||
|
);
|
||||||
|
await component.shareBooking(booking);
|
||||||
|
expect(
|
||||||
|
sharedText,
|
||||||
|
'Trip to name1\n'
|
||||||
|
'on 1 Jan - 12 Feb\n'
|
||||||
|
'Activities:\n'
|
||||||
|
' - NAME.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
import 'package:compass_app/data/repositories/activity/activity_repository.dart';
|
|
||||||
import 'package:compass_app/utils/result.dart';
|
|
||||||
import 'package:compass_model/src/model/activity/activity.dart';
|
|
||||||
|
|
||||||
class FakeActivityRepository implements ActivityRepository {
|
|
||||||
Map<String, List<Activity>> activities = {
|
|
||||||
"DESTINATION": [
|
|
||||||
const Activity(
|
|
||||||
description: 'DESCRIPTION',
|
|
||||||
destinationRef: 'DESTINATION',
|
|
||||||
duration: 3,
|
|
||||||
familyFriendly: true,
|
|
||||||
imageUrl: 'http://example.com/img.png',
|
|
||||||
locationName: 'LOCATION NAME',
|
|
||||||
name: 'NAME',
|
|
||||||
price: 3,
|
|
||||||
ref: 'REF',
|
|
||||||
timeOfDay: TimeOfDay.afternoon,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Result<List<Activity>>> getByDestination(String ref) async {
|
|
||||||
return Result.ok(activities[ref]!);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import 'package:compass_model/model.dart';
|
|
||||||
import 'package:compass_app/data/repositories/destination/destination_repository.dart';
|
|
||||||
import 'package:compass_app/utils/result.dart';
|
|
||||||
|
|
||||||
// TODO: Move to a better place
|
|
||||||
class FakeDestinationRepository implements DestinationRepository {
|
|
||||||
@override
|
|
||||||
Future<Result<List<Destination>>> getDestinations() {
|
|
||||||
return Future.value(
|
|
||||||
Result.ok(
|
|
||||||
[
|
|
||||||
const Destination(
|
|
||||||
ref: 'ref1',
|
|
||||||
name: 'name1',
|
|
||||||
country: 'country1',
|
|
||||||
continent: 'Europe',
|
|
||||||
knownFor: 'knownFor1',
|
|
||||||
tags: ['tags1'],
|
|
||||||
imageUrl: 'imageUrl1',
|
|
||||||
),
|
|
||||||
const Destination(
|
|
||||||
ref: 'ref2',
|
|
||||||
name: 'name2',
|
|
||||||
country: 'country2',
|
|
||||||
continent: 'Europe',
|
|
||||||
knownFor: 'knownFor2',
|
|
||||||
tags: ['tags2'],
|
|
||||||
imageUrl: 'imageUrl2',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:compass_app/ui/core/localization/applocalization.dart';
|
||||||
|
import 'package:compass_app/ui/core/themes/theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||||
|
|
||||||
|
import 'mocks.dart';
|
||||||
|
|
||||||
|
testApp(
|
||||||
|
WidgetTester tester,
|
||||||
|
Widget body, {
|
||||||
|
GoRouter? goRouter,
|
||||||
|
}) async {
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
await tester.binding.setSurfaceSize(const Size(1200, 800));
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: [
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
AppLocalizationDelegate(),
|
||||||
|
],
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
home: InheritedGoRouter(
|
||||||
|
goRouter: goRouter ?? MockGoRouter(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:compass_app/data/repositories/activity/activity_repository.dart';
|
||||||
|
import 'package:compass_app/utils/result.dart';
|
||||||
|
import 'package:compass_model/src/model/activity/activity.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../../models/activity.dart';
|
||||||
|
import '../../models/destination.dart';
|
||||||
|
|
||||||
|
class FakeActivityRepository implements ActivityRepository {
|
||||||
|
Map<String, List<Activity>> activities = {
|
||||||
|
"DESTINATION": [kActivity],
|
||||||
|
kDestination1.ref: [kActivity],
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<List<Activity>>> getByDestination(String ref) {
|
||||||
|
return SynchronousFuture(Result.ok(activities[ref]!));
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
import 'package:compass_model/model.dart';
|
import 'package:compass_model/model.dart';
|
||||||
import 'package:compass_app/data/repositories/continent/continent_repository.dart';
|
import 'package:compass_app/data/repositories/continent/continent_repository.dart';
|
||||||
import 'package:compass_app/utils/result.dart';
|
import 'package:compass_app/utils/result.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class FakeContinentRepository implements ContinentRepository {
|
class FakeContinentRepository implements ContinentRepository {
|
||||||
@override
|
@override
|
||||||
Future<Result<List<Continent>>> getContinents() async {
|
Future<Result<List<Continent>>> getContinents() {
|
||||||
return Result.ok([
|
return SynchronousFuture(Result.ok([
|
||||||
const Continent(name: 'CONTINENT', imageUrl: 'URL'),
|
const Continent(name: 'CONTINENT', imageUrl: 'URL'),
|
||||||
const Continent(name: 'CONTINENT2', imageUrl: 'URL'),
|
const Continent(name: 'CONTINENT2', imageUrl: 'URL'),
|
||||||
const Continent(name: 'CONTINENT3', imageUrl: 'URL'),
|
const Continent(name: 'CONTINENT3', imageUrl: 'URL'),
|
||||||
]);
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:compass_app/data/repositories/destination/destination_repository.dart';
|
||||||
|
import 'package:compass_app/utils/result.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../../models/destination.dart';
|
||||||
|
|
||||||
|
class FakeDestinationRepository implements DestinationRepository {
|
||||||
|
@override
|
||||||
|
Future<Result<List<Destination>>> getDestinations() {
|
||||||
|
return SynchronousFuture(Result.ok([kDestination1, kDestination2]));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
|
||||||
|
const kActivity = Activity(
|
||||||
|
description: 'DESCRIPTION',
|
||||||
|
destinationRef: 'DESTINATION',
|
||||||
|
duration: 3,
|
||||||
|
familyFriendly: true,
|
||||||
|
imageUrl: 'http://example.com/img.png',
|
||||||
|
locationName: 'LOCATION NAME',
|
||||||
|
name: 'NAME',
|
||||||
|
price: 3,
|
||||||
|
ref: 'REF',
|
||||||
|
timeOfDay: TimeOfDay.afternoon,
|
||||||
|
);
|
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
|
||||||
|
import 'activity.dart';
|
||||||
|
import 'destination.dart';
|
||||||
|
|
||||||
|
final kBooking = Booking(
|
||||||
|
startDate: DateTime(2024, 01, 01),
|
||||||
|
endDate: DateTime(2024, 02, 12),
|
||||||
|
destination: kDestination1,
|
||||||
|
activity: [kActivity],
|
||||||
|
);
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
|
||||||
|
const kDestination1 = Destination(
|
||||||
|
ref: 'ref1',
|
||||||
|
name: 'name1',
|
||||||
|
country: 'country1',
|
||||||
|
continent: 'Europe',
|
||||||
|
knownFor: 'knownFor1',
|
||||||
|
tags: ['tags1'],
|
||||||
|
imageUrl: 'imageUrl1',
|
||||||
|
);
|
||||||
|
|
||||||
|
const kDestination2 = Destination(
|
||||||
|
ref: 'ref2',
|
||||||
|
name: 'name2',
|
||||||
|
country: 'country2',
|
||||||
|
continent: 'Europe',
|
||||||
|
knownFor: 'knownFor2',
|
||||||
|
tags: ['tags2'],
|
||||||
|
imageUrl: 'imageUrl2',
|
||||||
|
);
|
@ -1,6 +1,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/model/activity/activity.dart';
|
export 'src/model/activity/activity.dart';
|
||||||
|
export 'src/model/booking/booking.dart';
|
||||||
export 'src/model/continent/continent.dart';
|
export 'src/model/continent/continent.dart';
|
||||||
export 'src/model/destination/destination.dart';
|
export 'src/model/destination/destination.dart';
|
||||||
export 'src/model/itinerary_config/itinerary_config.dart';
|
export 'src/model/itinerary_config/itinerary_config.dart';
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'booking.freezed.dart';
|
||||||
|
part 'booking.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class Booking with _$Booking {
|
||||||
|
const factory Booking({
|
||||||
|
/// Start date of the trip
|
||||||
|
required DateTime startDate,
|
||||||
|
|
||||||
|
/// End date of the trip
|
||||||
|
required DateTime endDate,
|
||||||
|
|
||||||
|
/// Destination of the trip
|
||||||
|
required Destination destination,
|
||||||
|
|
||||||
|
/// List of chosen activities
|
||||||
|
required List<Activity> activity,
|
||||||
|
}) = _Booking;
|
||||||
|
|
||||||
|
factory Booking.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$BookingFromJson(json);
|
||||||
|
}
|
@ -0,0 +1,271 @@
|
|||||||
|
// 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 'booking.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');
|
||||||
|
|
||||||
|
Booking _$BookingFromJson(Map<String, dynamic> json) {
|
||||||
|
return _Booking.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Booking {
|
||||||
|
/// Start date of the trip
|
||||||
|
DateTime get startDate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// End date of the trip
|
||||||
|
DateTime get endDate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Destination of the trip
|
||||||
|
Destination get destination => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// List of chosen activities
|
||||||
|
List<Activity> get activity => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this Booking to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$BookingCopyWith<Booking> get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $BookingCopyWith<$Res> {
|
||||||
|
factory $BookingCopyWith(Booking value, $Res Function(Booking) then) =
|
||||||
|
_$BookingCopyWithImpl<$Res, Booking>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
Destination destination,
|
||||||
|
List<Activity> activity});
|
||||||
|
|
||||||
|
$DestinationCopyWith<$Res> get destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$BookingCopyWithImpl<$Res, $Val extends Booking>
|
||||||
|
implements $BookingCopyWith<$Res> {
|
||||||
|
_$BookingCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? startDate = null,
|
||||||
|
Object? endDate = null,
|
||||||
|
Object? destination = null,
|
||||||
|
Object? activity = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
startDate: null == startDate
|
||||||
|
? _value.startDate
|
||||||
|
: startDate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
endDate: null == endDate
|
||||||
|
? _value.endDate
|
||||||
|
: endDate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
destination: null == destination
|
||||||
|
? _value.destination
|
||||||
|
: destination // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Destination,
|
||||||
|
activity: null == activity
|
||||||
|
? _value.activity
|
||||||
|
: activity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<Activity>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$DestinationCopyWith<$Res> get destination {
|
||||||
|
return $DestinationCopyWith<$Res>(_value.destination, (value) {
|
||||||
|
return _then(_value.copyWith(destination: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$BookingImplCopyWith<$Res> implements $BookingCopyWith<$Res> {
|
||||||
|
factory _$$BookingImplCopyWith(
|
||||||
|
_$BookingImpl value, $Res Function(_$BookingImpl) then) =
|
||||||
|
__$$BookingImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
Destination destination,
|
||||||
|
List<Activity> activity});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$DestinationCopyWith<$Res> get destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$BookingImplCopyWithImpl<$Res>
|
||||||
|
extends _$BookingCopyWithImpl<$Res, _$BookingImpl>
|
||||||
|
implements _$$BookingImplCopyWith<$Res> {
|
||||||
|
__$$BookingImplCopyWithImpl(
|
||||||
|
_$BookingImpl _value, $Res Function(_$BookingImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? startDate = null,
|
||||||
|
Object? endDate = null,
|
||||||
|
Object? destination = null,
|
||||||
|
Object? activity = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$BookingImpl(
|
||||||
|
startDate: null == startDate
|
||||||
|
? _value.startDate
|
||||||
|
: startDate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
endDate: null == endDate
|
||||||
|
? _value.endDate
|
||||||
|
: endDate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
destination: null == destination
|
||||||
|
? _value.destination
|
||||||
|
: destination // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Destination,
|
||||||
|
activity: null == activity
|
||||||
|
? _value._activity
|
||||||
|
: activity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<Activity>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$BookingImpl implements _Booking {
|
||||||
|
const _$BookingImpl(
|
||||||
|
{required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
required this.destination,
|
||||||
|
required final List<Activity> activity})
|
||||||
|
: _activity = activity;
|
||||||
|
|
||||||
|
factory _$BookingImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$BookingImplFromJson(json);
|
||||||
|
|
||||||
|
/// Start date of the trip
|
||||||
|
@override
|
||||||
|
final DateTime startDate;
|
||||||
|
|
||||||
|
/// End date of the trip
|
||||||
|
@override
|
||||||
|
final DateTime endDate;
|
||||||
|
|
||||||
|
/// Destination of the trip
|
||||||
|
@override
|
||||||
|
final Destination destination;
|
||||||
|
|
||||||
|
/// List of chosen activities
|
||||||
|
final List<Activity> _activity;
|
||||||
|
|
||||||
|
/// List of chosen activities
|
||||||
|
@override
|
||||||
|
List<Activity> get activity {
|
||||||
|
if (_activity is EqualUnmodifiableListView) return _activity;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Booking(startDate: $startDate, endDate: $endDate, destination: $destination, activity: $activity)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$BookingImpl &&
|
||||||
|
(identical(other.startDate, startDate) ||
|
||||||
|
other.startDate == startDate) &&
|
||||||
|
(identical(other.endDate, endDate) || other.endDate == endDate) &&
|
||||||
|
(identical(other.destination, destination) ||
|
||||||
|
other.destination == destination) &&
|
||||||
|
const DeepCollectionEquality().equals(other._activity, _activity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, startDate, endDate, destination,
|
||||||
|
const DeepCollectionEquality().hash(_activity));
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$BookingImplCopyWith<_$BookingImpl> get copyWith =>
|
||||||
|
__$$BookingImplCopyWithImpl<_$BookingImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$BookingImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Booking implements Booking {
|
||||||
|
const factory _Booking(
|
||||||
|
{required final DateTime startDate,
|
||||||
|
required final DateTime endDate,
|
||||||
|
required final Destination destination,
|
||||||
|
required final List<Activity> activity}) = _$BookingImpl;
|
||||||
|
|
||||||
|
factory _Booking.fromJson(Map<String, dynamic> json) = _$BookingImpl.fromJson;
|
||||||
|
|
||||||
|
/// Start date of the trip
|
||||||
|
@override
|
||||||
|
DateTime get startDate;
|
||||||
|
|
||||||
|
/// End date of the trip
|
||||||
|
@override
|
||||||
|
DateTime get endDate;
|
||||||
|
|
||||||
|
/// Destination of the trip
|
||||||
|
@override
|
||||||
|
Destination get destination;
|
||||||
|
|
||||||
|
/// List of chosen activities
|
||||||
|
@override
|
||||||
|
List<Activity> get activity;
|
||||||
|
|
||||||
|
/// Create a copy of Booking
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$BookingImplCopyWith<_$BookingImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'booking.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$BookingImpl(
|
||||||
|
startDate: DateTime.parse(json['startDate'] as String),
|
||||||
|
endDate: DateTime.parse(json['endDate'] as String),
|
||||||
|
destination:
|
||||||
|
Destination.fromJson(json['destination'] as Map<String, dynamic>),
|
||||||
|
activity: (json['activity'] as List<dynamic>)
|
||||||
|
.map((e) => Activity.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'startDate': instance.startDate.toIso8601String(),
|
||||||
|
'endDate': instance.endDate.toIso8601String(),
|
||||||
|
'destination': instance.destination,
|
||||||
|
'activity': instance.activity,
|
||||||
|
};
|
Loading…
Reference in new issue